[SDK] Agent in manifest (#18431)
# Introduction Adding agent in the manifest, required for twenty standard app extraction out of twenty-server
This commit is contained in:
@@ -113,6 +113,7 @@ export class CreateAppCommand {
|
||||
includeExampleView: false,
|
||||
includeExampleNavigationMenuItem: false,
|
||||
includeExampleSkill: false,
|
||||
includeExampleAgent: false,
|
||||
includeExampleIntegrationTest: false,
|
||||
};
|
||||
}
|
||||
@@ -126,6 +127,7 @@ export class CreateAppCommand {
|
||||
includeExampleNavigationMenuItem: true,
|
||||
includeExampleSkill: true,
|
||||
includeExampleIntegrationTest: true,
|
||||
includeExampleAgent: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,5 +8,6 @@ export type ExampleOptions = {
|
||||
includeExampleView: boolean;
|
||||
includeExampleNavigationMenuItem: boolean;
|
||||
includeExampleSkill: boolean;
|
||||
includeExampleAgent: boolean;
|
||||
includeExampleIntegrationTest: boolean;
|
||||
};
|
||||
|
||||
@@ -25,6 +25,7 @@ const ALL_EXAMPLES: ExampleOptions = {
|
||||
includeExampleView: true,
|
||||
includeExampleNavigationMenuItem: true,
|
||||
includeExampleSkill: true,
|
||||
includeExampleAgent: true,
|
||||
includeExampleIntegrationTest: true,
|
||||
};
|
||||
|
||||
@@ -41,6 +42,7 @@ const NO_EXAMPLES: ExampleOptions = {
|
||||
includeExampleObject: false,
|
||||
includeExampleField: false,
|
||||
includeExampleSkill: false,
|
||||
includeExampleAgent: false,
|
||||
includeExampleLogicFunction: false,
|
||||
includeExampleFrontComponent: false,
|
||||
includeExampleView: false,
|
||||
@@ -476,6 +478,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
includeExampleObject: false,
|
||||
includeExampleField: false,
|
||||
includeExampleSkill: false,
|
||||
includeExampleAgent: false,
|
||||
includeExampleLogicFunction: false,
|
||||
includeExampleFrontComponent: true,
|
||||
includeExampleView: false,
|
||||
@@ -513,6 +516,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
exampleOptions: {
|
||||
includeExampleObject: false,
|
||||
includeExampleSkill: false,
|
||||
includeExampleAgent: false,
|
||||
includeExampleField: false,
|
||||
includeExampleLogicFunction: true,
|
||||
includeExampleFrontComponent: false,
|
||||
|
||||
@@ -103,6 +103,14 @@ export const copyBaseApplicationProject = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (exampleOptions.includeExampleAgent) {
|
||||
await createExampleAgent({
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: 'agents',
|
||||
fileName: 'example-agent.ts',
|
||||
});
|
||||
}
|
||||
|
||||
if (exampleOptions.includeExampleIntegrationTest) {
|
||||
await scaffoldIntegrationTest({
|
||||
appDirectory,
|
||||
@@ -519,6 +527,36 @@ export default defineSkill({
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
};
|
||||
|
||||
const createExampleAgent = async ({
|
||||
appDirectory,
|
||||
fileFolder,
|
||||
fileName,
|
||||
}: {
|
||||
appDirectory: string;
|
||||
fileFolder?: string;
|
||||
fileName: string;
|
||||
}) => {
|
||||
const universalIdentifier = v4();
|
||||
|
||||
const content = `import { defineAgent } from 'twenty-sdk';
|
||||
|
||||
export const EXAMPLE_AGENT_UNIVERSAL_IDENTIFIER =
|
||||
'${universalIdentifier}';
|
||||
|
||||
export default defineAgent({
|
||||
universalIdentifier: EXAMPLE_AGENT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'example-agent',
|
||||
label: 'Example Agent',
|
||||
description: 'A sample AI agent for your application',
|
||||
icon: 'IconRobot',
|
||||
prompt: 'You are a helpful assistant. Help users with their questions and tasks.',
|
||||
});
|
||||
`;
|
||||
|
||||
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
};
|
||||
|
||||
const createApplicationConfig = async ({
|
||||
displayName,
|
||||
description,
|
||||
|
||||
@@ -14,7 +14,7 @@ Apps let you build and manage Twenty customizations **as code**. Instead of conf
|
||||
**What you can do today:**
|
||||
- Define custom objects and fields as code (managed data model)
|
||||
- Build logic functions with custom triggers
|
||||
- Define skills for AI agents
|
||||
- Define skills and agents for AI
|
||||
- Deploy the same app across multiple workspaces
|
||||
|
||||
## Prerequisites
|
||||
@@ -38,7 +38,7 @@ yarn twenty app:dev
|
||||
The scaffolder supports two modes for controlling which example files are included:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill)
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimal: only core files (application-config.ts and default-role.ts)
|
||||
@@ -114,8 +114,10 @@ my-twenty-app/
|
||||
│ └── example-view.ts # Example saved view definition
|
||||
├── navigation-menu-items/
|
||||
│ └── example-navigation-menu-item.ts # Example sidebar navigation link
|
||||
└── skills/
|
||||
└── example-skill.ts # Example AI agent skill definition
|
||||
├── skills/
|
||||
│ └── example-skill.ts # Example AI agent skill definition
|
||||
└── agents/
|
||||
└── example-agent.ts # Example AI agent definition
|
||||
```
|
||||
|
||||
With `--minimal`, only the core files are created (`application-config.ts`, `roles/default-role.ts`, `logic-functions/pre-install.ts`, and `logic-functions/post-install.ts`).
|
||||
@@ -147,6 +149,7 @@ The SDK detects entities by parsing your TypeScript files for **`export default
|
||||
| `defineView()` | Saved view definitions |
|
||||
| `defineNavigationMenuItem()` | Navigation menu item definitions |
|
||||
| `defineSkill()` | AI agent skill definitions |
|
||||
| `defineAgent()` | AI agent definitions |
|
||||
|
||||
<Note>
|
||||
**File naming is flexible.** Entity detection is AST-based — the SDK scans your source files for the `export default define<Entity>({...})` pattern. You can organize your files and folders however you like. Grouping by entity type (e.g., `logic-functions/`, `roles/`) is just a convention for code organization, not a requirement.
|
||||
@@ -223,6 +226,7 @@ The SDK provides helper functions for defining your app entities. As described i
|
||||
| `defineView()` | Define saved views for objects |
|
||||
| `defineNavigationMenuItem()` | Define sidebar navigation links |
|
||||
| `defineSkill()` | Define AI agent skills |
|
||||
| `defineAgent()` | Define AI agents with system prompts |
|
||||
|
||||
These functions validate your configuration at build time and provide IDE autocompletion and type safety.
|
||||
|
||||
@@ -790,6 +794,40 @@ You can create new skills in two ways:
|
||||
- **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new skill.
|
||||
- **Manual**: Create a new file and use `defineSkill()`, following the same pattern.
|
||||
|
||||
### Agents
|
||||
|
||||
Agents define AI agents with system prompts that can operate within your workspace. Use `defineAgent()` to define agents with built-in validation:
|
||||
|
||||
```typescript
|
||||
// src/agents/example-agent.ts
|
||||
import { defineAgent } from 'twenty-sdk';
|
||||
|
||||
export default defineAgent({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'sales-assistant',
|
||||
label: 'Sales Assistant',
|
||||
description: 'An AI agent that helps with sales tasks',
|
||||
icon: 'IconRobot',
|
||||
prompt: `You are a sales assistant. Help users with:
|
||||
1. Researching prospects and companies
|
||||
2. Drafting personalized outreach messages
|
||||
3. Tracking follow-ups and next steps
|
||||
4. Analyzing deal pipeline and suggesting actions`,
|
||||
});
|
||||
```
|
||||
|
||||
Key points:
|
||||
- `name` is a unique identifier string for the agent (kebab-case recommended).
|
||||
- `label` is the human-readable display name shown in the UI.
|
||||
- `prompt` contains the system prompt — this is the instruction text that defines the agent's behavior.
|
||||
- `icon` (optional) sets the icon displayed in the UI.
|
||||
- `description` (optional) provides additional context about the agent's purpose.
|
||||
|
||||
You can create new agents in two ways:
|
||||
|
||||
- **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new agent.
|
||||
- **Manual**: Create a new file and use `defineAgent()`, following the same pattern.
|
||||
|
||||
### Generated typed clients
|
||||
|
||||
Two typed clients are auto-generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated` based on your workspace schema:
|
||||
|
||||
+1
@@ -19,6 +19,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
},
|
||||
],
|
||||
skills: [],
|
||||
agents: [],
|
||||
application: {
|
||||
applicationVariables: {
|
||||
DEFAULT_RECIPIENT_NAME: {
|
||||
|
||||
+1
@@ -18,6 +18,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
apiClientChecksum: null,
|
||||
},
|
||||
skills: [],
|
||||
agents: [],
|
||||
publicAssets: [],
|
||||
fields: [
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@ import { getNavigationMenuItemBaseFile } from '@/cli/utilities/entity/entity-nav
|
||||
import { getObjectBaseFile } from '@/cli/utilities/entity/entity-object-template';
|
||||
import { getPageLayoutBaseFile } from '@/cli/utilities/entity/entity-page-layout-template';
|
||||
import { getRoleBaseFile } from '@/cli/utilities/entity/entity-role-template';
|
||||
import { getAgentBaseFile } from '@/cli/utilities/entity/entity-agent-template';
|
||||
import { getSkillBaseFile } from '@/cli/utilities/entity/entity-skill-template';
|
||||
import { getViewBaseFile } from '@/cli/utilities/entity/entity-view-template';
|
||||
import { ensureDir, pathExists } from '@/cli/utilities/file/fs-utils';
|
||||
@@ -144,6 +145,16 @@ export class EntityAddCommand {
|
||||
return { name, file };
|
||||
}
|
||||
|
||||
case SyncableEntity.Agent: {
|
||||
const name = await this.getEntityName(entity);
|
||||
|
||||
const file = getAgentBaseFile({
|
||||
name,
|
||||
});
|
||||
|
||||
return { name, file };
|
||||
}
|
||||
|
||||
case SyncableEntity.View: {
|
||||
const entityData = await this.getViewData();
|
||||
|
||||
|
||||
+1
@@ -33,6 +33,7 @@ const validManifest: Manifest = {
|
||||
logicFunctions: [],
|
||||
roles: [],
|
||||
skills: [],
|
||||
agents: [],
|
||||
publicAssets: [],
|
||||
views: [],
|
||||
navigationMenuItems: [],
|
||||
|
||||
@@ -19,6 +19,7 @@ import { readFile } from 'node:fs/promises';
|
||||
import { basename, extname, relative } from 'path';
|
||||
import { glob } from 'tinyglobby';
|
||||
import {
|
||||
type AgentManifest,
|
||||
type ApplicationManifest,
|
||||
type AssetManifest,
|
||||
ASSETS_DIR,
|
||||
@@ -68,6 +69,7 @@ export const buildManifest = async (
|
||||
const fields: FieldManifest[] = [];
|
||||
const roles: RoleManifest[] = [];
|
||||
const skills: SkillManifest[] = [];
|
||||
const agents: AgentManifest[] = [];
|
||||
const logicFunctions: LogicFunctionManifest[] = [];
|
||||
const frontComponents: FrontComponentManifest[] = [];
|
||||
const publicAssets: AssetManifest[] = [];
|
||||
@@ -82,6 +84,7 @@ export const buildManifest = async (
|
||||
const fieldsFilePaths: string[] = [];
|
||||
const rolesFilePaths: string[] = [];
|
||||
const skillsFilePaths: string[] = [];
|
||||
const agentsFilePaths: string[] = [];
|
||||
const logicFunctionsFilePaths: string[] = [];
|
||||
const frontComponentsFilePaths: string[] = [];
|
||||
const publicAssetsFilePaths: string[] = [];
|
||||
@@ -184,6 +187,16 @@ export const buildManifest = async (
|
||||
skillsFilePaths.push(relativePath);
|
||||
break;
|
||||
}
|
||||
case ManifestEntityKey.Agents: {
|
||||
const extract = await extractManifestFromFile<AgentManifest>({
|
||||
appPath,
|
||||
filePath,
|
||||
});
|
||||
agents.push(extract.config);
|
||||
errors.push(...extract.errors);
|
||||
agentsFilePaths.push(relativePath);
|
||||
break;
|
||||
}
|
||||
case ManifestEntityKey.LogicFunctions: {
|
||||
const extract = await extractManifestFromFile<LogicFunctionConfig>({
|
||||
appPath,
|
||||
@@ -363,6 +376,7 @@ export const buildManifest = async (
|
||||
fields,
|
||||
roles,
|
||||
skills,
|
||||
agents,
|
||||
logicFunctions,
|
||||
frontComponents,
|
||||
publicAssets,
|
||||
@@ -377,6 +391,7 @@ export const buildManifest = async (
|
||||
fields: fieldsFilePaths,
|
||||
roles: rolesFilePaths,
|
||||
skills: skillsFilePaths,
|
||||
agents: agentsFilePaths,
|
||||
logicFunctions: logicFunctionsFilePaths,
|
||||
frontComponents: frontComponentsFilePaths,
|
||||
publicAssets: publicAssetsFilePaths,
|
||||
|
||||
@@ -9,6 +9,7 @@ export enum TargetFunction {
|
||||
DefineObject = 'defineObject',
|
||||
DefineRole = 'defineRole',
|
||||
DefineSkill = 'defineSkill',
|
||||
DefineAgent = 'defineAgent',
|
||||
DefineFrontComponent = 'defineFrontComponent',
|
||||
DefineView = 'defineView',
|
||||
DefineNavigationMenuItem = 'defineNavigationMenuItem',
|
||||
@@ -22,6 +23,7 @@ export enum ManifestEntityKey {
|
||||
Objects = 'objects',
|
||||
Roles = 'roles',
|
||||
Skills = 'skills',
|
||||
Agents = 'agents',
|
||||
FrontComponents = 'frontComponents',
|
||||
PublicAssets = 'publicAssets',
|
||||
Views = 'views',
|
||||
@@ -45,6 +47,7 @@ export const TARGET_FUNCTION_TO_ENTITY_KEY_MAPPING: Record<
|
||||
[TargetFunction.DefineObject]: ManifestEntityKey.Objects,
|
||||
[TargetFunction.DefineRole]: ManifestEntityKey.Roles,
|
||||
[TargetFunction.DefineSkill]: ManifestEntityKey.Skills,
|
||||
[TargetFunction.DefineAgent]: ManifestEntityKey.Agents,
|
||||
[TargetFunction.DefineFrontComponent]: ManifestEntityKey.FrontComponents,
|
||||
[TargetFunction.DefineView]: ManifestEntityKey.Views,
|
||||
[TargetFunction.DefineNavigationMenuItem]:
|
||||
|
||||
@@ -102,6 +102,7 @@ export const ENTITY_LABELS: Record<SyncableEntity, string> = {
|
||||
[SyncableEntity.View]: 'Views',
|
||||
[SyncableEntity.NavigationMenuItem]: 'Navigation menu items',
|
||||
[SyncableEntity.PageLayout]: 'Page layouts',
|
||||
[SyncableEntity.Agent]: 'Agents',
|
||||
};
|
||||
|
||||
export const ENTITY_ORDER = Object.keys(ENTITY_LABELS) as SyncableEntity[];
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { kebabCase } from '@/cli/utilities/string/kebab-case';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export const getAgentBaseFile = ({
|
||||
name,
|
||||
universalIdentifier = v4(),
|
||||
}: {
|
||||
name: string;
|
||||
universalIdentifier?: string;
|
||||
}) => {
|
||||
const kebabCaseName = kebabCase(name);
|
||||
|
||||
return `import { defineAgent } from 'twenty-sdk';
|
||||
|
||||
export const ${kebabCaseName.toUpperCase().replace(/-/g, '_')}_AGENT_UNIVERSAL_IDENTIFIER =
|
||||
'${universalIdentifier}';
|
||||
|
||||
export default defineAgent({
|
||||
universalIdentifier: ${kebabCaseName.toUpperCase().replace(/-/g, '_')}_AGENT_UNIVERSAL_IDENTIFIER,
|
||||
name: '${kebabCaseName}',
|
||||
label: '${name}',
|
||||
description: 'Add a description for your agent',
|
||||
prompt: 'Add the agent system prompt here',
|
||||
});
|
||||
`;
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { type DefineEntity } from '@/sdk/common/types/define-entity.type';
|
||||
import { createValidationResult } from '@/sdk/common/utils/create-validation-result';
|
||||
import { type AgentManifest } from 'twenty-shared/application';
|
||||
|
||||
export const defineAgent: DefineEntity<AgentManifest> = (config) => {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!config.universalIdentifier) {
|
||||
errors.push('Agent must have a universalIdentifier');
|
||||
}
|
||||
|
||||
if (!config.name) {
|
||||
errors.push('Agent must have a name');
|
||||
}
|
||||
|
||||
if (!config.label) {
|
||||
errors.push('Agent must have a label');
|
||||
}
|
||||
|
||||
if (!config.prompt) {
|
||||
errors.push('Agent must have a prompt');
|
||||
}
|
||||
|
||||
return createValidationResult({ config, errors });
|
||||
};
|
||||
@@ -68,6 +68,7 @@ export { definePageLayout } from './page-layouts/define-page-layout';
|
||||
export type { PageLayoutConfig } from './page-layouts/page-layout-config';
|
||||
export { defineRole } from './roles/define-role';
|
||||
export { PermissionFlag } from './roles/permission-flag-type';
|
||||
export { defineAgent } from './agents/define-agent';
|
||||
export { defineSkill } from './skills/define-skill';
|
||||
export { defineView } from './views/define-view';
|
||||
export type { ViewConfig } from './views/view-config';
|
||||
|
||||
+13
-1
@@ -1,6 +1,5 @@
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { fromCommandMenuItemManifestToUniversalFlatCommandMenuItem } from 'src/engine/core-modules/application/application-manifest/converters/from-command-menu-item-manifest-to-universal-flat-command-menu-item.util';
|
||||
import { fromFieldManifestToUniversalFlatFieldMetadata } from 'src/engine/core-modules/application/application-manifest/converters/from-field-manifest-to-universal-flat-field-metadata.util';
|
||||
import { fromFrontComponentManifestToUniversalFlatFrontComponent } from 'src/engine/core-modules/application/application-manifest/converters/from-front-component-manifest-to-universal-flat-front-component.util';
|
||||
@@ -18,6 +17,8 @@ import { fromViewFilterGroupManifestToUniversalFlatViewFilterGroup } from 'src/e
|
||||
import { fromViewFilterManifestToUniversalFlatViewFilter } from 'src/engine/core-modules/application/application-manifest/converters/from-view-filter-manifest-to-universal-flat-view-filter.util';
|
||||
import { fromViewGroupManifestToUniversalFlatViewGroup } from 'src/engine/core-modules/application/application-manifest/converters/from-view-group-manifest-to-universal-flat-view-group.util';
|
||||
import { fromViewManifestToUniversalFlatView } from 'src/engine/core-modules/application/application-manifest/converters/from-view-manifest-to-universal-flat-view.util';
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { fromAgentManifestToUniversalFlatAgent } from 'src/engine/core-modules/application/utils/from-agent-manifest-to-universal-flat-agent.util';
|
||||
import { createEmptyAllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-all-flat-entity-maps.constant';
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/add-universal-flat-entity-to-universal-flat-entity-maps-through-mutation-or-throw.util';
|
||||
@@ -140,6 +141,17 @@ export const computeApplicationManifestAllUniversalFlatEntityMaps = ({
|
||||
});
|
||||
}
|
||||
|
||||
for (const agentManifest of manifest.agents ?? []) {
|
||||
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
|
||||
universalFlatEntity: fromAgentManifestToUniversalFlatAgent({
|
||||
agentManifest,
|
||||
applicationUniversalIdentifier,
|
||||
now,
|
||||
}),
|
||||
universalFlatEntityMapsToMutate: allUniversalFlatEntityMaps.flatAgentMaps,
|
||||
});
|
||||
}
|
||||
|
||||
for (const viewManifest of manifest.views ?? []) {
|
||||
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
|
||||
universalFlatEntity: fromViewManifestToUniversalFlatView({
|
||||
|
||||
+1
@@ -7,6 +7,7 @@ export const APPLICATION_MANIFEST_METADATA_NAMES = [
|
||||
'frontComponent',
|
||||
'role',
|
||||
'skill',
|
||||
'agent',
|
||||
'view',
|
||||
'viewField',
|
||||
'viewFieldGroup',
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { type AgentManifest } from 'twenty-shared/application';
|
||||
|
||||
import {
|
||||
DEFAULT_SMART_MODEL,
|
||||
type ModelId,
|
||||
} from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
|
||||
import { type UniversalFlatAgent } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-agent.type';
|
||||
|
||||
export const fromAgentManifestToUniversalFlatAgent = ({
|
||||
agentManifest,
|
||||
applicationUniversalIdentifier,
|
||||
now,
|
||||
}: {
|
||||
agentManifest: AgentManifest;
|
||||
applicationUniversalIdentifier: string;
|
||||
now: string;
|
||||
}): UniversalFlatAgent => {
|
||||
return {
|
||||
universalIdentifier: agentManifest.universalIdentifier,
|
||||
applicationUniversalIdentifier,
|
||||
name: agentManifest.name,
|
||||
label: agentManifest.label,
|
||||
icon: agentManifest.icon ?? null,
|
||||
description: agentManifest.description ?? null,
|
||||
prompt: agentManifest.prompt,
|
||||
modelId: (agentManifest.modelId as ModelId) ?? DEFAULT_SMART_MODEL,
|
||||
responseFormat: { type: 'text' },
|
||||
modelConfiguration: null,
|
||||
evaluationInputs: [],
|
||||
isCustom: false,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
};
|
||||
};
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util';
|
||||
import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util';
|
||||
import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util';
|
||||
import { uninstallApplication } from 'test/integration/metadata/suites/application/utils/uninstall-application.util';
|
||||
import { findAgents } from 'test/integration/metadata/suites/agent/utils/find-agents.util';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const TEST_APP_ID = uuidv4();
|
||||
const TEST_ROLE_ID = uuidv4();
|
||||
const TEST_AGENT_ID = uuidv4();
|
||||
const TEST_SECOND_AGENT_ID = uuidv4();
|
||||
|
||||
const AGENT_GQL_FIELDS =
|
||||
'id name label description prompt icon applicationId modelId';
|
||||
|
||||
const buildManifest = (overrides?: Partial<Pick<Manifest, 'agents'>>) =>
|
||||
buildBaseManifest({ appId: TEST_APP_ID, roleId: TEST_ROLE_ID, overrides });
|
||||
|
||||
const findAppAgents = async () => {
|
||||
const { data } = await findAgents({
|
||||
gqlFields: AGENT_GQL_FIELDS,
|
||||
expectToFail: false,
|
||||
input: undefined,
|
||||
});
|
||||
|
||||
return data.findManyAgents.filter(
|
||||
(agent) =>
|
||||
agent.name === 'sales-assistant' || agent.name === 'support-bot',
|
||||
);
|
||||
};
|
||||
|
||||
describe('Manifest update - agents', () => {
|
||||
beforeEach(async () => {
|
||||
await setupApplicationForSync({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
name: 'Test Application',
|
||||
description: 'App for testing agent manifest updates',
|
||||
sourcePath: 'test-manifest-update-agent',
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await uninstallApplication({
|
||||
universalIdentifier: TEST_APP_ID,
|
||||
expectToFail: false,
|
||||
});
|
||||
} catch {
|
||||
// May fail if the test didn't fully install/sync
|
||||
}
|
||||
|
||||
await globalThis.testDataSource.query(
|
||||
`DELETE FROM core."role" WHERE "universalIdentifier" = $1`,
|
||||
[TEST_ROLE_ID],
|
||||
);
|
||||
|
||||
await globalThis.testDataSource.query(
|
||||
`DELETE FROM core."file" WHERE "applicationId" IN (
|
||||
SELECT id FROM core."application" WHERE "universalIdentifier" = $1
|
||||
)`,
|
||||
[TEST_APP_ID],
|
||||
);
|
||||
|
||||
await globalThis.testDataSource.query(
|
||||
`DELETE FROM core."application"
|
||||
WHERE "universalIdentifier" = $1`,
|
||||
[TEST_APP_ID],
|
||||
);
|
||||
|
||||
await globalThis.testDataSource.query(
|
||||
`DELETE FROM core."applicationRegistration"
|
||||
WHERE "universalIdentifier" = $1`,
|
||||
[TEST_APP_ID],
|
||||
);
|
||||
});
|
||||
|
||||
it('should create a new agent when added to manifest on second sync', async () => {
|
||||
await syncApplication({
|
||||
manifest: buildManifest({ agents: [] }),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const agentsAfterFirstSync = await findAppAgents();
|
||||
|
||||
expect(agentsAfterFirstSync).toHaveLength(0);
|
||||
|
||||
await syncApplication({
|
||||
manifest: buildManifest({
|
||||
agents: [
|
||||
{
|
||||
universalIdentifier: TEST_AGENT_ID,
|
||||
name: 'sales-assistant',
|
||||
label: 'Sales Assistant',
|
||||
description: 'An AI agent that helps with sales tasks',
|
||||
icon: 'IconRobot',
|
||||
prompt:
|
||||
'You are a sales assistant. Help users with prospecting and outreach.',
|
||||
},
|
||||
],
|
||||
}),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const agentsAfterSecondSync = await findAppAgents();
|
||||
|
||||
expect(agentsAfterSecondSync).toHaveLength(1);
|
||||
expect(agentsAfterSecondSync[0]).toMatchObject({
|
||||
name: 'sales-assistant',
|
||||
label: 'Sales Assistant',
|
||||
description: 'An AI agent that helps with sales tasks',
|
||||
icon: 'IconRobot',
|
||||
prompt:
|
||||
'You are a sales assistant. Help users with prospecting and outreach.',
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
it('should update an agent when properties change in manifest on second sync', async () => {
|
||||
await syncApplication({
|
||||
manifest: buildManifest({
|
||||
agents: [
|
||||
{
|
||||
universalIdentifier: TEST_AGENT_ID,
|
||||
name: 'sales-assistant',
|
||||
label: 'Sales Assistant',
|
||||
description: 'An AI agent that helps with sales tasks',
|
||||
icon: 'IconRobot',
|
||||
prompt:
|
||||
'You are a sales assistant. Help users with prospecting and outreach.',
|
||||
},
|
||||
],
|
||||
}),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const agentsAfterFirstSync = await findAppAgents();
|
||||
|
||||
expect(agentsAfterFirstSync).toHaveLength(1);
|
||||
expect(agentsAfterFirstSync[0]).toMatchObject({
|
||||
label: 'Sales Assistant',
|
||||
description: 'An AI agent that helps with sales tasks',
|
||||
});
|
||||
|
||||
await syncApplication({
|
||||
manifest: buildManifest({
|
||||
agents: [
|
||||
{
|
||||
universalIdentifier: TEST_AGENT_ID,
|
||||
name: 'sales-assistant',
|
||||
label: 'Senior Sales Assistant',
|
||||
description: 'An advanced AI agent for enterprise sales',
|
||||
icon: 'IconRobot',
|
||||
prompt:
|
||||
'You are a senior sales assistant specializing in enterprise deals. Help users with complex prospecting, multi-threaded outreach, and deal strategy.',
|
||||
},
|
||||
],
|
||||
}),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const agentsAfterSecondSync = await findAppAgents();
|
||||
|
||||
expect(agentsAfterSecondSync).toHaveLength(1);
|
||||
expect(agentsAfterSecondSync[0]).toMatchObject({
|
||||
name: 'sales-assistant',
|
||||
label: 'Senior Sales Assistant',
|
||||
description: 'An advanced AI agent for enterprise sales',
|
||||
prompt:
|
||||
'You are a senior sales assistant specializing in enterprise deals. Help users with complex prospecting, multi-threaded outreach, and deal strategy.',
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
it('should delete an agent when removed from manifest on second sync', async () => {
|
||||
await syncApplication({
|
||||
manifest: buildManifest({
|
||||
agents: [
|
||||
{
|
||||
universalIdentifier: TEST_AGENT_ID,
|
||||
name: 'sales-assistant',
|
||||
label: 'Sales Assistant',
|
||||
description: 'An AI agent that helps with sales tasks',
|
||||
icon: 'IconRobot',
|
||||
prompt:
|
||||
'You are a sales assistant. Help users with prospecting and outreach.',
|
||||
},
|
||||
{
|
||||
universalIdentifier: TEST_SECOND_AGENT_ID,
|
||||
name: 'support-bot',
|
||||
label: 'Support Bot',
|
||||
description: 'An AI agent for customer support',
|
||||
icon: 'IconHeadset',
|
||||
prompt:
|
||||
'You are a customer support agent. Help users resolve their issues.',
|
||||
},
|
||||
],
|
||||
}),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const agentsAfterFirstSync = await findAppAgents();
|
||||
|
||||
expect(agentsAfterFirstSync).toHaveLength(2);
|
||||
expect(
|
||||
agentsAfterFirstSync.find(
|
||||
(agent) => agent.name === 'sales-assistant',
|
||||
),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
agentsAfterFirstSync.find((agent) => agent.name === 'support-bot'),
|
||||
).toBeDefined();
|
||||
|
||||
await syncApplication({
|
||||
manifest: buildManifest({
|
||||
agents: [
|
||||
{
|
||||
universalIdentifier: TEST_AGENT_ID,
|
||||
name: 'sales-assistant',
|
||||
label: 'Sales Assistant',
|
||||
description: 'An AI agent that helps with sales tasks',
|
||||
icon: 'IconRobot',
|
||||
prompt:
|
||||
'You are a sales assistant. Help users with prospecting and outreach.',
|
||||
},
|
||||
],
|
||||
}),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const agentsAfterSecondSync = await findAppAgents();
|
||||
|
||||
expect(agentsAfterSecondSync).toHaveLength(1);
|
||||
expect(agentsAfterSecondSync[0]).toMatchObject({
|
||||
name: 'sales-assistant',
|
||||
label: 'Sales Assistant',
|
||||
});
|
||||
expect(
|
||||
agentsAfterSecondSync.find((agent) => agent.name === 'support-bot'),
|
||||
).toBeUndefined();
|
||||
}, 60000);
|
||||
});
|
||||
+1
@@ -28,6 +28,7 @@ export const buildBaseManifest = ({
|
||||
},
|
||||
],
|
||||
skills: [],
|
||||
agents: [],
|
||||
objects: [],
|
||||
fields: [],
|
||||
logicFunctions: [],
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType';
|
||||
|
||||
export type AgentManifest = SyncableEntityOptions & {
|
||||
name: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
description?: string;
|
||||
prompt: string;
|
||||
modelId?: string;
|
||||
};
|
||||
@@ -5,6 +5,7 @@ export enum SyncableEntity {
|
||||
FrontComponent = 'frontComponent',
|
||||
Role = 'role',
|
||||
Skill = 'skill',
|
||||
Agent = 'agent',
|
||||
View = 'view',
|
||||
NavigationMenuItem = 'navigationMenuItem',
|
||||
PageLayout = 'pageLayout',
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* |___/
|
||||
*/
|
||||
|
||||
export type { AgentManifest } from './agentManifestType';
|
||||
export type { ApplicationManifest } from './applicationType';
|
||||
export type { ApplicationVariables } from './applicationVariablesType';
|
||||
export type { AssetManifest } from './assetManifestType';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { type AgentManifest } from './agentManifestType';
|
||||
import { type ApplicationManifest } from './applicationType';
|
||||
import { type AssetManifest } from './assetManifestType';
|
||||
import { type FieldManifest } from './fieldManifestType';
|
||||
@@ -18,6 +19,7 @@ export type Manifest = {
|
||||
frontComponents: FrontComponentManifest[];
|
||||
roles: RoleManifest[];
|
||||
skills: SkillManifest[];
|
||||
agents: AgentManifest[];
|
||||
publicAssets: AssetManifest[];
|
||||
views: ViewManifest[];
|
||||
navigationMenuItems: NavigationMenuItemManifest[];
|
||||
|
||||
Reference in New Issue
Block a user