Show tool execution messages in AI agent chat (#13117)
https://github.com/user-attachments/assets/c0a42726-50ac-496e-a993-9d6076a84a6a --------- Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
@ -0,0 +1,55 @@
|
||||
import { getAIModelsWithAuto } from 'src/engine/core-modules/ai/utils/get-ai-models-with-auto.util';
|
||||
import { getDefaultModelConfig } from 'src/engine/core-modules/ai/utils/get-default-model-config.util';
|
||||
|
||||
import { AI_MODELS, DEFAULT_MODEL_ID, ModelProvider } from './ai-models.const';
|
||||
|
||||
describe('AI_MODELS', () => {
|
||||
it('should contain all expected models', () => {
|
||||
expect(AI_MODELS).toHaveLength(6);
|
||||
expect(AI_MODELS.map((model) => model.modelId)).toEqual([
|
||||
'gpt-4o',
|
||||
'gpt-4o-mini',
|
||||
'gpt-4-turbo',
|
||||
'claude-opus-4-20250514',
|
||||
'claude-sonnet-4-20250514',
|
||||
'claude-3-5-haiku-20241022',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should have the default model as the first model', () => {
|
||||
const DEFAULT_MODEL = AI_MODELS.find(
|
||||
(model) => model.modelId === DEFAULT_MODEL_ID,
|
||||
);
|
||||
|
||||
expect(DEFAULT_MODEL).toBeDefined();
|
||||
expect(DEFAULT_MODEL?.modelId).toBe(DEFAULT_MODEL_ID);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAIModelsWithAuto', () => {
|
||||
it('should return AI_MODELS with auto model prepended', () => {
|
||||
const ORIGINAL_MODELS = AI_MODELS;
|
||||
const MODELS_WITH_AUTO = getAIModelsWithAuto();
|
||||
|
||||
expect(MODELS_WITH_AUTO).toHaveLength(ORIGINAL_MODELS.length + 1);
|
||||
expect(MODELS_WITH_AUTO[0].modelId).toBe('auto');
|
||||
expect(MODELS_WITH_AUTO[0].label).toBe('Auto');
|
||||
expect(MODELS_WITH_AUTO[0].provider).toBe(ModelProvider.NONE);
|
||||
|
||||
// Check that the rest of the models are the same
|
||||
expect(MODELS_WITH_AUTO.slice(1)).toEqual(ORIGINAL_MODELS);
|
||||
});
|
||||
|
||||
it('should have auto model with default model costs', () => {
|
||||
const MODELS_WITH_AUTO = getAIModelsWithAuto();
|
||||
const AUTO_MODEL = MODELS_WITH_AUTO[0];
|
||||
const DEFAULT_MODEL = getDefaultModelConfig();
|
||||
|
||||
expect(AUTO_MODEL.inputCostPer1kTokensInCents).toBe(
|
||||
DEFAULT_MODEL.inputCostPer1kTokensInCents,
|
||||
);
|
||||
expect(AUTO_MODEL.outputCostPer1kTokensInCents).toBe(
|
||||
DEFAULT_MODEL.outputCostPer1kTokensInCents,
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -1,9 +1,11 @@
|
||||
export enum ModelProvider {
|
||||
NONE = 'none',
|
||||
OPENAI = 'openai',
|
||||
ANTHROPIC = 'anthropic',
|
||||
}
|
||||
|
||||
export type ModelId =
|
||||
| 'auto'
|
||||
| 'gpt-4o'
|
||||
| 'gpt-4o-mini'
|
||||
| 'gpt-4-turbo'
|
||||
@ -11,6 +13,8 @@ export type ModelId =
|
||||
| 'claude-sonnet-4-20250514'
|
||||
| 'claude-3-5-haiku-20241022';
|
||||
|
||||
export const DEFAULT_MODEL_ID: ModelId = 'gpt-4o';
|
||||
|
||||
export interface AIModelConfig {
|
||||
modelId: ModelId;
|
||||
label: string;
|
||||
|
||||
@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { ModelId } from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||
import { DOLLAR_TO_CREDIT_MULTIPLIER } from 'src/engine/core-modules/ai/constants/dollar-to-credit-multiplier';
|
||||
import { getAIModelById } from 'src/engine/core-modules/ai/utils/get-ai-model-by-id';
|
||||
import { getAIModelById } from 'src/engine/core-modules/ai/utils/get-ai-model-by-id.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 { BillingUsageEvent } from 'src/engine/core-modules/billing/types/billing-usage-event.type';
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { ToolSet } from 'ai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { JsonRpc } from 'src/engine/core-modules/ai/dtos/json-rpc';
|
||||
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
|
||||
import { wrapJsonRpcResponse } from 'src/engine/core-modules/ai/utils/wrap-jsonrpc-response.util';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
|
||||
import { JsonRpc } from 'src/engine/core-modules/ai/dtos/json-rpc';
|
||||
import { wrapJsonRpcResponse } from 'src/engine/core-modules/ai/utils/wrap-jsonrpc-response';
|
||||
import { ADMIN_ROLE_LABEL } from 'src/engine/metadata-modules/permissions/constants/admin-role-label.constants';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
|
||||
@Injectable()
|
||||
export class McpService {
|
||||
|
||||
@ -1,30 +1,31 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { ToolSet } from 'ai';
|
||||
import {
|
||||
ILike,
|
||||
In,
|
||||
IsNull,
|
||||
LessThan,
|
||||
LessThanOrEqual,
|
||||
Like,
|
||||
ILike,
|
||||
MoreThan,
|
||||
MoreThanOrEqual,
|
||||
Not,
|
||||
} from 'typeorm';
|
||||
import { ToolSet } from 'ai';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
import {
|
||||
generateBulkDeleteToolSchema,
|
||||
generateFindOneToolSchema,
|
||||
generateFindToolSchema,
|
||||
generateSoftDeleteToolSchema,
|
||||
getRecordInputSchema,
|
||||
} from 'src/engine/metadata-modules/agent/utils/agent-tool-schema.utils';
|
||||
import { isWorkflowRelatedObject } from 'src/engine/metadata-modules/agent/utils/is-workflow-related-object.util';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.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';
|
||||
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||
|
||||
@Injectable()
|
||||
export class ToolService {
|
||||
@ -72,7 +73,7 @@ export class ToolService {
|
||||
execute: async (parameters) => {
|
||||
return this.createRecord(
|
||||
objectMetadata.nameSingular,
|
||||
parameters,
|
||||
parameters.input,
|
||||
workspaceId,
|
||||
roleId,
|
||||
);
|
||||
@ -85,7 +86,7 @@ export class ToolService {
|
||||
execute: async (parameters) => {
|
||||
return this.updateRecord(
|
||||
objectMetadata.nameSingular,
|
||||
parameters,
|
||||
parameters.input,
|
||||
workspaceId,
|
||||
roleId,
|
||||
);
|
||||
@ -100,7 +101,7 @@ export class ToolService {
|
||||
execute: async (parameters) => {
|
||||
return this.findRecords(
|
||||
objectMetadata.nameSingular,
|
||||
parameters,
|
||||
parameters.input,
|
||||
workspaceId,
|
||||
roleId,
|
||||
);
|
||||
@ -109,15 +110,11 @@ export class ToolService {
|
||||
|
||||
tools[`find_one_${objectMetadata.nameSingular}`] = {
|
||||
description: `Retrieve a single ${objectMetadata.labelSingular} record by its unique ID. Use this when you know the exact record ID and need the complete record data. Returns the full record or an error if not found.`,
|
||||
parameters: z.object({
|
||||
id: z
|
||||
.string()
|
||||
.describe('The unique UUID of the record to retrieve'),
|
||||
}),
|
||||
parameters: generateFindOneToolSchema(),
|
||||
execute: async (parameters) => {
|
||||
return this.findOneRecord(
|
||||
objectMetadata.nameSingular,
|
||||
parameters,
|
||||
parameters.input,
|
||||
workspaceId,
|
||||
roleId,
|
||||
);
|
||||
@ -128,15 +125,11 @@ export class ToolService {
|
||||
if (objectPermission.canSoftDelete) {
|
||||
tools[`soft_delete_${objectMetadata.nameSingular}`] = {
|
||||
description: `Soft delete a ${objectMetadata.labelSingular} record by marking it as deleted. The record remains in the database but is hidden from normal queries. This is reversible and preserves all data. Use this for temporary removal.`,
|
||||
parameters: z.object({
|
||||
id: z
|
||||
.string()
|
||||
.describe('The unique UUID of the record to soft delete'),
|
||||
}),
|
||||
parameters: generateSoftDeleteToolSchema(),
|
||||
execute: async (parameters) => {
|
||||
return this.softDeleteRecord(
|
||||
objectMetadata.nameSingular,
|
||||
parameters,
|
||||
parameters.input,
|
||||
workspaceId,
|
||||
roleId,
|
||||
);
|
||||
@ -149,7 +142,7 @@ export class ToolService {
|
||||
execute: async (parameters) => {
|
||||
return this.softDeleteManyRecords(
|
||||
objectMetadata.nameSingular,
|
||||
parameters,
|
||||
parameters.input,
|
||||
workspaceId,
|
||||
roleId,
|
||||
);
|
||||
|
||||
@ -4,4 +4,5 @@
|
||||
* @param cents - Cost in cents (real cost)
|
||||
* @returns Cost in credits (end-user cost)
|
||||
*/
|
||||
export const convertCentsToCredits = (cents: number): number => cents * 10;
|
||||
export const convertCentsToBillingCredits = (cents: number): number =>
|
||||
cents * 10;
|
||||
@ -1,9 +0,0 @@
|
||||
import {
|
||||
AI_MODELS,
|
||||
AIModelConfig,
|
||||
ModelId,
|
||||
} from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||
|
||||
export const getAIModelById = (modelId: ModelId): AIModelConfig | undefined => {
|
||||
return AI_MODELS.find((model) => model.modelId === modelId);
|
||||
};
|
||||
@ -0,0 +1,7 @@
|
||||
import { AIModelConfig } from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||
|
||||
import { getEffectiveModelConfig } from './get-effective-model-config.util';
|
||||
|
||||
export const getAIModelById = (modelId: string): AIModelConfig => {
|
||||
return getEffectiveModelConfig(modelId);
|
||||
};
|
||||
@ -0,0 +1,35 @@
|
||||
import {
|
||||
AI_MODELS,
|
||||
ModelProvider,
|
||||
} from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||
|
||||
import { getAIModelsWithAuto } from './get-ai-models-with-auto.util';
|
||||
import { getDefaultModelConfig } from './get-default-model-config.util';
|
||||
|
||||
describe('getAIModelsWithAuto', () => {
|
||||
it('should return AI_MODELS with auto model prepended', () => {
|
||||
const ORIGINAL_MODELS = AI_MODELS;
|
||||
const MODELS_WITH_AUTO = getAIModelsWithAuto();
|
||||
|
||||
expect(MODELS_WITH_AUTO).toHaveLength(ORIGINAL_MODELS.length + 1);
|
||||
expect(MODELS_WITH_AUTO[0].modelId).toBe('auto');
|
||||
expect(MODELS_WITH_AUTO[0].label).toBe('Auto');
|
||||
expect(MODELS_WITH_AUTO[0].provider).toBe(ModelProvider.NONE);
|
||||
|
||||
// Check that the rest of the models are the same
|
||||
expect(MODELS_WITH_AUTO.slice(1)).toEqual(ORIGINAL_MODELS);
|
||||
});
|
||||
|
||||
it('should have auto model with default model costs', () => {
|
||||
const MODELS_WITH_AUTO = getAIModelsWithAuto();
|
||||
const AUTO_MODEL = MODELS_WITH_AUTO[0];
|
||||
const DEFAULT_MODEL = getDefaultModelConfig();
|
||||
|
||||
expect(AUTO_MODEL.inputCostPer1kTokensInCents).toBe(
|
||||
DEFAULT_MODEL.inputCostPer1kTokensInCents,
|
||||
);
|
||||
expect(AUTO_MODEL.outputCostPer1kTokensInCents).toBe(
|
||||
DEFAULT_MODEL.outputCostPer1kTokensInCents,
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,22 @@
|
||||
import {
|
||||
AI_MODELS,
|
||||
AIModelConfig,
|
||||
ModelProvider,
|
||||
} from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||
|
||||
import { getDefaultModelConfig } from './get-default-model-config.util';
|
||||
|
||||
export const getAIModelsWithAuto = (): AIModelConfig[] => {
|
||||
return [
|
||||
{
|
||||
modelId: 'auto',
|
||||
label: 'Auto',
|
||||
provider: ModelProvider.NONE,
|
||||
inputCostPer1kTokensInCents:
|
||||
getDefaultModelConfig().inputCostPer1kTokensInCents,
|
||||
outputCostPer1kTokensInCents:
|
||||
getDefaultModelConfig().outputCostPer1kTokensInCents,
|
||||
},
|
||||
...AI_MODELS,
|
||||
];
|
||||
};
|
||||
@ -0,0 +1,29 @@
|
||||
import {
|
||||
AI_MODELS,
|
||||
DEFAULT_MODEL_ID,
|
||||
ModelProvider,
|
||||
} from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||
|
||||
import { getDefaultModelConfig } from './get-default-model-config.util';
|
||||
|
||||
describe('getDefaultModelConfig', () => {
|
||||
it('should return the configuration for the default model', () => {
|
||||
const result = getDefaultModelConfig();
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.modelId).toBe(DEFAULT_MODEL_ID);
|
||||
expect(result.provider).toBe(ModelProvider.OPENAI);
|
||||
});
|
||||
|
||||
it('should throw an error if default model is not found', () => {
|
||||
const originalFind = AI_MODELS.find;
|
||||
|
||||
AI_MODELS.find = jest.fn().mockReturnValue(undefined);
|
||||
|
||||
expect(() => getDefaultModelConfig()).toThrow(
|
||||
`Default model with ID ${DEFAULT_MODEL_ID} not found`,
|
||||
);
|
||||
|
||||
AI_MODELS.find = originalFind;
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,17 @@
|
||||
import {
|
||||
AI_MODELS,
|
||||
AIModelConfig,
|
||||
DEFAULT_MODEL_ID,
|
||||
} from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||
|
||||
export const getDefaultModelConfig = (): AIModelConfig => {
|
||||
const defaultModel = AI_MODELS.find(
|
||||
(model) => model.modelId === DEFAULT_MODEL_ID,
|
||||
);
|
||||
|
||||
if (!defaultModel) {
|
||||
throw new Error(`Default model with ID ${DEFAULT_MODEL_ID} not found`);
|
||||
}
|
||||
|
||||
return defaultModel;
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
import {
|
||||
DEFAULT_MODEL_ID,
|
||||
ModelProvider,
|
||||
} from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||
|
||||
import { getEffectiveModelConfig } from './get-effective-model-config.util';
|
||||
|
||||
describe('getEffectiveModelConfig', () => {
|
||||
it('should return default model config when modelId is "auto"', () => {
|
||||
const result = getEffectiveModelConfig('auto');
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.modelId).toBe(DEFAULT_MODEL_ID);
|
||||
expect(result.provider).toBe(ModelProvider.OPENAI);
|
||||
});
|
||||
|
||||
it('should return the correct model config for a specific model', () => {
|
||||
const result = getEffectiveModelConfig('gpt-4o');
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.modelId).toBe('gpt-4o');
|
||||
expect(result.provider).toBe(ModelProvider.OPENAI);
|
||||
});
|
||||
|
||||
it('should throw an error for non-existent model', () => {
|
||||
expect(() => getEffectiveModelConfig('non-existent-model' as any)).toThrow(
|
||||
`Model with ID non-existent-model not found`,
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,20 @@
|
||||
import {
|
||||
AI_MODELS,
|
||||
AIModelConfig,
|
||||
} from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||
|
||||
import { getDefaultModelConfig } from './get-default-model-config.util';
|
||||
|
||||
export const getEffectiveModelConfig = (modelId: string): AIModelConfig => {
|
||||
if (modelId === 'auto') {
|
||||
return getDefaultModelConfig();
|
||||
}
|
||||
|
||||
const model = AI_MODELS.find((model) => model.modelId === modelId);
|
||||
|
||||
if (!model) {
|
||||
throw new Error(`Model with ID ${modelId} not found`);
|
||||
}
|
||||
|
||||
return model;
|
||||
};
|
||||
@ -9,9 +9,12 @@ import { DomainManagerService } from 'src/engine/core-modules/domain-manager/ser
|
||||
import { PUBLIC_FEATURE_FLAGS } from 'src/engine/core-modules/feature-flag/constants/public-feature-flag.const';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
jest.mock('src/engine/core-modules/ai/constants/ai-models.const', () => ({
|
||||
AI_MODELS: [],
|
||||
}));
|
||||
jest.mock(
|
||||
'src/engine/core-modules/ai/utils/get-ai-models-with-auto.util',
|
||||
() => ({
|
||||
getAIModelsWithAuto: jest.fn(() => []),
|
||||
}),
|
||||
);
|
||||
|
||||
describe('ClientConfigService', () => {
|
||||
let service: ClientConfigService;
|
||||
|
||||
@ -3,11 +3,9 @@ import { Injectable } from '@nestjs/common';
|
||||
import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface';
|
||||
import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/support.interface';
|
||||
|
||||
import {
|
||||
AI_MODELS,
|
||||
ModelProvider,
|
||||
} from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||
import { convertCentsToCredits } from 'src/engine/core-modules/ai/utils/ai-cost.utils';
|
||||
import { ModelProvider } from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||
import { convertCentsToBillingCredits } from 'src/engine/core-modules/ai/utils/convert-cents-to-billing-credits.util';
|
||||
import { getAIModelsWithAuto } from 'src/engine/core-modules/ai/utils/get-ai-models-with-auto.util';
|
||||
import {
|
||||
ClientAIModelConfig,
|
||||
ClientConfig,
|
||||
@ -29,29 +27,32 @@ export class ClientConfigService {
|
||||
const openaiApiKey = this.twentyConfigService.get('OPENAI_API_KEY');
|
||||
const anthropicApiKey = this.twentyConfigService.get('ANTHROPIC_API_KEY');
|
||||
|
||||
const aiModels = AI_MODELS.reduce<ClientAIModelConfig[]>((acc, model) => {
|
||||
const isAvailable =
|
||||
(model.provider === ModelProvider.OPENAI && openaiApiKey) ||
|
||||
(model.provider === ModelProvider.ANTHROPIC && anthropicApiKey);
|
||||
const aiModels = getAIModelsWithAuto().reduce<ClientAIModelConfig[]>(
|
||||
(acc, model) => {
|
||||
const isAvailable =
|
||||
(model.provider === ModelProvider.OPENAI && openaiApiKey) ||
|
||||
(model.provider === ModelProvider.ANTHROPIC && anthropicApiKey);
|
||||
|
||||
if (!isAvailable) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc.push({
|
||||
modelId: model.modelId,
|
||||
label: model.label,
|
||||
provider: model.provider,
|
||||
inputCostPer1kTokensInCredits: convertCentsToBillingCredits(
|
||||
model.inputCostPer1kTokensInCents,
|
||||
),
|
||||
outputCostPer1kTokensInCredits: convertCentsToBillingCredits(
|
||||
model.outputCostPer1kTokensInCents,
|
||||
),
|
||||
});
|
||||
|
||||
if (!isAvailable) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc.push({
|
||||
modelId: model.modelId,
|
||||
label: model.label,
|
||||
provider: model.provider,
|
||||
inputCostPer1kTokensInCredits: convertCentsToCredits(
|
||||
model.inputCostPer1kTokensInCents,
|
||||
),
|
||||
outputCostPer1kTokensInCredits: convertCentsToCredits(
|
||||
model.outputCostPer1kTokensInCents,
|
||||
),
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const clientConfig: ClientConfig = {
|
||||
billing: {
|
||||
|
||||
@ -25,6 +25,7 @@ import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/worksp
|
||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { Webhook } from 'src/engine/core-modules/webhook/webhook.entity';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
|
||||
import { AgentDTO } from 'src/engine/metadata-modules/agent/dtos/agent.dto';
|
||||
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
|
||||
|
||||
registerEnumType(WorkspaceActivationStatus, {
|
||||
@ -171,12 +172,20 @@ export class Workspace {
|
||||
@Column({ default: false })
|
||||
isCustomDomainEnabled: boolean;
|
||||
|
||||
// TODO: set as non nullable
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
defaultRoleId: string | null;
|
||||
|
||||
@Field(() => RoleDTO, { nullable: true })
|
||||
defaultRole: RoleDTO | null;
|
||||
|
||||
// TODO: set as non nullable
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
defaultAgentId: string | null;
|
||||
|
||||
@Field(() => AgentDTO, { nullable: true })
|
||||
defaultAgent: AgentDTO | null;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
version: string | null;
|
||||
|
||||
@ -19,6 +19,7 @@ import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { WorkspaceWorkspaceMemberListener } from 'src/engine/core-modules/workspace/workspace-workspace-member.listener';
|
||||
import { WorkspaceResolver } from 'src/engine/core-modules/workspace/workspace.resolver';
|
||||
import { AgentModule } from 'src/engine/metadata-modules/agent/agent.module';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
|
||||
@ -57,6 +58,7 @@ import { WorkspaceService } from './services/workspace.service';
|
||||
WorkspaceCacheStorageModule,
|
||||
AuditModule,
|
||||
RoleModule,
|
||||
AgentModule,
|
||||
],
|
||||
services: [WorkspaceService],
|
||||
resolvers: workspaceAutoResolverOpts,
|
||||
|
||||
@ -56,6 +56,8 @@ import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
|
||||
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
|
||||
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
||||
import { AgentDTO } from 'src/engine/metadata-modules/agent/dtos/agent.dto';
|
||||
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
|
||||
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
|
||||
@ -92,6 +94,7 @@ export class WorkspaceResolver {
|
||||
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly roleService: RoleService,
|
||||
private readonly agentService: AgentService,
|
||||
@InjectRepository(BillingSubscription, 'core')
|
||||
private readonly billingSubscriptionRepository: Repository<BillingSubscription>,
|
||||
) {}
|
||||
@ -222,6 +225,29 @@ export class WorkspaceResolver {
|
||||
);
|
||||
}
|
||||
|
||||
@ResolveField(() => AgentDTO, { nullable: true })
|
||||
async defaultAgent(@Parent() workspace: Workspace): Promise<AgentDTO | null> {
|
||||
if (!workspace.defaultAgentId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const agent = await this.agentService.findOneAgent(
|
||||
workspace.defaultAgentId,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
// Convert roleId from null to undefined to match AgentDTO
|
||||
return {
|
||||
...agent,
|
||||
roleId: agent.roleId ?? undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
// If agent is not found, return null instead of throwing
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ResolveField(() => BillingSubscription, { nullable: true })
|
||||
async currentBillingSubscription(
|
||||
@Parent() workspace: Workspace,
|
||||
|
||||
Reference in New Issue
Block a user