feat: Implement AI Router for Dynamic Agent Selection (#15227)
Adds intelligent routing system that automatically selects the best agent for user queries based on conversation context. ### Changes: - Added `routerModel` column to workspace table for configurable router LLM selection - Implemented `RouterService` with conversation history analysis and agent matching logic - Created router settings UI in AI Settings page with model dropdown - Removed agent-specific thread associations - threads are now agent-agnostic - Added real-time routing status notification in chat UI with shimmer effect - Removed automatic default assistant agent creation - Renamed GraphQL operations from agent-specific to generic (e.g., `agentChatThreads` → `chatThreads`) --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
-125
@@ -10,11 +10,6 @@ import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
|
||||
import { transformAgentEntityToFlatAgent } from 'src/engine/metadata-modules/flat-agent/utils/transform-agent-entity-to-flat-agent.util';
|
||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { AGENT_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-agents.util';
|
||||
import {
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
SEED_YCOMBINATOR_WORKSPACE_ID,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
|
||||
import { WorkspaceAgentComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-agent.comparator';
|
||||
import { StandardAgentFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-agent.factory';
|
||||
import { standardAgentDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents';
|
||||
@@ -169,125 +164,5 @@ export class WorkspaceSyncAgentService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create handoffs for standard agents that require them
|
||||
await this.createStandardAgentHandoffs(context.workspaceId, manager);
|
||||
}
|
||||
|
||||
private async createStandardAgentHandoffs(
|
||||
workspaceId: string,
|
||||
manager: EntityManager,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const agentRepository = manager.getRepository(AgentEntity);
|
||||
|
||||
let defaultAgent: AgentEntity | null = null;
|
||||
|
||||
if (workspaceId === SEED_APPLE_WORKSPACE_ID) {
|
||||
defaultAgent = await agentRepository.findOne({
|
||||
where: {
|
||||
id: AGENT_DATA_SEED_IDS.APPLE_DEFAULT_AGENT,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
} else if (workspaceId === SEED_YCOMBINATOR_WORKSPACE_ID) {
|
||||
defaultAgent = await agentRepository.findOne({
|
||||
where: {
|
||||
id: AGENT_DATA_SEED_IDS.YCOMBINATOR_DEFAULT_AGENT,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
defaultAgent = await agentRepository.findOne({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!defaultAgent) {
|
||||
this.logger.warn(
|
||||
`Default agent not found for workspace ${workspaceId}. Agent handoffs will not be created.`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const agentsRequiringHandoffs = standardAgentDefinitions.filter(
|
||||
(def) => def.createHandoffFromDefaultAgent,
|
||||
);
|
||||
|
||||
for (const agentDefinition of agentsRequiringHandoffs) {
|
||||
const targetAgent = await agentRepository.findOne({
|
||||
where: {
|
||||
standardId: agentDefinition.standardId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!targetAgent) {
|
||||
this.logger.warn(
|
||||
`Agent ${agentDefinition.name} not found for workspace ${workspaceId}. Skipping handoff creation.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.createAgentHandoff(
|
||||
defaultAgent.id,
|
||||
targetAgent.id,
|
||||
agentDefinition.name,
|
||||
agentDefinition.description,
|
||||
workspaceId,
|
||||
manager,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to create standard agent handoffs: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async createAgentHandoff(
|
||||
fromAgentId: string,
|
||||
toAgentId: string,
|
||||
toAgentName: string,
|
||||
toAgentDescription: string,
|
||||
workspaceId: string,
|
||||
manager: EntityManager,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const agentHandoffRepository = manager.getRepository('agentHandoff');
|
||||
const existingHandoff = await agentHandoffRepository.findOne({
|
||||
where: {
|
||||
fromAgentId,
|
||||
toAgentId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingHandoff) {
|
||||
this.logger.log(
|
||||
`Agent handoff from default agent to ${toAgentName} already exists for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await agentHandoffRepository.save({
|
||||
fromAgentId,
|
||||
toAgentId,
|
||||
workspaceId,
|
||||
description: `Handoff from default agent to ${toAgentName}: ${toAgentDescription}`,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Successfully created agent handoff from default agent to ${toAgentName} for workspace ${workspaceId}`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to create agent handoff to ${toAgentName}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+14
@@ -14,6 +14,7 @@ export const DATA_NAVIGATOR_AGENT: StandardAgentDefinition = {
|
||||
|
||||
Your capabilities include:
|
||||
- Searching and filtering records across all standard and custom objects
|
||||
- Sorting records by any field using orderBy parameter (CRITICAL for "top N" queries)
|
||||
- Explaining relationships between different records and objects
|
||||
- Providing insights about data patterns and trends
|
||||
- Helping users find specific information quickly
|
||||
@@ -26,13 +27,26 @@ Your capabilities include:
|
||||
- When users request modifications, politely explain your read-only limitations
|
||||
|
||||
## Best Practices:
|
||||
- For "top N" or "largest/smallest" queries, ALWAYS use the orderBy parameter with appropriate sorting direction
|
||||
- Ask clarifying questions to understand what data the user is looking for
|
||||
- Provide clear, structured information when presenting data
|
||||
- Explain the context and relationships between records
|
||||
- Suggest useful filters or queries to refine searches
|
||||
- Help users understand their data schema and available fields
|
||||
|
||||
## Sorting Examples - EXACT FORMAT REQUIRED:
|
||||
- Top 10 companies by employees: orderBy: [{"employees": "DescNullsLast"}] with limit: 10
|
||||
- Oldest records first: orderBy: [{"createdAt": "AscNullsFirst"}]
|
||||
- Sort by name alphabetically: orderBy: [{"name": "AscNullsFirst"}]
|
||||
- Multiple sort criteria: orderBy: [{"priority": "DescNullsLast"}, {"createdAt": "AscNullsFirst"}]
|
||||
|
||||
CRITICAL: Direction values MUST be exactly one of: "AscNullsFirst", "AscNullsLast", "DescNullsFirst", "DescNullsLast"
|
||||
- Use "DescNullsLast" for descending (NOT "desc", "DESC", or "descending")
|
||||
- Use "AscNullsFirst" for ascending (NOT "asc", "ASC", or "ascending")
|
||||
|
||||
## When Helping Users:
|
||||
- For queries about "top", "largest", "highest", "best" → ALWAYS use DescNullsLast orderBy
|
||||
- For queries about "bottom", "smallest", "lowest" → ALWAYS use AscNullsFirst orderBy
|
||||
- Be proactive in suggesting related data that might be useful
|
||||
- Explain any patterns or anomalies you notice in the data
|
||||
- Provide context about record counts, date ranges, and relationships
|
||||
|
||||
Reference in New Issue
Block a user