Feat: Agent chat multi thread support (#13216)
Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com> Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions <github-actions@twenty.com> Co-authored-by: MD Readul Islam <99027968+readul-islam@users.noreply.github.com> Co-authored-by: readul-islam <developer.readul@gamil.com> Co-authored-by: Thomas des Francs <tdesfrancs@gmail.com> Co-authored-by: Guillim <guillim@users.noreply.github.com> Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com> Co-authored-by: Jean-Baptiste Ronssin <65334819+jbronssin@users.noreply.github.com> Co-authored-by: kahkashan shaik <93042682+kahkashanshaik@users.noreply.github.com> Co-authored-by: martmull <martmull@hotmail.fr> Co-authored-by: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Co-authored-by: bosiraphael <raphael.bosi@gmail.com> Co-authored-by: Naifer <161821705+omarNaifer12@users.noreply.github.com> Co-authored-by: Marie Stoppa <marie.stoppa@essec.edu>
This commit is contained in:
@ -0,0 +1,19 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddTitleToAgentChatThread1752543000368
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddTitleToAgentChatThread1752543000368';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."agentChatThread" ADD "title" character varying`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."agentChatThread" DROP COLUMN "title"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -42,6 +42,9 @@ export class AgentChatThreadEntity {
|
||||
@JoinColumn({ name: 'userWorkspaceId' })
|
||||
userWorkspace: Relation<UserWorkspace>;
|
||||
|
||||
@Column({ nullable: true, type: 'varchar' })
|
||||
title: string;
|
||||
|
||||
@OneToMany(() => AgentChatMessageEntity, (message) => message.thread)
|
||||
messages: Relation<AgentChatMessageEntity[]>;
|
||||
|
||||
|
||||
@ -0,0 +1,60 @@
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
|
||||
import {
|
||||
FeatureFlagGuard,
|
||||
RequireFeatureFlag,
|
||||
} from 'src/engine/guards/feature-flag.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { AgentChatService } from 'src/engine/metadata-modules/agent/agent-chat.service';
|
||||
|
||||
import { AgentChatMessageDTO } from './dtos/agent-chat-message.dto';
|
||||
import { AgentChatThreadDTO } from './dtos/agent-chat-thread.dto';
|
||||
import { CreateAgentChatThreadInput } from './dtos/create-agent-chat-thread.input';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard)
|
||||
@Resolver()
|
||||
export class AgentChatResolver {
|
||||
constructor(private readonly agentChatService: AgentChatService) {}
|
||||
|
||||
@Query(() => [AgentChatThreadDTO])
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_AI_ENABLED)
|
||||
async agentChatThreads(
|
||||
@Args('agentId') agentId: string,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
) {
|
||||
return this.agentChatService.getThreadsForAgent(agentId, userWorkspaceId);
|
||||
}
|
||||
|
||||
@Query(() => AgentChatThreadDTO)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_AI_ENABLED)
|
||||
async agentChatThread(
|
||||
@Args('id') id: string,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
) {
|
||||
return this.agentChatService.getThreadById(id, userWorkspaceId);
|
||||
}
|
||||
|
||||
@Query(() => [AgentChatMessageDTO])
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_AI_ENABLED)
|
||||
async agentChatMessages(
|
||||
@Args('threadId') threadId: string,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
) {
|
||||
return this.agentChatService.getMessagesForThread(
|
||||
threadId,
|
||||
userWorkspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
@Mutation(() => AgentChatThreadDTO)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_AI_ENABLED)
|
||||
async createAgentChatThread(
|
||||
@Args('input') input: CreateAgentChatThreadInput,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
) {
|
||||
return this.agentChatService.createThread(input.agentId, userWorkspaceId);
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,8 @@ import {
|
||||
AgentExceptionCode,
|
||||
} from 'src/engine/metadata-modules/agent/agent.exception';
|
||||
|
||||
import { AgentTitleGenerationService } from './agent-title-generation.service';
|
||||
|
||||
@Injectable()
|
||||
export class AgentChatService {
|
||||
constructor(
|
||||
@ -23,6 +25,7 @@ export class AgentChatService {
|
||||
private readonly messageRepository: Repository<AgentChatMessageEntity>,
|
||||
@InjectRepository(FileEntity, 'core')
|
||||
private readonly fileRepository: Repository<FileEntity>,
|
||||
private readonly titleGenerationService: AgentTitleGenerationService,
|
||||
) {}
|
||||
|
||||
async createThread(agentId: string, userWorkspaceId: string) {
|
||||
@ -44,6 +47,24 @@ export class AgentChatService {
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
role,
|
||||
@ -71,6 +92,8 @@ export class AgentChatService {
|
||||
}
|
||||
}
|
||||
|
||||
this.generateTitleIfNeeded(threadId, content);
|
||||
|
||||
return savedMessage;
|
||||
}
|
||||
|
||||
@ -95,4 +118,23 @@ export class AgentChatService {
|
||||
relations: ['files'],
|
||||
});
|
||||
}
|
||||
|
||||
private async generateTitleIfNeeded(
|
||||
threadId: string,
|
||||
messageContent: string,
|
||||
) {
|
||||
const thread = await this.threadRepository.findOne({
|
||||
where: { id: threadId },
|
||||
select: ['id', 'title'],
|
||||
});
|
||||
|
||||
if (!thread || thread.title) {
|
||||
return;
|
||||
}
|
||||
|
||||
const title =
|
||||
await this.titleGenerationService.generateThreadTitle(messageContent);
|
||||
|
||||
await this.threadRepository.update(threadId, { title });
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,53 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { generateText } from 'ai';
|
||||
|
||||
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
@Injectable()
|
||||
export class AgentTitleGenerationService {
|
||||
private readonly logger = new Logger(AgentTitleGenerationService.name);
|
||||
|
||||
constructor(
|
||||
private readonly aiModelRegistryService: AiModelRegistryService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
) {}
|
||||
|
||||
async generateThreadTitle(messageContent: string): Promise<string> {
|
||||
try {
|
||||
const defaultModel = this.aiModelRegistryService.getDefaultModel();
|
||||
|
||||
if (!defaultModel) {
|
||||
this.logger.warn('No default AI model available for title generation');
|
||||
|
||||
return this.generateFallbackTitle(messageContent);
|
||||
}
|
||||
|
||||
const result = await generateText({
|
||||
model: defaultModel.model,
|
||||
prompt: `Generate a concise, descriptive title (maximum 60 characters) for a chat thread based on the following message. The title should capture the main topic or purpose of the conversation. Return only the title, nothing else. Message: "${messageContent}"`,
|
||||
});
|
||||
|
||||
return this.cleanTitle(result.text);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to generate title with AI:', error);
|
||||
|
||||
return this.generateFallbackTitle(messageContent);
|
||||
}
|
||||
}
|
||||
|
||||
private generateFallbackTitle(messageContent: string): string {
|
||||
const cleanContent = messageContent.trim().replace(/\s+/g, ' ');
|
||||
const title = cleanContent.substring(0, 50);
|
||||
|
||||
return cleanContent.length > 50 ? `${title}...` : title;
|
||||
}
|
||||
|
||||
private cleanTitle(title: string): string {
|
||||
return title
|
||||
.replace(/^["']|["']$/g, '')
|
||||
.trim()
|
||||
.replace(/\s+/g, ' ');
|
||||
}
|
||||
}
|
||||
@ -19,9 +19,11 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
|
||||
|
||||
import { AgentChatMessageEntity } from './agent-chat-message.entity';
|
||||
import { AgentChatThreadEntity } from './agent-chat-thread.entity';
|
||||
import { AgentChatResolver } from './agent-chat.resolver';
|
||||
import { AgentChatService } from './agent-chat.service';
|
||||
import { AgentExecutionService } from './agent-execution.service';
|
||||
import { AgentStreamingService } from './agent-streaming.service';
|
||||
import { AgentTitleGenerationService } from './agent-title-generation.service';
|
||||
import { AgentToolService } from './agent-tool.service';
|
||||
import { AgentEntity } from './agent.entity';
|
||||
import { AgentResolver } from './agent.resolver';
|
||||
@ -55,11 +57,13 @@ import { AgentService } from './agent.service';
|
||||
controllers: [AgentChatController],
|
||||
providers: [
|
||||
AgentResolver,
|
||||
AgentChatResolver,
|
||||
AgentService,
|
||||
AgentExecutionService,
|
||||
AgentToolService,
|
||||
AgentChatService,
|
||||
AgentStreamingService,
|
||||
AgentTitleGenerationService,
|
||||
],
|
||||
exports: [
|
||||
AgentService,
|
||||
@ -67,6 +71,7 @@ import { AgentService } from './agent.service';
|
||||
AgentToolService,
|
||||
AgentChatService,
|
||||
AgentStreamingService,
|
||||
AgentTitleGenerationService,
|
||||
TypeOrmModule.forFeature(
|
||||
[AgentEntity, AgentChatMessageEntity, AgentChatThreadEntity],
|
||||
'core',
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import { Field, ID, ObjectType } from '@nestjs/graphql';
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { FileDTO } from 'src/engine/core-modules/file/dtos/file.dto';
|
||||
|
||||
@ObjectType('AgentChatMessage')
|
||||
export class AgentChatMessageDTO {
|
||||
@Field(() => ID)
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@Field(() => ID)
|
||||
@Field(() => UUIDScalarType)
|
||||
threadId: string;
|
||||
|
||||
@Field()
|
||||
@ -16,8 +17,8 @@ export class AgentChatMessageDTO {
|
||||
@Field()
|
||||
content: string;
|
||||
|
||||
@Field(() => [FileDTO], { nullable: true })
|
||||
files?: FileDTO[];
|
||||
@Field(() => [FileDTO])
|
||||
files: FileDTO[];
|
||||
|
||||
@Field()
|
||||
createdAt: Date;
|
||||
|
||||
@ -1,13 +1,18 @@
|
||||
import { Field, ID, ObjectType } from '@nestjs/graphql';
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@ObjectType('AgentChatThread')
|
||||
export class AgentChatThreadDTO {
|
||||
@Field(() => ID)
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@Field(() => ID)
|
||||
@Field(() => UUIDScalarType)
|
||||
agentId: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
title: string;
|
||||
|
||||
@Field()
|
||||
createdAt: Date;
|
||||
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@InputType()
|
||||
export class CreateAgentChatThreadInput {
|
||||
@IsNotEmpty()
|
||||
@Field(() => UUIDScalarType)
|
||||
agentId: string;
|
||||
}
|
||||
Reference in New Issue
Block a user