From de044f4b454bdee7bd2e07ed49c327600e5252e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Fri, 22 May 2026 17:27:06 +0200 Subject: [PATCH] feat(ai-chat): add navigation menu item + webhook tool providers (#20759) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Exposes two Twenty primitives to the AI chat that it could not previously manage: - **Navigation menu items** — workspace nav and personal favorites (favorites are just nav items with `scope: 'user'`). - **Webhooks** — full CRUD with a structured operations input (record + metadata events). Page layouts and workflow runs were originally in this PR but have been split out — they touch heavier surfaces (21 widget configurations and the workflow runner cycle, respectively) and deserve their own focused PRs. ### Tool inventory (8 new tools across 2 providers) | Provider | Tools | |---|---| | NavigationMenuItem | `list_`, `create_`, `update_`, `delete_navigation_menu_item` | | Webhook | `list_`, `create_`, `update_`, `delete_webhook` | ### Design notes - Both providers follow the established **view-style pattern**: tool workspace service lives in the entity module's `tools/` folder, is provided + exported by the entity module, and `ToolProviderModule` imports the entity module. No `@Global()` modules or injection tokens introduced. - `create_navigation_menu_item` uses a Zod `discriminatedUnion` on `type` (`FOLDER` / `LINK` / `OBJECT` / `VIEW` / `RECORD` / `PAGE_LAYOUT`). `scope: 'workspace' | 'user'` switches between shared nav and personal favorites — the underlying `NavigationMenuItemAccessService` enforces LAYOUTS for workspace writes. - Webhook operations accept both record events (`{kind:'record', object, event}` → `.`) and metadata events (`{kind:'metadata', metadataName, operation}` → `metadata..`). - Permissions reuse existing flags (`LAYOUTS`, `API_KEYS_AND_WEBHOOKS`). No new permission flags, no migrations. ### Category cleanup - New: `ToolCategory.NAVIGATION_MENU_ITEM`, `ToolCategory.WEBHOOK`. - `ToolCategory.VIEW_FIELD` → folded into `VIEW`. Same permission gate, same domain — separate category was organizational drift. - `navigate_app` action stays in `ToolCategory.ACTION` where it belongs. ### System prompt addition [chat-system-prompts.const.ts](packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/constants/chat-system-prompts.const.ts) now teaches the AI: - Favorites are nav items with `scope: 'user'`. - A default OBJECT nav item is auto-created with `create_object_metadata` — don't double-create. ### One file = one export Every new schema / type / util file has exactly one top-level export. ## Test plan - [ ] `npx nx typecheck twenty-server` — passes - [ ] Spin up locally and exercise via AI chat: - [ ] "Pin the Companies view to my favorites in a folder called Important." → `create_navigation_menu_item` (FOLDER, user) then (VIEW, user, folderId) - [ ] "Register a webhook to https://example.com firing when any person is created or updated." → `create_webhook` with discriminated operations - [ ] Verify workspace-scoped nav writes are denied for a user without LAYOUTS permission - [ ] Verify user-scoped nav writes work without LAYOUTS permission ## Follow-ups (separate PRs) - Page layout tools (record-page, record-index, standalone) — needs widget-config strategy. - Workflow run tools (list, get, run, stop) — uses the workflow-runner cycle path. - Dashboard / page-layout tool unification — `DashboardToolWorkspaceService` and a future `PageLayoutToolWorkspaceService` both inject the same trio (PageLayout/Tab/Widget services). - Webhook Settings page reads from raw Apollo query — switch to the metadata store so it refreshes when the AI mutates webhooks. --- .../src/metadata/generated/schema.graphql | 80 ++-- .../src/metadata/generated/schema.ts | 110 ++--- .../src/metadata/generated/types.ts | 442 +++++++++--------- .../navigation-menu-item-tool.provider.ts | 58 +++ .../providers/view-tool.provider.ts | 4 + ...l.provider.ts => webhook-tool.provider.ts} | 59 +-- .../tool-provider/tool-provider.module.ts | 32 +- .../constants/chat-system-prompts.const.ts | 5 + .../services/system-prompt-builder.service.ts | 8 +- .../navigation-menu-item.module.ts | 3 + .../navigation-menu-item.service.ts | 44 +- .../tools/create-navigation-menu-item.tool.ts | 197 ++++++++ .../tools/delete-navigation-menu-item.tool.ts | 48 ++ .../tools/list-navigation-menu-items.tool.ts | 92 ++++ .../navigation-menu-item-scope.schema.ts | 9 + .../navigation-menu-item-type.schema.ts | 11 + ...gation-menu-item-tool.workspace-service.ts | 50 ++ .../navigation-menu-item-tool-context.type.ts | 4 + ...gation-menu-item-tool-dependencies.type.ts | 5 + .../tools/update-navigation-menu-item.tool.ts | 93 ++++ .../webhook/tools/create-webhook.tool.ts | 75 +++ .../webhook/tools/delete-webhook.tool.ts | 41 ++ .../webhook/tools/list-webhooks.tool.ts | 44 ++ .../tools/schemas/webhook-operation.schema.ts | 45 ++ .../webhook-tool.workspace-service.ts | 35 ++ .../tools/types/webhook-tool-context.type.ts | 3 + .../types/webhook-tool-dependencies.type.ts | 5 + .../webhook/tools/update-webhook.tool.ts | 78 ++++ .../utils/compile-webhook-operations.util.ts | 14 + .../webhook/webhook.module.ts | 4 +- .../src/ai/constants/tool-category.const.ts | 3 +- 31 files changed, 1323 insertions(+), 378 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/tool-provider/providers/navigation-menu-item-tool.provider.ts rename packages/twenty-server/src/engine/core-modules/tool-provider/providers/{view-field-tool.provider.ts => webhook-tool.provider.ts} (58%) create mode 100644 packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/create-navigation-menu-item.tool.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/delete-navigation-menu-item.tool.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/list-navigation-menu-items.tool.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/schemas/navigation-menu-item-scope.schema.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/schemas/navigation-menu-item-type.schema.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/services/navigation-menu-item-tool.workspace-service.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/types/navigation-menu-item-tool-context.type.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/types/navigation-menu-item-tool-dependencies.type.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/update-navigation-menu-item.tool.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/webhook/tools/create-webhook.tool.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/webhook/tools/delete-webhook.tool.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/webhook/tools/list-webhooks.tool.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/webhook/tools/schemas/webhook-operation.schema.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/webhook/tools/services/webhook-tool.workspace-service.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/webhook/tools/types/webhook-tool-context.type.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/webhook/tools/types/webhook-tool-dependencies.type.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/webhook/tools/update-webhook.tool.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/webhook/tools/utils/compile-webhook-operations.util.ts diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index bd06ccc5f08..9dfae1e6412 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -2476,6 +2476,18 @@ type ImapSmtpCaldavConnectionSuccess { connectedAccountId: String! } +type Webhook { + id: UUID! + targetUrl: String! + operations: [String!]! + description: String + secret: String! + applicationId: UUID! + createdAt: DateTime! + updatedAt: DateTime! + deletedAt: DateTime +} + type ToolIndexEntry { name: String! description: String! @@ -2902,18 +2914,6 @@ type MinimalMetadata { collectionHashes: [CollectionHash!]! } -type Webhook { - id: UUID! - targetUrl: String! - operations: [String!]! - description: String - secret: String! - applicationId: UUID! - createdAt: DateTime! - updatedAt: DateTime! - deletedAt: DateTime -} - type Query { navigationMenuItems: [NavigationMenuItem!]! navigationMenuItem(id: UUID!): NavigationMenuItem @@ -2982,6 +2982,8 @@ type Query { getRoles: [Role!]! getToolIndex: [ToolIndexEntry!]! getToolInputSchema(toolName: String!): JSON + webhooks: [Webhook!]! + webhook(id: UUID!): Webhook field( """The id of the record to find.""" id: UUID! @@ -2999,8 +3001,6 @@ type Query { myMessageChannels(connectedAccountId: UUID): [MessageChannel!]! myConnectedAccounts: [ConnectedAccountPublicDTO!]! myCalendarChannels(connectedAccountId: UUID): [CalendarChannel!]! - webhooks: [Webhook!]! - webhook(id: UUID!): Webhook minimalMetadata: MinimalMetadata! chatThreads: [AgentChatThread!]! chatThread(id: UUID!): AgentChatThread! @@ -3222,6 +3222,9 @@ type Mutation { upsertRowLevelPermissionPredicates(input: UpsertRowLevelPermissionPredicatesInput!): UpsertRowLevelPermissionPredicatesResult! assignRoleToAgent(agentId: UUID!, roleId: UUID!): Boolean! removeRoleFromAgent(agentId: UUID!): Boolean! + createWebhook(input: CreateWebhookInput!): Webhook! + updateWebhook(input: UpdateWebhookInput!): Webhook! + deleteWebhook(id: UUID!): Webhook! createOneField(input: CreateOneFieldMetadataInput!): Field! updateOneField(input: UpdateOneFieldMetadataInput!): Field! deleteOneField(input: DeleteOneFieldInput!): Field! @@ -3238,9 +3241,6 @@ type Mutation { deleteEmailGroupChannel(id: UUID!): MessageChannel! deleteConnectedAccount(id: UUID!): ConnectedAccountPublicDTO! updateCalendarChannel(input: UpdateCalendarChannelInput!): CalendarChannel! - createWebhook(input: CreateWebhookInput!): Webhook! - updateWebhook(input: UpdateWebhookInput!): Webhook! - deleteWebhook(id: UUID!): Webhook! createChatThread: AgentChatThread! sendChatMessage(threadId: UUID!, text: String!, messageId: UUID!, browsingContext: JSON, modelId: String, fileAttachments: [FileAttachmentInput!]): SendChatMessageResult! stopAgentChatStream(threadId: UUID!): Boolean! @@ -4047,6 +4047,29 @@ input RowLevelPermissionPredicateGroupInput { positionInRowLevelPermissionPredicateGroup: Float } +input CreateWebhookInput { + id: UUID + targetUrl: String! + operations: [String!]! + description: String + secret: String +} + +input UpdateWebhookInput { + """The id of the webhook to update""" + id: UUID! + + """The webhook fields to update""" + update: UpdateWebhookInputUpdates! +} + +input UpdateWebhookInputUpdates { + targetUrl: String + operations: [String!] + description: String + secret: String +} + input CreateOneFieldMetadataInput { """The record to create""" field: CreateFieldInput! @@ -4184,29 +4207,6 @@ input UpdateCalendarChannelInputUpdates { isSyncEnabled: Boolean } -input CreateWebhookInput { - id: UUID - targetUrl: String! - operations: [String!]! - description: String - secret: String -} - -input UpdateWebhookInput { - """The id of the webhook to update""" - id: UUID! - - """The webhook fields to update""" - update: UpdateWebhookInputUpdates! -} - -input UpdateWebhookInputUpdates { - targetUrl: String - operations: [String!] - description: String - secret: String -} - input FileAttachmentInput { id: UUID! filename: String! diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index 54343e6f719..70080a01e7f 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -2160,6 +2160,19 @@ export interface ImapSmtpCaldavConnectionSuccess { __typename: 'ImapSmtpCaldavConnectionSuccess' } +export interface Webhook { + id: Scalars['UUID'] + targetUrl: Scalars['String'] + operations: Scalars['String'][] + description?: Scalars['String'] + secret: Scalars['String'] + applicationId: Scalars['UUID'] + createdAt: Scalars['DateTime'] + updatedAt: Scalars['DateTime'] + deletedAt?: Scalars['DateTime'] + __typename: 'Webhook' +} + export interface ToolIndexEntry { name: Scalars['String'] description: Scalars['String'] @@ -2528,19 +2541,6 @@ export interface MinimalMetadata { __typename: 'MinimalMetadata' } -export interface Webhook { - id: Scalars['UUID'] - targetUrl: Scalars['String'] - operations: Scalars['String'][] - description?: Scalars['String'] - secret: Scalars['String'] - applicationId: Scalars['UUID'] - createdAt: Scalars['DateTime'] - updatedAt: Scalars['DateTime'] - deletedAt?: Scalars['DateTime'] - __typename: 'Webhook' -} - export interface Query { navigationMenuItems: NavigationMenuItem[] navigationMenuItem?: NavigationMenuItem @@ -2591,6 +2591,8 @@ export interface Query { getRoles: Role[] getToolIndex: ToolIndexEntry[] getToolInputSchema?: Scalars['JSON'] + webhooks: Webhook[] + webhook?: Webhook field: Field fields: FieldConnection getViewGroups: ViewGroup[] @@ -2599,8 +2601,6 @@ export interface Query { myMessageChannels: MessageChannel[] myConnectedAccounts: ConnectedAccountPublicDTO[] myCalendarChannels: CalendarChannel[] - webhooks: Webhook[] - webhook?: Webhook minimalMetadata: MinimalMetadata chatThreads: AgentChatThread[] chatThread: AgentChatThread @@ -2755,6 +2755,9 @@ export interface Mutation { upsertRowLevelPermissionPredicates: UpsertRowLevelPermissionPredicatesResult assignRoleToAgent: Scalars['Boolean'] removeRoleFromAgent: Scalars['Boolean'] + createWebhook: Webhook + updateWebhook: Webhook + deleteWebhook: Webhook createOneField: Field updateOneField: Field deleteOneField: Field @@ -2771,9 +2774,6 @@ export interface Mutation { deleteEmailGroupChannel: MessageChannel deleteConnectedAccount: ConnectedAccountPublicDTO updateCalendarChannel: CalendarChannel - createWebhook: Webhook - updateWebhook: Webhook - deleteWebhook: Webhook createChatThread: AgentChatThread sendChatMessage: SendChatMessageResult stopAgentChatStream: Scalars['Boolean'] @@ -5166,6 +5166,20 @@ export interface ImapSmtpCaldavConnectionSuccessGenqlSelection{ __scalar?: boolean | number } +export interface WebhookGenqlSelection{ + id?: boolean | number + targetUrl?: boolean | number + operations?: boolean | number + description?: boolean | number + secret?: boolean | number + applicationId?: boolean | number + createdAt?: boolean | number + updatedAt?: boolean | number + deletedAt?: boolean | number + __typename?: boolean | number + __scalar?: boolean | number +} + export interface ToolIndexEntryGenqlSelection{ name?: boolean | number description?: boolean | number @@ -5541,20 +5555,6 @@ export interface MinimalMetadataGenqlSelection{ __scalar?: boolean | number } -export interface WebhookGenqlSelection{ - id?: boolean | number - targetUrl?: boolean | number - operations?: boolean | number - description?: boolean | number - secret?: boolean | number - applicationId?: boolean | number - createdAt?: boolean | number - updatedAt?: boolean | number - deletedAt?: boolean | number - __typename?: boolean | number - __scalar?: boolean | number -} - export interface QueryGenqlSelection{ navigationMenuItems?: NavigationMenuItemGenqlSelection navigationMenuItem?: (NavigationMenuItemGenqlSelection & { __args: {id: Scalars['UUID']} }) @@ -5617,6 +5617,8 @@ export interface QueryGenqlSelection{ getRoles?: RoleGenqlSelection getToolIndex?: ToolIndexEntryGenqlSelection getToolInputSchema?: { __args: {toolName: Scalars['String']} } + webhooks?: WebhookGenqlSelection + webhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} }) field?: (FieldGenqlSelection & { __args: { /** The id of the record to find. */ id: Scalars['UUID']} }) @@ -5631,8 +5633,6 @@ export interface QueryGenqlSelection{ myMessageChannels?: (MessageChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} }) myConnectedAccounts?: ConnectedAccountPublicDTOGenqlSelection myCalendarChannels?: (CalendarChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} }) - webhooks?: WebhookGenqlSelection - webhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} }) minimalMetadata?: MinimalMetadataGenqlSelection chatThreads?: AgentChatThreadGenqlSelection chatThread?: (AgentChatThreadGenqlSelection & { __args: {id: Scalars['UUID']} }) @@ -5808,6 +5808,9 @@ export interface MutationGenqlSelection{ upsertRowLevelPermissionPredicates?: (UpsertRowLevelPermissionPredicatesResultGenqlSelection & { __args: {input: UpsertRowLevelPermissionPredicatesInput} }) assignRoleToAgent?: { __args: {agentId: Scalars['UUID'], roleId: Scalars['UUID']} } removeRoleFromAgent?: { __args: {agentId: Scalars['UUID']} } + createWebhook?: (WebhookGenqlSelection & { __args: {input: CreateWebhookInput} }) + updateWebhook?: (WebhookGenqlSelection & { __args: {input: UpdateWebhookInput} }) + deleteWebhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} }) createOneField?: (FieldGenqlSelection & { __args: {input: CreateOneFieldMetadataInput} }) updateOneField?: (FieldGenqlSelection & { __args: {input: UpdateOneFieldMetadataInput} }) deleteOneField?: (FieldGenqlSelection & { __args: {input: DeleteOneFieldInput} }) @@ -5824,9 +5827,6 @@ export interface MutationGenqlSelection{ deleteEmailGroupChannel?: (MessageChannelGenqlSelection & { __args: {id: Scalars['UUID']} }) deleteConnectedAccount?: (ConnectedAccountPublicDTOGenqlSelection & { __args: {id: Scalars['UUID']} }) updateCalendarChannel?: (CalendarChannelGenqlSelection & { __args: {input: UpdateCalendarChannelInput} }) - createWebhook?: (WebhookGenqlSelection & { __args: {input: CreateWebhookInput} }) - updateWebhook?: (WebhookGenqlSelection & { __args: {input: UpdateWebhookInput} }) - deleteWebhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} }) createChatThread?: AgentChatThreadGenqlSelection sendChatMessage?: (SendChatMessageResultGenqlSelection & { __args: {threadId: Scalars['UUID'], text: Scalars['String'], messageId: Scalars['UUID'], browsingContext?: (Scalars['JSON'] | null), modelId?: (Scalars['String'] | null), fileAttachments?: (FileAttachmentInput[] | null)} }) stopAgentChatStream?: { __args: {threadId: Scalars['UUID']} } @@ -6154,6 +6154,16 @@ export interface RowLevelPermissionPredicateInput {id?: (Scalars['UUID'] | null) export interface RowLevelPermissionPredicateGroupInput {id?: (Scalars['UUID'] | null),objectMetadataId: Scalars['UUID'],parentRowLevelPermissionPredicateGroupId?: (Scalars['UUID'] | null),logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator,positionInRowLevelPermissionPredicateGroup?: (Scalars['Float'] | null)} +export interface CreateWebhookInput {id?: (Scalars['UUID'] | null),targetUrl: Scalars['String'],operations: Scalars['String'][],description?: (Scalars['String'] | null),secret?: (Scalars['String'] | null)} + +export interface UpdateWebhookInput { +/** The id of the webhook to update */ +id: Scalars['UUID'], +/** The webhook fields to update */ +update: UpdateWebhookInputUpdates} + +export interface UpdateWebhookInputUpdates {targetUrl?: (Scalars['String'] | null),operations?: (Scalars['String'][] | null),description?: (Scalars['String'] | null),secret?: (Scalars['String'] | null)} + export interface CreateOneFieldMetadataInput { /** The record to create */ field: CreateFieldInput} @@ -6206,16 +6216,6 @@ export interface UpdateCalendarChannelInput {id: Scalars['UUID'],update: UpdateC export interface UpdateCalendarChannelInputUpdates {visibility?: (CalendarChannelVisibility | null),isContactAutoCreationEnabled?: (Scalars['Boolean'] | null),contactAutoCreationPolicy?: (CalendarChannelContactAutoCreationPolicy | null),isSyncEnabled?: (Scalars['Boolean'] | null)} -export interface CreateWebhookInput {id?: (Scalars['UUID'] | null),targetUrl: Scalars['String'],operations: Scalars['String'][],description?: (Scalars['String'] | null),secret?: (Scalars['String'] | null)} - -export interface UpdateWebhookInput { -/** The id of the webhook to update */ -id: Scalars['UUID'], -/** The webhook fields to update */ -update: UpdateWebhookInputUpdates} - -export interface UpdateWebhookInputUpdates {targetUrl?: (Scalars['String'] | null),operations?: (Scalars['String'][] | null),description?: (Scalars['String'] | null),secret?: (Scalars['String'] | null)} - export interface FileAttachmentInput {id: Scalars['UUID'],filename: Scalars['String']} export interface CreateSkillInput {id?: (Scalars['UUID'] | null),name: Scalars['String'],label: Scalars['String'],icon?: (Scalars['String'] | null),description?: (Scalars['String'] | null),content: Scalars['String']} @@ -7937,6 +7937,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null + const Webhook_possibleTypes: string[] = ['Webhook'] + export const isWebhook = (obj?: { __typename?: any } | null): obj is Webhook => { + if (!obj?.__typename) throw new Error('__typename is missing in "isWebhook"') + return Webhook_possibleTypes.includes(obj.__typename) + } + + + const ToolIndexEntry_possibleTypes: string[] = ['ToolIndexEntry'] export const isToolIndexEntry = (obj?: { __typename?: any } | null): obj is ToolIndexEntry => { if (!obj?.__typename) throw new Error('__typename is missing in "isToolIndexEntry"') @@ -8201,14 +8209,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null - const Webhook_possibleTypes: string[] = ['Webhook'] - export const isWebhook = (obj?: { __typename?: any } | null): obj is Webhook => { - if (!obj?.__typename) throw new Error('__typename is missing in "isWebhook"') - return Webhook_possibleTypes.includes(obj.__typename) - } - - - const Query_possibleTypes: string[] = ['Query'] export const isQuery = (obj?: { __typename?: any } | null): obj is Query => { if (!obj?.__typename) throw new Error('__typename is missing in "isQuery"') diff --git a/packages/twenty-client-sdk/src/metadata/generated/types.ts b/packages/twenty-client-sdk/src/metadata/generated/types.ts index a5af84f43bd..bfff3b4d4ff 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/types.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/types.ts @@ -60,20 +60,20 @@ export default { 226, 262, 263, - 292, - 301, + 293, 302, 303, 304, - 306, + 305, 307, 308, 309, 310, 311, 312, - 315, - 317, + 313, + 316, + 318, 327, 334, 341, @@ -4912,6 +4912,38 @@ export default { 1 ] }, + "Webhook": { + "id": [ + 3 + ], + "targetUrl": [ + 1 + ], + "operations": [ + 1 + ], + "description": [ + 1 + ], + "secret": [ + 1 + ], + "applicationId": [ + 3 + ], + "createdAt": [ + 4 + ], + "updatedAt": [ + 4 + ], + "deletedAt": [ + 4 + ], + "__typename": [ + 1 + ] + }, "ToolIndexEntry": { "name": [ 1 @@ -5051,7 +5083,7 @@ export default { 1 ], "series": [ - 277 + 278 ], "xAxisLabel": [ 1 @@ -5100,7 +5132,7 @@ export default { 1 ], "data": [ - 279 + 280 ], "__typename": [ 1 @@ -5108,7 +5140,7 @@ export default { }, "LineChartData": { "series": [ - 280 + 281 ], "xAxisLabel": [ 1 @@ -5145,7 +5177,7 @@ export default { }, "PieChartData": { "data": [ - 282 + 283 ], "showLegend": [ 6 @@ -5239,13 +5271,13 @@ export default { }, "EventLogQueryResult": { "records": [ - 286 + 287 ], "totalCount": [ 21 ], "pageInfo": [ - 287 + 288 ], "__typename": [ 1 @@ -5309,7 +5341,7 @@ export default { 1 ], "parts": [ - 275 + 276 ], "processedAt": [ 4 @@ -5323,7 +5355,7 @@ export default { }, "AgentChatThread": { "id": [ - 292 + 293 ], "title": [ 1 @@ -5379,7 +5411,7 @@ export default { }, "AiSystemPromptPreview": { "sections": [ - 293 + 294 ], "estimatedTokenCount": [ 21 @@ -5455,10 +5487,10 @@ export default { 3 ], "evaluations": [ - 298 + 299 ], "messages": [ - 290 + 291 ], "createdAt": [ 4 @@ -5475,19 +5507,19 @@ export default { 1 ], "syncStatus": [ - 301 - ], - "syncStage": [ 302 ], - "visibility": [ + "syncStage": [ 303 ], + "visibility": [ + 304 + ], "isContactAutoCreationEnabled": [ 6 ], "contactAutoCreationPolicy": [ - 304 + 305 ], "isSyncEnabled": [ 6 @@ -5523,22 +5555,22 @@ export default { 3 ], "visibility": [ - 306 + 307 ], "handle": [ 1 ], "type": [ - 307 + 308 ], "isContactAutoCreationEnabled": [ 6 ], "contactAutoCreationPolicy": [ - 308 + 309 ], "messageFolderImportPolicy": [ - 309 + 310 ], "excludeNonProfessionalEmails": [ 6 @@ -5547,7 +5579,7 @@ export default { 6 ], "pendingGroupEmailsAction": [ - 310 + 311 ], "isSyncEnabled": [ 6 @@ -5556,10 +5588,10 @@ export default { 4 ], "syncStatus": [ - 311 + 312 ], "syncStage": [ - 312 + 313 ], "syncStageStartedAt": [ 4 @@ -5595,7 +5627,7 @@ export default { "MessageChannelSyncStage": {}, "CreateEmailGroupChannelOutput": { "messageChannel": [ - 305 + 306 ], "forwardingAddress": [ 1 @@ -5624,7 +5656,7 @@ export default { 1 ], "pendingSyncAction": [ - 315 + 316 ], "messageChannelId": [ 3 @@ -5642,7 +5674,7 @@ export default { "MessageFolderPendingSyncAction": {}, "CollectionHash": { "collectionName": [ - 317 + 318 ], "hash": [ 1 @@ -5709,45 +5741,13 @@ export default { }, "MinimalMetadata": { "objectMetadataItems": [ - 318 - ], - "views": [ 319 ], + "views": [ + 320 + ], "collectionHashes": [ - 316 - ], - "__typename": [ - 1 - ] - }, - "Webhook": { - "id": [ - 3 - ], - "targetUrl": [ - 1 - ], - "operations": [ - 1 - ], - "description": [ - 1 - ], - "secret": [ - 1 - ], - "applicationId": [ - 3 - ], - "createdAt": [ - 4 - ], - "updatedAt": [ - 4 - ], - "deletedAt": [ - 4 + 317 ], "__typename": [ 1 @@ -6107,7 +6107,7 @@ export default { 29 ], "getToolIndex": [ - 274 + 275 ], "getToolInputSchema": [ 15, @@ -6118,6 +6118,18 @@ export default { ] } ], + "webhooks": [ + 274 + ], + "webhook": [ + 274, + { + "id": [ + 3, + "UUID!" + ] + } + ], "field": [ 43, { @@ -6158,7 +6170,7 @@ export default { } ], "myMessageFolders": [ - 314, + 315, { "messageChannelId": [ 3 @@ -6166,7 +6178,7 @@ export default { } ], "myMessageChannels": [ - 305, + 306, { "connectedAccountId": [ 3 @@ -6177,33 +6189,21 @@ export default { 269 ], "myCalendarChannels": [ - 300, + 301, { "connectedAccountId": [ 3 ] } ], - "webhooks": [ + "minimalMetadata": [ 321 ], - "webhook": [ - 321, - { - "id": [ - 3, - "UUID!" - ] - } - ], - "minimalMetadata": [ - 320 - ], "chatThreads": [ - 291 + 292 ], "chatThread": [ - 291, + 292, { "id": [ 3, @@ -6212,7 +6212,7 @@ export default { } ], "chatMessages": [ - 290, + 291, { "threadId": [ 3, @@ -6221,7 +6221,7 @@ export default { } ], "chatStreamCatchupChunks": [ - 295, + 296, { "threadId": [ 3, @@ -6230,13 +6230,13 @@ export default { } ], "getAiSystemPromptPreview": [ - 294 + 295 ], "skills": [ - 289 + 290 ], "skill": [ - 289, + 290, { "id": [ 3, @@ -6245,7 +6245,7 @@ export default { } ], "agentTurns": [ - 299, + 300, { "agentId": [ 3, @@ -6390,7 +6390,7 @@ export default { 219 ], "eventLogs": [ - 288, + 289, { "input": [ 326, @@ -6399,7 +6399,7 @@ export default { } ], "pieChartData": [ - 283, + 284, { "input": [ 330, @@ -6408,7 +6408,7 @@ export default { } ], "lineChartData": [ - 281, + 282, { "input": [ 331, @@ -6417,7 +6417,7 @@ export default { } ], "barChartData": [ - 278, + 279, { "input": [ 332, @@ -6506,7 +6506,7 @@ export default { }, "LogicFunctionIdInput": { "id": [ - 292 + 293 ], "__typename": [ 1 @@ -7616,11 +7616,38 @@ export default { ] } ], + "createWebhook": [ + 274, + { + "input": [ + 418, + "CreateWebhookInput!" + ] + } + ], + "updateWebhook": [ + 274, + { + "input": [ + 419, + "UpdateWebhookInput!" + ] + } + ], + "deleteWebhook": [ + 274, + { + "id": [ + 3, + "UUID!" + ] + } + ], "createOneField": [ 43, { "input": [ - 418, + 421, "CreateOneFieldMetadataInput!" ] } @@ -7629,7 +7656,7 @@ export default { 43, { "input": [ - 420, + 423, "UpdateOneFieldMetadataInput!" ] } @@ -7638,7 +7665,7 @@ export default { 43, { "input": [ - 422, + 425, "DeleteOneFieldInput!" ] } @@ -7647,7 +7674,7 @@ export default { 65, { "input": [ - 423, + 426, "CreateViewGroupInput!" ] } @@ -7656,7 +7683,7 @@ export default { 65, { "inputs": [ - 423, + 426, "[CreateViewGroupInput!]!" ] } @@ -7665,7 +7692,7 @@ export default { 65, { "input": [ - 424, + 427, "UpdateViewGroupInput!" ] } @@ -7674,7 +7701,7 @@ export default { 65, { "inputs": [ - 424, + 427, "[UpdateViewGroupInput!]!" ] } @@ -7683,7 +7710,7 @@ export default { 65, { "input": [ - 426, + 429, "DeleteViewGroupInput!" ] } @@ -7692,49 +7719,49 @@ export default { 65, { "input": [ - 427, + 430, "DestroyViewGroupInput!" ] } ], "updateMessageFolder": [ - 314, + 315, { "input": [ - 428, + 431, "UpdateMessageFolderInput!" ] } ], "updateMessageFolders": [ - 314, + 315, { "input": [ - 430, + 433, "UpdateMessageFoldersInput!" ] } ], "updateMessageChannel": [ - 305, + 306, { "input": [ - 431, + 434, "UpdateMessageChannelInput!" ] } ], "createEmailGroupChannel": [ - 313, + 314, { "input": [ - 433, + 436, "CreateEmailGroupChannelInput!" ] } ], "deleteEmailGroupChannel": [ - 305, + 306, { "id": [ 3, @@ -7752,46 +7779,19 @@ export default { } ], "updateCalendarChannel": [ - 300, + 301, { "input": [ - 434, + 437, "UpdateCalendarChannelInput!" ] } ], - "createWebhook": [ - 321, - { - "input": [ - 436, - "CreateWebhookInput!" - ] - } - ], - "updateWebhook": [ - 321, - { - "input": [ - 437, - "UpdateWebhookInput!" - ] - } - ], - "deleteWebhook": [ - 321, - { - "id": [ - 3, - "UUID!" - ] - } - ], "createChatThread": [ - 291 + 292 ], "sendChatMessage": [ - 296, + 297, { "threadId": [ 3, @@ -7827,7 +7827,7 @@ export default { } ], "renameChatThread": [ - 291, + 292, { "id": [ 3, @@ -7840,7 +7840,7 @@ export default { } ], "archiveChatThread": [ - 291, + 292, { "id": [ 3, @@ -7849,7 +7849,7 @@ export default { } ], "unarchiveChatThread": [ - 291, + 292, { "id": [ 3, @@ -7876,7 +7876,7 @@ export default { } ], "createSkill": [ - 289, + 290, { "input": [ 440, @@ -7885,7 +7885,7 @@ export default { } ], "updateSkill": [ - 289, + 290, { "input": [ 441, @@ -7894,7 +7894,7 @@ export default { } ], "deleteSkill": [ - 289, + 290, { "id": [ 3, @@ -7903,7 +7903,7 @@ export default { } ], "activateSkill": [ - 289, + 290, { "id": [ 3, @@ -7912,7 +7912,7 @@ export default { } ], "deactivateSkill": [ - 289, + 290, { "id": [ 3, @@ -7921,7 +7921,7 @@ export default { } ], "evaluateAgentTurn": [ - 298, + 299, { "turnId": [ 3, @@ -7930,7 +7930,7 @@ export default { } ], "runEvaluationInput": [ - 299, + 300, { "agentId": [ 3, @@ -8443,7 +8443,7 @@ export default { } ], "duplicateDashboard": [ - 284, + 285, { "id": [ 3, @@ -8465,7 +8465,7 @@ export default { } ], "sendEmail": [ - 285, + 286, { "input": [ 459, @@ -8474,7 +8474,7 @@ export default { } ], "startChannelSync": [ - 276, + 277, { "connectedAccountId": [ 3, @@ -10320,9 +10320,57 @@ export default { 1 ] }, + "CreateWebhookInput": { + "id": [ + 3 + ], + "targetUrl": [ + 1 + ], + "operations": [ + 1 + ], + "description": [ + 1 + ], + "secret": [ + 1 + ], + "__typename": [ + 1 + ] + }, + "UpdateWebhookInput": { + "id": [ + 3 + ], + "update": [ + 420 + ], + "__typename": [ + 1 + ] + }, + "UpdateWebhookInputUpdates": { + "targetUrl": [ + 1 + ], + "operations": [ + 1 + ], + "description": [ + 1 + ], + "secret": [ + 1 + ], + "__typename": [ + 1 + ] + }, "CreateOneFieldMetadataInput": { "field": [ - 419 + 422 ], "__typename": [ 1 @@ -10395,7 +10443,7 @@ export default { 3 ], "update": [ - 421 + 424 ], "__typename": [ 1 @@ -10487,7 +10535,7 @@ export default { 3 ], "update": [ - 425 + 428 ], "__typename": [ 1 @@ -10531,7 +10579,7 @@ export default { 3 ], "update": [ - 429 + 432 ], "__typename": [ 1 @@ -10550,7 +10598,7 @@ export default { 3 ], "update": [ - 429 + 432 ], "__typename": [ 1 @@ -10561,7 +10609,7 @@ export default { 3 ], "update": [ - 432 + 435 ], "__typename": [ 1 @@ -10569,16 +10617,16 @@ export default { }, "UpdateMessageChannelInputUpdates": { "visibility": [ - 306 + 307 ], "isContactAutoCreationEnabled": [ 6 ], "contactAutoCreationPolicy": [ - 308 + 309 ], "messageFolderImportPolicy": [ - 309 + 310 ], "isSyncEnabled": [ 6 @@ -10606,7 +10654,7 @@ export default { 3 ], "update": [ - 435 + 438 ], "__typename": [ 1 @@ -10614,13 +10662,13 @@ export default { }, "UpdateCalendarChannelInputUpdates": { "visibility": [ - 303 + 304 ], "isContactAutoCreationEnabled": [ 6 ], "contactAutoCreationPolicy": [ - 304 + 305 ], "isSyncEnabled": [ 6 @@ -10629,54 +10677,6 @@ export default { 1 ] }, - "CreateWebhookInput": { - "id": [ - 3 - ], - "targetUrl": [ - 1 - ], - "operations": [ - 1 - ], - "description": [ - 1 - ], - "secret": [ - 1 - ], - "__typename": [ - 1 - ] - }, - "UpdateWebhookInput": { - "id": [ - 3 - ], - "update": [ - 438 - ], - "__typename": [ - 1 - ] - }, - "UpdateWebhookInputUpdates": { - "targetUrl": [ - 1 - ], - "operations": [ - 1 - ], - "description": [ - 1 - ], - "secret": [ - 1 - ], - "__typename": [ - 1 - ] - }, "FileAttachmentInput": { "id": [ 3 @@ -10947,7 +10947,7 @@ export default { 454 ], "metadataName": [ - 317 + 318 ], "universalIdentifier": [ 1 @@ -11138,7 +11138,7 @@ export default { } ], "onAgentChatEvent": [ - 297, + 298, { "threadId": [ 3, diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/navigation-menu-item-tool.provider.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/navigation-menu-item-tool.provider.ts new file mode 100644 index 00000000000..4dd18a8b7e7 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/navigation-menu-item-tool.provider.ts @@ -0,0 +1,58 @@ +import { Injectable } from '@nestjs/common'; + +import { type ToolSet } from 'ai'; +import { ToolCategory } from 'twenty-shared/ai'; + +import { type GenerateDescriptorOptions } from 'src/engine/core-modules/tool-provider/interfaces/generate-descriptor-options.type'; +import { type ToolProvider } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface'; +import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-context.type'; +import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type'; +import { type ToolIndexEntry } from 'src/engine/core-modules/tool-provider/types/tool-index-entry.type'; +import { executeToolFromToolSet } from 'src/engine/core-modules/tool-provider/utils/execute-tool-from-tool-set.util'; +import { toolSetToDescriptors } from 'src/engine/core-modules/tool-provider/utils/tool-set-to-descriptors.util'; +import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; +import { NavigationMenuItemToolWorkspaceService } from 'src/engine/metadata-modules/navigation-menu-item/tools/services/navigation-menu-item-tool.workspace-service'; + +@Injectable() +export class NavigationMenuItemToolProvider implements ToolProvider { + readonly category = ToolCategory.NAVIGATION_MENU_ITEM; + + constructor( + private readonly navigationMenuItemToolService: NavigationMenuItemToolWorkspaceService, + ) {} + + async isAvailable(_context: ToolProviderContext): Promise { + return true; + } + + async generateDescriptors( + context: ToolProviderContext, + options?: GenerateDescriptorOptions, + ): Promise<(ToolIndexEntry | ToolDescriptor)[]> { + return toolSetToDescriptors( + this.buildToolSet(context), + ToolCategory.NAVIGATION_MENU_ITEM, + { includeSchemas: options?.includeSchemas ?? true }, + ); + } + + async executeStaticTool( + toolName: string, + args: Record, + context: ToolProviderContext, + ): Promise { + return executeToolFromToolSet( + this.buildToolSet(context), + toolName, + args, + ToolCategory.NAVIGATION_MENU_ITEM, + ); + } + + private buildToolSet(context: ToolProviderContext): ToolSet { + return this.navigationMenuItemToolService.generateNavigationMenuItemTools( + context.workspaceId, + context.userWorkspaceId, + ); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/view-tool.provider.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/view-tool.provider.ts index 1ba30588446..3cc8443165a 100644 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/view-tool.provider.ts +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/view-tool.provider.ts @@ -14,6 +14,7 @@ import { executeToolFromToolSet } from 'src/engine/core-modules/tool-provider/ut import { toolSetToDescriptors } from 'src/engine/core-modules/tool-provider/utils/tool-set-to-descriptors.util'; import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; +import { ViewFieldToolsFactory } from 'src/engine/metadata-modules/view-field/tools/view-field-tools.factory'; import { ViewFilterToolsFactory } from 'src/engine/metadata-modules/view-filter/tools/view-filter-tools.factory'; import { ViewSortToolsFactory } from 'src/engine/metadata-modules/view-sort/tools/view-sort-tools.factory'; import { ViewToolsFactory } from 'src/engine/metadata-modules/view/tools/view-tools.factory'; @@ -24,6 +25,7 @@ export class ViewToolProvider implements ToolProvider { constructor( private readonly viewToolsFactory: ViewToolsFactory, + private readonly viewFieldToolsFactory: ViewFieldToolsFactory, private readonly viewFilterToolsFactory: ViewFilterToolsFactory, private readonly viewSortToolsFactory: ViewSortToolsFactory, private readonly permissionsService: PermissionsService, @@ -63,6 +65,7 @@ export class ViewToolProvider implements ToolProvider { workspaceMemberId ?? undefined, workspaceMemberId ?? undefined, ), + ...this.viewFieldToolsFactory.generateReadTools(context.workspaceId), ...this.viewFilterToolsFactory.generateReadTools(context.workspaceId), ...this.viewSortToolsFactory.generateReadTools(context.workspaceId), }; @@ -83,6 +86,7 @@ export class ViewToolProvider implements ToolProvider { context.workspaceId, workspaceMemberId ?? undefined, ), + ...this.viewFieldToolsFactory.generateWriteTools(context.workspaceId), ...this.viewFilterToolsFactory.generateWriteTools(context.workspaceId), ...this.viewSortToolsFactory.generateWriteTools(context.workspaceId), }; diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/view-field-tool.provider.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/webhook-tool.provider.ts similarity index 58% rename from packages/twenty-server/src/engine/core-modules/tool-provider/providers/view-field-tool.provider.ts rename to packages/twenty-server/src/engine/core-modules/tool-provider/providers/webhook-tool.provider.ts index 4b2ac1d90e0..33b73d51fe8 100644 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/view-field-tool.provider.ts +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/webhook-tool.provider.ts @@ -1,44 +1,46 @@ import { Injectable } from '@nestjs/common'; import { type ToolSet } from 'ai'; +import { ToolCategory } from 'twenty-shared/ai'; import { PermissionFlagType } from 'twenty-shared/constants'; import { type GenerateDescriptorOptions } from 'src/engine/core-modules/tool-provider/interfaces/generate-descriptor-options.type'; import { type ToolProvider } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface'; import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-context.type'; - -import { ToolCategory } from 'twenty-shared/ai'; import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type'; import { type ToolIndexEntry } from 'src/engine/core-modules/tool-provider/types/tool-index-entry.type'; import { executeToolFromToolSet } from 'src/engine/core-modules/tool-provider/utils/execute-tool-from-tool-set.util'; import { toolSetToDescriptors } from 'src/engine/core-modules/tool-provider/utils/tool-set-to-descriptors.util'; import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; -import { ViewFieldToolsFactory } from 'src/engine/metadata-modules/view-field/tools/view-field-tools.factory'; +import { WebhookToolWorkspaceService } from 'src/engine/metadata-modules/webhook/tools/services/webhook-tool.workspace-service'; @Injectable() -export class ViewFieldToolProvider implements ToolProvider { - readonly category = ToolCategory.VIEW_FIELD; +export class WebhookToolProvider implements ToolProvider { + readonly category = ToolCategory.WEBHOOK; constructor( - private readonly viewFieldToolsFactory: ViewFieldToolsFactory, + private readonly webhookToolService: WebhookToolWorkspaceService, private readonly permissionsService: PermissionsService, ) {} - async isAvailable(_context: ToolProviderContext): Promise { - return true; + async isAvailable(context: ToolProviderContext): Promise { + return this.permissionsService.checkRolesPermissions( + context.rolePermissionConfig, + context.workspaceId, + PermissionFlagType.API_KEYS_AND_WEBHOOKS, + ); } async generateDescriptors( context: ToolProviderContext, options?: GenerateDescriptorOptions, ): Promise<(ToolIndexEntry | ToolDescriptor)[]> { - const toolSet = await this.buildToolSet(context); - - return toolSetToDescriptors(toolSet, ToolCategory.VIEW_FIELD, { - includeSchemas: options?.includeSchemas ?? true, - icon: 'IconTable', - }); + return toolSetToDescriptors( + this.buildToolSet(context), + ToolCategory.WEBHOOK, + { includeSchemas: options?.includeSchemas ?? true }, + ); } async executeStaticTool( @@ -46,36 +48,15 @@ export class ViewFieldToolProvider implements ToolProvider { args: Record, context: ToolProviderContext, ): Promise { - const toolSet = await this.buildToolSet(context); - return executeToolFromToolSet( - toolSet, + this.buildToolSet(context), toolName, args, - ToolCategory.VIEW_FIELD, + ToolCategory.WEBHOOK, ); } - private async buildToolSet(context: ToolProviderContext): Promise { - const readTools = this.viewFieldToolsFactory.generateReadTools( - context.workspaceId, - ); - - const hasViewPermission = - await this.permissionsService.checkRolesPermissions( - context.rolePermissionConfig, - context.workspaceId, - PermissionFlagType.VIEWS, - ); - - if (!hasViewPermission) { - return readTools; - } - - const writeTools = this.viewFieldToolsFactory.generateWriteTools( - context.workspaceId, - ); - - return { ...readTools, ...writeTools }; + private buildToolSet(context: ToolProviderContext): ToolSet { + return this.webhookToolService.generateWebhookTools(context.workspaceId); } } diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/tool-provider.module.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/tool-provider.module.ts index b5bfd9c60bf..a1031e8f08f 100644 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/tool-provider.module.ts +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/tool-provider.module.ts @@ -9,8 +9,9 @@ import { DatabaseToolProvider } from 'src/engine/core-modules/tool-provider/prov import { LogicFunctionToolProvider } from 'src/engine/core-modules/tool-provider/providers/logic-function-tool.provider'; import { MetadataToolProvider } from 'src/engine/core-modules/tool-provider/providers/metadata-tool.provider'; import { NativeToolBinderService } from 'src/engine/core-modules/tool-provider/native/native-tool-binder.service'; -import { ViewFieldToolProvider } from 'src/engine/core-modules/tool-provider/providers/view-field-tool.provider'; +import { NavigationMenuItemToolProvider } from 'src/engine/core-modules/tool-provider/providers/navigation-menu-item-tool.provider'; import { ViewToolProvider } from 'src/engine/core-modules/tool-provider/providers/view-tool.provider'; +import { WebhookToolProvider } from 'src/engine/core-modules/tool-provider/providers/webhook-tool.provider'; import { WorkflowToolProvider } from 'src/engine/core-modules/tool-provider/providers/workflow-tool.provider'; import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service'; import { ToolModule } from 'src/engine/core-modules/tool/tool.module'; @@ -20,6 +21,7 @@ import { AiModelsModule } from 'src/engine/metadata-modules/ai/ai-models/ai-mode import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.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 { NavigationMenuItemModule } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.module'; import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; @@ -27,15 +29,21 @@ import { ViewFieldModule } from 'src/engine/metadata-modules/view-field/view-fie import { ViewFilterModule } from 'src/engine/metadata-modules/view-filter/view-filter.module'; import { ViewSortModule } from 'src/engine/metadata-modules/view-sort/view-sort.module'; import { ViewModule } from 'src/engine/metadata-modules/view/view.module'; +import { WebhookModule } from 'src/engine/metadata-modules/webhook/webhook.module'; import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; import { ToolIndexResolver } from './resolvers/tool-index.resolver'; import { ToolRegistryService } from './services/tool-registry.service'; -// NOTE: This module does NOT import WorkflowToolsModule or DashboardToolsModule to avoid -// circular dependencies. Instead, they are @Global() modules that provide their tokens. -// When imported anywhere in the app (e.g., AiChatModule), the tokens become available -// globally to their respective providers via @Optional() injection. +// NOTE: This module does NOT import WorkflowToolsModule or DashboardToolsModule +// directly: their service graphs transitively reach AiAgentExecutionModule which +// forwardRef's back into ToolProviderModule. Those two @Global() modules provide +// a service token that their respective providers consume via @Optional() +// @Inject, breaking the cycle. +// +// Webhook and NavigationMenuItem do NOT have that cycle, so we import their +// entity modules directly and the providers inject the services the normal way +// (same pattern as views/objects/metadata). @Module({ imports: [ @@ -53,6 +61,8 @@ import { ToolRegistryService } from './services/tool-registry.service'; WorkspaceCacheModule, WorkspaceManyOrAllFlatEntityMapsCacheModule, LogicFunctionModule, + NavigationMenuItemModule, + WebhookModule, UserRoleModule, TypeOrmModule.forFeature([UserEntity]), ], @@ -64,9 +74,10 @@ import { ToolRegistryService } from './services/tool-registry.service'; DatabaseToolProvider, MetadataToolProvider, NativeToolBinderService, + NavigationMenuItemToolProvider, LogicFunctionToolProvider, - ViewFieldToolProvider, ViewToolProvider, + WebhookToolProvider, WorkflowToolProvider, { // TOOL_PROVIDERS contains only providers implementing ToolProvider @@ -80,8 +91,9 @@ import { ToolRegistryService } from './services/tool-registry.service'; databaseProvider: DatabaseToolProvider, metadataProvider: MetadataToolProvider, logicFunctionProvider: LogicFunctionToolProvider, - viewFieldProvider: ViewFieldToolProvider, + navigationMenuItemProvider: NavigationMenuItemToolProvider, viewProvider: ViewToolProvider, + webhookProvider: WebhookToolProvider, workflowProvider: WorkflowToolProvider, ) => [ actionProvider, @@ -89,8 +101,9 @@ import { ToolRegistryService } from './services/tool-registry.service'; databaseProvider, metadataProvider, logicFunctionProvider, - viewFieldProvider, + navigationMenuItemProvider, viewProvider, + webhookProvider, workflowProvider, ], inject: [ @@ -99,8 +112,9 @@ import { ToolRegistryService } from './services/tool-registry.service'; DatabaseToolProvider, MetadataToolProvider, LogicFunctionToolProvider, - ViewFieldToolProvider, + NavigationMenuItemToolProvider, ViewToolProvider, + WebhookToolProvider, WorkflowToolProvider, ], }, diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/constants/chat-system-prompts.const.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/constants/chat-system-prompts.const.ts index 4fbf9366262..749d453f7cd 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/constants/chat-system-prompts.const.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/constants/chat-system-prompts.const.ts @@ -50,6 +50,11 @@ For simple CRUD operations (find/create/update/delete a record), you do NOT need - If a tool fails, analyze the error, adjust parameters, and try again - Don't give up after first failure — be persistent and try alternative approaches - Validate assumptions before making changes + +## Twenty primitives the AI commonly mixes up + +- **Favorites are navigation menu items.** Twenty has no separate "Favorites" concept. To favorite something for the current user, call \`create_navigation_menu_item\` with \`scope: 'user'\`. Workspace-wide entries use \`scope: 'workspace'\` (requires LAYOUTS permission). Both are the same primitive — do not look for a separate favorites tool. +- **A default OBJECT navigation menu item is auto-created with \`create_object_metadata\`.** Don't immediately create another OBJECT item for the new object — only add a follow-up navigation item when the user is asking to pin a *different* view, folder, link, record, or page layout. `, // Browsing context hint diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/system-prompt-builder.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/system-prompt-builder.service.ts index 8eed08ff8c3..16ade63dab0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/system-prompt-builder.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/system-prompt-builder.service.ts @@ -311,13 +311,15 @@ ${tools case ToolCategory.METADATA: return 'Metadata Tools (schema management)'; case ToolCategory.VIEW: - return 'View Tools (manage views, filters, and sorts)'; + return 'View Tools (manage views, fields, filters, and sorts)'; case ToolCategory.DASHBOARD: return 'Dashboard Tools (create/manage dashboards)'; case ToolCategory.LOGIC_FUNCTION: return 'Logic Functions (custom tools)'; - case ToolCategory.VIEW_FIELD: - return 'View Field Tools (manage view columns)'; + case ToolCategory.NAVIGATION_MENU_ITEM: + return 'Navigation Menu Item Tools (sidebar entries, folders, and user favorites)'; + case ToolCategory.WEBHOOK: + return 'Webhook Tools (outgoing webhooks)'; default: return assertUnreachable(category); } diff --git a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.module.ts b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.module.ts index 4c432b1550a..6decb5a3ee4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.module.ts @@ -12,6 +12,7 @@ import { NavigationMenuItemService } from 'src/engine/metadata-modules/navigatio import { NavigationMenuItemAccessService } from 'src/engine/metadata-modules/navigation-menu-item/services/navigation-menu-item-access.service'; import { NavigationMenuItemDeletionService } from 'src/engine/metadata-modules/navigation-menu-item/services/navigation-menu-item-deletion.service'; import { NavigationMenuItemRecordIdentifierService } from 'src/engine/metadata-modules/navigation-menu-item/services/navigation-menu-item-record-identifier.service'; +import { NavigationMenuItemToolWorkspaceService } from 'src/engine/metadata-modules/navigation-menu-item/tools/services/navigation-menu-item-tool.workspace-service'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor'; import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module'; @@ -35,10 +36,12 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace NavigationMenuItemRecordIdentifierService, NavigationMenuItemGraphqlApiExceptionInterceptor, WorkspaceMigrationGraphqlApiExceptionInterceptor, + NavigationMenuItemToolWorkspaceService, ], exports: [ NavigationMenuItemService, NavigationMenuItemRecordIdentifierService, + NavigationMenuItemToolWorkspaceService, ], }) export class NavigationMenuItemModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.service.ts b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.service.ts index 445ce6d194c..78e350cb362 100644 --- a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.service.ts @@ -44,9 +44,17 @@ export class NavigationMenuItemService { async findAll({ workspaceId, userWorkspaceId, + scope = 'all', + folderId, + type, + limit, }: { workspaceId: string; userWorkspaceId?: string; + scope?: 'all' | 'workspace' | 'user'; + folderId?: string; + type?: NavigationMenuItemType; + limit?: number; }): Promise { const { flatNavigationMenuItemMaps } = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( @@ -56,15 +64,33 @@ export class NavigationMenuItemService { }, ); - return Object.values(flatNavigationMenuItemMaps.byUniversalIdentifier) - .filter( - (item): item is NonNullable => - isDefined(item) && - (!isDefined(item.userWorkspaceId) || - item.userWorkspaceId === userWorkspaceId), - ) - .sort((a, b) => a.position - b.position) - .map(fromFlatNavigationMenuItemToNavigationMenuItemDto); + const filtered = Object.values( + flatNavigationMenuItemMaps.byUniversalIdentifier, + ) + .filter((item): item is NonNullable => { + if (!isDefined(item)) return false; + + const itemIsUserScoped = isDefined(item.userWorkspaceId); + + // Default scope returns workspace items + the caller's own user items. + if (scope === 'all') { + return !itemIsUserScoped || item.userWorkspaceId === userWorkspaceId; + } + + if (scope === 'workspace') { + return !itemIsUserScoped; + } + + // scope === 'user' + return itemIsUserScoped && item.userWorkspaceId === userWorkspaceId; + }) + .filter((item) => !isDefined(folderId) || item.folderId === folderId) + .filter((item) => !isDefined(type) || item.type === type) + .sort((a, b) => a.position - b.position); + + const bounded = isDefined(limit) ? filtered.slice(0, limit) : filtered; + + return bounded.map(fromFlatNavigationMenuItemToNavigationMenuItemDto); } async findById({ diff --git a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/create-navigation-menu-item.tool.ts b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/create-navigation-menu-item.tool.ts new file mode 100644 index 00000000000..d5cb31d4cb5 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/create-navigation-menu-item.tool.ts @@ -0,0 +1,197 @@ +import { z } from 'zod'; + +import { NavigationMenuItemType } from 'twenty-shared/types'; + +import { type CreateNavigationMenuItemInput } from 'src/engine/metadata-modules/navigation-menu-item/dtos/create-navigation-menu-item.input'; +import { navigationMenuItemScopeSchema } from 'src/engine/metadata-modules/navigation-menu-item/tools/schemas/navigation-menu-item-scope.schema'; +import { type NavigationMenuItemToolContext } from 'src/engine/metadata-modules/navigation-menu-item/tools/types/navigation-menu-item-tool-context.type'; +import { type NavigationMenuItemToolDependencies } from 'src/engine/metadata-modules/navigation-menu-item/tools/types/navigation-menu-item-tool-dependencies.type'; + +const commonOptionalFields = { + icon: z + .string() + .optional() + .describe('Icon identifier (e.g. "IconStar", "IconFolder")'), + color: z.string().optional().describe('Optional hex colour'), + position: z + .number() + .optional() + .describe('Position among siblings; defaults to the end.'), + folderId: z + .string() + .uuid() + .optional() + .describe('Parent folder id, if the item should live inside a folder.'), +}; + +const requiredNameField = z + .string() + .trim() + .min(1) + .describe('Label shown in the sidebar.'); + +const derivedNameField = z + .string() + .trim() + .min(1) + .optional() + .describe( + "Optional custom label. If omitted, the sidebar shows the target's own name (object's plural label / view name / record identifier). Only pass this if the user explicitly wants a different label.", + ); + +const createNavigationMenuItemSchema = z.discriminatedUnion('type', [ + z.object({ + type: z.literal(NavigationMenuItemType.FOLDER), + scope: navigationMenuItemScopeSchema, + name: requiredNameField, + ...commonOptionalFields, + }), + z.object({ + type: z.literal(NavigationMenuItemType.LINK), + scope: navigationMenuItemScopeSchema, + name: requiredNameField, + link: z.string().url().describe('Target URL'), + ...commonOptionalFields, + }), + z.object({ + type: z.literal(NavigationMenuItemType.OBJECT), + scope: navigationMenuItemScopeSchema, + targetObjectMetadataId: z + .string() + .uuid() + .describe('Id of the object to pin'), + name: derivedNameField, + ...commonOptionalFields, + }), + z.object({ + type: z.literal(NavigationMenuItemType.VIEW), + scope: navigationMenuItemScopeSchema, + viewId: z.string().uuid().describe('Id of the view to pin'), + name: derivedNameField, + ...commonOptionalFields, + }), + z.object({ + type: z.literal(NavigationMenuItemType.RECORD), + scope: navigationMenuItemScopeSchema, + targetRecordId: z.string().uuid().describe('Id of the record to pin'), + targetObjectMetadataId: z + .string() + .uuid() + .describe("Id of the record's object metadata"), + name: derivedNameField, + ...commonOptionalFields, + }), + z.object({ + type: z.literal(NavigationMenuItemType.PAGE_LAYOUT), + scope: navigationMenuItemScopeSchema, + pageLayoutId: z.string().uuid().describe('Id of the page layout to pin'), + name: requiredNameField, + ...commonOptionalFields, + }), +]); + +type CreateNavigationMenuItemParams = z.infer< + typeof createNavigationMenuItemSchema +>; + +const toServiceInput = ( + params: CreateNavigationMenuItemParams, + userWorkspaceId: string | undefined, +): CreateNavigationMenuItemInput => { + const resolvedUserWorkspaceId = + params.scope === 'user' ? userWorkspaceId : undefined; + const base = { + type: params.type as NavigationMenuItemType, + userWorkspaceId: resolvedUserWorkspaceId, + icon: params.icon, + color: params.color, + position: params.position, + folderId: params.folderId, + }; + + switch (params.type) { + case NavigationMenuItemType.FOLDER: + return { ...base, name: params.name }; + case NavigationMenuItemType.LINK: + return { ...base, name: params.name, link: params.link }; + case NavigationMenuItemType.OBJECT: + return { + ...base, + name: params.name, + targetObjectMetadataId: params.targetObjectMetadataId, + }; + case NavigationMenuItemType.VIEW: + return { ...base, name: params.name, viewId: params.viewId }; + case NavigationMenuItemType.RECORD: + return { + ...base, + name: params.name, + targetRecordId: params.targetRecordId, + targetObjectMetadataId: params.targetObjectMetadataId, + }; + case NavigationMenuItemType.PAGE_LAYOUT: + return { + ...base, + name: params.name, + pageLayoutId: params.pageLayoutId, + }; + } +}; + +export const createCreateNavigationMenuItemTool = ( + deps: Pick, + context: NavigationMenuItemToolContext, +) => ({ + name: 'create_navigation_menu_item' as const, + description: `Create a navigation menu item. With scope='user' it becomes a personal favorite for the current user; with scope='workspace' it is shared with everyone (requires LAYOUTS permission). + +Type chooses the variant: +- FOLDER: a group to nest other items into (name required). +- LINK: an external URL pinned in the sidebar (name + link required). +- OBJECT: pins an object's standard view (label auto-derived from the object's plural name; only pass 'name' if the user wants a custom label). +- VIEW: pins a saved view (label auto-derived from the view's name; only pass 'name' for a custom label). +- RECORD: pins a single record (label auto-derived from the record's identifier; only pass 'name' for a custom label). +- PAGE_LAYOUT: pins a page layout, e.g. a dashboard (name required — no auto-derivation). + +Note: creating a new custom object via create_object_metadata already auto-creates an OBJECT navigation menu item — do not double-create.`, + inputSchema: createNavigationMenuItemSchema, + execute: async (parameters: CreateNavigationMenuItemParams) => { + try { + if (parameters.scope === 'user' && !context.userWorkspaceId) { + return { + success: false, + message: + 'Cannot create a user-scoped favorite without an authenticated user context.', + error: 'missing_user_workspace_id', + }; + } + + const created = await deps.navigationMenuItemService.create({ + input: toServiceInput(parameters, context.userWorkspaceId), + workspaceId: context.workspaceId, + authUserWorkspaceId: context.userWorkspaceId, + }); + + return { + success: true, + message: `Navigation menu item ${created.id} (${created.type}) created`, + result: { + id: created.id, + type: created.type, + name: created.name, + scope: created.userWorkspaceId ? 'user' : 'workspace', + folderId: created.folderId, + position: created.position, + }, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + + return { + success: false, + message: `Failed to create navigation menu item: ${message}`, + error: message, + }; + } + }, +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/delete-navigation-menu-item.tool.ts b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/delete-navigation-menu-item.tool.ts new file mode 100644 index 00000000000..cd2109141a5 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/delete-navigation-menu-item.tool.ts @@ -0,0 +1,48 @@ +import { z } from 'zod'; + +import { type NavigationMenuItemToolContext } from 'src/engine/metadata-modules/navigation-menu-item/tools/types/navigation-menu-item-tool-context.type'; +import { type NavigationMenuItemToolDependencies } from 'src/engine/metadata-modules/navigation-menu-item/tools/types/navigation-menu-item-tool-dependencies.type'; + +const deleteNavigationMenuItemSchema = z.object({ + id: z.string().uuid().describe('Id of the navigation menu item to delete'), +}); + +type DeleteNavigationMenuItemParams = z.infer< + typeof deleteNavigationMenuItemSchema +>; + +export const createDeleteNavigationMenuItemTool = ( + deps: Pick, + context: NavigationMenuItemToolContext, +) => ({ + name: 'delete_navigation_menu_item' as const, + description: `Delete a navigation menu item. Deleting a folder also deletes everything inside it.`, + inputSchema: deleteNavigationMenuItemSchema, + execute: async (parameters: DeleteNavigationMenuItemParams) => { + try { + const deleted = await deps.navigationMenuItemService.delete({ + id: parameters.id, + workspaceId: context.workspaceId, + authUserWorkspaceId: context.userWorkspaceId, + }); + + return { + success: true, + message: `Navigation menu item ${deleted.id} deleted`, + result: { + deletedId: deleted.id, + type: deleted.type, + name: deleted.name, + }, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + + return { + success: false, + message: `Failed to delete navigation menu item: ${message}`, + error: message, + }; + } + }, +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/list-navigation-menu-items.tool.ts b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/list-navigation-menu-items.tool.ts new file mode 100644 index 00000000000..92f9afa59f4 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/list-navigation-menu-items.tool.ts @@ -0,0 +1,92 @@ +import { z } from 'zod'; + +import { NavigationMenuItemType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { navigationMenuItemTypeSchema } from 'src/engine/metadata-modules/navigation-menu-item/tools/schemas/navigation-menu-item-type.schema'; +import { type NavigationMenuItemToolContext } from 'src/engine/metadata-modules/navigation-menu-item/tools/types/navigation-menu-item-tool-context.type'; +import { type NavigationMenuItemToolDependencies } from 'src/engine/metadata-modules/navigation-menu-item/tools/types/navigation-menu-item-tool-dependencies.type'; + +const listNavigationMenuItemsSchema = z.object({ + scope: z + .enum(['workspace', 'user', 'all']) + .optional() + .default('all') + .describe( + "'workspace' = shared navigation, 'user' = current user's favorites, 'all' = both merged (default).", + ), + folderId: z + .string() + .uuid() + .optional() + .describe('Only return items inside this folder.'), + type: navigationMenuItemTypeSchema + .optional() + .describe( + 'Filter by item type (FOLDER, LINK, OBJECT, VIEW, RECORD, PAGE_LAYOUT).', + ), + limit: z + .number() + .int() + .min(1) + .max(200) + .optional() + .default(100) + .describe('Max number of items to return.'), +}); + +type ListNavigationMenuItemsParams = z.infer< + typeof listNavigationMenuItemsSchema +>; + +export const createListNavigationMenuItemsTool = ( + deps: Pick, + context: NavigationMenuItemToolContext, +) => ({ + name: 'list_navigation_menu_items' as const, + description: `List navigation menu items (shared workspace navigation and/or the current user's personal favorites). Returns items sorted by position.`, + inputSchema: listNavigationMenuItemsSchema, + execute: async (parameters: ListNavigationMenuItemsParams) => { + try { + const items = await deps.navigationMenuItemService.findAll({ + workspaceId: context.workspaceId, + userWorkspaceId: context.userWorkspaceId, + scope: parameters.scope, + folderId: parameters.folderId, + type: parameters.type as NavigationMenuItemType | undefined, + limit: parameters.limit, + }); + + return { + success: true, + message: `Found ${items.length} navigation menu item(s)`, + result: { + items: items.map((item) => ({ + id: item.id, + type: item.type, + name: item.name, + scope: isDefined(item.userWorkspaceId) ? 'user' : 'workspace', + folderId: item.folderId, + position: item.position, + icon: item.icon, + color: item.color, + link: item.link, + targetObjectMetadataId: item.targetObjectMetadataId, + targetRecordId: item.targetRecordId, + viewId: item.viewId, + pageLayoutId: item.pageLayoutId, + })), + count: items.length, + }, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + + return { + success: false, + message: `Failed to list navigation menu items: ${message}`, + error: message, + }; + } + }, +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/schemas/navigation-menu-item-scope.schema.ts b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/schemas/navigation-menu-item-scope.schema.ts new file mode 100644 index 00000000000..281e3038a95 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/schemas/navigation-menu-item-scope.schema.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; + +export const navigationMenuItemScopeSchema = z + .enum(['workspace', 'user']) + .describe( + "'user' creates a personal favorite, visible only to the current user. " + + "'workspace' creates a shared navigation menu item visible to everyone (requires the LAYOUTS permission). " + + 'Twenty has no separate Favorites concept — favorites are just navigation menu items with scope=user.', + ); diff --git a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/schemas/navigation-menu-item-type.schema.ts b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/schemas/navigation-menu-item-type.schema.ts new file mode 100644 index 00000000000..8565aae0755 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/schemas/navigation-menu-item-type.schema.ts @@ -0,0 +1,11 @@ +import { NavigationMenuItemType } from 'twenty-shared/types'; +import { z } from 'zod'; + +export const navigationMenuItemTypeSchema = z.enum([ + NavigationMenuItemType.FOLDER, + NavigationMenuItemType.LINK, + NavigationMenuItemType.OBJECT, + NavigationMenuItemType.VIEW, + NavigationMenuItemType.RECORD, + NavigationMenuItemType.PAGE_LAYOUT, +]); diff --git a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/services/navigation-menu-item-tool.workspace-service.ts b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/services/navigation-menu-item-tool.workspace-service.ts new file mode 100644 index 00000000000..2ac968bd9b3 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/services/navigation-menu-item-tool.workspace-service.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@nestjs/common'; + +import { type ToolSet } from 'ai'; + +import { NavigationMenuItemService } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.service'; +import { createCreateNavigationMenuItemTool } from 'src/engine/metadata-modules/navigation-menu-item/tools/create-navigation-menu-item.tool'; +import { createDeleteNavigationMenuItemTool } from 'src/engine/metadata-modules/navigation-menu-item/tools/delete-navigation-menu-item.tool'; +import { createListNavigationMenuItemsTool } from 'src/engine/metadata-modules/navigation-menu-item/tools/list-navigation-menu-items.tool'; +import { type NavigationMenuItemToolDependencies } from 'src/engine/metadata-modules/navigation-menu-item/tools/types/navigation-menu-item-tool-dependencies.type'; +import { createUpdateNavigationMenuItemTool } from 'src/engine/metadata-modules/navigation-menu-item/tools/update-navigation-menu-item.tool'; + +@Injectable() +export class NavigationMenuItemToolWorkspaceService { + private readonly deps: NavigationMenuItemToolDependencies; + + constructor(navigationMenuItemService: NavigationMenuItemService) { + this.deps = { navigationMenuItemService }; + } + + generateNavigationMenuItemTools( + workspaceId: string, + userWorkspaceId?: string, + ): ToolSet { + const context = { workspaceId, userWorkspaceId }; + + const listNavigationMenuItems = createListNavigationMenuItemsTool( + this.deps, + context, + ); + const createNavigationMenuItem = createCreateNavigationMenuItemTool( + this.deps, + context, + ); + const updateNavigationMenuItem = createUpdateNavigationMenuItemTool( + this.deps, + context, + ); + const deleteNavigationMenuItem = createDeleteNavigationMenuItemTool( + this.deps, + context, + ); + + return { + [listNavigationMenuItems.name]: listNavigationMenuItems, + [createNavigationMenuItem.name]: createNavigationMenuItem, + [updateNavigationMenuItem.name]: updateNavigationMenuItem, + [deleteNavigationMenuItem.name]: deleteNavigationMenuItem, + }; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/types/navigation-menu-item-tool-context.type.ts b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/types/navigation-menu-item-tool-context.type.ts new file mode 100644 index 00000000000..96e4ea04601 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/types/navigation-menu-item-tool-context.type.ts @@ -0,0 +1,4 @@ +export type NavigationMenuItemToolContext = { + workspaceId: string; + userWorkspaceId?: string; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/types/navigation-menu-item-tool-dependencies.type.ts b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/types/navigation-menu-item-tool-dependencies.type.ts new file mode 100644 index 00000000000..6d14615b4f1 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/types/navigation-menu-item-tool-dependencies.type.ts @@ -0,0 +1,5 @@ +import type { NavigationMenuItemService } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.service'; + +export type NavigationMenuItemToolDependencies = { + navigationMenuItemService: NavigationMenuItemService; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/update-navigation-menu-item.tool.ts b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/update-navigation-menu-item.tool.ts new file mode 100644 index 00000000000..7f553aae7da --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/tools/update-navigation-menu-item.tool.ts @@ -0,0 +1,93 @@ +import { z } from 'zod'; + +import { type UpdateNavigationMenuItemInput } from 'src/engine/metadata-modules/navigation-menu-item/dtos/update-navigation-menu-item.input'; +import { type NavigationMenuItemToolContext } from 'src/engine/metadata-modules/navigation-menu-item/tools/types/navigation-menu-item-tool-context.type'; +import { type NavigationMenuItemToolDependencies } from 'src/engine/metadata-modules/navigation-menu-item/tools/types/navigation-menu-item-tool-dependencies.type'; + +const updateNavigationMenuItemSchema = z.object({ + id: z.string().uuid().describe('Id of the navigation menu item to update'), + name: z + .string() + .trim() + .min(1) + .optional() + .describe( + "New display name. For OBJECT/VIEW/RECORD items the sidebar normally shows the target entity's own name — only set this if the user wants a custom label.", + ), + icon: z.string().optional().describe('New icon identifier'), + color: z.string().optional().describe('New hex colour'), + position: z.number().optional().describe('New position among siblings'), + folderId: z + .string() + .uuid() + .nullable() + .optional() + .describe( + 'Move into a different folder. Pass null to move to the top level.', + ), + link: z + .string() + .url() + .optional() + .describe('New URL (only meaningful for LINK items)'), + pageLayoutId: z + .string() + .uuid() + .optional() + .describe('New page layout id (only meaningful for PAGE_LAYOUT items)'), +}); + +type UpdateNavigationMenuItemParams = z.infer< + typeof updateNavigationMenuItemSchema +>; + +export const createUpdateNavigationMenuItemTool = ( + deps: Pick, + context: NavigationMenuItemToolContext, +) => ({ + name: 'update_navigation_menu_item' as const, + description: `Update a navigation menu item (rename, recolor, move between folders, reorder). Type and target ids are immutable — delete and recreate to convert one variant into another.`, + inputSchema: updateNavigationMenuItemSchema, + execute: async (parameters: UpdateNavigationMenuItemParams) => { + try { + const update: Partial = {}; + + if (parameters.name !== undefined) update.name = parameters.name; + if (parameters.icon !== undefined) update.icon = parameters.icon; + if (parameters.color !== undefined) update.color = parameters.color; + if (parameters.position !== undefined) + update.position = parameters.position; + if (parameters.folderId !== undefined) + update.folderId = parameters.folderId; + if (parameters.link !== undefined) update.link = parameters.link; + if (parameters.pageLayoutId !== undefined) + update.pageLayoutId = parameters.pageLayoutId; + + const updated = await deps.navigationMenuItemService.update({ + input: { id: parameters.id, ...update }, + workspaceId: context.workspaceId, + authUserWorkspaceId: context.userWorkspaceId, + }); + + return { + success: true, + message: `Navigation menu item ${updated.id} updated`, + result: { + id: updated.id, + type: updated.type, + name: updated.name, + folderId: updated.folderId, + position: updated.position, + }, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + + return { + success: false, + message: `Failed to update navigation menu item: ${message}`, + error: message, + }; + } + }, +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/webhook/tools/create-webhook.tool.ts b/packages/twenty-server/src/engine/metadata-modules/webhook/tools/create-webhook.tool.ts new file mode 100644 index 00000000000..f33f131d1f1 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/webhook/tools/create-webhook.tool.ts @@ -0,0 +1,75 @@ +import { z } from 'zod'; + +import { webhookOperationSchema } from 'src/engine/metadata-modules/webhook/tools/schemas/webhook-operation.schema'; +import { type WebhookToolContext } from 'src/engine/metadata-modules/webhook/tools/types/webhook-tool-context.type'; +import { type WebhookToolDependencies } from 'src/engine/metadata-modules/webhook/tools/types/webhook-tool-dependencies.type'; +import { compileWebhookOperations } from 'src/engine/metadata-modules/webhook/tools/utils/compile-webhook-operations.util'; + +const createWebhookSchema = z.object({ + targetUrl: z + .string() + .url() + .describe('Absolute URL the webhook payload should be POSTed to'), + operations: webhookOperationSchema, + description: z + .string() + .optional() + .describe('Optional human description for the webhook'), + secret: z + .string() + .optional() + .describe( + 'Optional shared secret used to sign payloads. A secret is generated if omitted.', + ), +}); + +type CreateWebhookParams = z.infer; + +export const createCreateWebhookTool = ( + deps: Pick, + context: WebhookToolContext, +) => ({ + name: 'create_webhook' as const, + description: `Register a new outgoing webhook for this workspace. + +Operations are structured entries discriminated by 'kind': +- {kind:'record', object:'person', event:'created'} → fires when a person is created (compiles to 'person.created'). +- {kind:'record', object:'*', event:'*'} → fires on every record event. +- {kind:'metadata', metadataName:'workflow', operation:'updated'} → fires when a workflow definition is updated (compiles to 'metadata.workflow.updated'). +- {kind:'metadata', metadataName:'*', operation:'*'} → fires on every metadata change. + +Mix as needed: pass one array containing both record and metadata operations.`, + inputSchema: createWebhookSchema, + execute: async (parameters: CreateWebhookParams) => { + try { + const webhook = await deps.webhookService.create( + { + targetUrl: parameters.targetUrl, + operations: compileWebhookOperations(parameters.operations), + description: parameters.description, + secret: parameters.secret, + }, + context.workspaceId, + ); + + return { + success: true, + message: `Webhook created for ${webhook.targetUrl}`, + result: { + id: webhook.id, + targetUrl: webhook.targetUrl, + operations: webhook.operations, + description: webhook.description, + }, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + + return { + success: false, + message: `Failed to create webhook: ${message}`, + error: message, + }; + } + }, +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/webhook/tools/delete-webhook.tool.ts b/packages/twenty-server/src/engine/metadata-modules/webhook/tools/delete-webhook.tool.ts new file mode 100644 index 00000000000..24213117f63 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/webhook/tools/delete-webhook.tool.ts @@ -0,0 +1,41 @@ +import { z } from 'zod'; + +import { type WebhookToolContext } from 'src/engine/metadata-modules/webhook/tools/types/webhook-tool-context.type'; +import { type WebhookToolDependencies } from 'src/engine/metadata-modules/webhook/tools/types/webhook-tool-dependencies.type'; + +const deleteWebhookSchema = z.object({ + id: z.string().uuid().describe('The id of the webhook to delete'), +}); + +type DeleteWebhookParams = z.infer; + +export const createDeleteWebhookTool = ( + deps: Pick, + context: WebhookToolContext, +) => ({ + name: 'delete_webhook' as const, + description: `Delete a webhook by id. Use list_webhooks first if you don't know the id.`, + inputSchema: deleteWebhookSchema, + execute: async (parameters: DeleteWebhookParams) => { + try { + const webhook = await deps.webhookService.delete( + parameters.id, + context.workspaceId, + ); + + return { + success: true, + message: `Webhook ${webhook.id} deleted`, + result: { deletedWebhookId: webhook.id, targetUrl: webhook.targetUrl }, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + + return { + success: false, + message: `Failed to delete webhook: ${message}`, + error: message, + }; + } + }, +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/webhook/tools/list-webhooks.tool.ts b/packages/twenty-server/src/engine/metadata-modules/webhook/tools/list-webhooks.tool.ts new file mode 100644 index 00000000000..ee6402eefd1 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/webhook/tools/list-webhooks.tool.ts @@ -0,0 +1,44 @@ +import { z } from 'zod'; + +import { type WebhookToolContext } from 'src/engine/metadata-modules/webhook/tools/types/webhook-tool-context.type'; +import { type WebhookToolDependencies } from 'src/engine/metadata-modules/webhook/tools/types/webhook-tool-dependencies.type'; + +const listWebhooksSchema = z.object({}); + +export const createListWebhooksTool = ( + deps: Pick, + context: WebhookToolContext, +) => ({ + name: 'list_webhooks' as const, + description: `List every webhook registered in the workspace. Returns id, targetUrl, operations (e.g. ['person.created','company.updated']), description and timestamps.`, + inputSchema: listWebhooksSchema, + execute: async () => { + try { + const webhooks = await deps.webhookService.findAll(context.workspaceId); + + return { + success: true, + message: `Found ${webhooks.length} webhook(s)`, + result: { + webhooks: webhooks.map((webhook) => ({ + id: webhook.id, + targetUrl: webhook.targetUrl, + operations: webhook.operations, + description: webhook.description, + createdAt: webhook.createdAt, + updatedAt: webhook.updatedAt, + })), + count: webhooks.length, + }, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + + return { + success: false, + message: `Failed to list webhooks: ${message}`, + error: message, + }; + } + }, +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/webhook/tools/schemas/webhook-operation.schema.ts b/packages/twenty-server/src/engine/metadata-modules/webhook/tools/schemas/webhook-operation.schema.ts new file mode 100644 index 00000000000..78716098df4 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/webhook/tools/schemas/webhook-operation.schema.ts @@ -0,0 +1,45 @@ +import { z } from 'zod'; + +const recordOperationSchema = z.object({ + kind: z + .literal('record') + .describe("Record event ('.')"), + object: z + .string() + .min(1) + .describe( + "Object name singular (e.g. 'person', 'company', 'task'), or '*' for all objects.", + ), + event: z + .enum(['created', 'updated', 'deleted', '*']) + .describe("Event kind. Use '*' to match every event for the given object."), +}); + +const metadataOperationSchema = z.object({ + kind: z + .literal('metadata') + .describe( + "Metadata event ('metadata..') — fires on changes to objects, fields, views, workflows, etc.", + ), + metadataName: z + .string() + .min(1) + .describe( + "Metadata name (e.g. 'object', 'field', 'view', 'workflow'), or '*' for all.", + ), + operation: z + .enum(['created', 'updated', 'deleted', '*']) + .describe("Operation kind. Use '*' to match every operation."), +}); + +export const webhookOperationSchema = z + .array( + z.discriminatedUnion('kind', [ + recordOperationSchema, + metadataOperationSchema, + ]), + ) + .min(1) + .describe( + "Events that trigger the webhook. Record events compile to '.' (e.g. 'person.created'). Metadata events compile to 'metadata..' (e.g. 'metadata.workflow.updated'). Use [{kind:'record',object:'*',event:'*'}] to subscribe to all record events.", + ); diff --git a/packages/twenty-server/src/engine/metadata-modules/webhook/tools/services/webhook-tool.workspace-service.ts b/packages/twenty-server/src/engine/metadata-modules/webhook/tools/services/webhook-tool.workspace-service.ts new file mode 100644 index 00000000000..807315ba318 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/webhook/tools/services/webhook-tool.workspace-service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@nestjs/common'; + +import { type ToolSet } from 'ai'; + +import { WebhookService } from 'src/engine/metadata-modules/webhook/webhook.service'; +import { createCreateWebhookTool } from 'src/engine/metadata-modules/webhook/tools/create-webhook.tool'; +import { createDeleteWebhookTool } from 'src/engine/metadata-modules/webhook/tools/delete-webhook.tool'; +import { createListWebhooksTool } from 'src/engine/metadata-modules/webhook/tools/list-webhooks.tool'; +import { type WebhookToolDependencies } from 'src/engine/metadata-modules/webhook/tools/types/webhook-tool-dependencies.type'; +import { createUpdateWebhookTool } from 'src/engine/metadata-modules/webhook/tools/update-webhook.tool'; + +@Injectable() +export class WebhookToolWorkspaceService { + private readonly deps: WebhookToolDependencies; + + constructor(webhookService: WebhookService) { + this.deps = { webhookService }; + } + + generateWebhookTools(workspaceId: string): ToolSet { + const context = { workspaceId }; + + const listWebhooks = createListWebhooksTool(this.deps, context); + const createWebhook = createCreateWebhookTool(this.deps, context); + const updateWebhook = createUpdateWebhookTool(this.deps, context); + const deleteWebhook = createDeleteWebhookTool(this.deps, context); + + return { + [listWebhooks.name]: listWebhooks, + [createWebhook.name]: createWebhook, + [updateWebhook.name]: updateWebhook, + [deleteWebhook.name]: deleteWebhook, + }; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/webhook/tools/types/webhook-tool-context.type.ts b/packages/twenty-server/src/engine/metadata-modules/webhook/tools/types/webhook-tool-context.type.ts new file mode 100644 index 00000000000..1a49f79a575 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/webhook/tools/types/webhook-tool-context.type.ts @@ -0,0 +1,3 @@ +export type WebhookToolContext = { + workspaceId: string; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/webhook/tools/types/webhook-tool-dependencies.type.ts b/packages/twenty-server/src/engine/metadata-modules/webhook/tools/types/webhook-tool-dependencies.type.ts new file mode 100644 index 00000000000..b3887a8b964 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/webhook/tools/types/webhook-tool-dependencies.type.ts @@ -0,0 +1,5 @@ +import type { WebhookService } from 'src/engine/metadata-modules/webhook/webhook.service'; + +export type WebhookToolDependencies = { + webhookService: WebhookService; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/webhook/tools/update-webhook.tool.ts b/packages/twenty-server/src/engine/metadata-modules/webhook/tools/update-webhook.tool.ts new file mode 100644 index 00000000000..275dbc8ef8d --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/webhook/tools/update-webhook.tool.ts @@ -0,0 +1,78 @@ +import { z } from 'zod'; + +import { webhookOperationSchema } from 'src/engine/metadata-modules/webhook/tools/schemas/webhook-operation.schema'; +import { type WebhookToolContext } from 'src/engine/metadata-modules/webhook/tools/types/webhook-tool-context.type'; +import { type WebhookToolDependencies } from 'src/engine/metadata-modules/webhook/tools/types/webhook-tool-dependencies.type'; +import { compileWebhookOperations } from 'src/engine/metadata-modules/webhook/tools/utils/compile-webhook-operations.util'; + +const updateWebhookSchema = z.object({ + id: z.string().uuid().describe('The id of the webhook to update'), + targetUrl: z + .string() + .url() + .optional() + .describe('New target URL. Leave unset to keep the current value.'), + operations: webhookOperationSchema + .optional() + .describe('Replaces the operations list. Leave unset to keep current.'), + description: z.string().optional(), + secret: z.string().optional(), +}); + +type UpdateWebhookParams = z.infer; + +export const createUpdateWebhookTool = ( + deps: Pick, + context: WebhookToolContext, +) => ({ + name: 'update_webhook' as const, + description: `Update an existing webhook. Only the fields you pass are modified; everything else is preserved.`, + inputSchema: updateWebhookSchema, + execute: async (parameters: UpdateWebhookParams) => { + try { + const update: { + targetUrl?: string; + operations?: string[]; + description?: string; + secret?: string; + } = {}; + + if (parameters.targetUrl !== undefined) { + update.targetUrl = parameters.targetUrl; + } + if (parameters.operations !== undefined) { + update.operations = compileWebhookOperations(parameters.operations); + } + if (parameters.description !== undefined) { + update.description = parameters.description; + } + if (parameters.secret !== undefined) { + update.secret = parameters.secret; + } + + const webhook = await deps.webhookService.update( + { id: parameters.id, update }, + context.workspaceId, + ); + + return { + success: true, + message: `Webhook ${webhook.id} updated`, + result: { + id: webhook.id, + targetUrl: webhook.targetUrl, + operations: webhook.operations, + description: webhook.description, + }, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + + return { + success: false, + message: `Failed to update webhook: ${message}`, + error: message, + }; + } + }, +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/webhook/tools/utils/compile-webhook-operations.util.ts b/packages/twenty-server/src/engine/metadata-modules/webhook/tools/utils/compile-webhook-operations.util.ts new file mode 100644 index 00000000000..74eca62bcc4 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/webhook/tools/utils/compile-webhook-operations.util.ts @@ -0,0 +1,14 @@ +import { type z } from 'zod'; + +import { webhookOperationSchema } from 'src/engine/metadata-modules/webhook/tools/schemas/webhook-operation.schema'; + +export const compileWebhookOperations = ( + operations: z.infer, +): string[] => + operations.map((operation) => { + if (operation.kind === 'record') { + return `${operation.object}.${operation.event}`; + } + + return `metadata.${operation.metadataName}.${operation.operation}`; + }); diff --git a/packages/twenty-server/src/engine/metadata-modules/webhook/webhook.module.ts b/packages/twenty-server/src/engine/metadata-modules/webhook/webhook.module.ts index 99d5f2f1892..a8064f90b1f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/webhook/webhook.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/webhook/webhook.module.ts @@ -10,6 +10,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi import { WebhookController } from 'src/engine/metadata-modules/webhook/controllers/webhook.controller'; import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity'; import { WebhookGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/webhook/interceptors/webhook-graphql-api-exception.interceptor'; +import { WebhookToolWorkspaceService } from 'src/engine/metadata-modules/webhook/tools/services/webhook-tool.workspace-service'; import { WebhookResolver } from 'src/engine/metadata-modules/webhook/webhook.resolver'; import { WebhookService } from 'src/engine/metadata-modules/webhook/webhook.service'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; @@ -33,7 +34,8 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace WebhookResolver, WebhookGraphqlApiExceptionInterceptor, WorkspaceMigrationGraphqlApiExceptionInterceptor, + WebhookToolWorkspaceService, ], - exports: [WebhookService], + exports: [WebhookService, WebhookToolWorkspaceService], }) export class WebhookModule {} diff --git a/packages/twenty-shared/src/ai/constants/tool-category.const.ts b/packages/twenty-shared/src/ai/constants/tool-category.const.ts index 80ea69a0e65..f7311b1fefa 100644 --- a/packages/twenty-shared/src/ai/constants/tool-category.const.ts +++ b/packages/twenty-shared/src/ai/constants/tool-category.const.ts @@ -4,7 +4,8 @@ export enum ToolCategory { WORKFLOW = 'WORKFLOW', METADATA = 'METADATA', VIEW = 'VIEW', - VIEW_FIELD = 'VIEW_FIELD', DASHBOARD = 'DASHBOARD', + NAVIGATION_MENU_ITEM = 'NAVIGATION_MENU_ITEM', + WEBHOOK = 'WEBHOOK', LOGIC_FUNCTION = 'LOGIC_FUNCTION', }