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,23 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddDefaultAgentId1752070094777 implements MigrationInterface {
|
||||
name = 'AddDefaultAgentId1752070094777';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."workspace" ADD "defaultAgentId" uuid`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."agent" ALTER COLUMN "modelId" SET DEFAULT 'auto'`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."agent" ALTER COLUMN "modelId" DROP DEFAULT`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."workspace" DROP COLUMN "defaultAgentId"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AgentAsStandardMetadata1752088464449
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AgentAsStandardMetadata1752088464449';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."agent" ADD "label" character varying NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."agent" ADD "icon" character varying`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."agent" ADD "isCustom" boolean NOT NULL DEFAULT false`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."agent" ADD CONSTRAINT "IDX_AGENT_NAME_WORKSPACE_ID_UNIQUE" UNIQUE ("name", "workspaceId")`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."agent" DROP CONSTRAINT "IDX_AGENT_NAME_WORKSPACE_ID_UNIQUE"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."agent" DROP COLUMN "isCustom"`,
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "core"."agent" DROP COLUMN "icon"`);
|
||||
await queryRunner.query(`ALTER TABLE "core"."agent" DROP COLUMN "label"`);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -62,11 +62,33 @@ export class AgentChatController {
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
await this.agentStreamingService.streamAgentChat({
|
||||
threadId: body.threadId,
|
||||
userMessage: body.userMessage,
|
||||
userWorkspaceId,
|
||||
res,
|
||||
});
|
||||
try {
|
||||
await this.agentStreamingService.streamAgentChat({
|
||||
threadId: body.threadId,
|
||||
userMessage: body.userMessage,
|
||||
userWorkspaceId,
|
||||
res,
|
||||
});
|
||||
} catch (error) {
|
||||
// Handle errors at controller level for streaming responses
|
||||
// since the RestApiExceptionFilter interferes with our streaming error handling
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Unknown error occurred';
|
||||
|
||||
if (!res.headersSent) {
|
||||
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||
res.setHeader('Transfer-Encoding', 'chunked');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
}
|
||||
|
||||
res.write(
|
||||
JSON.stringify({
|
||||
type: 'error',
|
||||
message: errorMessage,
|
||||
}) + '\n',
|
||||
);
|
||||
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,8 +13,6 @@ import {
|
||||
AgentExceptionCode,
|
||||
} from 'src/engine/metadata-modules/agent/agent.exception';
|
||||
|
||||
import { AgentExecutionService } from './agent-execution.service';
|
||||
|
||||
@Injectable()
|
||||
export class AgentChatService {
|
||||
constructor(
|
||||
@ -22,7 +20,6 @@ export class AgentChatService {
|
||||
private readonly threadRepository: Repository<AgentChatThreadEntity>,
|
||||
@InjectRepository(AgentChatMessageEntity, 'core')
|
||||
private readonly messageRepository: Repository<AgentChatMessageEntity>,
|
||||
private readonly agentExecutionService: AgentExecutionService,
|
||||
) {}
|
||||
|
||||
async createThread(agentId: string, userWorkspaceId: string) {
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
ModelId,
|
||||
ModelProvider,
|
||||
} from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||
import { getAIModelById } from 'src/engine/core-modules/ai/utils/get-ai-model-by-id';
|
||||
import { getEffectiveModelConfig } from 'src/engine/core-modules/ai/utils/get-effective-model-config.util';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import {
|
||||
AgentChatMessageEntity,
|
||||
@ -22,6 +22,7 @@ import { AGENT_SYSTEM_PROMPTS } from 'src/engine/metadata-modules/agent/constant
|
||||
import { convertOutputSchemaToZod } from 'src/engine/metadata-modules/agent/utils/convert-output-schema-to-zod';
|
||||
import { OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
|
||||
import { resolveInput } from 'src/modules/workflow/workflow-executor/utils/variable-resolver.util';
|
||||
import { getAIModelById } from 'src/engine/core-modules/ai/utils/get-ai-model-by-id.util';
|
||||
|
||||
import { AgentEntity } from './agent.entity';
|
||||
import { AgentException, AgentExceptionCode } from './agent.exception';
|
||||
@ -45,12 +46,17 @@ export class AgentExecutionService {
|
||||
private readonly agentToolService: AgentToolService,
|
||||
@InjectRepository(AgentEntity, 'core')
|
||||
private readonly agentRepository: Repository<AgentEntity>,
|
||||
@InjectRepository(AgentChatMessageEntity, 'core')
|
||||
private readonly agentChatmessageRepository: Repository<AgentChatMessageEntity>,
|
||||
) {}
|
||||
|
||||
getModel = (modelId: ModelId, provider: ModelProvider) => {
|
||||
switch (provider) {
|
||||
case ModelProvider.NONE: {
|
||||
const OpenAIProvider = createOpenAI({
|
||||
apiKey: this.twentyConfigService.get('OPENAI_API_KEY'),
|
||||
});
|
||||
|
||||
return OpenAIProvider(getEffectiveModelConfig(modelId).modelId);
|
||||
}
|
||||
case ModelProvider.OPENAI: {
|
||||
const OpenAIProvider = createOpenAI({
|
||||
apiKey: this.twentyConfigService.get('OPENAI_API_KEY'),
|
||||
@ -77,6 +83,9 @@ export class AgentExecutionService {
|
||||
let apiKey: string | undefined;
|
||||
|
||||
switch (provider) {
|
||||
case ModelProvider.NONE:
|
||||
apiKey = this.twentyConfigService.get('OPENAI_API_KEY');
|
||||
break;
|
||||
case ModelProvider.OPENAI:
|
||||
apiKey = this.twentyConfigService.get('OPENAI_API_KEY');
|
||||
break;
|
||||
@ -91,7 +100,7 @@ export class AgentExecutionService {
|
||||
}
|
||||
if (!apiKey) {
|
||||
throw new AgentException(
|
||||
`${provider.toUpperCase()} API key not configured`,
|
||||
`${provider === ModelProvider.NONE ? 'OPENAI' : provider.toUpperCase()} API key not configured`,
|
||||
AgentExceptionCode.API_KEY_NOT_CONFIGURED,
|
||||
);
|
||||
}
|
||||
|
||||
@ -65,7 +65,7 @@ export class AgentStreamingService {
|
||||
|
||||
this.setupStreamingHeaders(res);
|
||||
|
||||
const { textStream } =
|
||||
const { fullStream } =
|
||||
await this.agentExecutionService.streamChatResponse({
|
||||
agentId: thread.agent.id,
|
||||
userMessage,
|
||||
@ -74,9 +74,24 @@ export class AgentStreamingService {
|
||||
|
||||
let aiResponse = '';
|
||||
|
||||
for await (const chunk of textStream) {
|
||||
aiResponse += chunk;
|
||||
res.write(chunk);
|
||||
for await (const chunk of fullStream) {
|
||||
switch (chunk.type) {
|
||||
case 'text-delta':
|
||||
aiResponse += chunk.textDelta;
|
||||
this.sendStreamEvent(res, {
|
||||
type: chunk.type,
|
||||
message: chunk.textDelta,
|
||||
});
|
||||
break;
|
||||
case 'tool-call':
|
||||
this.sendStreamEvent(res, {
|
||||
type: chunk.type,
|
||||
message: chunk.args?.toolDescription,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await this.agentChatService.addMessage({
|
||||
@ -90,10 +105,26 @@ export class AgentStreamingService {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Unknown error occurred';
|
||||
|
||||
return { success: false, error: errorMessage };
|
||||
if (!res.headersSent) {
|
||||
this.setupStreamingHeaders(res);
|
||||
}
|
||||
|
||||
this.sendStreamEvent(res, {
|
||||
type: 'error',
|
||||
message: errorMessage,
|
||||
});
|
||||
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
|
||||
private sendStreamEvent(
|
||||
res: Response,
|
||||
event: { type: string; message: string },
|
||||
): void {
|
||||
res.write(JSON.stringify(event) + '\n');
|
||||
}
|
||||
|
||||
private setupStreamingHeaders(res: Response): void {
|
||||
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||
res.setHeader('Transfer-Encoding', 'chunked');
|
||||
|
||||
@ -4,9 +4,9 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { ToolSet } from 'ai';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
|
||||
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
|
||||
|
||||
@Injectable()
|
||||
export class AgentToolService {
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
Unique,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@ -20,6 +21,7 @@ import { AgentChatThreadEntity } from './agent-chat-thread.entity';
|
||||
|
||||
@Entity('agent')
|
||||
@Index('IDX_AGENT_ID_DELETED_AT', ['id', 'deletedAt'])
|
||||
@Unique('IDX_AGENT_NAME_WORKSPACE_ID_UNIQUE', ['name', 'workspaceId'])
|
||||
export class AgentEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
@ -27,13 +29,19 @@ export class AgentEntity {
|
||||
@Column({ nullable: false })
|
||||
name: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
label: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
icon: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
description: string;
|
||||
|
||||
@Column({ nullable: false, type: 'text' })
|
||||
prompt: string;
|
||||
|
||||
@Column({ nullable: false, type: 'varchar' })
|
||||
@Column({ nullable: false, type: 'varchar', default: 'auto' })
|
||||
modelId: ModelId;
|
||||
|
||||
@Column({ nullable: true, type: 'jsonb' })
|
||||
@ -42,6 +50,9 @@ export class AgentEntity {
|
||||
@Column({ nullable: false, type: 'uuid' })
|
||||
workspaceId: string;
|
||||
|
||||
@Column({ default: false })
|
||||
isCustom: boolean;
|
||||
|
||||
@ManyToOne(() => Workspace, (workspace) => workspace.agents, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
|
||||
@ -11,4 +11,5 @@ export enum AgentExceptionCode {
|
||||
AGENT_NOT_FOUND = 'AGENT_NOT_FOUND',
|
||||
AGENT_EXECUTION_FAILED = 'AGENT_EXECUTION_FAILED',
|
||||
API_KEY_NOT_CONFIGURED = 'API_KEY_NOT_CONFIGURED',
|
||||
USER_WORKSPACE_ID_NOT_FOUND = 'USER_WORKSPACE_ID_NOT_FOUND',
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ModelId } from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||
import { AgentChatService } from 'src/engine/metadata-modules/agent/agent-chat.service';
|
||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
|
||||
import { AgentEntity } from './agent.entity';
|
||||
@ -16,6 +17,7 @@ export class AgentService {
|
||||
private readonly agentRepository: Repository<AgentEntity>,
|
||||
@InjectRepository(RoleTargetsEntity, 'core')
|
||||
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
|
||||
private readonly agentChatService: AgentChatService,
|
||||
) {}
|
||||
|
||||
async findManyAgents(workspaceId: string) {
|
||||
@ -51,9 +53,35 @@ export class AgentService {
|
||||
};
|
||||
}
|
||||
|
||||
async createOneAgentAndFirstThread(
|
||||
input: {
|
||||
name: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
prompt: string;
|
||||
modelId: ModelId;
|
||||
},
|
||||
workspaceId: string,
|
||||
userWorkspaceId: string | null,
|
||||
) {
|
||||
const agent = await this.createOneAgent(input, workspaceId);
|
||||
|
||||
if (!userWorkspaceId) {
|
||||
throw new AgentException(
|
||||
'User workspace ID not found',
|
||||
AgentExceptionCode.USER_WORKSPACE_ID_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
await this.agentChatService.createThread(agent.id, userWorkspaceId);
|
||||
|
||||
return agent;
|
||||
}
|
||||
|
||||
async createOneAgent(
|
||||
input: {
|
||||
name: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
prompt: string;
|
||||
modelId: ModelId;
|
||||
|
||||
@ -10,14 +10,34 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
|
||||
import { convertObjectMetadataToSchemaProperties } from 'src/engine/utils/convert-object-metadata-to-schema-properties.util';
|
||||
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
||||
|
||||
export const getRecordInputSchema = (objectMetadata: ObjectMetadataEntity) => {
|
||||
const createToolSchema = (
|
||||
inputProperties: Record<string, JSONSchema7Definition>,
|
||||
required?: string[],
|
||||
) => {
|
||||
return jsonSchema({
|
||||
type: 'object',
|
||||
properties: convertObjectMetadataToSchemaProperties({
|
||||
properties: {
|
||||
toolDescription: {
|
||||
type: 'string',
|
||||
description:
|
||||
'A clear, human-readable description of the action being performed. Explain what operation you are executing and with what parameters in natural language.',
|
||||
},
|
||||
input: {
|
||||
type: 'object',
|
||||
properties: inputProperties,
|
||||
...(required && { required }),
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getRecordInputSchema = (objectMetadata: ObjectMetadataEntity) => {
|
||||
return createToolSchema(
|
||||
convertObjectMetadataToSchemaProperties({
|
||||
item: objectMetadata,
|
||||
forResponse: false,
|
||||
}),
|
||||
});
|
||||
);
|
||||
};
|
||||
|
||||
export const generateFindToolSchema = (
|
||||
@ -48,10 +68,7 @@ export const generateFindToolSchema = (
|
||||
}
|
||||
});
|
||||
|
||||
return jsonSchema({
|
||||
type: 'object',
|
||||
properties: schemaProperties,
|
||||
});
|
||||
return createToolSchema(schemaProperties);
|
||||
};
|
||||
|
||||
const generateFieldFilterJsonSchema = (
|
||||
@ -808,25 +825,22 @@ const generateFieldFilterJsonSchema = (
|
||||
};
|
||||
|
||||
export const generateBulkDeleteToolSchema = () => {
|
||||
return jsonSchema({
|
||||
type: 'object',
|
||||
properties: {
|
||||
filter: {
|
||||
type: 'object',
|
||||
description: 'Filter criteria to select records for bulk delete',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'object',
|
||||
description: 'Filter to select records to delete',
|
||||
properties: {
|
||||
in: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
},
|
||||
description: 'Array of record IDs to delete',
|
||||
return createToolSchema({
|
||||
filter: {
|
||||
type: 'object',
|
||||
description: 'Filter criteria to select records for bulk delete',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'object',
|
||||
description: 'Filter to select records to delete',
|
||||
properties: {
|
||||
in: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
},
|
||||
description: 'Array of record IDs to delete',
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -834,3 +848,29 @@ export const generateBulkDeleteToolSchema = () => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const generateFindOneToolSchema = () => {
|
||||
return createToolSchema(
|
||||
{
|
||||
id: {
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
description: 'The unique UUID of the record to retrieve',
|
||||
},
|
||||
},
|
||||
['id'],
|
||||
);
|
||||
};
|
||||
|
||||
export const generateSoftDeleteToolSchema = () => {
|
||||
return createToolSchema(
|
||||
{
|
||||
id: {
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
description: 'The unique UUID of the record to soft delete',
|
||||
},
|
||||
},
|
||||
['id'],
|
||||
);
|
||||
};
|
||||
|
||||
@ -6,6 +6,7 @@ import { Repository } from 'typeorm';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
||||
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
@ -115,6 +116,14 @@ describe('WorkspaceManagerService', () => {
|
||||
deleteObjectsMetadata: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: AgentService,
|
||||
useValue: {
|
||||
createOneAgent: jest
|
||||
.fn()
|
||||
.mockResolvedValue({ id: 'mock-agent-id' }),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
|
||||
@ -0,0 +1,277 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { AgentChatMessageRole } from 'src/engine/metadata-modules/agent/agent-chat-message.entity';
|
||||
import { USER_WORKSPACE_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-user-workspaces.util';
|
||||
import {
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
SEED_YCOMBINATOR_WORKSPACE_ID,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
|
||||
|
||||
const agentTableName = 'agent';
|
||||
const workspaceTableName = 'workspace';
|
||||
const agentChatThreadTableName = 'agentChatThread';
|
||||
const agentChatMessageTableName = 'agentChatMessage';
|
||||
|
||||
export const AGENT_DATA_SEED_IDS = {
|
||||
APPLE_DEFAULT_AGENT: '20202020-0000-4000-8000-000000000001',
|
||||
YCOMBINATOR_DEFAULT_AGENT: '20202020-0000-4000-8000-000000000002',
|
||||
};
|
||||
|
||||
export const AGENT_CHAT_THREAD_DATA_SEED_IDS = {
|
||||
APPLE_DEFAULT_THREAD: '20202020-0000-4000-8000-000000000011',
|
||||
YCOMBINATOR_DEFAULT_THREAD: '20202020-0000-4000-8000-000000000012',
|
||||
};
|
||||
|
||||
export const AGENT_CHAT_MESSAGE_DATA_SEED_IDS = {
|
||||
APPLE_MESSAGE_1: '20202020-0000-4000-8000-000000000021',
|
||||
APPLE_MESSAGE_2: '20202020-0000-4000-8000-000000000022',
|
||||
APPLE_MESSAGE_3: '20202020-0000-4000-8000-000000000023',
|
||||
APPLE_MESSAGE_4: '20202020-0000-4000-8000-000000000024',
|
||||
YCOMBINATOR_MESSAGE_1: '20202020-0000-4000-8000-000000000031',
|
||||
YCOMBINATOR_MESSAGE_2: '20202020-0000-4000-8000-000000000032',
|
||||
YCOMBINATOR_MESSAGE_3: '20202020-0000-4000-8000-000000000033',
|
||||
YCOMBINATOR_MESSAGE_4: '20202020-0000-4000-8000-000000000034',
|
||||
};
|
||||
|
||||
const seedAgentChatThreads = async (
|
||||
dataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
agentId: string,
|
||||
) => {
|
||||
let threadId: string;
|
||||
let userWorkspaceId: string;
|
||||
|
||||
if (workspaceId === SEED_APPLE_WORKSPACE_ID) {
|
||||
threadId = AGENT_CHAT_THREAD_DATA_SEED_IDS.APPLE_DEFAULT_THREAD;
|
||||
userWorkspaceId = USER_WORKSPACE_DATA_SEED_IDS.TIM;
|
||||
} else if (workspaceId === SEED_YCOMBINATOR_WORKSPACE_ID) {
|
||||
threadId = AGENT_CHAT_THREAD_DATA_SEED_IDS.YCOMBINATOR_DEFAULT_THREAD;
|
||||
userWorkspaceId = USER_WORKSPACE_DATA_SEED_IDS.TIM_ACME;
|
||||
} else {
|
||||
throw new Error(
|
||||
`Unsupported workspace ID for agent chat thread seeding: ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
|
||||
await dataSource
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.${agentChatThreadTableName}`, [
|
||||
'id',
|
||||
'agentId',
|
||||
'userWorkspaceId',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
])
|
||||
.orIgnore()
|
||||
.values([
|
||||
{
|
||||
id: threadId,
|
||||
agentId,
|
||||
userWorkspaceId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
])
|
||||
.execute();
|
||||
|
||||
return threadId;
|
||||
};
|
||||
|
||||
const seedAgentChatMessages = async (
|
||||
dataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
threadId: string,
|
||||
) => {
|
||||
let messageIds: string[];
|
||||
let messages: Array<{
|
||||
id: string;
|
||||
threadId: string;
|
||||
role: AgentChatMessageRole;
|
||||
content: string;
|
||||
createdAt: Date;
|
||||
}>;
|
||||
|
||||
const now = new Date();
|
||||
const baseTime = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 24 hours ago
|
||||
|
||||
if (workspaceId === SEED_APPLE_WORKSPACE_ID) {
|
||||
messageIds = [
|
||||
AGENT_CHAT_MESSAGE_DATA_SEED_IDS.APPLE_MESSAGE_1,
|
||||
AGENT_CHAT_MESSAGE_DATA_SEED_IDS.APPLE_MESSAGE_2,
|
||||
AGENT_CHAT_MESSAGE_DATA_SEED_IDS.APPLE_MESSAGE_3,
|
||||
AGENT_CHAT_MESSAGE_DATA_SEED_IDS.APPLE_MESSAGE_4,
|
||||
];
|
||||
messages = [
|
||||
{
|
||||
id: messageIds[0],
|
||||
threadId,
|
||||
role: AgentChatMessageRole.USER,
|
||||
content:
|
||||
'Hello! Can you help me understand our current product roadmap and key metrics?',
|
||||
createdAt: new Date(baseTime.getTime()),
|
||||
},
|
||||
{
|
||||
id: messageIds[1],
|
||||
threadId,
|
||||
role: AgentChatMessageRole.ASSISTANT,
|
||||
content:
|
||||
"Hello! I'd be happy to help you understand Apple's product roadmap and metrics. Based on your workspace data, I can see you have various projects and initiatives tracked. What specific aspect would you like to explore - product development timelines, user engagement metrics, or revenue targets?",
|
||||
createdAt: new Date(baseTime.getTime() + 5 * 60 * 1000), // 5 minutes later
|
||||
},
|
||||
{
|
||||
id: messageIds[2],
|
||||
threadId,
|
||||
role: AgentChatMessageRole.USER,
|
||||
content:
|
||||
"I'd like to focus on our user engagement metrics and how they're trending over the last quarter.",
|
||||
createdAt: new Date(baseTime.getTime() + 10 * 60 * 1000), // 10 minutes later
|
||||
},
|
||||
{
|
||||
id: messageIds[3],
|
||||
threadId,
|
||||
role: AgentChatMessageRole.ASSISTANT,
|
||||
content:
|
||||
'Great! Looking at your user engagement data, I can see several key trends from the last quarter. Your active user base has grown by 15%, with particularly strong engagement in the mobile app. Daily active users are averaging 2.3 million, and session duration has increased by 8%. Would you like me to dive deeper into any specific engagement metrics or create a detailed report?',
|
||||
createdAt: new Date(baseTime.getTime() + 15 * 60 * 1000), // 15 minutes later
|
||||
},
|
||||
];
|
||||
} else if (workspaceId === SEED_YCOMBINATOR_WORKSPACE_ID) {
|
||||
messageIds = [
|
||||
AGENT_CHAT_MESSAGE_DATA_SEED_IDS.YCOMBINATOR_MESSAGE_1,
|
||||
AGENT_CHAT_MESSAGE_DATA_SEED_IDS.YCOMBINATOR_MESSAGE_2,
|
||||
AGENT_CHAT_MESSAGE_DATA_SEED_IDS.YCOMBINATOR_MESSAGE_3,
|
||||
AGENT_CHAT_MESSAGE_DATA_SEED_IDS.YCOMBINATOR_MESSAGE_4,
|
||||
];
|
||||
messages = [
|
||||
{
|
||||
id: messageIds[0],
|
||||
threadId,
|
||||
role: AgentChatMessageRole.USER,
|
||||
content:
|
||||
'What are the current startup trends and which companies in our portfolio are performing best?',
|
||||
createdAt: new Date(baseTime.getTime()),
|
||||
},
|
||||
{
|
||||
id: messageIds[1],
|
||||
threadId,
|
||||
role: AgentChatMessageRole.ASSISTANT,
|
||||
content:
|
||||
'Hello! I can help you analyze startup trends and portfolio performance. From your YCombinator workspace data, I can see strong performance in AI/ML startups, particularly in the B2B SaaS space. Several companies are showing 40%+ month-over-month growth. Would you like me to provide specific company performance metrics or focus on broader industry trends?',
|
||||
createdAt: new Date(baseTime.getTime() + 3 * 60 * 1000), // 3 minutes later
|
||||
},
|
||||
{
|
||||
id: messageIds[2],
|
||||
threadId,
|
||||
role: AgentChatMessageRole.USER,
|
||||
content:
|
||||
'Please focus on our top 5 performing companies and their key metrics.',
|
||||
createdAt: new Date(baseTime.getTime() + 8 * 60 * 1000), // 8 minutes later
|
||||
},
|
||||
{
|
||||
id: messageIds[3],
|
||||
threadId,
|
||||
role: AgentChatMessageRole.ASSISTANT,
|
||||
content:
|
||||
'Here are your top 5 performing portfolio companies: 1) TechFlow AI - 45% MoM growth, $2M ARR, 2) DataSync Pro - 38% MoM growth, $1.5M ARR, 3) CloudOps Solutions - 35% MoM growth, $3.2M ARR, 4) SecureNet - 32% MoM growth, $1.8M ARR, 5) HealthTech Plus - 28% MoM growth, $2.5M ARR. All are showing strong customer retention (>95%) and expanding market share. Would you like detailed breakdowns for any specific company?',
|
||||
createdAt: new Date(baseTime.getTime() + 12 * 60 * 1000), // 12 minutes later
|
||||
},
|
||||
];
|
||||
} else {
|
||||
throw new Error(
|
||||
`Unsupported workspace ID for agent chat message seeding: ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
await dataSource
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.${agentChatMessageTableName}`, [
|
||||
'id',
|
||||
'threadId',
|
||||
'role',
|
||||
'content',
|
||||
'createdAt',
|
||||
])
|
||||
.orIgnore()
|
||||
.values(messages)
|
||||
.execute();
|
||||
};
|
||||
|
||||
export const seedAgents = async (
|
||||
dataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
) => {
|
||||
let agentId: string;
|
||||
let agentName: string;
|
||||
let agentLabel: string;
|
||||
let agentDescription: string;
|
||||
|
||||
if (workspaceId === SEED_APPLE_WORKSPACE_ID) {
|
||||
agentId = AGENT_DATA_SEED_IDS.APPLE_DEFAULT_AGENT;
|
||||
agentName = 'apple-ai-assistant';
|
||||
agentLabel = 'Apple AI Assistant';
|
||||
agentDescription =
|
||||
'AI assistant for Apple workspace to help with tasks, insights, and workflow guidance';
|
||||
} else if (workspaceId === SEED_YCOMBINATOR_WORKSPACE_ID) {
|
||||
agentId = AGENT_DATA_SEED_IDS.YCOMBINATOR_DEFAULT_AGENT;
|
||||
agentName = 'yc-ai-assistant';
|
||||
agentLabel = 'YC AI Assistant';
|
||||
agentDescription =
|
||||
'AI assistant for YCombinator workspace to help with tasks, insights, and workflow guidance';
|
||||
} else {
|
||||
throw new Error(
|
||||
`Unsupported workspace ID for agent seeding: ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
await dataSource
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.${agentTableName}`, [
|
||||
'id',
|
||||
'name',
|
||||
'label',
|
||||
'description',
|
||||
'prompt',
|
||||
'modelId',
|
||||
'responseFormat',
|
||||
'workspaceId',
|
||||
])
|
||||
.orIgnore()
|
||||
.values([
|
||||
{
|
||||
id: agentId,
|
||||
name: agentName,
|
||||
label: agentLabel,
|
||||
description: agentDescription,
|
||||
prompt:
|
||||
'You are a helpful AI assistant for this workspace. Help users with their tasks, provide insights about their data, and guide them through workflows. Be concise but thorough in your responses.',
|
||||
modelId: 'auto',
|
||||
responseFormat: null,
|
||||
workspaceId,
|
||||
},
|
||||
])
|
||||
.execute();
|
||||
|
||||
await dataSource
|
||||
.createQueryBuilder()
|
||||
.update(`${schemaName}.${workspaceTableName}`)
|
||||
.set({ defaultAgentId: agentId })
|
||||
.where('id = :workspaceId', { workspaceId })
|
||||
.execute();
|
||||
|
||||
const threadId = await seedAgentChatThreads(
|
||||
dataSource,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
agentId,
|
||||
);
|
||||
|
||||
await seedAgentChatMessages(dataSource, schemaName, workspaceId, threadId);
|
||||
};
|
||||
@ -1,6 +1,7 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { seedBillingSubscriptions } from 'src/engine/workspace-manager/dev-seeder/core/billing/utils/seed-billing-subscriptions.util';
|
||||
import { seedAgents } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-agents.util';
|
||||
import { seedApiKeys } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-api-keys.util';
|
||||
import { seedFeatureFlags } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util';
|
||||
import { seedUserWorkspaces } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-user-workspaces.util';
|
||||
@ -33,6 +34,8 @@ export const seedCoreSchema = async ({
|
||||
await seedUsers(dataSource, schemaName);
|
||||
await seedUserWorkspaces(dataSource, schemaName, workspaceId);
|
||||
|
||||
await seedAgents(dataSource, schemaName, workspaceId);
|
||||
|
||||
await seedApiKeys(dataSource, schemaName, workspaceId);
|
||||
|
||||
if (shouldSeedFeatureFlags) {
|
||||
|
||||
@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AgentModule } from 'src/engine/metadata-modules/agent/agent.module';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||
@ -33,6 +34,7 @@ import { WorkspaceManagerService } from './workspace-manager.service';
|
||||
WorkspaceHealthModule,
|
||||
FeatureFlagModule,
|
||||
PermissionsModule,
|
||||
AgentModule,
|
||||
TypeOrmModule.forFeature([UserWorkspace, Workspace], 'core'),
|
||||
RoleModule,
|
||||
UserRoleModule,
|
||||
|
||||
@ -3,9 +3,11 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
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 { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
||||
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
@ -42,6 +44,7 @@ export class WorkspaceManagerService {
|
||||
private readonly roleRepository: Repository<RoleEntity>,
|
||||
@InjectRepository(RoleTargetsEntity, 'core')
|
||||
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
|
||||
private readonly agentService: AgentService,
|
||||
) {}
|
||||
|
||||
public async init({
|
||||
@ -95,6 +98,17 @@ export class WorkspaceManagerService {
|
||||
`Permissions enabled took ${permissionsEnabledEnd - permissionsEnabledStart}ms`,
|
||||
);
|
||||
|
||||
if (featureFlags[FeatureFlagKey.IS_AI_ENABLED]) {
|
||||
const defaultAgentEnabledStart = performance.now();
|
||||
|
||||
await this.initDefaultAgent(workspaceId);
|
||||
const defaultAgentEnabledEnd = performance.now();
|
||||
|
||||
this.logger.log(
|
||||
`Default agent enabled took ${defaultAgentEnabledEnd - defaultAgentEnabledStart}ms`,
|
||||
);
|
||||
}
|
||||
|
||||
const prefillStandardObjectsStart = performance.now();
|
||||
|
||||
await this.prefillWorkspaceWithStandardObjectsRecords(
|
||||
@ -194,4 +208,21 @@ export class WorkspaceManagerService {
|
||||
defaultRoleId: memberRole.id,
|
||||
});
|
||||
}
|
||||
|
||||
private async initDefaultAgent(workspaceId: string) {
|
||||
const agent = await this.agentService.createOneAgent(
|
||||
{
|
||||
label: 'Routing Agent',
|
||||
name: 'routing-agent',
|
||||
description: 'Default Routing Agent',
|
||||
prompt: '',
|
||||
modelId: 'auto',
|
||||
},
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.workspaceRepository.update(workspaceId, {
|
||||
defaultAgentId: agent.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -616,14 +616,16 @@ export class WorkflowVersionStepWorkspaceService {
|
||||
};
|
||||
}
|
||||
case WorkflowActionType.AI_AGENT: {
|
||||
const newAgent = await this.agentService.createOneAgent(
|
||||
const newAgent = await this.agentService.createOneAgentAndFirstThread(
|
||||
{
|
||||
name: 'AI Agent Workflow Step',
|
||||
label: 'AI Agent Workflow Step',
|
||||
name: 'ai-agent-workflow',
|
||||
description: 'Created automatically for workflow step',
|
||||
prompt: '',
|
||||
modelId: 'gpt-4o',
|
||||
modelId: 'auto',
|
||||
},
|
||||
workspaceId,
|
||||
this.scopedWorkspaceContextFactory.create().userWorkspaceId,
|
||||
);
|
||||
|
||||
if (!isDefined(newAgent)) {
|
||||
@ -636,15 +638,13 @@ export class WorkflowVersionStepWorkspaceService {
|
||||
const userWorkspaceId =
|
||||
this.scopedWorkspaceContextFactory.create().userWorkspaceId;
|
||||
|
||||
if (!userWorkspaceId) {
|
||||
throw new WorkflowVersionStepException(
|
||||
'User workspace ID not found',
|
||||
WorkflowVersionStepExceptionCode.FAILURE,
|
||||
if (userWorkspaceId) {
|
||||
await this.agentChatService.createThread(
|
||||
newAgent.id,
|
||||
userWorkspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
await this.agentChatService.createThread(newAgent.id, userWorkspaceId);
|
||||
|
||||
return {
|
||||
id: newStepId,
|
||||
name: 'AI Agent',
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
||||
import { AgentResolver } from 'src/engine/metadata-modules/agent/agent.resolver';
|
||||
import {
|
||||
AgentException,
|
||||
AgentExceptionCode,
|
||||
} from 'src/engine/metadata-modules/agent/agent.exception';
|
||||
import { AgentResolver } from 'src/engine/metadata-modules/agent/agent.resolver';
|
||||
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
||||
|
||||
// Mock the agent service
|
||||
jest.mock('../../../../../src/engine/metadata-modules/agent/agent.service');
|
||||
|
||||
@ -216,7 +216,7 @@ describe('AgentToolService Integration', () => {
|
||||
}
|
||||
|
||||
const result = await createTool.execute(
|
||||
{ name: 'Test Record', description: 'Test description' },
|
||||
{ input: { name: 'Test Record', description: 'Test description' } },
|
||||
{
|
||||
toolCallId: 'test-tool-call-id',
|
||||
messages: [
|
||||
@ -260,7 +260,7 @@ describe('AgentToolService Integration', () => {
|
||||
}
|
||||
|
||||
const result = await createTool.execute(
|
||||
{ name: 'Test Record' },
|
||||
{ input: { name: 'Test Record' } },
|
||||
{
|
||||
toolCallId: 'test-tool-call-id',
|
||||
messages: [
|
||||
@ -304,7 +304,7 @@ describe('AgentToolService Integration', () => {
|
||||
}
|
||||
|
||||
const result = await findTool.execute(
|
||||
{ limit: 10, offset: 0 },
|
||||
{ input: { limit: 10, offset: 0 } },
|
||||
{
|
||||
toolCallId: 'test-tool-call-id',
|
||||
messages: [
|
||||
@ -352,7 +352,7 @@ describe('AgentToolService Integration', () => {
|
||||
}
|
||||
|
||||
const result = await findOneTool.execute(
|
||||
{ id: 'test-record-id' },
|
||||
{ input: { id: 'test-record-id' } },
|
||||
{
|
||||
toolCallId: 'test-tool-call-id',
|
||||
messages: [
|
||||
@ -393,7 +393,7 @@ describe('AgentToolService Integration', () => {
|
||||
}
|
||||
|
||||
const result = await findOneTool.execute(
|
||||
{ id: 'non-existent-id' },
|
||||
{ input: { id: 'non-existent-id' } },
|
||||
{
|
||||
toolCallId: 'test-tool-call-id',
|
||||
messages: [
|
||||
@ -430,7 +430,7 @@ describe('AgentToolService Integration', () => {
|
||||
}
|
||||
|
||||
const result = await findOneTool.execute(
|
||||
{},
|
||||
{ input: {} },
|
||||
{
|
||||
toolCallId: 'test-tool-call-id',
|
||||
messages: [
|
||||
@ -488,9 +488,11 @@ describe('AgentToolService Integration', () => {
|
||||
|
||||
const result = await updateTool.execute(
|
||||
{
|
||||
id: 'test-record-id',
|
||||
name: 'New Name',
|
||||
description: 'New description',
|
||||
input: {
|
||||
id: 'test-record-id',
|
||||
name: 'New Name',
|
||||
description: 'New description',
|
||||
},
|
||||
},
|
||||
{
|
||||
toolCallId: 'test-tool-call-id',
|
||||
@ -534,8 +536,10 @@ describe('AgentToolService Integration', () => {
|
||||
|
||||
const result = await updateTool.execute(
|
||||
{
|
||||
id: 'non-existent-id',
|
||||
name: 'New Name',
|
||||
input: {
|
||||
id: 'non-existent-id',
|
||||
name: 'New Name',
|
||||
},
|
||||
},
|
||||
{
|
||||
toolCallId: 'test-tool-call-id',
|
||||
@ -583,7 +587,7 @@ describe('AgentToolService Integration', () => {
|
||||
}
|
||||
|
||||
const result = await softDeleteTool.execute(
|
||||
{ id: 'test-record-id' },
|
||||
{ input: { id: 'test-record-id' } },
|
||||
{
|
||||
toolCallId: 'test-tool-call-id',
|
||||
messages: [
|
||||
@ -624,7 +628,9 @@ describe('AgentToolService Integration', () => {
|
||||
|
||||
const result = await softDeleteManyTool.execute(
|
||||
{
|
||||
filter: { id: { in: ['record-1', 'record-2', 'record-3'] } },
|
||||
input: {
|
||||
filter: { id: { in: ['record-1', 'record-2', 'record-3'] } },
|
||||
},
|
||||
},
|
||||
{
|
||||
toolCallId: 'test-tool-call-id',
|
||||
@ -671,7 +677,7 @@ describe('AgentToolService Integration', () => {
|
||||
}
|
||||
|
||||
const result = await findTool.execute(
|
||||
{},
|
||||
{ input: {} },
|
||||
{
|
||||
toolCallId: 'test-tool-call-id',
|
||||
messages: [
|
||||
@ -716,10 +722,12 @@ describe('AgentToolService Integration', () => {
|
||||
|
||||
const result = await findTool.execute(
|
||||
{
|
||||
name: null,
|
||||
description: undefined,
|
||||
status: '',
|
||||
validField: 'valid value',
|
||||
input: {
|
||||
name: null,
|
||||
description: undefined,
|
||||
status: '',
|
||||
validField: 'valid value',
|
||||
},
|
||||
},
|
||||
{
|
||||
toolCallId: 'test-tool-call-id',
|
||||
|
||||
@ -3,6 +3,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
|
||||
import { AgentToolService } from 'src/engine/metadata-modules/agent/agent-tool.service';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
|
||||
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
||||
@ -12,7 +13,6 @@ import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
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';
|
||||
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
|
||||
|
||||
export interface AgentToolTestContext {
|
||||
module: TestingModule;
|
||||
@ -103,7 +103,10 @@ export const createAgentToolTestModule =
|
||||
|
||||
const testAgent: AgentEntity & { roleId: string | null } = {
|
||||
id: testAgentId,
|
||||
name: 'Test Agent',
|
||||
name: 'test-agent',
|
||||
label: 'Test Agent',
|
||||
icon: 'IconTest',
|
||||
isCustom: false,
|
||||
description: 'Test agent for integration tests',
|
||||
prompt: 'You are a test agent',
|
||||
modelId: 'gpt-4o',
|
||||
|
||||
Reference in New Issue
Block a user