[AI] Improve tools tab (#19221)

This commit is contained in:
nitin
2026-04-02 19:06:33 +05:30
committed by GitHub
parent cd23a2bc80
commit 3c067f072c
46 changed files with 1134 additions and 486 deletions
@@ -2653,6 +2653,7 @@ type ToolIndexEntry {
description: String!
category: String!
objectName: String
icon: String
inputSchema: JSON
}
@@ -2297,6 +2297,7 @@ export interface ToolIndexEntry {
description: Scalars['String']
category: Scalars['String']
objectName?: Scalars['String']
icon?: Scalars['String']
inputSchema?: Scalars['JSON']
__typename: 'ToolIndexEntry'
}
@@ -5475,6 +5476,7 @@ export interface ToolIndexEntryGenqlSelection{
description?: boolean | number
category?: boolean | number
objectName?: boolean | number
icon?: boolean | number
inputSchema?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
@@ -5179,6 +5179,9 @@ export default {
"objectName": [
1
],
"icon": [
1
],
"inputSchema": [
15
],
Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

@@ -5141,6 +5141,7 @@ export type ToolIndexEntry = {
__typename?: 'ToolIndexEntry';
category: Scalars['String'];
description: Scalars['String'];
icon?: Maybe<Scalars['String']>;
inputSchema?: Maybe<Scalars['JSON']>;
name: Scalars['String'];
objectName?: Maybe<Scalars['String']>;
@@ -6404,7 +6405,7 @@ export type GetChatThreadsQuery = { __typename?: 'Query', chatThreads: { __typen
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 }> };
export type GetToolIndexQuery = { __typename?: 'Query', getToolIndex: Array<{ __typename?: 'ToolIndexEntry', name: string, description: string, category: string, objectName?: string | null, icon?: string | null }> };
export type GetToolInputSchemaQueryVariables = Exact<{
toolName: Scalars['String'];
@@ -8261,7 +8262,7 @@ export const FindOneSkillDocument = {"kind":"Document","definitions":[{"kind":"O
export const GetAgentTurnsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAgentTurns"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"agentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"agentTurns"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"agentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"agentId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"threadId"}},{"kind":"Field","name":{"kind":"Name","value":"agentId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"evaluations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"score"}},{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"messages"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"parts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"textContent"}},{"kind":"Field","name":{"kind":"Name","value":"reasoningContent"}},{"kind":"Field","name":{"kind":"Name","value":"toolName"}},{"kind":"Field","name":{"kind":"Name","value":"toolCallId"}},{"kind":"Field","name":{"kind":"Name","value":"toolInput"}},{"kind":"Field","name":{"kind":"Name","value":"toolOutput"}},{"kind":"Field","name":{"kind":"Name","value":"errorMessage"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"errorDetails"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrlSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrlUrl"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrlTitle"}},{"kind":"Field","name":{"kind":"Name","value":"sourceDocumentSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"sourceDocumentMediaType"}},{"kind":"Field","name":{"kind":"Name","value":"sourceDocumentTitle"}},{"kind":"Field","name":{"kind":"Name","value":"sourceDocumentFilename"}},{"kind":"Field","name":{"kind":"Name","value":"fileMediaType"}},{"kind":"Field","name":{"kind":"Name","value":"fileFilename"}},{"kind":"Field","name":{"kind":"Name","value":"fileUrl"}},{"kind":"Field","name":{"kind":"Name","value":"providerMetadata"}}]}}]}}]}}]}}]} as unknown as DocumentNode<GetAgentTurnsQuery, GetAgentTurnsQueryVariables>;
export const GetChatMessagesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetChatMessages"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chatMessages"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"threadId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"threadId"}},{"kind":"Field","name":{"kind":"Name","value":"turnId"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"parts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"messageId"}},{"kind":"Field","name":{"kind":"Name","value":"orderIndex"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"textContent"}},{"kind":"Field","name":{"kind":"Name","value":"reasoningContent"}},{"kind":"Field","name":{"kind":"Name","value":"toolName"}},{"kind":"Field","name":{"kind":"Name","value":"toolCallId"}},{"kind":"Field","name":{"kind":"Name","value":"toolInput"}},{"kind":"Field","name":{"kind":"Name","value":"toolOutput"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"errorMessage"}},{"kind":"Field","name":{"kind":"Name","value":"errorDetails"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrlSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrlUrl"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrlTitle"}},{"kind":"Field","name":{"kind":"Name","value":"sourceDocumentSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"sourceDocumentMediaType"}},{"kind":"Field","name":{"kind":"Name","value":"sourceDocumentTitle"}},{"kind":"Field","name":{"kind":"Name","value":"sourceDocumentFilename"}},{"kind":"Field","name":{"kind":"Name","value":"fileMediaType"}},{"kind":"Field","name":{"kind":"Name","value":"fileFilename"}},{"kind":"Field","name":{"kind":"Name","value":"fileUrl"}},{"kind":"Field","name":{"kind":"Name","value":"fileId"}},{"kind":"Field","name":{"kind":"Name","value":"providerMetadata"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"chatStreamCatchupChunks"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"threadId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chunks"}},{"kind":"Field","name":{"kind":"Name","value":"maxSeq"}}]}}]}}]} as unknown as DocumentNode<GetChatMessagesQuery, GetChatMessagesQueryVariables>;
export const GetChatThreadsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetChatThreads"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"paging"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"CursorPaging"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chatThreads"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"Variable","name":{"kind":"Name","value":"paging"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"totalInputTokens"}},{"kind":"Field","name":{"kind":"Name","value":"totalOutputTokens"}},{"kind":"Field","name":{"kind":"Name","value":"contextWindowTokens"}},{"kind":"Field","name":{"kind":"Name","value":"conversationSize"}},{"kind":"Field","name":{"kind":"Name","value":"totalInputCredits"}},{"kind":"Field","name":{"kind":"Name","value":"totalOutputCredits"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"endCursor"}},{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}}]}}]}}]}}]} as unknown as DocumentNode<GetChatThreadsQuery, GetChatThreadsQueryVariables>;
export const GetToolIndexDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetToolIndex"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getToolIndex"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"objectName"}}]}}]}}]} as unknown as DocumentNode<GetToolIndexQuery, GetToolIndexQueryVariables>;
export const GetToolIndexDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetToolIndex"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getToolIndex"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"objectName"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}}]}}]}}]} as unknown as DocumentNode<GetToolIndexQuery, GetToolIndexQueryVariables>;
export const GetToolInputSchemaDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetToolInputSchema"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"toolName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getToolInputSchema"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"toolName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"toolName"}}}]}]}}]} as unknown as DocumentNode<GetToolInputSchemaQuery, GetToolInputSchemaQueryVariables>;
export const OnAgentChatEventDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OnAgentChatEvent"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"onAgentChatEvent"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"threadId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"threadId"}},{"kind":"Field","name":{"kind":"Name","value":"event"}}]}}]}}]} as unknown as DocumentNode<OnAgentChatEventSubscription, OnAgentChatEventSubscriptionVariables>;
export const TrackAnalyticsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"TrackAnalytics"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"type"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"AnalyticsType"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"event"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"name"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"properties"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"JSON"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"trackAnalytics"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"type"},"value":{"kind":"Variable","name":{"kind":"Name","value":"type"}}},{"kind":"Argument","name":{"kind":"Name","value":"event"},"value":{"kind":"Variable","name":{"kind":"Name","value":"event"}}},{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}},{"kind":"Argument","name":{"kind":"Name","value":"properties"},"value":{"kind":"Variable","name":{"kind":"Name","value":"properties"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}}]}}]}}]} as unknown as DocumentNode<TrackAnalyticsMutation, TrackAnalyticsMutationVariables>;
@@ -7,6 +7,7 @@ export const GET_TOOL_INDEX = gql`
description
category
objectName
icon
}
}
`;
@@ -1,16 +1,7 @@
import { GET_TOOL_INDEX } from '@/ai/graphql/queries/getToolIndex';
import { useQuery } from '@apollo/client/react';
type ToolIndexEntry = {
name: string;
description: string;
category: string;
objectName?: string;
};
type GetToolIndexQuery = {
getToolIndex: ToolIndexEntry[];
};
import { type GetToolIndexQuery } from '~/generated-metadata/graphql';
export const useGetToolIndex = () => {
const { data, loading, error } = useQuery<GetToolIndexQuery>(GET_TOOL_INDEX);
@@ -161,6 +161,12 @@ const SettingsAIUsageUserDetail = lazy(() =>
})),
);
const SettingsToolDetail = lazy(() =>
import('~/pages/settings/ai/SettingsToolDetail').then((module) => ({
default: module.SettingsToolDetail,
})),
);
const SettingsApplications = lazy(() =>
import('~/pages/settings/applications/SettingsApplications').then(
(module) => ({
@@ -546,6 +552,10 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
path={SettingsPath.AIUsageUserDetail}
element={<SettingsAIUsageUserDetail />}
/>
<Route
path={SettingsPath.AIToolDetail}
element={<SettingsToolDetail />}
/>
<Route
path={SettingsPath.LogicFunctionDetail}
element={<SettingsLogicFunctionDetail />}
@@ -6,7 +6,10 @@ import {
type LogicFunctionIdInput,
} from '~/generated-metadata/graphql';
export const useGetOneLogicFunction = ({ id }: LogicFunctionIdInput) => {
export const useGetOneLogicFunction = ({
id,
skip,
}: LogicFunctionIdInput & { skip?: boolean }) => {
const { data, loading } = useQuery<
FindOneLogicFunctionQuery,
FindOneLogicFunctionQueryVariables
@@ -14,6 +17,7 @@ export const useGetOneLogicFunction = ({ id }: LogicFunctionIdInput) => {
variables: {
input: { id },
},
skip,
});
return {
@@ -0,0 +1,12 @@
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { type LogicFunction } from '@/logic-functions/types/LogicFunction';
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
export const logicFunctionsSelector = createAtomSelector<LogicFunction[]>({
key: 'logicFunctionsSelector',
get: ({ get }) => {
const storeItem = get(metadataStoreState, 'logicFunctions');
return storeItem.current as LogicFunction[];
},
});
@@ -0,0 +1,4 @@
import { type FindManyLogicFunctionsQuery } from '~/generated-metadata/graphql';
export type LogicFunction =
FindManyLogicFunctionsQuery['findManyLogicFunctions'][number];
@@ -5,10 +5,8 @@ import { splitPageLayoutWithRelated } from '@/metadata-store/utils/splitPageLayo
import { splitViewWithRelated } from '@/metadata-store/utils/splitViewWithRelated';
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries';
import { transformPageLayout } from '@/page-layout/utils/transformPageLayout';
import { logicFunctionsState } from '@/settings/logic-functions/states/logicFunctionsState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useApolloClient } from '@apollo/client/react';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import {
@@ -56,7 +54,6 @@ const hasOverlap = (
export const useLoadStaleMetadataEntities = () => {
const client = useApolloClient();
const store = useStore();
const { replaceDraft, applyChanges } = useUpdateMetadataStoreDraft();
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
@@ -166,10 +163,6 @@ export const useLoadStaleMetadataEntities = () => {
return;
}
store.set(
logicFunctionsState.atom,
result.data.findManyLogicFunctions,
);
replaceDraft(
'logicFunctions',
result.data.findManyLogicFunctions,
@@ -240,7 +233,7 @@ export const useLoadStaleMetadataEntities = () => {
await Promise.all(fetchPromises);
applyChanges();
},
[client, store, replaceDraft, applyChanges, isAiEnabled],
[client, replaceDraft, applyChanges, isAiEnabled],
);
return { loadStaleMetadataEntities };
@@ -1,3 +1,3 @@
import { type LogicFunction } from '@/settings/logic-functions/states/logicFunctionsState';
import { type LogicFunction } from '@/logic-functions/types/LogicFunction';
export type FlatLogicFunction = LogicFunction;
@@ -1,10 +0,0 @@
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { type FindManyLogicFunctionsQuery } from '~/generated-metadata/graphql';
export type LogicFunction =
FindManyLogicFunctionsQuery['findManyLogicFunctions'][number];
export const logicFunctionsState = createAtomState<LogicFunction[]>({
key: 'logicFunctionsState',
defaultValue: [],
});
@@ -1,5 +1,6 @@
import { WorkflowActionMenuItems } from '@/side-panel/pages/workflow/action/components/WorkflowActionMenuItems';
import { logicFunctionsState } from '@/settings/logic-functions/states/logicFunctionsState';
import { logicFunctionsSelector } from '@/logic-functions/states/logicFunctionsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { type WorkflowActionType } from '@/workflow/types/Workflow';
import { SidePanelStepListContainer } from '@/workflow/workflow-steps/components/SidePanelWorkflowSelectStepContainer';
import { SidePanelWorkflowSelectStepTitle } from '@/workflow/workflow-steps/components/SidePanelWorkflowSelectStepTitle';
@@ -10,7 +11,6 @@ import { HUMAN_INPUT_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/
import { RECORD_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/RecordActions';
import { getActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useLingui } from '@lingui/react/macro';
import { IconFunction } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
@@ -33,7 +33,7 @@ export const SidePanelWorkflowSelectAction = ({
const { t } = useLingui();
const logicFunctions = useAtomStateValue(logicFunctionsState);
const logicFunctions = useAtomStateValue(logicFunctionsSelector);
const toolFunctions = logicFunctions.filter((fn) => fn.isTool === true);
@@ -1,10 +1,15 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { usePersistLogicFunction } from '@/logic-functions/hooks/usePersistLogicFunction';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
@@ -12,18 +17,26 @@ import { t } from '@lingui/core/macro';
import {
IconChartBar,
IconCpu,
IconPlus,
IconSettingsBolt,
IconSparkles,
IconTool,
} from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { UndecoratedLink } from 'twenty-ui/navigation';
import { SettingsAIMoreTab } from '~/pages/settings/ai/components/SettingsAIMoreTab';
import { SettingsAgentToolsTab } from '~/pages/settings/ai/components/SettingsAgentToolsTab';
import { SettingsAIModelsTab } from './components/SettingsAIModelsTab';
import { SettingsAIUsageTab } from './components/SettingsAIUsageTab';
import { SettingsAgentSkills } from './components/SettingsAgentSkills';
import { SettingsToolsTable } from './components/SettingsToolsTable';
import { SETTINGS_AI_TABS } from './constants/SettingsAiTabs';
export const SettingsAI = () => {
const navigate = useNavigate();
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const { createLogicFunction } = usePersistLogicFunction();
const [isCreatingTool, setIsCreatingTool] = useState(false);
const activeTabId = useAtomComponentStateValue(
activeTabIdComponentState,
SETTINGS_AI_TABS.COMPONENT_INSTANCE_ID,
@@ -33,6 +46,44 @@ export const SettingsAI = () => {
FeatureFlagKey.IS_USAGE_ANALYTICS_ENABLED,
);
const handleCreateTool = async () => {
setIsCreatingTool(true);
try {
const result = await createLogicFunction({
input: {
name: 'new-tool',
isTool: true,
},
});
if (result.status === 'successful' && isDefined(result.response?.data)) {
const newLogicFunction = result.response.data.createOneLogicFunction;
enqueueSuccessSnackBar({ message: t`Tool created` });
const applicationId = (newLogicFunction as { applicationId?: string })
.applicationId;
if (isDefined(applicationId)) {
navigate(
getSettingsPath(SettingsPath.ApplicationLogicFunctionDetail, {
applicationId,
logicFunctionId: newLogicFunction.id,
}),
);
} else {
navigate(
getSettingsPath(SettingsPath.LogicFunctionDetail, {
logicFunctionId: newLogicFunction.id,
}),
);
}
} else {
enqueueErrorSnackBar({ message: t`Failed to create tool` });
}
} finally {
setIsCreatingTool(false);
}
};
const tabs = [
{
id: SETTINGS_AI_TABS.TABS_IDS.MODELS,
@@ -74,6 +125,27 @@ export const SettingsAI = () => {
return (
<SubMenuTopBarContainer
title={t`AI`}
actionButton={
isSkillsTab ? (
<UndecoratedLink to={getSettingsPath(SettingsPath.AINewSkill)}>
<Button
Icon={IconPlus}
title={t`New Skill`}
accent="blue"
size="small"
/>
</UndecoratedLink>
) : isToolsTab ? (
<Button
Icon={IconPlus}
title={t`New Tool`}
accent="blue"
size="small"
onClick={handleCreateTool}
disabled={isCreatingTool}
/>
) : undefined
}
links={[
{
children: t`Workspace`,
@@ -89,7 +161,7 @@ export const SettingsAI = () => {
/>
{isModelsTab && <SettingsAIModelsTab />}
{isSkillsTab && <SettingsAgentSkills />}
{isToolsTab && <SettingsToolsTable />}
{isToolsTab && <SettingsAgentToolsTab />}
{isUsageTab && <SettingsAIUsageTab />}
{isMoreTab && <SettingsAIMoreTab />}
</SettingsPageContainer>
@@ -0,0 +1,267 @@
import { useQuery } from '@apollo/client/react';
import { t } from '@lingui/core/macro';
import { useContext, useState } from 'react';
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
import { useNavigate, useParams } from 'react-router-dom';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useGetOneLogicFunction } from '@/logic-functions/hooks/useGetOneLogicFunction';
import { usePersistLogicFunction } from '@/logic-functions/hooks/usePersistLogicFunction';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsLogicFunctionLabelContainer } from '@/settings/logic-functions/components/SettingsLogicFunctionLabelContainer';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { TextArea } from '@/ui/input/components/TextArea';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined, isValidUuid } from 'twenty-shared/utils';
import { H2Title, IconTrash } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { ThemeContext } from 'twenty-ui/theme-constants';
import { useDebouncedCallback } from 'use-debounce';
import {
GetToolIndexDocument,
GetToolInputSchemaDocument,
} from '~/generated-metadata/graphql';
import { SettingsToolParameterTable } from '~/pages/settings/ai/components/SettingsToolParameterTable';
const DELETE_TOOL_MODAL_ID = 'delete-tool-modal';
export const SettingsToolDetail = () => {
const { toolIdentifier } = useParams();
const { theme } = useContext(ThemeContext);
const navigate = useNavigate();
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const { updateLogicFunction, deleteLogicFunction } =
usePersistLogicFunction();
const { openModal } = useModal();
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const [isDeleting, setIsDeleting] = useState(false);
const [editedName, setEditedName] = useState<string | null>(null);
const [editedDescription, setEditedDescription] = useState<string | null>(
null,
);
const isCustomTool = isDefined(toolIdentifier) && isValidUuid(toolIdentifier);
const { logicFunction, loading: logicFunctionLoading } =
useGetOneLogicFunction({
id: toolIdentifier ?? '',
skip: !isCustomTool,
});
const { data: toolIndexData, loading: toolIndexLoading } = useQuery(
GetToolIndexDocument,
{ skip: isCustomTool },
);
const systemTool = toolIndexData?.getToolIndex.find(
(entry) => entry.name === toolIdentifier,
);
const { data: schemaData, loading: schemaLoading } = useQuery(
GetToolInputSchemaDocument,
{
variables: { toolName: toolIdentifier ?? '' },
skip: isCustomTool || !isDefined(systemTool),
},
);
const loading = isCustomTool
? logicFunctionLoading
: toolIndexLoading || schemaLoading;
const workspaceCustomApplicationId =
currentWorkspace?.workspaceCustomApplication?.id;
const isManaged =
isCustomTool &&
isDefined(logicFunction?.applicationId) &&
logicFunction.applicationId !== workspaceCustomApplicationId;
const isReadOnly = !isCustomTool || isManaged;
const name = isCustomTool ? logicFunction?.name : toolIdentifier;
const description = isCustomTool
? logicFunction?.description
: systemTool?.description;
const inputSchema = isCustomTool
? logicFunction?.toolInputSchema
: schemaData?.getToolInputSchema;
const functionLink = isCustomTool
? isDefined(logicFunction?.applicationId)
? getSettingsPath(SettingsPath.ApplicationLogicFunctionDetail, {
applicationId: logicFunction.applicationId,
logicFunctionId: logicFunction?.id ?? '',
})
: getSettingsPath(SettingsPath.LogicFunctionDetail, {
logicFunctionId: logicFunction?.id ?? '',
})
: undefined;
const debouncedSaveName = useDebouncedCallback(async (value: string) => {
if (!isCustomTool || !isDefined(logicFunction)) return;
const result = await updateLogicFunction({
input: {
id: logicFunction.id,
update: { name: value },
},
});
if (result.status === 'failed') {
setEditedName(logicFunction.name);
}
}, 1_000);
const handleNameChange = (value: string) => {
setEditedName(value);
debouncedSaveName(value);
};
const debouncedSaveDescription = useDebouncedCallback(
async (value: string) => {
if (!isCustomTool || !isDefined(logicFunction)) return;
const result = await updateLogicFunction({
input: {
id: logicFunction.id,
update: { description: value },
},
});
if (result.status === 'failed') {
setEditedDescription(logicFunction.description ?? null);
}
},
1_000,
);
const handleDescriptionChange = (value: string) => {
setEditedDescription(value);
debouncedSaveDescription(value);
};
const handleDelete = async () => {
if (!isCustomTool || !isDefined(logicFunction)) return;
setIsDeleting(true);
const result = await deleteLogicFunction({
input: { id: logicFunction.id },
});
if (result.status === 'successful') {
enqueueSuccessSnackBar({ message: t`Tool deleted` });
navigate(getSettingsPath(SettingsPath.AI, undefined, undefined, 'tools'));
} else {
enqueueErrorSnackBar({ message: t`Failed to delete tool` });
}
setIsDeleting(false);
};
return (
<SubMenuTopBarContainer
title={
isCustomTool ? (
<SettingsLogicFunctionLabelContainer
value={editedName ?? name ?? ''}
onChange={handleNameChange}
/>
) : (
(name ?? '')
)
}
links={[
{
children: t`Workspace`,
href: getSettingsPath(SettingsPath.Workspace),
},
{
children: t`AI`,
href: getSettingsPath(SettingsPath.AI, undefined, undefined, 'tools'),
},
{ children: editedName ?? name ?? '' },
]}
>
<SettingsPageContainer>
{loading ? (
<SkeletonTheme
baseColor={theme.background.tertiary}
highlightColor={theme.background.transparent.lighter}
borderRadius={4}
>
<Section>
<Skeleton height={20} width={200} />
<Skeleton height={20} width={400} />
<Skeleton height={80} />
</Section>
</SkeletonTheme>
) : (
<>
<Section>
<H2Title
title={t`Parameters`}
description={t`Input parameters accepted by this tool`}
/>
<SettingsToolParameterTable
schemaProperties={inputSchema?.properties ?? {}}
requiredFields={inputSchema?.required}
functionLink={functionLink}
/>
</Section>
<Section>
<H2Title
title={t`Description`}
description={t`Define what this tool does`}
/>
<TextArea
textAreaId="tool-description-textarea"
placeholder={t`Write a description`}
minRows={3}
value={editedDescription ?? description ?? ''}
onChange={handleDescriptionChange}
disabled={isReadOnly}
/>
</Section>
{isCustomTool && !isManaged && (
<Section>
<H2Title
title={t`Danger zone`}
description={t`Delete this tool`}
/>
<Button
Icon={IconTrash}
title={t`Delete`}
accent="danger"
size="small"
variant="secondary"
onClick={() => openModal(DELETE_TOOL_MODAL_ID)}
/>
</Section>
)}
</>
)}
</SettingsPageContainer>
<ConfirmationModal
modalInstanceId={DELETE_TOOL_MODAL_ID}
title={t`Delete Tool`}
subtitle={t`Are you sure you want to delete this tool? This action cannot be undone.`}
onConfirmClick={handleDelete}
confirmButtonText={t`Delete`}
loading={isDeleting}
/>
</SubMenuTopBarContainer>
);
};
@@ -6,11 +6,9 @@ import { useSortedArray } from '@/ui/layout/table/hooks/useSortedArray';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { useContext, useMemo, useState } from 'react';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { H2Title, IconArchive, IconPlus } from 'twenty-ui/display';
import { Button, SearchInput } from 'twenty-ui/input';
import { MenuItemToggle, UndecoratedLink } from 'twenty-ui/navigation';
import { H2Title, IconArchive } from 'twenty-ui/display';
import { SearchInput } from 'twenty-ui/input';
import { MenuItemToggle } from 'twenty-ui/navigation';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import { useMutation, useQuery } from '@apollo/client/react';
@@ -31,13 +29,6 @@ const StyledCoverImage = styled.div`
overflow: hidden;
`;
const StyledFooterContainer = styled.div`
align-items: center;
display: flex;
justify-content: flex-end;
margin-top: ${themeCssVariables.spacing[4]};
`;
const StyledSearchInput = styled(SearchInput)`
margin-bottom: ${themeCssVariables.spacing[4]};
`;
@@ -147,16 +138,6 @@ export const SettingsAgentSkills = () => {
onActivate={handleActivate}
onDelete={handleDelete}
/>
<StyledFooterContainer>
<UndecoratedLink to={getSettingsPath(SettingsPath.AINewSkill)}>
<Button
Icon={IconPlus}
title={t`New Skill`}
size="small"
variant="secondary"
/>
</UndecoratedLink>
</StyledFooterContainer>
</Section>
</>
);
@@ -0,0 +1,28 @@
import { styled } from '@linaria/react';
import { useContext } from 'react';
import { Section } from 'twenty-ui/layout';
import { ThemeContext } from 'twenty-ui/theme-constants';
import { SettingsToolsTable } from '~/pages/settings/ai/components/SettingsToolsTable';
const StyledCoverImage = styled.div`
background-position: center;
background-size: cover;
height: 160px;
overflow: hidden;
`;
export const SettingsAgentToolsTab = () => {
const { colorScheme } = useContext(ThemeContext);
const coverImage =
colorScheme === 'light'
? '/images/ai/ai-tools-cover-light.png'
: '/images/ai/ai-tools-cover-dark.png';
return (
<>
<Section>
<StyledCoverImage style={{ backgroundImage: `url('${coverImage}')` }} />
</Section>
<SettingsToolsTable />
</>
);
};
@@ -1,195 +0,0 @@
import { useLazyQuery } from '@apollo/client/react';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { useCallback, 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';
import { isDefined } from 'twenty-shared/utils';
import {
IconChevronDown,
IconChevronRight,
IconCode,
IconDatabase,
IconPlayerPlay,
IconSettings,
IconTable,
OverflowingTextWithTooltip,
} from 'twenty-ui/display';
import { JsonTree } from 'twenty-ui/json-visualizer';
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
import { themeCssVariables } from 'twenty-ui/theme-constants';
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;
};
export type SettingsSystemToolTableRowProps = {
tool: SystemTool;
};
const StyledSystemToolTableRowContainer = styled.div`
> div,
> a {
opacity: 0.7;
&[data-clickable='true']:hover {
opacity: 0.85;
}
}
`;
export const StyledSystemToolTableRow = (
props: React.ComponentProps<typeof TableRow>,
) => (
<StyledSystemToolTableRowContainer>
{/* oxlint-disable-next-line react/jsx-props-no-spreading */}
<TableRow gridTemplateColumns="1fr 100px 36px" {...props} />
</StyledSystemToolTableRowContainer>
);
const StyledIconContainer = styled.div`
align-items: center;
display: flex;
flex-shrink: 0;
`;
const StyledExpandableContent = styled.div`
background-color: ${themeCssVariables.background.secondary};
border-top: 1px solid ${themeCssVariables.border.color.light};
padding: ${themeCssVariables.spacing[4]};
`;
const StyledSectionTitle = styled.div`
color: ${themeCssVariables.font.color.secondary};
font-size: ${themeCssVariables.font.size.sm};
font-weight: ${themeCssVariables.font.weight.medium};
margin-bottom: ${themeCssVariables.spacing[2]};
`;
const StyledDescription = styled.div`
color: ${themeCssVariables.font.color.tertiary};
font-size: ${themeCssVariables.font.size.sm};
margin-bottom: ${themeCssVariables.spacing[4]};
`;
const getCategoryIcon = (category: string) => {
switch (category) {
case 'database':
return IconDatabase;
case 'workflow':
return IconPlayerPlay;
case 'metadata':
return IconSettings;
case 'view':
return IconTable;
default:
return IconCode;
}
};
export const SettingsSystemToolTableRow = ({
tool,
}: SettingsSystemToolTableRowProps) => {
const [isExpanded, setIsExpanded] = useState(false);
const { copyToClipboard } = useCopyToClipboard();
const [fetchSchemaRaw, { data: schemaData, loading: schemaLoading }] =
useLazyQuery<GetToolInputSchemaQuery>(GET_TOOL_INPUT_SCHEMA);
const fetchSchema = useCallback(
() => fetchSchemaRaw({ variables: { toolName: tool.name } }),
[fetchSchemaRaw, tool.name],
);
const inputSchema = schemaData?.getToolInputSchema;
const hasInputSchema =
isDefined(inputSchema) && Object.keys(inputSchema).length > 0;
const Icon = getCategoryIcon(tool.category);
const handleRowClick = () => {
if (!schemaData && !schemaLoading) {
fetchSchema();
}
setIsExpanded(!isExpanded);
};
return (
<>
<StyledSystemToolTableRow
key={tool.name}
onClick={handleRowClick}
isClickable
>
<TableCell
color={themeCssVariables.font.color.primary}
gap={themeCssVariables.spacing[2]}
minWidth="0"
overflow="hidden"
>
<StyledIconContainer>
<Icon size={16} />
</StyledIconContainer>
<OverflowingTextWithTooltip text={tool.name} />
</TableCell>
<TableCell>
<SettingsItemTypeTag item={{ isCustom: false }} />
</TableCell>
<TableCell
align="right"
padding={`0 ${themeCssVariables.spacing[2]} 0 0`}
>
{isExpanded ? (
<IconChevronDown size={16} />
) : (
<IconChevronRight size={16} />
)}
</TableCell>
</StyledSystemToolTableRow>
<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>
</>
);
};
@@ -0,0 +1,184 @@
import { styled } from '@linaria/react';
import { useContext } from 'react';
import { getNavigationMenuItemIconStyleFromColor } from '@/navigation-menu-item/common/utils/getNavigationMenuItemIconStyleFromColor';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { getObjectColorWithFallback } from '@/object-metadata/utils/getObjectColorWithFallback';
import { isDefined } from 'twenty-shared/utils';
import {
Avatar,
type IconComponent,
IconCode,
IconEdit,
IconPlus,
IconSearch,
IconTrash,
useIcons,
} from 'twenty-ui/display';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
type ApplicationInfo = {
name: string;
};
type MarketplaceAppInfo = {
icon: string;
logo?: string | null;
};
type SettingsToolIconProps = {
icon?: string | null;
toolName?: string;
objectName?: string;
application?: ApplicationInfo;
marketplaceApp?: MarketplaceAppInfo;
};
const getOperationIcon = (toolName: string): IconComponent | null => {
if (toolName.startsWith('create_')) return IconPlus;
if (toolName.startsWith('update_')) return IconEdit;
if (toolName.startsWith('delete_')) return IconTrash;
if (toolName.startsWith('find_') || toolName.startsWith('get_'))
return IconSearch;
return null;
};
const StyledCompositeContainer = styled.div`
align-items: center;
border-radius: 4px;
box-sizing: border-box;
display: flex;
flex-shrink: 0;
height: 16px;
justify-content: center;
position: relative;
width: 16px;
`;
const StyledMainIconWrapper = styled.div<{
$backgroundColor: string;
$borderColor?: string;
}>`
align-items: center;
background-color: ${({ $backgroundColor }) => $backgroundColor};
border: ${({ $borderColor }) =>
$borderColor ? `1px solid ${$borderColor}` : 'none'};
border-radius: 4px;
box-sizing: border-box;
display: flex;
inset: 0;
justify-content: center;
position: absolute;
`;
const StyledOperationOverlay = styled.div`
align-items: center;
background-color: ${themeCssVariables.grayScale.gray4};
border-radius: 4px;
bottom: -5px;
display: flex;
height: 14px;
justify-content: center;
position: absolute;
right: -6px;
width: 12px;
`;
export const SettingsToolIcon = ({
icon,
toolName,
objectName,
application,
marketplaceApp,
}: SettingsToolIconProps) => {
const { getIcon } = useIcons();
const { theme } = useContext(ThemeContext);
const { objectMetadataItems } = useObjectMetadataItems();
// Custom tools: application/marketplace icons
if (isDefined(application) && isDefined(marketplaceApp?.logo)) {
return (
<Avatar
avatarUrl={marketplaceApp?.logo ?? null}
placeholder={application.name}
placeholderColorSeed={application.name}
type="squared"
size="xs"
/>
);
}
if (isDefined(marketplaceApp)) {
const MarketplaceIcon = getIcon(marketplaceApp.icon);
return <MarketplaceIcon size={16} />;
}
if (isDefined(application)) {
return (
<Avatar
placeholder={application.name}
placeholderColorSeed={application.name}
type="squared"
size="xs"
/>
);
}
// System tools: icon from server, color derived from object metadata
const MainIcon = isDefined(icon) ? getIcon(icon) : IconCode;
const OperationIcon = isDefined(toolName) ? getOperationIcon(toolName) : null;
const objectMetadata = isDefined(objectName)
? objectMetadataItems.find((item) => item.nameSingular === objectName)
: undefined;
const objectStyle = isDefined(objectMetadata)
? getNavigationMenuItemIconStyleFromColor(
getObjectColorWithFallback(objectMetadata),
)
: null;
if (isDefined(OperationIcon)) {
return (
<StyledCompositeContainer>
<StyledMainIconWrapper
$backgroundColor={objectStyle?.backgroundColor ?? 'transparent'}
$borderColor={objectStyle?.borderColor}
>
<MainIcon
size="14px"
stroke={theme.icon.stroke.md}
color={objectStyle?.iconColor ?? theme.font.color.secondary}
/>
</StyledMainIconWrapper>
<StyledOperationOverlay>
<OperationIcon
size="12px"
stroke={theme.icon.stroke.md}
color={themeCssVariables.grayScale.gray10}
/>
</StyledOperationOverlay>
</StyledCompositeContainer>
);
}
if (isDefined(objectStyle)) {
return (
<StyledCompositeContainer>
<StyledMainIconWrapper
$backgroundColor={objectStyle.backgroundColor}
$borderColor={objectStyle.borderColor}
>
<MainIcon
size="14px"
stroke={theme.icon.stroke.md}
color={objectStyle.iconColor}
/>
</StyledMainIconWrapper>
</StyledCompositeContainer>
);
}
return <MainIcon size={16} />;
};
@@ -0,0 +1,174 @@
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { type ComponentType, useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { Table } from '@/ui/layout/table/components/Table';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import {
AppTooltip,
IconArrowUpRight,
IconInfoCircle,
IllustrationIconArray,
IllustrationIconJson,
IllustrationIconNumbers,
IllustrationIconText,
IllustrationIconToggle,
OverflowingTextWithTooltip,
TooltipDelay,
} from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { UndecoratedLink } from 'twenty-ui/navigation';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
type SchemaProperty = {
type?: string;
description?: string;
format?: string;
items?: { type?: string };
};
export type SettingsToolParameterTableProps = {
schemaProperties: Record<string, SchemaProperty>;
requiredFields?: string[];
functionLink?: string;
};
const PARAMETER_TABLE_GRID = '140px 1fr 80px 24px';
const TYPE_ICON_MAP: Record<string, ComponentType<{ size?: number }>> = {
string: IllustrationIconText,
number: IllustrationIconNumbers,
integer: IllustrationIconNumbers,
boolean: IllustrationIconToggle,
array: IllustrationIconArray,
object: IllustrationIconJson,
};
const StyledTableHeaderRow = styled.div`
margin-bottom: ${themeCssVariables.spacing[2]};
`;
const StyledFieldsContainer = styled.div`
border-bottom: 1px solid ${themeCssVariables.border.color.light};
padding-bottom: ${themeCssVariables.spacing[2]};
`;
const StyledFooter = styled.div`
display: flex;
justify-content: flex-end;
padding: ${themeCssVariables.spacing[2]} 0;
`;
const StyledInfoIconContainer = styled.div`
align-items: center;
display: flex;
justify-content: center;
`;
const getDisplayType = (property: SchemaProperty): string => {
if (property.format) {
return property.format;
}
if (property.type === 'array' && property.items?.type) {
return `${property.items.type}[]`;
}
return property.type ?? '';
};
export const SettingsToolParameterTable = ({
schemaProperties,
requiredFields,
functionLink,
}: SettingsToolParameterTableProps) => {
const { theme } = useContext(ThemeContext);
const entries = Object.entries(schemaProperties);
if (entries.length === 0 && !functionLink) {
return <div>{t`No parameters`}</div>;
}
return (
<Table>
{entries.length > 0 && (
<>
<StyledTableHeaderRow>
<TableRow gridTemplateColumns={PARAMETER_TABLE_GRID}>
<TableHeader>{t`Name`}</TableHeader>
<TableHeader>{t`Type`}</TableHeader>
<TableHeader>{t`Required`}</TableHeader>
<TableHeader />
</TableRow>
</StyledTableHeaderRow>
<StyledFieldsContainer>
{entries.map(([paramName, property], index) => {
const infoIconId = `param-info-${index}`;
const TypeIcon = TYPE_ICON_MAP[property.type ?? ''];
return (
<TableRow
key={paramName}
gridTemplateColumns={PARAMETER_TABLE_GRID}
>
<TableCell
overflow="hidden"
textOverflow="ellipsis"
whiteSpace="nowrap"
>
<OverflowingTextWithTooltip text={paramName} />
</TableCell>
<TableCell gap={themeCssVariables.spacing[1]}>
{isDefined(TypeIcon) && (
<TypeIcon size={theme.icon.size.md} />
)}
{getDisplayType(property)}
</TableCell>
<TableCell color={themeCssVariables.font.color.tertiary}>
{requiredFields?.includes(paramName) ? t`Yes` : ''}
</TableCell>
<TableCell>
{property.description && (
<StyledInfoIconContainer>
<IconInfoCircle
id={infoIconId}
size={theme.icon.size.md}
color={theme.font.color.tertiary}
style={{ outline: 'none', cursor: 'pointer' }}
/>
<AppTooltip
anchorSelect={`#${infoIconId}`}
content={property.description}
offset={5}
noArrow
place="bottom"
positionStrategy="fixed"
delay={TooltipDelay.shortDelay}
/>
</StyledInfoIconContainer>
)}
</TableCell>
</TableRow>
);
})}
</StyledFieldsContainer>
</>
)}
{functionLink && (
<StyledFooter>
<UndecoratedLink to={functionLink}>
<Button
Icon={IconArrowUpRight}
title={t`See function`}
size="small"
variant="secondary"
/>
</UndecoratedLink>
</StyledFooter>
)}
</Table>
);
};
@@ -1,20 +1,15 @@
import { styled } from '@linaria/react';
import { type ReactNode } from 'react';
import { SettingsItemTypeTag } from '@/settings/components/SettingsItemTypeTag';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { IconCode, OverflowingTextWithTooltip } from 'twenty-ui/display';
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { type LogicFunction } from '~/generated-metadata/graphql';
type ToolWithApplicationId = LogicFunction & {
applicationId?: string | null;
};
export type SettingsToolTableRowProps = {
tool: ToolWithApplicationId;
leftIcon: ReactNode;
name: string;
appLabel: string;
action?: ReactNode;
link?: string;
};
@@ -28,7 +23,9 @@ const StyledIconContainer = styled.div`
`;
export const SettingsToolTableRow = ({
tool,
leftIcon,
name,
appLabel,
action,
link,
}: SettingsToolTableRowProps) => {
@@ -43,15 +40,11 @@ export const SettingsToolTableRow = ({
minWidth="0"
overflow="hidden"
>
<StyledIconContainer>
<IconCode size={16} />
</StyledIconContainer>
<OverflowingTextWithTooltip text={tool.name} />
<StyledIconContainer>{leftIcon}</StyledIconContainer>
<OverflowingTextWithTooltip text={name} />
</TableCell>
<TableCell>
<SettingsItemTypeTag
item={{ isCustom: true, applicationId: tool.applicationId }}
/>
<TableCell minWidth="0" overflow="hidden">
<OverflowingTextWithTooltip text={appLabel} />
</TableCell>
<TableCell
align="right"
@@ -1,31 +1,73 @@
import { styled } from '@linaria/react';
import { gql } from '@apollo/client';
import { useQuery } from '@apollo/client/react';
import { useLingui } from '@lingui/react/macro';
import { useContext, useMemo, useState } from 'react';
import Skeleton from 'react-loading-skeleton';
import { type ReactNode, useContext, useMemo, useState } from 'react';
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
import { useGetToolIndex } from '@/ai/hooks/useGetToolIndex';
import { usePersistLogicFunction } from '@/logic-functions/hooks/usePersistLogicFunction';
import { logicFunctionsState } from '@/settings/logic-functions/states/logicFunctionsState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { logicFunctionsSelector } from '@/logic-functions/states/logicFunctionsSelector';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { Table } from '@/ui/layout/table/components/Table';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { useNavigate } from 'react-router-dom';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { H2Title, IconChevronRight, IconPlus } from 'twenty-ui/display';
import { Button, SearchInput } from 'twenty-ui/input';
import {
H2Title,
IconChevronRight,
IconLock,
IconPuzzle,
IconTool,
} from 'twenty-ui/display';
import { SearchInput } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { MenuItemToggle } from 'twenty-ui/navigation';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import { ToolCategory } from 'twenty-shared/ai';
import { normalizeSearchText } from '~/utils/normalizeSearchText';
import { SettingsSystemToolTableRow } from './SettingsSystemToolTableRow';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { SettingsToolIcon } from './SettingsToolIcon';
import {
TOOL_TABLE_ROW_GRID_TEMPLATE_COLUMNS,
SettingsToolTableRow,
TOOL_TABLE_ROW_GRID_TEMPLATE_COLUMNS,
} from './SettingsToolTableRow';
type ToolItem = {
identifier: string;
name: string;
description?: string | null;
category?: string;
objectName?: string | null;
icon?: string | null;
applicationId?: string | null;
};
const FIND_MANY_APPLICATIONS_FOR_TOOL_TABLE = gql`
query FindManyApplicationsForToolTable {
findManyApplications {
id
name
universalIdentifier
}
}
`;
const FIND_MANY_MARKETPLACE_APPS_FOR_TOOL_TABLE = gql`
query FindManyMarketplaceAppsForToolTable {
findManyMarketplaceApps {
id
universalIdentifier
icon
logo
}
}
`;
const StyledSearchContainer = styled.div`
padding-bottom: ${themeCssVariables.spacing[2]};
`;
@@ -34,207 +76,223 @@ const StyledTableHeaderRowContainer = styled.div`
margin-bottom: ${themeCssVariables.spacing[2]};
`;
const StyledFooterContainer = styled.div`
align-items: center;
display: flex;
justify-content: flex-end;
margin-top: ${themeCssVariables.spacing[4]};
`;
export const SettingsToolsTable = () => {
const { theme } = useContext(ThemeContext);
const logicFunctions = useAtomStateValue(logicFunctionsState);
const { toolIndex, loading: toolIndexLoading } = useGetToolIndex();
const { createLogicFunction } = usePersistLogicFunction();
const logicFunctions = useAtomStateValue(logicFunctionsSelector);
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const {
toolIndex,
loading: toolIndexLoading,
error: toolIndexError,
} = useGetToolIndex();
const { data: applicationsData } = useQuery<{
findManyApplications: Array<{
id: string;
name: string;
universalIdentifier: string;
}>;
}>(FIND_MANY_APPLICATIONS_FOR_TOOL_TABLE);
const { data: marketplaceAppsData } = useQuery<{
findManyMarketplaceApps: Array<{
id: string;
universalIdentifier: string;
icon: string;
logo?: string | null;
}>;
}>(FIND_MANY_MARKETPLACE_APPS_FOR_TOOL_TABLE);
const { t } = useLingui();
const navigate = useNavigate();
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const [customSearchTerm, setCustomSearchTerm] = useState('');
const [builtInSearchTerm, setBuiltInSearchTerm] = useState('');
const [isCreating, setIsCreating] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [showCustomTools, setShowCustomTools] = useState(true);
const [showManagedTools, setShowManagedTools] = useState(true);
const [showStandardTools, setShowStandardTools] = useState(true);
const tools = useMemo(
() => logicFunctions.filter((fn) => fn.isTool === true),
[logicFunctions],
);
const workspaceCustomApplicationId =
currentWorkspace?.workspaceCustomApplication?.id;
const filteredTools = useMemo(
() =>
tools.filter((tool) => {
const searchNormalized = normalizeSearchText(customSearchTerm);
const matchesSearch =
normalizeSearchText(tool.name).includes(searchNormalized) ||
normalizeSearchText(tool.description ?? '').includes(
searchNormalized,
);
const isManaged = (applicationId?: string | null) =>
isDefined(applicationId) && applicationId !== workspaceCustomApplicationId;
return matchesSearch;
}),
[tools, customSearchTerm],
);
const isCustom = (item: ToolItem) => isDefined(item.applicationId);
const systemTools = useMemo(
() =>
toolIndex.filter(
(systemTool) => systemTool.category !== 'LOGIC_FUNCTION',
),
[toolIndex],
);
const filteredSystemTools = useMemo(
() =>
systemTools.filter((systemTool) => {
const searchNormalized = normalizeSearchText(builtInSearchTerm);
const matchesSearch =
normalizeSearchText(systemTool.name).includes(searchNormalized) ||
normalizeSearchText(systemTool.description).includes(
searchNormalized,
);
return matchesSearch;
}),
[systemTools, builtInSearchTerm],
);
const showSkeleton = toolIndexLoading && tools.length === 0;
const handleCreateTool = async () => {
setIsCreating(true);
try {
const result = await createLogicFunction({
input: {
name: 'new-tool',
isTool: true,
},
});
if (result.status === 'successful' && isDefined(result.response?.data)) {
const newLogicFunction = result.response.data.createOneLogicFunction;
enqueueSuccessSnackBar({ message: t`Tool created` });
const applicationId = (newLogicFunction as { applicationId?: string })
.applicationId;
if (isDefined(applicationId)) {
navigate(
getSettingsPath(SettingsPath.ApplicationLogicFunctionDetail, {
applicationId,
logicFunctionId: newLogicFunction.id,
}),
);
} else {
navigate(
getSettingsPath(SettingsPath.LogicFunctionDetail, {
logicFunctionId: newLogicFunction.id,
}),
);
}
}
} catch {
enqueueErrorSnackBar({ message: t`Failed to create tool` });
} finally {
setIsCreating(false);
}
};
const getToolLink = (tool: (typeof tools)[0]) => {
const applicationId = (tool as { applicationId?: string }).applicationId;
if (isDefined(applicationId)) {
return getSettingsPath(SettingsPath.ApplicationLogicFunctionDetail, {
applicationId,
logicFunctionId: tool.id,
});
}
return getSettingsPath(SettingsPath.LogicFunctionDetail, {
logicFunctionId: tool.id,
const getToolLink = (item: ToolItem) =>
getSettingsPath(SettingsPath.AIToolDetail, {
toolIdentifier: item.identifier,
});
};
const allTools: ToolItem[] = useMemo(
() => [
...logicFunctions
.filter((fn) => fn.isTool === true)
.map((fn) => ({
identifier: fn.id,
name: fn.name,
description: fn.description,
applicationId: fn.applicationId,
})),
...toolIndex
.filter((tool) => tool.category !== ToolCategory.LOGIC_FUNCTION)
.map((tool) => ({
identifier: tool.name,
name: tool.name,
description: tool.description,
category: tool.category,
objectName: tool.objectName,
icon: tool.icon,
})),
],
[logicFunctions, toolIndex],
);
const applicationById = new Map(
(applicationsData?.findManyApplications ?? []).map((application) => [
application.id,
application,
]),
);
const marketplaceAppByUniversalIdentifier = new Map(
(marketplaceAppsData?.findManyMarketplaceApps ?? []).map(
(marketplaceApp) => [marketplaceApp.universalIdentifier, marketplaceApp],
),
);
const filteredTools = allTools
.filter((item) => {
const searchNormalized = normalizeSearchText(searchTerm);
const matchesSearch =
normalizeSearchText(item.name).includes(searchNormalized) ||
normalizeSearchText(item.description ?? '').includes(searchNormalized);
if (!matchesSearch) {
return false;
}
if (!isCustom(item)) {
return showStandardTools;
}
if (isManaged(item.applicationId)) {
return showManagedTools;
}
return showCustomTools;
})
.sort((a, b) => a.name.localeCompare(b.name));
const showSkeleton = toolIndexLoading && !toolIndexError;
return (
<>
<Section>
<H2Title
title={t`Custom`}
description={t`Custom tools created in your workspace`}
<Section>
<H2Title
title={t`Tools`}
description={t`Use filter to see existing tools or create your own`}
/>
<StyledSearchContainer>
<SearchInput
placeholder={t`Search a tool...`}
value={searchTerm}
onChange={setSearchTerm}
filterDropdown={(filterButton: ReactNode) => (
<Dropdown
dropdownId="settings-tools-filter-dropdown"
dropdownPlacement="bottom-end"
dropdownOffset={{ x: 0, y: 8 }}
clickableComponent={filterButton}
dropdownComponents={
<DropdownContent>
<DropdownMenuItemsContainer>
<MenuItemToggle
LeftIcon={IconTool}
onToggleChange={() =>
setShowCustomTools(!showCustomTools)
}
toggled={showCustomTools}
text={t`Custom`}
toggleSize="small"
/>
<MenuItemToggle
LeftIcon={IconLock}
onToggleChange={() =>
setShowManagedTools(!showManagedTools)
}
toggled={showManagedTools}
text={t`Managed`}
toggleSize="small"
/>
<MenuItemToggle
LeftIcon={IconPuzzle}
onToggleChange={() =>
setShowStandardTools(!showStandardTools)
}
toggled={showStandardTools}
text={t`Standard`}
toggleSize="small"
/>
</DropdownMenuItemsContainer>
</DropdownContent>
}
/>
)}
/>
<StyledSearchContainer>
<SearchInput
placeholder={t`Search a custom tool...`}
value={customSearchTerm}
onChange={setCustomSearchTerm}
/>
</StyledSearchContainer>
<Table>
<StyledTableHeaderRowContainer>
<TableRow
gridTemplateColumns={TOOL_TABLE_ROW_GRID_TEMPLATE_COLUMNS}
>
<TableHeader>{t`Name`}</TableHeader>
<TableHeader align="right">{t`Type`}</TableHeader>
<TableHeader />
</TableRow>
</StyledTableHeaderRowContainer>
{showSkeleton
? Array.from({ length: 3 }).map((_, index) => (
<Skeleton height={32} borderRadius={4} key={index} />
))
: filteredTools.map((tool) => (
</StyledSearchContainer>
<Table>
<StyledTableHeaderRowContainer>
<TableRow gridTemplateColumns={TOOL_TABLE_ROW_GRID_TEMPLATE_COLUMNS}>
<TableHeader>{t`Name`}</TableHeader>
<TableHeader>{t`App`}</TableHeader>
<TableHeader />
</TableRow>
</StyledTableHeaderRowContainer>
{showSkeleton
? Array.from({ length: 3 }).map((_, index) => (
<SkeletonTheme
key={index}
baseColor={theme.background.tertiary}
highlightColor={theme.background.transparent.lighter}
borderRadius={4}
>
<Skeleton height={32} borderRadius={4} />
</SkeletonTheme>
))
: filteredTools.map((item) => {
const application = isDefined(item.applicationId)
? applicationById.get(item.applicationId)
: undefined;
const marketplaceApp = isDefined(application)
? marketplaceAppByUniversalIdentifier.get(
application.universalIdentifier,
)
: undefined;
const appLabel = isCustom(item)
? (application?.name ?? t`Custom`)
: t`Standard`;
return (
<SettingsToolTableRow
key={tool.id}
tool={tool}
key={item.identifier}
leftIcon={
<SettingsToolIcon
icon={item.icon}
toolName={item.name}
objectName={item.objectName ?? undefined}
application={application}
marketplaceApp={marketplaceApp}
/>
}
name={item.name}
appLabel={appLabel}
action={
<IconChevronRight
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
}
link={getToolLink(tool)}
link={getToolLink(item)}
/>
))}
</Table>
<StyledFooterContainer>
<Button
Icon={IconPlus}
title={t`New Tool`}
size="small"
variant="secondary"
onClick={handleCreateTool}
disabled={isCreating}
/>
</StyledFooterContainer>
</Section>
<Section>
<H2Title
title={t`Built-in`}
description={t`Standard tools available to AI agents`}
/>
<StyledSearchContainer>
<SearchInput
placeholder={t`Search a built-in tool...`}
value={builtInSearchTerm}
onChange={setBuiltInSearchTerm}
/>
</StyledSearchContainer>
<Table>
<StyledTableHeaderRowContainer>
<TableRow
gridTemplateColumns={TOOL_TABLE_ROW_GRID_TEMPLATE_COLUMNS}
>
<TableHeader>{t`Name`}</TableHeader>
<TableHeader align="right">{t`Type`}</TableHeader>
<TableHeader />
</TableRow>
</StyledTableHeaderRowContainer>
{filteredSystemTools.map((systemTool) => (
<SettingsSystemToolTableRow
key={systemTool.name}
tool={systemTool}
/>
))}
</Table>
</Section>
</>
);
})}
</Table>
</Section>
);
};
@@ -1,9 +1,8 @@
import { type ToolSet } from 'ai';
import { type CodeExecutionData } from 'twenty-shared/ai';
import { type CodeExecutionData, type ToolCategory } from 'twenty-shared/ai';
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,
type ToolIndexEntry,
@@ -9,7 +9,7 @@ import {
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 { ToolCategory } from 'twenty-shared/ai';
import {
type StaticToolHandler,
ToolExecutorService,
@@ -159,6 +159,7 @@ export class ActionToolProvider implements ToolProvider {
name: toolId,
description: tool.description,
category: ToolCategory.ACTION,
icon: 'IconPlayerPlay',
...(includeSchemas && {
inputSchema: z.toJSONSchema(tool.inputSchema as z.ZodType),
}),
@@ -9,7 +9,8 @@ import {
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
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 { ToolCategory } from 'twenty-shared/ai';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
import {
type ToolDescriptor,
@@ -17,6 +18,7 @@ import {
} 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 { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import type { DashboardToolWorkspaceService } from 'src/modules/dashboard/tools/services/dashboard-tool.workspace-service';
@Injectable()
@@ -29,6 +31,7 @@ export class DashboardToolProvider implements ToolProvider, OnModuleInit {
private readonly dashboardToolService: DashboardToolWorkspaceService | null,
private readonly permissionsService: PermissionsService,
private readonly toolExecutorService: ToolExecutorService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
onModuleInit(): void {
@@ -71,8 +74,33 @@ export class DashboardToolProvider implements ToolProvider, OnModuleInit {
context.rolePermissionConfig,
);
const icon = await this.resolveObjectIcon(
context.workspaceId,
CoreObjectNameSingular.Dashboard,
);
return toolSetToDescriptors(toolSet, ToolCategory.DASHBOARD, {
includeSchemas: options?.includeSchemas ?? true,
icon,
});
}
private async resolveObjectIcon(
workspaceId: string,
nameSingular: string,
): Promise<string | undefined> {
const { flatObjectMetadataMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatObjectMetadataMaps'],
},
);
const flatObject = Object.values(
flatObjectMetadataMaps.byUniversalIdentifier,
).find((obj) => obj?.nameSingular === nameSingular);
return flatObject?.icon ?? undefined;
}
}
@@ -21,7 +21,7 @@ import { generateUpdateRecordInputSchema } from 'src/engine/core-modules/record-
import { DeleteToolInputSchema } from 'src/engine/core-modules/record-crud/zod-schemas/delete-tool.zod-schema';
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 { ToolCategory } from 'twenty-shared/ai';
import {
type ToolDescriptor,
type ToolIndexEntry,
@@ -122,6 +122,7 @@ export class DatabaseToolProvider implements ToolProvider {
operation: 'find',
},
objectName: objectMetadata.nameSingular,
icon: flatObject.icon ?? undefined,
operation: 'find',
});
@@ -138,6 +139,7 @@ export class DatabaseToolProvider implements ToolProvider {
operation: 'find_one',
},
objectName: objectMetadata.nameSingular,
icon: flatObject.icon ?? undefined,
operation: 'find_one',
});
}
@@ -158,6 +160,7 @@ export class DatabaseToolProvider implements ToolProvider {
operation: 'create',
},
objectName: objectMetadata.nameSingular,
icon: flatObject.icon ?? undefined,
operation: 'create',
});
@@ -179,6 +182,7 @@ export class DatabaseToolProvider implements ToolProvider {
operation: 'create_many',
},
objectName: objectMetadata.nameSingular,
icon: flatObject.icon ?? undefined,
operation: 'create_many',
});
@@ -197,6 +201,7 @@ export class DatabaseToolProvider implements ToolProvider {
operation: 'update',
},
objectName: objectMetadata.nameSingular,
icon: flatObject.icon ?? undefined,
operation: 'update',
});
@@ -218,6 +223,7 @@ export class DatabaseToolProvider implements ToolProvider {
operation: 'update_many',
},
objectName: objectMetadata.nameSingular,
icon: flatObject.icon ?? undefined,
operation: 'update_many',
});
}
@@ -236,6 +242,7 @@ export class DatabaseToolProvider implements ToolProvider {
operation: 'delete',
},
objectName: objectMetadata.nameSingular,
icon: flatObject.icon ?? undefined,
operation: 'delete',
});
}
@@ -9,7 +9,7 @@ import {
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 { ToolCategory } from 'twenty-shared/ai';
import {
type ToolDescriptor,
type ToolIndexEntry,
@@ -8,7 +8,7 @@ import {
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 { ToolCategory } from 'twenty-shared/ai';
import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
import {
type ToolDescriptor,
@@ -62,6 +62,7 @@ export class MetadataToolProvider implements ToolProvider, OnModuleInit {
return toolSetToDescriptors(toolSet, ToolCategory.METADATA, {
includeSchemas: options?.includeSchemas ?? true,
icon: 'IconSettings',
});
}
}
@@ -8,7 +8,7 @@ import {
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 { ToolCategory } from 'twenty-shared/ai';
import { AgentModelConfigService } from 'src/engine/metadata-modules/ai/ai-models/services/agent-model-config.service';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
@@ -8,7 +8,7 @@ import {
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 { ToolCategory } from 'twenty-shared/ai';
import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
import {
type ToolDescriptor,
@@ -64,6 +64,7 @@ export class ViewFieldToolProvider implements ToolProvider, OnModuleInit {
): Promise<(ToolIndexEntry | ToolDescriptor)[]> {
const schemaOptions = {
includeSchemas: options?.includeSchemas ?? true,
icon: 'IconTable',
};
const readTools = this.viewFieldToolsFactory.generateReadTools(
@@ -8,7 +8,7 @@ import {
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 { ToolCategory } from 'twenty-shared/ai';
import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
import {
type ToolDescriptor,
@@ -74,6 +74,7 @@ export class ViewToolProvider implements ToolProvider, OnModuleInit {
const workspaceMemberId = context.actorContext?.workspaceMemberId;
const schemaOptions = {
includeSchemas: options?.includeSchemas ?? true,
icon: 'IconTable',
};
const readTools = this.viewToolsFactory.generateReadTools(
@@ -9,7 +9,8 @@ import {
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
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 { ToolCategory } from 'twenty-shared/ai';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
import {
type ToolDescriptor,
@@ -17,6 +18,7 @@ import {
} 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 { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import type { WorkflowToolWorkspaceService } from 'src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service';
@Injectable()
@@ -29,6 +31,7 @@ export class WorkflowToolProvider implements ToolProvider, OnModuleInit {
private readonly workflowToolService: WorkflowToolWorkspaceService | null,
private readonly permissionsService: PermissionsService,
private readonly toolExecutorService: ToolExecutorService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
onModuleInit(): void {
@@ -71,8 +74,33 @@ export class WorkflowToolProvider implements ToolProvider, OnModuleInit {
context.rolePermissionConfig,
);
const icon = await this.resolveObjectIcon(
context.workspaceId,
CoreObjectNameSingular.Workflow,
);
return toolSetToDescriptors(toolSet, ToolCategory.WORKFLOW, {
includeSchemas: options?.includeSchemas ?? true,
icon,
});
}
private async resolveObjectIcon(
workspaceId: string,
nameSingular: string,
): Promise<string | undefined> {
const { flatObjectMetadataMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatObjectMetadataMaps'],
},
);
const flatObject = Object.values(
flatObjectMetadataMaps.byUniversalIdentifier,
).find((obj) => obj?.nameSingular === nameSingular);
return flatObject?.icon ?? undefined;
}
}
@@ -28,6 +28,9 @@ export class ToolIndexEntryDTO {
@Field({ nullable: true })
objectName?: string;
@Field({ nullable: true })
icon?: string;
@Field(() => graphqlTypeJson, { nullable: true })
inputSchema?: object;
}
@@ -22,7 +22,7 @@ import { DeleteRecordService } from 'src/engine/core-modules/record-crud/service
import { FindRecordsService } from 'src/engine/core-modules/record-crud/services/find-records.service';
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 ToolCategory } from 'twenty-shared/ai';
import {
type ToolDescriptor,
type ToolIndexEntry,
@@ -10,7 +10,7 @@ import {
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
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 { ToolCategory } from 'twenty-shared/ai';
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';
@@ -1,6 +1,6 @@
import { z } from 'zod';
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
import { ToolCategory } from 'twenty-shared/ai';
import { type ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
import { type ToolIndexEntry } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
@@ -1,4 +1,4 @@
import { type ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
import { type ToolCategory } from 'twenty-shared/ai';
export type DatabaseCrudOperation =
| 'find'
@@ -26,6 +26,7 @@ export type ToolIndexEntry = {
executionRef: ToolExecutionRef;
objectName?: string;
operation?: string;
icon?: string;
};
// Full descriptor with schema (on-demand)
@@ -1,7 +1,7 @@
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 ToolCategory } from 'twenty-shared/ai';
import {
type ToolDescriptor,
type ToolIndexEntry,
@@ -9,6 +9,7 @@ import {
export type ToolSetToDescriptorsOptions = {
includeSchemas?: boolean;
icon?: string;
};
// Converts a ToolSet (with Zod schemas and closures) into an array of
@@ -27,6 +28,7 @@ export const toolSetToDescriptors = (
description: tool.description ?? '',
category,
executionRef: { kind: 'static' as const, toolId: name },
...(options?.icon && { icon: options.icon }),
};
if (!includeSchemas) {
@@ -16,7 +16,7 @@ import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/
import { isUserAuthContext } from 'src/engine/core-modules/auth/guards/is-user-auth-context.guard';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
import { ToolCategory } from 'twenty-shared/ai';
import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
import { type AgentExecutionResult } from 'src/engine/metadata-modules/ai/ai-agent-execution/types/agent-execution-result.type';
import { extractCacheCreationTokensFromSteps } from 'src/engine/metadata-modules/ai/ai-billing/utils/extract-cache-creation-tokens.util';
@@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
import { assertUnreachable } from 'twenty-shared/utils';
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 { ToolCategory } from 'twenty-shared/ai';
import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
import {
EXECUTE_TOOL_TOOL_NAME,
+1
View File
@@ -14,6 +14,7 @@ export type { DataResidency } from './constants/data-residency.const';
export { DATA_RESIDENCY_KEYS } from './constants/data-residency.const';
export type { NativeAiSdkProviderId } from './constants/native-ai-sdk-provider-ids.const';
export { NATIVE_AI_SDK_PROVIDER_IDS } from './constants/native-ai-sdk-provider-ids.const';
export { ToolCategory } from './constants/tool-category.const';
export type {
AgentResponseFieldType,
AgentResponseSchema,
@@ -39,6 +39,7 @@ export enum SettingsPath {
AIAgentTurnDetail = 'ai/agents/:agentId/turns/:turnId',
AINewSkill = 'ai/new-skill',
AISkillDetail = 'ai/skills/:skillId',
AIToolDetail = 'ai/tools/:toolIdentifier',
Applications = 'applications',
ApplicationDetail = 'applications/:applicationId',
ApplicationLogicFunctionDetail = 'applications/:applicationId/logicFunctions/:logicFunctionId',