Files
twenty/packages/twenty-server/src/engine/metadata-modules/agent/agent-chat.service.ts
T
Abdul Rahman 32558673c6 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>
2025-10-22 15:02:41 +02:00

146 lines
3.9 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { ExtendedUIMessage } from 'twenty-shared/ai';
import { Repository } from 'typeorm';
import type { UIDataTypes, UIMessagePart, UITools } from 'ai';
import { AgentChatMessagePartEntity } from 'src/engine/metadata-modules/agent/agent-chat-message-part.entity';
import {
AgentChatMessageEntity,
AgentChatMessageRole,
} from 'src/engine/metadata-modules/agent/agent-chat-message.entity';
import { AgentChatThreadEntity } from 'src/engine/metadata-modules/agent/agent-chat-thread.entity';
import {
AgentException,
AgentExceptionCode,
} from 'src/engine/metadata-modules/agent/agent.exception';
import { mapUIMessagePartsToDBParts } from 'src/engine/metadata-modules/agent/utils/mapUIMessagePartsToDBParts';
import { AgentTitleGenerationService } from './agent-title-generation.service';
@Injectable()
export class AgentChatService {
constructor(
@InjectRepository(AgentChatThreadEntity)
private readonly threadRepository: Repository<AgentChatThreadEntity>,
@InjectRepository(AgentChatMessageEntity)
private readonly messageRepository: Repository<AgentChatMessageEntity>,
@InjectRepository(AgentChatMessagePartEntity)
private readonly messagePartRepository: Repository<AgentChatMessagePartEntity>,
private readonly titleGenerationService: AgentTitleGenerationService,
) {}
async createThread(userWorkspaceId: string) {
const thread = this.threadRepository.create({
userWorkspaceId,
});
return this.threadRepository.save(thread);
}
async getThreadsForUser(userWorkspaceId: string) {
return this.threadRepository.find({
where: {
userWorkspaceId,
},
order: { createdAt: 'DESC' },
});
}
async getThreadById(threadId: string, userWorkspaceId: string) {
const thread = await this.threadRepository.findOne({
where: {
id: threadId,
userWorkspaceId,
},
});
if (!thread) {
throw new AgentException(
'Thread not found',
AgentExceptionCode.AGENT_EXECUTION_FAILED,
);
}
return thread;
}
async addMessage({
threadId,
uiMessage,
}: {
threadId: string;
uiMessage: Omit<ExtendedUIMessage, 'id'>;
uiMessageParts?: UIMessagePart<UIDataTypes, UITools>[];
}) {
const message = this.messageRepository.create({
threadId,
role: uiMessage.role as AgentChatMessageRole,
});
const savedMessage = await this.messageRepository.save(message);
if (uiMessage.parts && uiMessage.parts.length > 0) {
const dbParts = mapUIMessagePartsToDBParts(
uiMessage.parts,
savedMessage.id,
);
await this.messagePartRepository.save(dbParts);
}
const messageContent = uiMessage.parts.find(
(part) => part.type === 'text',
)?.text;
if (messageContent) {
this.generateTitleIfNeeded(threadId, messageContent);
}
return savedMessage;
}
async getMessagesForThread(threadId: string, userWorkspaceId: string) {
const thread = await this.threadRepository.findOne({
where: {
id: threadId,
userWorkspaceId,
},
});
if (!thread) {
throw new AgentException(
'Thread not found',
AgentExceptionCode.AGENT_EXECUTION_FAILED,
);
}
return this.messageRepository.find({
where: { threadId },
order: { createdAt: 'ASC' },
relations: ['parts'],
});
}
private async generateTitleIfNeeded(
threadId: string,
messageContent: string,
) {
const thread = await this.threadRepository.findOne({
where: { id: threadId },
select: ['id', 'title'],
});
if (!thread || thread.title || !messageContent) {
return;
}
const title =
await this.titleGenerationService.generateThreadTitle(messageContent);
await this.threadRepository.update(threadId, { title });
}
}