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:
@@ -1,77 +1,79 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type ToolInput } from '@/ai/types/ToolInput';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const DirectQuerySchema = z.object({ query: z.string() });
|
||||
const NestedQuerySchema = z.object({
|
||||
action: z.object({ query: z.string() }),
|
||||
});
|
||||
const CustomLoadingMessageSchema = z.object({ loadingMessage: z.string() });
|
||||
const ExecuteToolSchema = z.object({
|
||||
toolName: z.coerce.string(),
|
||||
arguments: z.unknown(),
|
||||
});
|
||||
const LearnToolsSchema = z.object({ toolNames: z.array(z.string()) });
|
||||
const LoadSkillsSchema = z.object({ skillNames: z.array(z.string()) });
|
||||
|
||||
const extractSearchQuery = (input: ToolInput): string => {
|
||||
if (!input) {
|
||||
return '';
|
||||
const direct = DirectQuerySchema.safeParse(input);
|
||||
|
||||
if (direct.success) {
|
||||
return direct.data.query;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof input === 'object' &&
|
||||
'query' in input &&
|
||||
typeof input.query === 'string'
|
||||
) {
|
||||
return input.query;
|
||||
}
|
||||
const nested = NestedQuerySchema.safeParse(input);
|
||||
|
||||
if (
|
||||
typeof input === 'object' &&
|
||||
'action' in input &&
|
||||
isDefined(input.action) &&
|
||||
typeof input.action === 'object' &&
|
||||
'query' in input.action &&
|
||||
typeof input.action.query === 'string'
|
||||
) {
|
||||
return input.action.query;
|
||||
if (nested.success) {
|
||||
return nested.data.action.query;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const extractLoadingMessage = (input: ToolInput): string => {
|
||||
if (
|
||||
isDefined(input) &&
|
||||
typeof input === 'object' &&
|
||||
'loadingMessage' in input &&
|
||||
typeof input.loadingMessage === 'string'
|
||||
) {
|
||||
return input.loadingMessage;
|
||||
}
|
||||
const extractCustomLoadingMessage = (input: ToolInput): string | null => {
|
||||
const parsed = CustomLoadingMessageSchema.safeParse(input);
|
||||
|
||||
return 'Processing...';
|
||||
return parsed.success ? parsed.data.loadingMessage : null;
|
||||
};
|
||||
|
||||
export const resolveToolInput = (
|
||||
input: ToolInput,
|
||||
toolName: string,
|
||||
): { resolvedInput: ToolInput; resolvedToolName: string } => {
|
||||
if (
|
||||
toolName === 'execute_tool' &&
|
||||
isDefined(input) &&
|
||||
typeof input === 'object' &&
|
||||
'toolName' in input &&
|
||||
'arguments' in input
|
||||
) {
|
||||
return {
|
||||
resolvedInput: input.arguments as ToolInput,
|
||||
resolvedToolName: String(input.toolName),
|
||||
};
|
||||
if (toolName !== 'execute_tool') {
|
||||
return { resolvedInput: input, resolvedToolName: toolName };
|
||||
}
|
||||
|
||||
return { resolvedInput: input, resolvedToolName: toolName };
|
||||
const parsed = ExecuteToolSchema.safeParse(input);
|
||||
|
||||
if (!parsed.success) {
|
||||
return { resolvedInput: input, resolvedToolName: toolName };
|
||||
}
|
||||
|
||||
return {
|
||||
resolvedInput: parsed.data.arguments as ToolInput,
|
||||
resolvedToolName: parsed.data.toolName,
|
||||
};
|
||||
};
|
||||
|
||||
const extractLearnToolNames = (input: ToolInput): string => {
|
||||
if (
|
||||
isDefined(input) &&
|
||||
typeof input === 'object' &&
|
||||
'toolNames' in input &&
|
||||
Array.isArray(input.toolNames)
|
||||
) {
|
||||
return input.toolNames.join(', ');
|
||||
}
|
||||
const parsed = LearnToolsSchema.safeParse(input);
|
||||
|
||||
return '';
|
||||
return parsed.success ? parsed.data.toolNames.join(', ') : '';
|
||||
};
|
||||
|
||||
const extractSkillNames = (input: ToolInput): string => {
|
||||
const parsed = LoadSkillsSchema.safeParse(input);
|
||||
|
||||
return parsed.success ? parsed.data.skillNames.join(', ') : '';
|
||||
};
|
||||
|
||||
const formatToolName = (toolName: string): string => {
|
||||
return toolName.replace(/_/g, ' ');
|
||||
};
|
||||
|
||||
export const getToolDisplayMessage = (
|
||||
@@ -81,19 +83,49 @@ export const getToolDisplayMessage = (
|
||||
): string => {
|
||||
const { resolvedInput, resolvedToolName } = resolveToolInput(input, toolName);
|
||||
|
||||
const byStatus = (finished: string, inProgress: string): string =>
|
||||
isFinished ? finished : inProgress;
|
||||
|
||||
if (resolvedToolName === 'web_search') {
|
||||
const query = extractSearchQuery(resolvedInput);
|
||||
const action = isFinished ? 'Searched' : 'Searching';
|
||||
|
||||
return query ? `${action} the web for '${query}'` : `${action} the web`;
|
||||
if (isNonEmptyString(query)) {
|
||||
return byStatus(
|
||||
t`Searched the web for '${query}'`,
|
||||
t`Searching the web for '${query}'`,
|
||||
);
|
||||
}
|
||||
|
||||
return byStatus(t`Searched the web`, t`Searching the web`);
|
||||
}
|
||||
|
||||
if (resolvedToolName === 'learn_tools') {
|
||||
const names = extractLearnToolNames(resolvedInput);
|
||||
const action = isFinished ? 'Learned' : 'Learning';
|
||||
|
||||
return names ? `${action} ${names}` : `${action} tools...`;
|
||||
if (isNonEmptyString(names)) {
|
||||
return byStatus(t`Learned ${names}`, t`Learning ${names}`);
|
||||
}
|
||||
|
||||
return byStatus(t`Learned tools`, t`Learning tools...`);
|
||||
}
|
||||
|
||||
return extractLoadingMessage(resolvedInput);
|
||||
if (resolvedToolName === 'load_skills') {
|
||||
const names = extractSkillNames(resolvedInput);
|
||||
|
||||
if (isNonEmptyString(names)) {
|
||||
return byStatus(t`Loaded ${names}`, t`Loading ${names}`);
|
||||
}
|
||||
|
||||
return byStatus(t`Loaded skills`, t`Loading skills...`);
|
||||
}
|
||||
|
||||
const customMessage = extractCustomLoadingMessage(resolvedInput);
|
||||
|
||||
if (isDefined(customMessage)) {
|
||||
return customMessage;
|
||||
}
|
||||
|
||||
const formattedName = formatToolName(resolvedToolName);
|
||||
|
||||
return byStatus(t`Ran ${formattedName}`, t`Running ${formattedName}`);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user