Improve AI agent chat, tool display, and workflow agent management (#17876)
## Summary - **Fix token renewal endpoint**: Use `/metadata` instead of `/graphql` for token renewal in agent chat, fixing auth issues - **Improve tool display**: Add `load_skills` support, show formatted tool names (underscores → spaces) with finish/loading states, display tool icons during loading, and support custom loading messages from tool input - **Refactor workflow agent management**: Replace direct `AgentRepository` access with `AgentService` for create/delete/find operations in workflow steps, improving encapsulation and consistency - **Simplify Apollo client usage**: Remove explicit Apollo client override in `useGetToolIndex`, add `AgentChatProvider` to `AppRouterProviders` - **Fix load-skill tool**: Change parameter type from `string` to `json` for proper schema parsing - **Update agent-chat-streaming**: Use `AgentService` for agent resolution and tool registration instead of direct repository queries ## Test plan - [ ] Verify AI agent chat works end-to-end (send message, receive response) - [ ] Verify tool steps display correctly with icons and proper messages during loading and after completion - [ ] Verify workflow AI agent step creation and deletion works correctly - [ ] Verify workflow version cloning preserves agent configuration - [ ] Verify token renewal works when tokens expire during agent chat Made with [Cursor](https://cursor.com) --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
+13
-26
@@ -2,7 +2,7 @@ import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { AiAgentRoleService } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.service';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.service';
|
||||
import { createEmptyAllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-all-flat-entity-maps.constant';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { LogicFunctionRuntime } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
|
||||
@@ -10,7 +10,6 @@ import { LogicFunctionMetadataService } from 'src/engine/metadata-modules/logic-
|
||||
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
|
||||
@@ -28,9 +27,8 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
|
||||
let globalWorkspaceOrmManager: jest.Mocked<GlobalWorkspaceOrmManager>;
|
||||
let logicFunctionMetadataService: jest.Mocked<LogicFunctionMetadataService>;
|
||||
let codeStepBuildService: jest.Mocked<CodeStepBuildService>;
|
||||
let agentRepository: jest.Mocked<any>;
|
||||
let agentService: jest.Mocked<AgentService>;
|
||||
let roleTargetRepository: jest.Mocked<any>;
|
||||
let roleRepository: jest.Mocked<any>;
|
||||
let objectMetadataRepository: jest.Mocked<any>;
|
||||
let workflowCommonWorkspaceService: jest.Mocked<WorkflowCommonWorkspaceService>;
|
||||
let aiAgentRoleService: jest.Mocked<AiAgentRoleService>;
|
||||
@@ -96,21 +94,17 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
|
||||
destroyOne: jest.fn(),
|
||||
} as unknown as jest.Mocked<LogicFunctionMetadataService>;
|
||||
|
||||
agentRepository = {
|
||||
findOne: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
};
|
||||
agentService = {
|
||||
deleteManyAgents: jest.fn().mockResolvedValue([]),
|
||||
findOneAgentById: jest.fn(),
|
||||
createOneAgent: jest.fn(),
|
||||
} as unknown as jest.Mocked<AgentService>;
|
||||
|
||||
roleTargetRepository = {
|
||||
findOne: jest.fn(),
|
||||
count: jest.fn(),
|
||||
};
|
||||
|
||||
roleRepository = {
|
||||
findOne: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
};
|
||||
|
||||
objectMetadataRepository = {
|
||||
findOne: jest.fn(),
|
||||
};
|
||||
@@ -146,17 +140,13 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
|
||||
useValue: codeStepBuildService,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(AgentEntity),
|
||||
useValue: agentRepository,
|
||||
provide: AgentService,
|
||||
useValue: agentService,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(RoleTargetEntity),
|
||||
useValue: roleTargetRepository,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(RoleEntity),
|
||||
useValue: roleRepository,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(ObjectMetadataEntity),
|
||||
useValue: objectMetadataRepository,
|
||||
@@ -239,15 +229,13 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
|
||||
},
|
||||
} as unknown as WorkflowAction;
|
||||
|
||||
agentRepository.findOne.mockResolvedValue({ id: 'agent-id' });
|
||||
|
||||
await service.runWorkflowVersionStepDeletionSideEffects({
|
||||
step,
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
|
||||
expect(agentRepository.delete).toHaveBeenCalledWith({
|
||||
id: 'agent-id',
|
||||
expect(agentService.deleteManyAgents).toHaveBeenCalledWith({
|
||||
ids: ['agent-id'],
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
});
|
||||
@@ -272,7 +260,6 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
|
||||
},
|
||||
} as unknown as WorkflowAction;
|
||||
|
||||
agentRepository.findOne.mockResolvedValue({ id: 'agent-id' });
|
||||
roleTargetRepository.findOne.mockResolvedValue({
|
||||
id: 'role-target-id',
|
||||
roleId: 'role-id',
|
||||
@@ -283,8 +270,8 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
|
||||
expect(agentRepository.delete).toHaveBeenCalledWith({
|
||||
id: 'agent-id',
|
||||
expect(agentService.deleteManyAgents).toHaveBeenCalledWith({
|
||||
ids: ['agent-id'],
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
expect(
|
||||
|
||||
+49
-56
@@ -17,7 +17,7 @@ import { v4 } from 'uuid';
|
||||
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
|
||||
import { type WorkflowStepPositionInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-step-position-input.dto';
|
||||
import { AiAgentRoleService } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.service';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.service';
|
||||
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { LogicFunctionMetadataService } from 'src/engine/metadata-modules/logic-function/services/logic-function-metadata.service';
|
||||
@@ -66,8 +66,7 @@ export class WorkflowVersionStepOperationsWorkspaceService {
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly logicFunctionMetadataService: LogicFunctionMetadataService,
|
||||
private readonly codeStepBuildService: CodeStepBuildService,
|
||||
@InjectRepository(AgentEntity)
|
||||
private readonly agentRepository: Repository<AgentEntity>,
|
||||
private readonly agentService: AgentService,
|
||||
@InjectRepository(RoleTargetEntity)
|
||||
private readonly roleTargetRepository: Repository<RoleTargetEntity>,
|
||||
@InjectRepository(ObjectMetadataEntity)
|
||||
@@ -98,27 +97,24 @@ export class WorkflowVersionStepOperationsWorkspaceService {
|
||||
break;
|
||||
}
|
||||
|
||||
const agent = await this.agentRepository.findOne({
|
||||
where: { id: step.settings.input.agentId, workspaceId },
|
||||
const roleTarget = await this.roleTargetRepository.findOne({
|
||||
where: {
|
||||
agentId: step.settings.input.agentId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (isDefined(agent)) {
|
||||
const roleTarget = await this.roleTargetRepository.findOne({
|
||||
where: {
|
||||
agentId: agent.id,
|
||||
workspaceId,
|
||||
},
|
||||
await this.agentService.deleteManyAgents({
|
||||
ids: [step.settings.input.agentId],
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (isDefined(roleTarget?.roleId) && isDefined(roleTarget?.id)) {
|
||||
await this.aiAgentRoleService.deleteAgentOnlyRoleIfUnused({
|
||||
roleId: roleTarget.roleId,
|
||||
roleTargetId: roleTarget.id,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
await this.agentRepository.delete({ id: agent.id, workspaceId });
|
||||
|
||||
if (isDefined(roleTarget?.roleId) && isDefined(roleTarget?.id)) {
|
||||
await this.aiAgentRoleService.deleteAgentOnlyRoleIfUnused({
|
||||
roleId: roleTarget.roleId,
|
||||
roleTargetId: roleTarget.id,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -415,27 +411,20 @@ export class WorkflowVersionStepOperationsWorkspaceService {
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const newAgent = await this.agentRepository.save({
|
||||
name: 'workflow-service-agent' + v4(),
|
||||
label: 'Workflow Agent' + workflowVersion.workflowId.substring(0, 4),
|
||||
icon: 'IconRobot',
|
||||
description: '',
|
||||
prompt:
|
||||
'You are a helpful AI assistant. Complete the task based on the workflow context.',
|
||||
modelId: DEFAULT_SMART_MODEL,
|
||||
responseFormat: { type: 'text' },
|
||||
const newAgent = await this.agentService.createOneAgent(
|
||||
{
|
||||
label:
|
||||
'Workflow Agent' + workflowVersion.workflowId.substring(0, 4),
|
||||
icon: 'IconRobot',
|
||||
description: '',
|
||||
prompt:
|
||||
'You are a helpful AI assistant. Complete the task based on the workflow context.',
|
||||
modelId: DEFAULT_SMART_MODEL,
|
||||
responseFormat: { type: 'text' },
|
||||
isCustom: true,
|
||||
},
|
||||
workspaceId,
|
||||
isCustom: true,
|
||||
});
|
||||
|
||||
if (!isDefined(newAgent)) {
|
||||
throw new WorkflowVersionStepException(
|
||||
'Failed to create AI Agent step',
|
||||
WorkflowVersionStepExceptionCode.AI_AGENT_STEP_FAILURE,
|
||||
);
|
||||
}
|
||||
|
||||
await this.workspaceCacheService.flush(workspaceId, ['flatAgentMaps']);
|
||||
);
|
||||
|
||||
return {
|
||||
builtStep: {
|
||||
@@ -676,30 +665,34 @@ export class WorkflowVersionStepOperationsWorkspaceService {
|
||||
};
|
||||
}
|
||||
case WorkflowActionType.AI_AGENT: {
|
||||
const existingAgent = await this.agentRepository.findOne({
|
||||
where: { id: step.settings.input.agentId, workspaceId },
|
||||
});
|
||||
const agentId = step.settings.input.agentId;
|
||||
|
||||
if (!isDefined(existingAgent)) {
|
||||
if (!isDefined(agentId)) {
|
||||
throw new WorkflowVersionStepException(
|
||||
'Agent not found for cloning',
|
||||
'Agent ID is required for cloning',
|
||||
WorkflowVersionStepExceptionCode.AI_AGENT_STEP_FAILURE,
|
||||
);
|
||||
}
|
||||
|
||||
const clonedAgent = await this.agentRepository.save({
|
||||
name: 'workflow-service-agent' + v4(),
|
||||
label: existingAgent.label,
|
||||
icon: existingAgent.icon,
|
||||
description: existingAgent.description,
|
||||
prompt: existingAgent.prompt,
|
||||
modelId: existingAgent.modelId,
|
||||
responseFormat: existingAgent.responseFormat,
|
||||
const existingAgent = await this.agentService.findOneAgentById({
|
||||
id: agentId,
|
||||
workspaceId,
|
||||
isCustom: true,
|
||||
modelConfiguration: existingAgent.modelConfiguration,
|
||||
});
|
||||
|
||||
const clonedAgent = await this.agentService.createOneAgent(
|
||||
{
|
||||
label: existingAgent.label,
|
||||
icon: existingAgent.icon ?? undefined,
|
||||
description: existingAgent.description ?? undefined,
|
||||
prompt: existingAgent.prompt,
|
||||
modelId: existingAgent.modelId,
|
||||
responseFormat: existingAgent.responseFormat ?? undefined,
|
||||
modelConfiguration: existingAgent.modelConfiguration ?? undefined,
|
||||
isCustom: true,
|
||||
},
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return {
|
||||
...step,
|
||||
id: v4(),
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ import { Module } from '@nestjs/common';
|
||||
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||
|
||||
import { AiAgentRoleModule } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.module';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { AiAgentModule } from 'src/engine/metadata-modules/ai/ai-agent/ai-agent.module';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
@@ -27,10 +27,10 @@ import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workfl
|
||||
WorkflowCommonModule,
|
||||
CodeStepBuildModule,
|
||||
AiAgentRoleModule,
|
||||
AiAgentModule,
|
||||
WorkspaceCacheModule,
|
||||
NestjsQueryTypeOrmModule.forFeature([
|
||||
ObjectMetadataEntity,
|
||||
AgentEntity,
|
||||
RoleTargetEntity,
|
||||
RoleEntity,
|
||||
]),
|
||||
|
||||
Reference in New Issue
Block a user