mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
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:
parent
5c3c2e08a6
commit
21c51ec251
39 changed files with 815 additions and 656 deletions
34
.cursor/Dockerfile
Normal file
34
.cursor/Dockerfile
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
FROM ubuntu:22.04
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
git \
|
||||
make \
|
||||
build-essential \
|
||||
postgresql-client \
|
||||
docker.io \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install nvm (project recommends nvm + .nvmrc for consistent Node versions)
|
||||
ENV NVM_DIR=/usr/local/nvm
|
||||
RUN mkdir -p $NVM_DIR \
|
||||
&& curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
|
||||
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
||||
# Copy .nvmrc so nvm install picks up the right version
|
||||
COPY .nvmrc /tmp/.nvmrc
|
||||
|
||||
# Install Node.js from .nvmrc, enable Corepack, and symlink binaries
|
||||
# so they're available on PATH without hardcoding a version
|
||||
RUN . $NVM_DIR/nvm.sh \
|
||||
&& nvm install $(cat /tmp/.nvmrc) \
|
||||
&& nvm alias default $(cat /tmp/.nvmrc) \
|
||||
&& corepack enable \
|
||||
&& BIN_DIR=$(dirname $(nvm which default)) \
|
||||
&& ln -sf $BIN_DIR/node /usr/local/bin/node \
|
||||
&& ln -sf $BIN_DIR/npm /usr/local/bin/npm \
|
||||
&& ln -sf $BIN_DIR/npx /usr/local/bin/npx \
|
||||
&& ln -sf $BIN_DIR/corepack /usr/local/bin/corepack
|
||||
|
|
@ -1,18 +1,10 @@
|
|||
{
|
||||
"install": "curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash - && sudo apt-get install -y nodejs && node --version && yarn install && echo 'Installing dependencies complete'",
|
||||
"start": "sudo service docker start && echo 'Docker service started' && sleep 3 && echo 'Starting PostgreSQL and Redis containers...' && make postgres-on-docker && make redis-on-docker && echo 'Waiting for containers to initialize...' && sleep 20 && echo 'Checking container status...' && docker ps --filter name=twenty_ && echo 'Waiting for PostgreSQL to be ready...' && until docker exec twenty_pg pg_isready -U postgres -h localhost; do echo 'PostgreSQL not ready yet, waiting...'; sleep 3; done && echo 'PostgreSQL is ready!' && echo 'Setting up database...' && cd packages/twenty-server && npx nx database:reset twenty-server || echo 'Database already initialized' && echo 'Environment setup complete!'",
|
||||
"install": "yarn install",
|
||||
"start": "sudo service docker start && sleep 2 && (docker start twenty_pg 2>/dev/null || make -C packages/twenty-docker postgres-on-docker) && (docker start twenty_redis 2>/dev/null || make -C packages/twenty-docker redis-on-docker) && until docker exec twenty_pg pg_isready -U postgres -h localhost 2>/dev/null; do sleep 1; done && echo 'PostgreSQL ready' && until docker exec twenty_redis redis-cli ping 2>/dev/null | grep -q PONG; do sleep 1; done && echo 'Redis ready' && bash packages/twenty-utils/setup-dev-env.sh && npx nx database:reset twenty-server",
|
||||
"terminals": [
|
||||
{
|
||||
"name": "Development Server",
|
||||
"command": "echo 'Waiting for database to be fully ready...' && sleep 30 && until docker exec twenty_pg pg_isready -U postgres -h localhost; do echo 'Waiting for PostgreSQL...'; sleep 2; done && echo 'Starting Twenty development server...' && export SERVER_URL=http://localhost:3000 && export PG_DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres && yarn start"
|
||||
},
|
||||
{
|
||||
"name": "Database Management",
|
||||
"command": "sleep 25 && echo 'Database management terminal ready' && echo 'Waiting for PostgreSQL to be available...' && until docker exec twenty_pg pg_isready -U postgres -h localhost; do echo 'Waiting for PostgreSQL...'; sleep 2; done && echo 'PostgreSQL is ready for database operations!' && echo 'You can now run database commands like:' && echo ' npx nx database:reset twenty-server' && echo ' npx nx database:migrate twenty-server' && bash"
|
||||
},
|
||||
{
|
||||
"name": "Container Logs & Status",
|
||||
"command": "sleep 10 && echo '=== Container Status Monitor ===' && while true; do echo '\\n=== Container Status at $(date) ===' && docker ps --filter name=twenty_ --format 'table {{.Names}}\\t{{.Status}}\\t{{.Ports}}' && echo '\\n=== PostgreSQL Status ===' && (docker exec twenty_pg pg_isready -U postgres -h localhost && echo 'PostgreSQL: ✅ Ready') || echo 'PostgreSQL: ❌ Not Ready' && echo '\\n=== Redis Status ===' && (docker exec twenty_redis redis-cli ping && echo 'Redis: ✅ Ready') || echo 'Redis: ❌ Not Ready' && sleep 30; done"
|
||||
"command": "yarn start"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3670,6 +3670,7 @@ export type Query = {
|
|||
getSSOIdentityProviders: Array<FindAvailableSsoidpOutput>;
|
||||
getSystemHealthStatus: SystemHealth;
|
||||
getToolIndex: Array<ToolIndexEntry>;
|
||||
getToolInputSchema?: Maybe<Scalars['JSON']>;
|
||||
index: Index;
|
||||
indexMetadatas: IndexConnection;
|
||||
lineChartData: LineChartDataOutput;
|
||||
|
|
@ -3941,6 +3942,11 @@ export type QueryGetQueueMetricsArgs = {
|
|||
};
|
||||
|
||||
|
||||
export type QueryGetToolInputSchemaArgs = {
|
||||
toolName: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryIndexArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
|
@ -5323,7 +5329,14 @@ export type GetChatThreadsQuery = { __typename?: 'Query', chatThreads: Array<{ _
|
|||
export type GetToolIndexQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetToolIndexQuery = { __typename?: 'Query', getToolIndex: Array<{ __typename?: 'ToolIndexEntry', name: string, description: string, category: string, objectName?: string | null, inputSchema?: any | null }> };
|
||||
export type GetToolIndexQuery = { __typename?: 'Query', getToolIndex: Array<{ __typename?: 'ToolIndexEntry', name: string, description: string, category: string, objectName?: string | null }> };
|
||||
|
||||
export type GetToolInputSchemaQueryVariables = Exact<{
|
||||
toolName: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetToolInputSchemaQuery = { __typename?: 'Query', getToolInputSchema?: any | null };
|
||||
|
||||
export type TrackAnalyticsMutationVariables = Exact<{
|
||||
type: AnalyticsType;
|
||||
|
|
@ -8486,7 +8499,6 @@ export const GetToolIndexDocument = gql`
|
|||
description
|
||||
category
|
||||
objectName
|
||||
inputSchema
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
@ -8517,6 +8529,39 @@ export function useGetToolIndexLazyQuery(baseOptions?: Apollo.LazyQueryHookOptio
|
|||
export type GetToolIndexQueryHookResult = ReturnType<typeof useGetToolIndexQuery>;
|
||||
export type GetToolIndexLazyQueryHookResult = ReturnType<typeof useGetToolIndexLazyQuery>;
|
||||
export type GetToolIndexQueryResult = Apollo.QueryResult<GetToolIndexQuery, GetToolIndexQueryVariables>;
|
||||
export const GetToolInputSchemaDocument = gql`
|
||||
query GetToolInputSchema($toolName: String!) {
|
||||
getToolInputSchema(toolName: $toolName)
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useGetToolInputSchemaQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useGetToolInputSchemaQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useGetToolInputSchemaQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useGetToolInputSchemaQuery({
|
||||
* variables: {
|
||||
* toolName: // value for 'toolName'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGetToolInputSchemaQuery(baseOptions: Apollo.QueryHookOptions<GetToolInputSchemaQuery, GetToolInputSchemaQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<GetToolInputSchemaQuery, GetToolInputSchemaQueryVariables>(GetToolInputSchemaDocument, options);
|
||||
}
|
||||
export function useGetToolInputSchemaLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetToolInputSchemaQuery, GetToolInputSchemaQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<GetToolInputSchemaQuery, GetToolInputSchemaQueryVariables>(GetToolInputSchemaDocument, options);
|
||||
}
|
||||
export type GetToolInputSchemaQueryHookResult = ReturnType<typeof useGetToolInputSchemaQuery>;
|
||||
export type GetToolInputSchemaLazyQueryHookResult = ReturnType<typeof useGetToolInputSchemaLazyQuery>;
|
||||
export type GetToolInputSchemaQueryResult = Apollo.QueryResult<GetToolInputSchemaQuery, GetToolInputSchemaQueryVariables>;
|
||||
export const TrackAnalyticsDocument = gql`
|
||||
mutation TrackAnalytics($type: AnalyticsType!, $event: String, $name: String, $properties: JSON) {
|
||||
trackAnalytics(type: $type, event: $event, name: $name, properties: $properties) {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import {
|
|||
getToolDisplayMessage,
|
||||
resolveToolInput,
|
||||
} from '@/ai/utils/getToolDisplayMessage';
|
||||
import { ToolOutputMessageSchema } from '@/ai/schemas/toolOutputMessageSchema';
|
||||
import { ToolOutputResultSchema } from '@/ai/schemas/toolOutputResultSchema';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { type ToolUIPart } from 'ai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
|
@ -25,12 +27,6 @@ const StyledContainer = styled.div`
|
|||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledLoadingContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledContentContainer = styled.div`
|
||||
background: ${({ theme }) => theme.background.transparent.lighter};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
|
|
@ -142,6 +138,7 @@ export const ToolStepRenderer = ({ toolPart }: { toolPart: ToolUIPart }) => {
|
|||
|
||||
const hasError = isDefined(errorText);
|
||||
const isExpandable = isDefined(output) || hasError;
|
||||
const ToolIcon = getToolIcon(toolName);
|
||||
|
||||
if (toolName === 'code_interpreter') {
|
||||
const codeInput = toolInput as { code?: string } | undefined;
|
||||
|
|
@ -173,13 +170,14 @@ export const ToolStepRenderer = ({ toolPart }: { toolPart: ToolUIPart }) => {
|
|||
<StyledContainer>
|
||||
<StyledToggleButton isExpandable={false}>
|
||||
<StyledLeftContent>
|
||||
<StyledLoadingContainer>
|
||||
<StyledIconTextContainer>
|
||||
<ToolIcon size={theme.icon.size.sm} />
|
||||
<ShimmeringText>
|
||||
<StyledDisplayMessage>
|
||||
{getToolDisplayMessage(input, rawToolName, false)}
|
||||
</StyledDisplayMessage>
|
||||
</ShimmeringText>
|
||||
</StyledLoadingContainer>
|
||||
</StyledIconTextContainer>
|
||||
</StyledLeftContent>
|
||||
<StyledRightContent>
|
||||
<StyledToolName>{toolName}</StyledToolName>
|
||||
|
|
@ -190,33 +188,28 @@ export const ToolStepRenderer = ({ toolPart }: { toolPart: ToolUIPart }) => {
|
|||
}
|
||||
|
||||
// For execute_tool, the actual result is nested inside output.result
|
||||
const outputResult = ToolOutputResultSchema.safeParse(output);
|
||||
const unwrappedOutput =
|
||||
rawToolName === 'execute_tool' &&
|
||||
isDefined(output) &&
|
||||
typeof output === 'object' &&
|
||||
'result' in output
|
||||
? (output as { result: unknown }).result
|
||||
rawToolName === 'execute_tool' && outputResult.success
|
||||
? outputResult.data.result
|
||||
: output;
|
||||
|
||||
const unwrappedResult = ToolOutputResultSchema.safeParse(unwrappedOutput);
|
||||
const unwrappedMessage = ToolOutputMessageSchema.safeParse(unwrappedOutput);
|
||||
|
||||
const displayMessage = hasError
|
||||
? t`Tool execution failed`
|
||||
: rawToolName === 'learn_tools' || rawToolName === 'execute_tool'
|
||||
: rawToolName === 'learn_tools' ||
|
||||
rawToolName === 'execute_tool' ||
|
||||
rawToolName === 'load_skills'
|
||||
? getToolDisplayMessage(input, rawToolName, true)
|
||||
: unwrappedOutput &&
|
||||
typeof unwrappedOutput === 'object' &&
|
||||
'message' in unwrappedOutput &&
|
||||
typeof unwrappedOutput.message === 'string'
|
||||
? unwrappedOutput.message
|
||||
: unwrappedMessage.success
|
||||
? unwrappedMessage.data.message
|
||||
: getToolDisplayMessage(input, rawToolName, true);
|
||||
|
||||
const result =
|
||||
unwrappedOutput &&
|
||||
typeof unwrappedOutput === 'object' &&
|
||||
'result' in unwrappedOutput
|
||||
? (unwrappedOutput as { result: string }).result
|
||||
: unwrappedOutput;
|
||||
|
||||
const ToolIcon = getToolIcon(toolName);
|
||||
const result = unwrappedResult.success
|
||||
? unwrappedResult.data.result
|
||||
: unwrappedOutput;
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ export const GET_TOOL_INDEX = gql`
|
|||
description
|
||||
category
|
||||
objectName
|
||||
inputSchema
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_TOOL_INPUT_SCHEMA = gql`
|
||||
query GetToolInputSchema($toolName: String!) {
|
||||
getToolInputSchema(toolName: $toolName)
|
||||
}
|
||||
`;
|
||||
|
|
@ -6,6 +6,8 @@ import { agentChatUploadedFilesState } from '@/ai/states/agentChatUploadedFilesS
|
|||
import { agentChatUsageState } from '@/ai/states/agentChatUsageState';
|
||||
import { currentAIChatThreadState } from '@/ai/states/currentAIChatThreadState';
|
||||
|
||||
import { agentChatInputState } from '@/ai/states/agentChatInputState';
|
||||
import { REST_API_BASE_URL } from '@/apollo/constant/rest-api-base-url';
|
||||
import { getTokenPair } from '@/apollo/utils/getTokenPair';
|
||||
import { renewToken } from '@/auth/services/AuthService';
|
||||
import { tokenPairState } from '@/auth/states/tokenPairState';
|
||||
|
|
@ -15,8 +17,6 @@ import { type ExtendedUIMessage } from 'twenty-shared/ai';
|
|||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import { cookieStorage } from '~/utils/cookie-storage';
|
||||
import { REST_API_BASE_URL } from '@/apollo/constant/rest-api-base-url';
|
||||
import { agentChatInputState } from '@/ai/states/agentChatInputState';
|
||||
|
||||
export const useAgentChat = (uiMessages: ExtendedUIMessage[]) => {
|
||||
const setTokenPair = useSetRecoilState(tokenPairState);
|
||||
|
|
@ -47,7 +47,7 @@ export const useAgentChat = (uiMessages: ExtendedUIMessage[]) => {
|
|||
|
||||
try {
|
||||
const renewedTokens = await renewToken(
|
||||
`${REACT_APP_SERVER_BASE_URL}/graphql`,
|
||||
`${REACT_APP_SERVER_BASE_URL}/metadata`,
|
||||
tokenPair,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { GET_TOOL_INDEX } from '@/ai/graphql/queries/getToolIndex';
|
||||
import { useQuery } from '@apollo/client';
|
||||
|
||||
|
|
@ -14,11 +13,7 @@ type GetToolIndexQuery = {
|
|||
};
|
||||
|
||||
export const useGetToolIndex = () => {
|
||||
const apolloMetadataClient = useApolloCoreClient();
|
||||
|
||||
const { data, loading, error } = useQuery<GetToolIndexQuery>(GET_TOOL_INDEX, {
|
||||
client: apolloMetadataClient ?? undefined,
|
||||
});
|
||||
const { data, loading, error } = useQuery<GetToolIndexQuery>(GET_TOOL_INDEX);
|
||||
|
||||
return {
|
||||
toolIndex: data?.getToolIndex ?? [],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const ToolOutputMessageSchema = z.object({ message: z.string() });
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const ToolOutputResultSchema = z.object({ result: z.unknown() });
|
||||
|
|
@ -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}`);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentChatProvider } from '@/ai/components/AgentChatProvider';
|
||||
import { ApolloProvider } from '@/apollo/components/ApolloProvider';
|
||||
import { GotoHotkeysEffectsProvider } from '@/app/effect-components/GotoHotkeysEffectsProvider';
|
||||
import { PageChangeEffect } from '@/app/effect-components/PageChangeEffect';
|
||||
|
|
@ -55,20 +56,22 @@ export const AppRouterProviders = () => {
|
|||
<UserThemeProviderEffect />
|
||||
<SnackBarProvider>
|
||||
<ErrorMessageEffect />
|
||||
<DialogComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'dialog-manager' }}
|
||||
>
|
||||
<DialogManager>
|
||||
<StrictMode>
|
||||
<PromiseRejectionEffect />
|
||||
<GotoHotkeysEffectsProvider />
|
||||
<PageTitle title={pageTitle} />
|
||||
<PageFavicon />
|
||||
<Outlet />
|
||||
<GlobalFilePreviewModal />
|
||||
</StrictMode>
|
||||
</DialogManager>
|
||||
</DialogComponentInstanceContext.Provider>
|
||||
<AgentChatProvider>
|
||||
<DialogComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'dialog-manager' }}
|
||||
>
|
||||
<DialogManager>
|
||||
<StrictMode>
|
||||
<PromiseRejectionEffect />
|
||||
<GotoHotkeysEffectsProvider />
|
||||
<PageTitle title={pageTitle} />
|
||||
<PageFavicon />
|
||||
<Outlet />
|
||||
<GlobalFilePreviewModal />
|
||||
</StrictMode>
|
||||
</DialogManager>
|
||||
</DialogComponentInstanceContext.Provider>
|
||||
</AgentChatProvider>
|
||||
</SnackBarProvider>
|
||||
<MainContextStoreProvider />
|
||||
<SupportChatEffect />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
import { AgentChatProvider } from '@/ai/components/AgentChatProvider';
|
||||
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||
|
|
@ -60,11 +59,9 @@ export const CommandMenuContainer = ({
|
|||
<ActionMenuComponentInstanceContext.Provider
|
||||
value={{ instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID }}
|
||||
>
|
||||
<AgentChatProvider>
|
||||
<StyledCommandMenuContainer isMobile={isMobile}>
|
||||
{children}
|
||||
</StyledCommandMenuContainer>
|
||||
</AgentChatProvider>
|
||||
<StyledCommandMenuContainer isMobile={isMobile}>
|
||||
{children}
|
||||
</StyledCommandMenuContainer>
|
||||
</ActionMenuComponentInstanceContext.Provider>
|
||||
</ContextStoreComponentInstanceContext.Provider>
|
||||
</RecordComponentInstanceContextsWrapper>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { useLazyQuery } from '@apollo/client';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { GET_TOOL_INPUT_SCHEMA } from '@/ai/graphql/queries/getToolInputSchemas';
|
||||
import { SettingsItemTypeTag } from '@/settings/components/SettingsItemTypeTag';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
|
|
@ -23,12 +25,15 @@ import { type JsonValue } from 'type-fest';
|
|||
|
||||
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||
|
||||
type GetToolInputSchemaQuery = {
|
||||
getToolInputSchema: object | null;
|
||||
};
|
||||
|
||||
export type SystemTool = {
|
||||
name: string;
|
||||
description: string;
|
||||
category: string;
|
||||
objectName?: string;
|
||||
inputSchema?: object;
|
||||
};
|
||||
|
||||
export type SettingsSystemToolTableRowProps = {
|
||||
|
|
@ -105,14 +110,24 @@ export const SettingsSystemToolTableRow = ({
|
|||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const { copyToClipboard } = useCopyToClipboard();
|
||||
|
||||
const Icon = getCategoryIcon(tool.category);
|
||||
// Fetch inputSchema for this specific tool on first expand
|
||||
const [fetchSchema, { data: schemaData, loading: schemaLoading }] =
|
||||
useLazyQuery<GetToolInputSchemaQuery>(GET_TOOL_INPUT_SCHEMA, {
|
||||
variables: { toolName: tool.name },
|
||||
});
|
||||
|
||||
const inputSchema = schemaData?.getToolInputSchema;
|
||||
|
||||
const hasInputSchema =
|
||||
isDefined(tool.inputSchema) && Object.keys(tool.inputSchema).length > 0;
|
||||
isDefined(inputSchema) && Object.keys(inputSchema).length > 0;
|
||||
|
||||
const Icon = getCategoryIcon(tool.category);
|
||||
|
||||
const handleRowClick = () => {
|
||||
if (hasInputSchema) {
|
||||
setIsExpanded(!isExpanded);
|
||||
if (!schemaData && !schemaLoading) {
|
||||
fetchSchema();
|
||||
}
|
||||
setIsExpanded(!isExpanded);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -120,7 +135,7 @@ export const SettingsSystemToolTableRow = ({
|
|||
<StyledSystemToolTableRow
|
||||
key={tool.name}
|
||||
onClick={handleRowClick}
|
||||
isExpandable={hasInputSchema}
|
||||
isExpandable
|
||||
>
|
||||
<StyledNameTableCell>
|
||||
<StyledIconContainer>
|
||||
|
|
@ -132,35 +147,42 @@ export const SettingsSystemToolTableRow = ({
|
|||
<SettingsItemTypeTag item={{ isCustom: false }} />
|
||||
</TableCell>
|
||||
<StyledActionTableCell>
|
||||
{hasInputSchema &&
|
||||
(isExpanded ? (
|
||||
<IconChevronDown size={16} />
|
||||
) : (
|
||||
<IconChevronRight size={16} />
|
||||
))}
|
||||
{isExpanded ? (
|
||||
<IconChevronDown size={16} />
|
||||
) : (
|
||||
<IconChevronRight size={16} />
|
||||
)}
|
||||
</StyledActionTableCell>
|
||||
</StyledSystemToolTableRow>
|
||||
|
||||
{hasInputSchema && (
|
||||
<AnimatedExpandableContainer isExpanded={isExpanded} mode="fit-content">
|
||||
<StyledExpandableContent>
|
||||
{tool.description && (
|
||||
<StyledDescription>{tool.description}</StyledDescription>
|
||||
)}
|
||||
<StyledSectionTitle>{t`Input Schema`}</StyledSectionTitle>
|
||||
<JsonTree
|
||||
value={tool.inputSchema as JsonValue}
|
||||
shouldExpandNodeInitially={() => true}
|
||||
emptyArrayLabel={t`Empty Array`}
|
||||
emptyObjectLabel={t`No parameters`}
|
||||
emptyStringLabel={t`[empty string]`}
|
||||
arrowButtonCollapsedLabel={t`Expand`}
|
||||
arrowButtonExpandedLabel={t`Collapse`}
|
||||
onNodeValueClick={copyToClipboard}
|
||||
/>
|
||||
</StyledExpandableContent>
|
||||
</AnimatedExpandableContainer>
|
||||
)}
|
||||
<AnimatedExpandableContainer isExpanded={isExpanded} mode="fit-content">
|
||||
<StyledExpandableContent>
|
||||
{tool.description && (
|
||||
<StyledDescription>{tool.description}</StyledDescription>
|
||||
)}
|
||||
{schemaLoading && (
|
||||
<StyledSectionTitle>{t`Loading schema...`}</StyledSectionTitle>
|
||||
)}
|
||||
{hasInputSchema && (
|
||||
<>
|
||||
<StyledSectionTitle>{t`Input Schema`}</StyledSectionTitle>
|
||||
<JsonTree
|
||||
value={inputSchema as JsonValue}
|
||||
shouldExpandNodeInitially={() => true}
|
||||
emptyArrayLabel={t`Empty Array`}
|
||||
emptyObjectLabel={t`No parameters`}
|
||||
emptyStringLabel={t`[empty string]`}
|
||||
arrowButtonCollapsedLabel={t`Expand`}
|
||||
arrowButtonExpandedLabel={t`Collapse`}
|
||||
onNodeValueClick={copyToClipboard}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!schemaLoading && !hasInputSchema && (
|
||||
<StyledSectionTitle>{t`No parameters`}</StyledSectionTitle>
|
||||
)}
|
||||
</StyledExpandableContent>
|
||||
</AnimatedExpandableContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ import { type ActorMetadata } from 'twenty-shared/types';
|
|||
|
||||
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
|
||||
import { type ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import {
|
||||
type ToolDescriptor,
|
||||
type ToolIndexEntry,
|
||||
} from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import { type FlatAgentWithRoleId } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
|
||||
|
|
@ -31,12 +34,19 @@ export type ToolRetrievalOptions = {
|
|||
wrapWithErrorContext?: boolean;
|
||||
};
|
||||
|
||||
export type GenerateDescriptorOptions = {
|
||||
includeSchemas?: boolean; // defaults to true for backward compat
|
||||
};
|
||||
|
||||
export interface ToolProvider {
|
||||
readonly category: ToolCategory;
|
||||
|
||||
isAvailable(context: ToolProviderContext): Promise<boolean>;
|
||||
|
||||
generateDescriptors(context: ToolProviderContext): Promise<ToolDescriptor[]>;
|
||||
generateDescriptors(
|
||||
context: ToolProviderContext,
|
||||
options?: GenerateDescriptorOptions,
|
||||
): Promise<(ToolIndexEntry | ToolDescriptor)[]>;
|
||||
}
|
||||
|
||||
// NativeModelToolProvider is special: SDK-native tools are opaque and not
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { PermissionFlagType } from 'twenty-shared/constants';
|
|||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
type GenerateDescriptorOptions,
|
||||
type ToolProvider,
|
||||
type ToolProviderContext,
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
|
|
@ -13,7 +14,10 @@ import {
|
|||
type StaticToolHandler,
|
||||
ToolExecutorService,
|
||||
} from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
|
||||
import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import {
|
||||
type ToolDescriptor,
|
||||
type ToolIndexEntry,
|
||||
} from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import { CodeInterpreterTool } from 'src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool';
|
||||
import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool';
|
||||
import { SearchHelpCenterTool } from 'src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool';
|
||||
|
|
@ -65,8 +69,10 @@ export class ActionToolProvider implements ToolProvider {
|
|||
|
||||
async generateDescriptors(
|
||||
context: ToolProviderContext,
|
||||
): Promise<ToolDescriptor[]> {
|
||||
const descriptors: ToolDescriptor[] = [];
|
||||
options?: GenerateDescriptorOptions,
|
||||
): Promise<(ToolIndexEntry | ToolDescriptor)[]> {
|
||||
const includeSchemas = options?.includeSchemas ?? true;
|
||||
const descriptors: (ToolIndexEntry | ToolDescriptor)[] = [];
|
||||
|
||||
const hasHttpPermission = await this.permissionsService.hasToolPermission(
|
||||
context.rolePermissionConfig,
|
||||
|
|
@ -75,7 +81,9 @@ export class ActionToolProvider implements ToolProvider {
|
|||
);
|
||||
|
||||
if (hasHttpPermission) {
|
||||
descriptors.push(this.buildDescriptor('http_request', this.httpTool));
|
||||
descriptors.push(
|
||||
this.buildDescriptor('http_request', this.httpTool, includeSchemas),
|
||||
);
|
||||
}
|
||||
|
||||
const hasEmailPermission = await this.permissionsService.hasToolPermission(
|
||||
|
|
@ -85,11 +93,17 @@ export class ActionToolProvider implements ToolProvider {
|
|||
);
|
||||
|
||||
if (hasEmailPermission) {
|
||||
descriptors.push(this.buildDescriptor('send_email', this.sendEmailTool));
|
||||
descriptors.push(
|
||||
this.buildDescriptor('send_email', this.sendEmailTool, includeSchemas),
|
||||
);
|
||||
}
|
||||
|
||||
descriptors.push(
|
||||
this.buildDescriptor('search_help_center', this.searchHelpCenterTool),
|
||||
this.buildDescriptor(
|
||||
'search_help_center',
|
||||
this.searchHelpCenterTool,
|
||||
includeSchemas,
|
||||
),
|
||||
);
|
||||
|
||||
const hasCodeInterpreterPermission =
|
||||
|
|
@ -101,19 +115,29 @@ export class ActionToolProvider implements ToolProvider {
|
|||
|
||||
if (hasCodeInterpreterPermission) {
|
||||
descriptors.push(
|
||||
this.buildDescriptor('code_interpreter', this.codeInterpreterTool),
|
||||
this.buildDescriptor(
|
||||
'code_interpreter',
|
||||
this.codeInterpreterTool,
|
||||
includeSchemas,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return descriptors;
|
||||
}
|
||||
|
||||
private buildDescriptor(toolId: string, tool: Tool): ToolDescriptor {
|
||||
private buildDescriptor(
|
||||
toolId: string,
|
||||
tool: Tool,
|
||||
includeSchemas: boolean,
|
||||
): ToolIndexEntry | ToolDescriptor {
|
||||
return {
|
||||
name: toolId,
|
||||
description: tool.description,
|
||||
category: ToolCategory.ACTION,
|
||||
inputSchema: z.toJSONSchema(tool.inputSchema as z.ZodType),
|
||||
...(includeSchemas && {
|
||||
inputSchema: z.toJSONSchema(tool.inputSchema as z.ZodType),
|
||||
}),
|
||||
executionRef: { kind: 'static', toolId },
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Inject, Injectable, OnModuleInit, Optional } from '@nestjs/common';
|
|||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import {
|
||||
type GenerateDescriptorOptions,
|
||||
type ToolProvider,
|
||||
type ToolProviderContext,
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
|
|
@ -10,7 +11,10 @@ import {
|
|||
import { DASHBOARD_TOOL_SERVICE_TOKEN } from 'src/engine/core-modules/tool-provider/constants/dashboard-tool-service.token';
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
|
||||
import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import {
|
||||
type ToolDescriptor,
|
||||
type ToolIndexEntry,
|
||||
} from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import { toolSetToDescriptors } from 'src/engine/core-modules/tool-provider/utils/tool-set-to-descriptors.util';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
import type { DashboardToolWorkspaceService } from 'src/modules/dashboard/tools/services/dashboard-tool.workspace-service';
|
||||
|
|
@ -56,7 +60,8 @@ export class DashboardToolProvider implements ToolProvider, OnModuleInit {
|
|||
|
||||
async generateDescriptors(
|
||||
context: ToolProviderContext,
|
||||
): Promise<ToolDescriptor[]> {
|
||||
options?: GenerateDescriptorOptions,
|
||||
): Promise<(ToolIndexEntry | ToolDescriptor)[]> {
|
||||
if (!this.dashboardToolService) {
|
||||
return [];
|
||||
}
|
||||
|
|
@ -66,6 +71,8 @@ export class DashboardToolProvider implements ToolProvider, OnModuleInit {
|
|||
context.rolePermissionConfig,
|
||||
);
|
||||
|
||||
return toolSetToDescriptors(toolSet, ToolCategory.DASHBOARD);
|
||||
return toolSetToDescriptors(toolSet, ToolCategory.DASHBOARD, {
|
||||
includeSchemas: options?.includeSchemas ?? true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { camelToSnakeCase, isDefined } from 'twenty-shared/utils';
|
|||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
type GenerateDescriptorOptions,
|
||||
type ToolProvider,
|
||||
type ToolProviderContext,
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
|
|
@ -21,7 +22,10 @@ import { DeleteToolInputSchema } from 'src/engine/core-modules/record-crud/zod-s
|
|||
import { FindOneToolInputSchema } from 'src/engine/core-modules/record-crud/zod-schemas/find-one-tool.zod-schema';
|
||||
import { generateFindToolInputSchema } from 'src/engine/core-modules/record-crud/zod-schemas/find-tool.zod-schema';
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import {
|
||||
type ToolDescriptor,
|
||||
type ToolIndexEntry,
|
||||
} from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import { isFavoriteRelatedObject } from 'src/engine/metadata-modules/ai/ai-agent/utils/is-favorite-related-object.util';
|
||||
import { isWorkflowRelatedObject } from 'src/engine/metadata-modules/ai/ai-agent/utils/is-workflow-related-object.util';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
|
|
@ -43,8 +47,10 @@ export class DatabaseToolProvider implements ToolProvider {
|
|||
|
||||
async generateDescriptors(
|
||||
context: ToolProviderContext,
|
||||
): Promise<ToolDescriptor[]> {
|
||||
const descriptors: ToolDescriptor[] = [];
|
||||
options?: GenerateDescriptorOptions,
|
||||
): Promise<(ToolIndexEntry | ToolDescriptor)[]> {
|
||||
const includeSchemas = options?.includeSchemas ?? true;
|
||||
const descriptors: (ToolIndexEntry | ToolDescriptor)[] = [];
|
||||
|
||||
if (!isDefined(context.userId) || !isDefined(context.userWorkspaceId)) {
|
||||
return descriptors;
|
||||
|
|
@ -109,9 +115,11 @@ export class DatabaseToolProvider implements ToolProvider {
|
|||
name: `find_${snakePlural}`,
|
||||
description: `Search for ${objectMetadata.labelPlural} records using flexible filtering criteria. Supports exact matches, pattern matching, ranges, and null checks. Use limit/offset for pagination and orderBy for sorting. To find by ID, use filter: { id: { eq: "record-id" } }. Returns an array of matching records with their full data.`,
|
||||
category: ToolCategory.DATABASE_CRUD,
|
||||
inputSchema: z.toJSONSchema(
|
||||
generateFindToolInputSchema(objectMetadata, restrictedFields),
|
||||
),
|
||||
...(includeSchemas && {
|
||||
inputSchema: z.toJSONSchema(
|
||||
generateFindToolInputSchema(objectMetadata, restrictedFields),
|
||||
),
|
||||
}),
|
||||
executionRef: {
|
||||
kind: 'database_crud',
|
||||
objectNameSingular: objectMetadata.nameSingular,
|
||||
|
|
@ -125,7 +133,9 @@ export class DatabaseToolProvider implements ToolProvider {
|
|||
name: `find_one_${snakeSingular}`,
|
||||
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.`,
|
||||
category: ToolCategory.DATABASE_CRUD,
|
||||
inputSchema: z.toJSONSchema(FindOneToolInputSchema),
|
||||
...(includeSchemas && {
|
||||
inputSchema: z.toJSONSchema(FindOneToolInputSchema),
|
||||
}),
|
||||
executionRef: {
|
||||
kind: 'database_crud',
|
||||
objectNameSingular: objectMetadata.nameSingular,
|
||||
|
|
@ -141,9 +151,11 @@ export class DatabaseToolProvider implements ToolProvider {
|
|||
name: `create_${snakeSingular}`,
|
||||
description: `Create a new ${objectMetadata.labelSingular} record. Provide all required fields and any optional fields you want to set. The system will automatically handle timestamps and IDs. Returns the created record with all its data.`,
|
||||
category: ToolCategory.DATABASE_CRUD,
|
||||
inputSchema: z.toJSONSchema(
|
||||
generateCreateRecordInputSchema(objectMetadata, restrictedFields),
|
||||
),
|
||||
...(includeSchemas && {
|
||||
inputSchema: z.toJSONSchema(
|
||||
generateCreateRecordInputSchema(objectMetadata, restrictedFields),
|
||||
),
|
||||
}),
|
||||
executionRef: {
|
||||
kind: 'database_crud',
|
||||
objectNameSingular: objectMetadata.nameSingular,
|
||||
|
|
@ -157,12 +169,14 @@ export class DatabaseToolProvider implements ToolProvider {
|
|||
name: `create_many_${snakePlural}`,
|
||||
description: `Create multiple ${objectMetadata.labelPlural} records in a single call. Provide an array of records, each containing the required fields. Maximum 20 records per call. Returns the created records.`,
|
||||
category: ToolCategory.DATABASE_CRUD,
|
||||
inputSchema: z.toJSONSchema(
|
||||
generateCreateManyRecordInputSchema(
|
||||
objectMetadata,
|
||||
restrictedFields,
|
||||
...(includeSchemas && {
|
||||
inputSchema: z.toJSONSchema(
|
||||
generateCreateManyRecordInputSchema(
|
||||
objectMetadata,
|
||||
restrictedFields,
|
||||
),
|
||||
),
|
||||
),
|
||||
}),
|
||||
executionRef: {
|
||||
kind: 'database_crud',
|
||||
objectNameSingular: objectMetadata.nameSingular,
|
||||
|
|
@ -176,9 +190,11 @@ export class DatabaseToolProvider implements ToolProvider {
|
|||
name: `update_${snakeSingular}`,
|
||||
description: `Update an existing ${objectMetadata.labelSingular} record. Provide the record ID and only the fields you want to change. Unspecified fields will remain unchanged. Returns the updated record with all current data.`,
|
||||
category: ToolCategory.DATABASE_CRUD,
|
||||
inputSchema: z.toJSONSchema(
|
||||
generateUpdateRecordInputSchema(objectMetadata, restrictedFields),
|
||||
),
|
||||
...(includeSchemas && {
|
||||
inputSchema: z.toJSONSchema(
|
||||
generateUpdateRecordInputSchema(objectMetadata, restrictedFields),
|
||||
),
|
||||
}),
|
||||
executionRef: {
|
||||
kind: 'database_crud',
|
||||
objectNameSingular: objectMetadata.nameSingular,
|
||||
|
|
@ -192,12 +208,14 @@ export class DatabaseToolProvider implements ToolProvider {
|
|||
name: `update_many_${snakePlural}`,
|
||||
description: `Update multiple ${objectMetadata.labelPlural} records matching a filter in a single operation. All matching records will receive the same field values. WARNING: Use specific filters to avoid unintended mass updates. Always verify the filter scope with a find query first. Returns the updated records.`,
|
||||
category: ToolCategory.DATABASE_CRUD,
|
||||
inputSchema: z.toJSONSchema(
|
||||
generateUpdateManyRecordInputSchema(
|
||||
objectMetadata,
|
||||
restrictedFields,
|
||||
...(includeSchemas && {
|
||||
inputSchema: z.toJSONSchema(
|
||||
generateUpdateManyRecordInputSchema(
|
||||
objectMetadata,
|
||||
restrictedFields,
|
||||
),
|
||||
),
|
||||
),
|
||||
}),
|
||||
executionRef: {
|
||||
kind: 'database_crud',
|
||||
objectNameSingular: objectMetadata.nameSingular,
|
||||
|
|
@ -213,7 +231,9 @@ export class DatabaseToolProvider implements ToolProvider {
|
|||
name: `delete_${snakeSingular}`,
|
||||
description: `Delete a ${objectMetadata.labelSingular} record by marking it as deleted. The record is hidden from normal queries. This is reversible. Use this to remove records.`,
|
||||
category: ToolCategory.DATABASE_CRUD,
|
||||
inputSchema: z.toJSONSchema(DeleteToolInputSchema),
|
||||
...(includeSchemas && {
|
||||
inputSchema: z.toJSONSchema(DeleteToolInputSchema),
|
||||
}),
|
||||
executionRef: {
|
||||
kind: 'database_crud',
|
||||
objectNameSingular: objectMetadata.nameSingular,
|
||||
|
|
|
|||
|
|
@ -3,12 +3,16 @@ import { Injectable } from '@nestjs/common';
|
|||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
type GenerateDescriptorOptions,
|
||||
type ToolProvider,
|
||||
type ToolProviderContext,
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import {
|
||||
type ToolDescriptor,
|
||||
type ToolIndexEntry,
|
||||
} from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
|
||||
|
||||
|
|
@ -26,7 +30,10 @@ export class LogicFunctionToolProvider implements ToolProvider {
|
|||
|
||||
async generateDescriptors(
|
||||
context: ToolProviderContext,
|
||||
): Promise<ToolDescriptor[]> {
|
||||
options?: GenerateDescriptorOptions,
|
||||
): Promise<(ToolIndexEntry | ToolDescriptor)[]> {
|
||||
const includeSchemas = options?.includeSchemas ?? true;
|
||||
|
||||
const { flatLogicFunctionMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
|
|
@ -42,29 +49,34 @@ export class LogicFunctionToolProvider implements ToolProvider {
|
|||
isDefined(fn) && fn.isTool === true && fn.deletedAt === null,
|
||||
);
|
||||
|
||||
const descriptors: ToolDescriptor[] = [];
|
||||
const descriptors: (ToolIndexEntry | ToolDescriptor)[] = [];
|
||||
|
||||
for (const logicFunction of logicFunctionsWithSchema) {
|
||||
const toolName = this.buildLogicFunctionToolName(logicFunction.name);
|
||||
|
||||
// Logic functions already store JSON Schema -- use it directly
|
||||
const inputSchema = (logicFunction.toolInputSchema as object) ?? {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
};
|
||||
|
||||
descriptors.push({
|
||||
const base: ToolIndexEntry = {
|
||||
name: toolName,
|
||||
description:
|
||||
logicFunction.description ||
|
||||
`Execute the ${logicFunction.name} logic function`,
|
||||
category: ToolCategory.LOGIC_FUNCTION,
|
||||
inputSchema,
|
||||
executionRef: {
|
||||
kind: 'logic_function',
|
||||
logicFunctionId: logicFunction.id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (includeSchemas) {
|
||||
// Logic functions already store JSON Schema -- use it directly
|
||||
const inputSchema = (logicFunction.toolInputSchema as object) ?? {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
};
|
||||
|
||||
descriptors.push({ ...base, inputSchema });
|
||||
} else {
|
||||
descriptors.push(base);
|
||||
}
|
||||
}
|
||||
|
||||
return descriptors;
|
||||
|
|
|
|||
|
|
@ -3,13 +3,17 @@ import { Injectable, OnModuleInit } from '@nestjs/common';
|
|||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import {
|
||||
type GenerateDescriptorOptions,
|
||||
type ToolProvider,
|
||||
type ToolProviderContext,
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
|
||||
import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import {
|
||||
type ToolDescriptor,
|
||||
type ToolIndexEntry,
|
||||
} from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import { toolSetToDescriptors } from 'src/engine/core-modules/tool-provider/utils/tool-set-to-descriptors.util';
|
||||
import { FieldMetadataToolsFactory } from 'src/engine/metadata-modules/field-metadata/tools/field-metadata-tools.factory';
|
||||
import { ObjectMetadataToolsFactory } from 'src/engine/metadata-modules/object-metadata/tools/object-metadata-tools.factory';
|
||||
|
|
@ -49,12 +53,15 @@ export class MetadataToolProvider implements ToolProvider, OnModuleInit {
|
|||
|
||||
async generateDescriptors(
|
||||
context: ToolProviderContext,
|
||||
): Promise<ToolDescriptor[]> {
|
||||
options?: GenerateDescriptorOptions,
|
||||
): Promise<(ToolIndexEntry | ToolDescriptor)[]> {
|
||||
const toolSet = {
|
||||
...this.objectMetadataToolsFactory.generateTools(context.workspaceId),
|
||||
...this.fieldMetadataToolsFactory.generateTools(context.workspaceId),
|
||||
};
|
||||
|
||||
return toolSetToDescriptors(toolSet, ToolCategory.METADATA);
|
||||
return toolSetToDescriptors(toolSet, ToolCategory.METADATA, {
|
||||
includeSchemas: options?.includeSchemas ?? true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,17 @@ import { Injectable, OnModuleInit } from '@nestjs/common';
|
|||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import {
|
||||
type GenerateDescriptorOptions,
|
||||
type ToolProvider,
|
||||
type ToolProviderContext,
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
|
||||
import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import {
|
||||
type ToolDescriptor,
|
||||
type ToolIndexEntry,
|
||||
} from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import { toolSetToDescriptors } from 'src/engine/core-modules/tool-provider/utils/tool-set-to-descriptors.util';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
import { ViewToolsFactory } from 'src/engine/metadata-modules/view/tools/view-tools.factory';
|
||||
|
|
@ -65,8 +69,12 @@ export class ViewToolProvider implements ToolProvider, OnModuleInit {
|
|||
|
||||
async generateDescriptors(
|
||||
context: ToolProviderContext,
|
||||
): Promise<ToolDescriptor[]> {
|
||||
options?: GenerateDescriptorOptions,
|
||||
): Promise<(ToolIndexEntry | ToolDescriptor)[]> {
|
||||
const workspaceMemberId = context.actorContext?.workspaceMemberId;
|
||||
const schemaOptions = {
|
||||
includeSchemas: options?.includeSchemas ?? true,
|
||||
};
|
||||
|
||||
const readTools = this.viewToolsFactory.generateReadTools(
|
||||
context.workspaceId,
|
||||
|
|
@ -90,9 +98,10 @@ export class ViewToolProvider implements ToolProvider, OnModuleInit {
|
|||
return toolSetToDescriptors(
|
||||
{ ...readTools, ...writeTools },
|
||||
ToolCategory.VIEW,
|
||||
schemaOptions,
|
||||
);
|
||||
}
|
||||
|
||||
return toolSetToDescriptors(readTools, ToolCategory.VIEW);
|
||||
return toolSetToDescriptors(readTools, ToolCategory.VIEW, schemaOptions);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Inject, Injectable, OnModuleInit, Optional } from '@nestjs/common';
|
|||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import {
|
||||
type GenerateDescriptorOptions,
|
||||
type ToolProvider,
|
||||
type ToolProviderContext,
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
|
|
@ -10,7 +11,10 @@ import {
|
|||
import { WORKFLOW_TOOL_SERVICE_TOKEN } from 'src/engine/core-modules/tool-provider/constants/workflow-tool-service.token';
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
|
||||
import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import {
|
||||
type ToolDescriptor,
|
||||
type ToolIndexEntry,
|
||||
} from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import { toolSetToDescriptors } from 'src/engine/core-modules/tool-provider/utils/tool-set-to-descriptors.util';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
import type { WorkflowToolWorkspaceService } from 'src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service';
|
||||
|
|
@ -56,7 +60,8 @@ export class WorkflowToolProvider implements ToolProvider, OnModuleInit {
|
|||
|
||||
async generateDescriptors(
|
||||
context: ToolProviderContext,
|
||||
): Promise<ToolDescriptor[]> {
|
||||
options?: GenerateDescriptorOptions,
|
||||
): Promise<(ToolIndexEntry | ToolDescriptor)[]> {
|
||||
if (!this.workflowToolService) {
|
||||
return [];
|
||||
}
|
||||
|
|
@ -66,6 +71,8 @@ export class WorkflowToolProvider implements ToolProvider, OnModuleInit {
|
|||
context.rolePermissionConfig,
|
||||
);
|
||||
|
||||
return toolSetToDescriptors(toolSet, ToolCategory.WORKFLOW);
|
||||
return toolSetToDescriptors(toolSet, ToolCategory.WORKFLOW, {
|
||||
includeSchemas: options?.includeSchemas ?? true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { UseGuards } from '@nestjs/common';
|
||||
import { Field, ObjectType, Query } from '@nestjs/graphql';
|
||||
import { Args, Field, ObjectType, Query } from '@nestjs/graphql';
|
||||
|
||||
import graphqlTypeJson from 'graphql-type-json';
|
||||
|
||||
|
|
@ -61,4 +61,34 @@ export class ToolIndexResolver {
|
|||
userWorkspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
// Resolves the inputSchema for a single tool on demand (avoids computing
|
||||
// schemas for every tool in the workspace when listing the tool index).
|
||||
@Query(() => graphqlTypeJson, { nullable: true })
|
||||
@UseGuards(NoPermissionGuard)
|
||||
async getToolInputSchema(
|
||||
@Args('toolName') toolName: string,
|
||||
@AuthUser({ allowUndefined: true }) user: UserEntity | undefined,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
): Promise<object | null> {
|
||||
const roleId = await this.userRoleService.getRoleIdForUserWorkspace({
|
||||
userWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
if (!roleId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const schemas = await this.toolRegistryService.resolveSchemas([toolName], {
|
||||
workspaceId: workspace.id,
|
||||
roleId,
|
||||
rolePermissionConfig: { unionOf: [roleId] },
|
||||
userId: user?.id,
|
||||
userWorkspaceId,
|
||||
});
|
||||
|
||||
return schemas.get(toolName) ?? null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,10 @@ import { FindRecordsService } from 'src/engine/core-modules/record-crud/services
|
|||
import { UpdateManyRecordsService } from 'src/engine/core-modules/record-crud/services/update-many-records.service';
|
||||
import { UpdateRecordService } from 'src/engine/core-modules/record-crud/services/update-record.service';
|
||||
import { type ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import {
|
||||
type ToolDescriptor,
|
||||
type ToolIndexEntry,
|
||||
} from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
|
||||
import { stripLoadingMessage } from 'src/engine/core-modules/tool/utils/wrap-tool-for-execution.util';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
|
|
@ -77,7 +80,7 @@ export class ToolExecutorService {
|
|||
}
|
||||
|
||||
async dispatch(
|
||||
descriptor: ToolDescriptor,
|
||||
descriptor: ToolIndexEntry | ToolDescriptor,
|
||||
args: Record<string, unknown>,
|
||||
context: ToolProviderContext,
|
||||
): Promise<unknown> {
|
||||
|
|
@ -194,7 +197,7 @@ export class ToolExecutorService {
|
|||
}
|
||||
|
||||
private async dispatchStaticTool(
|
||||
descriptor: ToolDescriptor,
|
||||
descriptor: ToolIndexEntry | ToolDescriptor,
|
||||
args: Record<string, unknown>,
|
||||
context: ToolProviderContext,
|
||||
): Promise<unknown> {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { type ToolCallOptions, type ToolSet, jsonSchema } from 'ai';
|
||||
import { type ActorMetadata } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
type CodeExecutionStreamEmitter,
|
||||
|
|
@ -14,107 +13,102 @@ import {
|
|||
import { TOOL_PROVIDERS } from 'src/engine/core-modules/tool-provider/constants/tool-providers.token';
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { compactToolOutput } from 'src/engine/core-modules/tool-provider/output-serialization/compact-tool-output.util';
|
||||
import { NativeModelToolProvider } from 'src/engine/core-modules/tool-provider/providers/native-model-tool.provider';
|
||||
import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
|
||||
import { type ExecuteToolResult } from 'src/engine/core-modules/tool-provider/tools/execute-tool.tool';
|
||||
import { type LearnToolsAspect } from 'src/engine/core-modules/tool-provider/tools/learn-tools.tool';
|
||||
import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import { type ToolContext } from 'src/engine/core-modules/tool-provider/types/tool-context.type';
|
||||
import {
|
||||
type ToolDescriptor,
|
||||
type ToolIndexEntry,
|
||||
} from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import {
|
||||
generateErrorSuggestion,
|
||||
wrapWithErrorHandler,
|
||||
} from 'src/engine/core-modules/tool-provider/utils/tool-error.util';
|
||||
import { wrapJsonSchemaForExecution } from 'src/engine/core-modules/tool/utils/wrap-tool-for-execution.util';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import { NativeModelToolProvider } from 'src/engine/core-modules/tool-provider/providers/native-model-tool.provider';
|
||||
|
||||
// Backward-compatible alias -- consumers can import this instead of ToolDescriptor
|
||||
export type ToolIndexEntry = ToolDescriptor;
|
||||
|
||||
export type ToolSearchOptions = {
|
||||
limit?: number;
|
||||
category?: ToolCategory;
|
||||
};
|
||||
|
||||
export type ToolContext = {
|
||||
workspaceId: string;
|
||||
roleId: string;
|
||||
actorContext?: ActorMetadata;
|
||||
userId?: string;
|
||||
userWorkspaceId?: string;
|
||||
onCodeExecutionUpdate?: CodeExecutionStreamEmitter;
|
||||
};
|
||||
|
||||
const RAM_TTL_MS = 5_000;
|
||||
const REDIS_TTL_MS = 300_000;
|
||||
export { type ToolContext } from 'src/engine/core-modules/tool-provider/types/tool-context.type';
|
||||
|
||||
@Injectable()
|
||||
export class ToolRegistryService {
|
||||
private readonly logger = new Logger(ToolRegistryService.name);
|
||||
|
||||
// Two-tier cache: RAM (5s) → Redis (5min) → generate from providers
|
||||
private readonly ramCache = new Map<
|
||||
string,
|
||||
{ descriptors: ToolDescriptor[]; cachedAt: number }
|
||||
>();
|
||||
|
||||
constructor(
|
||||
@Inject(TOOL_PROVIDERS)
|
||||
private readonly providers: ToolProvider[],
|
||||
private readonly nativeModelToolProvider: NativeModelToolProvider,
|
||||
private readonly toolExecutorService: ToolExecutorService,
|
||||
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
||||
) {}
|
||||
|
||||
// Core: returns cached ToolDescriptor[] for a workspace+role+user
|
||||
async getCatalog(context: ToolProviderContext): Promise<ToolDescriptor[]> {
|
||||
const cacheKey = await this.buildCacheKey(context);
|
||||
// Returns ToolIndexEntry[] (lightweight, no schemas).
|
||||
// Underlying data (metadata, permissions) is already cached by WorkspaceCacheService.
|
||||
// Providers run in parallel since they are independent.
|
||||
async getCatalog(context: ToolProviderContext): Promise<ToolIndexEntry[]> {
|
||||
const results = await Promise.all(
|
||||
this.providers.map(async (provider) => {
|
||||
if (await provider.isAvailable(context)) {
|
||||
return provider.generateDescriptors(context, {
|
||||
includeSchemas: false,
|
||||
});
|
||||
}
|
||||
|
||||
// 1. RAM hit?
|
||||
const ramEntry = this.ramCache.get(cacheKey);
|
||||
return [];
|
||||
}),
|
||||
);
|
||||
|
||||
if (ramEntry && Date.now() - ramEntry.cachedAt < RAM_TTL_MS) {
|
||||
return ramEntry.descriptors;
|
||||
return results.flat();
|
||||
}
|
||||
|
||||
// On-demand schema generation for specific tools
|
||||
async resolveSchemas(
|
||||
toolNames: string[],
|
||||
context: ToolProviderContext,
|
||||
): Promise<Map<string, object>> {
|
||||
const index = await this.getCatalog(context);
|
||||
const nameSet = new Set(toolNames);
|
||||
const matchingEntries = index.filter((entry) => nameSet.has(entry.name));
|
||||
|
||||
// Group matching entries by provider category
|
||||
const byCategory = new Map<string, ToolIndexEntry[]>();
|
||||
|
||||
for (const entry of matchingEntries) {
|
||||
const existing = byCategory.get(entry.category) ?? [];
|
||||
|
||||
existing.push(entry);
|
||||
byCategory.set(entry.category, existing);
|
||||
}
|
||||
|
||||
// 2. Redis hit?
|
||||
const redisData =
|
||||
await this.workspaceCacheStorageService.getToolCatalog(cacheKey);
|
||||
const schemas = new Map<string, object>();
|
||||
|
||||
if (redisData) {
|
||||
const descriptors = redisData as ToolDescriptor[];
|
||||
for (const [category, entries] of byCategory) {
|
||||
const provider = this.providers.find(
|
||||
(providerItem) => providerItem.category === category,
|
||||
);
|
||||
|
||||
this.ramCache.set(cacheKey, {
|
||||
descriptors,
|
||||
cachedAt: Date.now(),
|
||||
if (!provider) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fullDescriptors = await provider.generateDescriptors(context, {
|
||||
includeSchemas: true,
|
||||
});
|
||||
|
||||
return descriptors;
|
||||
}
|
||||
const entryNameSet = new Set(entries.map((entry) => entry.name));
|
||||
|
||||
// 3. Generate from providers (cache miss)
|
||||
const descriptors: ToolDescriptor[] = [];
|
||||
|
||||
for (const provider of this.providers) {
|
||||
if (await provider.isAvailable(context)) {
|
||||
const providerDescriptors = await provider.generateDescriptors(context);
|
||||
|
||||
descriptors.push(...providerDescriptors);
|
||||
for (const descriptor of fullDescriptors) {
|
||||
if (
|
||||
entryNameSet.has(descriptor.name) &&
|
||||
'inputSchema' in descriptor &&
|
||||
descriptor.inputSchema
|
||||
) {
|
||||
schemas.set(descriptor.name, descriptor.inputSchema);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Generated ${descriptors.length} tool descriptors for workspace ${context.workspaceId}`,
|
||||
);
|
||||
|
||||
// Store in both caches
|
||||
this.ramCache.set(cacheKey, {
|
||||
descriptors,
|
||||
cachedAt: Date.now(),
|
||||
});
|
||||
|
||||
await this.workspaceCacheStorageService.setToolCatalog(
|
||||
cacheKey,
|
||||
descriptors,
|
||||
REDIS_TTL_MS,
|
||||
);
|
||||
|
||||
return descriptors;
|
||||
return schemas;
|
||||
}
|
||||
|
||||
// Hydrate ToolDescriptor[] into an AI SDK ToolSet with thin dispatch closures
|
||||
|
|
@ -126,7 +120,6 @@ export class ToolRegistryService {
|
|||
const toolSet: ToolSet = {};
|
||||
|
||||
for (const descriptor of descriptors) {
|
||||
// Add loadingMessage to the clean stored schema
|
||||
const schemaWithLoading = wrapJsonSchemaForExecution(
|
||||
descriptor.inputSchema as Record<string, unknown>,
|
||||
);
|
||||
|
|
@ -140,7 +133,7 @@ export class ToolRegistryService {
|
|||
description: descriptor.description,
|
||||
inputSchema: jsonSchema(schemaWithLoading),
|
||||
execute: options?.wrapWithErrorContext
|
||||
? this.wrapWithErrorHandler(descriptor.name, executeFn)
|
||||
? wrapWithErrorHandler(descriptor.name, executeFn)
|
||||
: executeFn,
|
||||
};
|
||||
}
|
||||
|
|
@ -152,7 +145,7 @@ export class ToolRegistryService {
|
|||
workspaceId: string,
|
||||
roleId: string,
|
||||
options?: { userId?: string; userWorkspaceId?: string },
|
||||
): Promise<ToolDescriptor[]> {
|
||||
): Promise<ToolIndexEntry[]> {
|
||||
const context = this.buildContext(
|
||||
workspaceId,
|
||||
roleId,
|
||||
|
|
@ -164,81 +157,6 @@ export class ToolRegistryService {
|
|||
return this.getCatalog(context);
|
||||
}
|
||||
|
||||
async searchTools(
|
||||
query: string,
|
||||
workspaceId: string,
|
||||
roleId: string,
|
||||
options: ToolSearchOptions & {
|
||||
userId?: string;
|
||||
userWorkspaceId?: string;
|
||||
} = {},
|
||||
): Promise<ToolDescriptor[]> {
|
||||
const { limit = 5, category, userId, userWorkspaceId } = options;
|
||||
const context = this.buildContext(
|
||||
workspaceId,
|
||||
roleId,
|
||||
undefined,
|
||||
userId,
|
||||
userWorkspaceId,
|
||||
);
|
||||
|
||||
const descriptors = await this.getCatalog(context);
|
||||
|
||||
const queryLower = query.toLowerCase();
|
||||
const queryTerms = queryLower
|
||||
.split(/\s+/)
|
||||
.filter((term) => term.length > 2);
|
||||
|
||||
const scored = descriptors
|
||||
.filter((tool) => !category || tool.category === category)
|
||||
.map((tool) => {
|
||||
let score = 0;
|
||||
const nameLower = tool.name.toLowerCase();
|
||||
const descLower = tool.description.toLowerCase();
|
||||
const objectLower = tool.objectName?.toLowerCase() ?? '';
|
||||
|
||||
if (nameLower.includes(queryLower)) {
|
||||
score += 100;
|
||||
}
|
||||
|
||||
if (objectLower && queryLower.includes(objectLower)) {
|
||||
score += 80;
|
||||
}
|
||||
|
||||
for (const term of queryTerms) {
|
||||
if (nameLower.includes(term)) {
|
||||
score += 30;
|
||||
}
|
||||
if (objectLower.includes(term)) {
|
||||
score += 25;
|
||||
}
|
||||
if (descLower.includes(term)) {
|
||||
score += 10;
|
||||
}
|
||||
}
|
||||
|
||||
const operations = ['find', 'create', 'update', 'delete', 'search'];
|
||||
|
||||
for (const op of operations) {
|
||||
if (queryLower.includes(op) && nameLower.includes(op)) {
|
||||
score += 40;
|
||||
}
|
||||
}
|
||||
|
||||
return { tool, score };
|
||||
})
|
||||
.filter((item) => item.score > 0)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, limit)
|
||||
.map((item) => item.tool);
|
||||
|
||||
this.logger.log(
|
||||
`Tool search for "${query}" returned ${scored.length} results`,
|
||||
);
|
||||
|
||||
return scored;
|
||||
}
|
||||
|
||||
async getToolsByName(
|
||||
names: string[],
|
||||
context: ToolContext,
|
||||
|
|
@ -251,13 +169,20 @@ export class ToolRegistryService {
|
|||
context.userWorkspaceId,
|
||||
);
|
||||
|
||||
const descriptors = await this.getCatalog(fullContext);
|
||||
const index = await this.getCatalog(fullContext);
|
||||
const nameSet = new Set(names);
|
||||
const filtered = descriptors.filter((descriptor) =>
|
||||
nameSet.has(descriptor.name),
|
||||
);
|
||||
const matchingEntries = index.filter((entry) => nameSet.has(entry.name));
|
||||
|
||||
return this.hydrateToolSet(filtered, fullContext);
|
||||
const schemas = await this.resolveSchemas(names, fullContext);
|
||||
|
||||
const descriptors: ToolDescriptor[] = matchingEntries
|
||||
.filter((entry) => schemas.has(entry.name))
|
||||
.map((entry) => ({
|
||||
...entry,
|
||||
inputSchema: schemas.get(entry.name)!,
|
||||
}));
|
||||
|
||||
return this.hydrateToolSet(descriptors, fullContext);
|
||||
}
|
||||
|
||||
async getToolInfo(
|
||||
|
|
@ -275,12 +200,17 @@ export class ToolRegistryService {
|
|||
context.userWorkspaceId,
|
||||
);
|
||||
|
||||
const descriptors = await this.getCatalog(fullContext);
|
||||
|
||||
const index = await this.getCatalog(fullContext);
|
||||
const nameSet = new Set(names);
|
||||
const filtered = descriptors.filter((entry) => nameSet.has(entry.name));
|
||||
const matchingEntries = index.filter((entry) => nameSet.has(entry.name));
|
||||
|
||||
return filtered.map((entry) => {
|
||||
let schemas: Map<string, object> | undefined;
|
||||
|
||||
if (aspects.includes('schema')) {
|
||||
schemas = await this.resolveSchemas(names, fullContext);
|
||||
}
|
||||
|
||||
return matchingEntries.map((entry) => {
|
||||
const info: {
|
||||
name: string;
|
||||
description?: string;
|
||||
|
|
@ -291,8 +221,8 @@ export class ToolRegistryService {
|
|||
info.description = entry.description;
|
||||
}
|
||||
|
||||
if (aspects.includes('schema')) {
|
||||
info.inputSchema = entry.inputSchema;
|
||||
if (aspects.includes('schema') && schemas) {
|
||||
info.inputSchema = schemas.get(entry.name);
|
||||
}
|
||||
|
||||
return info;
|
||||
|
|
@ -314,10 +244,10 @@ export class ToolRegistryService {
|
|||
context.userWorkspaceId,
|
||||
);
|
||||
|
||||
const descriptors = await this.getCatalog(fullContext);
|
||||
const descriptor = descriptors.find((desc) => desc.name === toolName);
|
||||
const index = await this.getCatalog(fullContext);
|
||||
const entry = index.find((indexEntry) => indexEntry.name === toolName);
|
||||
|
||||
if (!descriptor) {
|
||||
if (!entry) {
|
||||
return {
|
||||
toolName,
|
||||
error: {
|
||||
|
|
@ -329,7 +259,7 @@ export class ToolRegistryService {
|
|||
}
|
||||
|
||||
const result = await this.toolExecutorService.dispatch(
|
||||
descriptor,
|
||||
entry,
|
||||
args,
|
||||
fullContext,
|
||||
);
|
||||
|
|
@ -348,33 +278,41 @@ export class ToolRegistryService {
|
|||
toolName,
|
||||
error: {
|
||||
message: errorMessage,
|
||||
suggestion: this.generateErrorSuggestion(toolName, errorMessage),
|
||||
suggestion: generateErrorSuggestion(toolName, errorMessage),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Main method for eager loading tools by categories
|
||||
// Eager loading tools by categories (MCP, workflow agent).
|
||||
// These paths need full schemas, so generate with includeSchemas: true.
|
||||
async getToolsByCategories(
|
||||
context: ToolProviderContext,
|
||||
options: ToolRetrievalOptions = {},
|
||||
): Promise<ToolSet> {
|
||||
const { categories, excludeTools, wrapWithErrorContext } = options;
|
||||
const descriptors = await this.getCatalog(context);
|
||||
const categorySet = categories ? new Set(categories) : undefined;
|
||||
|
||||
let filteredDescriptors: ToolDescriptor[];
|
||||
const results = await Promise.all(
|
||||
this.providers
|
||||
.filter(
|
||||
(provider) => !categorySet || categorySet.has(provider.category),
|
||||
)
|
||||
.map(async (provider) => {
|
||||
if (await provider.isAvailable(context)) {
|
||||
return provider.generateDescriptors(context, {
|
||||
includeSchemas: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (categories) {
|
||||
const categorySet = new Set(categories);
|
||||
return [];
|
||||
}),
|
||||
);
|
||||
|
||||
filteredDescriptors = descriptors.filter((descriptor) =>
|
||||
categorySet.has(descriptor.category),
|
||||
);
|
||||
} else {
|
||||
filteredDescriptors = [...descriptors];
|
||||
}
|
||||
const descriptors = results.flat() as ToolDescriptor[];
|
||||
|
||||
let filteredDescriptors = descriptors;
|
||||
|
||||
// Apply excludeTools filter
|
||||
if (excludeTools?.length) {
|
||||
const excludeSet = new Set(excludeTools);
|
||||
|
||||
|
|
@ -387,7 +325,6 @@ export class ToolRegistryService {
|
|||
wrapWithErrorContext,
|
||||
});
|
||||
|
||||
// Handle NativeModelToolProvider separately (SDK-opaque tools)
|
||||
if (categories?.includes(ToolCategory.NATIVE_MODEL)) {
|
||||
if (await this.nativeModelToolProvider.isAvailable(context)) {
|
||||
const nativeTools = await (
|
||||
|
|
@ -405,15 +342,6 @@ export class ToolRegistryService {
|
|||
return toolSet;
|
||||
}
|
||||
|
||||
private async buildCacheKey(context: ToolProviderContext): Promise<string> {
|
||||
const metadataVersion =
|
||||
(await this.workspaceCacheStorageService.getMetadataVersion(
|
||||
context.workspaceId,
|
||||
)) ?? 0;
|
||||
|
||||
return `${context.workspaceId}:v${metadataVersion}:${context.roleId}:${context.userId ?? 'system'}`;
|
||||
}
|
||||
|
||||
private buildContext(
|
||||
workspaceId: string,
|
||||
roleId: string,
|
||||
|
|
@ -434,66 +362,4 @@ export class ToolRegistryService {
|
|||
onCodeExecutionUpdate,
|
||||
};
|
||||
}
|
||||
|
||||
private wrapWithErrorHandler(
|
||||
toolName: string,
|
||||
executeFn: (args: Record<string, unknown>) => Promise<unknown>,
|
||||
): (args: Record<string, unknown>) => Promise<unknown> {
|
||||
return async (args: Record<string, unknown>) => {
|
||||
try {
|
||||
return await executeFn(args);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: errorMessage,
|
||||
tool: toolName,
|
||||
suggestion: this.generateErrorSuggestion(toolName, errorMessage),
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private generateErrorSuggestion(
|
||||
_toolName: string,
|
||||
errorMessage: string,
|
||||
): string {
|
||||
const lowerError = errorMessage.toLowerCase();
|
||||
|
||||
if (
|
||||
lowerError.includes('not found') ||
|
||||
lowerError.includes('does not exist')
|
||||
) {
|
||||
return 'Verify the ID or name exists with a search query first';
|
||||
}
|
||||
|
||||
if (
|
||||
lowerError.includes('permission') ||
|
||||
lowerError.includes('forbidden') ||
|
||||
lowerError.includes('unauthorized')
|
||||
) {
|
||||
return 'This operation requires elevated permissions or a different role';
|
||||
}
|
||||
|
||||
if (lowerError.includes('invalid') || lowerError.includes('validation')) {
|
||||
return 'Check the tool schema for valid parameter formats and types';
|
||||
}
|
||||
|
||||
if (
|
||||
lowerError.includes('duplicate') ||
|
||||
lowerError.includes('already exists')
|
||||
) {
|
||||
return 'A record with this identifier already exists. Try updating instead of creating';
|
||||
}
|
||||
|
||||
if (lowerError.includes('required') || lowerError.includes('missing')) {
|
||||
return 'Required fields are missing. Check which fields are mandatory for this operation';
|
||||
}
|
||||
|
||||
return 'Try adjusting the parameters or using a different approach';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/
|
|||
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||
import { ViewModule } from 'src/engine/metadata-modules/view/view.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
|
||||
import { ToolIndexResolver } from './resolvers/tool-index.resolver';
|
||||
import { ToolRegistryService } from './services/tool-registry.service';
|
||||
|
|
@ -47,7 +46,6 @@ import { ToolRegistryService } from './services/tool-registry.service';
|
|||
PermissionsModule,
|
||||
ViewModule,
|
||||
WorkspaceCacheModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
LogicFunctionModule,
|
||||
UserRoleModule,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import { type ToolCallOptions, type ToolSet } from 'ai';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
type ToolContext,
|
||||
type ToolRegistryService,
|
||||
} from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
|
||||
import { type ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
|
||||
import { type ToolContext } from 'src/engine/core-modules/tool-provider/types/tool-context.type';
|
||||
|
||||
export const EXECUTE_TOOL_TOOL_NAME = 'execute_tool';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
type ToolContext,
|
||||
type ToolRegistryService,
|
||||
} from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
|
||||
import { type ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
|
||||
import { type ToolContext } from 'src/engine/core-modules/tool-provider/types/tool-context.type';
|
||||
|
||||
export const LEARN_TOOLS_TOOL_NAME = 'learn_tools';
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export const createLoadSkillTool = (loadSkills: LoadSkillFunction) => ({
|
|||
label: skill.label,
|
||||
content: skill.content,
|
||||
})),
|
||||
message: `Loaded ${skills.length} skill(s). Follow the instructions in the skill content.`,
|
||||
message: `Loaded ${skills.map((skill) => skill.label).join(', ')}`,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
import { type ActorMetadata } from 'twenty-shared/types';
|
||||
|
||||
import { type CodeExecutionStreamEmitter } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
|
||||
export type ToolContext = {
|
||||
workspaceId: string;
|
||||
roleId: string;
|
||||
actorContext?: ActorMetadata;
|
||||
userId?: string;
|
||||
userWorkspaceId?: string;
|
||||
onCodeExecutionUpdate?: CodeExecutionStreamEmitter;
|
||||
};
|
||||
|
|
@ -18,13 +18,17 @@ export type ToolExecutionRef =
|
|||
| { kind: 'static'; toolId: string }
|
||||
| { kind: 'logic_function'; logicFunctionId: string };
|
||||
|
||||
// Fully JSON-serializable tool definition, stored in Redis
|
||||
export type ToolDescriptor = {
|
||||
// Lightweight entry for catalog/index (no schema)
|
||||
export type ToolIndexEntry = {
|
||||
name: string;
|
||||
description: string;
|
||||
category: ToolCategory;
|
||||
inputSchema: object;
|
||||
executionRef: ToolExecutionRef;
|
||||
objectName?: string;
|
||||
operation?: string;
|
||||
};
|
||||
|
||||
// Full descriptor with schema (on-demand)
|
||||
export type ToolDescriptor = ToolIndexEntry & {
|
||||
inputSchema: object;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
export const generateErrorSuggestion = (
|
||||
_toolName: string,
|
||||
errorMessage: string,
|
||||
): string => {
|
||||
const lowerError = errorMessage.toLowerCase();
|
||||
|
||||
if (
|
||||
lowerError.includes('not found') ||
|
||||
lowerError.includes('does not exist')
|
||||
) {
|
||||
return 'Verify the ID or name exists with a search query first';
|
||||
}
|
||||
|
||||
if (
|
||||
lowerError.includes('permission') ||
|
||||
lowerError.includes('forbidden') ||
|
||||
lowerError.includes('unauthorized')
|
||||
) {
|
||||
return 'This operation requires elevated permissions or a different role';
|
||||
}
|
||||
|
||||
if (lowerError.includes('invalid') || lowerError.includes('validation')) {
|
||||
return 'Check the tool schema for valid parameter formats and types';
|
||||
}
|
||||
|
||||
if (
|
||||
lowerError.includes('duplicate') ||
|
||||
lowerError.includes('already exists')
|
||||
) {
|
||||
return 'A record with this identifier already exists. Try updating instead of creating';
|
||||
}
|
||||
|
||||
if (lowerError.includes('required') || lowerError.includes('missing')) {
|
||||
return 'Required fields are missing. Check which fields are mandatory for this operation';
|
||||
}
|
||||
|
||||
return 'Try adjusting the parameters or using a different approach';
|
||||
};
|
||||
|
||||
export const wrapWithErrorHandler = (
|
||||
toolName: string,
|
||||
executeFn: (args: Record<string, unknown>) => Promise<unknown>,
|
||||
): ((args: Record<string, unknown>) => Promise<unknown>) => {
|
||||
return async (args: Record<string, unknown>) => {
|
||||
try {
|
||||
return await executeFn(args);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: errorMessage,
|
||||
tool: toolName,
|
||||
suggestion: generateErrorSuggestion(toolName, errorMessage),
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
@ -2,7 +2,14 @@ import { type ToolSet } from 'ai';
|
|||
import { z } from 'zod';
|
||||
|
||||
import { type ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import {
|
||||
type ToolDescriptor,
|
||||
type ToolIndexEntry,
|
||||
} from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
|
||||
export type ToolSetToDescriptorsOptions = {
|
||||
includeSchemas?: boolean;
|
||||
};
|
||||
|
||||
// Converts a ToolSet (with Zod schemas and closures) into an array of
|
||||
// serializable ToolDescriptor objects. Used by providers that delegate to
|
||||
|
|
@ -10,8 +17,22 @@ import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types
|
|||
export const toolSetToDescriptors = (
|
||||
toolSet: ToolSet,
|
||||
category: ToolCategory,
|
||||
): ToolDescriptor[] => {
|
||||
options?: ToolSetToDescriptorsOptions,
|
||||
): (ToolIndexEntry | ToolDescriptor)[] => {
|
||||
const includeSchemas = options?.includeSchemas ?? true;
|
||||
|
||||
return Object.entries(toolSet).map(([name, tool]) => {
|
||||
const base: ToolIndexEntry = {
|
||||
name,
|
||||
description: tool.description ?? '',
|
||||
category,
|
||||
executionRef: { kind: 'static' as const, toolId: name },
|
||||
};
|
||||
|
||||
if (!includeSchemas) {
|
||||
return base;
|
||||
}
|
||||
|
||||
let inputSchema: object;
|
||||
|
||||
try {
|
||||
|
|
@ -22,11 +43,8 @@ export const toolSetToDescriptors = (
|
|||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
description: tool.description ?? '',
|
||||
category,
|
||||
...base,
|
||||
inputSchema,
|
||||
executionRef: { kind: 'static' as const, toolId: name },
|
||||
};
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { createUIMessageStream, pipeUIMessageStreamToResponse } from 'ai';
|
||||
|
|
@ -34,8 +34,6 @@ export type StreamAgentChatOptions = {
|
|||
|
||||
@Injectable()
|
||||
export class AgentChatStreamingService {
|
||||
private readonly logger = new Logger(AgentChatStreamingService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(AgentChatThreadEntity)
|
||||
private readonly threadRepository: Repository<AgentChatThreadEntity>,
|
||||
|
|
@ -65,6 +63,25 @@ export class AgentChatStreamingService {
|
|||
);
|
||||
}
|
||||
|
||||
// Fire user-message save without awaiting to avoid delaying time-to-first-letter.
|
||||
// The promise is awaited inside onFinish where we need the turnId.
|
||||
const lastUserText =
|
||||
messages[messages.length - 1]?.parts.find((part) => part.type === 'text')
|
||||
?.text ?? '';
|
||||
|
||||
const userMessagePromise = this.agentChatService.addMessage({
|
||||
threadId: thread.id,
|
||||
uiMessage: {
|
||||
role: AgentMessageRole.USER,
|
||||
parts: [{ type: 'text', text: lastUserText }],
|
||||
},
|
||||
});
|
||||
|
||||
// Prevent unhandled rejection if onFinish never runs (e.g. stream
|
||||
// setup error or empty response early-return). The real error still
|
||||
// surfaces when awaited in onFinish.
|
||||
userMessagePromise.catch(() => {});
|
||||
|
||||
try {
|
||||
const uiStream = createUIMessageStream<ExtendedUIMessage>({
|
||||
execute: async ({ writer }) => {
|
||||
|
|
@ -97,8 +114,6 @@ export class AgentChatStreamingService {
|
|||
writer.merge(
|
||||
stream.toUIMessageStream({
|
||||
onError: (error) => {
|
||||
this.logger.error('Stream error:', error);
|
||||
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
},
|
||||
sendStart: false,
|
||||
|
|
@ -179,57 +194,26 @@ export class AgentChatStreamingService {
|
|||
return;
|
||||
}
|
||||
|
||||
const validThreadId = thread.id;
|
||||
const userMessage = await userMessagePromise;
|
||||
|
||||
if (!validThreadId) {
|
||||
this.logger.error('Thread ID is unexpectedly null/undefined');
|
||||
await this.agentChatService.addMessage({
|
||||
threadId: thread.id,
|
||||
uiMessage: responseMessage,
|
||||
turnId: userMessage.turnId,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const userMessage = await this.agentChatService.addMessage({
|
||||
threadId: validThreadId,
|
||||
uiMessage: {
|
||||
role: AgentMessageRole.USER,
|
||||
parts: [
|
||||
{
|
||||
type: 'text',
|
||||
text:
|
||||
messages[messages.length - 1].parts.find(
|
||||
(part) => part.type === 'text',
|
||||
)?.text ?? '',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await this.agentChatService.addMessage({
|
||||
threadId: validThreadId,
|
||||
uiMessage: responseMessage,
|
||||
turnId: userMessage.turnId,
|
||||
});
|
||||
|
||||
await this.threadRepository.update(validThreadId, {
|
||||
totalInputTokens: () =>
|
||||
`"totalInputTokens" + ${streamUsage.inputTokens}`,
|
||||
totalOutputTokens: () =>
|
||||
`"totalOutputTokens" + ${streamUsage.outputTokens}`,
|
||||
totalInputCredits: () =>
|
||||
`"totalInputCredits" + ${streamUsage.inputCredits}`,
|
||||
totalOutputCredits: () =>
|
||||
`"totalOutputCredits" + ${streamUsage.outputCredits}`,
|
||||
contextWindowTokens: modelConfig.contextWindowTokens,
|
||||
conversationSize: lastStepConversationSize,
|
||||
});
|
||||
} catch (saveError) {
|
||||
this.logger.error(
|
||||
'Failed to save messages:',
|
||||
saveError instanceof Error
|
||||
? saveError.message
|
||||
: String(saveError),
|
||||
);
|
||||
}
|
||||
await this.threadRepository.update(thread.id, {
|
||||
totalInputTokens: () =>
|
||||
`"totalInputTokens" + ${streamUsage.inputTokens}`,
|
||||
totalOutputTokens: () =>
|
||||
`"totalOutputTokens" + ${streamUsage.outputTokens}`,
|
||||
totalInputCredits: () =>
|
||||
`"totalInputCredits" + ${streamUsage.inputCredits}`,
|
||||
totalOutputCredits: () =>
|
||||
`"totalOutputCredits" + ${streamUsage.outputCredits}`,
|
||||
contextWindowTokens: modelConfig.contextWindowTokens,
|
||||
conversationSize: lastStepConversationSize,
|
||||
});
|
||||
},
|
||||
sendReasoning: true,
|
||||
}),
|
||||
|
|
@ -237,13 +221,18 @@ export class AgentChatStreamingService {
|
|||
},
|
||||
});
|
||||
|
||||
pipeUIMessageStreamToResponse({ stream: uiStream, response });
|
||||
pipeUIMessageStreamToResponse({
|
||||
stream: uiStream,
|
||||
response,
|
||||
// Consume the stream independently so onFinish fires even if
|
||||
// the client disconnects (e.g., page refresh mid-stream)
|
||||
consumeSseStream: ({ stream }) => {
|
||||
stream.pipeTo(new WritableStream()).catch(() => {});
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
'Failed to stream chat:',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
response.end();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
|
|||
import { COMMON_PRELOAD_TOOLS } from 'src/engine/core-modules/tool-provider/constants/common-preload-tools.const';
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
|
||||
import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import { type ToolIndexEntry } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import {
|
||||
EXECUTE_TOOL_TOOL_NAME,
|
||||
LEARN_TOOLS_TOOL_NAME,
|
||||
|
|
@ -131,7 +131,7 @@ export class SystemPromptBuilderService {
|
|||
}
|
||||
|
||||
buildFullPrompt(
|
||||
toolCatalog: ToolDescriptor[],
|
||||
toolCatalog: ToolIndexEntry[],
|
||||
skillCatalog: FlatSkill[],
|
||||
preloadedTools: string[],
|
||||
contextString?: string,
|
||||
|
|
@ -242,12 +242,12 @@ ${skillsList}`;
|
|||
}
|
||||
|
||||
buildToolCatalogSection(
|
||||
toolCatalog: ToolDescriptor[],
|
||||
toolCatalog: ToolIndexEntry[],
|
||||
preloadedTools: string[],
|
||||
): string {
|
||||
const preloadedSet = new Set(preloadedTools);
|
||||
|
||||
const toolsByCategory = new Map<string, ToolDescriptor[]>();
|
||||
const toolsByCategory = new Map<string, ToolIndexEntry[]>();
|
||||
|
||||
for (const tool of toolCatalog) {
|
||||
const category = tool.category;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ export const METADATA_VERSIONED_WORKSPACE_CACHE_KEY = {
|
|||
MetadataObjectMetadataMaps: 'metadata:object-metadata-maps',
|
||||
GraphQLUsedScalarNames: 'graphql:used-scalar-names',
|
||||
ORMEntitySchemas: 'orm:entity-schemas',
|
||||
ToolCatalog: 'tool-catalog',
|
||||
} as const;
|
||||
export const WORKSPACE_CACHE_KEYS = {
|
||||
GraphQLOperations: 'graphql:operations',
|
||||
|
|
@ -215,24 +214,6 @@ export class WorkspaceCacheStorageService {
|
|||
);
|
||||
}
|
||||
|
||||
setToolCatalog(
|
||||
cacheKey: string,
|
||||
descriptors: unknown[],
|
||||
ttl: number,
|
||||
): Promise<void> {
|
||||
return this.cacheStorageService.set<unknown[]>(
|
||||
`${METADATA_VERSIONED_WORKSPACE_CACHE_KEY.ToolCatalog}:${cacheKey}`,
|
||||
descriptors,
|
||||
ttl,
|
||||
);
|
||||
}
|
||||
|
||||
getToolCatalog(cacheKey: string): Promise<unknown[] | undefined> {
|
||||
return this.cacheStorageService.get<unknown[]>(
|
||||
`${METADATA_VERSIONED_WORKSPACE_CACHE_KEY.ToolCatalog}:${cacheKey}`,
|
||||
);
|
||||
}
|
||||
|
||||
async flush(workspaceId: string, metadataVersion?: number): Promise<void> {
|
||||
await this.flushVersionedMetadata(workspaceId, metadataVersion);
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]),
|
||||
|
|
|
|||
Loading…
Reference in a new issue