Feat - Agent chat tab (#13061)

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Marie <51697796+ijreilly@users.noreply.github.com>
Co-authored-by: Antoine Moreaux <moreaux.antoine@gmail.com>
Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com>
This commit is contained in:
Abdul Rahman
2025-07-08 02:17:41 +05:30
committed by GitHub
parent 29f7b74756
commit 51d02c13bf
40 changed files with 2777 additions and 127 deletions

View File

@ -0,0 +1,61 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddAgentChatMessageAndThreadTable1751467467020
implements MigrationInterface
{
name = 'AddAgentChatMessageAndThreadTable1751467467020';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TYPE "core"."agentChatMessage_role_enum" AS ENUM('user', 'assistant')`,
);
await queryRunner.query(
`CREATE TABLE "core"."agentChatMessage" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "threadId" uuid NOT NULL, "role" "core"."agentChatMessage_role_enum" NOT NULL, "content" text NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_f54a95b34e98d94251bce37a180" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_cd5b23d4e471b630137b3017ba" ON "core"."agentChatMessage" ("threadId") `,
);
await queryRunner.query(
`CREATE TABLE "core"."agentChatThread" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "agentId" uuid NOT NULL, "userWorkspaceId" uuid NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_a53b1d75d11ec67d13590cfa627" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_d0bdc80c68a48b1f26727aabfe" ON "core"."agentChatThread" ("agentId") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_3bd935d6f8c5ce87194b8db824" ON "core"."agentChatThread" ("userWorkspaceId") `,
);
await queryRunner.query(
`ALTER TABLE "core"."agentChatMessage" ADD CONSTRAINT "FK_cd5b23d4e471b630137b3017ba6" FOREIGN KEY ("threadId") REFERENCES "core"."agentChatThread"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "core"."agentChatThread" ADD CONSTRAINT "FK_d0bdc80c68a48b1f26727aabfe6" FOREIGN KEY ("agentId") REFERENCES "core"."agent"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "core"."agentChatThread" ADD CONSTRAINT "FK_3bd935d6f8c5ce87194b8db8240" FOREIGN KEY ("userWorkspaceId") REFERENCES "core"."userWorkspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."agentChatThread" DROP CONSTRAINT "FK_3bd935d6f8c5ce87194b8db8240"`,
);
await queryRunner.query(
`ALTER TABLE "core"."agentChatThread" DROP CONSTRAINT "FK_d0bdc80c68a48b1f26727aabfe6"`,
);
await queryRunner.query(
`ALTER TABLE "core"."agentChatMessage" DROP CONSTRAINT "FK_cd5b23d4e471b630137b3017ba6"`,
);
await queryRunner.query(`DROP TABLE "core"."agentChatThread"`);
await queryRunner.query(
`DROP INDEX "core"."IDX_cd5b23d4e471b630137b3017ba"`,
);
await queryRunner.query(
`DROP INDEX "core"."IDX_d0bdc80c68a48b1f26727aabfe"`,
);
await queryRunner.query(
`DROP INDEX "core"."IDX_3bd935d6f8c5ce87194b8db824"`,
);
await queryRunner.query(`DROP TABLE "core"."agentChatMessage"`);
await queryRunner.query(`DROP TYPE "core"."agentChatMessage_role_enum"`);
}
}

View File

@ -0,0 +1,42 @@
import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
Relation,
} from 'typeorm';
import { AgentChatThreadEntity } from 'src/engine/metadata-modules/agent/agent-chat-thread.entity';
export enum AgentChatMessageRole {
USER = 'user',
ASSISTANT = 'assistant',
}
@Entity('agentChatMessage')
export class AgentChatMessageEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column('uuid')
@Index()
threadId: string;
@ManyToOne(() => AgentChatThreadEntity, (thread) => thread.messages, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'threadId' })
thread: Relation<AgentChatThreadEntity>;
@Column({ type: 'enum', enum: AgentChatMessageRole })
role: AgentChatMessageRole;
@Column('text')
content: string;
@CreateDateColumn()
createdAt: Date;
}

View File

@ -0,0 +1,53 @@
import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { AgentChatMessageEntity } from 'src/engine/metadata-modules/agent/agent-chat-message.entity';
import { AgentEntity } from './agent.entity';
@Entity('agentChatThread')
export class AgentChatThreadEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column('uuid')
@Index()
agentId: string;
@ManyToOne(() => AgentEntity, (agent) => agent.chatThreads, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'agentId' })
agent: Relation<AgentEntity>;
@Column('uuid')
@Index()
userWorkspaceId: string;
@ManyToOne(() => UserWorkspace, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'userWorkspaceId' })
userWorkspace: Relation<UserWorkspace>;
@OneToMany(() => AgentChatMessageEntity, (message) => message.thread)
messages: Relation<AgentChatMessageEntity[]>;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1,72 @@
import {
Body,
Controller,
Get,
Param,
Post,
Res,
UseFilters,
UseGuards,
} from '@nestjs/common';
import { Response } from 'express';
import { RestApiExceptionFilter } from 'src/engine/api/rest/rest-api-exception.filter';
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { AgentChatService } from './agent-chat.service';
import { AgentStreamingService } from './agent-streaming.service';
@Controller('rest/agent-chat')
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
@UseFilters(RestApiExceptionFilter)
export class AgentChatController {
constructor(
private readonly agentChatService: AgentChatService,
private readonly agentStreamingService: AgentStreamingService,
) {}
@Get('threads/:agentId')
async getThreadsForAgent(
@Param('agentId') agentId: string,
@AuthUserWorkspaceId() userWorkspaceId: string,
) {
return this.agentChatService.getThreadsForAgent(agentId, userWorkspaceId);
}
@Get('threads/:threadId/messages')
async getMessagesForThread(
@Param('threadId') threadId: string,
@AuthUserWorkspaceId() userWorkspaceId: string,
) {
return this.agentChatService.getMessagesForThread(
threadId,
userWorkspaceId,
);
}
@Post('threads')
async createThread(
@Body() body: { agentId: string },
@AuthUserWorkspaceId() userWorkspaceId: string,
) {
return this.agentChatService.createThread(body.agentId, userWorkspaceId);
}
@Post('stream')
async streamAgentChat(
@Body()
body: { threadId: string; userMessage: string },
@AuthUserWorkspaceId() userWorkspaceId: string,
@Res() res: Response,
) {
await this.agentStreamingService.streamAgentChat({
threadId: body.threadId,
userMessage: body.userMessage,
userWorkspaceId,
res,
});
}
}

View File

@ -0,0 +1,85 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
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 { AgentExecutionService } from './agent-execution.service';
@Injectable()
export class AgentChatService {
constructor(
@InjectRepository(AgentChatThreadEntity, 'core')
private readonly threadRepository: Repository<AgentChatThreadEntity>,
@InjectRepository(AgentChatMessageEntity, 'core')
private readonly messageRepository: Repository<AgentChatMessageEntity>,
private readonly agentExecutionService: AgentExecutionService,
) {}
async createThread(agentId: string, userWorkspaceId: string) {
const thread = this.threadRepository.create({
agentId,
userWorkspaceId,
});
return this.threadRepository.save(thread);
}
async getThreadsForAgent(agentId: string, userWorkspaceId: string) {
return this.threadRepository.find({
where: {
agentId,
userWorkspaceId,
},
order: { createdAt: 'DESC' },
});
}
async addMessage({
threadId,
role,
content,
}: {
threadId: string;
role: AgentChatMessageRole;
content: string;
}) {
const message = this.messageRepository.create({
threadId,
role,
content,
});
return this.messageRepository.save(message);
}
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' },
});
}
}

View File

@ -1,8 +1,10 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { createAnthropic } from '@ai-sdk/anthropic';
import { createOpenAI } from '@ai-sdk/openai';
import { generateObject, generateText } from 'ai';
import { CoreMessage, generateObject, generateText, streamText } from 'ai';
import { Repository } from 'typeorm';
import {
ModelId,
@ -10,6 +12,10 @@ import {
} from 'src/engine/core-modules/ai/constants/ai-models.const';
import { getAIModelById } from 'src/engine/core-modules/ai/utils/get-ai-model-by-id';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import {
AgentChatMessageEntity,
AgentChatMessageRole,
} from 'src/engine/metadata-modules/agent/agent-chat-message.entity';
import { AgentToolService } from 'src/engine/metadata-modules/agent/agent-tool.service';
import { AGENT_CONFIG } from 'src/engine/metadata-modules/agent/constants/agent-config.const';
import { AGENT_SYSTEM_PROMPTS } from 'src/engine/metadata-modules/agent/constants/agent-system-prompts.const';
@ -37,9 +43,13 @@ export class AgentExecutionService {
constructor(
private readonly twentyConfigService: TwentyConfigService,
private readonly agentToolService: AgentToolService,
@InjectRepository(AgentEntity, 'core')
private readonly agentRepository: Repository<AgentEntity>,
@InjectRepository(AgentChatMessageEntity, 'core')
private readonly agentChatmessageRepository: Repository<AgentChatMessageEntity>,
) {}
private getModel = (modelId: ModelId, provider: ModelProvider) => {
getModel = (modelId: ModelId, provider: ModelProvider) => {
switch (provider) {
case ModelProvider.OPENAI: {
const OpenAIProvider = createOpenAI({
@ -79,7 +89,6 @@ export class AgentExecutionService {
AgentExceptionCode.AGENT_EXECUTION_FAILED,
);
}
if (!apiKey) {
throw new AgentException(
`${provider.toUpperCase()} API key not configured`,
@ -88,6 +97,76 @@ export class AgentExecutionService {
}
}
async prepareAIRequestConfig({
messages,
prompt,
system,
agent,
}: {
system: string;
agent: AgentEntity;
prompt?: string;
messages?: CoreMessage[];
}) {
const aiModel = getAIModelById(agent.modelId);
if (!aiModel) {
throw new AgentException(
`AI model with id ${agent.modelId} not found`,
AgentExceptionCode.AGENT_EXECUTION_FAILED,
);
}
const provider = aiModel.provider;
await this.validateApiKey(provider);
const tools = await this.agentToolService.generateToolsForAgent(
agent.id,
agent.workspaceId,
);
return {
system,
tools,
model: this.getModel(agent.modelId, aiModel.provider),
...(messages && { messages }),
...(prompt && { prompt }),
maxSteps: AGENT_CONFIG.MAX_STEPS,
};
}
async streamChatResponse({
agentId,
userMessage,
messages,
}: {
agentId: string;
userMessage: string;
messages: AgentChatMessageEntity[];
}) {
const agent = await this.agentRepository.findOneOrFail({
where: { id: agentId },
});
const llmMessages: CoreMessage[] = messages.map(({ role, content }) => ({
role,
content,
}));
llmMessages.push({
role: AgentChatMessageRole.USER,
content: userMessage,
});
const aiRequestConfig = await this.prepareAIRequestConfig({
system: `${AGENT_SYSTEM_PROMPTS.AGENT_CHAT}\n\n${agent.prompt}`,
agent,
messages: llmMessages,
});
return streamText(aiRequestConfig);
}
async executeAgent({
agent,
context,
@ -98,31 +177,12 @@ export class AgentExecutionService {
schema: OutputSchema;
}): Promise<AgentExecutionResult> {
try {
const aiModel = getAIModelById(agent.modelId);
if (!aiModel) {
throw new AgentException(
`AI model with id ${agent.modelId} not found`,
AgentExceptionCode.AGENT_EXECUTION_FAILED,
);
}
const provider = aiModel.provider;
await this.validateApiKey(provider);
const tools = await this.agentToolService.generateToolsForAgent(
agent.id,
agent.workspaceId,
);
const textResponse = await generateText({
const aiRequestConfig = await this.prepareAIRequestConfig({
system: AGENT_SYSTEM_PROMPTS.AGENT_EXECUTION,
model: this.getModel(agent.modelId, provider),
agent,
prompt: resolveInput(agent.prompt, context) as string,
tools,
maxSteps: AGENT_CONFIG.MAX_STEPS,
});
const textResponse = await generateText(aiRequestConfig);
if (Object.keys(schema).length === 0) {
return {
@ -130,10 +190,9 @@ export class AgentExecutionService {
usage: textResponse.usage,
};
}
const output = await generateObject({
system: AGENT_SYSTEM_PROMPTS.OUTPUT_GENERATOR,
model: this.getModel(agent.modelId, provider),
model: aiRequestConfig.model,
prompt: `Based on the following execution results, generate the structured output according to the schema:
Execution Results: ${textResponse.text}
@ -163,7 +222,6 @@ export class AgentExecutionService {
if (error instanceof AgentException) {
throw error;
}
throw new AgentException(
error instanceof Error ? error.message : 'Agent execution failed',
AgentExceptionCode.AGENT_EXECUTION_FAILED,

View File

@ -0,0 +1,102 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Response } from 'express';
import { Repository } from 'typeorm';
import { AgentChatMessageRole } from 'src/engine/metadata-modules/agent/agent-chat-message.entity';
import { AgentChatThreadEntity } from 'src/engine/metadata-modules/agent/agent-chat-thread.entity';
import { AgentChatService } from 'src/engine/metadata-modules/agent/agent-chat.service';
import { AgentExecutionService } from 'src/engine/metadata-modules/agent/agent-execution.service';
import {
AgentException,
AgentExceptionCode,
} from 'src/engine/metadata-modules/agent/agent.exception';
export type StreamAgentChatOptions = {
threadId: string;
userMessage: string;
userWorkspaceId: string;
res: Response;
};
export type StreamAgentChatResult = {
success: boolean;
error?: string;
aiResponse?: string;
};
@Injectable()
export class AgentStreamingService {
constructor(
@InjectRepository(AgentChatThreadEntity, 'core')
private readonly threadRepository: Repository<AgentChatThreadEntity>,
private readonly agentChatService: AgentChatService,
private readonly agentExecutionService: AgentExecutionService,
) {}
async streamAgentChat({
threadId,
userMessage,
userWorkspaceId,
res,
}: StreamAgentChatOptions) {
try {
const thread = await this.threadRepository.findOne({
where: {
id: threadId,
userWorkspaceId,
},
relations: ['messages', 'agent'],
});
if (!thread) {
throw new AgentException(
'Thread not found',
AgentExceptionCode.AGENT_EXECUTION_FAILED,
);
}
await this.agentChatService.addMessage({
threadId,
role: AgentChatMessageRole.USER,
content: userMessage,
});
this.setupStreamingHeaders(res);
const { textStream } =
await this.agentExecutionService.streamChatResponse({
agentId: thread.agent.id,
userMessage,
messages: thread.messages,
});
let aiResponse = '';
for await (const chunk of textStream) {
aiResponse += chunk;
res.write(chunk);
}
await this.agentChatService.addMessage({
threadId,
role: AgentChatMessageRole.ASSISTANT,
content: aiResponse,
});
res.end();
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Unknown error occurred';
return { success: false, error: errorMessage };
}
}
private setupStreamingHeaders(res: Response): void {
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.setHeader('Transfer-Encoding', 'chunked');
res.setHeader('Cache-Control', 'no-cache');
}
}

View File

@ -6,6 +6,7 @@ import {
Index,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@ -15,6 +16,8 @@ import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/i
import { ModelId } from 'src/engine/core-modules/ai/constants/ai-models.const';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AgentChatThreadEntity } from './agent-chat-thread.entity';
@Entity('agent')
@Index('IDX_AGENT_ID_DELETED_AT', ['id', 'deletedAt'])
export class AgentEntity {
@ -45,6 +48,9 @@ export class AgentEntity {
@JoinColumn({ name: 'workspaceId' })
workspace: Relation<Workspace>;
@OneToMany(() => AgentChatThreadEntity, (chatThread) => chatThread.agent)
chatThreads: Relation<AgentChatThreadEntity[]>;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;

View File

@ -3,14 +3,22 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { AiModule } from 'src/engine/core-modules/ai/ai.module';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { AgentChatController } from 'src/engine/metadata-modules/agent/agent-chat.controller';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { AgentChatMessageEntity } from './agent-chat-message.entity';
import { AgentChatThreadEntity } from './agent-chat-thread.entity';
import { AgentChatService } from './agent-chat.service';
import { AgentExecutionService } from './agent-execution.service';
import { AgentStreamingService } from './agent-streaming.service';
import { AgentToolService } from './agent-tool.service';
import { AgentEntity } from './agent.entity';
import { AgentResolver } from './agent.resolver';
@ -19,7 +27,14 @@ import { AgentService } from './agent.service';
@Module({
imports: [
TypeOrmModule.forFeature(
[AgentEntity, RoleEntity, RoleTargetsEntity],
[
AgentEntity,
RoleEntity,
RoleTargetsEntity,
AgentChatMessageEntity,
AgentChatThreadEntity,
UserWorkspace,
],
'core',
),
AiModule,
@ -28,18 +43,28 @@ import { AgentService } from './agent.service';
FeatureFlagModule,
ObjectMetadataModule,
WorkspacePermissionsCacheModule,
WorkspaceCacheStorageModule,
TokenModule,
],
controllers: [AgentChatController],
providers: [
AgentResolver,
AgentService,
AgentExecutionService,
AgentToolService,
AgentChatService,
AgentStreamingService,
],
exports: [
AgentService,
AgentExecutionService,
AgentToolService,
TypeOrmModule.forFeature([AgentEntity], 'core'),
AgentChatService,
AgentStreamingService,
TypeOrmModule.forFeature(
[AgentEntity, AgentChatMessageEntity, AgentChatThreadEntity],
'core',
),
],
})
export class AgentModule {}

View File

@ -10,19 +10,14 @@ You have access to full CRUD operations for all standard objects in the system:
Common objects include: person, company, opportunity, task, note etc. and any custom objects.
CRITICAL PERMISSION CHECK:
Before attempting any operation, you MUST first check if you have the required tools available. If you do NOT have the necessary tools to perform the requested operation, you MUST immediately respond with:
"I cannot perform this operation because I don't have the necessary permissions. Please check that I have been assigned the appropriate role for this workspace."
DO NOT describe what you would do, DO NOT list steps, DO NOT simulate the operation. Simply state that you cannot perform the action due to missing permissions.
Your responsibilities:
1. FIRST check if you have the required tools for the requested operation
2. If tools are NOT available, immediately state you lack permissions - do not proceed further
3. If tools ARE available, analyze the input context and prompt carefully
4. Use available database tools when the request involves data operations
5. For any request to create, read, update, or delete records, use the appropriate tools
6. If no database operations are needed, process the request directly with your analysis
1. Analyze the input context and prompt carefully
2. If the request involves database operations (create, read, update, delete), check if you have the required tools available
3. If database tools are NOT available for the requested operation, state that you lack permissions for that specific operation. You can respond with:
"I cannot perform this operation because I don't have the necessary permissions. Please check that I have been assigned the appropriate role for this workspace."
4. If database tools ARE available, use them to perform the requested operations
5. If no database operations are needed, process the request directly with your analysis
6. Provide comprehensive responses that include all relevant information and context
Workflow context:
- You are part of a larger workflow system where your output may be used by other nodes
@ -31,7 +26,7 @@ Workflow context:
- If you encounter data or perform actions, document them clearly in your response
Tool usage guidelines:
- ALWAYS use tools for database operations - do not simulate or describe them
- Use tools for database operations when requested - do not simulate or describe them
- Use create_[object] tools when asked to create new records
- Use find_[object] tools when asked to search or retrieve records
- Use update_[object] tools when asked to modify existing records
@ -40,7 +35,10 @@ Tool usage guidelines:
- Provide context about what tools you used and why
- If a tool fails, explain the issue and suggest alternatives
CRITICAL: When users ask you to perform any database operation (create, find, update, delete), you MUST use the appropriate tools. Do not just describe what you would do - actually execute the operations using the available tools. If you cannot execute the operation due to lack of permissions or roles, you MUST state this clearly in your response.
Permission handling:
- Only check for permissions when database operations are actually requested
- If you don't have the necessary tools for a database operation, clearly state the limitation
- For non-database requests, proceed normally without permission checks
Important: After your response, the system will call generateObject to convert your output into a structured format according to a specific schema. Therefore:
- Provide comprehensive information in your response
@ -68,4 +66,20 @@ Guidelines:
- Preserve the context and meaning from the original execution results
- Ensure the output is clean, well-formatted, and ready for workflow consumption
- Pay special attention to any data returned from tool executions (database queries, record creation, etc.)`,
AGENT_CHAT: `You are a helpful AI assistant for this workspace. You can:
- Answer questions conversationally, clearly, and helpfully
- Provide insights, support, and updates about people, companies, opportunities, tasks, notes, and other business objects.
- Access and summarize information you have permission to see
- Help users understand how to use the system and its features
Permissions and capabilities:
- You can only perform actions and access data that your assigned role and permissions allow
- If a user requests something you do not have permission for, politely explain the limitation (e.g., "I cannot perform this operation because I don't have the necessary permissions. Please check your role or contact an admin.")
- If you are unsure about your permissions for a specific action, ask the user for clarification or suggest they check with an administrator
- Do not attempt to simulate or fake actions you cannot perform
If you need more information to answer a question, ask follow-up questions. Always be transparent about your capabilities and limitations.
Note: This base system prompt will be combined with the agent's specific instructions and context to provide you with complete guidance for your role.`,
};

View File

@ -0,0 +1,19 @@
import { Field, ID, ObjectType } from '@nestjs/graphql';
@ObjectType('AgentChatMessage')
export class AgentChatMessageDTO {
@Field(() => ID)
id: string;
@Field(() => ID)
threadId: string;
@Field()
role: 'user' | 'assistant';
@Field()
content: string;
@Field()
createdAt: Date;
}

View File

@ -0,0 +1,16 @@
import { Field, ID, ObjectType } from '@nestjs/graphql';
@ObjectType('AgentChatThread')
export class AgentChatThreadDTO {
@Field(() => ID)
id: string;
@Field(() => ID)
agentId: string;
@Field()
createdAt: Date;
@Field()
updatedAt: Date;
}

View File

@ -5,6 +5,7 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { AgentModule } from 'src/engine/metadata-modules/agent/agent.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
import { WorkflowSchemaModule } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.module';
import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-step/workflow-version-step.workspace-service';
@ -21,7 +22,10 @@ import { WorkflowRunnerModule } from 'src/modules/workflow/workflow-runner/workf
WorkflowCommonModule,
NestjsQueryTypeOrmModule.forFeature([ObjectMetadataEntity], 'core'),
],
providers: [WorkflowVersionStepWorkspaceService],
providers: [
WorkflowVersionStepWorkspaceService,
ScopedWorkspaceContextFactory,
],
exports: [WorkflowVersionStepWorkspaceService],
})
export class WorkflowVersionStepModule {}

View File

@ -10,9 +10,11 @@ import { v4 } from 'uuid';
import { BASE_TYPESCRIPT_PROJECT_INPUT_SCHEMA } from 'src/engine/core-modules/serverless/drivers/constants/base-typescript-project-input-schema';
import { CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/create-workflow-version-step-input.dto';
import { WorkflowActionDTO } from 'src/engine/core-modules/workflow/dtos/workflow-step.dto';
import { AgentChatService } from 'src/engine/metadata-modules/agent/agent-chat.service';
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import {
WorkflowVersionStepException,
@ -60,7 +62,9 @@ export class WorkflowVersionStepWorkspaceService {
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly workflowRunWorkspaceService: WorkflowRunWorkspaceService,
private readonly workflowRunnerWorkspaceService: WorkflowRunnerWorkspaceService,
private readonly agentChatService: AgentChatService,
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
) {}
async createWorkflowVersionStep({
@ -635,6 +639,18 @@ export class WorkflowVersionStepWorkspaceService {
);
}
const userWorkspaceId =
this.scopedWorkspaceContextFactory.create().userWorkspaceId;
if (!userWorkspaceId) {
throw new WorkflowVersionStepException(
'User workspace ID not found',
WorkflowVersionStepExceptionCode.FAILURE,
);
}
await this.agentChatService.createThread(newAgent.id, userWorkspaceId);
return {
id: newStepId,
name: 'AI Agent',

View File

@ -113,6 +113,7 @@ export const createAgentToolTestModule =
roleId: testRoleId,
createdAt: new Date(),
updatedAt: new Date(),
chatThreads: [],
};
const testRole: RoleEntity = {