feat: improve AI chat - system prompt, tool output, context window display (#17769)
⚠️ **AI-generated PR — not ready for review** ⚠️ cc @FelixMalfait --- ## Changes ### System prompt improvements - Explicit skill-before-tools workflow to prevent the model from calling tools without loading the matching skill first - Data efficiency guidance (default small limits, use filters) - Pluralized `load_skill` → `load_skills` for consistency with `load_tools` ### Token usage reduction - Output serialization layer: strips null/undefined/empty values from tool results - Lowered default `find_*` limit from 100 → 10, max from 1000 → 100 ### System object tool generation - System objects (calendar events, messages, etc.) now generate AI tools - Only workflow-related and favorite-related objects are excluded ### Context window display fix - **Bug**: UI compared cumulative tokens (sum of all turns) against single-request context window → showed 100% after a few turns - **Fix**: Track `conversationSize` (last step's `inputTokens`) which represents the actual conversation history size sent to the model - New `conversationSize` column on thread entity with migration ### Workspace AI instructions - Support for custom workspace-level AI instructions --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
This commit is contained in:
+4
-3
@@ -30,6 +30,7 @@ const StyledEditorContainer = styled.div<{
|
||||
color: ${({ theme, readonly }) =>
|
||||
readonly ? theme.font.color.light : theme.font.color.primary};
|
||||
font-family: ${({ theme }) => theme.font.family};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
border: none !important;
|
||||
|
||||
@@ -54,15 +55,15 @@ const StyledEditorContainer = styled.div<{
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
li {
|
||||
|
||||
@@ -84,17 +84,18 @@ export const AIChatThreadGroup = ({
|
||||
const handleThreadClick = (thread: AgentChatThread) => {
|
||||
setCurrentAIChatThread(thread.id);
|
||||
|
||||
const totalTokens = thread.totalInputTokens + thread.totalOutputTokens;
|
||||
const hasUsageData =
|
||||
totalTokens > 0 && isDefined(thread.contextWindowTokens);
|
||||
(thread.conversationSize ?? 0) > 0 &&
|
||||
isDefined(thread.contextWindowTokens);
|
||||
|
||||
setAgentChatUsage(
|
||||
hasUsageData
|
||||
? {
|
||||
lastMessage: null,
|
||||
conversationSize: thread.conversationSize ?? 0,
|
||||
contextWindowTokens: thread.contextWindowTokens ?? 0,
|
||||
inputTokens: thread.totalInputTokens,
|
||||
outputTokens: thread.totalOutputTokens,
|
||||
totalTokens,
|
||||
contextWindowTokens: thread.contextWindowTokens ?? 0,
|
||||
inputCredits: thread.totalInputCredits,
|
||||
outputCredits: thread.totalOutputCredits,
|
||||
}
|
||||
|
||||
@@ -9,7 +9,10 @@ import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
import { CodeExecutionDisplay } from '@/ai/components/CodeExecutionDisplay';
|
||||
import { ShimmeringText } from '@/ai/components/ShimmeringText';
|
||||
import { getToolIcon } from '@/ai/utils/getToolIcon';
|
||||
import { getToolDisplayMessage } from '@/ai/utils/getWebSearchToolDisplayMessage';
|
||||
import {
|
||||
getToolDisplayMessage,
|
||||
resolveToolInput,
|
||||
} from '@/ai/utils/getToolDisplayMessage';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { type ToolUIPart } from 'ai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -132,12 +135,10 @@ export const ToolStepRenderer = ({ toolPart }: { toolPart: ToolUIPart }) => {
|
||||
const [activeTab, setActiveTab] = useState<TabType>('output');
|
||||
|
||||
const { input, output, type, errorText } = toolPart;
|
||||
const toolName = type.split('-')[1];
|
||||
const rawToolName = type.split('-')[1];
|
||||
|
||||
const toolInput =
|
||||
isDefined(input) && typeof input === 'object' && 'input' in input
|
||||
? input.input
|
||||
: input;
|
||||
const { resolvedInput: toolInput, resolvedToolName: toolName } =
|
||||
resolveToolInput(input, rawToolName);
|
||||
|
||||
const hasError = isDefined(errorText);
|
||||
const isExpandable = isDefined(output) || hasError;
|
||||
@@ -175,7 +176,7 @@ export const ToolStepRenderer = ({ toolPart }: { toolPart: ToolUIPart }) => {
|
||||
<StyledLoadingContainer>
|
||||
<ShimmeringText>
|
||||
<StyledDisplayMessage>
|
||||
{getToolDisplayMessage(input, toolName, false)}
|
||||
{getToolDisplayMessage(input, rawToolName, false)}
|
||||
</StyledDisplayMessage>
|
||||
</ShimmeringText>
|
||||
</StyledLoadingContainer>
|
||||
@@ -188,19 +189,32 @@ export const ToolStepRenderer = ({ toolPart }: { toolPart: ToolUIPart }) => {
|
||||
);
|
||||
}
|
||||
|
||||
// For execute_tool, the actual result is nested inside output.result
|
||||
const unwrappedOutput =
|
||||
rawToolName === 'execute_tool' &&
|
||||
isDefined(output) &&
|
||||
typeof output === 'object' &&
|
||||
'result' in output
|
||||
? (output as { result: unknown }).result
|
||||
: output;
|
||||
|
||||
const displayMessage = hasError
|
||||
? t`Tool execution failed`
|
||||
: output &&
|
||||
typeof output === 'object' &&
|
||||
'message' in output &&
|
||||
typeof output.message === 'string'
|
||||
? output.message
|
||||
: getToolDisplayMessage(input, toolName, true);
|
||||
: rawToolName === 'learn_tools' || rawToolName === 'execute_tool'
|
||||
? getToolDisplayMessage(input, rawToolName, true)
|
||||
: unwrappedOutput &&
|
||||
typeof unwrappedOutput === 'object' &&
|
||||
'message' in unwrappedOutput &&
|
||||
typeof unwrappedOutput.message === 'string'
|
||||
? unwrappedOutput.message
|
||||
: getToolDisplayMessage(input, rawToolName, true);
|
||||
|
||||
const result =
|
||||
output && typeof output === 'object' && 'result' in output
|
||||
? (output as { result: string }).result
|
||||
: output;
|
||||
unwrappedOutput &&
|
||||
typeof unwrappedOutput === 'object' &&
|
||||
'result' in unwrappedOutput
|
||||
? (unwrappedOutput as { result: string }).result
|
||||
: unwrappedOutput;
|
||||
|
||||
const ToolIcon = getToolIcon(toolName);
|
||||
|
||||
|
||||
@@ -82,8 +82,10 @@ print("Chart saved successfully!")`,
|
||||
usage: {
|
||||
inputTokens: 1250,
|
||||
outputTokens: 890,
|
||||
cachedInputTokens: 0,
|
||||
inputCredits: 12,
|
||||
outputCredits: 8,
|
||||
conversationSize: 1250,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
+95
-39
@@ -1,12 +1,17 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { ProgressBar } from 'twenty-ui/feedback';
|
||||
|
||||
import { ContextUsageProgressRing } from '@/ai/components/internal/ContextUsageProgressRing';
|
||||
import { agentChatUsageState } from '@/ai/states/agentChatUsageState';
|
||||
import {
|
||||
agentChatUsageState,
|
||||
type AgentChatLastMessageUsage,
|
||||
} from '@/ai/states/agentChatUsageState';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
position: relative;
|
||||
@@ -41,14 +46,14 @@ const StyledHoverCard = styled.div`
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
box-shadow: ${({ theme }) => theme.boxShadow.strong};
|
||||
min-width: 240px;
|
||||
min-width: 280px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: calc(100% + 8px);
|
||||
z-index: ${({ theme }) => theme.lastLayerZIndex};
|
||||
`;
|
||||
|
||||
const StyledHeader = styled.div`
|
||||
const StyledSection = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
@@ -61,14 +66,6 @@ const StyledRow = styled.div`
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const StyledBody = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
padding: ${({ theme }) => theme.spacing(3)};
|
||||
padding-top: 0;
|
||||
`;
|
||||
|
||||
const StyledLabel = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
@@ -79,15 +76,16 @@ const StyledValue = styled.span`
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
`;
|
||||
|
||||
const StyledFooter = styled.div`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
const StyledSectionTitle = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.xs};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
`;
|
||||
|
||||
const StyledDivider = styled.div`
|
||||
border-top: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
border-radius: 0 0 ${({ theme }) => theme.border.radius.md}
|
||||
${({ theme }) => theme.border.radius.md};
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
const formatTokenCount = (count: number): string => {
|
||||
@@ -103,6 +101,31 @@ const formatTokenCount = (count: number): string => {
|
||||
return count.toString();
|
||||
};
|
||||
|
||||
const formatCredits = (credits: number): string => {
|
||||
// Credits are already in display units from the API (internal / 1000)
|
||||
// Show up to 1 decimal for fractional values, none for whole numbers
|
||||
if (Number.isInteger(credits)) {
|
||||
return credits.toLocaleString();
|
||||
}
|
||||
|
||||
return credits.toLocaleString(undefined, {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 1,
|
||||
});
|
||||
};
|
||||
|
||||
const getCachedLabel = (lastMessage: AgentChatLastMessageUsage): string => {
|
||||
if (lastMessage.cachedInputTokens <= 0 || lastMessage.inputTokens <= 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const cachedPercent = Math.round(
|
||||
(lastMessage.cachedInputTokens / lastMessage.inputTokens) * 100,
|
||||
);
|
||||
|
||||
return ` (${t`${cachedPercent}% cached`})`;
|
||||
};
|
||||
|
||||
export const AIChatContextUsageButton = () => {
|
||||
const { t } = useLingui();
|
||||
const theme = useTheme();
|
||||
@@ -121,14 +144,14 @@ export const AIChatContextUsageButton = () => {
|
||||
}
|
||||
|
||||
const percentage = Math.min(
|
||||
(agentChatUsage.totalTokens / agentChatUsage.contextWindowTokens) * 100,
|
||||
(agentChatUsage.conversationSize / agentChatUsage.contextWindowTokens) *
|
||||
100,
|
||||
100,
|
||||
);
|
||||
const formattedPercentage = percentage.toFixed(1);
|
||||
const totalCredits =
|
||||
agentChatUsage.inputCredits + agentChatUsage.outputCredits;
|
||||
const inputCredits = agentChatUsage.inputCredits.toLocaleString();
|
||||
const outputCredits = agentChatUsage.outputCredits.toLocaleString();
|
||||
const lastMessage = agentChatUsage.lastMessage;
|
||||
|
||||
return (
|
||||
<StyledContainer
|
||||
@@ -142,12 +165,13 @@ export const AIChatContextUsageButton = () => {
|
||||
|
||||
{isHovered && (
|
||||
<StyledHoverCard>
|
||||
<StyledHeader>
|
||||
<StyledSection>
|
||||
<StyledRow>
|
||||
<StyledPercentage>{formattedPercentage}%</StyledPercentage>
|
||||
<StyledValue>
|
||||
{formatTokenCount(agentChatUsage.totalTokens)} /{' '}
|
||||
{formatTokenCount(agentChatUsage.contextWindowTokens)}
|
||||
{formatTokenCount(agentChatUsage.conversationSize)} /{' '}
|
||||
{formatTokenCount(agentChatUsage.contextWindowTokens)}{' '}
|
||||
{t`tokens`}
|
||||
</StyledValue>
|
||||
</StyledRow>
|
||||
<ProgressBar
|
||||
@@ -162,29 +186,61 @@ export const AIChatContextUsageButton = () => {
|
||||
backgroundColor={theme.background.quaternary}
|
||||
withBorderRadius
|
||||
/>
|
||||
</StyledHeader>
|
||||
</StyledSection>
|
||||
|
||||
<StyledBody>
|
||||
{isDefined(lastMessage) && (
|
||||
<>
|
||||
<StyledDivider />
|
||||
<StyledSection>
|
||||
<StyledSectionTitle>{t`Last message`}</StyledSectionTitle>
|
||||
<StyledRow>
|
||||
<StyledLabel>{t`Input tokens`}</StyledLabel>
|
||||
<StyledValue>
|
||||
{formatTokenCount(lastMessage.inputTokens)}
|
||||
{getCachedLabel(lastMessage)}
|
||||
</StyledValue>
|
||||
</StyledRow>
|
||||
<StyledRow>
|
||||
<StyledLabel>{t`Output tokens`}</StyledLabel>
|
||||
<StyledValue>
|
||||
{formatTokenCount(lastMessage.outputTokens)}
|
||||
</StyledValue>
|
||||
</StyledRow>
|
||||
<StyledRow>
|
||||
<StyledLabel>{t`Cost`}</StyledLabel>
|
||||
<StyledValue>
|
||||
{formatCredits(
|
||||
lastMessage.inputCredits + lastMessage.outputCredits,
|
||||
)}{' '}
|
||||
{t`credits`}
|
||||
</StyledValue>
|
||||
</StyledRow>
|
||||
</StyledSection>
|
||||
</>
|
||||
)}
|
||||
|
||||
<StyledDivider />
|
||||
<StyledSection>
|
||||
<StyledSectionTitle>{t`Conversation`}</StyledSectionTitle>
|
||||
<StyledRow>
|
||||
<StyledLabel>{t`Input`}</StyledLabel>
|
||||
<StyledLabel>{t`Input tokens`}</StyledLabel>
|
||||
<StyledValue>
|
||||
{formatTokenCount(agentChatUsage.inputTokens)} •{' '}
|
||||
{t`${inputCredits} credits`}
|
||||
{formatTokenCount(agentChatUsage.inputTokens)}
|
||||
</StyledValue>
|
||||
</StyledRow>
|
||||
<StyledRow>
|
||||
<StyledLabel>{t`Output`}</StyledLabel>
|
||||
<StyledLabel>{t`Output tokens`}</StyledLabel>
|
||||
<StyledValue>
|
||||
{formatTokenCount(agentChatUsage.outputTokens)} •{' '}
|
||||
{t`${outputCredits} credits`}
|
||||
{formatTokenCount(agentChatUsage.outputTokens)}
|
||||
</StyledValue>
|
||||
</StyledRow>
|
||||
</StyledBody>
|
||||
|
||||
<StyledFooter>
|
||||
<StyledLabel>{t`Total credits`}</StyledLabel>
|
||||
<StyledPercentage>{totalCredits.toLocaleString()}</StyledPercentage>
|
||||
</StyledFooter>
|
||||
<StyledRow>
|
||||
<StyledLabel>{t`Total cost`}</StyledLabel>
|
||||
<StyledValue>
|
||||
{formatCredits(totalCredits)} {t`credits`}
|
||||
</StyledValue>
|
||||
</StyledRow>
|
||||
</StyledSection>
|
||||
</StyledHoverCard>
|
||||
)}
|
||||
</StyledContainer>
|
||||
|
||||
@@ -8,6 +8,7 @@ export const GET_CHAT_THREADS = gql`
|
||||
totalInputTokens
|
||||
totalOutputTokens
|
||||
contextWindowTokens
|
||||
conversationSize
|
||||
totalInputCredits
|
||||
totalOutputCredits
|
||||
createdAt
|
||||
|
||||
@@ -119,8 +119,10 @@ export const useAgentChat = (uiMessages: ExtendedUIMessage[]) => {
|
||||
type UsageMetadata = {
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
cachedInputTokens: number;
|
||||
inputCredits: number;
|
||||
outputCredits: number;
|
||||
conversationSize: number;
|
||||
};
|
||||
type ModelMetadata = {
|
||||
contextWindowTokens: number;
|
||||
@@ -133,11 +135,17 @@ export const useAgentChat = (uiMessages: ExtendedUIMessage[]) => {
|
||||
|
||||
if (isDefined(usage) && isDefined(model)) {
|
||||
setAgentChatUsage((prev) => ({
|
||||
lastMessage: {
|
||||
inputTokens: usage.inputTokens,
|
||||
outputTokens: usage.outputTokens,
|
||||
cachedInputTokens: usage.cachedInputTokens,
|
||||
inputCredits: usage.inputCredits,
|
||||
outputCredits: usage.outputCredits,
|
||||
},
|
||||
conversationSize: usage.conversationSize,
|
||||
contextWindowTokens: model.contextWindowTokens,
|
||||
inputTokens: (prev?.inputTokens ?? 0) + usage.inputTokens,
|
||||
outputTokens: (prev?.outputTokens ?? 0) + usage.outputTokens,
|
||||
totalTokens:
|
||||
(prev?.totalTokens ?? 0) + usage.inputTokens + usage.outputTokens,
|
||||
contextWindowTokens: model.contextWindowTokens,
|
||||
inputCredits: (prev?.inputCredits ?? 0) + usage.inputCredits,
|
||||
outputCredits: (prev?.outputCredits ?? 0) + usage.outputCredits,
|
||||
}));
|
||||
|
||||
@@ -23,16 +23,17 @@ const setUsageFromThread = (
|
||||
thread: AgentChatThread,
|
||||
setAgentChatUsage: SetterOrUpdater<AgentChatUsageState | null>,
|
||||
) => {
|
||||
const totalTokens = thread.totalInputTokens + thread.totalOutputTokens;
|
||||
const hasUsageData = totalTokens > 0 && isDefined(thread.contextWindowTokens);
|
||||
const hasUsageData =
|
||||
(thread.conversationSize ?? 0) > 0 && isDefined(thread.contextWindowTokens);
|
||||
|
||||
setAgentChatUsage(
|
||||
hasUsageData
|
||||
? {
|
||||
lastMessage: null,
|
||||
conversationSize: thread.conversationSize ?? 0,
|
||||
contextWindowTokens: thread.contextWindowTokens ?? 0,
|
||||
inputTokens: thread.totalInputTokens,
|
||||
outputTokens: thread.totalOutputTokens,
|
||||
totalTokens,
|
||||
contextWindowTokens: thread.contextWindowTokens ?? 0,
|
||||
inputCredits: thread.totalInputCredits,
|
||||
outputCredits: thread.totalOutputCredits,
|
||||
}
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export type AgentChatUsageState = {
|
||||
export type AgentChatLastMessageUsage = {
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
totalTokens: number;
|
||||
cachedInputTokens: number;
|
||||
inputCredits: number;
|
||||
outputCredits: number;
|
||||
};
|
||||
|
||||
export type AgentChatUsageState = {
|
||||
lastMessage: AgentChatLastMessageUsage | null;
|
||||
conversationSize: number;
|
||||
contextWindowTokens: number;
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
inputCredits: number;
|
||||
outputCredits: number;
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ describe('groupThreadsByDate', () => {
|
||||
totalInputTokens: 0,
|
||||
totalOutputTokens: 0,
|
||||
contextWindowTokens: null,
|
||||
conversationSize: 0,
|
||||
totalInputCredits: 0,
|
||||
totalOutputCredits: 0,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { type ToolInput } from '@/ai/types/ToolInput';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const extractSearchQuery = (input: ToolInput): string => {
|
||||
if (!input) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (
|
||||
typeof input === 'object' &&
|
||||
'query' in input &&
|
||||
typeof input.query === 'string'
|
||||
) {
|
||||
return input.query;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const extractLoadingMessage = (input: ToolInput): string => {
|
||||
if (
|
||||
isDefined(input) &&
|
||||
typeof input === 'object' &&
|
||||
'loadingMessage' in input &&
|
||||
typeof input.loadingMessage === 'string'
|
||||
) {
|
||||
return input.loadingMessage;
|
||||
}
|
||||
|
||||
return 'Processing...';
|
||||
};
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
return { resolvedInput: input, resolvedToolName: toolName };
|
||||
};
|
||||
|
||||
const extractLearnToolNames = (input: ToolInput): string => {
|
||||
if (
|
||||
isDefined(input) &&
|
||||
typeof input === 'object' &&
|
||||
'toolNames' in input &&
|
||||
Array.isArray(input.toolNames)
|
||||
) {
|
||||
return input.toolNames.join(', ');
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
export const getToolDisplayMessage = (
|
||||
input: ToolInput,
|
||||
toolName: string,
|
||||
isFinished?: boolean,
|
||||
): string => {
|
||||
const { resolvedInput, resolvedToolName } = resolveToolInput(input, toolName);
|
||||
|
||||
if (resolvedToolName === 'web_search') {
|
||||
const query = extractSearchQuery(resolvedInput);
|
||||
const action = isFinished ? 'Searched' : 'Searching';
|
||||
|
||||
return query ? `${action} the web for '${query}'` : `${action} the web`;
|
||||
}
|
||||
|
||||
if (resolvedToolName === 'learn_tools') {
|
||||
const names = extractLearnToolNames(resolvedInput);
|
||||
const action = isFinished ? 'Learned' : 'Learning';
|
||||
|
||||
return names ? `${action} ${names}` : `${action} tools...`;
|
||||
}
|
||||
|
||||
return extractLoadingMessage(resolvedInput);
|
||||
};
|
||||
@@ -1,6 +1,16 @@
|
||||
import { IconDatabase, IconMail, IconTool, IconWorld } from 'twenty-ui/display';
|
||||
import {
|
||||
IconBook2,
|
||||
IconDatabase,
|
||||
IconMail,
|
||||
IconTool,
|
||||
IconWorld,
|
||||
} from 'twenty-ui/display';
|
||||
|
||||
const TOOL_ICON_MAPPINGS = [
|
||||
{
|
||||
keywords: ['learn_tools'],
|
||||
icon: IconBook2,
|
||||
},
|
||||
{
|
||||
keywords: ['email'],
|
||||
icon: IconMail,
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { type ToolInput } from '@/ai/types/ToolInput';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const extractSearchQuery = (input: ToolInput): string => {
|
||||
if (!input) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (
|
||||
typeof input === 'object' &&
|
||||
'query' in input &&
|
||||
typeof input.query === 'string'
|
||||
) {
|
||||
return input.query;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const extractLoadingMessage = (input: ToolInput): string => {
|
||||
if (
|
||||
isDefined(input) &&
|
||||
typeof input === 'object' &&
|
||||
'loadingMessage' in input &&
|
||||
typeof input.loadingMessage === 'string'
|
||||
) {
|
||||
return input.loadingMessage;
|
||||
}
|
||||
|
||||
return 'Processing...';
|
||||
};
|
||||
|
||||
export const getToolDisplayMessage = (
|
||||
input: ToolInput,
|
||||
toolName: string,
|
||||
isFinished?: boolean,
|
||||
): string => {
|
||||
if (toolName === 'web_search') {
|
||||
const query = extractSearchQuery(input);
|
||||
const action = isFinished ? 'Searched' : 'Searching';
|
||||
return query ? `${action} the web for '${query}'` : `${action} the web`;
|
||||
}
|
||||
|
||||
return extractLoadingMessage(input);
|
||||
};
|
||||
@@ -185,6 +185,12 @@ const SettingsSkillForm = lazy(() =>
|
||||
})),
|
||||
);
|
||||
|
||||
const SettingsAIPrompts = lazy(() =>
|
||||
import('~/pages/settings/ai/SettingsAIPrompts').then((module) => ({
|
||||
default: module.SettingsAIPrompts,
|
||||
})),
|
||||
);
|
||||
|
||||
const SettingsWorkspaceMembers = lazy(() =>
|
||||
import('~/pages/settings/members/SettingsWorkspaceMembers').then(
|
||||
(module) => ({
|
||||
@@ -442,6 +448,7 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
|
||||
element={<SettingsApiWebhooks />}
|
||||
/>
|
||||
<Route path={SettingsPath.AI} element={<SettingsAI />} />
|
||||
<Route path={SettingsPath.AIPrompts} element={<SettingsAIPrompts />} />
|
||||
<Route
|
||||
path={SettingsPath.AINewAgent}
|
||||
element={<SettingsAgentForm mode="create" />}
|
||||
|
||||
@@ -36,6 +36,7 @@ export type CurrentWorkspace = Pick<
|
||||
| 'eventLogRetentionDays'
|
||||
| 'fastModel'
|
||||
| 'smartModel'
|
||||
| 'aiAdditionalInstructions'
|
||||
| 'editableProfileFields'
|
||||
> & {
|
||||
defaultRole?: Omit<Role, 'workspaceMembers' | 'agents' | 'apiKeys'> | null;
|
||||
|
||||
@@ -88,6 +88,7 @@ export const USER_QUERY_FRAGMENT = gql`
|
||||
}
|
||||
fastModel
|
||||
smartModel
|
||||
aiAdditionalInstructions
|
||||
isTwoFactorAuthenticationEnforced
|
||||
trashRetentionDays
|
||||
eventLogRetentionDays
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_AI_SYSTEM_PROMPT_PREVIEW = gql`
|
||||
query GetAISystemPromptPreview {
|
||||
getAISystemPromptPreview {
|
||||
sections {
|
||||
title
|
||||
content
|
||||
estimatedTokenCount
|
||||
}
|
||||
estimatedTokenCount
|
||||
}
|
||||
}
|
||||
`;
|
||||
Reference in New Issue
Block a user