feat: simplify AI chat architecture and add record links (#16463)

## Summary

This PR significantly simplifies the AI chat architecture by removing
complex routing/planning mechanisms and introduces clickable record
links in AI responses.

## Changes

### AI Chat Architecture Simplification
- **Removed** the entire `ai-chat-router` module (~850 lines) including:
  - Strategy decider service
  - Plan generator service
  - Complex routing logic
- **Removed** agent execution planning services (~700 lines):
  - `agent-execution.service.ts`
  - `agent-plan-executor.service.ts`
  - `agent-tool-generator.service.ts`
- **Added** centralized `ToolRegistryService` for tool management:
  - Builds searchable tool index (database, action, workflow tools)
  - Provides tool lookup by name
  - Supports agent search for loading expertise
- **Added** `ChatExecutionService` as simple replacement:
  - Includes full tool catalog in system prompt
- Pre-loads common tools (find/create/update for company, person,
opportunity, task, note)
  - Uses `load_tools` mechanism for dynamic tool activation
  - Enables native web search by default

### Record References in AI Responses
- Added `recordReferences` field to tool outputs for create, find, and
update operations
- Implemented `[[record:objectName:recordId:displayName]]` syntax for AI
to reference records
- Created `RecordLink` component that renders clickable chips with
object icons
- Integrated record link parsing into the markdown renderer
- Users can now click directly on created/found records in AI responses

### Workflow Agent Fixes
- Fixed cache invalidation issue when creating agents in workflows
- Added default prompt for workflow-created agents to prevent validation
errors
- Relaxed agent validation to only check properties being updated (not
all required properties)

### Code Quality Improvements
- Extracted `getRecordDisplayName` utility that mirrors frontend's
`getLabelIdentifierFieldValue` logic
- Uses object metadata to determine the correct label identifier field
- Handles `FULL_NAME` composite type for person/workspaceMember objects
- Shared across create, find, and update record services

## Net Impact
- **~1,200 lines deleted** (complex routing/planning code)
- **~500 lines added** (simpler tool registry + record links)
- Significantly reduced code complexity
- Better tool discovery through full catalog in system prompt
- Improved UX with clickable record references

## Testing
- Typecheck passes
- Lint passes
- Manual testing of AI chat with record creation and linking
This commit is contained in:
Félix Malfait
2025-12-10 15:14:12 +01:00
committed by GitHub
parent 59b54f254e
commit 5df8fd90c3
49 changed files with 1708 additions and 2700 deletions
@@ -9,6 +9,7 @@ import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
import { WorkflowVersionStepOperationsWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service';
import {
@@ -28,6 +29,7 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
let objectMetadataRepository: jest.Mocked<any>;
let workflowCommonWorkspaceService: jest.Mocked<WorkflowCommonWorkspaceService>;
let aiAgentRoleService: jest.Mocked<AiAgentRoleService>;
let workspaceCacheService: jest.Mocked<WorkspaceCacheService>;
beforeEach(async () => {
serverlessFunctionService = {
@@ -65,6 +67,10 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
deleteAgentOnlyRoleIfUnused: jest.fn(),
} as unknown as jest.Mocked<AiAgentRoleService>;
workspaceCacheService = {
flush: jest.fn(),
} as unknown as jest.Mocked<WorkspaceCacheService>;
twentyORMGlobalManager = {
getRepositoryForWorkspace: jest.fn(),
} as unknown as jest.Mocked<TwentyORMGlobalManager>;
@@ -104,6 +110,10 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
provide: AiAgentRoleService,
useValue: aiAgentRoleService,
},
{
provide: WorkspaceCacheService,
useValue: workspaceCacheService,
},
],
}).compile();
@@ -16,6 +16,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import {
WorkflowVersionStepException,
WorkflowVersionStepExceptionCode,
@@ -61,6 +62,7 @@ export class WorkflowVersionStepOperationsWorkspaceService {
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
private readonly aiAgentRoleService: AiAgentRoleService,
private readonly workspaceCacheService: WorkspaceCacheService,
) {}
async runWorkflowVersionStepDeletionSideEffects({
@@ -369,7 +371,8 @@ export class WorkflowVersionStepOperationsWorkspaceService {
label: 'Workflow Agent' + workflowVersion.workflowId.substring(0, 4),
icon: 'IconRobot',
description: '',
prompt: '',
prompt:
'You are a helpful AI assistant. Complete the task based on the workflow context.',
modelId: DEFAULT_SMART_MODEL,
responseFormat: { type: 'text' },
workspaceId,
@@ -383,6 +386,8 @@ export class WorkflowVersionStepOperationsWorkspaceService {
);
}
await this.workspaceCacheService.flush(workspaceId, ['flatAgentMaps']);
return {
builtStep: {
...baseStep,
@@ -8,6 +8,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
import { WorkflowSchemaModule } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.module';
import { WorkflowVersionStepCreationWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-creation.workspace-service';
@@ -23,6 +24,7 @@ import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workfl
ServerlessFunctionModule,
WorkflowCommonModule,
AiAgentRoleModule,
WorkspaceCacheModule,
NestjsQueryTypeOrmModule.forFeature([
ObjectMetadataEntity,
AgentEntity,