feat(ai): add browsing context and fix tool loading (#16476)
## Summary - Add `BrowsingContext` type to automatically pass what the user is currently viewing (recordPage or listView) to the AI chat - Simplify context architecture: remove toggleable context UI, make it automatic and invisible to the user - Fix tool loading: add `unionOf` handling in `getDatabaseToolsForObject` and fix regex ordering so `find_one_*` tools are properly registered - Use plural names for find tools (`find_people` vs `find_one_person`) for better semantics - Clean up unused components and states ## Changes ### Frontend - New `BrowsingContext` type and `useGetBrowsingContext` hook to gather context from Recoil state - Simplified `useAgentChat` to use the new browsing context - Removed toggleable context UI components (`AgentChatContextRecordPreview`, `SendMessageWithRecordsContextButton`, etc.) - Removed `isAgentChatCurrentContextActiveState` ### Backend - New `BrowsingContextType` for recordPage and listView contexts - Updated `ChatExecutionService` to build context from browsing context - Fixed `tool-registry.service.ts`: - Added `unionOf` handling in permission config - Fixed regex ordering (`find_one` before `find`) so tools load correctly - Use plural names for search tools (`find_people` instead of `find_person`) ## Test plan - [x] Typecheck passes - [x] Lint passes - [ ] Test AI chat on record page - should show context in system prompt - [ ] Test AI chat on list view - should show view name and filters - [ ] Test `find_one_*` tools now load correctly - [ ] Test `find_*` tools use plural naming
This commit is contained in:
+2
-2
@@ -37,8 +37,8 @@ export const createDirectRecordToolsFactory = (deps: DirectRecordToolsDeps) => {
|
||||
const tools: ToolSet = {};
|
||||
|
||||
if (canRead) {
|
||||
tools[`find_${objectMetadata.nameSingular}`] = {
|
||||
description: `Search for ${objectMetadata.labelSingular} records using flexible filtering criteria. Supports exact matches, pattern matching, ranges, and null checks. Use limit/offset for pagination and orderBy for sorting. To find by ID, use filter: { id: { eq: "record-id" } }. Returns an array of matching records with their full data.`,
|
||||
tools[`find_${objectMetadata.namePlural}`] = {
|
||||
description: `Search for ${objectMetadata.labelPlural} records using flexible filtering criteria. Supports exact matches, pattern matching, ranges, and null checks. Use limit/offset for pagination and orderBy for sorting. To find by ID, use filter: { id: { eq: "record-id" } }. Returns an array of matching records with their full data.`,
|
||||
inputSchema: generateFindToolInputSchema(
|
||||
objectMetadata,
|
||||
restrictedFields,
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const TOOL_PROVIDERS = Symbol('TOOL_PROVIDERS');
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { type ToolSet } from 'ai';
|
||||
import { type ActorMetadata } from 'twenty-shared/types';
|
||||
|
||||
import { type ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
|
||||
export type ToolProviderContext = {
|
||||
workspaceId: string;
|
||||
roleId: string;
|
||||
rolePermissionConfig: RolePermissionConfig;
|
||||
actorContext?: ActorMetadata;
|
||||
};
|
||||
|
||||
export interface ToolProvider {
|
||||
readonly category: ToolCategory;
|
||||
|
||||
isAvailable(context: ToolProviderContext): Promise<boolean>;
|
||||
|
||||
generateTools(context: ToolProviderContext): Promise<ToolSet>;
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type ToolSet } from 'ai';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import {
|
||||
type ToolProvider,
|
||||
type ToolProviderContext,
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool';
|
||||
import { SearchHelpCenterTool } from 'src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool';
|
||||
import { SendEmailTool } from 'src/engine/core-modules/tool/tools/send-email-tool/send-email-tool';
|
||||
import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
|
||||
import { type Tool } from 'src/engine/core-modules/tool/types/tool.type';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
|
||||
@Injectable()
|
||||
export class ActionToolProvider implements ToolProvider {
|
||||
readonly category = ToolCategory.ACTION;
|
||||
|
||||
constructor(
|
||||
private readonly httpTool: HttpTool,
|
||||
private readonly sendEmailTool: SendEmailTool,
|
||||
private readonly searchHelpCenterTool: SearchHelpCenterTool,
|
||||
private readonly permissionsService: PermissionsService,
|
||||
) {}
|
||||
|
||||
async isAvailable(_context: ToolProviderContext): Promise<boolean> {
|
||||
// Action tools are always available (individual tool permissions checked in generateTools)
|
||||
return true;
|
||||
}
|
||||
|
||||
async generateTools(context: ToolProviderContext): Promise<ToolSet> {
|
||||
const tools: ToolSet = {};
|
||||
|
||||
const hasHttpPermission = await this.permissionsService.hasToolPermission(
|
||||
context.rolePermissionConfig,
|
||||
context.workspaceId,
|
||||
PermissionFlagType.HTTP_REQUEST_TOOL,
|
||||
);
|
||||
|
||||
if (hasHttpPermission) {
|
||||
tools['http_request'] = this.createToolEntry(
|
||||
this.httpTool,
|
||||
context.workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
const hasEmailPermission = await this.permissionsService.hasToolPermission(
|
||||
context.rolePermissionConfig,
|
||||
context.workspaceId,
|
||||
PermissionFlagType.SEND_EMAIL_TOOL,
|
||||
);
|
||||
|
||||
if (hasEmailPermission) {
|
||||
tools['send_email'] = this.createToolEntry(
|
||||
this.sendEmailTool,
|
||||
context.workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
tools['search_help_center'] = this.createToolEntry(
|
||||
this.searchHelpCenterTool,
|
||||
context.workspaceId,
|
||||
);
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
private createToolEntry(tool: Tool, workspaceId: string) {
|
||||
return {
|
||||
description: tool.description,
|
||||
inputSchema: tool.inputSchema,
|
||||
execute: async (parameters: { input: ToolInput }) =>
|
||||
tool.execute(parameters.input, workspaceId),
|
||||
};
|
||||
}
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type ToolSet } from 'ai';
|
||||
import {
|
||||
type ObjectsPermissions,
|
||||
type ObjectsPermissionsByRoleId,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
type ToolProvider,
|
||||
type ToolProviderContext,
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
|
||||
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
|
||||
import { CreateRecordService } from 'src/engine/core-modules/record-crud/services/create-record.service';
|
||||
import { DeleteRecordService } from 'src/engine/core-modules/record-crud/services/delete-record.service';
|
||||
import { FindRecordsService } from 'src/engine/core-modules/record-crud/services/find-records.service';
|
||||
import { UpdateRecordService } from 'src/engine/core-modules/record-crud/services/update-record.service';
|
||||
import { createDirectRecordToolsFactory } from 'src/engine/core-modules/record-crud/tool-factory/direct-record-tools.factory';
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { isWorkflowRelatedObject } from 'src/engine/metadata-modules/ai/ai-agent/utils/is-workflow-related-object.util';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { computePermissionIntersection } from 'src/engine/twenty-orm/utils/compute-permission-intersection.util';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
|
||||
@Injectable()
|
||||
export class DatabaseToolProvider implements ToolProvider {
|
||||
readonly category = ToolCategory.DATABASE_CRUD;
|
||||
|
||||
constructor(
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly createRecordService: CreateRecordService,
|
||||
private readonly updateRecordService: UpdateRecordService,
|
||||
private readonly deleteRecordService: DeleteRecordService,
|
||||
private readonly findRecordsService: FindRecordsService,
|
||||
) {}
|
||||
|
||||
async isAvailable(_context: ToolProviderContext): Promise<boolean> {
|
||||
// Database tools are always available (per-object permissions checked in generateTools)
|
||||
return true;
|
||||
}
|
||||
|
||||
async generateTools(context: ToolProviderContext): Promise<ToolSet> {
|
||||
const tools: ToolSet = {};
|
||||
|
||||
const { rolesPermissions } =
|
||||
await this.workspaceCacheService.getOrRecompute(context.workspaceId, [
|
||||
'rolesPermissions',
|
||||
]);
|
||||
|
||||
const objectPermissions = this.getObjectPermissions(
|
||||
rolesPermissions,
|
||||
context.rolePermissionConfig,
|
||||
);
|
||||
|
||||
if (!objectPermissions) {
|
||||
return tools;
|
||||
}
|
||||
|
||||
const { flatObjectMetadataMaps, flatFieldMetadataMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId: context.workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const allFlatObjects = Object.values(flatObjectMetadataMaps.byId)
|
||||
.filter(isDefined)
|
||||
.filter((obj) => obj.isActive && !obj.isSystem);
|
||||
|
||||
const factory = createDirectRecordToolsFactory({
|
||||
createRecordService: this.createRecordService,
|
||||
updateRecordService: this.updateRecordService,
|
||||
deleteRecordService: this.deleteRecordService,
|
||||
findRecordsService: this.findRecordsService,
|
||||
});
|
||||
|
||||
for (const flatObject of allFlatObjects) {
|
||||
if (isWorkflowRelatedObject(flatObject)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const permission = objectPermissions[flatObject.id];
|
||||
|
||||
if (!permission) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const objectMetadata = {
|
||||
...flatObject,
|
||||
fields: getFlatFieldsFromFlatObjectMetadata(
|
||||
flatObject,
|
||||
flatFieldMetadataMaps,
|
||||
),
|
||||
};
|
||||
|
||||
const objectTools = factory(
|
||||
{
|
||||
objectMetadata,
|
||||
restrictedFields: permission.restrictedFields,
|
||||
canCreate: permission.canUpdateObjectRecords,
|
||||
canRead: permission.canReadObjectRecords,
|
||||
canUpdate: permission.canUpdateObjectRecords,
|
||||
canDelete: permission.canSoftDeleteObjectRecords,
|
||||
},
|
||||
{
|
||||
workspaceId: context.workspaceId,
|
||||
rolePermissionConfig: context.rolePermissionConfig,
|
||||
actorContext: context.actorContext,
|
||||
},
|
||||
);
|
||||
|
||||
Object.assign(tools, objectTools);
|
||||
}
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
private getObjectPermissions(
|
||||
rolesPermissions: ObjectsPermissionsByRoleId,
|
||||
rolePermissionConfig: ToolProviderContext['rolePermissionConfig'],
|
||||
): ObjectsPermissions | null {
|
||||
if ('intersectionOf' in rolePermissionConfig) {
|
||||
const allRolePermissions = rolePermissionConfig.intersectionOf.map(
|
||||
(roleId: string) => rolesPermissions[roleId],
|
||||
);
|
||||
|
||||
return allRolePermissions.length === 1
|
||||
? allRolePermissions[0]
|
||||
: computePermissionIntersection(allRolePermissions);
|
||||
}
|
||||
|
||||
if ('unionOf' in rolePermissionConfig) {
|
||||
if (rolePermissionConfig.unionOf.length === 1) {
|
||||
return rolesPermissions[rolePermissionConfig.unionOf[0]];
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Union permission logic for multiple roles not yet implemented',
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type ToolSet } from 'ai';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import {
|
||||
type ToolProvider,
|
||||
type ToolProviderContext,
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { FieldMetadataToolsFactory } from 'src/engine/metadata-modules/field-metadata/tools/field-metadata-tools.factory';
|
||||
import { ObjectMetadataToolsFactory } from 'src/engine/metadata-modules/object-metadata/tools/object-metadata-tools.factory';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
|
||||
@Injectable()
|
||||
export class MetadataToolProvider implements ToolProvider {
|
||||
readonly category = ToolCategory.METADATA;
|
||||
|
||||
constructor(
|
||||
private readonly objectMetadataToolsFactory: ObjectMetadataToolsFactory,
|
||||
private readonly fieldMetadataToolsFactory: FieldMetadataToolsFactory,
|
||||
private readonly permissionsService: PermissionsService,
|
||||
) {}
|
||||
|
||||
async isAvailable(context: ToolProviderContext): Promise<boolean> {
|
||||
return this.permissionsService.checkRolesPermissions(
|
||||
context.rolePermissionConfig,
|
||||
context.workspaceId,
|
||||
PermissionFlagType.DATA_MODEL,
|
||||
);
|
||||
}
|
||||
|
||||
async generateTools(context: ToolProviderContext): Promise<ToolSet> {
|
||||
return {
|
||||
...this.objectMetadataToolsFactory.generateTools(context.workspaceId),
|
||||
...this.fieldMetadataToolsFactory.generateTools(context.workspaceId),
|
||||
};
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { Inject, Injectable, Optional } from '@nestjs/common';
|
||||
|
||||
import { type ToolSet } from 'ai';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import {
|
||||
type ToolProvider,
|
||||
type ToolProviderContext,
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
|
||||
import { WORKFLOW_TOOL_SERVICE_TOKEN } from 'src/engine/core-modules/tool-provider/constants/workflow-tool-service.token';
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
import type { WorkflowToolWorkspaceService } from 'src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service';
|
||||
|
||||
@Injectable()
|
||||
export class WorkflowToolProvider implements ToolProvider {
|
||||
readonly category = ToolCategory.WORKFLOW;
|
||||
|
||||
constructor(
|
||||
@Optional()
|
||||
@Inject(WORKFLOW_TOOL_SERVICE_TOKEN)
|
||||
private readonly workflowToolService: WorkflowToolWorkspaceService | null,
|
||||
private readonly permissionsService: PermissionsService,
|
||||
) {}
|
||||
|
||||
async isAvailable(context: ToolProviderContext): Promise<boolean> {
|
||||
if (!this.workflowToolService) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.permissionsService.checkRolesPermissions(
|
||||
context.rolePermissionConfig,
|
||||
context.workspaceId,
|
||||
PermissionFlagType.WORKFLOWS,
|
||||
);
|
||||
}
|
||||
|
||||
async generateTools(context: ToolProviderContext): Promise<ToolSet> {
|
||||
if (!this.workflowToolService) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return this.workflowToolService.generateWorkflowTools(
|
||||
context.workspaceId,
|
||||
context.rolePermissionConfig,
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -14,7 +14,7 @@ import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-c
|
||||
import { type ToolSpecification } from 'src/engine/core-modules/tool-provider/types/tool-specification.type';
|
||||
import { ToolType } from 'src/engine/core-modules/tool/enums/tool-type.enum';
|
||||
import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool';
|
||||
import { SearchArticlesTool } from 'src/engine/core-modules/tool/tools/search-articles-tool/search-articles-tool';
|
||||
import { SearchHelpCenterTool } from 'src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool';
|
||||
import { SendEmailTool } from 'src/engine/core-modules/tool/tools/send-email-tool/send-email-tool';
|
||||
import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
|
||||
import { type Tool } from 'src/engine/core-modules/tool/types/tool.type';
|
||||
@@ -41,7 +41,7 @@ export class ToolProviderService {
|
||||
// Action tools (individual tools)
|
||||
private readonly httpTool: HttpTool,
|
||||
private readonly sendEmailTool: SendEmailTool,
|
||||
private readonly searchArticlesTool: SearchArticlesTool,
|
||||
private readonly searchHelpCenterTool: SearchHelpCenterTool,
|
||||
// Database CRUD tools
|
||||
private readonly perObjectToolGenerator: PerObjectToolGeneratorService,
|
||||
private readonly createRecordService: CreateRecordService,
|
||||
@@ -80,9 +80,9 @@ export class ToolProviderService {
|
||||
},
|
||||
],
|
||||
[
|
||||
ToolType.SEARCH_ARTICLES,
|
||||
ToolType.SEARCH_HELP_CENTER,
|
||||
{
|
||||
tool: this.searchArticlesTool,
|
||||
tool: this.searchHelpCenterTool,
|
||||
// No permission flag - available to all agents
|
||||
},
|
||||
],
|
||||
|
||||
+58
-439
@@ -1,28 +1,16 @@
|
||||
import { Inject, Injectable, Logger, Optional } from '@nestjs/common';
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { type ToolSet } from 'ai';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { type ActorMetadata } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
|
||||
import { CreateRecordService } from 'src/engine/core-modules/record-crud/services/create-record.service';
|
||||
import { DeleteRecordService } from 'src/engine/core-modules/record-crud/services/delete-record.service';
|
||||
import { FindRecordsService } from 'src/engine/core-modules/record-crud/services/find-records.service';
|
||||
import { UpdateRecordService } from 'src/engine/core-modules/record-crud/services/update-record.service';
|
||||
import { createDirectRecordToolsFactory } from 'src/engine/core-modules/record-crud/tool-factory/direct-record-tools.factory';
|
||||
import { WORKFLOW_TOOL_SERVICE_TOKEN } from 'src/engine/core-modules/tool-provider/constants/workflow-tool-service.token';
|
||||
import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool';
|
||||
import { SearchArticlesTool } from 'src/engine/core-modules/tool/tools/search-articles-tool/search-articles-tool';
|
||||
import { SendEmailTool } from 'src/engine/core-modules/tool/tools/send-email-tool/send-email-tool';
|
||||
import { isWorkflowRelatedObject } from 'src/engine/metadata-modules/ai/ai-agent/utils/is-workflow-related-object.util';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
import {
|
||||
type ToolProvider,
|
||||
type ToolProviderContext,
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
|
||||
import { TOOL_PROVIDERS } from 'src/engine/core-modules/tool-provider/constants/tool-providers.token';
|
||||
import { type ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
import { computePermissionIntersection } from 'src/engine/twenty-orm/utils/compute-permission-intersection.util';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import type { WorkflowToolWorkspaceService } from 'src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service';
|
||||
|
||||
export type ToolIndexEntry = {
|
||||
name: string;
|
||||
@@ -43,109 +31,35 @@ export type ToolContext = {
|
||||
actorContext?: ActorMetadata;
|
||||
};
|
||||
|
||||
// Workflow tool definitions for the index (static metadata)
|
||||
const WORKFLOW_TOOLS_METADATA: Array<{ name: string; description: string }> = [
|
||||
{
|
||||
name: 'create_complete_workflow',
|
||||
description:
|
||||
'Create a complete workflow with trigger, steps, and connections in a single operation',
|
||||
},
|
||||
{
|
||||
name: 'create_workflow_version_step',
|
||||
description: 'Create a new step in a workflow version',
|
||||
},
|
||||
{
|
||||
name: 'update_workflow_version_step',
|
||||
description: 'Update an existing step in a workflow version',
|
||||
},
|
||||
{
|
||||
name: 'delete_workflow_version_step',
|
||||
description: 'Delete a step from a workflow version',
|
||||
},
|
||||
{
|
||||
name: 'create_workflow_version_edge',
|
||||
description: 'Create a connection (edge) between two workflow steps',
|
||||
},
|
||||
{
|
||||
name: 'delete_workflow_version_edge',
|
||||
description: 'Delete a connection (edge) between workflow steps',
|
||||
},
|
||||
{
|
||||
name: 'create_draft_from_workflow_version',
|
||||
description: 'Create a new draft workflow version from an existing one',
|
||||
},
|
||||
{
|
||||
name: 'update_workflow_version_positions',
|
||||
description: 'Update the positions of multiple workflow steps',
|
||||
},
|
||||
{
|
||||
name: 'activate_workflow_version',
|
||||
description:
|
||||
'Activate a workflow version to make it available for execution',
|
||||
},
|
||||
{
|
||||
name: 'deactivate_workflow_version',
|
||||
description: 'Deactivate a workflow version',
|
||||
},
|
||||
{
|
||||
name: 'compute_step_output_schema',
|
||||
description: 'Compute the output schema for a workflow step',
|
||||
},
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
export class ToolRegistryService {
|
||||
private readonly logger = new Logger(ToolRegistryService.name);
|
||||
|
||||
constructor(
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly permissionsService: PermissionsService,
|
||||
private readonly httpTool: HttpTool,
|
||||
private readonly sendEmailTool: SendEmailTool,
|
||||
private readonly searchArticlesTool: SearchArticlesTool,
|
||||
private readonly createRecordService: CreateRecordService,
|
||||
private readonly updateRecordService: UpdateRecordService,
|
||||
private readonly deleteRecordService: DeleteRecordService,
|
||||
private readonly findRecordsService: FindRecordsService,
|
||||
@Optional()
|
||||
@Inject(WORKFLOW_TOOL_SERVICE_TOKEN)
|
||||
private readonly workflowToolService: WorkflowToolWorkspaceService | null,
|
||||
@Inject(TOOL_PROVIDERS)
|
||||
private readonly providers: ToolProvider[],
|
||||
) {}
|
||||
|
||||
async buildToolIndex(
|
||||
workspaceId: string,
|
||||
roleId: string,
|
||||
): Promise<ToolIndexEntry[]> {
|
||||
const index: ToolIndexEntry[] = [];
|
||||
const context = this.buildContext(workspaceId, roleId);
|
||||
const entries: ToolIndexEntry[] = [];
|
||||
|
||||
const actionTools = await this.getActionToolIndex(workspaceId, roleId);
|
||||
for (const provider of this.providers) {
|
||||
if (await provider.isAvailable(context)) {
|
||||
const tools = await provider.generateTools(context);
|
||||
|
||||
index.push(...actionTools);
|
||||
|
||||
const databaseTools = await this.getDatabaseToolIndex(workspaceId, roleId);
|
||||
|
||||
index.push(...databaseTools);
|
||||
|
||||
if (this.workflowToolService) {
|
||||
const workflowTools = this.getWorkflowToolIndex();
|
||||
|
||||
index.push(...workflowTools);
|
||||
entries.push(...this.toolSetToIndex(tools, provider.category));
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Built tool index with ${index.length} tools for workspace ${workspaceId}`,
|
||||
`Built tool index with ${entries.length} tools for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
private getWorkflowToolIndex(): ToolIndexEntry[] {
|
||||
return WORKFLOW_TOOLS_METADATA.map((tool) => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
category: 'workflow' as const,
|
||||
}));
|
||||
return entries;
|
||||
}
|
||||
|
||||
async searchTools(
|
||||
@@ -170,17 +84,14 @@ export class ToolRegistryService {
|
||||
const descLower = tool.description.toLowerCase();
|
||||
const objectLower = tool.objectName?.toLowerCase() ?? '';
|
||||
|
||||
// Exact name match - highest priority
|
||||
if (nameLower.includes(queryLower)) {
|
||||
score += 100;
|
||||
}
|
||||
|
||||
// Object name match - high priority
|
||||
if (objectLower && queryLower.includes(objectLower)) {
|
||||
score += 80;
|
||||
}
|
||||
|
||||
// Term matches in name
|
||||
for (const term of queryTerms) {
|
||||
if (nameLower.includes(term)) {
|
||||
score += 30;
|
||||
@@ -193,7 +104,6 @@ export class ToolRegistryService {
|
||||
}
|
||||
}
|
||||
|
||||
// Operation keyword matches
|
||||
const operations = ['find', 'create', 'update', 'delete', 'search'];
|
||||
|
||||
for (const op of operations) {
|
||||
@@ -220,346 +130,55 @@ export class ToolRegistryService {
|
||||
names: string[],
|
||||
context: ToolContext,
|
||||
): Promise<ToolSet> {
|
||||
const tools: ToolSet = {};
|
||||
const rolePermissionConfig: RolePermissionConfig = {
|
||||
intersectionOf: [context.roleId],
|
||||
};
|
||||
const fullContext = this.buildContext(context.workspaceId, context.roleId);
|
||||
const allTools: ToolSet = {};
|
||||
|
||||
for (const name of names) {
|
||||
const tool = await this.getToolByName(
|
||||
name,
|
||||
context.workspaceId,
|
||||
rolePermissionConfig,
|
||||
context.actorContext,
|
||||
);
|
||||
for (const provider of this.providers) {
|
||||
if (await provider.isAvailable(fullContext)) {
|
||||
const tools = await provider.generateTools(fullContext);
|
||||
|
||||
if (tool) {
|
||||
tools[name] = tool;
|
||||
Object.assign(allTools, tools);
|
||||
}
|
||||
}
|
||||
|
||||
return tools;
|
||||
return Object.fromEntries(
|
||||
names
|
||||
.filter((name) => name in allTools)
|
||||
.map((name) => [name, allTools[name]]),
|
||||
);
|
||||
}
|
||||
|
||||
private async getToolByName(
|
||||
name: string,
|
||||
private buildContext(
|
||||
workspaceId: string,
|
||||
rolePermissionConfig: RolePermissionConfig,
|
||||
actorContext?: ActorMetadata,
|
||||
): Promise<ToolSet[string] | null> {
|
||||
const actionTool = this.getActionToolByName(name, workspaceId);
|
||||
roleId: string,
|
||||
): ToolProviderContext {
|
||||
const rolePermissionConfig: RolePermissionConfig = {
|
||||
unionOf: [roleId],
|
||||
};
|
||||
|
||||
if (actionTool) {
|
||||
return actionTool;
|
||||
}
|
||||
return {
|
||||
workspaceId,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
};
|
||||
}
|
||||
|
||||
const workflowTool = await this.getWorkflowToolByName(
|
||||
private toolSetToIndex(
|
||||
tools: ToolSet,
|
||||
category: ToolCategory,
|
||||
): ToolIndexEntry[] {
|
||||
const categoryMap: Record<ToolCategory, ToolIndexEntry['category']> = {
|
||||
DATABASE_CRUD: 'database',
|
||||
ACTION: 'action',
|
||||
WORKFLOW: 'workflow',
|
||||
METADATA: 'metadata',
|
||||
NATIVE_MODEL: 'action',
|
||||
};
|
||||
|
||||
return Object.entries(tools).map(([name, tool]) => ({
|
||||
name,
|
||||
workspaceId,
|
||||
rolePermissionConfig,
|
||||
);
|
||||
|
||||
if (workflowTool) {
|
||||
return workflowTool;
|
||||
}
|
||||
|
||||
const match = name.match(
|
||||
/^(find|find_one|create|update|soft_delete)_(.+)$/,
|
||||
);
|
||||
|
||||
if (match) {
|
||||
const [, _operation, objectName] = match;
|
||||
const dbTools = await this.getDatabaseToolsForObject(
|
||||
workspaceId,
|
||||
rolePermissionConfig,
|
||||
objectName,
|
||||
actorContext,
|
||||
);
|
||||
|
||||
return dbTools[name] ?? null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async getWorkflowToolByName(
|
||||
name: string,
|
||||
workspaceId: string,
|
||||
rolePermissionConfig: RolePermissionConfig,
|
||||
): Promise<ToolSet[string] | null> {
|
||||
if (!this.workflowToolService) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isWorkflowTool = WORKFLOW_TOOLS_METADATA.some(
|
||||
(tool) => tool.name === name,
|
||||
);
|
||||
|
||||
if (!isWorkflowTool) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Generate workflow tools and return the requested one
|
||||
const workflowTools = this.workflowToolService.generateWorkflowTools(
|
||||
workspaceId,
|
||||
rolePermissionConfig,
|
||||
);
|
||||
|
||||
return workflowTools[name] ?? null;
|
||||
}
|
||||
|
||||
private async getActionToolIndex(
|
||||
workspaceId: string,
|
||||
roleId: string,
|
||||
): Promise<ToolIndexEntry[]> {
|
||||
const index: ToolIndexEntry[] = [];
|
||||
const rolePermissionConfig: RolePermissionConfig = {
|
||||
intersectionOf: [roleId],
|
||||
};
|
||||
|
||||
// HTTP Request tool
|
||||
const hasHttpPermission = await this.permissionsService.hasToolPermission(
|
||||
rolePermissionConfig,
|
||||
workspaceId,
|
||||
PermissionFlagType.HTTP_REQUEST_TOOL,
|
||||
);
|
||||
|
||||
if (hasHttpPermission) {
|
||||
index.push({
|
||||
name: 'http_request',
|
||||
description: this.httpTool.description,
|
||||
category: 'action',
|
||||
});
|
||||
}
|
||||
|
||||
// Send Email tool
|
||||
const hasEmailPermission = await this.permissionsService.hasToolPermission(
|
||||
rolePermissionConfig,
|
||||
workspaceId,
|
||||
PermissionFlagType.SEND_EMAIL_TOOL,
|
||||
);
|
||||
|
||||
if (hasEmailPermission) {
|
||||
index.push({
|
||||
name: 'send_email',
|
||||
description: this.sendEmailTool.description,
|
||||
category: 'action',
|
||||
});
|
||||
}
|
||||
|
||||
index.push({
|
||||
name: 'search_articles',
|
||||
description: this.searchArticlesTool.description,
|
||||
category: 'action',
|
||||
});
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
private getActionToolByName(
|
||||
name: string,
|
||||
workspaceId: string,
|
||||
): ToolSet[string] | null {
|
||||
switch (name) {
|
||||
case 'http_request':
|
||||
return {
|
||||
description: this.httpTool.description,
|
||||
inputSchema: this.httpTool.inputSchema,
|
||||
execute: async (parameters: {
|
||||
input: z.infer<typeof this.httpTool.inputSchema>['input'];
|
||||
}) => this.httpTool.execute(parameters.input, workspaceId),
|
||||
};
|
||||
case 'send_email':
|
||||
return {
|
||||
description: this.sendEmailTool.description,
|
||||
inputSchema: this.sendEmailTool.inputSchema,
|
||||
execute: async (parameters: {
|
||||
input: z.infer<typeof this.sendEmailTool.inputSchema>['input'];
|
||||
}) => this.sendEmailTool.execute(parameters.input, workspaceId),
|
||||
};
|
||||
case 'search_articles':
|
||||
return {
|
||||
description: this.searchArticlesTool.description,
|
||||
inputSchema: this.searchArticlesTool.inputSchema,
|
||||
execute: async (parameters: {
|
||||
input: z.infer<typeof this.searchArticlesTool.inputSchema>['input'];
|
||||
}) => this.searchArticlesTool.execute(parameters.input),
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async getDatabaseToolIndex(
|
||||
workspaceId: string,
|
||||
roleId: string,
|
||||
): Promise<ToolIndexEntry[]> {
|
||||
const index: ToolIndexEntry[] = [];
|
||||
|
||||
const { rolesPermissions } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'rolesPermissions',
|
||||
]);
|
||||
|
||||
const objectPermissions = rolesPermissions[roleId];
|
||||
|
||||
if (!objectPermissions) {
|
||||
return index;
|
||||
}
|
||||
|
||||
const { flatObjectMetadataMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const allFlatObjects = Object.values(flatObjectMetadataMaps.byId)
|
||||
.filter(isDefined)
|
||||
.filter((obj) => obj.isActive && !obj.isSystem);
|
||||
|
||||
for (const flatObject of allFlatObjects) {
|
||||
if (isWorkflowRelatedObject(flatObject)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const permission = objectPermissions[flatObject.id];
|
||||
|
||||
if (!permission) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const objectName = flatObject.nameSingular;
|
||||
const objectLabel = flatObject.labelSingular;
|
||||
|
||||
if (permission.canReadObjectRecords) {
|
||||
index.push({
|
||||
name: `find_${objectName}`,
|
||||
description: `Search and find ${objectLabel} records`,
|
||||
category: 'database',
|
||||
objectName,
|
||||
operation: 'find',
|
||||
});
|
||||
|
||||
index.push({
|
||||
name: `find_one_${objectName}`,
|
||||
description: `Get a single ${objectLabel} record by ID`,
|
||||
category: 'database',
|
||||
objectName,
|
||||
operation: 'find_one',
|
||||
});
|
||||
}
|
||||
|
||||
if (permission.canUpdateObjectRecords) {
|
||||
index.push({
|
||||
name: `create_${objectName}`,
|
||||
description: `Create new ${objectLabel} records`,
|
||||
category: 'database',
|
||||
objectName,
|
||||
operation: 'create',
|
||||
});
|
||||
|
||||
index.push({
|
||||
name: `update_${objectName}`,
|
||||
description: `Update existing ${objectLabel} records`,
|
||||
category: 'database',
|
||||
objectName,
|
||||
operation: 'update',
|
||||
});
|
||||
}
|
||||
|
||||
if (permission.canSoftDeleteObjectRecords) {
|
||||
index.push({
|
||||
name: `soft_delete_${objectName}`,
|
||||
description: `Soft delete ${objectLabel} records`,
|
||||
category: 'database',
|
||||
objectName,
|
||||
operation: 'soft_delete',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
private async getDatabaseToolsForObject(
|
||||
workspaceId: string,
|
||||
rolePermissionConfig: RolePermissionConfig,
|
||||
objectName: string,
|
||||
actorContext?: ActorMetadata,
|
||||
): Promise<ToolSet> {
|
||||
const { rolesPermissions } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'rolesPermissions',
|
||||
]);
|
||||
|
||||
let objectPermissions;
|
||||
|
||||
if ('intersectionOf' in rolePermissionConfig) {
|
||||
const allRolePermissions = rolePermissionConfig.intersectionOf.map(
|
||||
(roleId: string) => rolesPermissions[roleId],
|
||||
);
|
||||
|
||||
objectPermissions =
|
||||
allRolePermissions.length === 1
|
||||
? allRolePermissions[0]
|
||||
: computePermissionIntersection(allRolePermissions);
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { flatObjectMetadataMaps, flatFieldMetadataMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const flatObject = Object.values(flatObjectMetadataMaps.byId)
|
||||
.filter(isDefined)
|
||||
.find((obj) => obj.nameSingular === objectName);
|
||||
|
||||
if (!flatObject) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const permission = objectPermissions[flatObject.id];
|
||||
|
||||
if (!permission) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const objectMetadata = {
|
||||
...flatObject,
|
||||
fields: getFlatFieldsFromFlatObjectMetadata(
|
||||
flatObject,
|
||||
flatFieldMetadataMaps,
|
||||
),
|
||||
};
|
||||
|
||||
const factory = createDirectRecordToolsFactory({
|
||||
createRecordService: this.createRecordService,
|
||||
updateRecordService: this.updateRecordService,
|
||||
deleteRecordService: this.deleteRecordService,
|
||||
findRecordsService: this.findRecordsService,
|
||||
});
|
||||
|
||||
return factory(
|
||||
{
|
||||
objectMetadata,
|
||||
restrictedFields: permission.restrictedFields,
|
||||
canCreate: permission.canUpdateObjectRecords,
|
||||
canRead: permission.canReadObjectRecords,
|
||||
canUpdate: permission.canUpdateObjectRecords,
|
||||
canDelete: permission.canSoftDeleteObjectRecords,
|
||||
},
|
||||
{
|
||||
workspaceId,
|
||||
rolePermissionConfig,
|
||||
actorContext,
|
||||
},
|
||||
);
|
||||
description: tool.description ?? '',
|
||||
category: categoryMap[category],
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
+33
-2
@@ -2,6 +2,11 @@ import { forwardRef, Module } from '@nestjs/common';
|
||||
|
||||
import { RecordCrudModule } from 'src/engine/core-modules/record-crud/record-crud.module';
|
||||
import { ToolGeneratorModule } from 'src/engine/core-modules/tool-generator/tool-generator.module';
|
||||
import { TOOL_PROVIDERS } from 'src/engine/core-modules/tool-provider/constants/tool-providers.token';
|
||||
import { ActionToolProvider } from 'src/engine/core-modules/tool-provider/providers/action-tool.provider';
|
||||
import { DatabaseToolProvider } from 'src/engine/core-modules/tool-provider/providers/database-tool.provider';
|
||||
import { MetadataToolProvider } from 'src/engine/core-modules/tool-provider/providers/metadata-tool.provider';
|
||||
import { WorkflowToolProvider } from 'src/engine/core-modules/tool-provider/providers/workflow-tool.provider';
|
||||
import { ToolModule } from 'src/engine/core-modules/tool/tool.module';
|
||||
import { AiAgentExecutionModule } from 'src/engine/metadata-modules/ai/ai-agent-execution/ai-agent-execution.module';
|
||||
import { AiModelsModule } from 'src/engine/metadata-modules/ai/ai-models/ai-models.module';
|
||||
@@ -28,7 +33,6 @@ import { ToolRegistryService } from './services/tool-registry.service';
|
||||
ToolGeneratorModule,
|
||||
RecordCrudModule,
|
||||
AiModelsModule,
|
||||
// forwardRef needed: AiAgentExecutionModule imports ToolProviderModule
|
||||
forwardRef(() => AiAgentExecutionModule),
|
||||
ObjectMetadataModule,
|
||||
FieldMetadataModule,
|
||||
@@ -36,7 +40,34 @@ import { ToolRegistryService } from './services/tool-registry.service';
|
||||
WorkspaceCacheModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
],
|
||||
providers: [ToolProviderService, ToolRegistryService],
|
||||
providers: [
|
||||
ActionToolProvider,
|
||||
DatabaseToolProvider,
|
||||
MetadataToolProvider,
|
||||
WorkflowToolProvider,
|
||||
{
|
||||
provide: TOOL_PROVIDERS,
|
||||
useFactory: (
|
||||
actionProvider: ActionToolProvider,
|
||||
databaseProvider: DatabaseToolProvider,
|
||||
metadataProvider: MetadataToolProvider,
|
||||
workflowProvider: WorkflowToolProvider,
|
||||
) => [
|
||||
actionProvider,
|
||||
databaseProvider,
|
||||
metadataProvider,
|
||||
workflowProvider,
|
||||
],
|
||||
inject: [
|
||||
ActionToolProvider,
|
||||
DatabaseToolProvider,
|
||||
MetadataToolProvider,
|
||||
WorkflowToolProvider,
|
||||
],
|
||||
},
|
||||
ToolProviderService,
|
||||
ToolRegistryService,
|
||||
],
|
||||
exports: [ToolProviderService, ToolRegistryService],
|
||||
})
|
||||
export class ToolProviderModule {}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export enum ToolType {
|
||||
HTTP_REQUEST = 'HTTP_REQUEST',
|
||||
SEND_EMAIL = 'SEND_EMAIL',
|
||||
SEARCH_ARTICLES = 'SEARCH_ARTICLES',
|
||||
SEARCH_HELP_CENTER = 'SEARCH_HELP_CENTER',
|
||||
}
|
||||
|
||||
+2
-2
@@ -4,10 +4,10 @@ import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { ToolType } from 'src/engine/core-modules/tool/enums/tool-type.enum';
|
||||
import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool';
|
||||
import { SendEmailTool } from 'src/engine/core-modules/tool/tools/send-email-tool/send-email-tool';
|
||||
import { type SendEmailTool } from 'src/engine/core-modules/tool/tools/send-email-tool/send-email-tool';
|
||||
import { type SendEmailInput } from 'src/engine/core-modules/tool/tools/send-email-tool/types/send-email-input.type';
|
||||
import { type Tool } from 'src/engine/core-modules/tool/types/tool.type';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { type TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
@Injectable()
|
||||
export class ToolRegistryService {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
|
||||
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||
import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool';
|
||||
import { SearchArticlesTool } from 'src/engine/core-modules/tool/tools/search-articles-tool/search-articles-tool';
|
||||
import { SearchHelpCenterTool } from 'src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool';
|
||||
import { SendEmailTool } from 'src/engine/core-modules/tool/tools/send-email-tool/send-email-tool';
|
||||
import { MessagingImportManagerModule } from 'src/modules/messaging/message-import-manager/messaging-import-manager.module';
|
||||
|
||||
@@ -14,7 +14,7 @@ import { MessagingImportManagerModule } from 'src/modules/messaging/message-impo
|
||||
TypeOrmModule.forFeature([FileEntity]),
|
||||
FileModule,
|
||||
],
|
||||
providers: [HttpTool, SendEmailTool, SearchArticlesTool],
|
||||
exports: [HttpTool, SendEmailTool, SearchArticlesTool],
|
||||
providers: [HttpTool, SendEmailTool, SearchHelpCenterTool],
|
||||
exports: [HttpTool, SendEmailTool, SearchHelpCenterTool],
|
||||
})
|
||||
export class ToolModule {}
|
||||
|
||||
+7
-5
@@ -1,18 +1,20 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const SearchArticlesInputZodSchema = z.object({
|
||||
export const SearchHelpCenterInputZodSchema = z.object({
|
||||
query: z
|
||||
.string()
|
||||
.describe('The search query to find relevant help articles about Twenty'),
|
||||
});
|
||||
|
||||
export const SearchArticlesToolParametersZodSchema = z.object({
|
||||
export const SearchHelpCenterToolParametersZodSchema = z.object({
|
||||
loadingMessage: z
|
||||
.string()
|
||||
.describe(
|
||||
'A clear, human-readable status message describing the search being performed. This will be shown to the user while the tool is being called, so phrase it as a present-tense status update (e.g., "Searching help articles for..."). Explain what you are searching for in natural language.',
|
||||
'A clear, human-readable status message describing the search being performed. This will be shown to the user while the tool is being called, so phrase it as a present-tense status update (e.g., "Searching help center for..."). Explain what you are searching for in natural language.',
|
||||
),
|
||||
input: SearchArticlesInputZodSchema,
|
||||
input: SearchHelpCenterInputZodSchema,
|
||||
});
|
||||
|
||||
export type SearchArticlesInput = z.infer<typeof SearchArticlesInputZodSchema>;
|
||||
export type SearchHelpCenterInput = z.infer<
|
||||
typeof SearchHelpCenterInputZodSchema
|
||||
>;
|
||||
+8
-8
@@ -2,17 +2,17 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
import { SearchArticlesToolParametersZodSchema } from 'src/engine/core-modules/tool/tools/search-articles-tool/search-articles-tool.schema';
|
||||
import { SearchHelpCenterToolParametersZodSchema } from 'src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool.schema';
|
||||
import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
|
||||
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
|
||||
import { type Tool } from 'src/engine/core-modules/tool/types/tool.type';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
@Injectable()
|
||||
export class SearchArticlesTool implements Tool {
|
||||
export class SearchHelpCenterTool implements Tool {
|
||||
description =
|
||||
'Search Twenty documentation and help articles to find information about features, setup, usage, and troubleshooting.';
|
||||
inputSchema = SearchArticlesToolParametersZodSchema;
|
||||
'Search Twenty documentation and help center to find information about features, setup, usage, and troubleshooting.';
|
||||
inputSchema = SearchHelpCenterToolParametersZodSchema;
|
||||
|
||||
constructor(private readonly twentyConfigService: TwentyConfigService) {}
|
||||
|
||||
@@ -46,14 +46,14 @@ export class SearchArticlesTool implements Tool {
|
||||
if (results.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
message: `No help articles found for "${query}"`,
|
||||
message: `No help center articles found for "${query}"`,
|
||||
result: [],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Found ${results.length} relevant help article${results.length === 1 ? '' : 's'} for "${query}"`,
|
||||
message: `Found ${results.length} relevant help center article${results.length === 1 ? '' : 's'} for "${query}"`,
|
||||
result: results,
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -61,11 +61,11 @@ export class SearchArticlesTool implements Tool {
|
||||
? error.response?.data?.message || error.message
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: 'Documentation search failed';
|
||||
: 'Help center search failed';
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to search help articles for "${query}"`,
|
||||
message: `Failed to search help center for "${query}"`,
|
||||
error: errorDetail,
|
||||
};
|
||||
}
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { render, toPlainText } from '@react-email/render';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { reactMarkupFromJSON } from 'twenty-emails';
|
||||
import { isDefined, isValidUuid } from 'twenty-shared/utils';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { In, type Repository } from 'typeorm';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
|
||||
|
||||
Reference in New Issue
Block a user