refactor(workflow-tools): reorganize to one file per tool with co-located schemas (#16313)
## Summary Reorganizes workflow tools to improve maintainability and discoverability by having one file per tool with co-located input schemas. ## Changes - Create individual tool files in `tools/` directory (11 files) - Co-locate input schemas with their tool implementations - Add shared types file for dependencies and context - Simplify workspace service to aggregate tool factories - Remove centralized `schemas/` directory ## New Structure ``` workflow-tools/ ├── services/ │ └── workflow-tool.workspace-service.ts ├── tools/ │ ├── activate-workflow-version.tool.ts │ ├── compute-step-output-schema.tool.ts │ ├── create-complete-workflow.tool.ts │ ├── create-draft-from-workflow-version.tool.ts │ ├── create-workflow-version-edge.tool.ts │ ├── create-workflow-version-step.tool.ts │ ├── deactivate-workflow-version.tool.ts │ ├── delete-workflow-version-edge.tool.ts │ ├── delete-workflow-version-step.tool.ts │ ├── update-workflow-version-positions.tool.ts │ └── update-workflow-version-step.tool.ts ├── types/ │ └── workflow-tool-dependencies.type.ts └── workflow-tools.module.ts ``` ## Benefits - **Co-location**: Schema and tool logic are in the same file - **Single responsibility**: Each file handles one tool - **Easier maintenance**: Changes to a tool only touch one file - **Better discoverability**: File names match tool names
This commit is contained in:
+32
-1
@@ -1,31 +1,62 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
|
||||
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
|
||||
import { AiAgentModule } from 'src/engine/metadata-modules/ai/ai-agent/ai-agent.module';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { AiBillingModule } from 'src/engine/metadata-modules/ai/ai-billing/ai-billing.module';
|
||||
import { AiModelsModule } from 'src/engine/metadata-modules/ai/ai-models/ai-models.module';
|
||||
import { AiToolsModule } from 'src/engine/metadata-modules/ai/ai-tools/ai-tools.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
|
||||
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
|
||||
import { AgentMessagePartEntity } from './entities/agent-message-part.entity';
|
||||
import { AgentMessageEntity } from './entities/agent-message.entity';
|
||||
import { AgentTurnEntity } from './entities/agent-turn.entity';
|
||||
import { AgentActorContextService } from './services/agent-actor-context.service';
|
||||
import { AgentAsyncExecutorService } from './services/agent-async-executor.service';
|
||||
import { AgentExecutionService } from './services/agent-execution.service';
|
||||
import { AgentModelConfigService } from './services/agent-model-config.service';
|
||||
import { AgentPlanExecutorService } from './services/agent-plan-executor.service';
|
||||
import { AgentToolGeneratorService } from './services/agent-tool-generator.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
AiBillingModule,
|
||||
AiModelsModule,
|
||||
AiToolsModule,
|
||||
AiAgentModule,
|
||||
WorkspaceDomainsModule,
|
||||
UserWorkspaceModule,
|
||||
UserRoleModule,
|
||||
PermissionsModule,
|
||||
WorkspaceCacheModule,
|
||||
TypeOrmModule.forFeature([
|
||||
AgentEntity,
|
||||
AgentMessageEntity,
|
||||
AgentMessagePartEntity,
|
||||
AgentTurnEntity,
|
||||
RoleTargetEntity,
|
||||
]),
|
||||
],
|
||||
providers: [AgentAsyncExecutorService],
|
||||
providers: [
|
||||
AgentAsyncExecutorService,
|
||||
AgentExecutionService,
|
||||
AgentToolGeneratorService,
|
||||
AgentModelConfigService,
|
||||
AgentActorContextService,
|
||||
AgentPlanExecutorService,
|
||||
],
|
||||
exports: [
|
||||
AgentAsyncExecutorService,
|
||||
AgentExecutionService,
|
||||
AgentPlanExecutorService,
|
||||
AgentToolGeneratorService,
|
||||
AgentActorContextService,
|
||||
AgentModelConfigService,
|
||||
TypeOrmModule.forFeature([
|
||||
AgentMessageEntity,
|
||||
AgentMessagePartEntity,
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type ActorMetadata } from 'twenty-shared/types';
|
||||
|
||||
import { buildCreatedByFromFullNameMetadata } from 'src/engine/core-modules/actor/utils/build-created-by-from-full-name-metadata.util';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import {
|
||||
AgentException,
|
||||
AgentExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
export type AgentActorContext = {
|
||||
actorContext: ActorMetadata;
|
||||
roleId: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
||||
export class AgentActorContextService {
|
||||
constructor(
|
||||
private readonly userWorkspaceService: UserWorkspaceService,
|
||||
private readonly userRoleService: UserRoleService,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
async buildUserAndAgentActorContext(
|
||||
userWorkspaceId: string,
|
||||
workspaceId: string,
|
||||
): Promise<AgentActorContext> {
|
||||
const userWorkspace =
|
||||
await this.userWorkspaceService.findById(userWorkspaceId);
|
||||
|
||||
if (!userWorkspace) {
|
||||
throw new AgentException(
|
||||
'User workspace not found',
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceMemberRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'workspaceMember',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workspaceMember = await workspaceMemberRepository.findOne({
|
||||
where: {
|
||||
userId: userWorkspace.userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!workspaceMember) {
|
||||
throw new AgentException(
|
||||
'Workspace member not found for user',
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
const roleId = await this.userRoleService.getRoleIdForUserWorkspace({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (!roleId) {
|
||||
throw new AgentException(
|
||||
'User role not found',
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
const actorContext = buildCreatedByFromFullNameMetadata({
|
||||
fullNameMetadata: workspaceMember.name,
|
||||
workspaceMemberId: workspaceMember.id,
|
||||
});
|
||||
|
||||
return {
|
||||
actorContext,
|
||||
roleId,
|
||||
};
|
||||
}
|
||||
}
|
||||
+93
-32
@@ -11,6 +11,7 @@ import {
|
||||
import { type ActorMetadata } from 'twenty-shared/types';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { type AgentExecutionResult } from 'src/engine/metadata-modules/ai/ai-agent-execution/types/agent-execution-result.type';
|
||||
import {
|
||||
AgentException,
|
||||
AgentExceptionCode,
|
||||
@@ -18,7 +19,7 @@ import {
|
||||
import { AGENT_CONFIG } from 'src/engine/metadata-modules/ai/ai-agent/constants/agent-config.const';
|
||||
import { AGENT_SYSTEM_PROMPTS } from 'src/engine/metadata-modules/ai/ai-agent/constants/agent-system-prompts.const';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { AgentExecutionResult } from 'src/engine/metadata-modules/ai/ai-agent/services/agent-execution.service';
|
||||
import { repairToolCall } from 'src/engine/metadata-modules/ai/ai-agent/utils/repair-tool-call.util';
|
||||
import { AI_TELEMETRY_CONFIG } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-telemetry.const';
|
||||
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
|
||||
import { ToolAdapterService } from 'src/engine/metadata-modules/ai/ai-tools/services/tool-adapter.service';
|
||||
@@ -26,18 +27,43 @@ import { ToolService } from 'src/engine/metadata-modules/ai/ai-tools/services/to
|
||||
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
|
||||
import { AgentModelConfigService } from './agent-model-config.service';
|
||||
|
||||
// Agent execution within workflows uses database and action tools only.
|
||||
// Workflow tools are intentionally excluded to avoid circular dependencies
|
||||
// and recursive workflow execution.
|
||||
@Injectable()
|
||||
export class AgentAsyncExecutorService {
|
||||
private readonly logger = new Logger(AgentAsyncExecutorService.name);
|
||||
|
||||
constructor(
|
||||
private readonly aiModelRegistryService: AiModelRegistryService,
|
||||
private readonly agentModelConfigService: AgentModelConfigService,
|
||||
private readonly toolAdapterService: ToolAdapterService,
|
||||
private readonly toolService: ToolService,
|
||||
@InjectRepository(RoleTargetEntity)
|
||||
private readonly roleTargetRepository: Repository<RoleTargetEntity>,
|
||||
private readonly toolService: ToolService,
|
||||
) {}
|
||||
|
||||
private async getTools(
|
||||
private extractRoleIds(
|
||||
rolePermissionConfig?: RolePermissionConfig,
|
||||
): string[] {
|
||||
if (!rolePermissionConfig) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ('intersectionOf' in rolePermissionConfig) {
|
||||
return rolePermissionConfig.intersectionOf;
|
||||
}
|
||||
|
||||
if ('unionOf' in rolePermissionConfig) {
|
||||
return rolePermissionConfig.unionOf;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private async getToolsForWorkflowExecution(
|
||||
agentId: string,
|
||||
workspaceId: string,
|
||||
actorContext?: ActorMetadata,
|
||||
@@ -45,43 +71,42 @@ export class AgentAsyncExecutorService {
|
||||
): Promise<ToolSet> {
|
||||
const roleTarget = await this.roleTargetRepository.findOne({
|
||||
where: {
|
||||
agentId: agentId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
},
|
||||
select: ['roleId'],
|
||||
});
|
||||
|
||||
const agentRoleId = roleTarget?.roleId;
|
||||
const configRoleIds = this.extractRoleIds(rolePermissionConfig);
|
||||
|
||||
if (!rolePermissionConfig && !agentRoleId) {
|
||||
return await this.toolAdapterService.getTools();
|
||||
// Combine role IDs from config and agent
|
||||
const allRoleIds = agentRoleId
|
||||
? [...new Set([...configRoleIds, agentRoleId])]
|
||||
: configRoleIds;
|
||||
|
||||
if (allRoleIds.length === 0) {
|
||||
// No role context - return basic action tools only
|
||||
return this.toolAdapterService.getTools();
|
||||
}
|
||||
|
||||
let effectiveRoleContext: RolePermissionConfig;
|
||||
|
||||
if (
|
||||
rolePermissionConfig &&
|
||||
('intersectionOf' in rolePermissionConfig ||
|
||||
'unionOf' in rolePermissionConfig)
|
||||
) {
|
||||
effectiveRoleContext = rolePermissionConfig;
|
||||
} else if (agentRoleId) {
|
||||
effectiveRoleContext = { unionOf: [agentRoleId] };
|
||||
} else {
|
||||
return await this.toolAdapterService.getTools();
|
||||
}
|
||||
|
||||
const actionTools = await this.toolAdapterService.getTools(
|
||||
effectiveRoleContext,
|
||||
workspaceId,
|
||||
);
|
||||
const effectiveRoleContext: RolePermissionConfig = {
|
||||
intersectionOf: allRoleIds,
|
||||
};
|
||||
|
||||
// Get database CRUD tools
|
||||
const databaseTools = await this.toolService.listTools(
|
||||
effectiveRoleContext,
|
||||
workspaceId,
|
||||
actorContext,
|
||||
);
|
||||
|
||||
// Get action tools (send email, http request, etc.)
|
||||
const actionTools = await this.toolAdapterService.getTools(
|
||||
effectiveRoleContext,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return {
|
||||
...databaseTools,
|
||||
...actionTools,
|
||||
@@ -103,14 +128,35 @@ export class AgentAsyncExecutorService {
|
||||
const registeredModel =
|
||||
await this.aiModelRegistryService.resolveModelForAgent(agent);
|
||||
|
||||
const tools = agent
|
||||
? await this.getTools(
|
||||
agent.id,
|
||||
agent.workspaceId,
|
||||
actorContext,
|
||||
rolePermissionConfig,
|
||||
)
|
||||
: {};
|
||||
let tools: ToolSet = {};
|
||||
let providerOptions = {};
|
||||
|
||||
if (agent) {
|
||||
tools = await this.getToolsForWorkflowExecution(
|
||||
agent.id,
|
||||
agent.workspaceId,
|
||||
actorContext,
|
||||
rolePermissionConfig,
|
||||
);
|
||||
|
||||
// Add native model tools (web search, etc.) if configured
|
||||
const nativeModelTools =
|
||||
this.agentModelConfigService.getNativeModelTools(
|
||||
registeredModel,
|
||||
agent as unknown as Parameters<
|
||||
typeof this.agentModelConfigService.getNativeModelTools
|
||||
>[1],
|
||||
);
|
||||
|
||||
tools = { ...tools, ...nativeModelTools };
|
||||
|
||||
providerOptions = this.agentModelConfigService.getProviderOptions(
|
||||
registeredModel,
|
||||
agent as unknown as Parameters<
|
||||
typeof this.agentModelConfigService.getProviderOptions
|
||||
>[1],
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(`Generated ${Object.keys(tools).length} tools for agent`);
|
||||
|
||||
@@ -120,7 +166,22 @@ export class AgentAsyncExecutorService {
|
||||
model: registeredModel.model,
|
||||
prompt: userPrompt,
|
||||
stopWhen: stepCountIs(AGENT_CONFIG.MAX_STEPS),
|
||||
providerOptions,
|
||||
experimental_telemetry: AI_TELEMETRY_CONFIG,
|
||||
experimental_repairToolCall: async ({
|
||||
toolCall,
|
||||
tools: toolsForRepair,
|
||||
inputSchema,
|
||||
error,
|
||||
}) => {
|
||||
return repairToolCall({
|
||||
toolCall,
|
||||
tools: toolsForRepair,
|
||||
inputSchema,
|
||||
error,
|
||||
model: registeredModel.model,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const agentSchema =
|
||||
|
||||
+411
@@ -0,0 +1,411 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
convertToModelMessages,
|
||||
stepCountIs,
|
||||
streamText,
|
||||
ToolSet,
|
||||
UIDataTypes,
|
||||
UIMessage,
|
||||
UITools,
|
||||
} from 'ai';
|
||||
import { AppPath, type ActorMetadata } from 'twenty-shared/types';
|
||||
import { getAppPath } from 'twenty-shared/utils';
|
||||
import { In } from 'typeorm';
|
||||
|
||||
import { getAllSelectableColumnNames } from 'src/engine/api/utils/get-all-selectable-column-names.utils';
|
||||
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import {
|
||||
AgentException,
|
||||
AgentExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
|
||||
import { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.service';
|
||||
import { AGENT_CONFIG } from 'src/engine/metadata-modules/ai/ai-agent/constants/agent-config.const';
|
||||
import { AGENT_SYSTEM_PROMPTS } from 'src/engine/metadata-modules/ai/ai-agent/constants/agent-system-prompts.const';
|
||||
import { RecordIdsByObjectMetadataNameSingularType } from 'src/engine/metadata-modules/ai/ai-agent/types/recordIdsByObjectMetadataNameSingular.type';
|
||||
import { repairToolCall } from 'src/engine/metadata-modules/ai/ai-agent/utils/repair-tool-call.util';
|
||||
import { AIBillingService } from 'src/engine/metadata-modules/ai/ai-billing/services/ai-billing.service';
|
||||
import { ToolHints } from 'src/engine/metadata-modules/ai/ai-chat-router/types/tool-hints.interface';
|
||||
import { AI_TELEMETRY_CONFIG } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-telemetry.const';
|
||||
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
|
||||
import { FlatAgentWithRoleId } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
|
||||
import { AgentActorContextService } from './agent-actor-context.service';
|
||||
import { AgentModelConfigService } from './agent-model-config.service';
|
||||
import { AgentToolGeneratorService } from './agent-tool-generator.service';
|
||||
|
||||
// Re-export for backward compatibility
|
||||
export { type AgentExecutionResult } from 'src/engine/metadata-modules/ai/ai-agent-execution/types/agent-execution-result.type';
|
||||
|
||||
export interface StreamChatResponseResult {
|
||||
stream: ReturnType<typeof streamText>;
|
||||
timings: {
|
||||
contextBuildTimeMs: number;
|
||||
toolGenerationTimeMs: number;
|
||||
aiRequestPrepTimeMs: number;
|
||||
toolCount: number;
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AgentExecutionService {
|
||||
private readonly logger = new Logger(AgentExecutionService.name);
|
||||
|
||||
constructor(
|
||||
private readonly workspaceDomainsService: WorkspaceDomainsService,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly aiModelRegistryService: AiModelRegistryService,
|
||||
private readonly agentToolGeneratorService: AgentToolGeneratorService,
|
||||
private readonly agentModelConfigService: AgentModelConfigService,
|
||||
private readonly aiBillingService: AIBillingService,
|
||||
private readonly agentActorContextService: AgentActorContextService,
|
||||
private readonly agentService: AgentService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {}
|
||||
|
||||
async prepareAIRequestConfig({
|
||||
messages,
|
||||
system,
|
||||
agent,
|
||||
actorContext,
|
||||
roleIds,
|
||||
toolHints,
|
||||
additionalTools,
|
||||
}: {
|
||||
system: string;
|
||||
agent: FlatAgentWithRoleId | null;
|
||||
messages: UIMessage<unknown, UIDataTypes, UITools>[];
|
||||
actorContext?: ActorMetadata;
|
||||
roleIds?: string[];
|
||||
toolHints?: ToolHints;
|
||||
additionalTools?: ToolSet;
|
||||
}) {
|
||||
try {
|
||||
if (agent) {
|
||||
this.logger.log(
|
||||
`Preparing AI request config for agent ${agent.id} with model ${agent.modelId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const registeredModel =
|
||||
await this.aiModelRegistryService.resolveModelForAgent(agent);
|
||||
|
||||
let tools: ToolSet = {};
|
||||
let providerOptions;
|
||||
|
||||
if (agent) {
|
||||
const baseTools =
|
||||
await this.agentToolGeneratorService.generateToolsForAgent(
|
||||
agent.id,
|
||||
agent.workspaceId,
|
||||
actorContext,
|
||||
roleIds,
|
||||
toolHints,
|
||||
);
|
||||
|
||||
const nativeModelTools =
|
||||
this.agentModelConfigService.getNativeModelTools(
|
||||
registeredModel,
|
||||
agent,
|
||||
);
|
||||
|
||||
tools = {
|
||||
...baseTools,
|
||||
...nativeModelTools,
|
||||
...(additionalTools || {}),
|
||||
};
|
||||
|
||||
providerOptions = this.agentModelConfigService.getProviderOptions(
|
||||
registeredModel,
|
||||
agent,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Generated ${Object.keys(tools).length} tools for agent (including ${Object.keys(additionalTools || {}).length} additional tools)`,
|
||||
);
|
||||
|
||||
return {
|
||||
system,
|
||||
tools,
|
||||
model: registeredModel.model,
|
||||
messages: convertToModelMessages(messages),
|
||||
stopWhen: stepCountIs(AGENT_CONFIG.MAX_STEPS),
|
||||
providerOptions,
|
||||
experimental_telemetry: AI_TELEMETRY_CONFIG,
|
||||
experimental_repairToolCall: async ({
|
||||
toolCall,
|
||||
tools: toolsForRepair,
|
||||
inputSchema,
|
||||
error,
|
||||
}: {
|
||||
toolCall: {
|
||||
type: 'tool-call';
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
input: string;
|
||||
};
|
||||
tools: Record<string, unknown>;
|
||||
inputSchema: (toolCall: { toolName: string }) => unknown;
|
||||
error: Error;
|
||||
}) => {
|
||||
return repairToolCall({
|
||||
toolCall,
|
||||
tools: toolsForRepair,
|
||||
inputSchema,
|
||||
error,
|
||||
model: registeredModel.model,
|
||||
});
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to prepare AI request config for agent ${agent?.id ?? 'no agent'}`,
|
||||
error instanceof Error ? error.stack : error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getContextForSystemPrompt(
|
||||
workspace: WorkspaceEntity,
|
||||
recordIdsByObjectMetadataNameSingular: RecordIdsByObjectMetadataNameSingularType,
|
||||
userWorkspaceId: string,
|
||||
) {
|
||||
const { userWorkspaceRoleMap } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspace.id, [
|
||||
'userWorkspaceRoleMap',
|
||||
]);
|
||||
|
||||
const roleId = userWorkspaceRoleMap[userWorkspaceId];
|
||||
|
||||
if (!roleId) {
|
||||
throw new AgentException(
|
||||
'Failed to retrieve user role.',
|
||||
AgentExceptionCode.ROLE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceDataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
const flatObjectMetadataMaps =
|
||||
workspaceDataSource.internalContext.flatObjectMetadataMaps;
|
||||
const flatFieldMetadataMaps =
|
||||
workspaceDataSource.internalContext.flatFieldMetadataMaps;
|
||||
const objectIdByNameSingular =
|
||||
workspaceDataSource.internalContext.objectIdByNameSingular;
|
||||
const objectMetadataPermissions = workspaceDataSource.permissionsPerRoleId;
|
||||
|
||||
const contextObject = (
|
||||
await Promise.all(
|
||||
recordIdsByObjectMetadataNameSingular.map(
|
||||
async (recordsWithObjectMetadataNameSingular) => {
|
||||
if (recordsWithObjectMetadataNameSingular.recordIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const objectMetadataId =
|
||||
objectIdByNameSingular[
|
||||
recordsWithObjectMetadataNameSingular.objectMetadataNameSingular
|
||||
];
|
||||
const objectMetadataMapItem = objectMetadataId
|
||||
? flatObjectMetadataMaps.byId[objectMetadataId]
|
||||
: undefined;
|
||||
|
||||
if (!objectMetadataMapItem) {
|
||||
this.logger.warn(
|
||||
`Object metadata not found for ${recordsWithObjectMetadataNameSingular.objectMetadataNameSingular}`,
|
||||
);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
const repository = workspaceDataSource.getRepository(
|
||||
recordsWithObjectMetadataNameSingular.objectMetadataNameSingular,
|
||||
{ unionOf: [roleId] },
|
||||
);
|
||||
|
||||
const restrictedFields =
|
||||
objectMetadataPermissions?.[roleId]?.[objectMetadataMapItem.id]
|
||||
?.restrictedFields ?? {};
|
||||
|
||||
const hasRestrictedFields = Object.values(restrictedFields).some(
|
||||
(field) => field.canRead === false,
|
||||
);
|
||||
|
||||
const selectOptions = hasRestrictedFields
|
||||
? getAllSelectableColumnNames({
|
||||
restrictedFields,
|
||||
objectMetadata: {
|
||||
objectMetadataMapItem,
|
||||
flatFieldMetadataMaps,
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
await repository.find({
|
||||
...(selectOptions && { select: selectOptions }),
|
||||
where: {
|
||||
id: In(recordsWithObjectMetadataNameSingular.recordIds),
|
||||
},
|
||||
})
|
||||
).map((record) => {
|
||||
return {
|
||||
...record,
|
||||
resourceUrl: this.workspaceDomainsService.buildWorkspaceURL({
|
||||
workspace,
|
||||
pathname: getAppPath(AppPath.RecordShowPage, {
|
||||
objectNameSingular:
|
||||
recordsWithObjectMetadataNameSingular.objectMetadataNameSingular,
|
||||
objectRecordId: record.id,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
),
|
||||
)
|
||||
).flat(2);
|
||||
|
||||
return JSON.stringify(contextObject);
|
||||
}
|
||||
|
||||
async streamChatResponse({
|
||||
workspace,
|
||||
userWorkspaceId,
|
||||
agentId,
|
||||
messages,
|
||||
recordIdsByObjectMetadataNameSingular,
|
||||
toolHints,
|
||||
additionalTools,
|
||||
}: {
|
||||
workspace: WorkspaceEntity;
|
||||
userWorkspaceId: string;
|
||||
agentId: string;
|
||||
messages: UIMessage<unknown, UIDataTypes, UITools>[];
|
||||
recordIdsByObjectMetadataNameSingular: RecordIdsByObjectMetadataNameSingularType;
|
||||
toolHints?: ToolHints;
|
||||
additionalTools?: ToolSet;
|
||||
}): Promise<{
|
||||
stream: ReturnType<typeof streamText>;
|
||||
timings: {
|
||||
contextBuildTimeMs: number;
|
||||
toolGenerationTimeMs: number;
|
||||
aiRequestPrepTimeMs: number;
|
||||
toolCount: number;
|
||||
};
|
||||
contextInfo: {
|
||||
contextString: string;
|
||||
contextRecordCount: number;
|
||||
contextSizeBytes: number;
|
||||
};
|
||||
}> {
|
||||
try {
|
||||
const agent = await this.agentService.findOneAgentById({
|
||||
workspaceId: workspace.id,
|
||||
id: agentId,
|
||||
});
|
||||
|
||||
const contextBuildStart = Date.now();
|
||||
let contextPart = '';
|
||||
let contextRecordCount = 0;
|
||||
|
||||
if (recordIdsByObjectMetadataNameSingular.length > 0) {
|
||||
contextPart = await this.getContextForSystemPrompt(
|
||||
workspace,
|
||||
recordIdsByObjectMetadataNameSingular,
|
||||
userWorkspaceId,
|
||||
);
|
||||
|
||||
try {
|
||||
const contextData = JSON.parse(contextPart);
|
||||
|
||||
contextRecordCount = Array.isArray(contextData)
|
||||
? contextData.length
|
||||
: 0;
|
||||
} catch (error) {
|
||||
this.logger.warn('Failed to parse context for record count:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const contextString = contextPart ? `\n\nCONTEXT:\n${contextPart}` : '';
|
||||
const contextBuildTime = Date.now() - contextBuildStart;
|
||||
|
||||
const { actorContext, roleId } =
|
||||
await this.agentActorContextService.buildUserAndAgentActorContext(
|
||||
userWorkspaceId,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
const aiRequestPrepStart = Date.now();
|
||||
|
||||
const aiRequestConfig = await this.prepareAIRequestConfig({
|
||||
system: `${AGENT_SYSTEM_PROMPTS.BASE}\n${AGENT_SYSTEM_PROMPTS.CHAT_ADDITIONS}\n\n${agent.prompt}${contextString}`,
|
||||
agent,
|
||||
messages,
|
||||
actorContext,
|
||||
roleIds: [roleId, ...(agent?.roleId ? [agent?.roleId] : [])],
|
||||
toolHints,
|
||||
additionalTools,
|
||||
});
|
||||
|
||||
const aiRequestPrepTime = Date.now() - aiRequestPrepStart;
|
||||
const toolCount = Object.keys(aiRequestConfig.tools || {}).length;
|
||||
const toolGenerationTime = aiRequestPrepTime;
|
||||
|
||||
this.logger.log(
|
||||
`Sending request to AI model with ${messages.length} messages and ${toolCount} tools`,
|
||||
);
|
||||
|
||||
const model =
|
||||
await this.aiModelRegistryService.resolveModelForAgent(agent);
|
||||
|
||||
const stream = streamText(aiRequestConfig);
|
||||
|
||||
stream.usage
|
||||
.then((usage) => {
|
||||
this.aiBillingService.calculateAndBillUsage(
|
||||
model.modelId,
|
||||
usage,
|
||||
workspace.id,
|
||||
agent.id,
|
||||
);
|
||||
})
|
||||
.catch((usageError) => {
|
||||
this.logger.error('Failed to get usage information:', usageError);
|
||||
});
|
||||
|
||||
return {
|
||||
stream,
|
||||
timings: {
|
||||
contextBuildTimeMs: contextBuildTime,
|
||||
toolGenerationTimeMs: toolGenerationTime,
|
||||
aiRequestPrepTimeMs: aiRequestPrepTime,
|
||||
toolCount,
|
||||
},
|
||||
contextInfo: {
|
||||
contextString: contextPart,
|
||||
contextRecordCount,
|
||||
contextSizeBytes: contextPart
|
||||
? Buffer.byteLength(contextPart, 'utf8')
|
||||
: 0,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('Error in streamChatResponse:', error);
|
||||
throw new AgentException(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to stream chat response',
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { anthropic } from '@ai-sdk/anthropic';
|
||||
import { openai } from '@ai-sdk/openai';
|
||||
import { ProviderOptions } from '@ai-sdk/provider-utils';
|
||||
import { ToolSet } from 'ai';
|
||||
|
||||
import { AGENT_CONFIG } from 'src/engine/metadata-modules/ai/ai-agent/constants/agent-config.const';
|
||||
import { ModelProvider } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
|
||||
import { RegisteredAIModel } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
|
||||
import { FlatAgentWithRoleId } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
|
||||
|
||||
@Injectable()
|
||||
export class AgentModelConfigService {
|
||||
constructor() {}
|
||||
|
||||
getProviderOptions(
|
||||
model: RegisteredAIModel,
|
||||
agent: FlatAgentWithRoleId,
|
||||
): ProviderOptions {
|
||||
switch (model.provider) {
|
||||
case ModelProvider.XAI:
|
||||
return this.getXaiProviderOptions(agent);
|
||||
case ModelProvider.ANTHROPIC:
|
||||
return this.getAnthropicProviderOptions(model);
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
getNativeModelTools(
|
||||
model: RegisteredAIModel,
|
||||
agent: FlatAgentWithRoleId,
|
||||
): ToolSet {
|
||||
const tools: ToolSet = {};
|
||||
|
||||
if (!agent.modelConfiguration) {
|
||||
return tools;
|
||||
}
|
||||
|
||||
switch (model.provider) {
|
||||
case ModelProvider.ANTHROPIC:
|
||||
if (agent.modelConfiguration.webSearch?.enabled) {
|
||||
tools.web_search = anthropic.tools.webSearch_20250305();
|
||||
}
|
||||
break;
|
||||
case ModelProvider.OPENAI:
|
||||
if (agent.modelConfiguration.webSearch?.enabled) {
|
||||
tools.web_search = openai.tools.webSearch();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
private getXaiProviderOptions(agent: FlatAgentWithRoleId): ProviderOptions {
|
||||
if (
|
||||
!agent.modelConfiguration ||
|
||||
(!agent.modelConfiguration.webSearch?.enabled &&
|
||||
!agent.modelConfiguration.twitterSearch?.enabled)
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const sources: Array<{ type: string }> = [];
|
||||
|
||||
if (agent.modelConfiguration.webSearch?.enabled) {
|
||||
sources.push({ type: 'web' });
|
||||
}
|
||||
|
||||
if (agent.modelConfiguration.twitterSearch?.enabled) {
|
||||
sources.push({ type: 'x' });
|
||||
}
|
||||
|
||||
return {
|
||||
xai: {
|
||||
searchParameters: {
|
||||
mode: 'auto',
|
||||
...(sources.length > 0 && { sources }),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private getAnthropicProviderOptions(
|
||||
model: RegisteredAIModel,
|
||||
): ProviderOptions {
|
||||
if (!model.doesSupportThinking) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
anthropic: {
|
||||
thinking: {
|
||||
type: 'enabled',
|
||||
budgetTokens: AGENT_CONFIG.REASONING_BUDGET_TOKENS,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.service';
|
||||
import { type RecordIdsByObjectMetadataNameSingularType } from 'src/engine/metadata-modules/ai/ai-agent/types/recordIdsByObjectMetadataNameSingular.type';
|
||||
import { type PlanStep } from 'src/engine/metadata-modules/ai/ai-chat-router/types/router-result.interface';
|
||||
import { standardAgentDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents';
|
||||
|
||||
import { AgentExecutionService } from './agent-execution.service';
|
||||
|
||||
export type PlanExecutionProgress = {
|
||||
type: 'plan-generated' | 'step-started' | 'step-completed';
|
||||
stepNumber?: number;
|
||||
agentName?: string;
|
||||
task?: string;
|
||||
output?: string;
|
||||
totalSteps?: number;
|
||||
reasoning?: string;
|
||||
};
|
||||
|
||||
export type StepResult = {
|
||||
stepNumber: number;
|
||||
agentName: string;
|
||||
output: string;
|
||||
};
|
||||
|
||||
export type PlanExecutionResult = {
|
||||
finalOutput: string;
|
||||
stepResults: StepResult[];
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class AgentPlanExecutorService {
|
||||
private readonly logger = new Logger(AgentPlanExecutorService.name);
|
||||
|
||||
constructor(
|
||||
private readonly agentExecutionService: AgentExecutionService,
|
||||
private readonly agentService: AgentService,
|
||||
) {}
|
||||
|
||||
async executePlan({
|
||||
steps,
|
||||
reasoning,
|
||||
workspace,
|
||||
userWorkspaceId,
|
||||
recordIdsByObjectMetadataNameSingular,
|
||||
onProgress,
|
||||
writer,
|
||||
}: {
|
||||
steps: PlanStep[];
|
||||
reasoning: string;
|
||||
workspace: WorkspaceEntity;
|
||||
userWorkspaceId: string;
|
||||
recordIdsByObjectMetadataNameSingular: RecordIdsByObjectMetadataNameSingularType;
|
||||
onProgress?: (progress: PlanExecutionProgress) => void;
|
||||
writer?: {
|
||||
write: (chunk: unknown) => void;
|
||||
merge: (stream: unknown) => void;
|
||||
};
|
||||
}): Promise<PlanExecutionResult> {
|
||||
this.logger.log(`Executing plan with ${steps.length} steps`);
|
||||
|
||||
onProgress?.({
|
||||
type: 'plan-generated',
|
||||
totalSteps: steps.length,
|
||||
reasoning,
|
||||
});
|
||||
|
||||
const stepResults: StepResult[] = [];
|
||||
|
||||
for (const step of steps) {
|
||||
try {
|
||||
this.logger.log(
|
||||
`[PLAN EXECUTION] Step ${step.stepNumber}: Looking up agent "${step.agentName}"`,
|
||||
);
|
||||
|
||||
const agent = await this.agentService.findOneAgentByName({
|
||||
name: step.agentName,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`[PLAN EXECUTION] Step ${step.stepNumber}: Found agent "${agent.label}" (${agent.id})`,
|
||||
);
|
||||
|
||||
onProgress?.({
|
||||
type: 'step-started',
|
||||
stepNumber: step.stepNumber,
|
||||
agentName: step.agentName,
|
||||
task: step.task,
|
||||
});
|
||||
|
||||
const dependencyOutputs = this.gatherDependencyOutputs(
|
||||
step,
|
||||
stepResults,
|
||||
);
|
||||
|
||||
const promptWithContext = this.buildStepPrompt(step, dependencyOutputs);
|
||||
|
||||
const { stream: stepStream } =
|
||||
await this.agentExecutionService.streamChatResponse({
|
||||
workspace,
|
||||
agentId: agent.id,
|
||||
userWorkspaceId,
|
||||
messages: [
|
||||
{
|
||||
id: `step-${step.stepNumber}`,
|
||||
role: 'user' as const,
|
||||
parts: [{ type: 'text' as const, text: promptWithContext }],
|
||||
},
|
||||
],
|
||||
recordIdsByObjectMetadataNameSingular,
|
||||
});
|
||||
|
||||
let stepOutput = '';
|
||||
|
||||
if (writer) {
|
||||
writer.merge(
|
||||
stepStream.toUIMessageStream({
|
||||
onError: (error) => {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
},
|
||||
sendStart: false,
|
||||
onFinish: async ({ responseMessage }) => {
|
||||
stepOutput = responseMessage.parts
|
||||
.filter((part) => part.type === 'text')
|
||||
.map((part) => {
|
||||
if (part.type === 'text') {
|
||||
return part.text;
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
.join('');
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await stepStream.text;
|
||||
} else {
|
||||
stepOutput = await stepStream.text;
|
||||
}
|
||||
|
||||
stepResults.push({
|
||||
stepNumber: step.stepNumber,
|
||||
agentName: step.agentName,
|
||||
output: stepOutput,
|
||||
});
|
||||
|
||||
onProgress?.({
|
||||
type: 'step-completed',
|
||||
stepNumber: step.stepNumber,
|
||||
agentName: step.agentName,
|
||||
output: stepOutput,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Completed step ${step.stepNumber}: ${step.task.substring(0, 50)}...`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to execute step ${step.stepNumber}: ${step.task}`,
|
||||
error,
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Plan execution failed at step ${step.stepNumber}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const finalOutput = this.synthesizeResults(stepResults, steps);
|
||||
|
||||
return {
|
||||
finalOutput,
|
||||
stepResults,
|
||||
};
|
||||
}
|
||||
|
||||
private gatherDependencyOutputs(
|
||||
step: PlanStep,
|
||||
previousResults: StepResult[],
|
||||
): string {
|
||||
if (!step.dependsOn || step.dependsOn.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const dependencyOutputs = step.dependsOn
|
||||
.map((depStepNum) => {
|
||||
const depResult = previousResults.find(
|
||||
(result) => result.stepNumber === depStepNum,
|
||||
);
|
||||
|
||||
if (!depResult) {
|
||||
throw new Error(
|
||||
`Dependency step ${depStepNum} not found for step ${step.stepNumber}`,
|
||||
);
|
||||
}
|
||||
|
||||
return `Step ${depStepNum} output:\n${depResult.output}`;
|
||||
})
|
||||
.join('\n\n');
|
||||
|
||||
return dependencyOutputs;
|
||||
}
|
||||
|
||||
private buildStepPrompt(step: PlanStep, dependencyOutputs: string): string {
|
||||
let prompt = `Task: ${step.task}\n\nExpected output: ${step.expectedOutput}`;
|
||||
|
||||
if (dependencyOutputs) {
|
||||
prompt += `\n\nPrevious step results:\n${dependencyOutputs}`;
|
||||
}
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
private synthesizeResults(
|
||||
stepResults: StepResult[],
|
||||
steps: PlanStep[],
|
||||
): string {
|
||||
const lastStep = stepResults[stepResults.length - 1];
|
||||
|
||||
if (!lastStep) {
|
||||
return 'No results produced';
|
||||
}
|
||||
|
||||
const lastStepDefinition = steps.find(
|
||||
(s) => s.stepNumber === lastStep.stepNumber,
|
||||
);
|
||||
|
||||
if (lastStepDefinition) {
|
||||
const agentDefinition = standardAgentDefinitions.find(
|
||||
(def) => def.name === lastStepDefinition.agentName,
|
||||
);
|
||||
|
||||
if (agentDefinition?.outputStrategy === 'direct') {
|
||||
return lastStep.output;
|
||||
}
|
||||
}
|
||||
|
||||
const summary = stepResults
|
||||
.map((result) => {
|
||||
const step = steps.find((s) => s.stepNumber === result.stepNumber);
|
||||
|
||||
return `**Step ${result.stepNumber}: ${step?.task || 'Unknown task'}**\n${result.output}`;
|
||||
})
|
||||
.join('\n\n---\n\n');
|
||||
|
||||
return summary;
|
||||
}
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { type ToolSet } from 'ai';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import type { ActorMetadata } from 'twenty-shared/types';
|
||||
|
||||
import { SearchArticlesTool } from 'src/engine/core-modules/tool/tools/search-articles-tool/search-articles-tool';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import type { ToolHints } from 'src/engine/metadata-modules/ai/ai-chat-router/types/tool-hints.interface';
|
||||
import { ToolAdapterService } from 'src/engine/metadata-modules/ai/ai-tools/services/tool-adapter.service';
|
||||
import { ToolService } from 'src/engine/metadata-modules/ai/ai-tools/services/tool.service';
|
||||
import { HELPER_AGENT } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/agents/helper-agent';
|
||||
|
||||
@Injectable()
|
||||
export class AgentToolGeneratorService {
|
||||
private readonly logger = new Logger(AgentToolGeneratorService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(AgentEntity)
|
||||
private readonly agentRepository: Repository<AgentEntity>,
|
||||
private readonly toolAdapterService: ToolAdapterService,
|
||||
private readonly toolService: ToolService,
|
||||
private readonly searchArticlesTool: SearchArticlesTool,
|
||||
) {}
|
||||
|
||||
async generateToolsForAgent(
|
||||
agentId: string,
|
||||
workspaceId: string,
|
||||
actorContext?: ActorMetadata,
|
||||
roleIds?: string[],
|
||||
toolHints?: ToolHints,
|
||||
): Promise<ToolSet> {
|
||||
let tools: ToolSet = {};
|
||||
|
||||
try {
|
||||
const agent = await this.agentRepository.findOne({
|
||||
where: { id: agentId },
|
||||
});
|
||||
|
||||
if (agent?.standardId === HELPER_AGENT.standardId) {
|
||||
return this.wrapToolsWithErrorContext(this.getHelperAgentTools());
|
||||
}
|
||||
|
||||
const actionTools = await this.toolAdapterService.getTools();
|
||||
|
||||
tools = { ...actionTools };
|
||||
|
||||
if (!roleIds) {
|
||||
return this.wrapToolsWithErrorContext(tools);
|
||||
}
|
||||
|
||||
// Workflow tools are NOT generated here to avoid circular dependencies
|
||||
// They are provided via additionalTools from ChatToolsProviderService in the chat context
|
||||
|
||||
const databaseTools = await this.toolService.listTools(
|
||||
{ intersectionOf: roleIds },
|
||||
workspaceId,
|
||||
actorContext,
|
||||
toolHints,
|
||||
);
|
||||
|
||||
tools = { ...tools, ...databaseTools };
|
||||
|
||||
const roleActionTools = await this.toolAdapterService.getTools(
|
||||
{ intersectionOf: roleIds },
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
tools = { ...tools, ...roleActionTools };
|
||||
} catch (toolError) {
|
||||
this.logger.warn(
|
||||
`Failed to generate tools for agent ${agentId}: ${toolError.message}. Proceeding without tools.`,
|
||||
);
|
||||
}
|
||||
|
||||
return this.wrapToolsWithErrorContext(tools);
|
||||
}
|
||||
|
||||
private getHelperAgentTools(): ToolSet {
|
||||
const tools: ToolSet = {
|
||||
search_articles: {
|
||||
description: this.searchArticlesTool.description,
|
||||
inputSchema: this.searchArticlesTool.inputSchema,
|
||||
execute: async (params) =>
|
||||
this.searchArticlesTool.execute(params.input),
|
||||
},
|
||||
};
|
||||
|
||||
this.logger.log('Generated search_articles tool for Helper agent');
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
private wrapToolsWithErrorContext(tools: ToolSet): ToolSet {
|
||||
const wrappedTools: ToolSet = {};
|
||||
|
||||
for (const [toolName, tool] of Object.entries(tools)) {
|
||||
if (!tool.execute) {
|
||||
wrappedTools[toolName] = tool;
|
||||
continue;
|
||||
}
|
||||
|
||||
const originalExecute = tool.execute;
|
||||
|
||||
wrappedTools[toolName] = {
|
||||
...tool,
|
||||
execute: async (...args: Parameters<typeof originalExecute>) => {
|
||||
try {
|
||||
return await originalExecute(...args);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: errorMessage,
|
||||
tool: toolName,
|
||||
suggestion: this.generateErrorSuggestion(
|
||||
toolName,
|
||||
errorMessage,
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return wrappedTools;
|
||||
}
|
||||
|
||||
private generateErrorSuggestion(
|
||||
toolName: string,
|
||||
errorMessage: string,
|
||||
): string {
|
||||
const lowerError = errorMessage.toLowerCase();
|
||||
|
||||
if (
|
||||
lowerError.includes('not found') ||
|
||||
lowerError.includes('does not exist')
|
||||
) {
|
||||
return 'Verify the ID or name exists with a search query first';
|
||||
}
|
||||
|
||||
if (
|
||||
lowerError.includes('permission') ||
|
||||
lowerError.includes('forbidden') ||
|
||||
lowerError.includes('unauthorized')
|
||||
) {
|
||||
return 'This operation requires elevated permissions or a different role';
|
||||
}
|
||||
|
||||
if (lowerError.includes('invalid') || lowerError.includes('validation')) {
|
||||
return 'Check the tool schema for valid parameter formats and types';
|
||||
}
|
||||
|
||||
if (
|
||||
lowerError.includes('duplicate') ||
|
||||
lowerError.includes('already exists')
|
||||
) {
|
||||
return 'A record with this identifier already exists. Try updating instead of creating';
|
||||
}
|
||||
|
||||
if (lowerError.includes('required') || lowerError.includes('missing')) {
|
||||
return 'Required fields are missing. Check which fields are mandatory for this operation';
|
||||
}
|
||||
|
||||
return 'Try adjusting the parameters or using a different approach';
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import { type LanguageModelUsage } from 'ai';
|
||||
|
||||
export interface AgentExecutionResult {
|
||||
result: object;
|
||||
usage: LanguageModelUsage;
|
||||
}
|
||||
Reference in New Issue
Block a user