From d67aa2889b4db619b3e2bf1325fc598a9fafa9f0 Mon Sep 17 00:00:00 2001 From: Etienne <45695613+etiennejouan@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:00:21 +0200 Subject: [PATCH] feat(workflow): add update_agent tool and responseFormat-aware AI Agent step schema (#21755) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Brings the workflow AI/MCP tooling for **AI Agent steps** to parity with the existing **CODE / logic-function** flow, and makes AI Agent output references reliable in validation and the variable picker. Just like a CODE step needs a logic function, an `AI_AGENT` step needs an agent. The agent is already created as a side effect of step creation; this PR adds the missing "configure it" tooling and fixes the output schema so it reflects the agent's actual response format. ## Changes ### New `update_agent` MCP tool - `update-agent.tool.ts`: lets the assistant configure the agent backing an `AI_AGENT` step — `prompt` (system prompt), optional `modelId`, optional `responseFormat` (text or structured json) — via `AgentService.updateOneAgent`. Direct analog of `update_logic_function_source`. - Wired through: added `agentService` to `WorkflowToolDependencies`, injected `AgentService` and registered the tool in `workflow-tool.workspace-service.ts`, imported `AiAgentModule` in `workflow-tools.module.ts`. ### Guided creation flow (mirrors CODE) - `create-workflow-version-step.tool.ts`: `enrichResultWithNextStep` now returns an `AI_AGENT` hint instructing the assistant to call `update_agent` with the step's `settings.input.agentId` (and to set the task prompt via `update_workflow_version_step` if needed). - `create-complete-workflow.tool.ts`: rejects `AI_AGENT` steps (it inserts steps directly and never runs the side effect that creates the agent), with a description note pointing to `create_workflow_version_step` + `update_agent`. Same treatment CODE already gets. ### Correct output schema for AI Agent steps - Backend `computeStepOutputSchema` (`workflow-schema.workspace-service.ts`): the `AI_AGENT` case now derives the output schema from the agent's `responseFormat` instead of a hardcoded `{ response }`: - text → `{ response: string }` - json → one leaf per `responseFormat.schema.properties` field This makes workflow validation resolve `{{stepId.fieldName}}` references against the agent's real output (previously json agents validated wrong: real fields rejected, `{{stepId.response}}` accepted but undefined at runtime). - Frontend `useStepsOutputSchema.ts`: when an `AI_AGENT` step has no persisted `outputSchema`, fall back to generating it from the agent's `responseFormat` (via `FindManyAgents` + the existing `agentResponseSchemaToOutputSchema`) instead of the hardcoded `{ response }`. Keeps the variable picker correct for structured agents. ## Why output schema matters Validation resolves every `{{stepId.path}}` against the referenced step's `settings.outputSchema` (`validateWorkflowVariableReferences`). The agent step's output schema is therefore the single source of truth for "is the right output referenced." Because the runtime output depends on `responseFormat` (text → `{ response }`, json → schema fields directly), the schema must be derived from `responseFormat` to be accurate. ## Not solved issue We want each AI_AGENT step's settings.outputSchema to always match the backing agent's responseFormat: - text → { response } - json → one field per responseFormat.schema.properties That output schema is what everything downstream relies on: validation (validateWorkflowVariableReferences resolves {{stepId.field}} against it), the frontend variable picker (useStepsOutputSchema), and it's also persisted inside workflowVersion.steps. Agent should be unique source of truth but syncing agent -> step is not possible Review in cubic --- .../src/metadata/generated/schema.graphql | 240 +++--- .../src/metadata/generated/schema.ts | 134 ++-- .../src/metadata/generated/types.ts | 738 +++++++++--------- .../workflow-schema.workspace-service.ts | 70 +- .../workflow-tool.workspace-service.ts | 6 + .../tools/create-complete-workflow.tool.ts | 13 + .../create-workflow-version-step.tool.ts | 6 + .../workflow-tools/tools/update-agent.tool.ts | 104 +++ .../types/workflow-tool-dependencies.type.ts | 2 + .../workflow-tools/workflow-tools.module.ts | 2 + 10 files changed, 751 insertions(+), 564 deletions(-) create mode 100644 packages/twenty-server/src/modules/workflow/workflow-tools/tools/update-agent.tool.ts diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index 1841eaceec..b47f88af34 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -3046,14 +3046,8 @@ type Query { getPageLayout(id: String!): PageLayout getPageLayoutWidgets(pageLayoutTabId: String!): [PageLayoutWidget!]! getPageLayoutWidget(id: String!): PageLayoutWidget! - findOneLogicFunction(input: LogicFunctionIdInput!): LogicFunction! - findManyLogicFunctions: [LogicFunction!]! - getAvailablePackages(input: LogicFunctionIdInput!): JSON! - getLogicFunctionSourceCode(input: LogicFunctionIdInput!): String - commandMenuItems: [CommandMenuItem!]! - commandMenuItem(id: UUID!): CommandMenuItem - frontComponents: [FrontComponent!]! - frontComponent(id: UUID!): FrontComponent + findManyAgents: [Agent!]! + findOneAgent(input: AgentIdInput!): Agent! objectRecordCounts: [ObjectRecordCount!]! object( """The id of the record to find.""" @@ -3077,8 +3071,14 @@ type Query { """Specify to filter the records returned.""" filter: IndexFilter! = {} ): IndexConnection! - findManyAgents: [Agent!]! - findOneAgent(input: AgentIdInput!): Agent! + findOneLogicFunction(input: LogicFunctionIdInput!): LogicFunction! + findManyLogicFunctions: [LogicFunction!]! + getAvailablePackages(input: LogicFunctionIdInput!): JSON! + getLogicFunctionSourceCode(input: LogicFunctionIdInput!): String + commandMenuItems: [CommandMenuItem!]! + commandMenuItem(id: UUID!): CommandMenuItem + frontComponents: [FrontComponent!]! + frontComponent(id: UUID!): FrontComponent previewMessageCampaignAudience(input: PreviewMessageCampaignAudienceInput!): CampaignAudiencePreviewDTO! unsubscribeTopics: [UnsubscribeTopic!]! unsubscribePagePreviewUrl: String! @@ -3154,16 +3154,16 @@ input GetApiKeyInput { id: UUID! } -input LogicFunctionIdInput { - """The id of the function.""" - id: ID! -} - input AgentIdInput { """The id of the agent.""" id: UUID! } +input LogicFunctionIdInput { + """The id of the function.""" + id: ID! +} + input PreviewMessageCampaignAudienceInput { listId: String! unsubscribeTopicId: String @@ -3314,6 +3314,14 @@ type Mutation { createPageLayoutWidget(input: CreatePageLayoutWidgetInput!): PageLayoutWidget! updatePageLayoutWidget(id: String!, input: UpdatePageLayoutWidgetInput!): PageLayoutWidget! destroyPageLayoutWidget(id: String!): Boolean! + createOneAgent(input: CreateAgentInput!): Agent! + updateOneAgent(input: UpdateAgentInput!): Agent! + deleteOneAgent(input: AgentIdInput!): Agent! + createOneObject(input: CreateOneObjectInput!): Object! + deleteOneObject(input: DeleteOneObjectInput!): Object! + updateOneObject(input: UpdateOneObjectInput!): Object! + createOneIndex(input: CreateOneIndexInput!): Index! + deleteOneIndex(input: DeleteOneIndexInput!): Index! deleteOneLogicFunction(input: LogicFunctionIdInput!): LogicFunction! createOneLogicFunction(input: CreateLogicFunctionFromSourceInput!): LogicFunction! executeOneLogicFunction(input: ExecuteOneLogicFunctionInput!): LogicFunctionExecutionResult! @@ -3325,14 +3333,6 @@ type Mutation { createFrontComponent(input: CreateFrontComponentInput!): FrontComponent! updateFrontComponent(input: UpdateFrontComponentInput!): FrontComponent! deleteFrontComponent(id: UUID!): FrontComponent! - createOneObject(input: CreateOneObjectInput!): Object! - deleteOneObject(input: DeleteOneObjectInput!): Object! - updateOneObject(input: UpdateOneObjectInput!): Object! - createOneIndex(input: CreateOneIndexInput!): Index! - deleteOneIndex(input: DeleteOneIndexInput!): Index! - createOneAgent(input: CreateAgentInput!): Agent! - updateOneAgent(input: UpdateAgentInput!): Agent! - deleteOneAgent(input: AgentIdInput!): Agent! sendEmailViaEmailingDomain(input: SendEmailViaDomainInput!): SendEmailViaDomainOutput! sendMessageCampaign(input: SendMessageCampaignInput!): SendMessageCampaignOutputDTO! createUnsubscribeTopic(input: CreateUnsubscribeTopicInput!): UnsubscribeTopic! @@ -3903,6 +3903,103 @@ input UpdatePageLayoutWidgetInput { conditionalAvailabilityExpression: String } +input CreateAgentInput { + name: String + label: String! + icon: String + description: String + prompt: String! + modelId: String! + roleId: UUID + responseFormat: JSON + modelConfiguration: JSON + evaluationInputs: [String!] +} + +input UpdateAgentInput { + id: UUID! + name: String + label: String + icon: String + description: String + prompt: String + modelId: String + roleId: UUID + responseFormat: JSON + modelConfiguration: JSON + evaluationInputs: [String!] +} + +input CreateOneObjectInput { + """The object to create""" + object: CreateObjectInput! +} + +input CreateObjectInput { + nameSingular: String! + namePlural: String! + labelSingular: String! + labelPlural: String! + description: String + icon: String + shortcut: String + color: String + skipNameField: Boolean + isRemote: Boolean + primaryKeyColumnType: String + primaryKeyFieldMetadataSettings: JSON + isLabelSyncedWithName: Boolean +} + +input DeleteOneObjectInput { + """The id of the record to delete.""" + id: UUID! +} + +input UpdateOneObjectInput { + update: UpdateObjectPayload! + + """The id of the object to update""" + id: UUID! +} + +input UpdateObjectPayload { + labelSingular: String + labelPlural: String + nameSingular: String + namePlural: String + description: String + icon: String + shortcut: String + color: String + isActive: Boolean + labelIdentifierFieldMetadataId: UUID + imageIdentifierFieldMetadataId: UUID + isLabelSyncedWithName: Boolean + isSearchable: Boolean +} + +input CreateOneIndexInput { + """The custom index to create""" + index: CreateIndexInput! +} + +input CreateIndexInput { + objectMetadataId: UUID! + fields: [CreateIndexFieldInput!]! + indexType: IndexType! = BTREE +} + +input CreateIndexFieldInput { + fieldMetadataId: UUID! + subFieldName: String +} + +input DeleteOneIndexInput { + """The id of the custom index to delete.""" + id: UUID! +} + input CreateLogicFunctionFromSourceInput { id: UUID universalIdentifier: UUID @@ -4001,103 +4098,6 @@ input UpdateFrontComponentInputUpdates { description: String } -input CreateOneObjectInput { - """The object to create""" - object: CreateObjectInput! -} - -input CreateObjectInput { - nameSingular: String! - namePlural: String! - labelSingular: String! - labelPlural: String! - description: String - icon: String - shortcut: String - color: String - skipNameField: Boolean - isRemote: Boolean - primaryKeyColumnType: String - primaryKeyFieldMetadataSettings: JSON - isLabelSyncedWithName: Boolean -} - -input DeleteOneObjectInput { - """The id of the record to delete.""" - id: UUID! -} - -input UpdateOneObjectInput { - update: UpdateObjectPayload! - - """The id of the object to update""" - id: UUID! -} - -input UpdateObjectPayload { - labelSingular: String - labelPlural: String - nameSingular: String - namePlural: String - description: String - icon: String - shortcut: String - color: String - isActive: Boolean - labelIdentifierFieldMetadataId: UUID - imageIdentifierFieldMetadataId: UUID - isLabelSyncedWithName: Boolean - isSearchable: Boolean -} - -input CreateOneIndexInput { - """The custom index to create""" - index: CreateIndexInput! -} - -input CreateIndexInput { - objectMetadataId: UUID! - fields: [CreateIndexFieldInput!]! - indexType: IndexType! = BTREE -} - -input CreateIndexFieldInput { - fieldMetadataId: UUID! - subFieldName: String -} - -input DeleteOneIndexInput { - """The id of the custom index to delete.""" - id: UUID! -} - -input CreateAgentInput { - name: String - label: String! - icon: String - description: String - prompt: String! - modelId: String! - roleId: UUID - responseFormat: JSON - modelConfiguration: JSON - evaluationInputs: [String!] -} - -input UpdateAgentInput { - id: UUID! - name: String - label: String - icon: String - description: String - prompt: String - modelId: String - roleId: UUID - responseFormat: JSON - modelConfiguration: JSON - evaluationInputs: [String!] -} - input SendEmailViaDomainInput { emailingDomainId: String! to: [String!]! diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index ad3e5c2294..bd42fca367 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -2673,6 +2673,13 @@ export interface Query { getPageLayout?: PageLayout getPageLayoutWidgets: PageLayoutWidget[] getPageLayoutWidget: PageLayoutWidget + findManyAgents: Agent[] + findOneAgent: Agent + objectRecordCounts: ObjectRecordCount[] + object: Object + objects: ObjectConnection + index: Index + indexMetadatas: IndexConnection findOneLogicFunction: LogicFunction findManyLogicFunctions: LogicFunction[] getAvailablePackages: Scalars['JSON'] @@ -2681,13 +2688,6 @@ export interface Query { commandMenuItem?: CommandMenuItem frontComponents: FrontComponent[] frontComponent?: FrontComponent - objectRecordCounts: ObjectRecordCount[] - object: Object - objects: ObjectConnection - index: Index - indexMetadatas: IndexConnection - findManyAgents: Agent[] - findOneAgent: Agent previewMessageCampaignAudience: CampaignAudiencePreviewDTO unsubscribeTopics: UnsubscribeTopic[] unsubscribePagePreviewUrl: Scalars['String'] @@ -2835,6 +2835,14 @@ export interface Mutation { createPageLayoutWidget: PageLayoutWidget updatePageLayoutWidget: PageLayoutWidget destroyPageLayoutWidget: Scalars['Boolean'] + createOneAgent: Agent + updateOneAgent: Agent + deleteOneAgent: Agent + createOneObject: Object + deleteOneObject: Object + updateOneObject: Object + createOneIndex: Index + deleteOneIndex: Index deleteOneLogicFunction: LogicFunction createOneLogicFunction: LogicFunction executeOneLogicFunction: LogicFunctionExecutionResult @@ -2846,14 +2854,6 @@ export interface Mutation { createFrontComponent: FrontComponent updateFrontComponent: FrontComponent deleteFrontComponent: FrontComponent - createOneObject: Object - deleteOneObject: Object - updateOneObject: Object - createOneIndex: Index - deleteOneIndex: Index - createOneAgent: Agent - updateOneAgent: Agent - deleteOneAgent: Agent sendEmailViaEmailingDomain: SendEmailViaDomainOutput sendMessageCampaign: SendMessageCampaignOutputDTO createUnsubscribeTopic: UnsubscribeTopic @@ -5808,14 +5808,8 @@ export interface QueryGenqlSelection{ getPageLayout?: (PageLayoutGenqlSelection & { __args: {id: Scalars['String']} }) getPageLayoutWidgets?: (PageLayoutWidgetGenqlSelection & { __args: {pageLayoutTabId: Scalars['String']} }) getPageLayoutWidget?: (PageLayoutWidgetGenqlSelection & { __args: {id: Scalars['String']} }) - findOneLogicFunction?: (LogicFunctionGenqlSelection & { __args: {input: LogicFunctionIdInput} }) - findManyLogicFunctions?: LogicFunctionGenqlSelection - getAvailablePackages?: { __args: {input: LogicFunctionIdInput} } - getLogicFunctionSourceCode?: { __args: {input: LogicFunctionIdInput} } - commandMenuItems?: CommandMenuItemGenqlSelection - commandMenuItem?: (CommandMenuItemGenqlSelection & { __args: {id: Scalars['UUID']} }) - frontComponents?: FrontComponentGenqlSelection - frontComponent?: (FrontComponentGenqlSelection & { __args: {id: Scalars['UUID']} }) + findManyAgents?: AgentGenqlSelection + findOneAgent?: (AgentGenqlSelection & { __args: {input: AgentIdInput} }) objectRecordCounts?: ObjectRecordCountGenqlSelection object?: (ObjectGenqlSelection & { __args: { /** The id of the record to find. */ @@ -5833,8 +5827,14 @@ export interface QueryGenqlSelection{ paging: CursorPaging, /** Specify to filter the records returned. */ filter: IndexFilter} }) - findManyAgents?: AgentGenqlSelection - findOneAgent?: (AgentGenqlSelection & { __args: {input: AgentIdInput} }) + findOneLogicFunction?: (LogicFunctionGenqlSelection & { __args: {input: LogicFunctionIdInput} }) + findManyLogicFunctions?: LogicFunctionGenqlSelection + getAvailablePackages?: { __args: {input: LogicFunctionIdInput} } + getLogicFunctionSourceCode?: { __args: {input: LogicFunctionIdInput} } + commandMenuItems?: CommandMenuItemGenqlSelection + commandMenuItem?: (CommandMenuItemGenqlSelection & { __args: {id: Scalars['UUID']} }) + frontComponents?: FrontComponentGenqlSelection + frontComponent?: (FrontComponentGenqlSelection & { __args: {id: Scalars['UUID']} }) previewMessageCampaignAudience?: (CampaignAudiencePreviewDTOGenqlSelection & { __args: {input: PreviewMessageCampaignAudienceInput} }) unsubscribeTopics?: UnsubscribeTopicGenqlSelection unsubscribePagePreviewUrl?: boolean | number @@ -5907,14 +5907,14 @@ export interface QueryGenqlSelection{ export interface GetApiKeyInput {id: Scalars['UUID']} -export interface LogicFunctionIdInput { -/** The id of the function. */ -id: Scalars['ID']} - export interface AgentIdInput { /** The id of the agent. */ id: Scalars['UUID']} +export interface LogicFunctionIdInput { +/** The id of the function. */ +id: Scalars['ID']} + export interface PreviewMessageCampaignAudienceInput {listId: Scalars['String'],unsubscribeTopicId?: (Scalars['String'] | null)} export interface ListAppConnectionsInput {providerName?: (Scalars['String'] | null),userWorkspaceId?: (Scalars['String'] | null),visibility?: (Scalars['String'] | null)} @@ -6013,6 +6013,14 @@ export interface MutationGenqlSelection{ createPageLayoutWidget?: (PageLayoutWidgetGenqlSelection & { __args: {input: CreatePageLayoutWidgetInput} }) updatePageLayoutWidget?: (PageLayoutWidgetGenqlSelection & { __args: {id: Scalars['String'], input: UpdatePageLayoutWidgetInput} }) destroyPageLayoutWidget?: { __args: {id: Scalars['String']} } + createOneAgent?: (AgentGenqlSelection & { __args: {input: CreateAgentInput} }) + updateOneAgent?: (AgentGenqlSelection & { __args: {input: UpdateAgentInput} }) + deleteOneAgent?: (AgentGenqlSelection & { __args: {input: AgentIdInput} }) + createOneObject?: (ObjectGenqlSelection & { __args: {input: CreateOneObjectInput} }) + deleteOneObject?: (ObjectGenqlSelection & { __args: {input: DeleteOneObjectInput} }) + updateOneObject?: (ObjectGenqlSelection & { __args: {input: UpdateOneObjectInput} }) + createOneIndex?: (IndexGenqlSelection & { __args: {input: CreateOneIndexInput} }) + deleteOneIndex?: (IndexGenqlSelection & { __args: {input: DeleteOneIndexInput} }) deleteOneLogicFunction?: (LogicFunctionGenqlSelection & { __args: {input: LogicFunctionIdInput} }) createOneLogicFunction?: (LogicFunctionGenqlSelection & { __args: {input: CreateLogicFunctionFromSourceInput} }) executeOneLogicFunction?: (LogicFunctionExecutionResultGenqlSelection & { __args: {input: ExecuteOneLogicFunctionInput} }) @@ -6024,14 +6032,6 @@ export interface MutationGenqlSelection{ createFrontComponent?: (FrontComponentGenqlSelection & { __args: {input: CreateFrontComponentInput} }) updateFrontComponent?: (FrontComponentGenqlSelection & { __args: {input: UpdateFrontComponentInput} }) deleteFrontComponent?: (FrontComponentGenqlSelection & { __args: {id: Scalars['UUID']} }) - createOneObject?: (ObjectGenqlSelection & { __args: {input: CreateOneObjectInput} }) - deleteOneObject?: (ObjectGenqlSelection & { __args: {input: DeleteOneObjectInput} }) - updateOneObject?: (ObjectGenqlSelection & { __args: {input: UpdateOneObjectInput} }) - createOneIndex?: (IndexGenqlSelection & { __args: {input: CreateOneIndexInput} }) - deleteOneIndex?: (IndexGenqlSelection & { __args: {input: DeleteOneIndexInput} }) - createOneAgent?: (AgentGenqlSelection & { __args: {input: CreateAgentInput} }) - updateOneAgent?: (AgentGenqlSelection & { __args: {input: UpdateAgentInput} }) - deleteOneAgent?: (AgentGenqlSelection & { __args: {input: AgentIdInput} }) sendEmailViaEmailingDomain?: (SendEmailViaDomainOutputGenqlSelection & { __args: {input: SendEmailViaDomainInput} }) sendMessageCampaign?: (SendMessageCampaignOutputDTOGenqlSelection & { __args: {input: SendMessageCampaignInput} }) createUnsubscribeTopic?: (UnsubscribeTopicGenqlSelection & { __args: {input: CreateUnsubscribeTopicInput} }) @@ -6323,35 +6323,9 @@ export interface CreatePageLayoutWidgetInput {pageLayoutTabId: Scalars['UUID'],t export interface UpdatePageLayoutWidgetInput {pageLayoutTabId?: (Scalars['UUID'] | null),title?: (Scalars['String'] | null),type?: (WidgetType | null),objectMetadataId?: (Scalars['UUID'] | null),gridPosition?: (GridPositionInput | null),position?: (Scalars['JSON'] | null),configuration?: (Scalars['JSON'] | null),conditionalDisplay?: (Scalars['JSON'] | null),conditionalAvailabilityExpression?: (Scalars['String'] | null)} -export interface CreateLogicFunctionFromSourceInput {id?: (Scalars['UUID'] | null),universalIdentifier?: (Scalars['UUID'] | null),name: Scalars['String'],description?: (Scalars['String'] | null),timeoutSeconds?: (Scalars['Float'] | null),source?: (Scalars['JSON'] | null),cronTriggerSettings?: (Scalars['JSON'] | null),databaseEventTriggerSettings?: (Scalars['JSON'] | null),httpRouteTriggerSettings?: (Scalars['JSON'] | null),toolTriggerSettings?: (Scalars['JSON'] | null),workflowActionTriggerSettings?: (Scalars['JSON'] | null)} +export interface CreateAgentInput {name?: (Scalars['String'] | null),label: Scalars['String'],icon?: (Scalars['String'] | null),description?: (Scalars['String'] | null),prompt: Scalars['String'],modelId: Scalars['String'],roleId?: (Scalars['UUID'] | null),responseFormat?: (Scalars['JSON'] | null),modelConfiguration?: (Scalars['JSON'] | null),evaluationInputs?: (Scalars['String'][] | null)} -export interface ExecuteOneLogicFunctionInput { -/** Id of the logic function to execute */ -id: Scalars['UUID'], -/** Payload in JSON format */ -payload: Scalars['JSON']} - -export interface UpdateLogicFunctionFromSourceInput { -/** Id of the logic function to update */ -id: Scalars['UUID'], -/** The logic function updates */ -update: UpdateLogicFunctionFromSourceInputUpdates} - -export interface UpdateLogicFunctionFromSourceInputUpdates {name?: (Scalars['String'] | null),description?: (Scalars['String'] | null),timeoutSeconds?: (Scalars['Float'] | null),sourceHandlerCode?: (Scalars['String'] | null),handlerName?: (Scalars['String'] | null),sourceHandlerPath?: (Scalars['String'] | null),cronTriggerSettings?: (Scalars['JSON'] | null),databaseEventTriggerSettings?: (Scalars['JSON'] | null),httpRouteTriggerSettings?: (Scalars['JSON'] | null),toolTriggerSettings?: (Scalars['JSON'] | null),workflowActionTriggerSettings?: (Scalars['JSON'] | null)} - -export interface CreateCommandMenuItemInput {workflowVersionId?: (Scalars['UUID'] | null),frontComponentId?: (Scalars['UUID'] | null),engineComponentKey: EngineComponentKey,label: Scalars['String'],icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),hotKeys?: (Scalars['String'][] | null),conditionalAvailabilityExpression?: (Scalars['String'] | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null),payload?: (Scalars['JSON'] | null),pageLayoutId?: (Scalars['UUID'] | null)} - -export interface UpdateCommandMenuItemInput {id: Scalars['UUID'],label?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null),engineComponentKey?: (EngineComponentKey | null),hotKeys?: (Scalars['String'][] | null),pageLayoutId?: (Scalars['UUID'] | null)} - -export interface CreateFrontComponentInput {id?: (Scalars['UUID'] | null),name: Scalars['String'],description?: (Scalars['String'] | null),sourceComponentPath: Scalars['String'],builtComponentPath: Scalars['String'],componentName: Scalars['String'],builtComponentChecksum: Scalars['String']} - -export interface UpdateFrontComponentInput { -/** The id of the front component to update */ -id: Scalars['UUID'], -/** The front component fields to update */ -update: UpdateFrontComponentInputUpdates} - -export interface UpdateFrontComponentInputUpdates {name?: (Scalars['String'] | null),description?: (Scalars['String'] | null)} +export interface UpdateAgentInput {id: Scalars['UUID'],name?: (Scalars['String'] | null),label?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),description?: (Scalars['String'] | null),prompt?: (Scalars['String'] | null),modelId?: (Scalars['String'] | null),roleId?: (Scalars['UUID'] | null),responseFormat?: (Scalars['JSON'] | null),modelConfiguration?: (Scalars['JSON'] | null),evaluationInputs?: (Scalars['String'][] | null)} export interface CreateOneObjectInput { /** The object to create */ @@ -6381,9 +6355,35 @@ export interface DeleteOneIndexInput { /** The id of the custom index to delete. */ id: Scalars['UUID']} -export interface CreateAgentInput {name?: (Scalars['String'] | null),label: Scalars['String'],icon?: (Scalars['String'] | null),description?: (Scalars['String'] | null),prompt: Scalars['String'],modelId: Scalars['String'],roleId?: (Scalars['UUID'] | null),responseFormat?: (Scalars['JSON'] | null),modelConfiguration?: (Scalars['JSON'] | null),evaluationInputs?: (Scalars['String'][] | null)} +export interface CreateLogicFunctionFromSourceInput {id?: (Scalars['UUID'] | null),universalIdentifier?: (Scalars['UUID'] | null),name: Scalars['String'],description?: (Scalars['String'] | null),timeoutSeconds?: (Scalars['Float'] | null),source?: (Scalars['JSON'] | null),cronTriggerSettings?: (Scalars['JSON'] | null),databaseEventTriggerSettings?: (Scalars['JSON'] | null),httpRouteTriggerSettings?: (Scalars['JSON'] | null),toolTriggerSettings?: (Scalars['JSON'] | null),workflowActionTriggerSettings?: (Scalars['JSON'] | null)} -export interface UpdateAgentInput {id: Scalars['UUID'],name?: (Scalars['String'] | null),label?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),description?: (Scalars['String'] | null),prompt?: (Scalars['String'] | null),modelId?: (Scalars['String'] | null),roleId?: (Scalars['UUID'] | null),responseFormat?: (Scalars['JSON'] | null),modelConfiguration?: (Scalars['JSON'] | null),evaluationInputs?: (Scalars['String'][] | null)} +export interface ExecuteOneLogicFunctionInput { +/** Id of the logic function to execute */ +id: Scalars['UUID'], +/** Payload in JSON format */ +payload: Scalars['JSON']} + +export interface UpdateLogicFunctionFromSourceInput { +/** Id of the logic function to update */ +id: Scalars['UUID'], +/** The logic function updates */ +update: UpdateLogicFunctionFromSourceInputUpdates} + +export interface UpdateLogicFunctionFromSourceInputUpdates {name?: (Scalars['String'] | null),description?: (Scalars['String'] | null),timeoutSeconds?: (Scalars['Float'] | null),sourceHandlerCode?: (Scalars['String'] | null),handlerName?: (Scalars['String'] | null),sourceHandlerPath?: (Scalars['String'] | null),cronTriggerSettings?: (Scalars['JSON'] | null),databaseEventTriggerSettings?: (Scalars['JSON'] | null),httpRouteTriggerSettings?: (Scalars['JSON'] | null),toolTriggerSettings?: (Scalars['JSON'] | null),workflowActionTriggerSettings?: (Scalars['JSON'] | null)} + +export interface CreateCommandMenuItemInput {workflowVersionId?: (Scalars['UUID'] | null),frontComponentId?: (Scalars['UUID'] | null),engineComponentKey: EngineComponentKey,label: Scalars['String'],icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),hotKeys?: (Scalars['String'][] | null),conditionalAvailabilityExpression?: (Scalars['String'] | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null),payload?: (Scalars['JSON'] | null),pageLayoutId?: (Scalars['UUID'] | null)} + +export interface UpdateCommandMenuItemInput {id: Scalars['UUID'],label?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null),engineComponentKey?: (EngineComponentKey | null),hotKeys?: (Scalars['String'][] | null),pageLayoutId?: (Scalars['UUID'] | null)} + +export interface CreateFrontComponentInput {id?: (Scalars['UUID'] | null),name: Scalars['String'],description?: (Scalars['String'] | null),sourceComponentPath: Scalars['String'],builtComponentPath: Scalars['String'],componentName: Scalars['String'],builtComponentChecksum: Scalars['String']} + +export interface UpdateFrontComponentInput { +/** The id of the front component to update */ +id: Scalars['UUID'], +/** The front component fields to update */ +update: UpdateFrontComponentInputUpdates} + +export interface UpdateFrontComponentInputUpdates {name?: (Scalars['String'] | null),description?: (Scalars['String'] | null)} export interface SendEmailViaDomainInput {emailingDomainId: Scalars['String'],to: Scalars['String'][],cc?: (Scalars['String'][] | null),bcc?: (Scalars['String'][] | null),subject: Scalars['String'],text: Scalars['String'],html?: (Scalars['String'] | null),from: Scalars['String'],replyTo?: (Scalars['String'][] | null)} diff --git a/packages/twenty-client-sdk/src/metadata/generated/types.ts b/packages/twenty-client-sdk/src/metadata/generated/types.ts index b7ab046889..043f8d5c09 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/types.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/types.ts @@ -6186,57 +6186,15 @@ export default { ] } ], - "findOneLogicFunction": [ - 41, + "findManyAgents": [ + 25 + ], + "findOneAgent": [ + 25, { "input": [ 338, - "LogicFunctionIdInput!" - ] - } - ], - "findManyLogicFunctions": [ - 41 - ], - "getAvailablePackages": [ - 15, - { - "input": [ - 338, - "LogicFunctionIdInput!" - ] - } - ], - "getLogicFunctionSourceCode": [ - 1, - { - "input": [ - 338, - "LogicFunctionIdInput!" - ] - } - ], - "commandMenuItems": [ - 35 - ], - "commandMenuItem": [ - 35, - { - "id": [ - 3, - "UUID!" - ] - } - ], - "frontComponents": [ - 34 - ], - "frontComponent": [ - 34, - { - "id": [ - 3, - "UUID!" + "AgentIdInput!" ] } ], @@ -6287,15 +6245,57 @@ export default { ] } ], - "findManyAgents": [ - 25 - ], - "findOneAgent": [ - 25, + "findOneLogicFunction": [ + 41, { "input": [ 339, - "AgentIdInput!" + "LogicFunctionIdInput!" + ] + } + ], + "findManyLogicFunctions": [ + 41 + ], + "getAvailablePackages": [ + 15, + { + "input": [ + 339, + "LogicFunctionIdInput!" + ] + } + ], + "getLogicFunctionSourceCode": [ + 1, + { + "input": [ + 339, + "LogicFunctionIdInput!" + ] + } + ], + "commandMenuItems": [ + 35 + ], + "commandMenuItem": [ + 35, + { + "id": [ + 3, + "UUID!" + ] + } + ], + "frontComponents": [ + 34 + ], + "frontComponent": [ + 34, + { + "id": [ + 3, + "UUID!" ] } ], @@ -6747,17 +6747,17 @@ export default { 1 ] }, - "LogicFunctionIdInput": { + "AgentIdInput": { "id": [ - 218 + 3 ], "__typename": [ 1 ] }, - "AgentIdInput": { + "LogicFunctionIdInput": { "id": [ - 3 + 218 ], "__typename": [ 1 @@ -7604,11 +7604,83 @@ export default { ] } ], + "createOneAgent": [ + 25, + { + "input": [ + 406, + "CreateAgentInput!" + ] + } + ], + "updateOneAgent": [ + 25, + { + "input": [ + 407, + "UpdateAgentInput!" + ] + } + ], + "deleteOneAgent": [ + 25, + { + "input": [ + 338, + "AgentIdInput!" + ] + } + ], + "createOneObject": [ + 56, + { + "input": [ + 408, + "CreateOneObjectInput!" + ] + } + ], + "deleteOneObject": [ + 56, + { + "input": [ + 410, + "DeleteOneObjectInput!" + ] + } + ], + "updateOneObject": [ + 56, + { + "input": [ + 411, + "UpdateOneObjectInput!" + ] + } + ], + "createOneIndex": [ + 47, + { + "input": [ + 413, + "CreateOneIndexInput!" + ] + } + ], + "deleteOneIndex": [ + 47, + { + "input": [ + 416, + "DeleteOneIndexInput!" + ] + } + ], "deleteOneLogicFunction": [ 41, { "input": [ - 338, + 339, "LogicFunctionIdInput!" ] } @@ -7617,7 +7689,7 @@ export default { 41, { "input": [ - 406, + 417, "CreateLogicFunctionFromSourceInput!" ] } @@ -7626,7 +7698,7 @@ export default { 168, { "input": [ - 407, + 418, "ExecuteOneLogicFunctionInput!" ] } @@ -7635,7 +7707,7 @@ export default { 6, { "input": [ - 408, + 419, "UpdateLogicFunctionFromSourceInput!" ] } @@ -7644,7 +7716,7 @@ export default { 35, { "input": [ - 410, + 421, "CreateCommandMenuItemInput!" ] } @@ -7653,7 +7725,7 @@ export default { 35, { "input": [ - 411, + 422, "UpdateCommandMenuItemInput!" ] } @@ -7680,7 +7752,7 @@ export default { 34, { "input": [ - 412, + 423, "CreateFrontComponentInput!" ] } @@ -7689,7 +7761,7 @@ export default { 34, { "input": [ - 413, + 424, "UpdateFrontComponentInput!" ] } @@ -7703,78 +7775,6 @@ export default { ] } ], - "createOneObject": [ - 56, - { - "input": [ - 415, - "CreateOneObjectInput!" - ] - } - ], - "deleteOneObject": [ - 56, - { - "input": [ - 417, - "DeleteOneObjectInput!" - ] - } - ], - "updateOneObject": [ - 56, - { - "input": [ - 418, - "UpdateOneObjectInput!" - ] - } - ], - "createOneIndex": [ - 47, - { - "input": [ - 420, - "CreateOneIndexInput!" - ] - } - ], - "deleteOneIndex": [ - 47, - { - "input": [ - 423, - "DeleteOneIndexInput!" - ] - } - ], - "createOneAgent": [ - 25, - { - "input": [ - 424, - "CreateAgentInput!" - ] - } - ], - "updateOneAgent": [ - 25, - { - "input": [ - 425, - "UpdateAgentInput!" - ] - } - ], - "deleteOneAgent": [ - 25, - { - "input": [ - 339, - "AgentIdInput!" - ] - } - ], "sendEmailViaEmailingDomain": [ 283, { @@ -10021,6 +10021,235 @@ export default { 1 ] }, + "CreateAgentInput": { + "name": [ + 1 + ], + "label": [ + 1 + ], + "icon": [ + 1 + ], + "description": [ + 1 + ], + "prompt": [ + 1 + ], + "modelId": [ + 1 + ], + "roleId": [ + 3 + ], + "responseFormat": [ + 15 + ], + "modelConfiguration": [ + 15 + ], + "evaluationInputs": [ + 1 + ], + "__typename": [ + 1 + ] + }, + "UpdateAgentInput": { + "id": [ + 3 + ], + "name": [ + 1 + ], + "label": [ + 1 + ], + "icon": [ + 1 + ], + "description": [ + 1 + ], + "prompt": [ + 1 + ], + "modelId": [ + 1 + ], + "roleId": [ + 3 + ], + "responseFormat": [ + 15 + ], + "modelConfiguration": [ + 15 + ], + "evaluationInputs": [ + 1 + ], + "__typename": [ + 1 + ] + }, + "CreateOneObjectInput": { + "object": [ + 409 + ], + "__typename": [ + 1 + ] + }, + "CreateObjectInput": { + "nameSingular": [ + 1 + ], + "namePlural": [ + 1 + ], + "labelSingular": [ + 1 + ], + "labelPlural": [ + 1 + ], + "description": [ + 1 + ], + "icon": [ + 1 + ], + "shortcut": [ + 1 + ], + "color": [ + 1 + ], + "skipNameField": [ + 6 + ], + "isRemote": [ + 6 + ], + "primaryKeyColumnType": [ + 1 + ], + "primaryKeyFieldMetadataSettings": [ + 15 + ], + "isLabelSyncedWithName": [ + 6 + ], + "__typename": [ + 1 + ] + }, + "DeleteOneObjectInput": { + "id": [ + 3 + ], + "__typename": [ + 1 + ] + }, + "UpdateOneObjectInput": { + "update": [ + 412 + ], + "id": [ + 3 + ], + "__typename": [ + 1 + ] + }, + "UpdateObjectPayload": { + "labelSingular": [ + 1 + ], + "labelPlural": [ + 1 + ], + "nameSingular": [ + 1 + ], + "namePlural": [ + 1 + ], + "description": [ + 1 + ], + "icon": [ + 1 + ], + "shortcut": [ + 1 + ], + "color": [ + 1 + ], + "isActive": [ + 6 + ], + "labelIdentifierFieldMetadataId": [ + 3 + ], + "imageIdentifierFieldMetadataId": [ + 3 + ], + "isLabelSyncedWithName": [ + 6 + ], + "isSearchable": [ + 6 + ], + "__typename": [ + 1 + ] + }, + "CreateOneIndexInput": { + "index": [ + 414 + ], + "__typename": [ + 1 + ] + }, + "CreateIndexInput": { + "objectMetadataId": [ + 3 + ], + "fields": [ + 415 + ], + "indexType": [ + 48 + ], + "__typename": [ + 1 + ] + }, + "CreateIndexFieldInput": { + "fieldMetadataId": [ + 3 + ], + "subFieldName": [ + 1 + ], + "__typename": [ + 1 + ] + }, + "DeleteOneIndexInput": { + "id": [ + 3 + ], + "__typename": [ + 1 + ] + }, "CreateLogicFunctionFromSourceInput": { "id": [ 3 @@ -10075,7 +10304,7 @@ export default { 3 ], "update": [ - 409 + 420 ], "__typename": [ 1 @@ -10235,7 +10464,7 @@ export default { 3 ], "update": [ - 414 + 425 ], "__typename": [ 1 @@ -10252,235 +10481,6 @@ export default { 1 ] }, - "CreateOneObjectInput": { - "object": [ - 416 - ], - "__typename": [ - 1 - ] - }, - "CreateObjectInput": { - "nameSingular": [ - 1 - ], - "namePlural": [ - 1 - ], - "labelSingular": [ - 1 - ], - "labelPlural": [ - 1 - ], - "description": [ - 1 - ], - "icon": [ - 1 - ], - "shortcut": [ - 1 - ], - "color": [ - 1 - ], - "skipNameField": [ - 6 - ], - "isRemote": [ - 6 - ], - "primaryKeyColumnType": [ - 1 - ], - "primaryKeyFieldMetadataSettings": [ - 15 - ], - "isLabelSyncedWithName": [ - 6 - ], - "__typename": [ - 1 - ] - }, - "DeleteOneObjectInput": { - "id": [ - 3 - ], - "__typename": [ - 1 - ] - }, - "UpdateOneObjectInput": { - "update": [ - 419 - ], - "id": [ - 3 - ], - "__typename": [ - 1 - ] - }, - "UpdateObjectPayload": { - "labelSingular": [ - 1 - ], - "labelPlural": [ - 1 - ], - "nameSingular": [ - 1 - ], - "namePlural": [ - 1 - ], - "description": [ - 1 - ], - "icon": [ - 1 - ], - "shortcut": [ - 1 - ], - "color": [ - 1 - ], - "isActive": [ - 6 - ], - "labelIdentifierFieldMetadataId": [ - 3 - ], - "imageIdentifierFieldMetadataId": [ - 3 - ], - "isLabelSyncedWithName": [ - 6 - ], - "isSearchable": [ - 6 - ], - "__typename": [ - 1 - ] - }, - "CreateOneIndexInput": { - "index": [ - 421 - ], - "__typename": [ - 1 - ] - }, - "CreateIndexInput": { - "objectMetadataId": [ - 3 - ], - "fields": [ - 422 - ], - "indexType": [ - 48 - ], - "__typename": [ - 1 - ] - }, - "CreateIndexFieldInput": { - "fieldMetadataId": [ - 3 - ], - "subFieldName": [ - 1 - ], - "__typename": [ - 1 - ] - }, - "DeleteOneIndexInput": { - "id": [ - 3 - ], - "__typename": [ - 1 - ] - }, - "CreateAgentInput": { - "name": [ - 1 - ], - "label": [ - 1 - ], - "icon": [ - 1 - ], - "description": [ - 1 - ], - "prompt": [ - 1 - ], - "modelId": [ - 1 - ], - "roleId": [ - 3 - ], - "responseFormat": [ - 15 - ], - "modelConfiguration": [ - 15 - ], - "evaluationInputs": [ - 1 - ], - "__typename": [ - 1 - ] - }, - "UpdateAgentInput": { - "id": [ - 3 - ], - "name": [ - 1 - ], - "label": [ - 1 - ], - "icon": [ - 1 - ], - "description": [ - 1 - ], - "prompt": [ - 1 - ], - "modelId": [ - 1 - ], - "roleId": [ - 3 - ], - "responseFormat": [ - 15 - ], - "modelConfiguration": [ - 15 - ], - "evaluationInputs": [ - 1 - ], - "__typename": [ - 1 - ] - }, "SendEmailViaDomainInput": { "emailingDomainId": [ 1 diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts index 834ae46d18..e2174e58a6 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts @@ -30,6 +30,7 @@ import { generateFakeValue } from 'src/engine/utils/generate-fake-value'; import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { DEFAULT_ITERATOR_CURRENT_ITEM } from 'src/modules/workflow/workflow-builder/workflow-schema/constants/default-iterator-current-item.const'; import { + type BaseOutputSchema, Leaf, Node, type OutputSchema, @@ -134,14 +135,10 @@ export class WorkflowSchemaWorkspaceService { }; } case WorkflowActionType.AI_AGENT: { - return { - response: { - label: 'Response', - isLeaf: true, - type: 'string', - value: 'Response of the agent', - }, - }; + return this.computeAiAgentActionOutputSchema({ + agentId: step.settings.input.agentId, + workspaceId, + }); } case WorkflowTriggerType.WEBHOOK: case WorkflowActionType.CODE: @@ -379,6 +376,63 @@ export class WorkflowSchemaWorkspaceService { return { success: { isLeaf: true, type: 'boolean', value: true } }; } + private async computeAiAgentActionOutputSchema({ + agentId, + workspaceId, + }: { + agentId?: string; + workspaceId: string; + }): Promise { + const textResponseOutputSchema: OutputSchema = { + response: { + label: 'Response', + isLeaf: true, + type: 'string', + value: 'Response of the agent', + }, + }; + + if (!isDefined(agentId)) { + return textResponseOutputSchema; + } + + const { flatAgentMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatAgentMaps'], + }, + ); + + const flatAgent = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: agentId, + flatEntityMaps: flatAgentMaps, + }); + + const responseFormat = flatAgent?.responseFormat; + + if (responseFormat?.type !== 'json') { + return textResponseOutputSchema; + } + + return Object.entries(responseFormat.schema.properties || {}).reduce( + (outputSchema, [propertyName, property]) => { + outputSchema[propertyName] = { + isLeaf: true, + type: property.type, + label: propertyName, + ...(isDefined(property.description) + ? { description: property.description } + : {}), + value: generateFakeValue(property.type), + }; + + return outputSchema; + }, + {} as BaseOutputSchema, + ); + } + private async computeFormActionOutputSchema({ formFieldMetadataItems, workspaceId, diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts index 1a8c4c546f..9c7c22a258 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts @@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common'; import { type ToolSet } from 'ai'; import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service'; +import { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.service'; import { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; @@ -24,6 +25,7 @@ import { createDeleteWorkflowVersionEdgeTool } from 'src/modules/workflow/workfl import { createDeleteWorkflowVersionStepTool } from 'src/modules/workflow/workflow-tools/tools/delete-workflow-version-step.tool'; import { createGetWorkflowCurrentVersionTool } from 'src/modules/workflow/workflow-tools/tools/get-workflow-current-version.tool'; import { createListLogicFunctionToolsTool } from 'src/modules/workflow/workflow-tools/tools/list-logic-function-tools.tool'; +import { createUpdateAgentTool } from 'src/modules/workflow/workflow-tools/tools/update-agent.tool'; import { createUpdateLogicFunctionSourceTool } from 'src/modules/workflow/workflow-tools/tools/update-logic-function-source.tool'; import { createUpdateWorkflowVersionPositionsTool } from 'src/modules/workflow/workflow-tools/tools/update-workflow-version-positions.tool'; import { createUpdateWorkflowVersionStepTool } from 'src/modules/workflow/workflow-tools/tools/update-workflow-version-step.tool'; @@ -48,6 +50,7 @@ export class WorkflowToolWorkspaceService { recordPositionService: RecordPositionService, logicFunctionFromSourceService: LogicFunctionFromSourceService, flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, + agentService: AgentService, ) { this.deps = { workflowVersionStepService, @@ -61,6 +64,7 @@ export class WorkflowToolWorkspaceService { recordPositionService, logicFunctionFromSourceService, flatEntityMapsCacheService, + agentService, }; } @@ -128,6 +132,7 @@ export class WorkflowToolWorkspaceService { this.deps, context, ); + const updateAgent = createUpdateAgentTool(this.deps, context); const validateWorkflow = createValidateWorkflowTool(this.deps, context); return { @@ -146,6 +151,7 @@ export class WorkflowToolWorkspaceService { [getWorkflowCurrentVersion.name]: getWorkflowCurrentVersion, [updateLogicFunctionSource.name]: updateLogicFunctionSource, [listLogicFunctionTools.name]: listLogicFunctionTools, + [updateAgent.name]: updateAgent, [validateWorkflow.name]: validateWorkflow, }; } diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/tools/create-complete-workflow.tool.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/create-complete-workflow.tool.ts index 0e7f6d6acf..7f650e9e77 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-tools/tools/create-complete-workflow.tool.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/create-complete-workflow.tool.ts @@ -1,5 +1,6 @@ import { workflowActionSchema, + WorkflowActionType, workflowTriggerSchema, } from 'twenty-shared/workflow'; import { v4 as uuidv4 } from 'uuid'; @@ -99,6 +100,7 @@ Common mistakes to avoid: - Missing the "objectRecord" field in CREATE_RECORD actions - Using "fieldsToUpdate" instead of "objectRecord" in CREATE_RECORD actions - Including CODE steps in this tool — this tool does NOT create the underlying logic function needed by CODE steps. Instead, create the workflow without CODE steps first, then add CODE steps individually using create_workflow_version_step (which properly creates the logic function), then call update_logic_function_source to define the code. +- Including AI_AGENT steps in this tool — this tool does NOT create the underlying agent needed by AI_AGENT steps. Instead, create the workflow without AI_AGENT steps first, then add AI_AGENT steps individually using create_workflow_version_step (which properly creates the agent), then call update_agent to configure the agent. IMPORTANT: The tool schema provides comprehensive field descriptions, examples, and validation rules. Always refer to the schema for: - Field requirements and data types @@ -134,6 +136,17 @@ The response includes a compact validation summary. For the full validation repo WorkflowVersionStepExceptionCode.INVALID_REQUEST, ); } + + const aiAgentSteps = parameters.steps.filter( + (step) => step.type === WorkflowActionType.AI_AGENT, + ); + + if (aiAgentSteps.length > 0) { + throw new WorkflowVersionStepException( + 'AI_AGENT steps cannot be created via create_complete_workflow because it does not create the underlying agent. Use create_workflow_version_step instead, then call update_agent to configure the agent.', + WorkflowVersionStepExceptionCode.INVALID_REQUEST, + ); + } const workflowId = await createWorkflow({ deps, context, diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/tools/create-workflow-version-step.tool.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/create-workflow-version-step.tool.ts index eeb354dae3..169bfd1995 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-tools/tools/create-workflow-version-step.tool.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/create-workflow-version-step.tool.ts @@ -86,6 +86,12 @@ const enrichResultWithNextStep = ({ nextStep: 'This CODE step was created with a default placeholder function. You MUST now call update_logic_function_source with the logicFunctionId from this step to define the actual code. IMPORTANT: Also provide outputSchema (an example return value, e.g. { datePlus7: "2026-06-16" }) so downstream steps can reference this step\'s output variables via {{stepId.fieldName}}.', }; + case WorkflowActionType.AI_AGENT: + return { + ...result, + nextStep: + 'This AI_AGENT step was created with a default placeholder agent. You MUST now call update_agent with the agentId from this step\'s settings.input.agentId to set the agent\'s system prompt (and optionally its model and responseFormat). Use responseFormat { type: "json", schema: { ... } } when downstream steps need to reference structured fields via {{stepId.fieldName}}, otherwise the output is referenced as {{stepId.response}}. If the step needs a task-specific prompt, also set it via update_workflow_version_step on settings.input.prompt.', + }; default: return result; } diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/tools/update-agent.tool.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/update-agent.tool.ts new file mode 100644 index 0000000000..179d3a30ae --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/update-agent.tool.ts @@ -0,0 +1,104 @@ +import { isDefined } from 'twenty-shared/utils'; +import { z } from 'zod'; + +import { type AgentResponseFormat } from 'src/engine/metadata-modules/ai/ai-agent/types/agent-response-format.type'; +import { type ModelId } from 'src/engine/metadata-modules/ai/ai-models/types/model-id.type'; +import { + type WorkflowToolContext, + type WorkflowToolDependencies, +} from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type'; + +const agentResponseFormatSchema = z.union([ + z.object({ type: z.literal('text') }), + z.object({ + type: z.literal('json'), + schema: z.object({ + type: z.literal('object'), + properties: z.record( + z.string(), + z.object({ + type: z.enum(['string', 'number', 'boolean']), + description: z.string().optional(), + }), + ), + required: z.array(z.string()).optional(), + additionalProperties: z.literal(false).optional(), + }), + }), +]); + +const updateAgentSchema = z.object({ + agentId: z + .string() + .uuid() + .describe( + "The ID of the agent to update (from the AI_AGENT step's settings.input.agentId)", + ), + prompt: z + .string() + .optional() + .describe( + "The agent's system prompt describing its role, behavior and the task it must accomplish.", + ), + modelId: z + .string() + .optional() + .describe( + 'Optional model id to use for the agent. Leave empty to keep the auto-selected model.', + ), + responseFormat: agentResponseFormatSchema + .optional() + .describe( + 'Optional response format. Use { type: "text" } for free-form text output, or { type: "json", schema: { type: "object", properties: { fieldName: { type: "string" } } } } for structured output. Downstream steps can reference structured fields via {{stepId.fieldName}} (or {{stepId.response}} for text format).', + ), +}); + +export const createUpdateAgentTool = ( + deps: Pick, + context: WorkflowToolContext, +) => ({ + name: 'update_agent' as const, + description: `Update the AI agent used by a workflow AI_AGENT step. + +Use this tool to configure the agent created when an AI_AGENT step is added: set its system prompt, the model it should use, and the format of its output. + +- prompt: the agent's system prompt (its role, behavior and task). +- modelId: optional model id; omit to keep the auto-selected model. +- responseFormat: { type: "text" } for free-form text (referenced as {{stepId.response}}), or { type: "json", schema: { ... } } for structured output whose fields can be referenced as {{stepId.fieldName}}. + +To find the agentId, look at the AI_AGENT step's settings.input.agentId field.`, + inputSchema: updateAgentSchema, + execute: async (parameters: { + agentId: string; + prompt?: string; + modelId?: string; + responseFormat?: AgentResponseFormat; + }) => { + try { + const { agentId, prompt, modelId, responseFormat } = parameters; + const { workspaceId } = context; + + const updatedAgent = await deps.agentService.updateOneAgent({ + input: { + id: agentId, + ...(isDefined(prompt) ? { prompt } : {}), + ...(isDefined(modelId) ? { modelId: modelId as ModelId } : {}), + ...(isDefined(responseFormat) ? { responseFormat } : {}), + }, + workspaceId, + }); + + return { + success: true, + message: `Successfully updated agent ${agentId}`, + agentId: updatedAgent.id, + }; + } catch (error) { + return { + success: false, + error: error.message, + message: `Failed to update agent: ${error.message}`, + }; + } + }, +}); diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type.ts index ae71996652..67c75eeac7 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type.ts @@ -1,4 +1,5 @@ import type { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service'; +import type { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.service'; import type { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service'; import type { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import type { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; @@ -22,6 +23,7 @@ export type WorkflowToolDependencies = { recordPositionService: RecordPositionService; logicFunctionFromSourceService: LogicFunctionFromSourceService; flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService; + agentService: AgentService; }; export type WorkflowToolContext = { diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/workflow-tools.module.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/workflow-tools.module.ts index bb926a6c01..95fcf83fef 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-tools/workflow-tools.module.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/workflow-tools.module.ts @@ -2,6 +2,7 @@ import { Global, Module } from '@nestjs/common'; import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module'; import { WORKFLOW_TOOL_SERVICE_TOKEN } from 'src/engine/core-modules/tool-provider/constants/workflow-tool-service.token'; +import { AiAgentModule } from 'src/engine/metadata-modules/ai/ai-agent/ai-agent.module'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module'; import { WorkflowSchemaModule } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.module'; @@ -27,6 +28,7 @@ import { WorkflowToolWorkspaceService } from './services/workflow-tool.workspace RecordPositionModule, LogicFunctionModule, WorkspaceManyOrAllFlatEntityMapsCacheModule, + AiAgentModule, ], providers: [ WorkflowToolWorkspaceService,