feat: Add Agent Evaluation System and Refactor AI Modules (#16111)

## Summary

This PR introduces a comprehensive agent evaluation system and refactors
the AI module structure for better organization.

## Key Changes

### 🎯 Agent Evaluation System
- Added **Agent Turn Evaluation** entities, DTOs, and database schema
- New GraphQL mutations: `evaluateAgentTurn` and `runEvaluationInput`
- Added `evaluationInputs` field to Agent entity for storing test inputs
- New `AgentTurnGraderService` for automatic turn evaluation
- Added evaluation UI with new **Evals** and **Logs** tabs in agent
detail pages

### 🏗️ Entity & Module Refactoring
- Renamed `AgentChatMessage` → `AgentMessage` for clarity
- Consolidated chat entities: `AgentMessage`, `AgentTurn`, and
`AgentChatThread`
- Reorganized AI modules under `ai/` subdirectory structure
- Updated imports across codebase to reflect new module paths

### 🤖 New Agents & Roles
- Added **Dashboard Builder Agent** for dashboard creation and
management
- Added **Dashboard Manager Role** with appropriate permissions
- Updated role permissions to be more granular (users vs agents vs API
keys)

### 🔐 Permission System Updates
- Added `HTTP_REQUEST_TOOL` permission flag
- Updated Workflow Manager role permissions (restricted tool access)
- Enhanced permission flag types to differentiate between user/agent/API
key contexts
- Added `isRelevantForAgents`, `isRelevantForApiKeys`,
`isRelevantForUsers` to permission flags

### 📨 Message Role Enhancement
- Added `system` role to `AgentMessageRole` enum (alongside
user/assistant)
- Updated message handling to support system prompts

### 🎨 UI/UX Improvements
- New tabs in agent detail: **Evals** and **Logs**
- Added turn detail page: `/ai/agents/:agentId/turns/:turnId`
- Fixed text overflow in `SettingsListItemCardContent`
- Updated role applicability labels ("Assignable to Workspace Members")

### 🛠️ Technical Improvements
- Fixed Zod schema validation for UUID and Date fields (use string
validators)
- Updated `ToolRegistryService` to properly register HTTP tool with
permission flag
- Enhanced error handling in agent execution services
- Updated database migrations for new entity schema

## Database Migrations
- `1764210000000-add-system-role-to-agent-message.ts`
- `1764220000000-add-evaluation-inputs-to-agent.ts`
- `1764200000000-add-agent-turn-evaluation.ts`
- `1764100000000-refactor-agent-chat-entities.ts`

## Testing
- [ ] Agent evaluation flow tested
- [ ] Dashboard Builder agent tested
- [ ] Permission system validated
- [ ] UI tabs and navigation tested
- [ ] Database migrations run successfully

## Breaking Changes
⚠️ **Entity Rename**: `AgentChatMessage` renamed to `AgentMessage` -
GraphQL queries need updating

## Related Issues
<!-- Link any related issues here -->

## Screenshots
<!-- Add screenshots if applicable -->
This commit is contained in:
Félix Malfait
2025-11-27 08:25:40 +01:00
committed by GitHub
parent 35f81805b8
commit 4f20fd35c5
158 changed files with 2954 additions and 556 deletions
@@ -1,23 +0,0 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { AgentChatMessagePartDTO } from './agent-chat-message-part.dto';
@ObjectType('AgentChatMessage')
export class AgentChatMessageDTO {
@Field(() => UUIDScalarType)
id: string;
@Field(() => UUIDScalarType)
threadId: string;
@Field()
role: 'user' | 'assistant';
@Field(() => [AgentChatMessagePartDTO])
parts: AgentChatMessagePartDTO[];
@Field()
createdAt: Date;
}
@@ -1,45 +0,0 @@
import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
Relation,
} from 'typeorm';
import { AgentChatThreadEntity } from 'src/engine/metadata-modules/ai-chat/entities/agent-chat-thread.entity';
import { AgentChatMessagePartEntity } from './agent-chat-message-part.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;
@OneToMany(() => AgentChatMessagePartEntity, (part) => part.message)
parts: Relation<AgentChatMessagePartEntity[]>;
@CreateDateColumn()
createdAt: Date;
}
@@ -1,29 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AiModelsModule } from 'src/engine/metadata-modules/ai-models/ai-models.module';
import { AiToolsModule } from 'src/engine/metadata-modules/ai-tools/ai-tools.module';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AgentEntity } from 'src/engine/metadata-modules/ai-agent/entities/agent.entity';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { AiRouterService } from './ai-router.service';
import { AiRouterPlanGeneratorService } from './services/ai-router-plan-generator.service';
import { AiRouterStrategyDeciderService } from './services/ai-router-strategy-decider.service';
@Module({
imports: [
TypeOrmModule.forFeature([AgentEntity, WorkspaceEntity]),
AiModelsModule,
AiToolsModule,
ObjectMetadataModule,
],
providers: [
AiRouterService,
AiRouterStrategyDeciderService,
AiRouterPlanGeneratorService,
],
exports: [AiRouterService],
})
export class AiRouterModule {}
@@ -0,0 +1,36 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AiBillingModule } from 'src/engine/metadata-modules/ai/ai-billing/ai-billing.module';
import { AiModelsModule } from 'src/engine/metadata-modules/ai/ai-models/ai-models.module';
import { AiToolsModule } from 'src/engine/metadata-modules/ai/ai-tools/ai-tools.module';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { AgentMessagePartEntity } from './entities/agent-message-part.entity';
import { AgentMessageEntity } from './entities/agent-message.entity';
import { AgentTurnEntity } from './entities/agent-turn.entity';
import { AgentAsyncExecutorService } from './services/agent-async-executor.service';
@Module({
imports: [
AiBillingModule,
AiModelsModule,
AiToolsModule,
TypeOrmModule.forFeature([
AgentMessageEntity,
AgentMessagePartEntity,
AgentTurnEntity,
RoleTargetsEntity,
]),
],
providers: [AgentAsyncExecutorService],
exports: [
AgentAsyncExecutorService,
TypeOrmModule.forFeature([
AgentMessageEntity,
AgentMessagePartEntity,
AgentTurnEntity,
]),
],
})
export class AiAgentExecutionModule {}
@@ -1,12 +1,12 @@
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { JSONValue } from 'ai';
import { IsDateString } from 'class-validator';
import GraphQLJSON from 'graphql-type-json';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@ObjectType('AgentChatMessagePart')
export class AgentChatMessagePartDTO {
@ObjectType('AgentMessagePart')
export class AgentMessagePartDTO {
@Field(() => UUIDScalarType)
id: string;
@@ -77,8 +77,9 @@ export class AgentChatMessagePartDTO {
fileUrl?: string;
@Field(() => GraphQLJSON, { nullable: true })
providerMetadata?: Record<string, Record<string, JSONValue>>;
providerMetadata?: Record<string, unknown>;
@IsDateString()
@Field()
createdAt: Date;
}
@@ -0,0 +1,31 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { IsDateString } from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { AgentMessagePartDTO } from 'src/engine/metadata-modules/ai/ai-agent-execution/dtos/agent-message-part.dto';
@ObjectType('AgentMessage')
export class AgentMessageDTO {
@Field(() => UUIDScalarType)
id: string;
@Field(() => UUIDScalarType)
threadId: string;
@Field(() => UUIDScalarType)
turnId: string;
@Field(() => UUIDScalarType, { nullable: true })
agentId: string | null;
@Field()
role: 'system' | 'user' | 'assistant';
@Field(() => [AgentMessagePartDTO])
parts: AgentMessagePartDTO[];
@IsDateString()
@Field()
createdAt: Date;
}
@@ -0,0 +1,29 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { IsDateString } from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { AgentMessageDTO } from 'src/engine/metadata-modules/ai/ai-agent-execution/dtos/agent-message.dto';
import { AgentTurnEvaluationDTO } from 'src/engine/metadata-modules/ai/ai-agent-monitor/dtos/agent-turn-evaluation.dto';
@ObjectType('AgentTurn')
export class AgentTurnDTO {
@Field(() => UUIDScalarType)
id: string;
@Field(() => UUIDScalarType)
threadId: string;
@Field(() => UUIDScalarType, { nullable: true })
agentId: string | null;
@Field(() => [AgentTurnEvaluationDTO])
evaluations: AgentTurnEvaluationDTO[];
@Field(() => [AgentMessageDTO])
messages: AgentMessageDTO[];
@IsDateString()
@Field()
createdAt: Date;
}
@@ -10,10 +10,10 @@ import {
Relation,
} from 'typeorm';
import { AgentChatMessageEntity } from './agent-chat-message.entity';
import { AgentMessageEntity } from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message.entity';
@Entity('agentChatMessagePart')
export class AgentChatMessagePartEntity {
@Entity('agentMessagePart')
export class AgentMessagePartEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@@ -21,11 +21,11 @@ export class AgentChatMessagePartEntity {
@Index()
messageId: string;
@ManyToOne(() => AgentChatMessageEntity, (message) => message.parts, {
@ManyToOne(() => AgentMessageEntity, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'messageId' })
message: Relation<AgentChatMessageEntity>;
message: Relation<AgentMessageEntity>;
@Column({ type: 'int' })
orderIndex: number;
@@ -0,0 +1,60 @@
import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
Relation,
} from 'typeorm';
import { AgentMessagePartEntity } from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message-part.entity';
import { AgentTurnEntity } from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-turn.entity';
import { AgentChatThreadEntity } from 'src/engine/metadata-modules/ai/ai-chat/entities/agent-chat-thread.entity';
export enum AgentMessageRole {
SYSTEM = 'system',
USER = 'user',
ASSISTANT = 'assistant',
}
@Entity('agentMessage')
export class AgentMessageEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column('uuid')
@Index()
threadId: string;
@ManyToOne(() => AgentChatThreadEntity, (thread) => thread.messages, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'threadId' })
thread: Relation<AgentChatThreadEntity>;
@Column('uuid')
@Index()
turnId: string;
@ManyToOne(() => AgentTurnEntity, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'turnId' })
turn: Relation<AgentTurnEntity>;
@Column({ type: 'uuid', nullable: true })
@Index()
agentId: string | null;
@Column({ type: 'enum', enum: AgentMessageRole })
role: AgentMessageRole;
@OneToMany(() => AgentMessagePartEntity, (part) => part.message)
parts: Relation<AgentMessagePartEntity[]>;
@CreateDateColumn()
createdAt: Date;
}
@@ -0,0 +1,44 @@
import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
Relation,
} from 'typeorm';
import { AgentMessageEntity } from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message.entity';
import { AgentTurnEvaluationEntity } from 'src/engine/metadata-modules/ai/ai-agent-monitor/entities/agent-turn-evaluation.entity';
import { AgentChatThreadEntity } from 'src/engine/metadata-modules/ai/ai-chat/entities/agent-chat-thread.entity';
@Entity('agentTurn')
export class AgentTurnEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column('uuid')
@Index()
threadId: string;
@ManyToOne(() => AgentChatThreadEntity, (thread) => thread.turns, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'threadId' })
thread: Relation<AgentChatThreadEntity>;
@Column({ type: 'uuid', nullable: true })
@Index()
agentId: string | null;
@OneToMany(() => AgentMessageEntity, (message) => message.turn)
messages: Relation<AgentMessageEntity[]>;
@OneToMany(() => AgentTurnEvaluationEntity, (evaluation) => evaluation.turn)
evaluations: Relation<AgentTurnEvaluationEntity[]>;
@CreateDateColumn()
createdAt: Date;
}
@@ -0,0 +1,174 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import {
generateObject,
generateText,
jsonSchema,
stepCountIs,
ToolSet,
} from 'ai';
import { type ActorMetadata } from 'twenty-shared/types';
import { Repository } from 'typeorm';
import {
AgentException,
AgentExceptionCode,
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
import { AGENT_CONFIG } from 'src/engine/metadata-modules/ai/ai-agent/constants/agent-config.const';
import { AGENT_SYSTEM_PROMPTS } from 'src/engine/metadata-modules/ai/ai-agent/constants/agent-system-prompts.const';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { AgentExecutionResult } from 'src/engine/metadata-modules/ai/ai-agent/services/agent-execution.service';
import { AI_TELEMETRY_CONFIG } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-telemetry.const';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
import { ToolAdapterService } from 'src/engine/metadata-modules/ai/ai-tools/services/tool-adapter.service';
import { ToolService } from 'src/engine/metadata-modules/ai/ai-tools/services/tool.service';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
@Injectable()
export class AgentAsyncExecutorService {
private readonly logger = new Logger(AgentAsyncExecutorService.name);
constructor(
private readonly aiModelRegistryService: AiModelRegistryService,
private readonly toolAdapterService: ToolAdapterService,
@InjectRepository(RoleTargetsEntity)
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
private readonly toolService: ToolService,
) {}
private async getTools(
agentId: string,
workspaceId: string,
actorContext?: ActorMetadata,
rolePermissionConfig?: RolePermissionConfig,
): Promise<ToolSet> {
const roleTarget = await this.roleTargetsRepository.findOne({
where: {
agentId: agentId,
workspaceId,
},
select: ['roleId'],
});
const agentRoleId = roleTarget?.roleId;
if (!rolePermissionConfig && !agentRoleId) {
return await this.toolAdapterService.getTools();
}
let effectiveRoleContext: RolePermissionConfig;
if (
rolePermissionConfig &&
('intersectionOf' in rolePermissionConfig ||
'unionOf' in rolePermissionConfig)
) {
effectiveRoleContext = rolePermissionConfig;
} else if (agentRoleId) {
effectiveRoleContext = { unionOf: [agentRoleId] };
} else {
return await this.toolAdapterService.getTools();
}
const actionTools = await this.toolAdapterService.getTools(
effectiveRoleContext,
workspaceId,
);
const databaseTools = await this.toolService.listTools(
effectiveRoleContext,
workspaceId,
actorContext,
);
return {
...databaseTools,
...actionTools,
};
}
async executeAgent({
agent,
userPrompt,
actorContext,
rolePermissionConfig,
}: {
agent: AgentEntity | null;
userPrompt: string;
actorContext?: ActorMetadata;
rolePermissionConfig?: RolePermissionConfig;
}): Promise<AgentExecutionResult> {
try {
const registeredModel =
await this.aiModelRegistryService.resolveModelForAgent(agent);
const tools = agent
? await this.getTools(
agent.id,
agent.workspaceId,
actorContext,
rolePermissionConfig,
)
: {};
this.logger.log(`Generated ${Object.keys(tools).length} tools for agent`);
const textResponse = await generateText({
system: `${AGENT_SYSTEM_PROMPTS.BASE}\n${AGENT_SYSTEM_PROMPTS.WORKFLOW_ADDITIONS}\n\n${agent ? agent.prompt : ''}`,
tools,
model: registeredModel.model,
prompt: userPrompt,
stopWhen: stepCountIs(AGENT_CONFIG.MAX_STEPS),
experimental_telemetry: AI_TELEMETRY_CONFIG,
});
const agentSchema =
agent?.responseFormat?.type === 'json'
? agent.responseFormat.schema
: undefined;
if (!agentSchema) {
return {
result: { response: textResponse.text },
usage: textResponse.usage,
};
}
const output = await generateObject({
system: AGENT_SYSTEM_PROMPTS.OUTPUT_GENERATOR,
model: registeredModel.model,
prompt: `Based on the following execution results, generate the structured output according to the schema:
Execution Results: ${textResponse.text}
Please generate the structured output based on the execution results and context above.`,
schema: jsonSchema(agentSchema),
experimental_telemetry: AI_TELEMETRY_CONFIG,
});
return {
result: output.object as object,
usage: {
inputTokens:
(textResponse.usage?.inputTokens ?? 0) +
(output.usage?.inputTokens ?? 0),
outputTokens:
(textResponse.usage?.outputTokens ?? 0) +
(output.usage?.outputTokens ?? 0),
totalTokens:
(textResponse.usage?.totalTokens ?? 0) +
(output.usage?.totalTokens ?? 0),
},
};
} catch (error) {
if (error instanceof AgentException) {
throw error;
}
throw new AgentException(
error instanceof Error ? error.message : 'Agent execution failed',
AgentExceptionCode.AGENT_EXECUTION_FAILED,
);
}
}
}
@@ -1,7 +1,7 @@
import { type ToolUIPart } from 'ai';
import { type ExtendedUIMessagePart } from 'twenty-shared/ai';
import { type AgentChatMessagePartEntity } from 'src/engine/metadata-modules/ai-chat/entities/agent-chat-message-part.entity';
import { type AgentMessagePartEntity } from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message-part.entity';
const isToolPart = (part: ExtendedUIMessagePart): part is ToolUIPart => {
return part.type.includes('tool-') && 'toolCallId' in part;
@@ -10,9 +10,9 @@ const isToolPart = (part: ExtendedUIMessagePart): part is ToolUIPart => {
export const mapUIMessagePartsToDBParts = (
uiMessageParts: ExtendedUIMessagePart[],
messageId: string,
): Partial<AgentChatMessagePartEntity>[] => {
): Partial<AgentMessagePartEntity>[] => {
return uiMessageParts.map((part, index) => {
const basePart: Partial<AgentChatMessagePartEntity> = {
const basePart: Partial<AgentMessagePartEntity> = {
messageId,
orderIndex: index,
type: part.type,
@@ -0,0 +1,32 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AiAgentExecutionModule } from 'src/engine/metadata-modules/ai/ai-agent-execution/ai-agent-execution.module';
import { AiAgentModule } from 'src/engine/metadata-modules/ai/ai-agent/ai-agent.module';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { AiChatModule } from 'src/engine/metadata-modules/ai/ai-chat/ai-chat.module';
import { AgentChatThreadEntity } from 'src/engine/metadata-modules/ai/ai-chat/entities/agent-chat-thread.entity';
import { AiModelsModule } from 'src/engine/metadata-modules/ai/ai-models/ai-models.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { AgentTurnEvaluationEntity } from './entities/agent-turn-evaluation.entity';
import { AgentTurnResolver } from './resolvers/agent-turn.resolver';
import { AgentTurnGraderService } from './services/agent-turn-grader.service';
@Module({
imports: [
TypeOrmModule.forFeature([
AgentTurnEvaluationEntity,
AgentEntity,
AgentChatThreadEntity,
]),
AiAgentModule,
AiAgentExecutionModule,
AiChatModule,
AiModelsModule,
PermissionsModule,
],
providers: [AgentTurnGraderService, AgentTurnResolver],
exports: [AgentTurnGraderService],
})
export class AiAgentMonitorModule {}
@@ -0,0 +1,24 @@
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { IsDateString } from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@ObjectType('AgentTurnEvaluation')
export class AgentTurnEvaluationDTO {
@Field(() => UUIDScalarType)
id: string;
@Field(() => UUIDScalarType)
turnId: string;
@Field(() => Int)
score: number;
@Field({ nullable: true })
comment?: string;
@IsDateString()
@Field()
createdAt: Date;
}
@@ -0,0 +1,37 @@
import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
Relation,
} from 'typeorm';
import { AgentTurnEntity } from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-turn.entity';
@Entity('agentTurnEvaluation')
export class AgentTurnEvaluationEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column('uuid')
@Index()
turnId: string;
@ManyToOne(() => AgentTurnEntity, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'turnId' })
turn: Relation<AgentTurnEntity>;
@Column({ type: 'int' })
score: number;
@Column({ type: 'text', nullable: true })
comment: string | null;
@CreateDateColumn()
createdAt: Date;
}
@@ -0,0 +1,125 @@
import { UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { AgentTurnDTO } from 'src/engine/metadata-modules/ai/ai-agent-execution/dtos/agent-turn.dto';
import { AgentTurnEntity } from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-turn.entity';
import { AgentAsyncExecutorService } from 'src/engine/metadata-modules/ai/ai-agent-execution/services/agent-async-executor.service';
import { AgentTurnEvaluationDTO } from 'src/engine/metadata-modules/ai/ai-agent-monitor/dtos/agent-turn-evaluation.dto';
import { AgentTurnGraderService } from 'src/engine/metadata-modules/ai/ai-agent-monitor/services/agent-turn-grader.service';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { AgentChatThreadEntity } from 'src/engine/metadata-modules/ai/ai-chat/entities/agent-chat-thread.entity';
import { AgentChatService } from 'src/engine/metadata-modules/ai/ai-chat/services/agent-chat.service';
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
@UseGuards(WorkspaceAuthGuard, SettingsPermissionGuard(PermissionFlagType.AI))
@Resolver()
export class AgentTurnResolver {
constructor(
@InjectRepository(AgentTurnEntity)
private readonly turnRepository: Repository<AgentTurnEntity>,
@InjectRepository(AgentChatThreadEntity)
private readonly threadRepository: Repository<AgentChatThreadEntity>,
@InjectRepository(AgentEntity)
private readonly agentRepository: Repository<AgentEntity>,
private readonly graderService: AgentTurnGraderService,
private readonly aiAgentExecutorService: AgentAsyncExecutorService,
private readonly agentChatService: AgentChatService,
) {}
@Query(() => [AgentTurnDTO])
async agentTurns(
@Args('agentId', { type: () => UUIDScalarType }) agentId: string,
): Promise<AgentTurnDTO[]> {
const turns = await this.turnRepository.find({
where: { agentId },
relations: ['evaluations', 'messages', 'messages.parts'],
order: { createdAt: 'DESC' },
});
return turns as unknown as AgentTurnDTO[];
}
@Mutation(() => AgentTurnEvaluationDTO)
async evaluateAgentTurn(
@Args('turnId', { type: () => UUIDScalarType }) turnId: string,
): Promise<AgentTurnEvaluationDTO> {
const evaluation = await this.graderService.evaluateTurn(turnId);
return evaluation as unknown as AgentTurnEvaluationDTO;
}
@Mutation(() => AgentTurnDTO)
async runEvaluationInput(
@Args('agentId', { type: () => UUIDScalarType }) agentId: string,
@Args('input') input: string,
@AuthWorkspace() workspace: WorkspaceEntity,
@AuthUserWorkspaceId() userWorkspaceId: string,
): Promise<AgentTurnDTO> {
const thread = this.threadRepository.create({
userWorkspaceId,
title: `Eval: ${input.substring(0, 50)}...`,
});
const savedThread = await this.threadRepository.save(thread);
const turn = this.turnRepository.create({
threadId: savedThread.id,
agentId,
});
const savedTurn = await this.turnRepository.save(turn);
const agent = await this.agentRepository.findOne({
where: { id: agentId },
});
await this.agentChatService.addMessage({
threadId: savedThread.id,
turnId: savedTurn.id,
uiMessage: {
role: 'user',
parts: [{ type: 'text', text: input }],
},
});
const executionResult = await this.aiAgentExecutorService.executeAgent({
agent,
userPrompt: input,
});
await this.agentChatService.addMessage({
threadId: savedThread.id,
turnId: savedTurn.id,
agentId: agent?.id,
uiMessage: {
role: 'assistant',
parts: [
{
type: 'text',
text: JSON.stringify(executionResult.result) || '',
},
],
},
});
await this.graderService.evaluateTurn(savedTurn.id);
const turnWithEvaluations = await this.turnRepository.findOne({
where: { id: savedTurn.id },
relations: ['evaluations', 'messages', 'messages.parts'],
});
if (!turnWithEvaluations) {
throw new Error('Turn not found after execution');
}
return turnWithEvaluations as unknown as AgentTurnDTO;
}
}
@@ -0,0 +1,183 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { generateText } from 'ai';
import { Repository } from 'typeorm';
import { AgentMessageEntity } from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message.entity';
import { AgentTurnEntity } from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-turn.entity';
import { AgentTurnEvaluationEntity } from 'src/engine/metadata-modules/ai/ai-agent-monitor/entities/agent-turn-evaluation.entity';
import { AI_TELEMETRY_CONFIG } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-telemetry.const';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
@Injectable()
export class AgentTurnGraderService {
private readonly logger = new Logger(AgentTurnGraderService.name);
constructor(
@InjectRepository(AgentTurnEntity)
private readonly turnRepository: Repository<AgentTurnEntity>,
@InjectRepository(AgentTurnEvaluationEntity)
private readonly evaluationRepository: Repository<AgentTurnEvaluationEntity>,
private readonly aiModelRegistryService: AiModelRegistryService,
) {}
async evaluateTurn(turnId: string): Promise<AgentTurnEvaluationEntity> {
const turn = await this.turnRepository.findOne({
where: { id: turnId },
relations: ['messages', 'messages.parts'],
});
if (!turn) {
throw new Error(`Turn ${turnId} not found`);
}
const { score, comment } = await this.evaluateWithAI(turn);
const evaluation = this.evaluationRepository.create({
turnId,
score,
comment,
});
return this.evaluationRepository.save(evaluation);
}
private async evaluateWithAI(
turn: AgentTurnEntity & { messages: AgentMessageEntity[] },
): Promise<{ score: number; comment: string }> {
try {
const defaultModel = this.aiModelRegistryService.getDefaultSpeedModel();
if (!defaultModel) {
this.logger.warn('No default AI model available for evaluation');
return this.getFallbackEvaluation(turn);
}
const evaluationContext = this.buildEvaluationContext(turn);
const prompt = `You are evaluating an AI agent's performance on a single turn (user request + agent response).
${evaluationContext}
Evaluate this agent turn based on:
1. **Task Completion**: Did the agent accomplish what the user asked?
2. **Tool Usage**: Were tools used correctly and appropriately?
3. **Response Quality**: Is the response clear, accurate, and helpful?
4. **Error Handling**: Were errors handled gracefully?
Provide:
- A score from 0 to 100 (0 = complete failure, 100 = perfect)
- A brief comment explaining the score (max 200 characters)
Respond ONLY with valid JSON in this exact format:
{"score": <number>, "comment": "<string>"}`;
const result = await generateText({
model: defaultModel.model,
prompt,
temperature: 0.3,
experimental_telemetry: AI_TELEMETRY_CONFIG,
});
const parsed = JSON.parse(result.text);
return {
score: Math.max(0, Math.min(100, Math.round(parsed.score))),
comment: (parsed.comment || 'Evaluation completed').substring(0, 500),
};
} catch (error) {
this.logger.error('Failed to evaluate turn with AI:', error);
return this.getFallbackEvaluation(turn);
}
}
private buildEvaluationContext(
turn: AgentTurnEntity & { messages: AgentMessageEntity[] },
): string {
const userMessages = turn.messages.filter((m) => m.role === 'user');
const assistantMessages = turn.messages.filter(
(m) => m.role === 'assistant',
);
const userText = userMessages
.flatMap((m) => m.parts || [])
.filter((p) => p.textContent)
.map((p) => p.textContent)
.join('\n');
const assistantParts = assistantMessages.flatMap((m) => m.parts || []);
const assistantText = assistantParts
.filter((p) => p.textContent)
.map((p) => p.textContent)
.join('\n');
const toolCalls = assistantParts
.filter((p) => p.toolName)
.map((p) => ({
tool: p.toolName,
hasError: !!p.errorMessage,
error: p.errorMessage,
}));
const errors = assistantParts
.filter((p) => p.errorMessage)
.map((p) => p.errorMessage);
let context = `**User Request:**\n${userText || '(no text)'}\n\n`;
context += `**Agent Response:**\n${assistantText || '(no text response)'}\n\n`;
if (toolCalls.length > 0) {
context += `**Tools Used:**\n${toolCalls.map((t) => `- ${t.tool}${t.hasError ? ' (FAILED)' : ''}`).join('\n')}\n\n`;
}
if (errors.length > 0) {
context += `**Errors:**\n${errors.map((e) => `- ${e}`).join('\n')}\n\n`;
}
return context;
}
private getFallbackEvaluation(
turn: AgentTurnEntity & { messages: AgentMessageEntity[] },
): {
score: number;
comment: string;
} {
const parts = turn.messages.flatMap((m) => m.parts || []);
const errorCount = parts.filter((p) => p.errorMessage).length;
const hasResponse = parts.some((p) => p.textContent);
const toolCount = parts.filter((p) => p.toolName).length;
let score = 100;
if (errorCount > 0) {
score -= errorCount * 30;
}
if (!hasResponse) {
score -= 50;
}
const comments = [];
if (errorCount > 0) {
comments.push(`${errorCount} error(s)`);
}
if (toolCount > 0) {
comments.push(`${toolCount} tool(s) used`);
}
if (!hasResponse) {
comments.push('No response');
}
return {
score: Math.max(0, score),
comment: comments.length > 0 ? comments.join('; ') : 'Completed',
};
}
}
@@ -3,12 +3,12 @@ import { getRepositoryToken } from '@nestjs/typeorm';
import { type Repository } from 'typeorm';
import { type ModelId } from 'src/engine/metadata-modules/ai-models/constants/ai-models.const';
import { AgentEntity } from 'src/engine/metadata-modules/ai-agent/entities/agent.entity';
import {
AgentException,
AgentExceptionCode,
} from 'src/engine/metadata-modules/ai-agent/agent.exception';
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { type ModelId } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
@@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AgentEntity } from 'src/engine/metadata-modules/ai-agent/entities/agent.entity';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
@@ -3,11 +3,11 @@ import { InjectRepository } from '@nestjs/typeorm';
import { In, IsNull, Not, Repository } from 'typeorm';
import { AgentEntity } from 'src/engine/metadata-modules/ai-agent/entities/agent.entity';
import {
AgentException,
AgentExceptionCode,
} from 'src/engine/metadata-modules/ai-agent/agent.exception';
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
@@ -4,9 +4,9 @@ import { InjectRepository } from '@nestjs/typeorm';
import { isNonEmptyString } from '@sniptt/guards';
import { In, Repository } from 'typeorm';
import { AiAgentRoleService } from 'src/engine/metadata-modules/ai-agent-role/ai-agent-role.service';
import { type CreateAgentInput } from 'src/engine/metadata-modules/ai-agent/dtos/create-agent.input';
import { type UpdateAgentInput } from 'src/engine/metadata-modules/ai-agent/dtos/update-agent.input';
import { AiAgentRoleService } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.service';
import { type CreateAgentInput } from 'src/engine/metadata-modules/ai/ai-agent/dtos/create-agent.input';
import { type UpdateAgentInput } from 'src/engine/metadata-modules/ai/ai-agent/dtos/update-agent.input';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/compute-metadata-name-from-label.util';
@@ -10,11 +10,11 @@ import { FileModule } from 'src/engine/core-modules/file/file.module';
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { UserModule } from 'src/engine/core-modules/user/user.module';
import { AiAgentRoleModule } from 'src/engine/metadata-modules/ai-agent-role/ai-agent-role.module';
import { AiBillingModule } from 'src/engine/metadata-modules/ai-billing/ai-billing.module';
import { AiModelsModule } from 'src/engine/metadata-modules/ai-models/ai-models.module';
import { AiRouterModule } from 'src/engine/metadata-modules/ai-router/ai-router.module';
import { AiToolsModule } from 'src/engine/metadata-modules/ai-tools/ai-tools.module';
import { AiAgentRoleModule } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.module';
import { AiBillingModule } from 'src/engine/metadata-modules/ai/ai-billing/ai-billing.module';
import { AiChatRouterModule } from 'src/engine/metadata-modules/ai/ai-chat-router/ai-chat-router.module';
import { AiModelsModule } from 'src/engine/metadata-modules/ai/ai-models/ai-models.module';
import { AiToolsModule } from 'src/engine/metadata-modules/ai/ai-tools/ai-tools.module';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
@@ -48,7 +48,7 @@ import { AgentToolGeneratorService } from './services/agent-tool-generator.servi
FileModule,
ObjectMetadataModule,
PermissionsModule,
AiRouterModule,
AiChatRouterModule,
WorkspacePermissionsCacheModule,
WorkspaceCacheStorageModule,
TokenModule,
@@ -63,17 +63,19 @@ import { AgentToolGeneratorService } from './services/agent-tool-generator.servi
AgentService,
AgentExecutionService,
AgentModelConfigService,
AgentToolGeneratorService,
AgentPlanExecutorService,
AgentToolGeneratorService,
AgentTitleGenerationService,
AgentActorContextService,
],
exports: [
AgentService,
AgentExecutionService,
AgentToolGeneratorService,
AgentPlanExecutorService,
AgentToolGeneratorService,
AgentTitleGenerationService,
AgentActorContextService,
AgentModelConfigService,
TypeOrmModule.forFeature([AgentEntity]),
],
})
@@ -10,8 +10,8 @@ import {
import GraphQLJSON from 'graphql-type-json';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ModelId } from 'src/engine/metadata-modules/ai-models/constants/ai-models.const';
import { ModelConfiguration } from 'src/engine/metadata-modules/ai-agent/types/modelConfiguration';
import { ModelConfiguration } from 'src/engine/metadata-modules/ai/ai-agent/types/modelConfiguration';
import { ModelId } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
@ObjectType('Agent')
export class AgentDTO {
@@ -73,4 +73,7 @@ export class AgentDTO {
@Field(() => GraphQLJSON, { nullable: true })
modelConfiguration: ModelConfiguration;
@Field(() => [String])
evaluationInputs: string[];
}
@@ -1,6 +1,7 @@
import { Field, HideField, InputType } from '@nestjs/graphql';
import {
IsArray,
IsNotEmpty,
IsObject,
IsOptional,
@@ -10,8 +11,8 @@ import {
import GraphQLJSON from 'graphql-type-json';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ModelId } from 'src/engine/metadata-modules/ai-models/constants/ai-models.const';
import { ModelConfiguration } from 'src/engine/metadata-modules/ai-agent/types/modelConfiguration';
import { ModelConfiguration } from 'src/engine/metadata-modules/ai/ai-agent/types/modelConfiguration';
import { ModelId } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
@InputType()
export class CreateAgentInput {
@@ -60,6 +61,12 @@ export class CreateAgentInput {
@Field(() => GraphQLJSON, { nullable: true })
modelConfiguration?: ModelConfiguration;
@IsArray()
@IsString({ each: true })
@IsOptional()
@Field(() => [String], { nullable: true })
evaluationInputs?: string[];
@HideField()
standardId?: string;
@@ -1,6 +1,7 @@
import { Field, InputType } from '@nestjs/graphql';
import {
IsArray,
IsNotEmpty,
IsObject,
IsOptional,
@@ -10,8 +11,8 @@ import {
import GraphQLJSON from 'graphql-type-json';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ModelId } from 'src/engine/metadata-modules/ai-models/constants/ai-models.const';
import { ModelConfiguration } from 'src/engine/metadata-modules/ai-agent/types/modelConfiguration';
import { ModelConfiguration } from 'src/engine/metadata-modules/ai/ai-agent/types/modelConfiguration';
import { ModelId } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
@InputType()
export class UpdateAgentInput {
@@ -64,4 +65,10 @@ export class UpdateAgentInput {
@IsOptional()
@Field(() => GraphQLJSON, { nullable: true })
modelConfiguration?: ModelConfiguration;
@IsArray()
@IsString({ each: true })
@IsOptional()
@Field(() => [String], { nullable: true })
evaluationInputs?: string[];
}
@@ -13,13 +13,13 @@ import {
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/interfaces/syncable-entity.interface';
import {
ModelId,
DEFAULT_SMART_MODEL,
} from 'src/engine/metadata-modules/ai-models/constants/ai-models.const';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AgentResponseFormat } from 'src/engine/metadata-modules/ai-agent/types/agent-response-format.type';
import { ModelConfiguration } from 'src/engine/metadata-modules/ai-agent/types/modelConfiguration';
import { AgentResponseFormat } from 'src/engine/metadata-modules/ai/ai-agent/types/agent-response-format.type';
import { ModelConfiguration } from 'src/engine/metadata-modules/ai/ai-agent/types/modelConfiguration';
import {
DEFAULT_SMART_MODEL,
ModelId,
} from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
@Entity('agent')
@Index('IDX_AGENT_ID_DELETED_AT', ['id', 'deletedAt'])
@@ -81,4 +81,7 @@ export class AgentEntity
@Column({ nullable: true, type: 'jsonb' })
modelConfiguration: ModelConfiguration;
@Column({ type: 'text', array: true, default: '{}' })
evaluationInputs: string[];
}
@@ -7,7 +7,7 @@ import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/use
import {
AgentException,
AgentExceptionCode,
} from 'src/engine/metadata-modules/ai-agent/agent.exception';
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@@ -15,29 +15,28 @@ import { getAppPath } from 'twenty-shared/utils';
import { In } from 'typeorm';
import { getAllSelectableColumnNames } from 'src/engine/api/utils/get-all-selectable-column-names.utils';
import { AI_TELEMETRY_CONFIG } from 'src/engine/metadata-modules/ai-models/constants/ai-telemetry.const';
import { AIBillingService } from 'src/engine/metadata-modules/ai-billing/services/ai-billing.service';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai-models/services/ai-model-registry.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AgentService } from 'src/engine/metadata-modules/ai-agent/agent.service';
import { AGENT_CONFIG } from 'src/engine/metadata-modules/ai-agent/constants/agent-config.const';
import { AGENT_SYSTEM_PROMPTS } from 'src/engine/metadata-modules/ai-agent/constants/agent-system-prompts.const';
import { AgentActorContextService } from 'src/engine/metadata-modules/ai-agent/services/agent-actor-context.service';
import { type RecordIdsByObjectMetadataNameSingularType } from 'src/engine/metadata-modules/ai-agent/types/recordIdsByObjectMetadataNameSingular.type';
import { type ToolHints } from 'src/engine/metadata-modules/ai-router/types/tool-hints.interface';
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { AgentEntity } from 'src/engine/metadata-modules/ai-agent/entities/agent.entity';
import {
AgentException,
AgentExceptionCode,
} from 'src/engine/metadata-modules/ai-agent/agent.exception';
import { repairToolCall } from 'src/engine/metadata-modules/ai-agent/utils/repair-tool-call.util';
import { AgentToolGeneratorService } from './agent-tool-generator.service';
import { AgentModelConfigService } from './agent-model-config.service';
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
import { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.service';
import { AGENT_CONFIG } from 'src/engine/metadata-modules/ai/ai-agent/constants/agent-config.const';
import { AGENT_SYSTEM_PROMPTS } from 'src/engine/metadata-modules/ai/ai-agent/constants/agent-system-prompts.const';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { AgentActorContextService } from 'src/engine/metadata-modules/ai/ai-agent/services/agent-actor-context.service';
import { AgentModelConfigService } from 'src/engine/metadata-modules/ai/ai-agent/services/agent-model-config.service';
import { AgentToolGeneratorService } from 'src/engine/metadata-modules/ai/ai-agent/services/agent-tool-generator.service';
import { type RecordIdsByObjectMetadataNameSingularType } from 'src/engine/metadata-modules/ai/ai-agent/types/recordIdsByObjectMetadataNameSingular.type';
import { repairToolCall } from 'src/engine/metadata-modules/ai/ai-agent/utils/repair-tool-call.util';
import { AIBillingService } from 'src/engine/metadata-modules/ai/ai-billing/services/ai-billing.service';
import { type ToolHints } from 'src/engine/metadata-modules/ai/ai-chat-router/types/tool-hints.interface';
import { AI_TELEMETRY_CONFIG } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-telemetry.const';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
export interface AgentExecutionResult {
result: object;
@@ -5,10 +5,10 @@ import { openai } from '@ai-sdk/openai';
import { ProviderOptions } from '@ai-sdk/provider-utils';
import { ToolSet } from 'ai';
import { ModelProvider } from 'src/engine/metadata-modules/ai-models/constants/ai-models.const';
import { RegisteredAIModel } from 'src/engine/metadata-modules/ai-models/services/ai-model-registry.service';
import { AGENT_CONFIG } from 'src/engine/metadata-modules/ai-agent/constants/agent-config.const';
import { AgentEntity } from 'src/engine/metadata-modules/ai-agent/entities/agent.entity';
import { AGENT_CONFIG } from 'src/engine/metadata-modules/ai/ai-agent/constants/agent-config.const';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { ModelProvider } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
import { RegisteredAIModel } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
@Injectable()
export class AgentModelConfigService {
@@ -1,8 +1,8 @@
import { Injectable, Logger } from '@nestjs/common';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { type RecordIdsByObjectMetadataNameSingularType } from 'src/engine/metadata-modules/ai-agent/types/recordIdsByObjectMetadataNameSingular.type';
import { type PlanStep } from 'src/engine/metadata-modules/ai-router/types/router-result.interface';
import { type RecordIdsByObjectMetadataNameSingularType } from 'src/engine/metadata-modules/ai/ai-agent/types/recordIdsByObjectMetadataNameSingular.type';
import { type PlanStep } from 'src/engine/metadata-modules/ai/ai-chat-router/types/router-result.interface';
import { standardAgentDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents';
import { AgentExecutionService } from './agent-execution.service';
@@ -2,8 +2,8 @@ import { Injectable, Logger } from '@nestjs/common';
import { generateText } from 'ai';
import { AI_TELEMETRY_CONFIG } from 'src/engine/metadata-modules/ai-models/constants/ai-telemetry.const';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai-models/services/ai-model-registry.service';
import { AI_TELEMETRY_CONFIG } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-telemetry.const';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
@Injectable()
export class AgentTitleGenerationService {
@@ -6,11 +6,11 @@ import { Repository } from 'typeorm';
import type { ActorMetadata } from 'twenty-shared/types';
import { ToolAdapterService } from 'src/engine/metadata-modules/ai-tools/services/tool-adapter.service';
import { ToolService } from 'src/engine/metadata-modules/ai-tools/services/tool.service';
import { SearchArticlesTool } from 'src/engine/core-modules/tool/tools/search-articles-tool/search-articles-tool';
import { AgentEntity } from 'src/engine/metadata-modules/ai-agent/entities/agent.entity';
import type { ToolHints } from 'src/engine/metadata-modules/ai-router/types/tool-hints.interface';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import type { ToolHints } from 'src/engine/metadata-modules/ai/ai-chat-router/types/tool-hints.interface';
import { ToolAdapterService } from 'src/engine/metadata-modules/ai/ai-tools/services/tool-adapter.service';
import { ToolService } from 'src/engine/metadata-modules/ai/ai-tools/services/tool.service';
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
import { HELPER_AGENT } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/agents/helper-agent';
@@ -1,6 +1,6 @@
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import { isWorkflowRelatedObject } from 'src/engine/metadata-modules/ai-agent/utils/is-workflow-related-object.util';
import { isWorkflowRelatedObject } from 'src/engine/metadata-modules/ai/ai-agent/utils/is-workflow-related-object.util';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
describe('isWorkflowRelatedObject', () => {
@@ -1,7 +1,7 @@
import { generateObject, type LanguageModel, NoSuchToolError } from 'ai';
import { type z } from 'zod';
import { AI_TELEMETRY_CONFIG } from 'src/engine/metadata-modules/ai-models/constants/ai-telemetry.const';
import { AI_TELEMETRY_CONFIG } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-telemetry.const';
type ToolCall = {
type: 'tool-call';
@@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { AIBillingService } from 'src/engine/metadata-modules/ai-billing/services/ai-billing.service';
import { AiModelsModule } from 'src/engine/metadata-modules/ai-models/ai-models.module';
import { AIBillingService } from 'src/engine/metadata-modules/ai/ai-billing/services/ai-billing.service';
import { AiModelsModule } from 'src/engine/metadata-modules/ai/ai-models/ai-models.module';
import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module';
@Module({
@@ -2,9 +2,9 @@ import { Test, type TestingModule } from '@nestjs/testing';
import { BILLING_FEATURE_USED } from 'src/engine/core-modules/billing/constants/billing-feature-used.constant';
import { BillingMeterEventName } from 'src/engine/core-modules/billing/enums/billing-meter-event-names';
import { AIBillingService } from 'src/engine/metadata-modules/ai/ai-billing/services/ai-billing.service';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
import { AIBillingService } from 'src/engine/metadata-modules/ai-billing/services/ai-billing.service';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai-models/services/ai-model-registry.service';
describe('AIBillingService', () => {
let service: AIBillingService;
@@ -2,12 +2,12 @@ import { Injectable, Logger } from '@nestjs/common';
import { LanguageModelUsage } from 'ai';
import { type ModelId } from 'src/engine/metadata-modules/ai-models/constants/ai-models.const';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai-models/services/ai-model-registry.service';
import { convertCentsToBillingCredits } from 'src/engine/metadata-modules/ai-billing/utils/convert-cents-to-billing-credits.util';
import { BILLING_FEATURE_USED } from 'src/engine/core-modules/billing/constants/billing-feature-used.constant';
import { BillingMeterEventName } from 'src/engine/core-modules/billing/enums/billing-meter-event-names';
import { type BillingUsageEvent } from 'src/engine/core-modules/billing/types/billing-usage-event.type';
import { convertCentsToBillingCredits } from 'src/engine/metadata-modules/ai/ai-billing/utils/convert-cents-to-billing-credits.util';
import { type ModelId } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
@Injectable()
@@ -1,4 +1,4 @@
import { DOLLAR_TO_CREDIT_MULTIPLIER } from 'src/engine/metadata-modules/ai-billing/constants/dollar-to-credit-multiplier';
import { DOLLAR_TO_CREDIT_MULTIPLIER } from 'src/engine/metadata-modules/ai/ai-billing/constants/dollar-to-credit-multiplier';
// Converts cost in cents to cost in credits
// Formula: credits = (cents / 100) * DOLLAR_TO_CREDIT_MULTIPLIER
@@ -0,0 +1,29 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { AiModelsModule } from 'src/engine/metadata-modules/ai/ai-models/ai-models.module';
import { AiToolsModule } from 'src/engine/metadata-modules/ai/ai-tools/ai-tools.module';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { AiChatRouterService } from './ai-chat-router.service';
import { AiChatRouterPlanGeneratorService } from './services/ai-chat-router-plan-generator.service';
import { AiChatRouterStrategyDeciderService } from './services/ai-chat-router-strategy-decider.service';
@Module({
imports: [
TypeOrmModule.forFeature([AgentEntity, WorkspaceEntity]),
AiModelsModule,
AiToolsModule,
ObjectMetadataModule,
],
providers: [
AiChatRouterService,
AiChatRouterStrategyDeciderService,
AiChatRouterPlanGeneratorService,
],
exports: [AiChatRouterService],
})
export class AiChatRouterModule {}
@@ -4,24 +4,24 @@ import { InjectRepository } from '@nestjs/typeorm';
import { type UIDataTypes, type UIMessage, type UITools } from 'ai';
import { IsNull, type Repository } from 'typeorm';
import { type ModelId } from 'src/engine/metadata-modules/ai-models/constants/ai-models.const';
import { AgentEntity } from 'src/engine/metadata-modules/ai-agent/entities/agent.entity';
import { isWorkflowRelatedObject } from 'src/engine/metadata-modules/ai-agent/utils/is-workflow-related-object.util';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { isWorkflowRelatedObject } from 'src/engine/metadata-modules/ai/ai-agent/utils/is-workflow-related-object.util';
import { type ModelId } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service';
import { HELPER_AGENT } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/agents/helper-agent';
import { AiRouterPlanGeneratorService } from './services/ai-router-plan-generator.service';
import { AiChatRouterPlanGeneratorService } from './services/ai-chat-router-plan-generator.service';
import {
AiRouterStrategyDeciderService,
AiChatRouterStrategyDeciderService,
type StrategyDecision,
} from './services/ai-router-strategy-decider.service';
} from './services/ai-chat-router-strategy-decider.service';
import {
type RouterDebugInfo,
type UnifiedRouterResult,
} from './types/router-result.interface';
import { type ToolHints } from './types/tool-hints.interface';
export interface AiRouterContext {
export interface AiChatRouterContext {
messages: UIMessage<unknown, UIDataTypes, UITools>[];
workspaceId: string;
fastModel: ModelId;
@@ -29,19 +29,19 @@ export interface AiRouterContext {
}
@Injectable()
export class AiRouterService {
private readonly logger = new Logger(AiRouterService.name);
export class AiChatRouterService {
private readonly logger = new Logger(AiChatRouterService.name);
constructor(
@InjectRepository(AgentEntity)
private readonly agentRepository: Repository<AgentEntity>,
private readonly strategyDecider: AiRouterStrategyDeciderService,
private readonly planGenerator: AiRouterPlanGeneratorService,
private readonly strategyDecider: AiChatRouterStrategyDeciderService,
private readonly planGenerator: AiChatRouterPlanGeneratorService,
private readonly objectMetadataService: ObjectMetadataServiceV2,
) {}
async routeMessage(
context: AiRouterContext,
context: AiChatRouterContext,
includeDebugInfo = false,
): Promise<UnifiedRouterResult> {
try {
@@ -8,18 +8,18 @@ import {
} from 'ai';
import { z } from 'zod';
import { type AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { type ExecutionPlan } from 'src/engine/metadata-modules/ai/ai-chat-router/types/router-result.interface';
import {
DEFAULT_SMART_MODEL,
type ModelId,
} from 'src/engine/metadata-modules/ai-models/constants/ai-models.const';
import { AI_TELEMETRY_CONFIG } from 'src/engine/metadata-modules/ai-models/constants/ai-telemetry.const';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai-models/services/ai-model-registry.service';
import { type AgentEntity } from 'src/engine/metadata-modules/ai-agent/entities/agent.entity';
import { type ExecutionPlan } from 'src/engine/metadata-modules/ai-router/types/router-result.interface';
} from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
import { AI_TELEMETRY_CONFIG } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-telemetry.const';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
@Injectable()
export class AiRouterPlanGeneratorService {
private readonly logger = new Logger(AiRouterPlanGeneratorService.name);
export class AiChatRouterPlanGeneratorService {
private readonly logger = new Logger(AiChatRouterPlanGeneratorService.name);
constructor(
private readonly aiModelRegistryService: AiModelRegistryService,
@@ -9,14 +9,14 @@ import {
} from 'ai';
import { z } from 'zod';
import { AGENT_SYSTEM_PROMPTS } from 'src/engine/metadata-modules/ai/ai-agent/constants/agent-system-prompts.const';
import { type AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import {
DEFAULT_FAST_MODEL,
type ModelId,
} from 'src/engine/metadata-modules/ai-models/constants/ai-models.const';
import { AI_TELEMETRY_CONFIG } from 'src/engine/metadata-modules/ai-models/constants/ai-telemetry.const';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai-models/services/ai-model-registry.service';
import { type AgentEntity } from 'src/engine/metadata-modules/ai-agent/entities/agent.entity';
import { AGENT_SYSTEM_PROMPTS } from 'src/engine/metadata-modules/ai-agent/constants/agent-system-prompts.const';
} from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
import { AI_TELEMETRY_CONFIG } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-telemetry.const';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
export type StrategyDecision = {
strategy: 'simple' | 'planned';
@@ -28,8 +28,8 @@ export type StrategyDecision = {
};
@Injectable()
export class AiRouterStrategyDeciderService {
private readonly logger = new Logger(AiRouterStrategyDeciderService.name);
export class AiChatRouterStrategyDeciderService {
private readonly logger = new Logger(AiChatRouterStrategyDeciderService.name);
constructor(
private readonly aiModelRegistryService: AiModelRegistryService,
@@ -1,4 +1,4 @@
import { type AgentEntity } from 'src/engine/metadata-modules/ai-agent/entities/agent.entity';
import { type AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { type ToolHints } from './tool-hints.interface';
@@ -9,35 +9,31 @@ import { FileModule } from 'src/engine/core-modules/file/file.module';
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { AiAgentModule } from 'src/engine/metadata-modules/ai-agent/ai-agent.module';
import { AiBillingModule } from 'src/engine/metadata-modules/ai-billing/ai-billing.module';
import { AiModelsModule } from 'src/engine/metadata-modules/ai-models/ai-models.module';
import { AiRouterModule } from 'src/engine/metadata-modules/ai-router/ai-router.module';
import { AiAgentExecutionModule } from 'src/engine/metadata-modules/ai/ai-agent-execution/ai-agent-execution.module';
import { AiAgentModule } from 'src/engine/metadata-modules/ai/ai-agent/ai-agent.module';
import { AiBillingModule } from 'src/engine/metadata-modules/ai/ai-billing/ai-billing.module';
import { AiChatRouterModule } from 'src/engine/metadata-modules/ai/ai-chat-router/ai-chat-router.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
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 { AgentChatController } from './controllers/agent-chat.controller';
import { AgentChatMessagePartEntity } from './entities/agent-chat-message-part.entity';
import { AgentChatMessageEntity } from './entities/agent-chat-message.entity';
import { AgentChatThreadEntity } from './entities/agent-chat-thread.entity';
import { AgentChatResolver } from './resolvers/agent-chat.resolver';
import { AgentChatRoutingService } from './services/agent-chat-routing.service';
import { AgentChatStreamingService } from './services/agent-chat-streaming.service';
import { AgentChatService } from './services/agent-chat.service';
import { AgentStreamingService } from './services/agent-streaming.service';
@Module({
imports: [
TypeOrmModule.forFeature([
AgentChatMessageEntity,
AgentChatMessagePartEntity,
AgentChatThreadEntity,
FileEntity,
UserWorkspaceEntity,
]),
AiModelsModule,
AiBillingModule,
AiRouterModule,
AiAgentModule,
AiAgentExecutionModule,
AiChatRouterModule,
ThrottlerModule,
FeatureFlagModule,
FileUploadModule,
@@ -47,17 +43,19 @@ import { AgentStreamingService } from './services/agent-streaming.service';
WorkspaceCacheStorageModule,
TokenModule,
UserWorkspaceModule,
AiBillingModule,
],
controllers: [AgentChatController],
providers: [AgentChatResolver, AgentChatService, AgentStreamingService],
providers: [
AgentChatResolver,
AgentChatService,
AgentChatStreamingService,
AgentChatRoutingService,
],
exports: [
AgentChatService,
AgentStreamingService,
TypeOrmModule.forFeature([
AgentChatMessageEntity,
AgentChatMessagePartEntity,
AgentChatThreadEntity,
]),
AgentChatStreamingService,
TypeOrmModule.forFeature([AgentChatThreadEntity]),
],
})
export class AiChatModule {}
@@ -7,8 +7,8 @@ import {
UseGuards,
} from '@nestjs/common';
import { UIDataTypes, UIMessage, UITools } from 'ai';
import { Response } from 'express';
import { type ExtendedUIMessage } from 'twenty-shared/ai';
import { RestApiExceptionFilter } from 'src/engine/api/rest/rest-api-exception.filter';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -17,18 +17,16 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { type RecordIdsByObjectMetadataNameSingularType } from 'src/engine/metadata-modules/ai-agent/types/recordIdsByObjectMetadataNameSingular.type';
import { type RecordIdsByObjectMetadataNameSingularType } from 'src/engine/metadata-modules/ai/ai-agent/types/recordIdsByObjectMetadataNameSingular.type';
import { AgentChatStreamingService } from 'src/engine/metadata-modules/ai/ai-chat/services/agent-chat-streaming.service';
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
import { AgentChatService } from 'src/engine/metadata-modules/ai-chat/services/agent-chat.service';
import { AgentStreamingService } from 'src/engine/metadata-modules/ai-chat/services/agent-streaming.service';
@Controller('rest/agent-chat')
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
@UseFilters(RestApiExceptionFilter)
export class AgentChatController {
constructor(
private readonly agentChatService: AgentChatService,
private readonly agentStreamingService: AgentStreamingService,
private readonly agentStreamingService: AgentChatStreamingService,
) {}
@Post('stream')
@@ -37,7 +35,7 @@ export class AgentChatController {
@Body()
body: {
threadId: string;
messages: UIMessage<unknown, UIDataTypes, UITools>[];
messages: ExtendedUIMessage[];
recordIdsByObjectMetadataNameSingular?: RecordIdsByObjectMetadataNameSingularType;
},
@AuthUserWorkspaceId() userWorkspaceId: string,
@@ -13,7 +13,8 @@ import {
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { AgentChatMessageEntity } from 'src/engine/metadata-modules/ai-chat/entities/agent-chat-message.entity';
import { AgentMessageEntity } from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message.entity';
import { AgentTurnEntity } from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-turn.entity';
@Entity('agentChatThread')
export class AgentChatThreadEntity {
@@ -33,8 +34,11 @@ export class AgentChatThreadEntity {
@Column({ nullable: true, type: 'varchar' })
title: string;
@OneToMany(() => AgentChatMessageEntity, (message) => message.thread)
messages: Relation<AgentChatMessageEntity[]>;
@OneToMany(() => AgentTurnEntity, (turn) => turn.thread)
turns: Relation<AgentTurnEntity[]>;
@OneToMany(() => AgentMessageEntity, (message) => message.thread)
messages: Relation<AgentMessageEntity[]>;
@CreateDateColumn()
createdAt: Date;
@@ -10,10 +10,10 @@ import {
} from 'src/engine/guards/feature-flag.guard';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { AgentChatService } from 'src/engine/metadata-modules/ai-chat/services/agent-chat.service';
import { AgentMessageDTO } from 'src/engine/metadata-modules/ai/ai-agent-execution/dtos/agent-message.dto';
import { AgentChatThreadDTO } from 'src/engine/metadata-modules/ai/ai-chat/dtos/agent-chat-thread.dto';
import { AgentChatService } from 'src/engine/metadata-modules/ai/ai-chat/services/agent-chat.service';
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
import { AgentChatMessageDTO } from 'src/engine/metadata-modules/ai-chat/dtos/agent-chat-message.dto';
import { AgentChatThreadDTO } from 'src/engine/metadata-modules/ai-chat/dtos/agent-chat-thread.dto';
@UseGuards(
WorkspaceAuthGuard,
@@ -39,7 +39,7 @@ export class AgentChatResolver {
return this.agentChatService.getThreadById(id, userWorkspaceId);
}
@Query(() => [AgentChatMessageDTO])
@Query(() => [AgentMessageDTO])
@RequireFeatureFlag(FeatureFlagKey.IS_AI_ENABLED)
async chatMessages(
@Args('threadId', { type: () => UUIDScalarType }) threadId: string,
@@ -1,32 +1,18 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import {
createUIMessageStream,
pipeUIMessageStreamToResponse,
type UIDataTypes,
type UIMessage,
type UITools,
} from 'ai';
import { createUIMessageStream, pipeUIMessageStreamToResponse } from 'ai';
import { type Response } from 'express';
import { type ExtendedUIMessage } from 'twenty-shared/ai';
import { type Repository } from 'typeorm';
import { type ModelId } from 'src/engine/metadata-modules/ai-models/constants/ai-models.const';
import { AIBillingService } from 'src/engine/metadata-modules/ai-billing/services/ai-billing.service';
import { convertCentsToBillingCredits } from 'src/engine/metadata-modules/ai-billing/utils/convert-cents-to-billing-credits.util';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AgentChatMessageRole } from 'src/engine/metadata-modules/ai-chat/entities/agent-chat-message.entity';
import { AgentChatThreadEntity } from 'src/engine/metadata-modules/ai-chat/entities/agent-chat-thread.entity';
import { AgentChatService } from 'src/engine/metadata-modules/ai-chat/services/agent-chat.service';
import { AgentExecutionService } from 'src/engine/metadata-modules/ai-agent/services/agent-execution.service';
import {
AgentException,
AgentExceptionCode,
} from 'src/engine/metadata-modules/ai-agent/agent.exception';
import { AgentPlanExecutorService } from 'src/engine/metadata-modules/ai-agent/services/agent-plan-executor.service';
import { type RecordIdsByObjectMetadataNameSingularType } from 'src/engine/metadata-modules/ai-agent/types/recordIdsByObjectMetadataNameSingular.type';
import { AiRouterService } from 'src/engine/metadata-modules/ai-router/ai-router.service';
import { AgentMessageRole } from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message.entity';
import { AgentExecutionService } from 'src/engine/metadata-modules/ai/ai-agent/services/agent-execution.service';
import { AgentPlanExecutorService } from 'src/engine/metadata-modules/ai/ai-agent/services/agent-plan-executor.service';
import { type RecordIdsByObjectMetadataNameSingularType } from 'src/engine/metadata-modules/ai/ai-agent/types/recordIdsByObjectMetadataNameSingular.type';
import { AIBillingService } from 'src/engine/metadata-modules/ai/ai-billing/services/ai-billing.service';
import { convertCentsToBillingCredits } from 'src/engine/metadata-modules/ai/ai-billing/utils/convert-cents-to-billing-credits.util';
import { AiChatRouterService } from 'src/engine/metadata-modules/ai/ai-chat-router/ai-chat-router.service';
import { type ModelId } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
export type TokenUsage = {
promptTokens: number;
@@ -34,53 +20,50 @@ export type TokenUsage = {
totalTokens: number;
};
export type StreamAgentChatOptions = {
threadId: string;
export type MessagePersistenceCallbacks = {
saveSystemMessage?: (message: Omit<ExtendedUIMessage, 'id'>) => Promise<{
turnId: string;
}>;
saveUserMessage: (
message: Omit<ExtendedUIMessage, 'id'>,
turnId?: string,
) => Promise<{ turnId: string }>;
saveAssistantMessage: (
message: Omit<ExtendedUIMessage, 'id'>,
turnId: string,
agentId: string,
) => Promise<void>;
};
export type StreamAgentExecutionOptions = {
userWorkspaceId: string;
workspace: WorkspaceEntity;
recordIdsByObjectMetadataNameSingular: RecordIdsByObjectMetadataNameSingularType;
response: Response;
messages: UIMessage<unknown, UIDataTypes, UITools>[];
messages: ExtendedUIMessage[];
persistenceCallbacks: MessagePersistenceCallbacks;
};
@Injectable()
export class AgentStreamingService {
private readonly logger = new Logger(AgentStreamingService.name);
export class AgentChatRoutingService {
private readonly logger = new Logger(AgentChatRoutingService.name);
constructor(
@InjectRepository(AgentChatThreadEntity)
private readonly threadRepository: Repository<AgentChatThreadEntity>,
private readonly agentChatService: AgentChatService,
private readonly agentExecutionService: AgentExecutionService,
private readonly agentPlanExecutorService: AgentPlanExecutorService,
private readonly aiRouterService: AiRouterService,
private readonly aiChatRouterService: AiChatRouterService,
private readonly aiBillingService: AIBillingService,
) {}
async streamAgentChat({
threadId,
async streamAgentExecution({
userWorkspaceId,
workspace,
messages,
recordIdsByObjectMetadataNameSingular,
response,
}: StreamAgentChatOptions) {
persistenceCallbacks,
}: StreamAgentExecutionOptions) {
try {
const thread = await this.threadRepository.findOne({
where: {
id: threadId,
userWorkspaceId,
},
relations: ['messages'],
});
if (!thread) {
throw new AgentException(
'Thread not found',
AgentExceptionCode.AGENT_EXECUTION_FAILED,
);
}
const stream = createUIMessageStream<ExtendedUIMessage>({
execute: async ({ writer }) => {
const startTime = Date.now();
@@ -96,7 +79,7 @@ export class AgentStreamingService {
const routingStart = Date.now();
const includeDebugInfo = true;
const routeResult = await this.aiRouterService.routeMessage(
const routeResult = await this.aiChatRouterService.routeMessage(
{
messages,
workspaceId: workspace.id,
@@ -199,10 +182,22 @@ export class AgentStreamingService {
},
});
await this.agentChatService.addMessage({
threadId,
uiMessage: {
role: AgentChatMessageRole.USER,
const systemMessage = messages.find((msg) => msg.role === 'system');
let turnId: string | undefined;
if (systemMessage && persistenceCallbacks.saveSystemMessage) {
const savedSystemMessage =
await persistenceCallbacks.saveSystemMessage({
role: AgentMessageRole.SYSTEM,
parts: systemMessage.parts,
});
turnId = savedSystemMessage.turnId;
}
const userMessage = await persistenceCallbacks.saveUserMessage(
{
role: AgentMessageRole.USER,
parts: [
{
type: 'text',
@@ -213,12 +208,12 @@ export class AgentStreamingService {
},
],
},
});
turnId,
);
await this.agentChatService.addMessage({
threadId,
uiMessage: {
role: AgentChatMessageRole.ASSISTANT,
await persistenceCallbacks.saveAssistantMessage(
{
role: AgentMessageRole.ASSISTANT,
parts: [
{
type: 'text',
@@ -226,7 +221,9 @@ export class AgentStreamingService {
},
],
},
});
userMessage.turnId,
'',
);
return;
}
@@ -353,10 +350,9 @@ export class AgentStreamingService {
writer.write(updatedRoutedStatusPart);
await this.agentChatService.addMessage({
threadId,
uiMessage: {
role: AgentChatMessageRole.USER,
const userMessage = await persistenceCallbacks.saveUserMessage(
{
role: AgentMessageRole.USER,
parts: [
{
type: 'text',
@@ -367,15 +363,17 @@ export class AgentStreamingService {
},
],
},
});
undefined,
);
await this.agentChatService.addMessage({
threadId,
uiMessage: {
await persistenceCallbacks.saveAssistantMessage(
{
...responseMessage,
parts: [updatedRoutedStatusPart, ...responseMessage.parts],
},
});
userMessage.turnId,
agent.id,
);
},
sendReasoning: true,
}),
@@ -386,7 +384,7 @@ export class AgentStreamingService {
pipeUIMessageStreamToResponse({ stream, response });
} catch (error) {
this.logger.error(
'Failed to stream agent chat:',
'Failed to stream agent execution:',
error instanceof Error ? error.message : String(error),
);
response.end();
@@ -0,0 +1,94 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { type Response } from 'express';
import { type ExtendedUIMessage } from 'twenty-shared/ai';
import { type Repository } from 'typeorm';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import {
AgentException,
AgentExceptionCode,
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
import { type RecordIdsByObjectMetadataNameSingularType } from 'src/engine/metadata-modules/ai/ai-agent/types/recordIdsByObjectMetadataNameSingular.type';
import { AgentChatThreadEntity } from 'src/engine/metadata-modules/ai/ai-chat/entities/agent-chat-thread.entity';
import { AgentChatRoutingService } from 'src/engine/metadata-modules/ai/ai-chat/services/agent-chat-routing.service';
import { AgentChatService } from 'src/engine/metadata-modules/ai/ai-chat/services/agent-chat.service';
export type StreamAgentChatOptions = {
threadId: string;
userWorkspaceId: string;
workspace: WorkspaceEntity;
recordIdsByObjectMetadataNameSingular: RecordIdsByObjectMetadataNameSingularType;
response: Response;
messages: ExtendedUIMessage[];
};
@Injectable()
export class AgentChatStreamingService {
constructor(
@InjectRepository(AgentChatThreadEntity)
private readonly threadRepository: Repository<AgentChatThreadEntity>,
private readonly agentChatService: AgentChatService,
private readonly agentChatRoutingService: AgentChatRoutingService,
) {}
async streamAgentChat({
threadId,
userWorkspaceId,
workspace,
messages,
recordIdsByObjectMetadataNameSingular,
response,
}: StreamAgentChatOptions) {
const thread = await this.threadRepository.findOne({
where: {
id: threadId,
userWorkspaceId,
},
relations: ['messages'],
});
if (!thread) {
throw new AgentException(
'Thread not found',
AgentExceptionCode.AGENT_EXECUTION_FAILED,
);
}
await this.agentChatRoutingService.streamAgentExecution({
userWorkspaceId,
workspace,
messages,
recordIdsByObjectMetadataNameSingular,
response,
persistenceCallbacks: {
saveSystemMessage: async (message) => {
const savedMessage = await this.agentChatService.addMessage({
threadId,
uiMessage: message,
});
return { turnId: savedMessage.turnId };
},
saveUserMessage: async (message, turnId) => {
const savedMessage = await this.agentChatService.addMessage({
threadId,
uiMessage: message,
turnId,
});
return { turnId: savedMessage.turnId };
},
saveAssistantMessage: async (message, turnId, agentId) => {
await this.agentChatService.addMessage({
threadId,
uiMessage: message,
agentId,
turnId,
});
},
},
});
}
}
@@ -6,28 +6,31 @@ import { Repository } from 'typeorm';
import type { UIDataTypes, UIMessagePart, UITools } from 'ai';
import { AgentChatMessagePartEntity } from 'src/engine/metadata-modules/ai-chat/entities/agent-chat-message-part.entity';
import { AgentMessagePartEntity } from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message-part.entity';
import {
AgentChatMessageEntity,
AgentChatMessageRole,
} from 'src/engine/metadata-modules/ai-chat/entities/agent-chat-message.entity';
import { AgentChatThreadEntity } from 'src/engine/metadata-modules/ai-chat/entities/agent-chat-thread.entity';
AgentMessageEntity,
AgentMessageRole,
} from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message.entity';
import { AgentTurnEntity } from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-turn.entity';
import { mapUIMessagePartsToDBParts } from 'src/engine/metadata-modules/ai/ai-agent-execution/utils/mapUIMessagePartsToDBParts';
import {
AgentException,
AgentExceptionCode,
} from 'src/engine/metadata-modules/ai-agent/agent.exception';
import { mapUIMessagePartsToDBParts } from 'src/engine/metadata-modules/ai-chat/utils/mapUIMessagePartsToDBParts';
import { AgentTitleGenerationService } from 'src/engine/metadata-modules/ai-agent/services/agent-title-generation.service';
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
import { AgentTitleGenerationService } from 'src/engine/metadata-modules/ai/ai-agent/services/agent-title-generation.service';
import { AgentChatThreadEntity } from 'src/engine/metadata-modules/ai/ai-chat/entities/agent-chat-thread.entity';
@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>,
@InjectRepository(AgentTurnEntity)
private readonly turnRepository: Repository<AgentTurnEntity>,
@InjectRepository(AgentMessageEntity)
private readonly messageRepository: Repository<AgentMessageEntity>,
@InjectRepository(AgentMessagePartEntity)
private readonly messagePartRepository: Repository<AgentMessagePartEntity>,
private readonly titleGenerationService: AgentTitleGenerationService,
) {}
@@ -69,14 +72,33 @@ export class AgentChatService {
async addMessage({
threadId,
uiMessage,
agentId,
turnId,
}: {
threadId: string;
uiMessage: Omit<ExtendedUIMessage, 'id'>;
uiMessageParts?: UIMessagePart<UIDataTypes, UITools>[];
agentId?: string;
turnId?: string;
}) {
let actualTurnId = turnId;
if (!actualTurnId) {
const turn = this.turnRepository.create({
threadId,
agentId: agentId ?? null,
});
const savedTurn = await this.turnRepository.save(turn);
actualTurnId = savedTurn.id;
}
const message = this.messageRepository.create({
threadId,
role: uiMessage.role as AgentChatMessageRole,
turnId: actualTurnId,
role: uiMessage.role as AgentMessageRole,
agentId: agentId ?? null,
});
const savedMessage = await this.messageRepository.save(message);
@@ -90,7 +112,7 @@ export class AgentChatService {
await this.messagePartRepository.save(dbParts);
}
if (uiMessage.role === AgentChatMessageRole.USER) {
if (uiMessage.role === AgentMessageRole.USER) {
const messageContent = uiMessage.parts.find(
(part) => part.type === 'text',
)?.text;
@@ -1,7 +1,7 @@
import { Global, Module } from '@nestjs/common';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai-models/services/ai-model-registry.service';
import { AiService } from 'src/engine/metadata-modules/ai-models/services/ai.service';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
import { AiService } from 'src/engine/metadata-modules/ai/ai-models/services/ai.service';
@Global()
@Module({
@@ -1,7 +1,7 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai-models/services/ai-model-registry.service';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
import {
AI_MODELS,
@@ -5,14 +5,14 @@ import { createOpenAI, openai } from '@ai-sdk/openai';
import { xai } from '@ai-sdk/xai';
import { type LanguageModel } from 'ai';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import {
AI_MODELS,
DEFAULT_FAST_MODEL,
DEFAULT_SMART_MODEL,
ModelProvider,
type AIModelConfig,
} from 'src/engine/metadata-modules/ai-models/constants/ai-models.const';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
} from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
export interface RegisteredAIModel {
modelId: string;
@@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common';
import { LanguageModel, type ModelMessage, streamText } from 'ai';
import { AI_TELEMETRY_CONFIG } from 'src/engine/metadata-modules/ai-models/constants/ai-telemetry.const';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai-models/services/ai-model-registry.service';
import { AI_TELEMETRY_CONFIG } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-telemetry.const';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
@Injectable()
export class AiService {
@@ -1,8 +1,6 @@
import { Global, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ToolAdapterService } from 'src/engine/metadata-modules/ai-tools/services/tool-adapter.service';
import { ToolService } from 'src/engine/metadata-modules/ai-tools/services/tool.service';
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
@@ -10,6 +8,8 @@ import { FileModule } from 'src/engine/core-modules/file/file.module';
import { RecordCrudModule } from 'src/engine/core-modules/record-crud/record-crud.module';
import { ToolModule } from 'src/engine/core-modules/tool/tool.module';
import { SearchArticlesTool } from 'src/engine/core-modules/tool/tools/search-articles-tool/search-articles-tool';
import { ToolAdapterService } from 'src/engine/metadata-modules/ai/ai-tools/services/tool-adapter.service';
import { ToolService } from 'src/engine/metadata-modules/ai/ai-tools/services/tool.service';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
@@ -2,11 +2,11 @@ import { Test } from '@nestjs/testing';
import { jsonSchema } from 'ai';
import { ToolAdapterService } from 'src/engine/metadata-modules/ai-tools/services/tool-adapter.service';
import { ToolType } from 'src/engine/core-modules/tool/enums/tool-type.enum';
import { ToolRegistryService } from 'src/engine/core-modules/tool/services/tool-registry.service';
import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
import { type Tool } from 'src/engine/core-modules/tool/types/tool.type';
import { ToolAdapterService } from 'src/engine/metadata-modules/ai/ai-tools/services/tool-adapter.service';
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
@@ -1,11 +1,11 @@
import { Test } from '@nestjs/testing';
import { ToolService } from 'src/engine/metadata-modules/ai-tools/services/tool.service';
import { CreateRecordService } from 'src/engine/core-modules/record-crud/services/create-record.service';
import { DeleteRecordService } from 'src/engine/core-modules/record-crud/services/delete-record.service';
import { FindRecordsService } from 'src/engine/core-modules/record-crud/services/find-records.service';
import { UpdateRecordService } from 'src/engine/core-modules/record-crud/services/update-record.service';
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
import { ToolService } from 'src/engine/metadata-modules/ai/ai-tools/services/tool.service';
import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service';
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@@ -10,14 +10,14 @@ import { UpdateRecordService } from 'src/engine/core-modules/record-crud/service
import { generateCreateRecordInputSchema } from 'src/engine/core-modules/record-crud/utils/generate-create-record-input-schema.util';
import { generateUpdateRecordInputSchema } from 'src/engine/core-modules/record-crud/utils/generate-update-record-input-schema.util';
import { BulkDeleteToolInputSchema } from 'src/engine/core-modules/record-crud/zod-schemas/bulk-delete-tool.zod-schema';
import { FindOneToolInputSchema } from 'src/engine/core-modules/record-crud/zod-schemas/find-one-tool.zod-schema';
import { generateFindToolInputSchema } from 'src/engine/core-modules/record-crud/zod-schemas/find-tool.zod-schema';
import { SoftDeleteToolInputSchema } from 'src/engine/core-modules/record-crud/zod-schemas/soft-delete-tool.zod-schema';
import { FindOneToolInputSchema } from 'src/engine/core-modules/record-crud/zod-schemas/find-one-tool.zod-schema';
import { isWorkflowRelatedObject } from 'src/engine/metadata-modules/ai-agent/utils/is-workflow-related-object.util';
import { isWorkflowRelatedObject } from 'src/engine/metadata-modules/ai/ai-agent/utils/is-workflow-related-object.util';
import {
type ToolHints,
type ToolOperation,
} from 'src/engine/metadata-modules/ai-router/types/tool-hints.interface';
} from 'src/engine/metadata-modules/ai/ai-chat-router/types/tool-hints.interface';
import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service';
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@@ -1,4 +1,4 @@
import { type AgentEntity } from 'src/engine/metadata-modules/ai-agent/entities/agent.entity';
import { type AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
export const agentEntityRelationProperties = [
@@ -1,4 +1,4 @@
import { type AgentEntity } from 'src/engine/metadata-modules/ai-agent/entities/agent.entity';
import { type AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
export const transformAgentEntityToFlatAgent = (
@@ -19,5 +19,6 @@ export const transformAgentEntityToFlatAgent = (
universalIdentifier: agentEntity.standardId || agentEntity.id,
applicationId: agentEntity.applicationId,
modelConfiguration: agentEntity.modelConfiguration,
evaluationInputs: agentEntity.evaluationInputs,
};
};
@@ -1,7 +1,8 @@
import { Module } from '@nestjs/common';
import { AiAgentModule } from 'src/engine/metadata-modules/ai-agent/ai-agent.module';
import { AiChatModule } from 'src/engine/metadata-modules/ai-chat/ai-chat.module';
import { AiAgentMonitorModule } from 'src/engine/metadata-modules/ai/ai-agent-monitor/ai-agent-monitor.module';
import { AiAgentModule } from 'src/engine/metadata-modules/ai/ai-agent/ai-agent.module';
import { AiChatModule } from 'src/engine/metadata-modules/ai/ai-chat/ai-chat.module';
import { CronTriggerModule } from 'src/engine/metadata-modules/cron-trigger/cron-trigger.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { DatabaseEventTriggerModule } from 'src/engine/metadata-modules/database-event-trigger/database-event-trigger.module';
@@ -27,6 +28,7 @@ import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-
ServerlessFunctionModule,
ServerlessFunctionLayerModule,
AiAgentModule,
AiAgentMonitorModule,
AiChatModule,
ViewModule,
WorkspaceMetadataVersionModule,
@@ -20,6 +20,7 @@ export enum PermissionFlagType {
UPLOAD_FILE = 'UPLOAD_FILE',
DOWNLOAD_FILE = 'DOWNLOAD_FILE',
SEND_EMAIL_TOOL = 'SEND_EMAIL_TOOL',
HTTP_REQUEST_TOOL = 'HTTP_REQUEST_TOOL',
IMPORT_CSV = 'IMPORT_CSV',
EXPORT_CSV = 'EXPORT_CSV',
CONNECTED_ACCOUNTS = 'CONNECTED_ACCOUNTS',
@@ -4,6 +4,7 @@ export const TOOL_PERMISSION_FLAGS = [
'UPLOAD_FILE',
'DOWNLOAD_FILE',
'SEND_EMAIL_TOOL',
'HTTP_REQUEST_TOOL',
'IMPORT_CSV',
'EXPORT_CSV',
'CONNECTED_ACCOUNTS',
@@ -109,6 +109,7 @@ export class PermissionsService {
[PermissionFlagType.UPLOAD_FILE]: false,
[PermissionFlagType.DOWNLOAD_FILE]: false,
[PermissionFlagType.SEND_EMAIL_TOOL]: false,
[PermissionFlagType.HTTP_REQUEST_TOOL]: false,
[PermissionFlagType.IMPORT_CSV]: false,
[PermissionFlagType.EXPORT_CSV]: false,
[PermissionFlagType.CONNECTED_ACCOUNTS]: false,
@@ -4,7 +4,7 @@ import { Relation } from 'typeorm';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { WorkspaceMemberDTO } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
import { AgentDTO } from 'src/engine/metadata-modules/ai-agent/dtos/agent.dto';
import { AgentDTO } from 'src/engine/metadata-modules/ai/ai-agent/dtos/agent.dto';
import { FieldPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/field-permission.dto';
import { ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto';
import { PermissionFlagDTO } from 'src/engine/metadata-modules/permission-flag/dtos/permission-flag.dto';
@@ -7,7 +7,7 @@ import { FileModule } from 'src/engine/core-modules/file/file.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AiAgentRoleModule } from 'src/engine/metadata-modules/ai-agent-role/ai-agent-role.module';
import { AiAgentRoleModule } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.module';
import { ObjectPermissionModule } from 'src/engine/metadata-modules/object-permission/object-permission.module';
import { PermissionFlagModule } from 'src/engine/metadata-modules/permission-flag/permission-flag.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
@@ -24,8 +24,8 @@ import { RequireFeatureFlag } from 'src/engine/guards/feature-flag.guard';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { AiAgentRoleService } from 'src/engine/metadata-modules/ai-agent-role/ai-agent-role.service';
import { AgentDTO } from 'src/engine/metadata-modules/ai-agent/dtos/agent.dto';
import { AiAgentRoleService } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.service';
import { AgentDTO } from 'src/engine/metadata-modules/ai/ai-agent/dtos/agent.dto';
import { FieldPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/field-permission.dto';
import { ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto';
import { UpsertFieldPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-field-permissions.input';