Show tool execution messages in AI agent chat (#13117)
https://github.com/user-attachments/assets/c0a42726-50ac-496e-a993-9d6076a84a6a --------- Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
@ -62,11 +62,33 @@ export class AgentChatController {
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
await this.agentStreamingService.streamAgentChat({
|
||||
threadId: body.threadId,
|
||||
userMessage: body.userMessage,
|
||||
userWorkspaceId,
|
||||
res,
|
||||
});
|
||||
try {
|
||||
await this.agentStreamingService.streamAgentChat({
|
||||
threadId: body.threadId,
|
||||
userMessage: body.userMessage,
|
||||
userWorkspaceId,
|
||||
res,
|
||||
});
|
||||
} catch (error) {
|
||||
// Handle errors at controller level for streaming responses
|
||||
// since the RestApiExceptionFilter interferes with our streaming error handling
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Unknown error occurred';
|
||||
|
||||
if (!res.headersSent) {
|
||||
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||
res.setHeader('Transfer-Encoding', 'chunked');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
}
|
||||
|
||||
res.write(
|
||||
JSON.stringify({
|
||||
type: 'error',
|
||||
message: errorMessage,
|
||||
}) + '\n',
|
||||
);
|
||||
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,8 +13,6 @@ import {
|
||||
AgentExceptionCode,
|
||||
} from 'src/engine/metadata-modules/agent/agent.exception';
|
||||
|
||||
import { AgentExecutionService } from './agent-execution.service';
|
||||
|
||||
@Injectable()
|
||||
export class AgentChatService {
|
||||
constructor(
|
||||
@ -22,7 +20,6 @@ export class AgentChatService {
|
||||
private readonly threadRepository: Repository<AgentChatThreadEntity>,
|
||||
@InjectRepository(AgentChatMessageEntity, 'core')
|
||||
private readonly messageRepository: Repository<AgentChatMessageEntity>,
|
||||
private readonly agentExecutionService: AgentExecutionService,
|
||||
) {}
|
||||
|
||||
async createThread(agentId: string, userWorkspaceId: string) {
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
ModelId,
|
||||
ModelProvider,
|
||||
} 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 { getEffectiveModelConfig } from 'src/engine/core-modules/ai/utils/get-effective-model-config.util';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import {
|
||||
AgentChatMessageEntity,
|
||||
@ -22,6 +22,7 @@ import { AGENT_SYSTEM_PROMPTS } from 'src/engine/metadata-modules/agent/constant
|
||||
import { convertOutputSchemaToZod } from 'src/engine/metadata-modules/agent/utils/convert-output-schema-to-zod';
|
||||
import { OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
|
||||
import { resolveInput } from 'src/modules/workflow/workflow-executor/utils/variable-resolver.util';
|
||||
import { getAIModelById } from 'src/engine/core-modules/ai/utils/get-ai-model-by-id.util';
|
||||
|
||||
import { AgentEntity } from './agent.entity';
|
||||
import { AgentException, AgentExceptionCode } from './agent.exception';
|
||||
@ -45,12 +46,17 @@ export class AgentExecutionService {
|
||||
private readonly agentToolService: AgentToolService,
|
||||
@InjectRepository(AgentEntity, 'core')
|
||||
private readonly agentRepository: Repository<AgentEntity>,
|
||||
@InjectRepository(AgentChatMessageEntity, 'core')
|
||||
private readonly agentChatmessageRepository: Repository<AgentChatMessageEntity>,
|
||||
) {}
|
||||
|
||||
getModel = (modelId: ModelId, provider: ModelProvider) => {
|
||||
switch (provider) {
|
||||
case ModelProvider.NONE: {
|
||||
const OpenAIProvider = createOpenAI({
|
||||
apiKey: this.twentyConfigService.get('OPENAI_API_KEY'),
|
||||
});
|
||||
|
||||
return OpenAIProvider(getEffectiveModelConfig(modelId).modelId);
|
||||
}
|
||||
case ModelProvider.OPENAI: {
|
||||
const OpenAIProvider = createOpenAI({
|
||||
apiKey: this.twentyConfigService.get('OPENAI_API_KEY'),
|
||||
@ -77,6 +83,9 @@ export class AgentExecutionService {
|
||||
let apiKey: string | undefined;
|
||||
|
||||
switch (provider) {
|
||||
case ModelProvider.NONE:
|
||||
apiKey = this.twentyConfigService.get('OPENAI_API_KEY');
|
||||
break;
|
||||
case ModelProvider.OPENAI:
|
||||
apiKey = this.twentyConfigService.get('OPENAI_API_KEY');
|
||||
break;
|
||||
@ -91,7 +100,7 @@ export class AgentExecutionService {
|
||||
}
|
||||
if (!apiKey) {
|
||||
throw new AgentException(
|
||||
`${provider.toUpperCase()} API key not configured`,
|
||||
`${provider === ModelProvider.NONE ? 'OPENAI' : provider.toUpperCase()} API key not configured`,
|
||||
AgentExceptionCode.API_KEY_NOT_CONFIGURED,
|
||||
);
|
||||
}
|
||||
|
||||
@ -65,7 +65,7 @@ export class AgentStreamingService {
|
||||
|
||||
this.setupStreamingHeaders(res);
|
||||
|
||||
const { textStream } =
|
||||
const { fullStream } =
|
||||
await this.agentExecutionService.streamChatResponse({
|
||||
agentId: thread.agent.id,
|
||||
userMessage,
|
||||
@ -74,9 +74,24 @@ export class AgentStreamingService {
|
||||
|
||||
let aiResponse = '';
|
||||
|
||||
for await (const chunk of textStream) {
|
||||
aiResponse += chunk;
|
||||
res.write(chunk);
|
||||
for await (const chunk of fullStream) {
|
||||
switch (chunk.type) {
|
||||
case 'text-delta':
|
||||
aiResponse += chunk.textDelta;
|
||||
this.sendStreamEvent(res, {
|
||||
type: chunk.type,
|
||||
message: chunk.textDelta,
|
||||
});
|
||||
break;
|
||||
case 'tool-call':
|
||||
this.sendStreamEvent(res, {
|
||||
type: chunk.type,
|
||||
message: chunk.args?.toolDescription,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await this.agentChatService.addMessage({
|
||||
@ -90,10 +105,26 @@ export class AgentStreamingService {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Unknown error occurred';
|
||||
|
||||
return { success: false, error: errorMessage };
|
||||
if (!res.headersSent) {
|
||||
this.setupStreamingHeaders(res);
|
||||
}
|
||||
|
||||
this.sendStreamEvent(res, {
|
||||
type: 'error',
|
||||
message: errorMessage,
|
||||
});
|
||||
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
|
||||
private sendStreamEvent(
|
||||
res: Response,
|
||||
event: { type: string; message: string },
|
||||
): void {
|
||||
res.write(JSON.stringify(event) + '\n');
|
||||
}
|
||||
|
||||
private setupStreamingHeaders(res: Response): void {
|
||||
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||
res.setHeader('Transfer-Encoding', 'chunked');
|
||||
|
||||
@ -4,9 +4,9 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { ToolSet } from 'ai';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
|
||||
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
|
||||
|
||||
@Injectable()
|
||||
export class AgentToolService {
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
Unique,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@ -20,6 +21,7 @@ import { AgentChatThreadEntity } from './agent-chat-thread.entity';
|
||||
|
||||
@Entity('agent')
|
||||
@Index('IDX_AGENT_ID_DELETED_AT', ['id', 'deletedAt'])
|
||||
@Unique('IDX_AGENT_NAME_WORKSPACE_ID_UNIQUE', ['name', 'workspaceId'])
|
||||
export class AgentEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
@ -27,13 +29,19 @@ export class AgentEntity {
|
||||
@Column({ nullable: false })
|
||||
name: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
label: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
icon: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
description: string;
|
||||
|
||||
@Column({ nullable: false, type: 'text' })
|
||||
prompt: string;
|
||||
|
||||
@Column({ nullable: false, type: 'varchar' })
|
||||
@Column({ nullable: false, type: 'varchar', default: 'auto' })
|
||||
modelId: ModelId;
|
||||
|
||||
@Column({ nullable: true, type: 'jsonb' })
|
||||
@ -42,6 +50,9 @@ export class AgentEntity {
|
||||
@Column({ nullable: false, type: 'uuid' })
|
||||
workspaceId: string;
|
||||
|
||||
@Column({ default: false })
|
||||
isCustom: boolean;
|
||||
|
||||
@ManyToOne(() => Workspace, (workspace) => workspace.agents, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
|
||||
@ -11,4 +11,5 @@ export enum AgentExceptionCode {
|
||||
AGENT_NOT_FOUND = 'AGENT_NOT_FOUND',
|
||||
AGENT_EXECUTION_FAILED = 'AGENT_EXECUTION_FAILED',
|
||||
API_KEY_NOT_CONFIGURED = 'API_KEY_NOT_CONFIGURED',
|
||||
USER_WORKSPACE_ID_NOT_FOUND = 'USER_WORKSPACE_ID_NOT_FOUND',
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ModelId } from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||
import { AgentChatService } from 'src/engine/metadata-modules/agent/agent-chat.service';
|
||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
|
||||
import { AgentEntity } from './agent.entity';
|
||||
@ -16,6 +17,7 @@ export class AgentService {
|
||||
private readonly agentRepository: Repository<AgentEntity>,
|
||||
@InjectRepository(RoleTargetsEntity, 'core')
|
||||
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
|
||||
private readonly agentChatService: AgentChatService,
|
||||
) {}
|
||||
|
||||
async findManyAgents(workspaceId: string) {
|
||||
@ -51,9 +53,35 @@ export class AgentService {
|
||||
};
|
||||
}
|
||||
|
||||
async createOneAgentAndFirstThread(
|
||||
input: {
|
||||
name: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
prompt: string;
|
||||
modelId: ModelId;
|
||||
},
|
||||
workspaceId: string,
|
||||
userWorkspaceId: string | null,
|
||||
) {
|
||||
const agent = await this.createOneAgent(input, workspaceId);
|
||||
|
||||
if (!userWorkspaceId) {
|
||||
throw new AgentException(
|
||||
'User workspace ID not found',
|
||||
AgentExceptionCode.USER_WORKSPACE_ID_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
await this.agentChatService.createThread(agent.id, userWorkspaceId);
|
||||
|
||||
return agent;
|
||||
}
|
||||
|
||||
async createOneAgent(
|
||||
input: {
|
||||
name: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
prompt: string;
|
||||
modelId: ModelId;
|
||||
|
||||
@ -10,14 +10,34 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
|
||||
import { convertObjectMetadataToSchemaProperties } from 'src/engine/utils/convert-object-metadata-to-schema-properties.util';
|
||||
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
||||
|
||||
export const getRecordInputSchema = (objectMetadata: ObjectMetadataEntity) => {
|
||||
const createToolSchema = (
|
||||
inputProperties: Record<string, JSONSchema7Definition>,
|
||||
required?: string[],
|
||||
) => {
|
||||
return jsonSchema({
|
||||
type: 'object',
|
||||
properties: convertObjectMetadataToSchemaProperties({
|
||||
properties: {
|
||||
toolDescription: {
|
||||
type: 'string',
|
||||
description:
|
||||
'A clear, human-readable description of the action being performed. Explain what operation you are executing and with what parameters in natural language.',
|
||||
},
|
||||
input: {
|
||||
type: 'object',
|
||||
properties: inputProperties,
|
||||
...(required && { required }),
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getRecordInputSchema = (objectMetadata: ObjectMetadataEntity) => {
|
||||
return createToolSchema(
|
||||
convertObjectMetadataToSchemaProperties({
|
||||
item: objectMetadata,
|
||||
forResponse: false,
|
||||
}),
|
||||
});
|
||||
);
|
||||
};
|
||||
|
||||
export const generateFindToolSchema = (
|
||||
@ -48,10 +68,7 @@ export const generateFindToolSchema = (
|
||||
}
|
||||
});
|
||||
|
||||
return jsonSchema({
|
||||
type: 'object',
|
||||
properties: schemaProperties,
|
||||
});
|
||||
return createToolSchema(schemaProperties);
|
||||
};
|
||||
|
||||
const generateFieldFilterJsonSchema = (
|
||||
@ -808,25 +825,22 @@ const generateFieldFilterJsonSchema = (
|
||||
};
|
||||
|
||||
export const generateBulkDeleteToolSchema = () => {
|
||||
return jsonSchema({
|
||||
type: 'object',
|
||||
properties: {
|
||||
filter: {
|
||||
type: 'object',
|
||||
description: 'Filter criteria to select records for bulk delete',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'object',
|
||||
description: 'Filter to select records to delete',
|
||||
properties: {
|
||||
in: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
},
|
||||
description: 'Array of record IDs to delete',
|
||||
return createToolSchema({
|
||||
filter: {
|
||||
type: 'object',
|
||||
description: 'Filter criteria to select records for bulk delete',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'object',
|
||||
description: 'Filter to select records to delete',
|
||||
properties: {
|
||||
in: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
},
|
||||
description: 'Array of record IDs to delete',
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -834,3 +848,29 @@ export const generateBulkDeleteToolSchema = () => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const generateFindOneToolSchema = () => {
|
||||
return createToolSchema(
|
||||
{
|
||||
id: {
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
description: 'The unique UUID of the record to retrieve',
|
||||
},
|
||||
},
|
||||
['id'],
|
||||
);
|
||||
};
|
||||
|
||||
export const generateSoftDeleteToolSchema = () => {
|
||||
return createToolSchema(
|
||||
{
|
||||
id: {
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
description: 'The unique UUID of the record to soft delete',
|
||||
},
|
||||
},
|
||||
['id'],
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user