refactor(server): rename Agent exception to Ai; add THREAD_NOT_FOUND / MESSAGE_NOT_FOUND codes (fixes 500s) (#19831)
## Summary - The exception class under `ai-agent/` was serving every AI surface (agent, chat, role, models, generate-text), so `Agent` was a misnomer. Promoted to the `ai/` namespace; renamed `AgentException` → `AiException`, `AgentExceptionCode` → `AiExceptionCode`, and related interceptor / filter / handler / file names accordingly. - Split the single `AGENT_NOT_FOUND` code into entity-specific codes. Chat-thread lookups no longer reuse the agent identifier. - **Fixes Sentry 500s on `GetChatMessages` / `chatThread`.** Every "Thread not found" and "Queued message not found" throw site in ai-chat was previously wired to `AGENT_EXECUTION_FAILED`, which maps to `InternalServerError` (HTTP 500). They now use `THREAD_NOT_FOUND` / `MESSAGE_NOT_FOUND`, both of which map to `NotFoundError` (HTTP 404) in the GraphQL and REST handlers. The underlying cause of *why* clients are asking for threads that no longer resolve for them — per-user chat-thread create events being broadcast workspace-wide — is addressed separately in a follow-up PR. ### Code map - Added: `ai/ai.exception.ts`, `ai/utils/ai-graphql-api-exception-handler.util.ts` (+ spec with new THREAD/MESSAGE cases), `ai/interceptors/ai-graphql-api-exception.interceptor.ts`, `ai/filters/ai-api-exception.filter.ts` - Deleted: `ai/ai-agent/agent.exception.ts`, `ai/ai-agent/utils/agent-graphql-api-exception-handler.util.ts` (+ spec), `ai/ai-agent/interceptors/agent-graphql-api-exception.interceptor.ts`, `ai/ai-agent/filters/agent-api-exception.filter.ts` - Updated: 21 call sites across ai-agent, ai-agent-execution, ai-agent-role, ai-chat, ai-generate-text, ai-models, role, and workspace-migration validators. ## Test plan - [x] `npx nx typecheck twenty-server` - [x] `npx jest ai-graphql-api-exception-handler` (3/3 including new THREAD_NOT_FOUND and MESSAGE_NOT_FOUND cases) - [x] `npx jest agent-role.service` (9/9) - [x] `npx oxlint --type-aware` on all changed files (0 warnings/errors) - [x] `npx prettier --check` on all changed files - [ ] CI
This commit is contained in:
+9
-9
@@ -5,9 +5,9 @@ import { type ActorMetadata } from 'twenty-shared/types';
|
||||
import { buildCreatedByFromFullNameMetadata } from 'src/engine/core-modules/actor/utils/build-created-by-from-full-name-metadata.util';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import {
|
||||
AgentException,
|
||||
AgentExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
|
||||
AiException,
|
||||
AiExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai.exception';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
@@ -46,9 +46,9 @@ export class AgentActorContextService {
|
||||
await this.userWorkspaceService.findById(userWorkspaceId);
|
||||
|
||||
if (!userWorkspace) {
|
||||
throw new AgentException(
|
||||
throw new AiException(
|
||||
'User workspace not found',
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
AiExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,9 +72,9 @@ export class AgentActorContextService {
|
||||
);
|
||||
|
||||
if (!workspaceMember) {
|
||||
throw new AgentException(
|
||||
throw new AiException(
|
||||
'Workspace member not found for user',
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
AiExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -84,9 +84,9 @@ export class AgentActorContextService {
|
||||
});
|
||||
|
||||
if (!roleId) {
|
||||
throw new AgentException(
|
||||
throw new AiException(
|
||||
'User role not found',
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
AiExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+8
-8
@@ -23,9 +23,9 @@ import { countNativeWebSearchCallsFromSteps } from 'src/engine/metadata-modules/
|
||||
import { extractCacheCreationTokensFromSteps } from 'src/engine/metadata-modules/ai/ai-billing/utils/extract-cache-creation-tokens.util';
|
||||
import { mergeLanguageModelUsage } from 'src/engine/metadata-modules/ai/ai-billing/utils/merge-language-model-usage.util';
|
||||
import {
|
||||
AgentException,
|
||||
AgentExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
|
||||
AiException,
|
||||
AiExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai.exception';
|
||||
import { AGENT_CONFIG } from 'src/engine/metadata-modules/ai/ai-agent/constants/agent-config.const';
|
||||
import { WORKFLOW_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';
|
||||
@@ -239,9 +239,9 @@ export class AgentAsyncExecutorService {
|
||||
});
|
||||
|
||||
if (structuredResult.output == null) {
|
||||
throw new AgentException(
|
||||
throw new AiException(
|
||||
'Failed to generate structured output from execution results',
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
AiExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -255,12 +255,12 @@ export class AgentAsyncExecutorService {
|
||||
nativeWebSearchCallCount,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof AgentException) {
|
||||
if (error instanceof AiException) {
|
||||
throw error;
|
||||
}
|
||||
throw new AgentException(
|
||||
throw new AiException(
|
||||
error instanceof Error ? error.message : 'Agent execution failed',
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
AiExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+14
-14
@@ -4,9 +4,9 @@ import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { type Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
AgentException,
|
||||
AgentExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
|
||||
AiException,
|
||||
AiExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai.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/types/model-id.type';
|
||||
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
|
||||
@@ -226,7 +226,7 @@ describe('AiAgentRoleService', () => {
|
||||
expect(roleTargetService.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw AgentException when agent does not exist', async () => {
|
||||
it('should throw AiException when agent does not exist', async () => {
|
||||
// Arrange
|
||||
const nonExistentAgentId = 'non-existent-agent-id';
|
||||
|
||||
@@ -239,7 +239,7 @@ describe('AiAgentRoleService', () => {
|
||||
agentId: nonExistentAgentId,
|
||||
roleId: testRole.id,
|
||||
}),
|
||||
).rejects.toThrow(AgentException);
|
||||
).rejects.toThrow(AiException);
|
||||
|
||||
await expect(
|
||||
service.assignRoleToAgent({
|
||||
@@ -248,12 +248,12 @@ describe('AiAgentRoleService', () => {
|
||||
roleId: testRole.id,
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: AgentExceptionCode.AGENT_NOT_FOUND,
|
||||
code: AiExceptionCode.AGENT_NOT_FOUND,
|
||||
message: `Agent with id ${nonExistentAgentId} not found in workspace`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw AgentException when role does not exist', async () => {
|
||||
it('should throw AiException when role does not exist', async () => {
|
||||
// Arrange
|
||||
const nonExistentRoleId = 'non-existent-role-id';
|
||||
|
||||
@@ -267,7 +267,7 @@ describe('AiAgentRoleService', () => {
|
||||
agentId: testAgent.id,
|
||||
roleId: nonExistentRoleId,
|
||||
}),
|
||||
).rejects.toThrow(AgentException);
|
||||
).rejects.toThrow(AiException);
|
||||
|
||||
await expect(
|
||||
service.assignRoleToAgent({
|
||||
@@ -276,12 +276,12 @@ describe('AiAgentRoleService', () => {
|
||||
roleId: nonExistentRoleId,
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: AgentExceptionCode.ROLE_NOT_FOUND,
|
||||
code: AiExceptionCode.ROLE_NOT_FOUND,
|
||||
message: `Role with id ${nonExistentRoleId} not found in workspace`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw AgentException when agent belongs to different workspace', async () => {
|
||||
it('should throw AiException when agent belongs to different workspace', async () => {
|
||||
// Arrange
|
||||
const differentWorkspaceId = 'different-workspace-id';
|
||||
|
||||
@@ -294,7 +294,7 @@ describe('AiAgentRoleService', () => {
|
||||
agentId: testAgent.id,
|
||||
roleId: testRole.id,
|
||||
}),
|
||||
).rejects.toThrow(AgentException);
|
||||
).rejects.toThrow(AiException);
|
||||
|
||||
await expect(
|
||||
service.assignRoleToAgent({
|
||||
@@ -303,7 +303,7 @@ describe('AiAgentRoleService', () => {
|
||||
roleId: testRole.id,
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: AgentExceptionCode.AGENT_NOT_FOUND,
|
||||
code: AiExceptionCode.AGENT_NOT_FOUND,
|
||||
message: `Agent with id ${testAgent.id} not found in workspace`,
|
||||
});
|
||||
});
|
||||
@@ -355,7 +355,7 @@ describe('AiAgentRoleService', () => {
|
||||
workspaceId: testWorkspaceId,
|
||||
agentId: testAgent.id,
|
||||
}),
|
||||
).rejects.toThrow(AgentException);
|
||||
).rejects.toThrow(AiException);
|
||||
|
||||
await expect(
|
||||
service.removeRoleFromAgent({
|
||||
@@ -363,7 +363,7 @@ describe('AiAgentRoleService', () => {
|
||||
agentId: testAgent.id,
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: AgentExceptionCode.ROLE_NOT_FOUND,
|
||||
code: AiExceptionCode.ROLE_NOT_FOUND,
|
||||
message: `Role target not found for agent ${testAgent.id}`,
|
||||
});
|
||||
});
|
||||
|
||||
+11
-11
@@ -5,9 +5,9 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { In, IsNull, Not, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
AgentException,
|
||||
AgentExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
|
||||
AiException,
|
||||
AiExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai.exception';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
|
||||
import { RoleTargetService } from 'src/engine/metadata-modules/role-target/services/role-target.service';
|
||||
@@ -69,9 +69,9 @@ export class AiAgentRoleService {
|
||||
});
|
||||
|
||||
if (!isDefined(existingRoleTarget)) {
|
||||
throw new AgentException(
|
||||
throw new AiException(
|
||||
`Role target not found for agent ${agentId}`,
|
||||
AgentExceptionCode.ROLE_NOT_FOUND,
|
||||
AiExceptionCode.ROLE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -125,9 +125,9 @@ export class AiAgentRoleService {
|
||||
});
|
||||
|
||||
if (!agent) {
|
||||
throw new AgentException(
|
||||
throw new AiException(
|
||||
`Agent with id ${agentId} not found in workspace`,
|
||||
AgentExceptionCode.AGENT_NOT_FOUND,
|
||||
AiExceptionCode.AGENT_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -136,16 +136,16 @@ export class AiAgentRoleService {
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
throw new AgentException(
|
||||
throw new AiException(
|
||||
`Role with id ${roleId} not found in workspace`,
|
||||
AgentExceptionCode.ROLE_NOT_FOUND,
|
||||
AiExceptionCode.ROLE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (!role.canBeAssignedToAgents) {
|
||||
throw new AgentException(
|
||||
throw new AiException(
|
||||
`Role "${role.label}" cannot be assigned to agents`,
|
||||
AgentExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS,
|
||||
AiExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import { AgentIdInput } from './dtos/agent-id.input';
|
||||
import { AgentDTO } from './dtos/agent.dto';
|
||||
import { CreateAgentInput } from './dtos/create-agent.input';
|
||||
import { UpdateAgentInput } from './dtos/update-agent.input';
|
||||
import { AgentGraphqlApiExceptionInterceptor } from './interceptors/agent-graphql-api-exception.interceptor';
|
||||
import { AiGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/ai/interceptors/ai-graphql-api-exception.interceptor';
|
||||
|
||||
@UseGuards(
|
||||
WorkspaceAuthGuard,
|
||||
@@ -33,7 +33,7 @@ import { AgentGraphqlApiExceptionInterceptor } from './interceptors/agent-graphq
|
||||
)
|
||||
@UseInterceptors(
|
||||
WorkspaceMigrationGraphqlApiExceptionInterceptor,
|
||||
AgentGraphqlApiExceptionInterceptor,
|
||||
AiGraphqlApiExceptionInterceptor,
|
||||
)
|
||||
@MetadataResolver()
|
||||
export class AgentResolver {
|
||||
|
||||
@@ -16,7 +16,10 @@ import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/works
|
||||
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
import { AgentException, AgentExceptionCode } from './agent.exception';
|
||||
import {
|
||||
AiException,
|
||||
AiExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai.exception';
|
||||
|
||||
import { AgentEntity } from './entities/agent.entity';
|
||||
|
||||
@@ -63,9 +66,9 @@ export class AgentService {
|
||||
if (!agent) {
|
||||
const identifier = `name "${name}"`;
|
||||
|
||||
throw new AgentException(
|
||||
throw new AiException(
|
||||
`Agent with ${identifier} not found`,
|
||||
AgentExceptionCode.AGENT_NOT_FOUND,
|
||||
AiExceptionCode.AGENT_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -91,10 +94,7 @@ export class AgentService {
|
||||
});
|
||||
|
||||
if (!isDefined(flatAgent)) {
|
||||
throw new AgentException(
|
||||
`Agent not found`,
|
||||
AgentExceptionCode.AGENT_NOT_FOUND,
|
||||
);
|
||||
throw new AiException(`Agent not found`, AiExceptionCode.AGENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
const roleId = flatRoleTargetByAgentIdMaps[flatAgent.id]?.roleId;
|
||||
@@ -281,9 +281,9 @@ export class AgentService {
|
||||
});
|
||||
|
||||
if (deletedAgents.length !== 1) {
|
||||
throw new AgentException(
|
||||
throw new AiException(
|
||||
'Could not retrieve deleted agent',
|
||||
AgentExceptionCode.AGENT_NOT_FOUND,
|
||||
AiExceptionCode.AGENT_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-
|
||||
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
|
||||
import { AiAgentRoleModule } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.module';
|
||||
import { AgentGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/ai/ai-agent/interceptors/agent-graphql-api-exception.interceptor';
|
||||
import { AiGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/ai/interceptors/ai-graphql-api-exception.interceptor';
|
||||
import { AiModelsModule } from 'src/engine/metadata-modules/ai/ai-models/ai-models.module';
|
||||
import { FlatAgentModule } from 'src/engine/metadata-modules/flat-agent/flat-agent.module';
|
||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||
@@ -45,7 +45,7 @@ import { AgentEntity } from './entities/agent.entity';
|
||||
AgentResolver,
|
||||
AgentService,
|
||||
WorkspaceMigrationGraphqlApiExceptionInterceptor,
|
||||
AgentGraphqlApiExceptionInterceptor,
|
||||
AiGraphqlApiExceptionInterceptor,
|
||||
],
|
||||
exports: [AgentService, TypeOrmModule.forFeature([AgentEntity])],
|
||||
})
|
||||
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
import {
|
||||
type CallHandler,
|
||||
type ExecutionContext,
|
||||
Injectable,
|
||||
type NestInterceptor,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { type Observable, catchError } from 'rxjs';
|
||||
|
||||
import { agentGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/ai/ai-agent/utils/agent-graphql-api-exception-handler.util';
|
||||
|
||||
@Injectable()
|
||||
export class AgentGraphqlApiExceptionInterceptor implements NestInterceptor {
|
||||
intercept(
|
||||
_context: ExecutionContext,
|
||||
next: CallHandler,
|
||||
): Observable<unknown> {
|
||||
return next.handle().pipe(catchError(agentGraphqlApiExceptionHandler));
|
||||
}
|
||||
}
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
import {
|
||||
AgentException,
|
||||
AgentExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
|
||||
import { agentGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/ai/ai-agent/utils/agent-graphql-api-exception-handler.util';
|
||||
import {
|
||||
ErrorCode,
|
||||
type BaseGraphQLError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
|
||||
const catchGraphqlError = (error: Error): BaseGraphQLError => {
|
||||
try {
|
||||
agentGraphqlApiExceptionHandler(error);
|
||||
throw new Error('Expected agentGraphqlApiExceptionHandler to throw');
|
||||
} catch (graphqlError) {
|
||||
return graphqlError as BaseGraphQLError;
|
||||
}
|
||||
};
|
||||
|
||||
describe('agentGraphqlApiExceptionHandler', () => {
|
||||
it('maps API key configuration failures to INTERNAL_SERVER_ERROR with a subCode', () => {
|
||||
const error = new AgentException(
|
||||
'No AI models are available',
|
||||
AgentExceptionCode.API_KEY_NOT_CONFIGURED,
|
||||
);
|
||||
|
||||
const graphqlError = catchGraphqlError(error);
|
||||
|
||||
expect(graphqlError.extensions.code).toBe(ErrorCode.INTERNAL_SERVER_ERROR);
|
||||
expect(graphqlError.extensions.subCode).toBe(
|
||||
AgentExceptionCode.API_KEY_NOT_CONFIGURED,
|
||||
);
|
||||
expect(graphqlError.extensions.userFriendlyMessage).toBeDefined();
|
||||
});
|
||||
});
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
ConflictError,
|
||||
ForbiddenError,
|
||||
InternalServerError,
|
||||
NotFoundError,
|
||||
UserInputError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import {
|
||||
AgentException,
|
||||
AgentExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
|
||||
|
||||
export const agentGraphqlApiExceptionHandler = (error: Error) => {
|
||||
if (error instanceof AgentException) {
|
||||
switch (error.code) {
|
||||
case AgentExceptionCode.AGENT_NOT_FOUND:
|
||||
case AgentExceptionCode.ROLE_NOT_FOUND:
|
||||
throw new NotFoundError(error);
|
||||
case AgentExceptionCode.INVALID_AGENT_INPUT:
|
||||
throw new UserInputError(error);
|
||||
case AgentExceptionCode.AGENT_ALREADY_EXISTS:
|
||||
throw new ConflictError(error);
|
||||
case AgentExceptionCode.AGENT_IS_STANDARD:
|
||||
case AgentExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS:
|
||||
throw new ForbiddenError(error);
|
||||
case AgentExceptionCode.AGENT_EXECUTION_FAILED:
|
||||
case AgentExceptionCode.API_KEY_NOT_CONFIGURED:
|
||||
case AgentExceptionCode.USER_WORKSPACE_ID_NOT_FOUND:
|
||||
throw new InternalServerError(error);
|
||||
default: {
|
||||
return assertUnreachable(error.code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
};
|
||||
+6
-10
@@ -7,9 +7,9 @@ import {
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
AgentException,
|
||||
AgentExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
|
||||
AiException,
|
||||
AiExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai.exception';
|
||||
import { type UpdateAgentInput } from 'src/engine/metadata-modules/ai/ai-agent/dtos/update-agent.input';
|
||||
import { FLAT_AGENT_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-agent/constants/flat-agent-editable-properties.constant';
|
||||
import { type FlatAgentMaps } from 'src/engine/metadata-modules/flat-agent/types/flat-agent-maps.type';
|
||||
@@ -115,13 +115,9 @@ export const fromUpdateAgentInputToFlatAgentToUpdate = ({
|
||||
});
|
||||
|
||||
if (!isDefined(existingFlatAgent)) {
|
||||
throw new AgentException(
|
||||
'Agent not found',
|
||||
AgentExceptionCode.AGENT_NOT_FOUND,
|
||||
{
|
||||
userFriendlyMessage: msg`The agent you are looking for could not be found. It may have been deleted or you may not have access to it.`,
|
||||
},
|
||||
);
|
||||
throw new AiException('Agent not found', AiExceptionCode.AGENT_NOT_FOUND, {
|
||||
userFriendlyMessage: msg`The agent you are looking for could not be found. It may have been deleted or you may not have access to it.`,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedEditableAgentProperties = extractAndSanitizeObjectStringFields(
|
||||
|
||||
@@ -25,7 +25,7 @@ import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.g
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { AiAgentExecutionModule } from 'src/engine/metadata-modules/ai/ai-agent-execution/ai-agent-execution.module';
|
||||
import { AiBillingModule } from 'src/engine/metadata-modules/ai/ai-billing/ai-billing.module';
|
||||
import { AgentGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/ai/ai-agent/interceptors/agent-graphql-api-exception.interceptor';
|
||||
import { AiGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/ai/interceptors/ai-graphql-api-exception.interceptor';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { SkillModule } from 'src/engine/metadata-modules/skill/skill.module';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
@@ -115,7 +115,7 @@ import { SystemPromptBuilderService } from './services/system-prompt-builder.ser
|
||||
MessagePruningService,
|
||||
StreamAgentChatJob,
|
||||
SystemPromptBuilderService,
|
||||
AgentGraphqlApiExceptionInterceptor,
|
||||
AiGraphqlApiExceptionInterceptor,
|
||||
],
|
||||
exports: [
|
||||
AgentChatService,
|
||||
|
||||
+7
-7
@@ -16,10 +16,10 @@ import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.g
|
||||
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import {
|
||||
AgentException,
|
||||
AgentExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
|
||||
import { AgentGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/ai/ai-agent/interceptors/agent-graphql-api-exception.interceptor';
|
||||
AiException,
|
||||
AiExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai.exception';
|
||||
import { AiGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/ai/interceptors/ai-graphql-api-exception.interceptor';
|
||||
import { AgentChatEventDTO } from 'src/engine/metadata-modules/ai/ai-chat/dtos/agent-chat-event.dto';
|
||||
import { AgentChatThreadEntity } from 'src/engine/metadata-modules/ai/ai-chat/entities/agent-chat-thread.entity';
|
||||
import { SubscriptionService } from 'src/engine/subscriptions/subscription.service';
|
||||
@@ -27,7 +27,7 @@ import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
@MetadataResolver()
|
||||
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
|
||||
@UseInterceptors(AgentGraphqlApiExceptionInterceptor)
|
||||
@UseInterceptors(AiGraphqlApiExceptionInterceptor)
|
||||
export class AgentChatSubscriptionResolver {
|
||||
constructor(
|
||||
private readonly subscriptionService: SubscriptionService,
|
||||
@@ -56,9 +56,9 @@ export class AgentChatSubscriptionResolver {
|
||||
});
|
||||
|
||||
if (!isDefined(thread)) {
|
||||
throw new AgentException(
|
||||
throw new AiException(
|
||||
'Thread not found',
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
AiExceptionCode.THREAD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+13
-13
@@ -36,10 +36,10 @@ import {
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import {
|
||||
AgentException,
|
||||
AgentExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
|
||||
import { AgentGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/ai/ai-agent/interceptors/agent-graphql-api-exception.interceptor';
|
||||
AiException,
|
||||
AiExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai.exception';
|
||||
import { AiGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/ai/interceptors/ai-graphql-api-exception.interceptor';
|
||||
import { type BrowsingContextType } from 'src/engine/metadata-modules/ai/ai-agent/types/browsingContext.type';
|
||||
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';
|
||||
@@ -59,7 +59,7 @@ import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models
|
||||
FeatureFlagGuard,
|
||||
SettingsPermissionGuard(PermissionFlagType.AI),
|
||||
)
|
||||
@UseInterceptors(AgentGraphqlApiExceptionInterceptor)
|
||||
@UseInterceptors(AiGraphqlApiExceptionInterceptor)
|
||||
@MetadataResolver(() => AgentChatThreadDTO)
|
||||
export class AgentChatResolver {
|
||||
constructor(
|
||||
@@ -135,9 +135,9 @@ export class AgentChatResolver {
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<SendChatMessageResultDTO> {
|
||||
if (this.aiModelRegistryService.getAvailableModels().length === 0) {
|
||||
throw new AgentException(
|
||||
throw new AiException(
|
||||
'No AI models are available. Configure at least one AI provider.',
|
||||
AgentExceptionCode.API_KEY_NOT_CONFIGURED,
|
||||
AiExceptionCode.API_KEY_NOT_CONFIGURED,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -167,9 +167,9 @@ export class AgentChatResolver {
|
||||
});
|
||||
|
||||
if (!isDefined(thread)) {
|
||||
throw new AgentException(
|
||||
throw new AiException(
|
||||
'Thread not found',
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
AiExceptionCode.THREAD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -245,9 +245,9 @@ export class AgentChatResolver {
|
||||
const message = await this.agentChatService.findQueuedMessage(messageId);
|
||||
|
||||
if (!isDefined(message)) {
|
||||
throw new AgentException(
|
||||
throw new AiException(
|
||||
'Queued message not found',
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
AiExceptionCode.MESSAGE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -256,9 +256,9 @@ export class AgentChatResolver {
|
||||
});
|
||||
|
||||
if (!isDefined(thread)) {
|
||||
throw new AgentException(
|
||||
throw new AiException(
|
||||
'Thread not found',
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
AiExceptionCode.THREAD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -22,9 +22,9 @@ import {
|
||||
} from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message.entity';
|
||||
import { mapDBPartsToUIMessageParts } from 'src/engine/metadata-modules/ai/ai-agent-execution/utils/mapDBPartsToUIMessageParts';
|
||||
import {
|
||||
AgentException,
|
||||
AgentExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
|
||||
AiException,
|
||||
AiExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai.exception';
|
||||
import { type BrowsingContextType } from 'src/engine/metadata-modules/ai/ai-agent/types/browsingContext.type';
|
||||
import { AgentChatThreadEntity } from 'src/engine/metadata-modules/ai/ai-chat/entities/agent-chat-thread.entity';
|
||||
import { STREAM_AGENT_CHAT_JOB_NAME } from 'src/engine/metadata-modules/ai/ai-chat/jobs/stream-agent-chat-job-name.constant';
|
||||
@@ -77,9 +77,9 @@ export class AgentChatStreamingService {
|
||||
});
|
||||
|
||||
if (!thread) {
|
||||
throw new AgentException(
|
||||
throw new AiException(
|
||||
'Thread not found',
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
AiExceptionCode.THREAD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+7
-7
@@ -17,9 +17,9 @@ import {
|
||||
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/ai-agent/agent.exception';
|
||||
AiException,
|
||||
AiExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai.exception';
|
||||
import { AgentChatThreadEntity } from 'src/engine/metadata-modules/ai/ai-chat/entities/agent-chat-thread.entity';
|
||||
import { WorkspaceEventBroadcaster } from 'src/engine/subscriptions/workspace-event-broadcaster/workspace-event-broadcaster.service';
|
||||
|
||||
@@ -96,9 +96,9 @@ export class AgentChatService {
|
||||
});
|
||||
|
||||
if (!thread) {
|
||||
throw new AgentException(
|
||||
throw new AiException(
|
||||
'Thread not found',
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
AiExceptionCode.THREAD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -179,9 +179,9 @@ export class AgentChatService {
|
||||
});
|
||||
|
||||
if (!thread) {
|
||||
throw new AgentException(
|
||||
throw new AiException(
|
||||
'Thread not found',
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
AiExceptionCode.THREAD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+7
-7
@@ -10,16 +10,16 @@ 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 {
|
||||
AgentException,
|
||||
AgentExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
|
||||
import { AgentRestApiExceptionFilter } from 'src/engine/metadata-modules/ai/ai-agent/filters/agent-api-exception.filter';
|
||||
AiException,
|
||||
AiExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai.exception';
|
||||
import { AiRestApiExceptionFilter } from 'src/engine/metadata-modules/ai/filters/ai-api-exception.filter';
|
||||
import { GenerateTextInput } from 'src/engine/metadata-modules/ai/ai-generate-text/dtos/generate-text-input.dto';
|
||||
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
|
||||
|
||||
@Controller('rest/ai')
|
||||
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
|
||||
@UseFilters(AgentRestApiExceptionFilter, RestApiExceptionFilter)
|
||||
@UseFilters(AiRestApiExceptionFilter, RestApiExceptionFilter)
|
||||
export class AiGenerateTextController {
|
||||
constructor(
|
||||
private readonly aiModelRegistryService: AiModelRegistryService,
|
||||
@@ -32,9 +32,9 @@ export class AiGenerateTextController {
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
) {
|
||||
if (this.aiModelRegistryService.getAvailableModels().length === 0) {
|
||||
throw new AgentException(
|
||||
throw new AiException(
|
||||
'No AI models are available. Please configure at least one AI provider API key.',
|
||||
AgentExceptionCode.API_KEY_NOT_CONFIGURED,
|
||||
AiExceptionCode.API_KEY_NOT_CONFIGURED,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+15
-15
@@ -8,9 +8,9 @@ import { ConfigGroupHashService } from 'src/engine/core-modules/twenty-config/se
|
||||
import { AiModelRole } from 'src/engine/metadata-modules/ai/ai-models/types/ai-model-role.enum';
|
||||
|
||||
import {
|
||||
AgentException,
|
||||
AgentExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
|
||||
AiException,
|
||||
AiExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai.exception';
|
||||
import { AiModelPreferencesService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-preferences.service';
|
||||
import { ProviderConfigService } from 'src/engine/metadata-modules/ai/ai-models/services/provider-config.service';
|
||||
import { SdkProviderFactoryService } from 'src/engine/metadata-modules/ai/ai-models/services/sdk-provider-factory.service';
|
||||
@@ -219,9 +219,9 @@ export class AiModelRegistryService {
|
||||
}
|
||||
|
||||
if (!model) {
|
||||
throw new AgentException(
|
||||
throw new AiException(
|
||||
'No AI models are available. Configure at least one AI provider.',
|
||||
AgentExceptionCode.API_KEY_NOT_CONFIGURED,
|
||||
AiExceptionCode.API_KEY_NOT_CONFIGURED,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -255,9 +255,9 @@ export class AiModelRegistryService {
|
||||
return this.createDefaultConfigForCustomModel(registeredModel);
|
||||
}
|
||||
|
||||
throw new AgentException(
|
||||
throw new AiException(
|
||||
`Model with ID ${modelId} not found`,
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
AiExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -296,9 +296,9 @@ export class AiModelRegistryService {
|
||||
workspace: WorkspaceModelAvailabilitySettings,
|
||||
): void {
|
||||
if (!this.isModelAdminAllowed(modelId)) {
|
||||
throw new AgentException(
|
||||
throw new AiException(
|
||||
'The selected model has been disabled by the administrator.',
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
AiExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -309,9 +309,9 @@ export class AiModelRegistryService {
|
||||
this.getRecommendedModelIds(),
|
||||
)
|
||||
) {
|
||||
throw new AgentException(
|
||||
throw new AiException(
|
||||
'The selected model is not available in this workspace.',
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
AiExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -386,9 +386,9 @@ export class AiModelRegistryService {
|
||||
this.ensureFresh();
|
||||
|
||||
if (!this.providerModelDefCache.has(modelId)) {
|
||||
throw new AgentException(
|
||||
throw new AiException(
|
||||
`Cannot update model "${modelId}": not found in registry`,
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
AiExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -409,9 +409,9 @@ export class AiModelRegistryService {
|
||||
const registeredModel = this.getModel(aiModel.modelId);
|
||||
|
||||
if (!registeredModel) {
|
||||
throw new AgentException(
|
||||
throw new AiException(
|
||||
`Model ${aiModel.modelId} not found in registry. Check that the corresponding AI provider is configured.`,
|
||||
AgentExceptionCode.API_KEY_NOT_CONFIGURED,
|
||||
AiExceptionCode.API_KEY_NOT_CONFIGURED,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+29
-23
@@ -4,52 +4,58 @@ import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export enum AgentExceptionCode {
|
||||
export enum AiExceptionCode {
|
||||
AGENT_NOT_FOUND = 'AGENT_NOT_FOUND',
|
||||
AGENT_ALREADY_EXISTS = 'AGENT_ALREADY_EXISTS',
|
||||
AGENT_IS_STANDARD = 'AGENT_IS_STANDARD',
|
||||
AGENT_EXECUTION_FAILED = 'AGENT_EXECUTION_FAILED',
|
||||
INVALID_AGENT_INPUT = 'INVALID_AGENT_INPUT',
|
||||
THREAD_NOT_FOUND = 'THREAD_NOT_FOUND',
|
||||
MESSAGE_NOT_FOUND = 'MESSAGE_NOT_FOUND',
|
||||
API_KEY_NOT_CONFIGURED = 'API_KEY_NOT_CONFIGURED',
|
||||
USER_WORKSPACE_ID_NOT_FOUND = 'USER_WORKSPACE_ID_NOT_FOUND',
|
||||
ROLE_NOT_FOUND = 'ROLE_NOT_FOUND',
|
||||
ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS = 'ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS',
|
||||
INVALID_AGENT_INPUT = 'INVALID_AGENT_INPUT',
|
||||
AGENT_ALREADY_EXISTS = 'AGENT_ALREADY_EXISTS',
|
||||
AGENT_IS_STANDARD = 'AGENT_IS_STANDARD',
|
||||
}
|
||||
|
||||
const getAgentExceptionUserFriendlyMessage = (code: AgentExceptionCode) => {
|
||||
const getAiExceptionUserFriendlyMessage = (code: AiExceptionCode) => {
|
||||
switch (code) {
|
||||
case AgentExceptionCode.AGENT_NOT_FOUND:
|
||||
case AiExceptionCode.AGENT_NOT_FOUND:
|
||||
return msg`Agent not found.`;
|
||||
case AgentExceptionCode.AGENT_EXECUTION_FAILED:
|
||||
return msg`Agent execution failed.`;
|
||||
case AgentExceptionCode.API_KEY_NOT_CONFIGURED:
|
||||
return msg`API key is not configured.`;
|
||||
case AgentExceptionCode.USER_WORKSPACE_ID_NOT_FOUND:
|
||||
return msg`User workspace not found.`;
|
||||
case AgentExceptionCode.ROLE_NOT_FOUND:
|
||||
return msg`Role not found.`;
|
||||
case AgentExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS:
|
||||
return msg`This role cannot be assigned to agents.`;
|
||||
case AgentExceptionCode.INVALID_AGENT_INPUT:
|
||||
return msg`Invalid agent input.`;
|
||||
case AgentExceptionCode.AGENT_ALREADY_EXISTS:
|
||||
case AiExceptionCode.AGENT_ALREADY_EXISTS:
|
||||
return msg`An agent with this name already exists.`;
|
||||
case AgentExceptionCode.AGENT_IS_STANDARD:
|
||||
case AiExceptionCode.AGENT_IS_STANDARD:
|
||||
return msg`Standard agents cannot be modified.`;
|
||||
case AiExceptionCode.AGENT_EXECUTION_FAILED:
|
||||
return msg`Agent execution failed.`;
|
||||
case AiExceptionCode.INVALID_AGENT_INPUT:
|
||||
return msg`Invalid agent input.`;
|
||||
case AiExceptionCode.THREAD_NOT_FOUND:
|
||||
return msg`Chat thread not found.`;
|
||||
case AiExceptionCode.MESSAGE_NOT_FOUND:
|
||||
return msg`Chat message not found.`;
|
||||
case AiExceptionCode.API_KEY_NOT_CONFIGURED:
|
||||
return msg`API key is not configured.`;
|
||||
case AiExceptionCode.USER_WORKSPACE_ID_NOT_FOUND:
|
||||
return msg`User workspace not found.`;
|
||||
case AiExceptionCode.ROLE_NOT_FOUND:
|
||||
return msg`Role not found.`;
|
||||
case AiExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS:
|
||||
return msg`This role cannot be assigned to agents.`;
|
||||
default:
|
||||
assertUnreachable(code);
|
||||
}
|
||||
};
|
||||
|
||||
export class AgentException extends CustomException<AgentExceptionCode> {
|
||||
export class AiException extends CustomException<AiExceptionCode> {
|
||||
constructor(
|
||||
message: string,
|
||||
code: AgentExceptionCode,
|
||||
code: AiExceptionCode,
|
||||
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
|
||||
) {
|
||||
super(message, code, {
|
||||
userFriendlyMessage:
|
||||
userFriendlyMessage ?? getAgentExceptionUserFriendlyMessage(code),
|
||||
userFriendlyMessage ?? getAiExceptionUserFriendlyMessage(code),
|
||||
});
|
||||
}
|
||||
}
|
||||
+17
-15
@@ -8,40 +8,42 @@ import type { Response } from 'express';
|
||||
|
||||
import { HttpExceptionHandlerService } from 'src/engine/core-modules/exception-handler/http-exception-handler.service';
|
||||
import {
|
||||
AgentException,
|
||||
AgentExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
|
||||
AiException,
|
||||
AiExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai.exception';
|
||||
|
||||
@Catch(AgentException)
|
||||
export class AgentRestApiExceptionFilter implements ExceptionFilter {
|
||||
@Catch(AiException)
|
||||
export class AiRestApiExceptionFilter implements ExceptionFilter {
|
||||
constructor(
|
||||
private readonly httpExceptionHandlerService: HttpExceptionHandlerService,
|
||||
) {}
|
||||
|
||||
catch(exception: AgentException, host: ArgumentsHost) {
|
||||
catch(exception: AiException, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
|
||||
switch (exception.code) {
|
||||
case AgentExceptionCode.AGENT_NOT_FOUND:
|
||||
case AgentExceptionCode.USER_WORKSPACE_ID_NOT_FOUND:
|
||||
case AgentExceptionCode.ROLE_NOT_FOUND:
|
||||
case AiExceptionCode.AGENT_NOT_FOUND:
|
||||
case AiExceptionCode.THREAD_NOT_FOUND:
|
||||
case AiExceptionCode.MESSAGE_NOT_FOUND:
|
||||
case AiExceptionCode.USER_WORKSPACE_ID_NOT_FOUND:
|
||||
case AiExceptionCode.ROLE_NOT_FOUND:
|
||||
return this.httpExceptionHandlerService.handleError(
|
||||
exception,
|
||||
response,
|
||||
404,
|
||||
);
|
||||
case AgentExceptionCode.API_KEY_NOT_CONFIGURED:
|
||||
case AiExceptionCode.API_KEY_NOT_CONFIGURED:
|
||||
return this.httpExceptionHandlerService.handleError(
|
||||
exception,
|
||||
response,
|
||||
503, // Service Unavailable - the AI service is not configured
|
||||
);
|
||||
case AgentExceptionCode.AGENT_EXECUTION_FAILED:
|
||||
case AgentExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS:
|
||||
case AgentExceptionCode.INVALID_AGENT_INPUT:
|
||||
case AgentExceptionCode.AGENT_ALREADY_EXISTS:
|
||||
case AgentExceptionCode.AGENT_IS_STANDARD:
|
||||
case AiExceptionCode.AGENT_EXECUTION_FAILED:
|
||||
case AiExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS:
|
||||
case AiExceptionCode.INVALID_AGENT_INPUT:
|
||||
case AiExceptionCode.AGENT_ALREADY_EXISTS:
|
||||
case AiExceptionCode.AGENT_IS_STANDARD:
|
||||
return this.httpExceptionHandlerService.handleError(
|
||||
exception,
|
||||
response,
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
type CallHandler,
|
||||
type ExecutionContext,
|
||||
Injectable,
|
||||
type NestInterceptor,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { type Observable, catchError } from 'rxjs';
|
||||
|
||||
import { aiGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/ai/utils/ai-graphql-api-exception-handler.util';
|
||||
|
||||
@Injectable()
|
||||
export class AiGraphqlApiExceptionInterceptor implements NestInterceptor {
|
||||
intercept(
|
||||
_context: ExecutionContext,
|
||||
next: CallHandler,
|
||||
): Observable<unknown> {
|
||||
return next.handle().pipe(catchError(aiGraphqlApiExceptionHandler));
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
AiException,
|
||||
AiExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai.exception';
|
||||
import { aiGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/ai/utils/ai-graphql-api-exception-handler.util';
|
||||
import {
|
||||
ErrorCode,
|
||||
type BaseGraphQLError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
|
||||
const catchGraphqlError = (error: Error): BaseGraphQLError => {
|
||||
try {
|
||||
aiGraphqlApiExceptionHandler(error);
|
||||
throw new Error('Expected aiGraphqlApiExceptionHandler to throw');
|
||||
} catch (graphqlError) {
|
||||
return graphqlError as BaseGraphQLError;
|
||||
}
|
||||
};
|
||||
|
||||
describe('aiGraphqlApiExceptionHandler', () => {
|
||||
it('maps API key configuration failures to INTERNAL_SERVER_ERROR with a subCode', () => {
|
||||
const error = new AiException(
|
||||
'No AI models are available',
|
||||
AiExceptionCode.API_KEY_NOT_CONFIGURED,
|
||||
);
|
||||
|
||||
const graphqlError = catchGraphqlError(error);
|
||||
|
||||
expect(graphqlError.extensions.code).toBe(ErrorCode.INTERNAL_SERVER_ERROR);
|
||||
expect(graphqlError.extensions.subCode).toBe(
|
||||
AiExceptionCode.API_KEY_NOT_CONFIGURED,
|
||||
);
|
||||
expect(graphqlError.extensions.userFriendlyMessage).toBeDefined();
|
||||
});
|
||||
|
||||
it('maps THREAD_NOT_FOUND to NOT_FOUND', () => {
|
||||
const error = new AiException(
|
||||
'Thread not found',
|
||||
AiExceptionCode.THREAD_NOT_FOUND,
|
||||
);
|
||||
|
||||
const graphqlError = catchGraphqlError(error);
|
||||
|
||||
expect(graphqlError.extensions.code).toBe(ErrorCode.NOT_FOUND);
|
||||
expect(graphqlError.extensions.subCode).toBe(
|
||||
AiExceptionCode.THREAD_NOT_FOUND,
|
||||
);
|
||||
});
|
||||
|
||||
it('maps MESSAGE_NOT_FOUND to NOT_FOUND', () => {
|
||||
const error = new AiException(
|
||||
'Message not found',
|
||||
AiExceptionCode.MESSAGE_NOT_FOUND,
|
||||
);
|
||||
|
||||
const graphqlError = catchGraphqlError(error);
|
||||
|
||||
expect(graphqlError.extensions.code).toBe(ErrorCode.NOT_FOUND);
|
||||
expect(graphqlError.extensions.subCode).toBe(
|
||||
AiExceptionCode.MESSAGE_NOT_FOUND,
|
||||
);
|
||||
});
|
||||
});
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
ConflictError,
|
||||
ForbiddenError,
|
||||
InternalServerError,
|
||||
NotFoundError,
|
||||
UserInputError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import {
|
||||
AiException,
|
||||
AiExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai.exception';
|
||||
|
||||
export const aiGraphqlApiExceptionHandler = (error: Error) => {
|
||||
if (error instanceof AiException) {
|
||||
switch (error.code) {
|
||||
case AiExceptionCode.AGENT_NOT_FOUND:
|
||||
case AiExceptionCode.THREAD_NOT_FOUND:
|
||||
case AiExceptionCode.MESSAGE_NOT_FOUND:
|
||||
case AiExceptionCode.ROLE_NOT_FOUND:
|
||||
throw new NotFoundError(error);
|
||||
case AiExceptionCode.INVALID_AGENT_INPUT:
|
||||
throw new UserInputError(error);
|
||||
case AiExceptionCode.AGENT_ALREADY_EXISTS:
|
||||
throw new ConflictError(error);
|
||||
case AiExceptionCode.AGENT_IS_STANDARD:
|
||||
case AiExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS:
|
||||
throw new ForbiddenError(error);
|
||||
case AiExceptionCode.AGENT_EXECUTION_FAILED:
|
||||
case AiExceptionCode.API_KEY_NOT_CONFIGURED:
|
||||
case AiExceptionCode.USER_WORKSPACE_ID_NOT_FOUND:
|
||||
throw new InternalServerError(error);
|
||||
default: {
|
||||
return assertUnreachable(error.code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
};
|
||||
@@ -28,9 +28,9 @@ 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/ai-agent-role/ai-agent-role.service';
|
||||
import {
|
||||
AgentException,
|
||||
AgentExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
|
||||
AiException,
|
||||
AiExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai.exception';
|
||||
import { AgentDTO } from 'src/engine/metadata-modules/ai/ai-agent/dtos/agent.dto';
|
||||
import { fromFlatAgentWithRoleIdToAgentDto } from 'src/engine/metadata-modules/flat-agent/utils/from-agent-entity-to-agent-dto.util';
|
||||
import { FieldPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/field-permission.dto';
|
||||
@@ -333,9 +333,9 @@ export class RoleResolver {
|
||||
flatApplicationMaps.byId[agentEntity.applicationId];
|
||||
|
||||
if (!isDefined(flatApplication)) {
|
||||
throw new AgentException(
|
||||
throw new AiException(
|
||||
`Application not found for agent ${agentEntity.id}`,
|
||||
AgentExceptionCode.AGENT_NOT_FOUND,
|
||||
AiExceptionCode.AGENT_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -4,7 +4,7 @@ import { msg, t } from '@lingui/core/macro';
|
||||
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { AgentExceptionCode } from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
|
||||
import { AiExceptionCode } from 'src/engine/metadata-modules/ai/ai.exception';
|
||||
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
|
||||
import { type UniversalFlatAgent } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-agent.type';
|
||||
import { belongsToTwentyStandardApp } from 'src/engine/metadata-modules/utils/belongs-to-twenty-standard-app.util';
|
||||
@@ -89,7 +89,7 @@ export class FlatAgentValidatorService {
|
||||
|
||||
if (!isDefined(existingAgent)) {
|
||||
validationResult.errors.push({
|
||||
code: AgentExceptionCode.AGENT_NOT_FOUND,
|
||||
code: AiExceptionCode.AGENT_NOT_FOUND,
|
||||
message: t`Agent not found`,
|
||||
userFriendlyMessage: msg`Agent not found`,
|
||||
});
|
||||
@@ -106,7 +106,7 @@ export class FlatAgentValidatorService {
|
||||
})
|
||||
) {
|
||||
validationResult.errors.push({
|
||||
code: AgentExceptionCode.AGENT_IS_STANDARD,
|
||||
code: AiExceptionCode.AGENT_IS_STANDARD,
|
||||
message: t`Cannot delete standard agent`,
|
||||
userFriendlyMessage: msg`Cannot delete standard agent`,
|
||||
});
|
||||
@@ -140,7 +140,7 @@ export class FlatAgentValidatorService {
|
||||
|
||||
if (!isDefined(fromFlatAgent)) {
|
||||
validationResult.errors.push({
|
||||
code: AgentExceptionCode.AGENT_NOT_FOUND,
|
||||
code: AiExceptionCode.AGENT_NOT_FOUND,
|
||||
message: t`Agent not found`,
|
||||
userFriendlyMessage: msg`Agent not found`,
|
||||
});
|
||||
@@ -157,7 +157,7 @@ export class FlatAgentValidatorService {
|
||||
})
|
||||
) {
|
||||
validationResult.errors.push({
|
||||
code: AgentExceptionCode.AGENT_IS_STANDARD,
|
||||
code: AiExceptionCode.AGENT_IS_STANDARD,
|
||||
message: t`Cannot update standard agent`,
|
||||
userFriendlyMessage: msg`Cannot update standard agent`,
|
||||
});
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
import { msg, t } from '@lingui/core/macro';
|
||||
|
||||
import { AgentExceptionCode } from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
|
||||
import { AiExceptionCode } from 'src/engine/metadata-modules/ai/ai.exception';
|
||||
import { type UniversalFlatAgent } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-agent.type';
|
||||
import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
|
||||
|
||||
@@ -10,12 +10,12 @@ export const validateAgentNameUniqueness = ({
|
||||
}: {
|
||||
name: string;
|
||||
existingFlatAgents: UniversalFlatAgent[];
|
||||
}): FlatEntityValidationError<AgentExceptionCode>[] => {
|
||||
const errors: FlatEntityValidationError<AgentExceptionCode>[] = [];
|
||||
}): FlatEntityValidationError<AiExceptionCode>[] => {
|
||||
const errors: FlatEntityValidationError<AiExceptionCode>[] = [];
|
||||
|
||||
if (existingFlatAgents.some((agent) => agent.name === name)) {
|
||||
errors.push({
|
||||
code: AgentExceptionCode.AGENT_ALREADY_EXISTS,
|
||||
code: AiExceptionCode.AGENT_ALREADY_EXISTS,
|
||||
message: t`Agent with name "${name}" already exists`,
|
||||
userFriendlyMessage: msg`An agent with this name already exists`,
|
||||
});
|
||||
|
||||
+6
-6
@@ -1,7 +1,7 @@
|
||||
import { msg, t } from '@lingui/core/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { AgentExceptionCode } from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
|
||||
import { AiExceptionCode } from 'src/engine/metadata-modules/ai/ai.exception';
|
||||
import { type UniversalFlatAgent } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-agent.type';
|
||||
import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
|
||||
|
||||
@@ -13,8 +13,8 @@ type ValidateAgentRequiredPropertiesArgs = {
|
||||
export const validateAgentRequiredProperties = ({
|
||||
flatAgent,
|
||||
updatedProperties,
|
||||
}: ValidateAgentRequiredPropertiesArgs): FlatEntityValidationError<AgentExceptionCode>[] => {
|
||||
const errors: FlatEntityValidationError<AgentExceptionCode>[] = [];
|
||||
}: ValidateAgentRequiredPropertiesArgs): FlatEntityValidationError<AiExceptionCode>[] => {
|
||||
const errors: FlatEntityValidationError<AiExceptionCode>[] = [];
|
||||
|
||||
// For updates, only validate properties that are being changed
|
||||
const isUpdate = updatedProperties !== undefined;
|
||||
@@ -24,7 +24,7 @@ export const validateAgentRequiredProperties = ({
|
||||
|
||||
if (shouldValidateLabel && !isNonEmptyString(flatAgent.label)) {
|
||||
errors.push({
|
||||
code: AgentExceptionCode.INVALID_AGENT_INPUT,
|
||||
code: AiExceptionCode.INVALID_AGENT_INPUT,
|
||||
message: t`Label cannot be empty`,
|
||||
userFriendlyMessage: msg`Label cannot be empty`,
|
||||
});
|
||||
@@ -32,7 +32,7 @@ export const validateAgentRequiredProperties = ({
|
||||
|
||||
if (shouldValidatePrompt && !isNonEmptyString(flatAgent.prompt)) {
|
||||
errors.push({
|
||||
code: AgentExceptionCode.INVALID_AGENT_INPUT,
|
||||
code: AiExceptionCode.INVALID_AGENT_INPUT,
|
||||
message: t`Prompt cannot be empty`,
|
||||
userFriendlyMessage: msg`Prompt cannot be empty`,
|
||||
});
|
||||
@@ -40,7 +40,7 @@ export const validateAgentRequiredProperties = ({
|
||||
|
||||
if (shouldValidateModelId && !isNonEmptyString(flatAgent.modelId)) {
|
||||
errors.push({
|
||||
code: AgentExceptionCode.INVALID_AGENT_INPUT,
|
||||
code: AiExceptionCode.INVALID_AGENT_INPUT,
|
||||
message: t`Model ID cannot be empty`,
|
||||
userFriendlyMessage: msg`Model ID cannot be empty`,
|
||||
});
|
||||
|
||||
+6
-6
@@ -1,7 +1,7 @@
|
||||
import { msg, t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { AgentExceptionCode } from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
|
||||
import { AiExceptionCode } from 'src/engine/metadata-modules/ai/ai.exception';
|
||||
import {
|
||||
type AgentJsonResponseFormat,
|
||||
type AgentResponseFormat,
|
||||
@@ -12,13 +12,13 @@ export const validateAgentResponseFormat = ({
|
||||
responseFormat,
|
||||
}: {
|
||||
responseFormat: AgentResponseFormat;
|
||||
}): FlatEntityValidationError<AgentExceptionCode>[] => {
|
||||
const errors: FlatEntityValidationError<AgentExceptionCode>[] = [];
|
||||
}): FlatEntityValidationError<AiExceptionCode>[] => {
|
||||
const errors: FlatEntityValidationError<AiExceptionCode>[] = [];
|
||||
const type = responseFormat.type;
|
||||
|
||||
if (type !== 'text' && type !== 'json') {
|
||||
errors.push({
|
||||
code: AgentExceptionCode.INVALID_AGENT_INPUT,
|
||||
code: AiExceptionCode.INVALID_AGENT_INPUT,
|
||||
message: t`Response format type must be either "text" or "json"`,
|
||||
userFriendlyMessage: msg`Invalid response format type`,
|
||||
});
|
||||
@@ -26,7 +26,7 @@ export const validateAgentResponseFormat = ({
|
||||
|
||||
if (type === 'json' && !isDefined(responseFormat.schema)) {
|
||||
errors.push({
|
||||
code: AgentExceptionCode.INVALID_AGENT_INPUT,
|
||||
code: AiExceptionCode.INVALID_AGENT_INPUT,
|
||||
message: t`Response format with type "json" must include a schema`,
|
||||
userFriendlyMessage: msg`JSON response format requires a schema`,
|
||||
});
|
||||
@@ -37,7 +37,7 @@ export const validateAgentResponseFormat = ({
|
||||
isDefined((responseFormat as unknown as AgentJsonResponseFormat).schema)
|
||||
) {
|
||||
errors.push({
|
||||
code: AgentExceptionCode.INVALID_AGENT_INPUT,
|
||||
code: AiExceptionCode.INVALID_AGENT_INPUT,
|
||||
message: t`Response format with type "text" should not include a schema`,
|
||||
userFriendlyMessage: msg`Text response format should not have a schema`,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user