refactor(tool-provider): dedupe descriptor/generator paths (#19960)
## Summary - Every tool provider used to implement `generateDescriptors()` **and** register a category generator at `onModuleInit()` that re-ran the same factories at execute time. `ToolExecutorService` carried two registries (`staticToolHandlers`, `categoryGenerators`) to route between them. - Providers now own execution of their own tools via a new `executeStaticTool()` method. `ToolExecutorService` drops both maps and delegates by `descriptor.category`. Each factory-backed provider has a single `buildToolSet()` used by both descriptor generation and execution. - Extracts `resolveObjectIcon` shared util (was duplicated verbatim in workflow + dashboard providers), and deletes the orphaned `ToolGeneratorModule` whose consumers were removed in the earlier AI chat simplification refactor. No behavior change. Same factories run, same permission checks, same tools execute. Net diff: 18 files, +311 / −480. ## Key changes - `ToolProvider` interface gains `executeStaticTool(name, args, context)`. - `ToolExecutorService` loses its `staticToolHandlers` and `categoryGenerators` maps, injects `TOOL_PROVIDERS`, and does `providers.find(p => p.category === descriptor.category).executeStaticTool(...)` for `kind: 'static'` descriptors. - `ActionToolProvider` drops the register-handler loop in its constructor; `executeStaticTool` looks up in the existing `toolMap`. - `View`, `Metadata`, `Workflow`, `Dashboard`, `ViewField` providers each have a single `buildToolSet(context)` private method used by both `generateDescriptors` and `executeStaticTool`. No more `onModuleInit`, no `ToolExecutorService` dependency. - `DatabaseToolProvider` and `LogicFunctionToolProvider` implement `executeStaticTool` with an invariant-violation throw — they only emit `database_crud` / `logic_function` kinds, so the static-tool path is unreachable for them. - Deletes `tool-generator/` (dead code — zero consumers). ## Dependency graph before/after **Before:** provider → `ToolExecutorService` (for `register*` calls) **After:** `ToolExecutorService` → `TOOL_PROVIDERS` → providers. Cleaner, no cycle. ## Test plan - [ ] `npx nx typecheck twenty-server` passes (verified: same 7 pre-existing unrelated errors) - [ ] `npx nx lint twenty-server` passes - [ ] AI chat: trigger a tool call that hits `execute_tool` fallback (e.g. a view/metadata tool not in the preloaded set) — verify it still executes - [ ] AI chat: trigger a preloaded action tool (e.g. `search_help_center`) — verify it still executes - [ ] MCP: `tools/list` and `tools/call` for both preloaded and catalog-discovered tools - [ ] Workflow AI agent: run a workflow with AI agent step that calls DATABASE_CRUD tools - [ ] Verify the `web_search` / `code_interpreter` tools (if enabled) still dispatch correctly --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
-134
@@ -1,134 +0,0 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { type ToolSet } from 'ai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
|
||||
import {
|
||||
type ObjectWithPermission,
|
||||
type ToolFactory,
|
||||
type ToolGeneratorContext,
|
||||
} from 'src/engine/core-modules/tool-generator/types/tool-generator.types';
|
||||
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 { 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';
|
||||
|
||||
@Injectable()
|
||||
export class PerObjectToolGeneratorService {
|
||||
private readonly logger = new Logger(PerObjectToolGeneratorService.name);
|
||||
|
||||
constructor(
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {}
|
||||
|
||||
// Generate tools by iterating over workspace objects once and applying all factories
|
||||
async generate(
|
||||
context: ToolGeneratorContext,
|
||||
factories: ToolFactory[],
|
||||
): Promise<ToolSet> {
|
||||
const objects = await this.getObjectsWithPermissions(
|
||||
context.workspaceId,
|
||||
context.rolePermissionConfig,
|
||||
);
|
||||
|
||||
const tools: ToolSet = {};
|
||||
|
||||
for (const objectWithPermission of objects) {
|
||||
for (const factory of factories) {
|
||||
Object.assign(tools, factory(objectWithPermission, context));
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Generated ${Object.keys(tools).length} tools from ${factories.length} factories for ${objects.length} objects`,
|
||||
);
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
// Get workspace objects with their permissions
|
||||
async getObjectsWithPermissions(
|
||||
workspaceId: string,
|
||||
rolePermissionConfig: RolePermissionConfig,
|
||||
): Promise<ObjectWithPermission[]> {
|
||||
const { rolesPermissions } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'rolesPermissions',
|
||||
]);
|
||||
|
||||
let objectPermissions;
|
||||
|
||||
if ('unionOf' in rolePermissionConfig) {
|
||||
if (rolePermissionConfig.unionOf.length === 1) {
|
||||
objectPermissions = rolesPermissions[rolePermissionConfig.unionOf[0]];
|
||||
} else {
|
||||
throw new Error(
|
||||
'Union permission logic for multiple roles not yet implemented',
|
||||
);
|
||||
}
|
||||
} else 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 allFlatObjects = Object.values(
|
||||
flatObjectMetadataMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((obj) => obj.isActive && !obj.isSystem);
|
||||
|
||||
const allObjectMetadata = allFlatObjects.map((flatObject) => ({
|
||||
...flatObject,
|
||||
fields: getFlatFieldsFromFlatObjectMetadata(
|
||||
flatObject,
|
||||
flatFieldMetadataMaps,
|
||||
),
|
||||
}));
|
||||
|
||||
// Filter out workflow-related objects
|
||||
const filteredObjectMetadata = allObjectMetadata.filter(
|
||||
(objectMetadata) => !isWorkflowRelatedObject(objectMetadata),
|
||||
);
|
||||
|
||||
// Map to ObjectWithPermission
|
||||
const result: ObjectWithPermission[] = [];
|
||||
|
||||
for (const objectMetadata of filteredObjectMetadata) {
|
||||
const permission = objectPermissions[objectMetadata.id];
|
||||
|
||||
if (!permission) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push({
|
||||
objectMetadata,
|
||||
restrictedFields: permission.restrictedFields,
|
||||
canCreate: permission.canUpdateObjectRecords,
|
||||
canRead: permission.canReadObjectRecords,
|
||||
canUpdate: permission.canUpdateObjectRecords,
|
||||
canDelete: permission.canSoftDeleteObjectRecords,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
|
||||
import { PerObjectToolGeneratorService } from './services/per-object-tool-generator.service';
|
||||
|
||||
@Module({
|
||||
imports: [WorkspaceCacheModule, WorkspaceManyOrAllFlatEntityMapsCacheModule],
|
||||
providers: [PerObjectToolGeneratorService],
|
||||
exports: [PerObjectToolGeneratorService],
|
||||
})
|
||||
export class ToolGeneratorModule {}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
import { type ToolSet } from 'ai';
|
||||
import {
|
||||
type ActorMetadata,
|
||||
type RestrictedFieldsPermissions,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
|
||||
import { type ObjectMetadataForToolSchema } from 'src/engine/core-modules/record-crud/types/object-metadata-for-tool-schema.type';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
|
||||
// Object metadata enriched with permission information
|
||||
export type ObjectWithPermission = {
|
||||
objectMetadata: ObjectMetadataForToolSchema;
|
||||
restrictedFields: RestrictedFieldsPermissions;
|
||||
canCreate: boolean;
|
||||
canRead: boolean;
|
||||
canUpdate: boolean;
|
||||
canDelete: boolean;
|
||||
};
|
||||
|
||||
// Context passed to tool factories
|
||||
export type ToolGeneratorContext = {
|
||||
workspaceId: string;
|
||||
authContext?: WorkspaceAuthContext;
|
||||
rolePermissionConfig: RolePermissionConfig;
|
||||
actorContext?: ActorMetadata;
|
||||
};
|
||||
|
||||
// Factory function type for generating tools from an object
|
||||
export type ToolFactory = (
|
||||
objectWithPermission: ObjectWithPermission,
|
||||
context: ToolGeneratorContext,
|
||||
) => ToolSet;
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-context.type';
|
||||
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';
|
||||
|
||||
export interface StaticToolHandler {
|
||||
execute(args: ToolInput, context: ToolProviderContext): Promise<ToolOutput>;
|
||||
}
|
||||
+13
@@ -4,6 +4,7 @@ import { type GenerateDescriptorOptions } from 'src/engine/core-modules/tool-pro
|
||||
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 { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
|
||||
|
||||
export interface ToolProvider {
|
||||
readonly category: ToolCategory;
|
||||
@@ -14,4 +15,16 @@ export interface ToolProvider {
|
||||
context: ToolProviderContext,
|
||||
options?: GenerateDescriptorOptions,
|
||||
): Promise<(ToolIndexEntry | ToolDescriptor)[]>;
|
||||
|
||||
// Execute a tool whose descriptor has `executionRef.kind === 'static'` and
|
||||
// `descriptor.category === this.category`. Providers own the execution of
|
||||
// the tools they emit.
|
||||
//
|
||||
// Providers that never emit 'static' descriptors (database CRUD, logic
|
||||
// functions) should throw — the call is unreachable by construction.
|
||||
executeStaticTool(
|
||||
toolName: string,
|
||||
args: Record<string, unknown>,
|
||||
context: ToolProviderContext,
|
||||
): Promise<ToolOutput>;
|
||||
}
|
||||
|
||||
+22
-19
@@ -8,8 +8,6 @@ import { type ToolProvider } from 'src/engine/core-modules/tool-provider/interfa
|
||||
import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-context.type';
|
||||
|
||||
import { ToolCategory } from 'twenty-shared/ai';
|
||||
import { type StaticToolHandler } from 'src/engine/core-modules/tool-provider/interfaces/static-tool-handler.interface';
|
||||
import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
|
||||
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 { CodeInterpreterService } from 'src/engine/core-modules/code-interpreter/code-interpreter.service';
|
||||
@@ -20,7 +18,7 @@ import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool
|
||||
import { NavigateAppTool } from 'src/engine/core-modules/tool/tools/navigate-tool/navigate-app-tool';
|
||||
import { SearchHelpCenterTool } from 'src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool';
|
||||
import { WebSearchTool } from 'src/engine/core-modules/tool/tools/web-search-tool/web-search-tool';
|
||||
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 { WebSearchService } from 'src/engine/core-modules/web-search/web-search.service';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
@@ -42,7 +40,6 @@ export class ActionToolProvider implements ToolProvider {
|
||||
private readonly codeInterpreterService: CodeInterpreterService,
|
||||
private readonly webSearchService: WebSearchService,
|
||||
private readonly permissionsService: PermissionsService,
|
||||
private readonly toolExecutorService: ToolExecutorService,
|
||||
) {
|
||||
this.toolMap = new Map<string, Tool>([
|
||||
['http_request', this.httpTool],
|
||||
@@ -53,21 +50,6 @@ export class ActionToolProvider implements ToolProvider {
|
||||
['navigate_app', this.navigateAppTool],
|
||||
['web_search', this.webSearchTool],
|
||||
]);
|
||||
|
||||
// Register each action tool as a static handler in the executor
|
||||
for (const [toolId, tool] of this.toolMap) {
|
||||
const handler: StaticToolHandler = {
|
||||
execute: async (args: ToolInput, context: ToolProviderContext) =>
|
||||
tool.execute(args, {
|
||||
workspaceId: context.workspaceId,
|
||||
userId: context.userId,
|
||||
userWorkspaceId: context.userWorkspaceId,
|
||||
onCodeExecutionUpdate: context.onCodeExecutionUpdate,
|
||||
}),
|
||||
};
|
||||
|
||||
this.toolExecutorService.registerStaticHandler(toolId, handler);
|
||||
}
|
||||
}
|
||||
|
||||
async isAvailable(_context: ToolProviderContext): Promise<boolean> {
|
||||
@@ -155,6 +137,27 @@ export class ActionToolProvider implements ToolProvider {
|
||||
return descriptors;
|
||||
}
|
||||
|
||||
async executeStaticTool(
|
||||
toolName: string,
|
||||
args: Record<string, unknown>,
|
||||
context: ToolProviderContext,
|
||||
): Promise<ToolOutput> {
|
||||
const tool = this.toolMap.get(toolName);
|
||||
|
||||
if (!tool) {
|
||||
throw new Error(
|
||||
`Unknown action tool "${toolName}" (category: ${this.category})`,
|
||||
);
|
||||
}
|
||||
|
||||
return tool.execute(args, {
|
||||
workspaceId: context.workspaceId,
|
||||
userId: context.userId,
|
||||
userWorkspaceId: context.userWorkspaceId,
|
||||
onCodeExecutionUpdate: context.onCodeExecutionUpdate,
|
||||
});
|
||||
}
|
||||
|
||||
private buildDescriptor(
|
||||
toolId: string,
|
||||
tool: Tool,
|
||||
|
||||
+41
-41
@@ -1,5 +1,6 @@
|
||||
import { Inject, Injectable, OnModuleInit, Optional } from '@nestjs/common';
|
||||
import { Inject, Injectable, Optional } from '@nestjs/common';
|
||||
|
||||
import { type ToolSet } from 'ai';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { type GenerateDescriptorOptions } from 'src/engine/core-modules/tool-provider/interfaces/generate-descriptor-options.type';
|
||||
@@ -9,16 +10,18 @@ import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/
|
||||
import { DASHBOARD_TOOL_SERVICE_TOKEN } from 'src/engine/core-modules/tool-provider/constants/dashboard-tool-service.token';
|
||||
import { ToolCategory } from 'twenty-shared/ai';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
|
||||
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 { resolveObjectIcon } from 'src/engine/core-modules/tool-provider/utils/resolve-object-icon.util';
|
||||
import { toolSetToDescriptors } from 'src/engine/core-modules/tool-provider/utils/tool-set-to-descriptors.util';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
|
||||
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 { DashboardToolWorkspaceService } from 'src/modules/dashboard/tools/services/dashboard-tool.workspace-service';
|
||||
|
||||
@Injectable()
|
||||
export class DashboardToolProvider implements ToolProvider, OnModuleInit {
|
||||
export class DashboardToolProvider implements ToolProvider {
|
||||
readonly category = ToolCategory.DASHBOARD;
|
||||
|
||||
constructor(
|
||||
@@ -26,25 +29,9 @@ export class DashboardToolProvider implements ToolProvider, OnModuleInit {
|
||||
@Inject(DASHBOARD_TOOL_SERVICE_TOKEN)
|
||||
private readonly dashboardToolService: DashboardToolWorkspaceService | null,
|
||||
private readonly permissionsService: PermissionsService,
|
||||
private readonly toolExecutorService: ToolExecutorService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {}
|
||||
|
||||
onModuleInit(): void {
|
||||
if (this.dashboardToolService) {
|
||||
const service = this.dashboardToolService;
|
||||
|
||||
this.toolExecutorService.registerCategoryGenerator(
|
||||
ToolCategory.DASHBOARD,
|
||||
async (context) =>
|
||||
service.generateDashboardTools(
|
||||
context.workspaceId,
|
||||
context.rolePermissionConfig,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async isAvailable(context: ToolProviderContext): Promise<boolean> {
|
||||
if (!this.dashboardToolService) {
|
||||
return false;
|
||||
@@ -61,16 +48,14 @@ export class DashboardToolProvider implements ToolProvider, OnModuleInit {
|
||||
context: ToolProviderContext,
|
||||
options?: GenerateDescriptorOptions,
|
||||
): Promise<(ToolIndexEntry | ToolDescriptor)[]> {
|
||||
if (!this.dashboardToolService) {
|
||||
const toolSet = await this.buildToolSet(context);
|
||||
|
||||
if (!toolSet) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const toolSet = await this.dashboardToolService.generateDashboardTools(
|
||||
context.workspaceId,
|
||||
context.rolePermissionConfig,
|
||||
);
|
||||
|
||||
const icon = await this.resolveObjectIcon(
|
||||
const icon = await resolveObjectIcon(
|
||||
this.flatEntityMapsCacheService,
|
||||
context.workspaceId,
|
||||
CoreObjectNameSingular.Dashboard,
|
||||
);
|
||||
@@ -81,22 +66,37 @@ export class DashboardToolProvider implements ToolProvider, OnModuleInit {
|
||||
});
|
||||
}
|
||||
|
||||
private async resolveObjectIcon(
|
||||
workspaceId: string,
|
||||
nameSingular: string,
|
||||
): Promise<string | undefined> {
|
||||
const { flatObjectMetadataMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps'],
|
||||
},
|
||||
async executeStaticTool(
|
||||
toolName: string,
|
||||
args: Record<string, unknown>,
|
||||
context: ToolProviderContext,
|
||||
): Promise<ToolOutput> {
|
||||
const toolSet = await this.buildToolSet(context);
|
||||
|
||||
if (!toolSet) {
|
||||
throw new Error(
|
||||
`Dashboard tool service is not available (tool: ${toolName})`,
|
||||
);
|
||||
}
|
||||
|
||||
const flatObject = Object.values(
|
||||
flatObjectMetadataMaps.byUniversalIdentifier,
|
||||
).find((obj) => obj?.nameSingular === nameSingular);
|
||||
return executeToolFromToolSet(
|
||||
toolSet,
|
||||
toolName,
|
||||
args,
|
||||
ToolCategory.DASHBOARD,
|
||||
);
|
||||
}
|
||||
|
||||
return flatObject?.icon ?? undefined;
|
||||
private async buildToolSet(
|
||||
context: ToolProviderContext,
|
||||
): Promise<ToolSet | null> {
|
||||
if (!this.dashboardToolService) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.dashboardToolService.generateDashboardTools(
|
||||
context.workspaceId,
|
||||
context.rolePermissionConfig,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+15
@@ -26,6 +26,7 @@ import {
|
||||
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 { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
|
||||
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';
|
||||
@@ -44,6 +45,20 @@ export class DatabaseToolProvider implements ToolProvider {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Database CRUD tools emit `executionRef.kind === 'database_crud'` descriptors
|
||||
// and are dispatched inline by ToolExecutorService. The static-tool path is
|
||||
// unreachable for this provider; this method exists only to satisfy the
|
||||
// interface.
|
||||
async executeStaticTool(
|
||||
toolName: string,
|
||||
_args: Record<string, unknown>,
|
||||
_context: ToolProviderContext,
|
||||
): Promise<ToolOutput> {
|
||||
throw new Error(
|
||||
`DatabaseToolProvider does not emit static-kind descriptors (tool: ${toolName})`,
|
||||
);
|
||||
}
|
||||
|
||||
async generateDescriptors(
|
||||
context: ToolProviderContext,
|
||||
options?: GenerateDescriptorOptions,
|
||||
|
||||
+15
@@ -10,6 +10,7 @@ import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/
|
||||
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 { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
|
||||
|
||||
@@ -25,6 +26,20 @@ export class LogicFunctionToolProvider implements ToolProvider {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Logic function tools emit `executionRef.kind === 'logic_function'`
|
||||
// descriptors and are dispatched inline by ToolExecutorService. The
|
||||
// static-tool path is unreachable for this provider; this method exists
|
||||
// only to satisfy the interface.
|
||||
async executeStaticTool(
|
||||
toolName: string,
|
||||
_args: Record<string, unknown>,
|
||||
_context: ToolProviderContext,
|
||||
): Promise<ToolOutput> {
|
||||
throw new Error(
|
||||
`LogicFunctionToolProvider does not emit static-kind descriptors (tool: ${toolName})`,
|
||||
);
|
||||
}
|
||||
|
||||
async generateDescriptors(
|
||||
context: ToolProviderContext,
|
||||
options?: GenerateDescriptorOptions,
|
||||
|
||||
+28
-21
@@ -1,5 +1,6 @@
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type ToolSet } from 'ai';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { type GenerateDescriptorOptions } from 'src/engine/core-modules/tool-provider/interfaces/generate-descriptor-options.type';
|
||||
@@ -7,38 +8,25 @@ import { type ToolProvider } from 'src/engine/core-modules/tool-provider/interfa
|
||||
import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-context.type';
|
||||
|
||||
import { ToolCategory } from 'twenty-shared/ai';
|
||||
import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
|
||||
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 { 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, OnModuleInit {
|
||||
export class MetadataToolProvider implements ToolProvider {
|
||||
readonly category = ToolCategory.METADATA;
|
||||
|
||||
constructor(
|
||||
private readonly objectMetadataToolsFactory: ObjectMetadataToolsFactory,
|
||||
private readonly fieldMetadataToolsFactory: FieldMetadataToolsFactory,
|
||||
private readonly permissionsService: PermissionsService,
|
||||
private readonly toolExecutorService: ToolExecutorService,
|
||||
) {}
|
||||
|
||||
onModuleInit(): void {
|
||||
const objectFactory = this.objectMetadataToolsFactory;
|
||||
const fieldFactory = this.fieldMetadataToolsFactory;
|
||||
|
||||
this.toolExecutorService.registerCategoryGenerator(
|
||||
ToolCategory.METADATA,
|
||||
async (context) => ({
|
||||
...objectFactory.generateTools(context.workspaceId),
|
||||
...fieldFactory.generateTools(context.workspaceId),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async isAvailable(context: ToolProviderContext): Promise<boolean> {
|
||||
return this.permissionsService.checkRolesPermissions(
|
||||
context.rolePermissionConfig,
|
||||
@@ -51,14 +39,33 @@ export class MetadataToolProvider implements ToolProvider, OnModuleInit {
|
||||
context: ToolProviderContext,
|
||||
options?: GenerateDescriptorOptions,
|
||||
): Promise<(ToolIndexEntry | ToolDescriptor)[]> {
|
||||
const toolSet = {
|
||||
...this.objectMetadataToolsFactory.generateTools(context.workspaceId),
|
||||
...this.fieldMetadataToolsFactory.generateTools(context.workspaceId),
|
||||
};
|
||||
const toolSet = this.buildToolSet(context);
|
||||
|
||||
return toolSetToDescriptors(toolSet, ToolCategory.METADATA, {
|
||||
includeSchemas: options?.includeSchemas ?? true,
|
||||
icon: 'IconSettings',
|
||||
});
|
||||
}
|
||||
|
||||
async executeStaticTool(
|
||||
toolName: string,
|
||||
args: Record<string, unknown>,
|
||||
context: ToolProviderContext,
|
||||
): Promise<ToolOutput> {
|
||||
const toolSet = this.buildToolSet(context);
|
||||
|
||||
return executeToolFromToolSet(
|
||||
toolSet,
|
||||
toolName,
|
||||
args,
|
||||
ToolCategory.METADATA,
|
||||
);
|
||||
}
|
||||
|
||||
private buildToolSet(context: ToolProviderContext): ToolSet {
|
||||
return {
|
||||
...this.objectMetadataToolsFactory.generateTools(context.workspaceId),
|
||||
...this.fieldMetadataToolsFactory.generateTools(context.workspaceId),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+32
-46
@@ -1,5 +1,6 @@
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type ToolSet } from 'ai';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { type GenerateDescriptorOptions } from 'src/engine/core-modules/tool-provider/interfaces/generate-descriptor-options.type';
|
||||
@@ -7,49 +8,23 @@ import { type ToolProvider } from 'src/engine/core-modules/tool-provider/interfa
|
||||
import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-context.type';
|
||||
|
||||
import { ToolCategory } from 'twenty-shared/ai';
|
||||
import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
|
||||
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';
|
||||
|
||||
@Injectable()
|
||||
export class ViewFieldToolProvider implements ToolProvider, OnModuleInit {
|
||||
export class ViewFieldToolProvider implements ToolProvider {
|
||||
readonly category = ToolCategory.VIEW_FIELD;
|
||||
|
||||
constructor(
|
||||
private readonly viewFieldToolsFactory: ViewFieldToolsFactory,
|
||||
private readonly permissionsService: PermissionsService,
|
||||
private readonly toolExecutorService: ToolExecutorService,
|
||||
) {}
|
||||
|
||||
onModuleInit(): void {
|
||||
const factory = this.viewFieldToolsFactory;
|
||||
|
||||
this.toolExecutorService.registerCategoryGenerator(
|
||||
ToolCategory.VIEW_FIELD,
|
||||
async (context) => {
|
||||
const readTools = factory.generateReadTools(context.workspaceId);
|
||||
|
||||
const hasViewPermission =
|
||||
await this.permissionsService.checkRolesPermissions(
|
||||
context.rolePermissionConfig,
|
||||
context.workspaceId,
|
||||
PermissionFlagType.VIEWS,
|
||||
);
|
||||
|
||||
if (hasViewPermission) {
|
||||
const writeTools = factory.generateWriteTools(context.workspaceId);
|
||||
|
||||
return { ...readTools, ...writeTools };
|
||||
}
|
||||
|
||||
return readTools;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async isAvailable(_context: ToolProviderContext): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
@@ -58,11 +33,30 @@ export class ViewFieldToolProvider implements ToolProvider, OnModuleInit {
|
||||
context: ToolProviderContext,
|
||||
options?: GenerateDescriptorOptions,
|
||||
): Promise<(ToolIndexEntry | ToolDescriptor)[]> {
|
||||
const schemaOptions = {
|
||||
const toolSet = await this.buildToolSet(context);
|
||||
|
||||
return toolSetToDescriptors(toolSet, ToolCategory.VIEW_FIELD, {
|
||||
includeSchemas: options?.includeSchemas ?? true,
|
||||
icon: 'IconTable',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async executeStaticTool(
|
||||
toolName: string,
|
||||
args: Record<string, unknown>,
|
||||
context: ToolProviderContext,
|
||||
): Promise<ToolOutput> {
|
||||
const toolSet = await this.buildToolSet(context);
|
||||
|
||||
return executeToolFromToolSet(
|
||||
toolSet,
|
||||
toolName,
|
||||
args,
|
||||
ToolCategory.VIEW_FIELD,
|
||||
);
|
||||
}
|
||||
|
||||
private async buildToolSet(context: ToolProviderContext): Promise<ToolSet> {
|
||||
const readTools = this.viewFieldToolsFactory.generateReadTools(
|
||||
context.workspaceId,
|
||||
);
|
||||
@@ -74,22 +68,14 @@ export class ViewFieldToolProvider implements ToolProvider, OnModuleInit {
|
||||
PermissionFlagType.VIEWS,
|
||||
);
|
||||
|
||||
if (hasViewPermission) {
|
||||
const writeTools = this.viewFieldToolsFactory.generateWriteTools(
|
||||
context.workspaceId,
|
||||
);
|
||||
|
||||
return toolSetToDescriptors(
|
||||
{ ...readTools, ...writeTools },
|
||||
ToolCategory.VIEW_FIELD,
|
||||
schemaOptions,
|
||||
);
|
||||
if (!hasViewPermission) {
|
||||
return readTools;
|
||||
}
|
||||
|
||||
return toolSetToDescriptors(
|
||||
readTools,
|
||||
ToolCategory.VIEW_FIELD,
|
||||
schemaOptions,
|
||||
const writeTools = this.viewFieldToolsFactory.generateWriteTools(
|
||||
context.workspaceId,
|
||||
);
|
||||
|
||||
return { ...readTools, ...writeTools };
|
||||
}
|
||||
}
|
||||
|
||||
+35
-68
@@ -1,5 +1,6 @@
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type ToolSet } from 'ai';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { type GenerateDescriptorOptions } from 'src/engine/core-modules/tool-provider/interfaces/generate-descriptor-options.type';
|
||||
@@ -7,17 +8,18 @@ import { type ToolProvider } from 'src/engine/core-modules/tool-provider/interfa
|
||||
import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-context.type';
|
||||
|
||||
import { ToolCategory } from 'twenty-shared/ai';
|
||||
import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
|
||||
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 { 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';
|
||||
|
||||
@Injectable()
|
||||
export class ViewToolProvider implements ToolProvider, OnModuleInit {
|
||||
export class ViewToolProvider implements ToolProvider {
|
||||
readonly category = ToolCategory.VIEW;
|
||||
|
||||
constructor(
|
||||
@@ -25,54 +27,8 @@ export class ViewToolProvider implements ToolProvider, OnModuleInit {
|
||||
private readonly viewFilterToolsFactory: ViewFilterToolsFactory,
|
||||
private readonly viewSortToolsFactory: ViewSortToolsFactory,
|
||||
private readonly permissionsService: PermissionsService,
|
||||
private readonly toolExecutorService: ToolExecutorService,
|
||||
) {}
|
||||
|
||||
onModuleInit(): void {
|
||||
const viewFactory = this.viewToolsFactory;
|
||||
const filterFactory = this.viewFilterToolsFactory;
|
||||
const sortFactory = this.viewSortToolsFactory;
|
||||
|
||||
this.toolExecutorService.registerCategoryGenerator(
|
||||
ToolCategory.VIEW,
|
||||
async (context) => {
|
||||
const workspaceMemberId = context.actorContext?.workspaceMemberId;
|
||||
|
||||
const readTools = {
|
||||
...viewFactory.generateReadTools(
|
||||
context.workspaceId,
|
||||
workspaceMemberId ?? undefined,
|
||||
workspaceMemberId ?? undefined,
|
||||
),
|
||||
...filterFactory.generateReadTools(context.workspaceId),
|
||||
...sortFactory.generateReadTools(context.workspaceId),
|
||||
};
|
||||
|
||||
const hasViewPermission =
|
||||
await this.permissionsService.checkRolesPermissions(
|
||||
context.rolePermissionConfig,
|
||||
context.workspaceId,
|
||||
PermissionFlagType.VIEWS,
|
||||
);
|
||||
|
||||
if (hasViewPermission) {
|
||||
const writeTools = {
|
||||
...viewFactory.generateWriteTools(
|
||||
context.workspaceId,
|
||||
workspaceMemberId ?? undefined,
|
||||
),
|
||||
...filterFactory.generateWriteTools(context.workspaceId),
|
||||
...sortFactory.generateWriteTools(context.workspaceId),
|
||||
};
|
||||
|
||||
return { ...readTools, ...writeTools };
|
||||
}
|
||||
|
||||
return readTools;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async isAvailable(_context: ToolProviderContext): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
@@ -81,10 +37,25 @@ export class ViewToolProvider implements ToolProvider, OnModuleInit {
|
||||
context: ToolProviderContext,
|
||||
options?: GenerateDescriptorOptions,
|
||||
): Promise<(ToolIndexEntry | ToolDescriptor)[]> {
|
||||
const workspaceMemberId = context.actorContext?.workspaceMemberId;
|
||||
const schemaOptions = {
|
||||
const toolSet = await this.buildToolSet(context);
|
||||
|
||||
return toolSetToDescriptors(toolSet, ToolCategory.VIEW, {
|
||||
includeSchemas: options?.includeSchemas ?? true,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async executeStaticTool(
|
||||
toolName: string,
|
||||
args: Record<string, unknown>,
|
||||
context: ToolProviderContext,
|
||||
): Promise<ToolOutput> {
|
||||
const toolSet = await this.buildToolSet(context);
|
||||
|
||||
return executeToolFromToolSet(toolSet, toolName, args, ToolCategory.VIEW);
|
||||
}
|
||||
|
||||
private async buildToolSet(context: ToolProviderContext): Promise<ToolSet> {
|
||||
const workspaceMemberId = context.actorContext?.workspaceMemberId;
|
||||
|
||||
const readTools = {
|
||||
...this.viewToolsFactory.generateReadTools(
|
||||
@@ -103,23 +74,19 @@ export class ViewToolProvider implements ToolProvider, OnModuleInit {
|
||||
PermissionFlagType.VIEWS,
|
||||
);
|
||||
|
||||
if (hasViewPermission) {
|
||||
const writeTools = {
|
||||
...this.viewToolsFactory.generateWriteTools(
|
||||
context.workspaceId,
|
||||
workspaceMemberId ?? undefined,
|
||||
),
|
||||
...this.viewFilterToolsFactory.generateWriteTools(context.workspaceId),
|
||||
...this.viewSortToolsFactory.generateWriteTools(context.workspaceId),
|
||||
};
|
||||
|
||||
return toolSetToDescriptors(
|
||||
{ ...readTools, ...writeTools },
|
||||
ToolCategory.VIEW,
|
||||
schemaOptions,
|
||||
);
|
||||
if (!hasViewPermission) {
|
||||
return readTools;
|
||||
}
|
||||
|
||||
return toolSetToDescriptors(readTools, ToolCategory.VIEW, schemaOptions);
|
||||
const writeTools = {
|
||||
...this.viewToolsFactory.generateWriteTools(
|
||||
context.workspaceId,
|
||||
workspaceMemberId ?? undefined,
|
||||
),
|
||||
...this.viewFilterToolsFactory.generateWriteTools(context.workspaceId),
|
||||
...this.viewSortToolsFactory.generateWriteTools(context.workspaceId),
|
||||
};
|
||||
|
||||
return { ...readTools, ...writeTools };
|
||||
}
|
||||
}
|
||||
|
||||
+41
-41
@@ -1,5 +1,6 @@
|
||||
import { Inject, Injectable, OnModuleInit, Optional } from '@nestjs/common';
|
||||
import { Inject, Injectable, Optional } from '@nestjs/common';
|
||||
|
||||
import { type ToolSet } from 'ai';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { type GenerateDescriptorOptions } from 'src/engine/core-modules/tool-provider/interfaces/generate-descriptor-options.type';
|
||||
@@ -9,16 +10,18 @@ import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/
|
||||
import { WORKFLOW_TOOL_SERVICE_TOKEN } from 'src/engine/core-modules/tool-provider/constants/workflow-tool-service.token';
|
||||
import { ToolCategory } from 'twenty-shared/ai';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
|
||||
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 { resolveObjectIcon } from 'src/engine/core-modules/tool-provider/utils/resolve-object-icon.util';
|
||||
import { toolSetToDescriptors } from 'src/engine/core-modules/tool-provider/utils/tool-set-to-descriptors.util';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
|
||||
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 { WorkflowToolWorkspaceService } from 'src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service';
|
||||
|
||||
@Injectable()
|
||||
export class WorkflowToolProvider implements ToolProvider, OnModuleInit {
|
||||
export class WorkflowToolProvider implements ToolProvider {
|
||||
readonly category = ToolCategory.WORKFLOW;
|
||||
|
||||
constructor(
|
||||
@@ -26,25 +29,9 @@ export class WorkflowToolProvider implements ToolProvider, OnModuleInit {
|
||||
@Inject(WORKFLOW_TOOL_SERVICE_TOKEN)
|
||||
private readonly workflowToolService: WorkflowToolWorkspaceService | null,
|
||||
private readonly permissionsService: PermissionsService,
|
||||
private readonly toolExecutorService: ToolExecutorService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {}
|
||||
|
||||
onModuleInit(): void {
|
||||
if (this.workflowToolService) {
|
||||
const service = this.workflowToolService;
|
||||
|
||||
this.toolExecutorService.registerCategoryGenerator(
|
||||
ToolCategory.WORKFLOW,
|
||||
async (context) =>
|
||||
service.generateWorkflowTools(
|
||||
context.workspaceId,
|
||||
context.rolePermissionConfig,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async isAvailable(context: ToolProviderContext): Promise<boolean> {
|
||||
if (!this.workflowToolService) {
|
||||
return false;
|
||||
@@ -61,16 +48,14 @@ export class WorkflowToolProvider implements ToolProvider, OnModuleInit {
|
||||
context: ToolProviderContext,
|
||||
options?: GenerateDescriptorOptions,
|
||||
): Promise<(ToolIndexEntry | ToolDescriptor)[]> {
|
||||
if (!this.workflowToolService) {
|
||||
const toolSet = await this.buildToolSet(context);
|
||||
|
||||
if (!toolSet) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const toolSet = await this.workflowToolService.generateWorkflowTools(
|
||||
context.workspaceId,
|
||||
context.rolePermissionConfig,
|
||||
);
|
||||
|
||||
const icon = await this.resolveObjectIcon(
|
||||
const icon = await resolveObjectIcon(
|
||||
this.flatEntityMapsCacheService,
|
||||
context.workspaceId,
|
||||
CoreObjectNameSingular.Workflow,
|
||||
);
|
||||
@@ -81,22 +66,37 @@ export class WorkflowToolProvider implements ToolProvider, OnModuleInit {
|
||||
});
|
||||
}
|
||||
|
||||
private async resolveObjectIcon(
|
||||
workspaceId: string,
|
||||
nameSingular: string,
|
||||
): Promise<string | undefined> {
|
||||
const { flatObjectMetadataMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps'],
|
||||
},
|
||||
async executeStaticTool(
|
||||
toolName: string,
|
||||
args: Record<string, unknown>,
|
||||
context: ToolProviderContext,
|
||||
): Promise<ToolOutput> {
|
||||
const toolSet = await this.buildToolSet(context);
|
||||
|
||||
if (!toolSet) {
|
||||
throw new Error(
|
||||
`Workflow tool service is not available (tool: ${toolName})`,
|
||||
);
|
||||
}
|
||||
|
||||
const flatObject = Object.values(
|
||||
flatObjectMetadataMaps.byUniversalIdentifier,
|
||||
).find((obj) => obj?.nameSingular === nameSingular);
|
||||
return executeToolFromToolSet(
|
||||
toolSet,
|
||||
toolName,
|
||||
args,
|
||||
ToolCategory.WORKFLOW,
|
||||
);
|
||||
}
|
||||
|
||||
return flatObject?.icon ?? undefined;
|
||||
private async buildToolSet(
|
||||
context: ToolProviderContext,
|
||||
): Promise<ToolSet | null> {
|
||||
if (!this.workflowToolService) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.workflowToolService.generateWorkflowTools(
|
||||
context.workspaceId,
|
||||
context.rolePermissionConfig,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+24
-46
@@ -1,4 +1,4 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { type AggregateOperations } from 'twenty-shared/types';
|
||||
@@ -26,9 +26,8 @@ import { GroupByRecordsService } from 'src/engine/core-modules/record-crud/servi
|
||||
import { type FindRecordsParams } from 'src/engine/core-modules/record-crud/types/find-records-params.type';
|
||||
import { UpdateManyRecordsService } from 'src/engine/core-modules/record-crud/services/update-many-records.service';
|
||||
import { UpdateRecordService } from 'src/engine/core-modules/record-crud/services/update-record.service';
|
||||
import { type ToolCategory } from 'twenty-shared/ai';
|
||||
import { type StaticToolHandler } from 'src/engine/core-modules/tool-provider/interfaces/static-tool-handler.interface';
|
||||
import { type CategoryToolGenerator } from 'src/engine/core-modules/tool-provider/types/category-tool-generator.type';
|
||||
import { TOOL_PROVIDERS } from 'src/engine/core-modules/tool-provider/constants/tool-providers.token';
|
||||
import { type ToolProvider } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import { type ToolExecutionRef } from 'src/engine/core-modules/tool-provider/types/tool-execution-ref.type';
|
||||
import { type ToolIndexEntry } from 'src/engine/core-modules/tool-provider/types/tool-index-entry.type';
|
||||
@@ -40,16 +39,9 @@ import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/works
|
||||
export class ToolExecutorService {
|
||||
private readonly logger = new Logger(ToolExecutorService.name);
|
||||
|
||||
// Per-tool handlers (action tools, etc.)
|
||||
private readonly staticToolHandlers = new Map<string, StaticToolHandler>();
|
||||
|
||||
// Category-level ToolSet generators (workflow, view, dashboard, metadata)
|
||||
private readonly categoryGenerators = new Map<
|
||||
ToolCategory,
|
||||
CategoryToolGenerator
|
||||
>();
|
||||
|
||||
constructor(
|
||||
@Inject(TOOL_PROVIDERS)
|
||||
private readonly providers: ToolProvider[],
|
||||
private readonly findRecordsService: FindRecordsService,
|
||||
private readonly groupByRecordsService: GroupByRecordsService,
|
||||
private readonly createRecordService: CreateRecordService,
|
||||
@@ -63,17 +55,6 @@ export class ToolExecutorService {
|
||||
private readonly userRepository: Repository<UserEntity>,
|
||||
) {}
|
||||
|
||||
registerStaticHandler(toolId: string, handler: StaticToolHandler): void {
|
||||
this.staticToolHandlers.set(toolId, handler);
|
||||
}
|
||||
|
||||
registerCategoryGenerator(
|
||||
category: ToolCategory,
|
||||
generator: CategoryToolGenerator,
|
||||
): void {
|
||||
this.categoryGenerators.set(category, generator);
|
||||
}
|
||||
|
||||
async dispatch(
|
||||
descriptor: ToolIndexEntry | ToolDescriptor,
|
||||
args: Record<string, unknown> | undefined,
|
||||
@@ -222,35 +203,32 @@ export class ToolExecutorService {
|
||||
throw new Error('Expected static executionRef');
|
||||
}
|
||||
|
||||
// Per-tool handler first (action tools)
|
||||
const handler = this.staticToolHandlers.get(descriptor.executionRef.toolId);
|
||||
const provider = this.providers.find(
|
||||
(candidate) => candidate.category === descriptor.category,
|
||||
);
|
||||
|
||||
if (handler) {
|
||||
return handler.execute(args, context);
|
||||
}
|
||||
|
||||
// Category-level generator fallback (workflow, view, dashboard, metadata)
|
||||
const generator = this.categoryGenerators.get(descriptor.category);
|
||||
|
||||
if (!generator) {
|
||||
if (!provider) {
|
||||
throw new Error(
|
||||
`No handler or generator for static tool: ${descriptor.executionRef.toolId}`,
|
||||
`No provider registered for category "${descriptor.category}" (tool: ${descriptor.executionRef.toolId})`,
|
||||
);
|
||||
}
|
||||
|
||||
const toolSet = await generator(context);
|
||||
const tool = toolSet[descriptor.name];
|
||||
|
||||
if (!tool?.execute) {
|
||||
throw new Error(
|
||||
`Tool ${descriptor.name} not found in generated ToolSet for category ${descriptor.category}`,
|
||||
);
|
||||
// Defense-in-depth: catalog and by-name lookups already filter by
|
||||
// `isAvailable`, but re-verify at dispatch so the gate is enforced in
|
||||
// one place regardless of how the descriptor reached us.
|
||||
if (!(await provider.isAvailable(context))) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Tool "${descriptor.name}" is not available`,
|
||||
error: `Tool "${descriptor.name}" is not available in this context. Use get_tool_catalog to see available tools.`,
|
||||
};
|
||||
}
|
||||
|
||||
return tool.execute(
|
||||
{ loadingMessage: '', ...args },
|
||||
{ toolCallId: '', messages: [] },
|
||||
) as Promise<ToolOutput>;
|
||||
return provider.executeStaticTool(
|
||||
descriptor.executionRef.toolId,
|
||||
args,
|
||||
context,
|
||||
);
|
||||
}
|
||||
|
||||
private async dispatchLogicFunction(
|
||||
|
||||
@@ -2,7 +2,6 @@ import { forwardRef, Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
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 { DashboardToolProvider } from 'src/engine/core-modules/tool-provider/providers/dashboard-tool.provider';
|
||||
@@ -41,7 +40,6 @@ import { ToolRegistryService } from './services/tool-registry.service';
|
||||
@Module({
|
||||
imports: [
|
||||
ToolModule,
|
||||
ToolGeneratorModule,
|
||||
RecordCrudModule,
|
||||
AiModelsModule,
|
||||
forwardRef(() => AiAgentExecutionModule),
|
||||
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
import { type ToolSet } from 'ai';
|
||||
|
||||
import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-context.type';
|
||||
|
||||
export type CategoryToolGenerator = (
|
||||
context: ToolProviderContext,
|
||||
) => Promise<ToolSet>;
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { type ToolSet } from 'ai';
|
||||
import { type ToolCategory } from 'twenty-shared/ai';
|
||||
|
||||
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
|
||||
|
||||
// Invokes a tool from a factory-generated ToolSet by name. Used by providers
|
||||
// whose tools are produced as opaque AI-SDK ToolSet closures (view, metadata,
|
||||
// workflow, dashboard, view-field) and which therefore cannot dispatch by
|
||||
// executionRef alone.
|
||||
//
|
||||
// The factory closures expect a `loadingMessage` field (added by the chat UX
|
||||
// wrapper) and a ToolExecutionOptions object; neither is meaningful when the
|
||||
// executor is invoking them internally, so we pass empty defaults.
|
||||
export const executeToolFromToolSet = async (
|
||||
toolSet: ToolSet,
|
||||
toolName: string,
|
||||
args: Record<string, unknown>,
|
||||
category: ToolCategory,
|
||||
): Promise<ToolOutput> => {
|
||||
const tool = toolSet[toolName];
|
||||
|
||||
if (!tool?.execute) {
|
||||
throw new Error(
|
||||
`Tool "${toolName}" not found in ToolSet for category "${category}"`,
|
||||
);
|
||||
}
|
||||
|
||||
return tool.execute(
|
||||
{ loadingMessage: '', ...args },
|
||||
{ toolCallId: '', messages: [] },
|
||||
) as Promise<ToolOutput>;
|
||||
};
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
|
||||
// Looks up a core object's icon by its nameSingular. Shared by providers that
|
||||
// tag descriptor batches with the icon of the underlying object (workflows,
|
||||
// dashboards) so the UI can render per-tool icons.
|
||||
export const resolveObjectIcon = async (
|
||||
flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
workspaceId: string,
|
||||
nameSingular: string,
|
||||
): Promise<string | undefined> => {
|
||||
const { flatObjectMetadataMaps } =
|
||||
await flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps({
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps'],
|
||||
});
|
||||
|
||||
const flatObject = Object.values(
|
||||
flatObjectMetadataMaps.byUniversalIdentifier,
|
||||
).find((obj) => obj?.nameSingular === nameSingular);
|
||||
|
||||
return flatObject?.icon ?? undefined;
|
||||
};
|
||||
Reference in New Issue
Block a user