mirror of
https://github.com/twentyhq/twenty.git
synced 2026-06-12 01:46:39 -04:00
feat(ai-chat): add navigation menu item + webhook tool providers (#20759)
## 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}` → `<object>.<event>`) and metadata events (`{kind:'metadata',
metadataName, operation}` → `metadata.<metadataName>.<operation>`).
- 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.
This commit is contained in:
@@ -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!
|
||||
|
||||
@@ -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"')
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<boolean> {
|
||||
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<string, unknown>,
|
||||
context: ToolProviderContext,
|
||||
): Promise<ToolOutput> {
|
||||
return executeToolFromToolSet(
|
||||
this.buildToolSet(context),
|
||||
toolName,
|
||||
args,
|
||||
ToolCategory.NAVIGATION_MENU_ITEM,
|
||||
);
|
||||
}
|
||||
|
||||
private buildToolSet(context: ToolProviderContext): ToolSet {
|
||||
return this.navigationMenuItemToolService.generateNavigationMenuItemTools(
|
||||
context.workspaceId,
|
||||
context.userWorkspaceId,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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<boolean> {
|
||||
return true;
|
||||
async isAvailable(context: ToolProviderContext): Promise<boolean> {
|
||||
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<string, unknown>,
|
||||
context: ToolProviderContext,
|
||||
): Promise<ToolOutput> {
|
||||
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<ToolSet> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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<NavigationMenuItemDTO[]> {
|
||||
const { flatNavigationMenuItemMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
@@ -56,15 +64,33 @@ export class NavigationMenuItemService {
|
||||
},
|
||||
);
|
||||
|
||||
return Object.values(flatNavigationMenuItemMaps.byUniversalIdentifier)
|
||||
.filter(
|
||||
(item): item is NonNullable<typeof item> =>
|
||||
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<typeof item> => {
|
||||
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({
|
||||
|
||||
@@ -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<NavigationMenuItemToolDependencies, 'navigationMenuItemService'>,
|
||||
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,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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<NavigationMenuItemToolDependencies, 'navigationMenuItemService'>,
|
||||
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,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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<NavigationMenuItemToolDependencies, 'navigationMenuItemService'>,
|
||||
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,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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.',
|
||||
);
|
||||
@@ -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,
|
||||
]);
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export type NavigationMenuItemToolContext = {
|
||||
workspaceId: string;
|
||||
userWorkspaceId?: string;
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { NavigationMenuItemService } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.service';
|
||||
|
||||
export type NavigationMenuItemToolDependencies = {
|
||||
navigationMenuItemService: NavigationMenuItemService;
|
||||
};
|
||||
@@ -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<NavigationMenuItemToolDependencies, 'navigationMenuItemService'>,
|
||||
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<UpdateNavigationMenuItemInput> = {};
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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<typeof createWebhookSchema>;
|
||||
|
||||
export const createCreateWebhookTool = (
|
||||
deps: Pick<WebhookToolDependencies, 'webhookService'>,
|
||||
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,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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<typeof deleteWebhookSchema>;
|
||||
|
||||
export const createDeleteWebhookTool = (
|
||||
deps: Pick<WebhookToolDependencies, 'webhookService'>,
|
||||
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,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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<WebhookToolDependencies, 'webhookService'>,
|
||||
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,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const recordOperationSchema = z.object({
|
||||
kind: z
|
||||
.literal('record')
|
||||
.describe("Record event ('<objectNameSingular>.<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.<metadataName>.<operation>') — 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 '<object>.<event>' (e.g. 'person.created'). Metadata events compile to 'metadata.<metadataName>.<operation>' (e.g. 'metadata.workflow.updated'). Use [{kind:'record',object:'*',event:'*'}] to subscribe to all record events.",
|
||||
);
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export type WebhookToolContext = {
|
||||
workspaceId: string;
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { WebhookService } from 'src/engine/metadata-modules/webhook/webhook.service';
|
||||
|
||||
export type WebhookToolDependencies = {
|
||||
webhookService: WebhookService;
|
||||
};
|
||||
@@ -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<typeof updateWebhookSchema>;
|
||||
|
||||
export const createUpdateWebhookTool = (
|
||||
deps: Pick<WebhookToolDependencies, 'webhookService'>,
|
||||
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,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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<typeof webhookOperationSchema>,
|
||||
): string[] =>
|
||||
operations.map((operation) => {
|
||||
if (operation.kind === 'record') {
|
||||
return `${operation.object}.${operation.event}`;
|
||||
}
|
||||
|
||||
return `metadata.${operation.metadataName}.${operation.operation}`;
|
||||
});
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user