diff --git a/.github/workflows/ci-breaking-changes.yaml b/.github/workflows/ci-breaking-changes.yaml index fc6d4246eba..ba4c4b1b696 100644 --- a/.github/workflows/ci-breaking-changes.yaml +++ b/.github/workflows/ci-breaking-changes.yaml @@ -257,13 +257,16 @@ jobs: run: | git stash git checkout origin/main - git clean -fd + git reset --hard + git clean -xfd -ff + rm -rf node_modules packages/*/node_modules packages/*/dist dist .nx/cache - name: Install dependencies for main branch uses: ./.github/workflows/actions/yarn-install - name: Build main branch dependencies run: | + npx nx reset npx nx build twenty-shared npx nx build twenty-emails @@ -272,6 +275,7 @@ jobs: - name: Setup main branch database run: | + npx nx reset:env twenty-server # Function to set or update environment variable set_env_var() { local var_name="$1" @@ -287,6 +291,9 @@ jobs: set_env_var "PG_DATABASE_URL" "postgres://postgres:postgres@localhost:5432/main_branch" set_env_var "NODE_PORT" "${{ env.MAIN_SERVER_PORT }}" + set_env_var "REDIS_URL" "redis://localhost:6379" + set_env_var "CLICKHOUSE_URL" "http://default:clickhousePassword@localhost:8123/twenty" + set_env_var "CLICKHOUSE_PASSWORD" "clickhousePassword" npx nx run twenty-server:database:init:prod npx nx run twenty-server:database:migrate:prod diff --git a/packages/twenty-front/src/modules/ai/components/AIChatAssistantMessageRenderer.tsx b/packages/twenty-front/src/modules/ai/components/AIChatAssistantMessageRenderer.tsx index aa5c0129c17..27c47f753fc 100644 --- a/packages/twenty-front/src/modules/ai/components/AIChatAssistantMessageRenderer.tsx +++ b/packages/twenty-front/src/modules/ai/components/AIChatAssistantMessageRenderer.tsx @@ -1,7 +1,8 @@ import { ErrorStepRenderer } from '@/ai/components/ErrorStepRenderer'; import { ReasoningSummaryDisplay } from '@/ai/components/ReasoningSummaryDisplay'; import { ToolStepRenderer } from '@/ai/components/ToolStepRenderer'; -import type { ParsedStep } from '@/ai/types/streamTypes'; +import type { ParsedStep } from '@/ai/types/ParsedStep'; +import { hasStructuredStreamData } from '@/ai/utils/hasStructuredStreamData'; import { parseStream } from '@/ai/utils/parseStream'; import { IconDotsVertical } from 'twenty-ui/display'; @@ -64,25 +65,15 @@ export const AIChatAssistantMessageRenderer = ({ streamData: string; }) => { const agentStreamingMessage = useRecoilValue(agentStreamingMessageState); - const isStreaming = - Boolean(agentStreamingMessage) && streamData === agentStreamingMessage; + const isStreaming = streamData === agentStreamingMessage; if (!streamData) { return ; } - const isPlainString = - !streamData.includes('\n') || - !streamData.split('\n').some((line) => { - try { - JSON.parse(line); - return true; - } catch { - return false; - } - }); + const hasStructuredData = hasStructuredStreamData(streamData); - if (isPlainString) { + if (!hasStructuredData) { return ; } diff --git a/packages/twenty-front/src/modules/ai/components/ErrorStepRenderer.tsx b/packages/twenty-front/src/modules/ai/components/ErrorStepRenderer.tsx index 0ad53d105f6..628c4a11b95 100644 --- a/packages/twenty-front/src/modules/ai/components/ErrorStepRenderer.tsx +++ b/packages/twenty-front/src/modules/ai/components/ErrorStepRenderer.tsx @@ -1,6 +1,7 @@ import { extractErrorMessage } from '@/ai/utils/extractErrorMessage'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import { t } from '@lingui/core/macro'; import { IconAlertCircle } from 'twenty-ui/display'; const StyledContainer = styled.div` @@ -53,7 +54,7 @@ export const ErrorStepRenderer = ({ - Error + {t`Error`} {errorMessage} diff --git a/packages/twenty-front/src/modules/ai/components/ToolStepRenderer.tsx b/packages/twenty-front/src/modules/ai/components/ToolStepRenderer.tsx index d1d868221b2..036f7c66a8b 100644 --- a/packages/twenty-front/src/modules/ai/components/ToolStepRenderer.tsx +++ b/packages/twenty-front/src/modules/ai/components/ToolStepRenderer.tsx @@ -6,13 +6,13 @@ import { IconChevronDown, IconChevronUp } from 'twenty-ui/display'; import { AnimatedExpandableContainer } from 'twenty-ui/layout'; import { ShimmeringText } from '@/ai/components/ShimmeringText'; +import { getToolIcon } from '@/ai/utils/getToolIcon'; import type { ToolCallEvent, ToolEvent, ToolResultEvent, -} from '@/ai/types/streamTypes'; -import { extractErrorMessage } from '@/ai/utils/extractErrorMessage'; -import { getToolIcon } from '@/ai/utils/getToolIcon'; +} from 'twenty-shared/ai'; +import { isDefined } from 'twenty-shared/utils'; const StyledContainer = styled.div` display: flex; @@ -74,32 +74,27 @@ export const ToolStepRenderer = ({ events }: { events: ToolEvent[] }) => { const theme = useTheme(); const [isExpanded, setIsExpanded] = useState(false); - const toolCall = events[0] as ToolCallEvent | undefined; - const toolResult = events.find( + const toolCallEvent = events[0] as ToolCallEvent | undefined; + const toolResultEvent = events.find( (event): event is ToolResultEvent => event.type === 'tool-result', ); - if (!toolCall) { + if (!toolCallEvent) { return null; } - const toolOutput = toolResult?.result as ToolResultEvent['result']; - const isStandardizedFormat = - toolOutput && typeof toolOutput === 'object' && 'success' in toolOutput; + const toolOutput = + toolResultEvent?.output?.error ?? toolResultEvent?.output?.result; - const hasResult = isStandardizedFormat - ? Boolean(toolOutput.result) - : Boolean(toolResult?.result); - const hasError = isStandardizedFormat ? Boolean(toolOutput.error) : false; - const isExpandable = hasResult || hasError; + const isExpandable = isDefined(toolOutput); - if (!toolResult) { + if (!toolResultEvent) { return ( - {toolCall.args.loadingMessage} + {toolCallEvent.input.loadingMessage} @@ -108,13 +103,13 @@ export const ToolStepRenderer = ({ events }: { events: ToolEvent[] }) => { } const displayMessage = - toolResult?.result && - typeof toolResult.result === 'object' && - 'message' in toolResult.result - ? (toolResult.result as { message: string }).message + toolResultEvent?.output && + typeof toolResultEvent.output === 'object' && + 'message' in toolResultEvent.output + ? (toolResultEvent.output as { message: string }).message : undefined; - const ToolIcon = getToolIcon(toolCall.toolName); + const ToolIcon = getToolIcon(toolCallEvent.toolName); return ( @@ -137,20 +132,7 @@ export const ToolStepRenderer = ({ events }: { events: ToolEvent[] }) => { {isExpandable && ( - {isStandardizedFormat ? ( - <> - {hasError &&
{extractErrorMessage(toolOutput.error)}
} - {hasResult && ( -
- - {JSON.stringify(toolOutput.result, null, 2)} - -
- )} - - ) : toolResult?.result ? ( - JSON.stringify(toolResult.result, null, 2) - ) : undefined} + {JSON.stringify(toolOutput, null, 2)}
)} diff --git a/packages/twenty-front/src/modules/ai/types/ParsedStep.ts b/packages/twenty-front/src/modules/ai/types/ParsedStep.ts new file mode 100644 index 00000000000..037d2af5598 --- /dev/null +++ b/packages/twenty-front/src/modules/ai/types/ParsedStep.ts @@ -0,0 +1,7 @@ +import type { ToolEvent } from 'twenty-shared/ai'; + +export type ParsedStep = + | { type: 'tool'; events: ToolEvent[] } + | { type: 'reasoning'; content: string; isThinking: boolean } + | { type: 'text'; content: string } + | { type: 'error'; message: string; error?: unknown }; diff --git a/packages/twenty-front/src/modules/ai/types/streamTypes.ts b/packages/twenty-front/src/modules/ai/types/streamTypes.ts deleted file mode 100644 index 2d580e999d0..00000000000 --- a/packages/twenty-front/src/modules/ai/types/streamTypes.ts +++ /dev/null @@ -1,36 +0,0 @@ -export type ToolCallEvent = { - type: 'tool-call'; - toolCallId: string; - toolName: string; - args: { - loadingMessage: string; - input: unknown; - }; -}; - -export type ToolResultEvent = { - type: 'tool-result'; - toolCallId: string; - toolName: string; - result: { - success: boolean; - result?: unknown; - error?: string; - message: string; - }; - message: string; -}; - -export type ToolEvent = ToolCallEvent | ToolResultEvent; - -export type ErrorEvent = { - type: 'error'; - message: string; - error?: unknown; -}; - -export type ParsedStep = - | { type: 'tool'; events: ToolEvent[] } - | { type: 'reasoning'; content: string; isThinking: boolean } - | { type: 'text'; content: string } - | { type: 'error'; message: string; error?: unknown }; diff --git a/packages/twenty-front/src/modules/ai/utils/__tests__/parseStream.test.ts b/packages/twenty-front/src/modules/ai/utils/__tests__/parseStream.test.ts index 79ec1d28bca..26db8c14155 100644 --- a/packages/twenty-front/src/modules/ai/utils/__tests__/parseStream.test.ts +++ b/packages/twenty-front/src/modules/ai/utils/__tests__/parseStream.test.ts @@ -46,7 +46,7 @@ describe('parseStream', () => { type: 'tool-result', toolCallId: 'call-123', toolName: 'send_email', - result: { sucess: true, result: 'Email sent', message: 'Success' }, + result: { success: true, result: 'Email sent', message: 'Success' }, message: 'Email sent successfully', }), ].join('\n'); @@ -67,7 +67,7 @@ describe('parseStream', () => { type: 'tool-result', toolCallId: 'call-123', toolName: 'send_email', - result: { sucess: true, result: 'Email sent', message: 'Success' }, + result: { success: true, result: 'Email sent', message: 'Success' }, message: 'Email sent successfully', }, ], @@ -80,7 +80,7 @@ describe('parseStream', () => { toolCallId: 'call-456', toolName: 'http_request', result: { - sucess: true, + success: true, result: 'Response received', message: 'Success', }, @@ -98,7 +98,7 @@ describe('parseStream', () => { toolCallId: 'call-456', toolName: 'http_request', result: { - sucess: true, + success: true, result: 'Response received', message: 'Success', }, @@ -112,15 +112,16 @@ describe('parseStream', () => { describe('reasoning events', () => { it('should parse reasoning events correctly', () => { const streamText = [ + JSON.stringify({ type: 'reasoning-start' }), JSON.stringify({ - type: 'reasoning', - textDelta: 'Let me think about this...', + type: 'reasoning-delta', + text: 'Let me think about this...', }), JSON.stringify({ - type: 'reasoning', - textDelta: ' I need to consider the options.', + type: 'reasoning-delta', + text: ' I need to consider the options.', }), - JSON.stringify({ type: 'reasoning-signature' }), + JSON.stringify({ type: 'reasoning-end' }), ].join('\n'); const result = parseStream(streamText); @@ -133,11 +134,14 @@ describe('parseStream', () => { }); }); - it('should handle reasoning without signature as thinking', () => { - const streamText = JSON.stringify({ - type: 'reasoning', - textDelta: 'Still thinking...', - }); + it('should handle reasoning without end as thinking', () => { + const streamText = [ + JSON.stringify({ type: 'reasoning-start' }), + JSON.stringify({ + type: 'reasoning-delta', + text: 'Still thinking...', + }), + ].join('\n'); const result = parseStream(streamText); @@ -151,9 +155,10 @@ describe('parseStream', () => { it('should concatenate multiple reasoning deltas', () => { const streamText = [ - JSON.stringify({ type: 'reasoning', textDelta: 'First part' }), - JSON.stringify({ type: 'reasoning', textDelta: ' second part' }), - JSON.stringify({ type: 'reasoning', textDelta: ' third part' }), + JSON.stringify({ type: 'reasoning-start' }), + JSON.stringify({ type: 'reasoning-delta', text: 'First part' }), + JSON.stringify({ type: 'reasoning-delta', text: ' second part' }), + JSON.stringify({ type: 'reasoning-delta', text: ' third part' }), ].join('\n'); const result = parseStream(streamText); @@ -170,8 +175,8 @@ describe('parseStream', () => { describe('text-delta events', () => { it('should parse text-delta events correctly', () => { const streamText = [ - JSON.stringify({ type: 'text-delta', textDelta: 'Hello, ' }), - JSON.stringify({ type: 'text-delta', textDelta: 'world!' }), + JSON.stringify({ type: 'text-delta', text: 'Hello, ' }), + JSON.stringify({ type: 'text-delta', text: 'world!' }), ].join('\n'); const result = parseStream(streamText); @@ -183,8 +188,8 @@ describe('parseStream', () => { }); }); - it('should handle empty textDelta', () => { - const streamText = JSON.stringify({ type: 'text-delta', textDelta: '' }); + it('should handle empty text', () => { + const streamText = JSON.stringify({ type: 'text-delta', text: '' }); const result = parseStream(streamText); @@ -249,7 +254,7 @@ describe('parseStream', () => { describe('step-finish events', () => { it('should flush current text block on step-finish', () => { const streamText = [ - JSON.stringify({ type: 'text-delta', textDelta: 'Some text' }), + JSON.stringify({ type: 'text-delta', text: 'Some text' }), JSON.stringify({ type: 'step-finish' }), ].join('\n'); @@ -264,7 +269,8 @@ describe('parseStream', () => { it('should mark reasoning as not thinking on step-finish', () => { const streamText = [ - JSON.stringify({ type: 'reasoning', textDelta: 'Thinking...' }), + JSON.stringify({ type: 'reasoning-start' }), + JSON.stringify({ type: 'reasoning-delta', text: 'Thinking...' }), JSON.stringify({ type: 'step-finish' }), ].join('\n'); @@ -282,22 +288,23 @@ describe('parseStream', () => { describe('mixed events', () => { it('should handle mixed event types correctly', () => { const streamText = [ - JSON.stringify({ type: 'text-delta', textDelta: 'Starting...' }), + JSON.stringify({ type: 'text-delta', text: 'Starting...' }), JSON.stringify({ type: 'tool-call', toolCallId: 'call-1', toolName: 'send_email', args: { loadingMessage: 'Sending...', input: {} }, }), - JSON.stringify({ type: 'reasoning', textDelta: 'Let me think...' }), + JSON.stringify({ type: 'reasoning-start' }), + JSON.stringify({ type: 'reasoning-delta', text: 'Let me think...' }), JSON.stringify({ type: 'tool-result', toolCallId: 'call-1', toolName: 'send_email', - result: { sucess: true, message: 'Done' }, + result: { success: true, message: 'Done' }, message: 'Email sent', }), - JSON.stringify({ type: 'text-delta', textDelta: 'Finished!' }), + JSON.stringify({ type: 'text-delta', text: 'Finished!' }), ].join('\n'); const result = parseStream(streamText); @@ -317,7 +324,7 @@ describe('parseStream', () => { type: 'tool-result', toolCallId: 'call-1', toolName: 'send_email', - result: { sucess: true, message: 'Done' }, + result: { success: true, message: 'Done' }, message: 'Email sent', }, ], @@ -348,7 +355,7 @@ describe('parseStream', () => { it('should skip invalid JSON lines', () => { const streamText = [ 'invalid json line', - JSON.stringify({ type: 'text-delta', textDelta: 'Valid content' }), + JSON.stringify({ type: 'text-delta', text: 'Valid content' }), 'another invalid line', ].join('\n'); @@ -374,7 +381,7 @@ describe('parseStream', () => { it('should flush remaining text block at end', () => { const streamText = JSON.stringify({ type: 'text-delta', - textDelta: 'Unflushed content', + text: 'Unflushed content', }); const result = parseStream(streamText); @@ -390,8 +397,9 @@ describe('parseStream', () => { describe('text block transitions', () => { it('should create new text block when switching from reasoning to text', () => { const streamText = [ - JSON.stringify({ type: 'reasoning', textDelta: 'Thinking...' }), - JSON.stringify({ type: 'text-delta', textDelta: 'Speaking...' }), + JSON.stringify({ type: 'reasoning-start' }), + JSON.stringify({ type: 'reasoning-delta', text: 'Thinking...' }), + JSON.stringify({ type: 'text-delta', text: 'Speaking...' }), ].join('\n'); const result = parseStream(streamText); @@ -410,8 +418,9 @@ describe('parseStream', () => { it('should create new reasoning block when switching from text to reasoning', () => { const streamText = [ - JSON.stringify({ type: 'text-delta', textDelta: 'Speaking...' }), - JSON.stringify({ type: 'reasoning', textDelta: 'Thinking...' }), + JSON.stringify({ type: 'text-delta', text: 'Speaking...' }), + JSON.stringify({ type: 'reasoning-start' }), + JSON.stringify({ type: 'reasoning-delta', text: 'Thinking...' }), ].join('\n'); const result = parseStream(streamText); diff --git a/packages/twenty-front/src/modules/ai/utils/extractErrorMessage.ts b/packages/twenty-front/src/modules/ai/utils/extractErrorMessage.ts index 3e2c9de1514..06930743144 100644 --- a/packages/twenty-front/src/modules/ai/utils/extractErrorMessage.ts +++ b/packages/twenty-front/src/modules/ai/utils/extractErrorMessage.ts @@ -1,29 +1,39 @@ +import { t } from '@lingui/core/macro'; import { isDefined } from 'twenty-shared/utils'; -export const extractErrorMessage = (error: unknown): string => { - if (typeof error === 'string') { - return error; - } +const isObjectWithMessage = (error: unknown): error is { message: string } => { + return ( + isDefined(error) && + typeof error === 'object' && + 'message' in error && + typeof error.message === 'string' + ); +}; - if (!isDefined(error) || typeof error !== 'object') { - return 'An unexpected error occurred'; - } - - if ('message' in error && typeof error.message === 'string') { - return error.message; - } - - if ( +const isErrorWithNestedError = ( + error: unknown, +): error is { + error: { message: string }; +} => { + return ( + isDefined(error) && + typeof error === 'object' && 'error' in error && isDefined(error.error) && typeof error.error === 'object' && 'message' in error.error && typeof error.error.message === 'string' - ) { - return error.error.message; - } + ); +}; - if ( +const isDeepNestedError = ( + error: unknown, +): error is { + data: { error: { message: string } }; +} => { + return ( + isDefined(error) && + typeof error === 'object' && 'data' in error && isDefined(error.data) && typeof error.data === 'object' && @@ -32,9 +42,25 @@ export const extractErrorMessage = (error: unknown): string => { typeof error.data.error === 'object' && 'message' in error.data.error && typeof error.data.error.message === 'string' - ) { + ); +}; + +export const extractErrorMessage = (error: unknown): string => { + if (typeof error === 'string') { + return error; + } + + if (isObjectWithMessage(error)) { + return error.message; + } + + if (isErrorWithNestedError(error)) { + return error.error.message; + } + + if (isDeepNestedError(error)) { return error.data.error.message; } - return 'An unexpected error occurred'; + return t`An unexpected error occurred`; }; diff --git a/packages/twenty-front/src/modules/ai/utils/hasStructuredStreamData.ts b/packages/twenty-front/src/modules/ai/utils/hasStructuredStreamData.ts new file mode 100644 index 00000000000..ccd61fe6882 --- /dev/null +++ b/packages/twenty-front/src/modules/ai/utils/hasStructuredStreamData.ts @@ -0,0 +1,14 @@ +export const hasStructuredStreamData = (data: string): boolean => { + if (!data.includes('\n')) { + return false; + } + + return data.split('\n').some((line) => { + try { + JSON.parse(line); + return true; + } catch { + return false; + } + }); +}; diff --git a/packages/twenty-front/src/modules/ai/utils/parseStream.ts b/packages/twenty-front/src/modules/ai/utils/parseStream.ts index 6f66f932708..5b095e99986 100644 --- a/packages/twenty-front/src/modules/ai/utils/parseStream.ts +++ b/packages/twenty-front/src/modules/ai/utils/parseStream.ts @@ -1,19 +1,130 @@ -import { isDefined } from 'twenty-shared/utils'; - import { - type ParsedStep, + parseStreamLine, + splitStreamIntoLines, + type ErrorEvent, + type ReasoningDeltaEvent, + type TextBlock, + type TextDeltaEvent, + type ToolCallEvent, type ToolEvent, type ToolResultEvent, -} from '@/ai/types/streamTypes'; +} from 'twenty-shared/ai'; +import { isDefined } from 'twenty-shared/utils'; -type TextBlock = - | { type: 'reasoning'; content: string; isThinking: boolean } - | { type: 'text'; content: string } - | null; +import type { ParsedStep } from '@/ai/types/ParsedStep'; + +const handleToolCall = ( + event: ToolCallEvent, + output: ParsedStep[], + flushTextBlock: () => void, +) => { + flushTextBlock(); + output.push({ + type: 'tool', + events: [event], + }); +}; + +const handleToolResult = ( + event: ToolResultEvent, + output: ParsedStep[], + flushTextBlock: () => void, +) => { + flushTextBlock(); + + const toolEntry = output.find( + (item): item is { type: 'tool'; events: ToolEvent[] } => + item.type === 'tool' && + item.events.some( + (e) => e.type === 'tool-call' && e.toolCallId === event.toolCallId, + ), + ); + + if (isDefined(toolEntry)) { + toolEntry.events.push(event); + } else { + output.push({ + type: 'tool', + events: [event], + }); + } +}; + +const handleReasoningStart = (flushTextBlock: () => void): TextBlock => { + flushTextBlock(); + return { + type: 'reasoning', + content: '', + isThinking: true, + }; +}; + +const handleReasoningDelta = ( + event: ReasoningDeltaEvent, + currentTextBlock: TextBlock, + flushTextBlock: () => void, +): TextBlock => { + if (!currentTextBlock || currentTextBlock.type !== 'reasoning') { + flushTextBlock(); + return { + type: 'reasoning', + content: event.text || '', + isThinking: true, + }; + } + currentTextBlock.content += event.text || ''; + return currentTextBlock; +}; + +const handleReasoningEnd = (currentTextBlock: TextBlock): TextBlock => { + if (currentTextBlock?.type === 'reasoning') { + return { + ...currentTextBlock, + isThinking: false, + }; + } + return currentTextBlock; +}; + +const handleTextDelta = ( + event: TextDeltaEvent, + currentTextBlock: TextBlock, + flushTextBlock: () => void, +): TextBlock => { + if (!currentTextBlock || currentTextBlock.type !== 'text') { + flushTextBlock(); + return { type: 'text', content: event.text || '' }; + } + currentTextBlock.content += event.text || ''; + return currentTextBlock; +}; + +const handleStepFinish = ( + currentTextBlock: TextBlock, + flushTextBlock: () => void, +): TextBlock => { + if (currentTextBlock?.type === 'reasoning') { + currentTextBlock.isThinking = false; + } + flushTextBlock(); + return null; +}; + +const handleError = ( + event: ErrorEvent, + output: ParsedStep[], + flushTextBlock: () => void, +) => { + flushTextBlock(); + output.push({ + type: 'error', + message: event.message || 'An error occurred', + error: event.error, + }); +}; export const parseStream = (streamText: string): ParsedStep[] => { - const lines = streamText.trim().split('\n'); - + const lines = splitStreamIntoLines(streamText); const output: ParsedStep[] = []; let currentTextBlock: TextBlock = null; @@ -25,100 +136,50 @@ export const parseStream = (streamText: string): ParsedStep[] => { }; for (const line of lines) { - let event; - try { - event = JSON.parse(line); - } catch { + const event = parseStreamLine(line); + if (!event) { continue; } switch (event.type) { case 'tool-call': - flushTextBlock(); - output.push({ - type: 'tool', - events: [ - { - type: 'tool-call', - toolCallId: event.toolCallId, - toolName: event.toolName, - args: event.args, - }, - ] as ToolEvent[], - }); + handleToolCall(event, output, flushTextBlock); break; - case 'tool-result': { - flushTextBlock(); + case 'tool-result': + handleToolResult(event, output, flushTextBlock); + break; - const toolEntry = output.find( - (item): item is { type: 'tool'; events: ToolEvent[] } => - item.type === 'tool' && - item.events.some( - (e) => - e.type === 'tool-call' && e.toolCallId === event.toolCallId, - ), + case 'reasoning-start': + currentTextBlock = handleReasoningStart(flushTextBlock); + break; + + case 'reasoning-delta': + currentTextBlock = handleReasoningDelta( + event, + currentTextBlock, + flushTextBlock, ); - - const resultEvent: ToolResultEvent = { - type: 'tool-result', - toolCallId: event.toolCallId, - toolName: event.toolName, - result: event.result, - message: event.message, - }; - - if (isDefined(toolEntry)) { - toolEntry.events.push(resultEvent); - } else { - output.push({ - type: 'tool', - events: [resultEvent], - }); - } break; - } - case 'reasoning': - if (!currentTextBlock || currentTextBlock.type !== 'reasoning') { - flushTextBlock(); - currentTextBlock = { - type: 'reasoning', - content: '', - isThinking: true, - }; - } - currentTextBlock.content += event.textDelta || ''; + case 'reasoning-end': + currentTextBlock = handleReasoningEnd(currentTextBlock); break; case 'text-delta': - if (!currentTextBlock || currentTextBlock.type !== 'text') { - flushTextBlock(); - currentTextBlock = { type: 'text', content: '' }; - } - currentTextBlock.content += event.textDelta || ''; - break; - - case 'reasoning-signature': - if (currentTextBlock?.type === 'reasoning') { - currentTextBlock.isThinking = false; - } + currentTextBlock = handleTextDelta( + event, + currentTextBlock, + flushTextBlock, + ); break; case 'step-finish': - if (currentTextBlock?.type === 'reasoning') { - currentTextBlock.isThinking = false; - } - flushTextBlock(); + currentTextBlock = handleStepFinish(currentTextBlock, flushTextBlock); break; case 'error': - flushTextBlock(); - output.push({ - type: 'error', - message: event.message || 'An error occurred', - error: event.error, - }); + handleError(event, output, flushTextBlock); break; } } diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/utils/orderWorkflowRunState.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/utils/orderWorkflowRunState.ts index 71f22435a4c..8d74e6cef8d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/utils/orderWorkflowRunState.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/utils/orderWorkflowRunState.ts @@ -1,6 +1,6 @@ import { type WorkflowRunState } from '@/workflow/types/Workflow'; +import { workflowRunStateSchema } from '@/workflow/validation-schemas/workflowSchema'; import { isDefined } from 'twenty-shared/utils'; -import { workflowRunStateSchema } from 'twenty-shared/workflow'; import { type JsonValue } from 'type-fest'; export const orderWorkflowRunState = (value: JsonValue) => { diff --git a/packages/twenty-front/src/modules/workflow/hooks/useWorkflowRun.ts b/packages/twenty-front/src/modules/workflow/hooks/useWorkflowRun.ts index cb0424c2d98..abf82937606 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/useWorkflowRun.ts +++ b/packages/twenty-front/src/modules/workflow/hooks/useWorkflowRun.ts @@ -1,9 +1,9 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; import { type WorkflowRun } from '@/workflow/types/Workflow'; +import { workflowRunSchema } from '@/workflow/validation-schemas/workflowSchema'; import { useMemo } from 'react'; import { isDefined } from 'twenty-shared/utils'; -import { workflowRunSchema } from 'twenty-shared/workflow'; export const useWorkflowRun = ({ workflowRunId, diff --git a/packages/twenty-front/src/modules/workflow/types/Workflow.ts b/packages/twenty-front/src/modules/workflow/types/Workflow.ts index bc94c37b1f4..4c9eb36b9bb 100644 --- a/packages/twenty-front/src/modules/workflow/types/Workflow.ts +++ b/packages/twenty-front/src/modules/workflow/types/Workflow.ts @@ -20,7 +20,7 @@ import { type workflowTriggerSchema, type workflowUpdateRecordActionSchema, type workflowWebhookTriggerSchema, -} from 'twenty-shared/workflow'; +} from '@/workflow/validation-schemas/workflowSchema'; import { type z } from 'zod'; export type WorkflowCodeAction = z.infer; diff --git a/packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts b/packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts new file mode 100644 index 00000000000..69e2a81e2be --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts @@ -0,0 +1,390 @@ +import { FieldMetadataType } from 'twenty-shared/types'; +import { StepStatus } from 'twenty-shared/workflow'; +import { z } from 'zod'; + +export const objectRecordSchema = z.record(z.any()); + +export const baseWorkflowActionSettingsSchema = z.object({ + input: z.object({}).passthrough(), + outputSchema: z.object({}).passthrough(), + errorHandlingOptions: z.object({ + retryOnFailure: z.object({ + value: z.boolean(), + }), + continueOnFailure: z.object({ + value: z.boolean(), + }), + }), +}); + +export const baseWorkflowActionSchema = z.object({ + id: z.string(), + name: z.string(), + valid: z.boolean(), + nextStepIds: z.array(z.string()).optional().nullable(), + position: z.object({ x: z.number(), y: z.number() }).optional().nullable(), +}); + +export const baseTriggerSchema = z.object({ + name: z.string().optional(), + type: z.string(), + position: z.object({ x: z.number(), y: z.number() }).optional().nullable(), + nextStepIds: z.array(z.string()).optional().nullable(), +}); + +export const workflowCodeActionSettingsSchema = + baseWorkflowActionSettingsSchema.extend({ + input: z.object({ + serverlessFunctionId: z.string(), + serverlessFunctionVersion: z.string(), + serverlessFunctionInput: z.record(z.any()), + }), + }); + +export const workflowSendEmailActionSettingsSchema = + baseWorkflowActionSettingsSchema.extend({ + input: z.object({ + connectedAccountId: z.string(), + email: z.string(), + subject: z.string().optional(), + body: z.string().optional(), + }), + }); + +export const workflowCreateRecordActionSettingsSchema = + baseWorkflowActionSettingsSchema.extend({ + input: z.object({ + objectName: z.string(), + objectRecord: objectRecordSchema, + }), + }); + +export const workflowUpdateRecordActionSettingsSchema = + baseWorkflowActionSettingsSchema.extend({ + input: z.object({ + objectName: z.string(), + objectRecord: objectRecordSchema, + objectRecordId: z.string(), + fieldsToUpdate: z.array(z.string()), + }), + }); + +export const workflowDeleteRecordActionSettingsSchema = + baseWorkflowActionSettingsSchema.extend({ + input: z.object({ + objectName: z.string(), + objectRecordId: z.string(), + }), + }); + +export const workflowFindRecordsActionSettingsSchema = + baseWorkflowActionSettingsSchema.extend({ + input: z.object({ + objectName: z.string(), + limit: z.number().optional(), + filter: z + .object({ + recordFilterGroups: z.array(z.object({})).optional(), + recordFilters: z.array(z.object({})).optional(), + gqlOperationFilter: z.object({}).optional().nullable(), + }) + .optional(), + }), + }); + +export const workflowFormActionSettingsSchema = + baseWorkflowActionSettingsSchema.extend({ + input: z.array( + z.object({ + id: z.string(), + name: z.string(), + label: z.string(), + type: z.union([ + z.literal(FieldMetadataType.TEXT), + z.literal(FieldMetadataType.NUMBER), + z.literal(FieldMetadataType.DATE), + z.literal(FieldMetadataType.SELECT), + z.literal('RECORD'), + ]), + placeholder: z.string().optional(), + settings: z.record(z.any()).optional(), + value: z.any().optional(), + }), + ), + }); + +export const workflowHttpRequestActionSettingsSchema = + baseWorkflowActionSettingsSchema.extend({ + input: z.object({ + url: z.string(), + method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']), + headers: z.record(z.string()).optional(), + body: z + .record( + z.union([ + z.string(), + z.number(), + z.boolean(), + z.null(), + z.array(z.union([z.string(), z.number(), z.boolean(), z.null()])), + ]), + ) + .or(z.string()) + .optional(), + }), + }); + +export const workflowAiAgentActionSettingsSchema = + baseWorkflowActionSettingsSchema.extend({ + input: z.object({ + agentId: z.string().optional(), + prompt: z.string().optional(), + }), + }); + +export const workflowFilterActionSettingsSchema = + baseWorkflowActionSettingsSchema.extend({ + input: z.object({ + stepFilterGroups: z.array(z.any()), + stepFilters: z.array(z.any()), + }), + }); + +export const workflowIteratorActionSettingsSchema = + baseWorkflowActionSettingsSchema.extend({ + input: z.object({ + items: z + .union([ + z.array( + z.union([ + z.string(), + z.number(), + z.boolean(), + z.null(), + z.record(z.any()), + z.any(), + ]), + ), + z.string(), + ]) + .optional(), + initialLoopStepIds: z.array(z.string()).optional(), + }), + }); + +export const workflowEmptyActionSettingsSchema = + baseWorkflowActionSettingsSchema.extend({ + input: z.object({}), + }); + +export const workflowCodeActionSchema = baseWorkflowActionSchema.extend({ + type: z.literal('CODE'), + settings: workflowCodeActionSettingsSchema, +}); + +export const workflowSendEmailActionSchema = baseWorkflowActionSchema.extend({ + type: z.literal('SEND_EMAIL'), + settings: workflowSendEmailActionSettingsSchema, +}); + +export const workflowCreateRecordActionSchema = baseWorkflowActionSchema.extend( + { + type: z.literal('CREATE_RECORD'), + settings: workflowCreateRecordActionSettingsSchema, + }, +); + +export const workflowUpdateRecordActionSchema = baseWorkflowActionSchema.extend( + { + type: z.literal('UPDATE_RECORD'), + settings: workflowUpdateRecordActionSettingsSchema, + }, +); + +export const workflowDeleteRecordActionSchema = baseWorkflowActionSchema.extend( + { + type: z.literal('DELETE_RECORD'), + settings: workflowDeleteRecordActionSettingsSchema, + }, +); + +export const workflowFindRecordsActionSchema = baseWorkflowActionSchema.extend({ + type: z.literal('FIND_RECORDS'), + settings: workflowFindRecordsActionSettingsSchema, +}); + +export const workflowFormActionSchema = baseWorkflowActionSchema.extend({ + type: z.literal('FORM'), + settings: workflowFormActionSettingsSchema, +}); + +export const workflowHttpRequestActionSchema = baseWorkflowActionSchema.extend({ + type: z.literal('HTTP_REQUEST'), + settings: workflowHttpRequestActionSettingsSchema, +}); + +export const workflowAiAgentActionSchema = baseWorkflowActionSchema.extend({ + type: z.literal('AI_AGENT'), + settings: workflowAiAgentActionSettingsSchema, +}); + +export const workflowFilterActionSchema = baseWorkflowActionSchema.extend({ + type: z.literal('FILTER'), + settings: workflowFilterActionSettingsSchema, +}); + +export const workflowIteratorActionSchema = baseWorkflowActionSchema.extend({ + type: z.literal('ITERATOR'), + settings: workflowIteratorActionSettingsSchema, +}); + +export const workflowEmptyActionSchema = baseWorkflowActionSchema.extend({ + type: z.literal('EMPTY'), + settings: workflowEmptyActionSettingsSchema, +}); + +export const workflowActionSchema = z.discriminatedUnion('type', [ + workflowCodeActionSchema, + workflowSendEmailActionSchema, + workflowCreateRecordActionSchema, + workflowUpdateRecordActionSchema, + workflowDeleteRecordActionSchema, + workflowFindRecordsActionSchema, + workflowFormActionSchema, + workflowHttpRequestActionSchema, + workflowAiAgentActionSchema, + workflowFilterActionSchema, + workflowIteratorActionSchema, + workflowEmptyActionSchema, +]); + +export const workflowDatabaseEventTriggerSchema = baseTriggerSchema.extend({ + type: z.literal('DATABASE_EVENT'), + settings: z.object({ + eventName: z.string(), + input: z.object({}).passthrough().optional(), + outputSchema: z.object({}).passthrough(), + objectType: z.string().optional(), + fields: z.array(z.string()).optional().nullable(), + }), +}); + +export const workflowManualTriggerSchema = baseTriggerSchema + .extend({ + type: z.literal('MANUAL'), + settings: z.object({ + objectType: z.string().optional(), + outputSchema: z + .object({}) + .passthrough() + .describe( + 'Schema defining the output data structure. When a record is selected, it is accessible via {{trigger.record.fieldName}}. When no record is selected, no data is available.', + ), + icon: z.string().optional(), + isPinned: z.boolean().optional(), + }), + }) + .describe( + 'Manual trigger that can be launched by the user. If a record is selected when launched, it is accessible via {{trigger.record.fieldName}}. If no record is selected, no data context is available.', + ); + +export const workflowCronTriggerSchema = baseTriggerSchema.extend({ + type: z.literal('CRON'), + settings: z.discriminatedUnion('type', [ + z.object({ + type: z.literal('DAYS'), + schedule: z.object({ + day: z.number().min(1), + hour: z.number().min(0).max(23), + minute: z.number().min(0).max(59), + }), + outputSchema: z.object({}).passthrough(), + }), + z.object({ + type: z.literal('HOURS'), + schedule: z.object({ + hour: z.number().min(1), + minute: z.number().min(0).max(59), + }), + outputSchema: z.object({}).passthrough(), + }), + z.object({ + type: z.literal('MINUTES'), + schedule: z.object({ minute: z.number().min(1) }), + outputSchema: z.object({}).passthrough(), + }), + z.object({ + type: z.literal('CUSTOM'), + pattern: z.string(), + outputSchema: z.object({}).passthrough(), + }), + ]), +}); + +export const workflowWebhookTriggerSchema = baseTriggerSchema.extend({ + type: z.literal('WEBHOOK'), + settings: z.discriminatedUnion('httpMethod', [ + z.object({ + outputSchema: z.object({}).passthrough(), + httpMethod: z.literal('GET'), + authentication: z.literal('API_KEY').nullable(), + }), + z.object({ + outputSchema: z.object({}).passthrough(), + httpMethod: z.literal('POST'), + expectedBody: z.object({}).passthrough(), + authentication: z.literal('API_KEY').nullable(), + }), + ]), +}); + +export const workflowTriggerSchema = z.discriminatedUnion('type', [ + workflowDatabaseEventTriggerSchema, + workflowManualTriggerSchema, + workflowCronTriggerSchema, + workflowWebhookTriggerSchema, +]); + +export const workflowRunStepStatusSchema = z.nativeEnum(StepStatus); + +export const workflowRunStateStepInfoSchema = z.object({ + result: z.any().optional(), + error: z.any().optional(), + status: workflowRunStepStatusSchema, +}); + +export const workflowRunStateStepInfosSchema = z.record( + workflowRunStateStepInfoSchema, +); + +export const workflowRunStateSchema = z.object({ + flow: z.object({ + trigger: workflowTriggerSchema, + steps: z.array(workflowActionSchema), + }), + stepInfos: workflowRunStateStepInfosSchema, + workflowRunError: z.any().optional(), +}); + +export const workflowRunStatusSchema = z.enum([ + 'NOT_STARTED', + 'RUNNING', + 'COMPLETED', + 'FAILED', + 'ENQUEUED', +]); + +export const workflowRunSchema = z + .object({ + __typename: z.literal('WorkflowRun'), + id: z.string(), + workflowVersionId: z.string(), + workflowId: z.string(), + state: workflowRunStateSchema.nullable(), + status: workflowRunStatusSchema, + createdAt: z.string(), + deletedAt: z.string().nullable(), + endedAt: z.string().nullable(), + name: z.string(), + }) + .passthrough(); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/hooks/__tests__/useWorkflowActionHeader.test.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/hooks/__tests__/useWorkflowActionHeader.test.tsx index 93293eed01e..3d83c4382b2 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/hooks/__tests__/useWorkflowActionHeader.test.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/hooks/__tests__/useWorkflowActionHeader.test.tsx @@ -3,13 +3,13 @@ import { type WorkflowHttpRequestAction, type WorkflowSendEmailAction, } from '@/workflow/types/Workflow'; -import { renderHook } from '@testing-library/react'; -import { FieldMetadataType } from 'twenty-shared/types'; import { workflowFormActionSettingsSchema, workflowHttpRequestActionSettingsSchema, workflowSendEmailActionSettingsSchema, -} from 'twenty-shared/workflow'; +} from '@/workflow/validation-schemas/workflowSchema'; +import { renderHook } from '@testing-library/react'; +import { FieldMetadataType } from 'twenty-shared/types'; import { useWorkflowActionHeader } from '../useWorkflowActionHeader'; jest.mock('../useActionIconColorOrThrow', () => ({ diff --git a/packages/twenty-server/package.json b/packages/twenty-server/package.json index 11848be1005..dc8ba1e1688 100644 --- a/packages/twenty-server/package.json +++ b/packages/twenty-server/package.json @@ -15,9 +15,10 @@ "typeorm": "../../node_modules/typeorm/.bin/typeorm" }, "dependencies": { - "@ai-sdk/anthropic": "^1.2.12", - "@ai-sdk/openai": "^1.3.22", - "@ai-sdk/xai": "1.2.15", + "@ai-sdk/anthropic": "^2.0.17", + "@ai-sdk/openai": "^2.0.30", + "@ai-sdk/provider-utils": "^3.0.9", + "@ai-sdk/xai": "^2.0.19", "@aws-sdk/client-lambda": "3.825.0", "@aws-sdk/client-s3": "3.825.0", "@aws-sdk/client-sts": "3.825.0", @@ -77,7 +78,7 @@ "@sentry/profiling-node": "9.26.0", "@sniptt/guards": "0.2.0", "addressparser": "1.0.1", - "ai": "^4.3.16", + "ai": "^5.0.44", "apollo-server-core": "3.13.0", "archiver": "7.0.1", "axios": "1.10.0", @@ -186,7 +187,7 @@ "unzipper": "^0.12.3", "uuid": "9.0.1", "vite-tsconfig-paths": "4.3.2", - "zod": "3.23.8", + "zod": "^4.1.11", "zod-to-json-schema": "^3.23.1" }, "devDependencies": { diff --git a/packages/twenty-server/src/engine/core-modules/ai/controllers/ai.controller.spec.ts b/packages/twenty-server/src/engine/core-modules/ai/controllers/ai.controller.spec.ts index e049083e40a..b26afd8e571 100644 --- a/packages/twenty-server/src/engine/core-modules/ai/controllers/ai.controller.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/ai/controllers/ai.controller.spec.ts @@ -1,8 +1,12 @@ import { Test, type TestingModule } from '@nestjs/testing'; +import { openai } from '@ai-sdk/openai'; + +import { ModelProvider } from 'src/engine/core-modules/ai/constants/ai-models.const'; +import { AIBillingService } from 'src/engine/core-modules/ai/services/ai-billing.service'; +import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service'; import { AiService } from 'src/engine/core-modules/ai/services/ai.service'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; -import { AIBillingService } from 'src/engine/core-modules/ai/services/ai-billing.service'; import { AiController } from './ai.controller'; @@ -11,6 +15,7 @@ describe('AiController', () => { let aiService: jest.Mocked; let featureFlagService: jest.Mocked; let aiBillingService: jest.Mocked; + let aiModelRegistryService: jest.Mocked; beforeEach(async () => { const mockAiService = { @@ -26,6 +31,14 @@ describe('AiController', () => { calculateAndBillUsage: jest.fn(), }; + const mockAiModelRegistryService = { + getDefaultModel: jest.fn().mockReturnValue({ + modelId: 'gpt-4o', + provider: ModelProvider.OPENAI, + model: openai('gpt-4o'), + }), + }; + const module: TestingModule = await Test.createTestingModule({ controllers: [AiController], providers: [ @@ -41,6 +54,10 @@ describe('AiController', () => { provide: AIBillingService, useValue: mockAIBillingService, }, + { + provide: AiModelRegistryService, + useValue: mockAiModelRegistryService, + }, ], }).compile(); @@ -48,6 +65,7 @@ describe('AiController', () => { aiService = module.get(AiService); featureFlagService = module.get(FeatureFlagService); aiBillingService = module.get(AIBillingService); + aiModelRegistryService = module.get(AiModelRegistryService); }); it('should be defined', () => { @@ -61,7 +79,7 @@ describe('AiController', () => { const mockRequest = { messages: [{ role: 'user' as const, content: 'Hello' }], temperature: 0.7, - maxTokens: 100, + maxOutputTokens: 100, }; const mockRes = { @@ -70,19 +88,23 @@ describe('AiController', () => { end: jest.fn(), } as any; - const mockModel = { modelId: 'gpt-4o' } as any; + const mockModel = openai('gpt-4o'); - aiService.getModel.mockReturnValue(mockModel); + aiModelRegistryService.getDefaultModel.mockReturnValue({ + modelId: 'gpt-4o', + provider: ModelProvider.OPENAI, + model: mockModel, + }); const mockUsage = { - promptTokens: 10, - completionTokens: 20, + inputTokens: 10, + outputTokens: 20, totalTokens: 30, }; const mockStreamTextResult = { usage: Promise.resolve(mockUsage), - pipeDataStreamToResponse: jest.fn(), + pipeUIMessageStreamToResponse: jest.fn(), }; aiService.streamText.mockReturnValue(mockStreamTextResult as any); @@ -96,12 +118,12 @@ describe('AiController', () => { messages: mockRequest.messages, options: { temperature: 0.7, - maxTokens: 100, + maxOutputTokens: 100, model: mockModel, }, }); expect( - mockStreamTextResult.pipeDataStreamToResponse, + mockStreamTextResult.pipeUIMessageStreamToResponse, ).toHaveBeenCalledWith(mockRes); expect(aiBillingService.calculateAndBillUsage).toHaveBeenCalledWith( mockModel.modelId, @@ -131,7 +153,11 @@ describe('AiController', () => { const mockRes = {} as any; - aiService.getModel.mockReturnValue({ modelId: 'gpt-4o' } as any); + aiModelRegistryService.getDefaultModel.mockReturnValue({ + modelId: 'gpt-4o', + provider: ModelProvider.OPENAI, + model: openai('gpt-4o'), + }); aiService.streamText.mockImplementation(() => { throw new Error('Service error'); }); diff --git a/packages/twenty-server/src/engine/core-modules/ai/controllers/ai.controller.ts b/packages/twenty-server/src/engine/core-modules/ai/controllers/ai.controller.ts index 80c001303c0..a0efc1f629f 100644 --- a/packages/twenty-server/src/engine/core-modules/ai/controllers/ai.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/ai/controllers/ai.controller.ts @@ -8,21 +8,22 @@ import { UseGuards, } from '@nestjs/common'; -import { type CoreMessage } from 'ai'; +import { type ModelMessage } from 'ai'; import { Response } from 'express'; +import { AIBillingService } from 'src/engine/core-modules/ai/services/ai-billing.service'; +import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service'; import { AiService } from 'src/engine/core-modules/ai/services/ai.service'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -import { AIBillingService } from 'src/engine/core-modules/ai/services/ai-billing.service'; export interface ChatRequest { - messages: CoreMessage[]; + messages: ModelMessage[]; temperature?: number; - maxTokens?: number; + maxOutputTokens?: number; } @Controller('chat') @@ -32,6 +33,7 @@ export class AiController { private readonly aiService: AiService, private readonly featureFlagService: FeatureFlagService, private readonly aiBillingService: AIBillingService, + private readonly aiModelRegistryService: AiModelRegistryService, ) {} @Post() @@ -52,7 +54,7 @@ export class AiController { ); } - const { messages, temperature, maxTokens } = request; + const { messages, temperature, maxOutputTokens } = request; if (!messages || messages.length === 0) { throw new HttpException( @@ -62,27 +64,26 @@ export class AiController { } try { - // TODO: Add support for custom models - const model = this.aiService.getModel(undefined); + const registeredModel = this.aiModelRegistryService.getDefaultModel(); const result = this.aiService.streamText({ messages, options: { temperature, - maxTokens, - model, + maxOutputTokens, + model: registeredModel.model, }, }); result.usage.then((usage) => { this.aiBillingService.calculateAndBillUsage( - model.modelId, + registeredModel.modelId, usage, workspace.id, ); }); - result.pipeDataStreamToResponse(res); + result.pipeUIMessageStreamToResponse(res); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; diff --git a/packages/twenty-server/src/engine/core-modules/ai/services/__tests__/ai-billing.service.spec.ts b/packages/twenty-server/src/engine/core-modules/ai/services/__tests__/ai-billing.service.spec.ts index 969c3141f8c..00fdcbd235a 100644 --- a/packages/twenty-server/src/engine/core-modules/ai/services/__tests__/ai-billing.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/ai/services/__tests__/ai-billing.service.spec.ts @@ -11,8 +11,8 @@ describe('AIBillingService', () => { let mockWorkspaceEventEmitter: jest.Mocked; const mockTokenUsage = { - promptTokens: 1000, - completionTokens: 500, + inputTokens: 1000, + outputTokens: 500, totalTokens: 1500, }; @@ -63,8 +63,8 @@ describe('AIBillingService', () => { it('should calculate cost correctly with different token usage', async () => { const differentTokenUsage = { - promptTokens: 2000, - completionTokens: 1000, + inputTokens: 2000, + outputTokens: 1000, totalTokens: 3000, }; diff --git a/packages/twenty-server/src/engine/core-modules/ai/services/__tests__/mcp.service.spec.ts b/packages/twenty-server/src/engine/core-modules/ai/services/__tests__/mcp.service.spec.ts index 1ce4590038f..be552b65a8f 100644 --- a/packages/twenty-server/src/engine/core-modules/ai/services/__tests__/mcp.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/ai/services/__tests__/mcp.service.spec.ts @@ -1,17 +1,19 @@ -import { Test, type TestingModule } from '@nestjs/testing'; import { HttpException, HttpStatus } from '@nestjs/common'; +import { Test, type TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; +import { jsonSchema } from 'ai'; + +import { MCP_SERVER_METADATA } from 'src/engine/core-modules/ai/constants/mcp.const'; +import { type JsonRpc } from 'src/engine/core-modules/ai/dtos/json-rpc'; +import { McpService } from 'src/engine/core-modules/ai/services/mcp.service'; +import { ToolService } from 'src/engine/core-modules/ai/services/tool.service'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; -import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; -import { ToolService } from 'src/engine/core-modules/ai/services/tool.service'; import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { type JsonRpc } from 'src/engine/core-modules/ai/dtos/json-rpc'; -import { MCP_SERVER_METADATA } from 'src/engine/core-modules/ai/constants/mcp.const'; import { ADMIN_ROLE_LABEL } from 'src/engine/metadata-modules/permissions/constants/admin-role-label.constants'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; -import { McpService } from 'src/engine/core-modules/ai/services/mcp.service'; +import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; describe('McpService', () => { let service: McpService; @@ -197,7 +199,7 @@ describe('McpService', () => { const mockTool = { description: 'Test tool', - parameters: { jsonSchema: { type: 'object', properties: {} } }, + inputSchema: jsonSchema({ type: 'object', properties: {} }), execute: jest.fn().mockResolvedValue({ result: 'success' }), }; @@ -245,7 +247,7 @@ describe('McpService', () => { const mockTool = { description: 'Test tool', - parameters: { jsonSchema: { type: 'object', properties: {} } }, + inputSchema: jsonSchema({ type: 'object', properties: {} }), execute: jest.fn().mockResolvedValue({ result: 'success' }), }; @@ -299,7 +301,7 @@ describe('McpService', () => { const mockToolsMap = { testTool: { description: 'Test tool', - parameters: { jsonSchema: { type: 'object', properties: {} } }, + inputSchema: jsonSchema({ type: 'object', properties: {} }), }, }; @@ -328,7 +330,7 @@ describe('McpService', () => { { name: 'testTool', description: 'Test tool', - inputSchema: { type: 'object', properties: {} }, + inputSchema: jsonSchema({ type: 'object', properties: {} }), }, ], }), diff --git a/packages/twenty-server/src/engine/core-modules/ai/services/__tests__/tool-adapter.service.spec.ts b/packages/twenty-server/src/engine/core-modules/ai/services/__tests__/tool-adapter.service.spec.ts index a2ea3d20505..51bc7bb26d2 100644 --- a/packages/twenty-server/src/engine/core-modules/ai/services/__tests__/tool-adapter.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/ai/services/__tests__/tool-adapter.service.spec.ts @@ -1,5 +1,7 @@ import { Test } from '@nestjs/testing'; +import { jsonSchema } from 'ai'; + import { ToolAdapterService } from 'src/engine/core-modules/ai/services/tool-adapter.service'; import { ToolType } from 'src/engine/core-modules/tool/enums/tool-type.enum'; import { ToolRegistryService } from 'src/engine/core-modules/tool/services/tool-registry.service'; @@ -33,7 +35,7 @@ describe('ToolAdapterService', () => { })); const unflaggedTool: Tool = { description: 'HTTP Request tool', - parameters: { type: 'object', properties: {} }, + inputSchema: jsonSchema({ type: 'object', properties: {} }), execute: unflaggedToolExecute, }; @@ -44,7 +46,7 @@ describe('ToolAdapterService', () => { })); const flaggedTool: Tool = { description: 'Send Email tool', - parameters: { type: 'object', properties: {} }, + inputSchema: jsonSchema({ type: 'object', properties: {} }), execute: flaggedToolExecute, flag: PermissionFlagType.SEND_EMAIL_TOOL, }; diff --git a/packages/twenty-server/src/engine/core-modules/ai/services/ai-billing.service.ts b/packages/twenty-server/src/engine/core-modules/ai/services/ai-billing.service.ts index c569e0166d2..b377748ba9b 100644 --- a/packages/twenty-server/src/engine/core-modules/ai/services/ai-billing.service.ts +++ b/packages/twenty-server/src/engine/core-modules/ai/services/ai-billing.service.ts @@ -1,5 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; +import { LanguageModelUsage } from 'ai'; + import { type ModelId } from 'src/engine/core-modules/ai/constants/ai-models.const'; import { DOLLAR_TO_CREDIT_MULTIPLIER } from 'src/engine/core-modules/ai/constants/dollar-to-credit-multiplier'; import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service'; @@ -8,12 +10,6 @@ import { BillingMeterEventName } from 'src/engine/core-modules/billing/enums/bil import { type BillingUsageEvent } from 'src/engine/core-modules/billing/types/billing-usage-event.type'; import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; -export interface TokenUsage { - promptTokens: number; - completionTokens: number; - totalTokens: number; -} - @Injectable() export class AIBillingService { private readonly logger = new Logger(AIBillingService.name); @@ -23,7 +19,10 @@ export class AIBillingService { private readonly aiModelRegistryService: AiModelRegistryService, ) {} - async calculateCost(modelId: ModelId, usage: TokenUsage): Promise { + async calculateCost( + modelId: ModelId, + usage: LanguageModelUsage, + ): Promise { const model = this.aiModelRegistryService.getEffectiveModelConfig(modelId); if (!model) { @@ -31,9 +30,9 @@ export class AIBillingService { } const inputCost = - (usage.promptTokens / 1000) * model.inputCostPer1kTokensInCents; + ((usage.inputTokens ?? 0) / 1000) * model.inputCostPer1kTokensInCents; const outputCost = - (usage.completionTokens / 1000) * model.outputCostPer1kTokensInCents; + ((usage.outputTokens ?? 0) / 1000) * model.outputCostPer1kTokensInCents; const totalCost = inputCost + outputCost; @@ -46,7 +45,7 @@ export class AIBillingService { async calculateAndBillUsage( modelId: ModelId, - usage: TokenUsage, + usage: LanguageModelUsage, workspaceId: string, ): Promise { const costInCents = await this.calculateCost(modelId, usage); diff --git a/packages/twenty-server/src/engine/core-modules/ai/services/ai-model-registry.service.ts b/packages/twenty-server/src/engine/core-modules/ai/services/ai-model-registry.service.ts index c775371f3ac..9ecd75fe509 100644 --- a/packages/twenty-server/src/engine/core-modules/ai/services/ai-model-registry.service.ts +++ b/packages/twenty-server/src/engine/core-modules/ai/services/ai-model-registry.service.ts @@ -138,7 +138,7 @@ export class AiModelRegistryService { return Array.from(this.modelRegistry.values()); } - getDefaultModel(): RegisteredAIModel | undefined { + getDefaultModel(): RegisteredAIModel { const defaultModelId = this.twentyConfigService.get('DEFAULT_MODEL_ID'); let model = this.getModel(defaultModelId); diff --git a/packages/twenty-server/src/engine/core-modules/ai/services/ai.service.ts b/packages/twenty-server/src/engine/core-modules/ai/services/ai.service.ts index d6508aecd8e..52d6c070024 100644 --- a/packages/twenty-server/src/engine/core-modules/ai/services/ai.service.ts +++ b/packages/twenty-server/src/engine/core-modules/ai/services/ai.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { type CoreMessage, streamText, LanguageModelV1 } from 'ai'; +import { LanguageModel, type ModelMessage, streamText } from 'ai'; import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service'; @@ -28,18 +28,18 @@ export class AiService { messages, options, }: { - messages: CoreMessage[]; + messages: ModelMessage[]; options: { temperature?: number; - maxTokens?: number; - model: LanguageModelV1; + maxOutputTokens?: number; + model: LanguageModel; }; }) { return streamText({ model: options.model, messages, temperature: options?.temperature, - maxTokens: options?.maxTokens, + maxOutputTokens: options?.maxOutputTokens, }); } } diff --git a/packages/twenty-server/src/engine/core-modules/ai/services/mcp.service.ts b/packages/twenty-server/src/engine/core-modules/ai/services/mcp.service.ts index 264244afbbc..3b3564296e9 100644 --- a/packages/twenty-server/src/engine/core-modules/ai/services/mcp.service.ts +++ b/packages/twenty-server/src/engine/core-modules/ai/services/mcp.service.ts @@ -204,11 +204,11 @@ export class McpService { private handleToolsListing(id: string | number, toolSet: ToolSet) { const toolsArray = Object.entries(toolSet) - .filter(([, def]) => !!def.parameters.jsonSchema) + .filter(([, def]) => !!def.inputSchema) .map(([name, def]) => ({ name, description: def.description, - inputSchema: def.parameters.jsonSchema, + inputSchema: def.inputSchema, })); return wrapJsonRpcResponse(id, { diff --git a/packages/twenty-server/src/engine/core-modules/ai/services/tool-adapter.service.ts b/packages/twenty-server/src/engine/core-modules/ai/services/tool-adapter.service.ts index a99ac215b34..26a2545d235 100644 --- a/packages/twenty-server/src/engine/core-modules/ai/services/tool-adapter.service.ts +++ b/packages/twenty-server/src/engine/core-modules/ai/services/tool-adapter.service.ts @@ -42,7 +42,7 @@ export class ToolAdapterService { private createToolSet(tool: Tool) { return { description: tool.description, - parameters: tool.parameters, + inputSchema: tool.inputSchema, execute: async (parameters: { input: ToolInput }) => tool.execute(parameters.input), }; diff --git a/packages/twenty-server/src/engine/core-modules/ai/services/tool.service.ts b/packages/twenty-server/src/engine/core-modules/ai/services/tool.service.ts index bfccb6985ca..150b77a40c9 100644 --- a/packages/twenty-server/src/engine/core-modules/ai/services/tool.service.ts +++ b/packages/twenty-server/src/engine/core-modules/ai/services/tool.service.ts @@ -61,7 +61,7 @@ export class ToolService { if (objectPermission.canUpdate) { tools[`create_${objectMetadata.nameSingular}`] = { description: `Create a new ${objectMetadata.labelSingular} record. Provide all required fields and any optional fields you want to set. The system will automatically handle timestamps and IDs. Returns the created record with all its data.`, - parameters: getRecordInputSchema(objectMetadata), + inputSchema: getRecordInputSchema(objectMetadata), execute: async (parameters) => { return this.createRecord( objectMetadata.nameSingular, @@ -74,7 +74,7 @@ export class ToolService { tools[`update_${objectMetadata.nameSingular}`] = { description: `Update an existing ${objectMetadata.labelSingular} record. Provide the record ID and only the fields you want to change. Unspecified fields will remain unchanged. Returns the updated record with all current data.`, - parameters: getRecordInputSchema(objectMetadata), + inputSchema: getRecordInputSchema(objectMetadata), execute: async (parameters) => { return this.updateRecord( objectMetadata.nameSingular, @@ -89,7 +89,7 @@ export class ToolService { if (objectPermission.canRead) { tools[`find_${objectMetadata.nameSingular}`] = { description: `Search for ${objectMetadata.labelSingular} records using flexible filtering criteria. Supports exact matches, pattern matching, ranges, and null checks. Use limit/offset for pagination. Returns an array of matching records with their full data.`, - parameters: generateFindToolSchema(objectMetadata), + inputSchema: generateFindToolSchema(objectMetadata), execute: async (parameters) => { return this.findRecords( objectMetadata.nameSingular, @@ -102,7 +102,7 @@ export class ToolService { tools[`find_one_${objectMetadata.nameSingular}`] = { description: `Retrieve a single ${objectMetadata.labelSingular} record by its unique ID. Use this when you know the exact record ID and need the complete record data. Returns the full record or an error if not found.`, - parameters: generateFindOneToolSchema(), + inputSchema: generateFindOneToolSchema(), execute: async (parameters) => { return this.findOneRecord( objectMetadata.nameSingular, @@ -117,7 +117,7 @@ export class ToolService { if (objectPermission.canSoftDelete) { tools[`soft_delete_${objectMetadata.nameSingular}`] = { description: `Soft delete a ${objectMetadata.labelSingular} record by marking it as deleted. The record remains in the database but is hidden from normal queries. This is reversible and preserves all data. Use this for temporary removal.`, - parameters: generateSoftDeleteToolSchema(), + inputSchema: generateSoftDeleteToolSchema(), execute: async (parameters) => { return this.softDeleteRecord( objectMetadata.nameSingular, @@ -130,7 +130,7 @@ export class ToolService { tools[`soft_delete_many_${objectMetadata.nameSingular}`] = { description: `Soft delete multiple ${objectMetadata.labelSingular} records at once by providing an array of record IDs. All records are marked as deleted but remain in the database. This is efficient for bulk operations and preserves all data.`, - parameters: generateBulkDeleteToolSchema(), + inputSchema: generateBulkDeleteToolSchema(), execute: async (parameters) => { return this.softDeleteManyRecords( objectMetadata.nameSingular, diff --git a/packages/twenty-server/src/engine/core-modules/audit/services/audit.service.spec.ts b/packages/twenty-server/src/engine/core-modules/audit/services/audit.service.spec.ts index 8a39b90b884..a60594ba42c 100644 --- a/packages/twenty-server/src/engine/core-modules/audit/services/audit.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/audit/services/audit.service.spec.ts @@ -3,6 +3,7 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { AuditContextMock } from 'test/utils/audit-context.mock'; import { ClickHouseService } from 'src/database/clickHouse/clickHouse.service'; +import { OBJECT_RECORD_CREATED_EVENT } from 'src/engine/core-modules/audit/utils/events/object-event/object-record-created'; import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-activated'; import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; @@ -135,7 +136,7 @@ describe('AuditService', () => { const context = service.createContext(mockUserIdAndWorkspaceId); const result = await context.createObjectEvent( - CUSTOM_DOMAIN_ACTIVATED_EVENT, + OBJECT_RECORD_CREATED_EVENT, { recordId: 'test-record-id', objectMetadataId: 'test-object-metadata-id', diff --git a/packages/twenty-server/src/engine/core-modules/audit/utils/events/common/base-schemas.ts b/packages/twenty-server/src/engine/core-modules/audit/utils/events/common/base-schemas.ts index e56283daf44..c4f113e4d27 100644 --- a/packages/twenty-server/src/engine/core-modules/audit/utils/events/common/base-schemas.ts +++ b/packages/twenty-server/src/engine/core-modules/audit/utils/events/common/base-schemas.ts @@ -1,10 +1,8 @@ import { z } from 'zod'; -export const baseEventSchema = z - .object({ - timestamp: z.string(), - userId: z.string().nullish(), - workspaceId: z.string().nullish(), - version: z.string(), - }) - .strict(); +export const baseEventSchema = z.strictObject({ + timestamp: z.string(), + userId: z.string().nullish(), + workspaceId: z.string().nullish(), + version: z.string(), +}); diff --git a/packages/twenty-server/src/engine/core-modules/audit/utils/events/object-event/object-record-created.ts b/packages/twenty-server/src/engine/core-modules/audit/utils/events/object-event/object-record-created.ts index 00b077c5511..5daccf50adc 100644 --- a/packages/twenty-server/src/engine/core-modules/audit/utils/events/object-event/object-record-created.ts +++ b/packages/twenty-server/src/engine/core-modules/audit/utils/events/object-event/object-record-created.ts @@ -5,7 +5,7 @@ import { registerEvent } from 'src/engine/core-modules/audit/utils/events/worksp export const OBJECT_RECORD_CREATED_EVENT = 'Object Record Created' as const; export const objectRecordCreatedSchema = z.object({ event: z.literal(OBJECT_RECORD_CREATED_EVENT), - properties: z.object({}).passthrough(), + properties: z.looseObject({}), }); export type ObjectRecordCreatedTrackEvent = z.infer< diff --git a/packages/twenty-server/src/engine/core-modules/audit/utils/events/object-event/object-record-delete.ts b/packages/twenty-server/src/engine/core-modules/audit/utils/events/object-event/object-record-delete.ts index 95809869348..143c25c1400 100644 --- a/packages/twenty-server/src/engine/core-modules/audit/utils/events/object-event/object-record-delete.ts +++ b/packages/twenty-server/src/engine/core-modules/audit/utils/events/object-event/object-record-delete.ts @@ -5,7 +5,7 @@ import { registerEvent } from 'src/engine/core-modules/audit/utils/events/worksp export const OBJECT_RECORD_DELETED_EVENT = 'Object Record Deleted' as const; export const objectRecordDeletedSchema = z.object({ event: z.literal(OBJECT_RECORD_DELETED_EVENT), - properties: z.object({}).passthrough(), + properties: z.looseObject({}), }); export type ObjectRecordDeletedTrackEvent = z.infer< diff --git a/packages/twenty-server/src/engine/core-modules/audit/utils/events/object-event/object-record-updated.ts b/packages/twenty-server/src/engine/core-modules/audit/utils/events/object-event/object-record-updated.ts index 01c0f12bb59..c19d667a9e2 100644 --- a/packages/twenty-server/src/engine/core-modules/audit/utils/events/object-event/object-record-updated.ts +++ b/packages/twenty-server/src/engine/core-modules/audit/utils/events/object-event/object-record-updated.ts @@ -5,7 +5,7 @@ import { registerEvent } from 'src/engine/core-modules/audit/utils/events/worksp export const OBJECT_RECORD_UPDATED_EVENT = 'Object Record Updated' as const; export const objectRecordUpdatedSchema = z.object({ event: z.literal(OBJECT_RECORD_UPDATED_EVENT), - properties: z.object({}).passthrough(), + properties: z.looseObject({}), }); export type ObjectRecordUpdatedTrackEvent = z.infer< diff --git a/packages/twenty-server/src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-activated.ts b/packages/twenty-server/src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-activated.ts index 15841e83957..1c236fa9abc 100644 --- a/packages/twenty-server/src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-activated.ts +++ b/packages/twenty-server/src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-activated.ts @@ -3,12 +3,10 @@ import { z } from 'zod'; import { registerEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track'; export const CUSTOM_DOMAIN_ACTIVATED_EVENT = 'Custom Domain Activated' as const; -export const customDomainActivatedSchema = z - .object({ - event: z.literal(CUSTOM_DOMAIN_ACTIVATED_EVENT), - properties: z.object({}).strict(), - }) - .strict(); +export const customDomainActivatedSchema = z.strictObject({ + event: z.literal(CUSTOM_DOMAIN_ACTIVATED_EVENT), + properties: z.strictObject({}), +}); export type CustomDomainActivatedTrackEvent = z.infer< typeof customDomainActivatedSchema diff --git a/packages/twenty-server/src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-deactivated.ts b/packages/twenty-server/src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-deactivated.ts index a5015f63d09..1a20aaf1f0c 100644 --- a/packages/twenty-server/src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-deactivated.ts +++ b/packages/twenty-server/src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-deactivated.ts @@ -4,12 +4,10 @@ import { registerEvent } from 'src/engine/core-modules/audit/utils/events/worksp export const CUSTOM_DOMAIN_DEACTIVATED_EVENT = 'Custom Domain Deactivated' as const; -export const customDomainDeactivatedSchema = z - .object({ - event: z.literal(CUSTOM_DOMAIN_DEACTIVATED_EVENT), - properties: z.object({}).strict(), - }) - .strict(); +export const customDomainDeactivatedSchema = z.strictObject({ + event: z.literal(CUSTOM_DOMAIN_DEACTIVATED_EVENT), + properties: z.strictObject({}), +}); export type CustomDomainDeactivatedTrackEvent = z.infer< typeof customDomainDeactivatedSchema diff --git a/packages/twenty-server/src/engine/core-modules/audit/utils/events/workspace-event/monitoring/monitoring.ts b/packages/twenty-server/src/engine/core-modules/audit/utils/events/workspace-event/monitoring/monitoring.ts index bc8f29cdf8d..5ab0e968090 100644 --- a/packages/twenty-server/src/engine/core-modules/audit/utils/events/workspace-event/monitoring/monitoring.ts +++ b/packages/twenty-server/src/engine/core-modules/audit/utils/events/workspace-event/monitoring/monitoring.ts @@ -3,19 +3,15 @@ import { z } from 'zod'; import { registerEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track'; export const MONITORING_EVENT = 'Monitoring' as const; -export const monitoringSchema = z - .object({ - event: z.literal(MONITORING_EVENT), - properties: z - .object({ - eventName: z.string(), - connectedAccountId: z.string().optional(), - messageChannelId: z.string().optional(), - message: z.string().optional(), - }) - .strict(), - }) - .strict(); +export const monitoringSchema = z.strictObject({ + event: z.literal(MONITORING_EVENT), + properties: z.strictObject({ + eventName: z.string(), + connectedAccountId: z.string().optional(), + messageChannelId: z.string().optional(), + message: z.string().optional(), + }), +}); export type MonitoringTrackEvent = z.infer; diff --git a/packages/twenty-server/src/engine/core-modules/audit/utils/events/workspace-event/serverless-function/serverless-function-executed.ts b/packages/twenty-server/src/engine/core-modules/audit/utils/events/workspace-event/serverless-function/serverless-function-executed.ts index 50b084c99b9..202bba6ce7a 100644 --- a/packages/twenty-server/src/engine/core-modules/audit/utils/events/workspace-event/serverless-function/serverless-function-executed.ts +++ b/packages/twenty-server/src/engine/core-modules/audit/utils/events/workspace-event/serverless-function/serverless-function-executed.ts @@ -4,20 +4,16 @@ import { registerEvent } from 'src/engine/core-modules/audit/utils/events/worksp export const SERVERLESS_FUNCTION_EXECUTED_EVENT = 'Serverless Function Executed' as const; -export const serverlessFunctionExecutedSchema = z - .object({ - event: z.literal(SERVERLESS_FUNCTION_EXECUTED_EVENT), - properties: z - .object({ - duration: z.number(), - status: z.enum(['IDLE', 'SUCCESS', 'ERROR']), - errorType: z.string().optional(), - functionId: z.string(), - functionName: z.string(), - }) - .strict(), - }) - .strict(); +export const serverlessFunctionExecutedSchema = z.strictObject({ + event: z.literal(SERVERLESS_FUNCTION_EXECUTED_EVENT), + properties: z.strictObject({ + duration: z.number(), + status: z.enum(['IDLE', 'SUCCESS', 'ERROR']), + errorType: z.string().optional(), + functionId: z.string(), + functionName: z.string(), + }), +}); export type ServerlessFunctionExecutedTrackEvent = z.infer< typeof serverlessFunctionExecutedSchema diff --git a/packages/twenty-server/src/engine/core-modules/audit/utils/events/workspace-event/user/user-signup.ts b/packages/twenty-server/src/engine/core-modules/audit/utils/events/workspace-event/user/user-signup.ts index 7453bf2a0de..231c7a52c92 100644 --- a/packages/twenty-server/src/engine/core-modules/audit/utils/events/workspace-event/user/user-signup.ts +++ b/packages/twenty-server/src/engine/core-modules/audit/utils/events/workspace-event/user/user-signup.ts @@ -3,12 +3,10 @@ import { z } from 'zod'; import { registerEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track'; export const USER_SIGNUP_EVENT = 'User Signup' as const; -export const userSignupSchema = z - .object({ - event: z.literal(USER_SIGNUP_EVENT), - properties: z.object({}).strict(), - }) - .strict(); +export const userSignupSchema = z.strictObject({ + event: z.literal(USER_SIGNUP_EVENT), + properties: z.strictObject({}), +}); export type UserSignupTrackEvent = z.infer; diff --git a/packages/twenty-server/src/engine/core-modules/audit/utils/events/workspace-event/webhook/webhook-response.ts b/packages/twenty-server/src/engine/core-modules/audit/utils/events/workspace-event/webhook/webhook-response.ts index 5806eda9495..f0c44114249 100644 --- a/packages/twenty-server/src/engine/core-modules/audit/utils/events/workspace-event/webhook/webhook-response.ts +++ b/packages/twenty-server/src/engine/core-modules/audit/utils/events/workspace-event/webhook/webhook-response.ts @@ -3,20 +3,16 @@ import { z } from 'zod'; import { registerEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track'; export const WEBHOOK_RESPONSE_EVENT = 'Webhook Response' as const; -export const webhookResponseSchema = z - .object({ - event: z.literal(WEBHOOK_RESPONSE_EVENT), - properties: z - .object({ - status: z.number().optional(), - success: z.boolean(), - url: z.string(), - webhookId: z.string(), - eventName: z.string(), - }) - .strict(), - }) - .strict(); +export const webhookResponseSchema = z.strictObject({ + event: z.literal(WEBHOOK_RESPONSE_EVENT), + properties: z.strictObject({ + status: z.number().optional(), + success: z.boolean(), + url: z.string(), + webhookId: z.string(), + eventName: z.string(), + }), +}); export type WebhookResponseTrackEvent = z.infer; diff --git a/packages/twenty-server/src/engine/core-modules/audit/utils/events/workspace-event/workspace-entity/workspace-entity-created.ts b/packages/twenty-server/src/engine/core-modules/audit/utils/events/workspace-event/workspace-entity/workspace-entity-created.ts index 5d921ab958a..d9ed1638499 100644 --- a/packages/twenty-server/src/engine/core-modules/audit/utils/events/workspace-event/workspace-entity/workspace-entity-created.ts +++ b/packages/twenty-server/src/engine/core-modules/audit/utils/events/workspace-event/workspace-entity/workspace-entity-created.ts @@ -4,14 +4,12 @@ import { registerEvent } from 'src/engine/core-modules/audit/utils/events/worksp export const WORKSPACE_ENTITY_CREATED_EVENT = 'Workspace Entity Created' as const; -export const workspaceEntityCreatedSchema = z - .object({ - event: z.literal(WORKSPACE_ENTITY_CREATED_EVENT), - properties: z.object({ - name: z.string(), - }), - }) - .strict(); +export const workspaceEntityCreatedSchema = z.strictObject({ + event: z.literal(WORKSPACE_ENTITY_CREATED_EVENT), + properties: z.strictObject({ + name: z.string(), + }), +}); export type WorkspaceEntityCreatedTrackEvent = z.infer< typeof workspaceEntityCreatedSchema diff --git a/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection-validator.service.ts b/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection-validator.service.ts index cac0c24aaa4..7408f5a8327 100644 --- a/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection-validator.service.ts +++ b/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection-validator.service.ts @@ -9,7 +9,7 @@ import { type ConnectionParameters } from 'src/engine/core-modules/imap-smtp-cal export class ImapSmtpCaldavValidatorService { private readonly protocolConnectionSchema = z.object({ host: z.string().min(1, 'Host is required'), - port: z.number().int().positive('Port must be a positive number'), + port: z.int().positive('Port must be a positive number'), username: z.string().optional(), password: z.string().min(1, 'Password is required'), secure: z.boolean().optional(), @@ -29,7 +29,7 @@ export class ImapSmtpCaldavValidatorService { return this.protocolConnectionSchema.parse(params); } catch (error) { if (error instanceof z.ZodError) { - const errorMessages = error.errors + const errorMessages = error.issues .map((err) => `${err.path.join('.')}: ${err.message}`) .join(', '); diff --git a/packages/twenty-server/src/engine/core-modules/tool/services/tool-registry.service.ts b/packages/twenty-server/src/engine/core-modules/tool/services/tool-registry.service.ts index f5fff4d3b95..e7d0ee3e5c5 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/services/tool-registry.service.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/services/tool-registry.service.ts @@ -18,7 +18,7 @@ export class ToolRegistryService { ToolType.SEND_EMAIL, () => ({ description: this.sendEmailTool.description, - parameters: this.sendEmailTool.parameters, + inputSchema: this.sendEmailTool.inputSchema, execute: (params) => this.sendEmailTool.execute(params as SendEmailInput), flag: PermissionFlagType.SEND_EMAIL_TOOL, diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/http-tool/http-tool.schema.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/http-tool/http-tool.schema.ts index e3bbdfece1f..0feb1e97cb9 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/http-tool/http-tool.schema.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/http-tool/http-tool.schema.ts @@ -6,7 +6,7 @@ export const HttpRequestInputZodSchema = z.object({ .enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']) .describe('The HTTP method to use'), headers: z - .record(z.string()) + .record(z.string(), z.string()) .optional() .describe('HTTP headers to include in the request'), body: z diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/http-tool/http-tool.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/http-tool/http-tool.ts index da6d4ab8d1b..e010a34b05f 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/http-tool/http-tool.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/http-tool/http-tool.ts @@ -13,7 +13,7 @@ import { type Tool } from 'src/engine/core-modules/tool/types/tool.type'; export class HttpTool implements Tool { description = 'Make an HTTP request to any URL with configurable method, headers, and body.'; - parameters = HttpToolParametersZodSchema; + inputSchema = HttpToolParametersZodSchema; async execute(parameters: ToolInput): Promise { const { url, method, headers, body } = parameters as HttpRequestInput; diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.schema.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.schema.ts index d9ef21756e9..438e7cd158f 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.schema.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.schema.ts @@ -1,11 +1,10 @@ import { z } from 'zod'; export const SendEmailInputZodSchema = z.object({ - email: z.string().email().describe('The recipient email address'), + email: z.email().describe('The recipient email address'), subject: z.string().describe('The email subject line'), body: z.string().describe('The email body content (HTML or plain text)'), connectedAccountId: z - .string() .uuid() .describe( 'The UUID of the connected account to send the email from. Provide this only if you have it; otherwise, leave blank.', diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.ts index f09ef2e7f92..3aa0554ca91 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.ts @@ -26,7 +26,7 @@ export class SendEmailTool implements Tool { description = 'Send an email using a connected account. Requires SEND_EMAIL_TOOL permission.'; - parameters = SendEmailToolParametersZodSchema; + inputSchema = SendEmailToolParametersZodSchema; constructor( private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory, @@ -91,7 +91,10 @@ export class SendEmailTool implements Tool { let { connectedAccountId } = parameters; try { - const emailSchema = z.string().trim().email('Invalid email'); + const emailSchema = z + .string() + .trim() + .pipe(z.email({ error: 'Invalid email' })); const emailValidation = emailSchema.safeParse(email); if (!emailValidation.success) { diff --git a/packages/twenty-server/src/engine/core-modules/tool/types/tool.type.ts b/packages/twenty-server/src/engine/core-modules/tool/types/tool.type.ts index 44f6b9a15d9..9880c284edd 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/types/tool.type.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/types/tool.type.ts @@ -1,5 +1,4 @@ -import { type JSONSchema7 } from 'json-schema'; -import { type ZodType } from 'zod'; +import { type FlexibleSchema } from '@ai-sdk/provider-utils'; import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type'; import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; @@ -7,7 +6,7 @@ import { type PermissionFlagType } from 'src/engine/metadata-modules/permissions export type Tool = { description: string; - parameters: JSONSchema7 | ZodType; + inputSchema: FlexibleSchema; execute(input: ToolInput): Promise; flag?: PermissionFlagType; }; diff --git a/packages/twenty-server/src/engine/core-modules/two-factor-authentication/strategies/otp/totp/constants/totp.strategy.constants.ts b/packages/twenty-server/src/engine/core-modules/two-factor-authentication/strategies/otp/totp/constants/totp.strategy.constants.ts index be5293e7dcc..a0d74ed20a1 100644 --- a/packages/twenty-server/src/engine/core-modules/two-factor-authentication/strategies/otp/totp/constants/totp.strategy.constants.ts +++ b/packages/twenty-server/src/engine/core-modules/two-factor-authentication/strategies/otp/totp/constants/totp.strategy.constants.ts @@ -33,33 +33,28 @@ export type TOTPStrategyConfig = z.infer; export const TOTP_STRATEGY_CONFIG_SCHEMA = z.object({ algorithm: z - .nativeEnum(TOTPHashAlgorithms, { - errorMap: () => ({ - message: - 'Invalid algorithm specified. Must be SHA1, SHA256, or SHA512.', - }), + .enum(TOTPHashAlgorithms, { + error: () => + 'Invalid algorithm specified. Must be SHA1, SHA256, or SHA512.', }) .optional(), digits: z - .number({ - invalid_type_error: 'Digits must be a number.', + .int({ + error: 'Digits must be a whole number.', + }) + .min(6, { + error: 'Digits must be at least 6.', + }) + .max(8, { + error: 'Digits cannot be more than 8.', }) - .int({ message: 'Digits must be a whole number.' }) - .min(6, { message: 'Digits must be at least 6.' }) - .max(8, { message: 'Digits cannot be more than 8.' }) .optional(), encodings: z - .nativeEnum(TOTPKeyEncodings, { - errorMap: () => ({ message: 'Invalid encoding specified.' }), + .enum(TOTPKeyEncodings, { + error: () => 'Invalid encoding specified.', }) .optional(), - window: z.number().int().min(0).optional(), - step: z - .number({ - invalid_type_error: 'Step must be a number.', - }) - .int() - .min(1) - .optional(), - epoch: z.number().int().min(0).optional(), + window: z.int().min(0).optional(), + step: z.int().min(1).optional(), + epoch: z.int().min(0).optional(), }); diff --git a/packages/twenty-server/src/engine/core-modules/two-factor-authentication/strategies/otp/totp/totp.strategy.ts b/packages/twenty-server/src/engine/core-modules/two-factor-authentication/strategies/otp/totp/totp.strategy.ts index aa83a8dd956..d21fa98cb64 100644 --- a/packages/twenty-server/src/engine/core-modules/two-factor-authentication/strategies/otp/totp/totp.strategy.ts +++ b/packages/twenty-server/src/engine/core-modules/two-factor-authentication/strategies/otp/totp/totp.strategy.ts @@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common'; import { authenticator } from 'otplib'; import { TwoFactorAuthenticationStrategy } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; -import { type SafeParseReturnType } from 'zod'; +import { type ZodSafeParseResult } from 'zod'; import { type OTPAuthenticationStrategyInterface } from 'src/engine/core-modules/two-factor-authentication/strategies/otp/interfaces/otp.strategy.interface'; @@ -24,7 +24,7 @@ export class TotpStrategy implements OTPAuthenticationStrategyInterface { public readonly name = TwoFactorAuthenticationStrategy.TOTP; constructor(options?: TOTPStrategyConfig) { - let result: SafeParseReturnType | undefined; + let result: ZodSafeParseResult | undefined; if (isDefined(options)) { result = TOTP_STRATEGY_CONFIG_SCHEMA.safeParse(options); diff --git a/packages/twenty-server/src/engine/metadata-modules/agent/agent-execution.service.ts b/packages/twenty-server/src/engine/metadata-modules/agent/agent-execution.service.ts index 7d17a8dcba8..c82437be790 100644 --- a/packages/twenty-server/src/engine/metadata-modules/agent/agent-execution.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/agent/agent-execution.service.ts @@ -2,13 +2,14 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { - type CoreMessage, - type CoreUserMessage, type FilePart, type ImagePart, + LanguageModelUsage, + type ModelMessage, streamText, ToolSet, type UserContent, + UserModelMessage, } from 'ai'; import { AppPath } from 'twenty-shared/types'; import { getAppPath } from 'twenty-shared/utils'; @@ -40,11 +41,7 @@ import { AgentException, AgentExceptionCode } from './agent.exception'; export interface AgentExecutionResult { result: object; - usage: { - promptTokens: number; - completionTokens: number; - totalTokens: number; - }; + usage: LanguageModelUsage; } @Injectable() @@ -68,14 +65,12 @@ export class AgentExecutionService { async prepareAIRequestConfig({ messages, - prompt, system, agent, }: { system: string; agent: AgentEntity | null; - prompt?: string; - messages?: CoreMessage[]; + messages: ModelMessage[]; }) { try { if (agent) { @@ -111,8 +106,7 @@ export class AgentExecutionService { system, tools, model: registeredModel.model, - ...(messages && { messages }), - ...(prompt && { prompt }), + messages, maxSteps: AGENT_CONFIG.MAX_STEPS, ...(registeredModel.doesSupportThinking && { providerOptions: { @@ -149,7 +143,7 @@ export class AgentExecutionService { private async buildUserMessage( userMessage: string, fileIds: string[], - ): Promise { + ): Promise { const content: Exclude = [ { type: 'text', @@ -248,22 +242,22 @@ export class AgentExecutionService { return { type: 'image', image: fileBuffer, - mimeType: file.type, + mediaType: file.type, }; } return { type: 'file', data: fileBuffer, - mimeType: file.type, + mediaType: file.type, }; } private mapMessagesToCoreMessages( messages: AgentChatMessageEntity[], - ): CoreMessage[] { + ): ModelMessage[] { return messages - .map(({ role, rawContent }): CoreMessage => { + .map(({ role, rawContent }): ModelMessage => { if (role === AgentChatMessageRole.USER) { return { role: 'user', @@ -300,7 +294,8 @@ export class AgentExecutionService { where: { id: agentId }, }); - const llmMessages: CoreMessage[] = this.mapMessagesToCoreMessages(messages); + const llmMessages: ModelMessage[] = + this.mapMessagesToCoreMessages(messages); let contextString = ''; diff --git a/packages/twenty-server/src/engine/metadata-modules/agent/agent-handoff-executor.service.ts b/packages/twenty-server/src/engine/metadata-modules/agent/agent-handoff-executor.service.ts index 423c8a97e6c..f2c20db8024 100644 --- a/packages/twenty-server/src/engine/metadata-modules/agent/agent-handoff-executor.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/agent/agent-handoff-executor.service.ts @@ -1,7 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { CoreMessage, generateText } from 'ai'; +import { ModelMessage, generateText } from 'ai'; import { Repository } from 'typeorm'; import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service'; @@ -15,7 +15,7 @@ export type HandoffRequest = { fromAgentId: string; toAgentId: string; workspaceId: string; - messages?: CoreMessage[]; + messages: ModelMessage[]; }; @Injectable() diff --git a/packages/twenty-server/src/engine/metadata-modules/agent/agent-handoff-tool.service.ts b/packages/twenty-server/src/engine/metadata-modules/agent/agent-handoff-tool.service.ts index 0bc32ed10f3..2531cd733d3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/agent/agent-handoff-tool.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/agent/agent-handoff-tool.service.ts @@ -35,7 +35,7 @@ export class AgentHandoffToolService { '{agentName}', handoff.toAgent.name, ), - parameters: AGENT_HANDOFF_SCHEMA, + inputSchema: AGENT_HANDOFF_SCHEMA, execute: async ({ input }) => { const result = await this.agentHandoffExecutorService.executeHandoff({ fromAgentId: agentId, diff --git a/packages/twenty-server/src/engine/metadata-modules/agent/agent-streaming.service.ts b/packages/twenty-server/src/engine/metadata-modules/agent/agent-streaming.service.ts index 3e4eb183172..e462ed02e79 100644 --- a/packages/twenty-server/src/engine/metadata-modules/agent/agent-streaming.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/agent/agent-streaming.service.ts @@ -25,6 +25,16 @@ export type StreamAgentChatOptions = { res: Response; }; +const CLIENT_FORWARDED_EVENT_TYPES = [ + 'text-delta', + 'reasoning', + 'reasoning-delta', + 'tool-call', + 'tool-input-delta', + 'tool-result', + 'error', +]; + @Injectable() export class AgentStreamingService { private readonly logger = new Logger(AgentStreamingService.name); @@ -81,14 +91,7 @@ export class AgentStreamingService { this.sendStreamEvent( res, - [ - 'text-delta', - 'reasoning', - 'reasoning-signature', - 'tool-call', - 'tool-result', - 'error', - ].includes(chunk.type) + CLIENT_FORWARDED_EVENT_TYPES.includes(chunk.type) ? chunk : { type: chunk.type }, ); diff --git a/packages/twenty-server/src/engine/metadata-modules/agent/constants/agent-handoff-schema.const.ts b/packages/twenty-server/src/engine/metadata-modules/agent/constants/agent-handoff-schema.const.ts index abc46db9ff9..0213d6b8628 100644 --- a/packages/twenty-server/src/engine/metadata-modules/agent/constants/agent-handoff-schema.const.ts +++ b/packages/twenty-server/src/engine/metadata-modules/agent/constants/agent-handoff-schema.const.ts @@ -26,24 +26,16 @@ export const AGENT_HANDOFF_SCHEMA = z.object({ }), z.object({ type: z.literal('image'), - image: z.union([ - z.string(), - z.instanceof(Uint8Array), - z.instanceof(Buffer), - z.instanceof(ArrayBuffer), - z.string().url(), - ]), + image: z + .string() + .describe('Base64 encoded image data or URL'), mediaType: z.string().optional(), }), z.object({ type: z.literal('file'), - data: z.union([ - z.string(), - z.instanceof(Uint8Array), - z.instanceof(Buffer), - z.instanceof(ArrayBuffer), - z.string().url(), - ]), + data: z + .string() + .describe('Base64 encoded file data or URL'), mediaType: z.string(), }), ]), @@ -62,13 +54,9 @@ export const AGENT_HANDOFF_SCHEMA = z.object({ }), z.object({ type: z.literal('file'), - data: z.union([ - z.string(), - z.instanceof(Uint8Array), - z.instanceof(Buffer), - z.instanceof(ArrayBuffer), - z.string().url(), - ]), + data: z + .string() + .describe('Base64 encoded file data or URL'), mediaType: z.string(), filename: z.string().optional(), }), @@ -80,7 +68,7 @@ export const AGENT_HANDOFF_SCHEMA = z.object({ type: z.literal('tool-call'), toolCallId: z.string(), toolName: z.string(), - input: z.record(z.any()), + input: z.record(z.string(), z.any()), }), ]), ), diff --git a/packages/twenty-server/src/engine/metadata-modules/agent/utils/constructAssistantMessageContentFromStream.ts b/packages/twenty-server/src/engine/metadata-modules/agent/utils/constructAssistantMessageContentFromStream.ts index 3127e455f10..4a23e93b1a5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/agent/utils/constructAssistantMessageContentFromStream.ts +++ b/packages/twenty-server/src/engine/metadata-modules/agent/utils/constructAssistantMessageContentFromStream.ts @@ -1,60 +1,96 @@ +import { type ReasoningPart } from '@ai-sdk/provider-utils'; import { type TextPart } from 'ai'; - -type ReasoningPart = { - type: 'reasoning'; - text: string; - signature: string; -}; +import { + parseStreamLine, + splitStreamIntoLines, + type TextBlock, +} from 'twenty-shared/ai'; export const constructAssistantMessageContentFromStream = ( rawContent: string, ) => { - const lines = rawContent.trim().split('\n'); + const lines = splitStreamIntoLines(rawContent); const output: Array = []; - let reasoningText = ''; - let textContent = ''; + let currentTextBlock: TextBlock = null; + + const flushTextBlock = () => { + if (currentTextBlock) { + if (currentTextBlock.type === 'reasoning') { + output.push({ + type: 'reasoning', + text: currentTextBlock.content, + }); + } else { + output.push({ + type: 'text', + text: currentTextBlock.content, + }); + } + currentTextBlock = null; + } + }; for (const line of lines) { - let event; + const event = parseStreamLine(line); - try { - event = JSON.parse(line); - } catch { + if (!event) { continue; } switch (event.type) { - case 'reasoning': - reasoningText += event.textDelta || ''; + case 'reasoning-start': + flushTextBlock(); + currentTextBlock = { + type: 'reasoning', + content: '', + isThinking: true, + }; break; - case 'reasoning-signature': - if (reasoningText) { - output.push({ + case 'reasoning-delta': + if (!currentTextBlock || currentTextBlock.type !== 'reasoning') { + flushTextBlock(); + currentTextBlock = { type: 'reasoning', - text: reasoningText, - signature: event.signature, - }); - reasoningText = ''; + content: '', + isThinking: true, + }; + } + currentTextBlock.content += event.text || ''; + break; + + case 'reasoning-end': + if (currentTextBlock?.type === 'reasoning') { + currentTextBlock.isThinking = false; } break; case 'text-delta': - textContent += event.textDelta || ''; + if (!currentTextBlock || currentTextBlock.type !== 'text') { + flushTextBlock(); + currentTextBlock = { type: 'text', content: '' }; + } + currentTextBlock.content += event.text || ''; + break; + + case 'step-finish': + if (currentTextBlock?.type === 'reasoning') { + currentTextBlock.isThinking = false; + } + flushTextBlock(); + break; + + case 'error': + flushTextBlock(); break; default: - if (textContent) { - output.push({ - type: 'text', - text: textContent, - }); - textContent = ''; - } break; } } + flushTextBlock(); + return output; }; diff --git a/packages/twenty-server/src/modules/blocklist/blocklist-validation-manager/services/blocklist-validation.service.ts b/packages/twenty-server/src/modules/blocklist/blocklist-validation-manager/services/blocklist-validation.service.ts index bf2b6ce79f8..821b85944e4 100644 --- a/packages/twenty-server/src/modules/blocklist/blocklist-validation-manager/services/blocklist-validation.service.ts +++ b/packages/twenty-server/src/modules/blocklist/blocklist-validation-manager/services/blocklist-validation.service.ts @@ -55,7 +55,7 @@ export class BlocklistValidationService { const emailOrDomainSchema = z .string() .trim() - .email('Invalid email or domain') + .pipe(z.email({ error: 'Invalid email or domain' })) .or( z .string() @@ -73,7 +73,7 @@ export class BlocklistValidationService { const result = emailOrDomainSchema.safeParse(handle); if (!result.success) { - throw new BadRequestException(result.error.errors[0].message); + throw new BadRequestException(result.error.issues[0].message); } } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/services/ai-agent-executor.service.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/services/ai-agent-executor.service.ts index 2fc6d33794e..9c67fbd2262 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/services/ai-agent-executor.service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/services/ai-agent-executor.service.ts @@ -1,7 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { generateObject, generateText, ToolSet } from 'ai'; +import { generateObject, generateText, stepCountIs, ToolSet } from 'ai'; import { Repository } from 'typeorm'; import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service'; @@ -98,7 +98,7 @@ export class AiAgentExecutorService { tools, model: registeredModel.model, prompt: userPrompt, - maxSteps: AGENT_CONFIG.MAX_STEPS, + stopWhen: stepCountIs(AGENT_CONFIG.MAX_STEPS), }); if (Object.keys(schema).length === 0) { @@ -121,12 +121,12 @@ export class AiAgentExecutorService { return { result: output.object, usage: { - promptTokens: - (textResponse.usage?.promptTokens ?? 0) + - (output.usage?.promptTokens ?? 0), - completionTokens: - (textResponse.usage?.completionTokens ?? 0) + - (output.usage?.completionTokens ?? 0), + inputTokens: + (textResponse.usage?.inputTokens ?? 0) + + (output.usage?.inputTokens ?? 0), + outputTokens: + (textResponse.usage?.outputTokens ?? 0) + + (output.usage?.outputTokens ?? 0), totalTokens: (textResponse.usage?.totalTokens ?? 0) + (output.usage?.totalTokens ?? 0), diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/schemas/workflow-tool-schemas.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/schemas/workflow-tool-schemas.ts index 31394818104..97951f4c1be 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-tools/schemas/workflow-tool-schemas.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/schemas/workflow-tool-schemas.ts @@ -1,11 +1,9 @@ -import { - workflowActionSchema, - workflowTriggerSchema, -} from 'twenty-shared/workflow'; import { z } from 'zod'; import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; +import { workflowActionSchema, workflowTriggerSchema } from './workflow.schema'; + export const createWorkflowVersionStepSchema = z.object({ workflowVersionId: z .string() diff --git a/packages/twenty-shared/src/workflow/schemas/workflow.schema.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/schemas/workflow.schema.ts similarity index 61% rename from packages/twenty-shared/src/workflow/schemas/workflow.schema.ts rename to packages/twenty-server/src/modules/workflow/workflow-tools/schemas/workflow.schema.ts index 3298979f5da..9a6e9b2f191 100644 --- a/packages/twenty-shared/src/workflow/schemas/workflow.schema.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/schemas/workflow.schema.ts @@ -1,27 +1,28 @@ +import { + FieldMetadataType, + StepLogicalOperator, + ViewFilterOperand, +} from 'twenty-shared/types'; +import { StepStatus } from 'twenty-shared/workflow'; import { z } from 'zod'; -import { FieldMetadataType } from '../../types/FieldMetadataType'; -import { StepLogicalOperator } from '../../types/StepFilters'; -import { ViewFilterOperand } from '../../types/ViewFilterOperand'; -import { StepStatus } from '../types/WorkflowRunStateStepInfos'; -// Base schemas export const objectRecordSchema = z - .record(z.any()) + .record(z.string(), z.any()) .describe( 'Record data object. Use nested objects for relationships (e.g., "company": {"id": "{{reference}}"}). Common patterns:\n' + - '- Person: {"name": {"firstName": "John", "lastName": "Doe"}, "emails": {"primaryEmail": "john@example.com"}, "company": {"id": "{{trigger.object.id}}"}}\n' + - '- Company: {"name": "Acme Corp", "domainName": {"primaryLinkUrl": "https://acme.com"}}\n' + - '- Task: {"title": "Follow up", "status": "TODO", "assignee": {"id": "{{user.id}}"}}', + '- Person: {"name": {"firstName": "John", "lastName": "Doe"}, "emails": {"primaryEmail": "john@example.com"}, "company": {"id": "{{trigger.object.id}}"}}\n' + + '- Company: {"name": "Acme Corp", "domainName": {"primaryLinkUrl": "https://acme.com"}}\n' + + '- Task: {"title": "Follow up", "status": "TODO", "assignee": {"id": "{{user.id}}"}}', ); export const baseWorkflowActionSettingsSchema = z.object({ input: z - .object({}) - .passthrough() - .describe('Input data for the workflow action. Structure depends on the action type.'), + .looseObject({}) + .describe( + 'Input data for the workflow action. Structure depends on the action type.', + ), outputSchema: z - .object({}) - .passthrough() + .looseObject({}) .describe( 'Schema defining the output data structure. This data can be referenced in subsequent steps using {{stepId.fieldName}}.', ), @@ -30,7 +31,9 @@ export const baseWorkflowActionSettingsSchema = z.object({ value: z.boolean().describe('Whether to retry the action if it fails.'), }), continueOnFailure: z.object({ - value: z.boolean().describe('Whether to continue to the next step if this action fails.'), + value: z + .boolean() + .describe('Whether to continue to the next step if this action fails.'), }), }), }); @@ -38,18 +41,26 @@ export const baseWorkflowActionSettingsSchema = z.object({ export const baseWorkflowActionSchema = z.object({ id: z .string() - .describe('Unique identifier for the workflow step. Must be unique within the workflow.'), + .describe( + 'Unique identifier for the workflow step. Must be unique within the workflow.', + ), name: z .string() - .describe('Human-readable name for the workflow step. Should clearly describe what the step does.'), + .describe( + 'Human-readable name for the workflow step. Should clearly describe what the step does.', + ), valid: z .boolean() - .describe('Whether the step configuration is valid. Set to true when all required fields are properly configured.'), + .describe( + 'Whether the step configuration is valid. Set to true when all required fields are properly configured.', + ), nextStepIds: z .array(z.string()) .optional() .nullable() - .describe('Array of step IDs that this step connects to. Leave empty or null for the final step.'), + .describe( + 'Array of step IDs that this step connects to. Leave empty or null for the final step.', + ), position: z .object({ x: z.number(), y: z.number() }) .optional() @@ -61,7 +72,9 @@ export const baseTriggerSchema = z.object({ name: z .string() .optional() - .describe('Human-readable name for the trigger. Optional but recommended for clarity.'), + .describe( + 'Human-readable name for the trigger. Optional but recommended for clarity.', + ), type: z .enum(['DATABASE_EVENT', 'MANUAL', 'CRON', 'WEBHOOK']) .describe( @@ -71,21 +84,24 @@ export const baseTriggerSchema = z.object({ .object({ x: z.number(), y: z.number() }) .optional() .nullable() - .describe('Position coordinates for the trigger in the workflow diagram. Use (0, 0) for the trigger step.'), + .describe( + 'Position coordinates for the trigger in the workflow diagram. Use (0, 0) for the trigger step.', + ), nextStepIds: z .array(z.string()) .optional() .nullable() - .describe('Array of step IDs that the trigger connects to. These are the first steps in the workflow.'), + .describe( + 'Array of step IDs that the trigger connects to. These are the first steps in the workflow.', + ), }); -// Action settings schemas export const workflowCodeActionSettingsSchema = baseWorkflowActionSettingsSchema.extend({ input: z.object({ serverlessFunctionId: z.string(), serverlessFunctionVersion: z.string(), - serverlessFunctionInput: z.record(z.any()), + serverlessFunctionInput: z.record(z.string(), z.any()), }), }); @@ -107,10 +123,7 @@ export const workflowCreateRecordActionSettingsSchema = .describe( 'The name of the object to create a record in. Must be lowercase (e.g., "person", "company", "task").', ), - objectRecord: objectRecordSchema - .describe( - 'The record data to create.', - ) + objectRecord: objectRecordSchema.describe('The record data to create.'), }), }); @@ -162,7 +175,7 @@ export const workflowFormActionSettingsSchema = z.literal('RECORD'), ]), placeholder: z.string().optional(), - settings: z.record(z.any()).optional(), + settings: z.record(z.string(), z.any()).optional(), value: z.any().optional(), }), ), @@ -173,9 +186,10 @@ export const workflowHttpRequestActionSettingsSchema = input: z.object({ url: z.string(), method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']), - headers: z.record(z.string()).optional(), + headers: z.record(z.string(), z.string()).optional(), body: z .record( + z.string(), z.union([ z.string(), z.number(), @@ -200,30 +214,48 @@ export const workflowAiAgentActionSettingsSchema = export const workflowFilterActionSettingsSchema = baseWorkflowActionSettingsSchema.extend({ input: z.object({ - stepFilterGroups: z.array(z.object({ - id: z.string(), - logicalOperator: z.nativeEnum(StepLogicalOperator), - parentStepFilterGroupId: z.string().optional(), - positionInStepFilterGroup: z.number().optional(), - })), - stepFilters: z.array(z.object({ - id: z.string(), - type: z.string(), - stepOutputKey: z.string(), - operand: z.nativeEnum(ViewFilterOperand), - value: z.string(), - stepFilterGroupId: z.string(), - positionInStepFilterGroup: z.number().optional(), - fieldMetadataId: z.string().optional(), - compositeFieldSubFieldName: z.string().optional(), - })), + stepFilterGroups: z.array( + z.object({ + id: z.string(), + logicalOperator: z.enum(StepLogicalOperator), + parentStepFilterGroupId: z.string().optional(), + positionInStepFilterGroup: z.number().optional(), + }), + ), + stepFilters: z.array( + z.object({ + id: z.string(), + type: z.string(), + stepOutputKey: z.string(), + operand: z.enum(ViewFilterOperand), + value: z.string(), + stepFilterGroupId: z.string(), + positionInStepFilterGroup: z.number().optional(), + fieldMetadataId: z.string().optional(), + compositeFieldSubFieldName: z.string().optional(), + }), + ), }), }); export const workflowIteratorActionSettingsSchema = baseWorkflowActionSettingsSchema.extend({ input: z.object({ - items: z.union([z.array(z.union([z.string(), z.number(), z.boolean(), z.null(), z.record(z.any()), z.any()])), z.string()]).optional(), + items: z + .union([ + z.array( + z.union([ + z.string(), + z.number(), + z.boolean(), + z.null(), + z.record(z.string(), z.any()), + z.any(), + ]), + ), + z.string(), + ]) + .optional(), initialLoopStepIds: z.array(z.string()).optional(), }), }); @@ -233,7 +265,6 @@ export const workflowEmptyActionSettingsSchema = input: z.object({}), }); -// Action schemas export const workflowCodeActionSchema = baseWorkflowActionSchema.extend({ type: z.literal('CODE'), settings: workflowCodeActionSettingsSchema, @@ -244,20 +275,26 @@ export const workflowSendEmailActionSchema = baseWorkflowActionSchema.extend({ settings: workflowSendEmailActionSettingsSchema, }); -export const workflowCreateRecordActionSchema = baseWorkflowActionSchema.extend({ - type: z.literal('CREATE_RECORD'), - settings: workflowCreateRecordActionSettingsSchema, -}); +export const workflowCreateRecordActionSchema = baseWorkflowActionSchema.extend( + { + type: z.literal('CREATE_RECORD'), + settings: workflowCreateRecordActionSettingsSchema, + }, +); -export const workflowUpdateRecordActionSchema = baseWorkflowActionSchema.extend({ - type: z.literal('UPDATE_RECORD'), - settings: workflowUpdateRecordActionSettingsSchema, -}); +export const workflowUpdateRecordActionSchema = baseWorkflowActionSchema.extend( + { + type: z.literal('UPDATE_RECORD'), + settings: workflowUpdateRecordActionSettingsSchema, + }, +); -export const workflowDeleteRecordActionSchema = baseWorkflowActionSchema.extend({ - type: z.literal('DELETE_RECORD'), - settings: workflowDeleteRecordActionSettingsSchema, -}); +export const workflowDeleteRecordActionSchema = baseWorkflowActionSchema.extend( + { + type: z.literal('DELETE_RECORD'), + settings: workflowDeleteRecordActionSettingsSchema, + }, +); export const workflowFindRecordsActionSchema = baseWorkflowActionSchema.extend({ type: z.literal('FIND_RECORDS'), @@ -294,7 +331,6 @@ export const workflowEmptyActionSchema = baseWorkflowActionSchema.extend({ settings: workflowEmptyActionSettingsSchema, }); -// Combined action schema export const workflowActionSchema = z.discriminatedUnion('type', [ workflowCodeActionSchema, workflowSendEmailActionSchema, @@ -310,49 +346,50 @@ export const workflowActionSchema = z.discriminatedUnion('type', [ workflowEmptyActionSchema, ]); -// Trigger schemas -export const workflowDatabaseEventTriggerSchema = baseTriggerSchema.extend({ - type: z.literal('DATABASE_EVENT'), - settings: z.object({ - eventName: z - .string() - .regex( - /^[a-z][a-zA-Z0-9_]*\.(created|updated|deleted)$/, - 'Event name must follow the pattern: objectName.action (e.g., "company.created", "person.updated")', - ) - .describe( - 'Event name in format: objectName.action (e.g., "company.created", "person.updated", "task.deleted"). Use lowercase object names.', - ), - input: z.object({}).passthrough().optional(), - outputSchema: z - .object({}) - .passthrough() - .describe( - 'Schema defining the output data structure. For database events, this includes the record that triggered the workflow accessible via {{trigger.object.fieldName}}.', - ), - objectType: z.string().optional(), - fields: z.array(z.string()).optional().nullable(), - }), -}).describe( - 'Database event trigger that fires when a record is created, updated, or deleted. The triggered record is accessible in workflow steps via {{trigger.object.fieldName}}.', -); +export const workflowDatabaseEventTriggerSchema = baseTriggerSchema + .extend({ + type: z.literal('DATABASE_EVENT'), + settings: z.object({ + eventName: z + .string() + .regex( + /^[a-z][a-zA-Z0-9_]*\.(created|updated|deleted)$/, + 'Event name must follow the pattern: objectName.action (e.g., "company.created", "person.updated")', + ) + .describe( + 'Event name in format: objectName.action (e.g., "company.created", "person.updated", "task.deleted"). Use lowercase object names.', + ), + input: z.looseObject({}).optional(), + outputSchema: z + .looseObject({}) + .describe( + 'Schema defining the output data structure. For database events, this includes the record that triggered the workflow accessible via {{trigger.object.fieldName}}.', + ), + objectType: z.string().optional(), + fields: z.array(z.string()).optional().nullable(), + }), + }) + .describe( + 'Database event trigger that fires when a record is created, updated, or deleted. The triggered record is accessible in workflow steps via {{trigger.object.fieldName}}.', + ); -export const workflowManualTriggerSchema = baseTriggerSchema.extend({ - type: z.literal('MANUAL'), - settings: z.object({ - objectType: z.string().optional(), - outputSchema: z - .object({}) - .passthrough() - .describe( - 'Schema defining the output data structure. When a record is selected, it is accessible via {{trigger.record.fieldName}}. When no record is selected, no data is available.', - ), - icon: z.string().optional(), - isPinned: z.boolean().optional(), - }), -}).describe( - 'Manual trigger that can be launched by the user. If a record is selected when launched, it is accessible via {{trigger.record.fieldName}}. If no record is selected, no data context is available.', -); +export const workflowManualTriggerSchema = baseTriggerSchema + .extend({ + type: z.literal('MANUAL'), + settings: z.object({ + objectType: z.string().optional(), + outputSchema: z + .looseObject({}) + .describe( + 'Schema defining the output data structure. When a record is selected, it is accessible via {{trigger.record.fieldName}}. When no record is selected, no data is available.', + ), + icon: z.string().optional(), + isPinned: z.boolean().optional(), + }), + }) + .describe( + 'Manual trigger that can be launched by the user. If a record is selected when launched, it is accessible via {{trigger.record.fieldName}}. If no record is selected, no data context is available.', + ); export const workflowCronTriggerSchema = baseTriggerSchema.extend({ type: z.literal('CRON'), @@ -364,7 +401,7 @@ export const workflowCronTriggerSchema = baseTriggerSchema.extend({ hour: z.number().min(0).max(23), minute: z.number().min(0).max(59), }), - outputSchema: z.object({}).passthrough(), + outputSchema: z.looseObject({}), }), z.object({ type: z.literal('HOURS'), @@ -372,17 +409,17 @@ export const workflowCronTriggerSchema = baseTriggerSchema.extend({ hour: z.number().min(1), minute: z.number().min(0).max(59), }), - outputSchema: z.object({}).passthrough(), + outputSchema: z.looseObject({}), }), z.object({ type: z.literal('MINUTES'), schedule: z.object({ minute: z.number().min(1) }), - outputSchema: z.object({}).passthrough(), + outputSchema: z.looseObject({}), }), z.object({ type: z.literal('CUSTOM'), pattern: z.string(), - outputSchema: z.object({}).passthrough(), + outputSchema: z.looseObject({}), }), ]), }); @@ -391,20 +428,19 @@ export const workflowWebhookTriggerSchema = baseTriggerSchema.extend({ type: z.literal('WEBHOOK'), settings: z.discriminatedUnion('httpMethod', [ z.object({ - outputSchema: z.object({}).passthrough(), + outputSchema: z.looseObject({}), httpMethod: z.literal('GET'), authentication: z.literal('API_KEY').nullable(), }), z.object({ - outputSchema: z.object({}).passthrough(), + outputSchema: z.looseObject({}), httpMethod: z.literal('POST'), - expectedBody: z.object({}).passthrough(), + expectedBody: z.looseObject({}), authentication: z.literal('API_KEY').nullable(), }), ]), }); -// Combined trigger schema export const workflowTriggerSchema = z.discriminatedUnion('type', [ workflowDatabaseEventTriggerSchema, workflowManualTriggerSchema, @@ -412,7 +448,7 @@ export const workflowTriggerSchema = z.discriminatedUnion('type', [ workflowWebhookTriggerSchema, ]); -export const workflowRunStepStatusSchema = z.nativeEnum(StepStatus); +export const workflowRunStepStatusSchema = z.enum(StepStatus); export const workflowRunStateStepInfoSchema = z.object({ result: z.any().optional(), @@ -421,6 +457,7 @@ export const workflowRunStateStepInfoSchema = z.object({ }); export const workflowRunStateStepInfosSchema = z.record( + z.string(), workflowRunStateStepInfoSchema, ); @@ -441,17 +478,15 @@ export const workflowRunStatusSchema = z.enum([ 'ENQUEUED', ]); -export const workflowRunSchema = z - .object({ - __typename: z.literal('WorkflowRun'), - id: z.string(), - workflowVersionId: z.string(), - workflowId: z.string(), - state: workflowRunStateSchema.nullable(), - status: workflowRunStatusSchema, - createdAt: z.string(), - deletedAt: z.string().nullable(), - endedAt: z.string().nullable(), - name: z.string(), - }) - .passthrough(); +export const workflowRunSchema = z.looseObject({ + __typename: z.literal('WorkflowRun'), + id: z.string(), + workflowVersionId: z.string(), + workflowId: z.string(), + state: workflowRunStateSchema.nullable(), + status: workflowRunStatusSchema, + createdAt: z.string(), + deletedAt: z.string().nullable(), + endedAt: z.string().nullable(), + name: z.string(), +}); diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts index 3cfa89279e7..9b7639503b3 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts @@ -71,7 +71,7 @@ IMPORTANT: The tool schema provides comprehensive field descriptions, examples, - Error handling options This is the most efficient way for AI to create workflows as it handles all the complexity in one call.`, - parameters: createCompleteWorkflowSchema, + inputSchema: createCompleteWorkflowSchema, execute: async (parameters: { name: string; description?: string; @@ -160,7 +160,7 @@ This is the most efficient way for AI to create workflows as it handles all the tools.create_workflow_version_step = { description: 'Create a new step in a workflow version. This adds a step to the specified workflow version with the given configuration.', - parameters: createWorkflowVersionStepSchema, + inputSchema: createWorkflowVersionStepSchema, execute: async (parameters: CreateWorkflowVersionStepInput) => { try { return await this.workflowVersionStepService.createWorkflowVersionStep( @@ -182,7 +182,7 @@ This is the most efficient way for AI to create workflows as it handles all the tools.update_workflow_version_step = { description: 'Update an existing step in a workflow version. This modifies the step configuration.', - parameters: updateWorkflowVersionStepSchema, + inputSchema: updateWorkflowVersionStepSchema, execute: async (parameters: UpdateWorkflowVersionStepInput) => { try { return await this.workflowVersionStepService.updateWorkflowVersionStep( @@ -205,7 +205,7 @@ This is the most efficient way for AI to create workflows as it handles all the tools.delete_workflow_version_step = { description: 'Delete a step from a workflow version. This removes the step and updates the workflow structure.', - parameters: deleteWorkflowVersionStepSchema, + inputSchema: deleteWorkflowVersionStepSchema, execute: async (parameters: { workflowVersionId: string; stepId: string; @@ -231,7 +231,7 @@ This is the most efficient way for AI to create workflows as it handles all the tools.create_workflow_version_edge = { description: 'Create a connection (edge) between two workflow steps. This defines the flow between steps.', - parameters: createWorkflowVersionEdgeSchema, + inputSchema: createWorkflowVersionEdgeSchema, execute: async (parameters: { workflowVersionId: string; source: string; @@ -258,7 +258,7 @@ This is the most efficient way for AI to create workflows as it handles all the tools.delete_workflow_version_edge = { description: 'Delete a connection (edge) between workflow steps.', - parameters: deleteWorkflowVersionEdgeSchema, + inputSchema: deleteWorkflowVersionEdgeSchema, execute: async (parameters: { workflowVersionId: string; source: string; @@ -286,7 +286,7 @@ This is the most efficient way for AI to create workflows as it handles all the tools.create_draft_from_workflow_version = { description: 'Create a new draft workflow version from an existing one. This allows for iterative workflow development.', - parameters: createDraftFromWorkflowVersionSchema, + inputSchema: createDraftFromWorkflowVersionSchema, execute: async (parameters: { workflowId: string; workflowVersionIdToCopy: string; @@ -312,7 +312,7 @@ This is the most efficient way for AI to create workflows as it handles all the tools.update_workflow_version_positions = { description: 'Update the positions of multiple workflow steps. This is useful for reorganizing the workflow layout.', - parameters: updateWorkflowVersionPositionsSchema, + inputSchema: updateWorkflowVersionPositionsSchema, execute: async (parameters: UpdateWorkflowVersionPositionsInput) => { try { return await this.workflowVersionService.updateWorkflowVersionPositions( @@ -335,7 +335,7 @@ This is the most efficient way for AI to create workflows as it handles all the tools.activate_workflow_version = { description: 'Activate a workflow version. This makes the workflow version active and available for execution.', - parameters: activateWorkflowVersionSchema, + inputSchema: activateWorkflowVersionSchema, execute: async (parameters: { workflowVersionId: string }) => { try { return await this.workflowTriggerService.activateWorkflowVersion( @@ -354,7 +354,7 @@ This is the most efficient way for AI to create workflows as it handles all the tools.deactivate_workflow_version = { description: 'Deactivate a workflow version. This makes the workflow version inactive and unavailable for execution.', - parameters: deactivateWorkflowVersionSchema, + inputSchema: deactivateWorkflowVersionSchema, execute: async (parameters: { workflowVersionId: string }) => { try { return await this.workflowTriggerService.deactivateWorkflowVersion( @@ -373,7 +373,7 @@ This is the most efficient way for AI to create workflows as it handles all the tools.compute_step_output_schema = { description: 'Compute the output schema for a workflow step. This determines what data the step produces. The step parameter must be a valid WorkflowTrigger or WorkflowAction with the correct settings structure for its type.', - parameters: computeStepOutputSchemaSchema, + inputSchema: computeStepOutputSchemaSchema, execute: async (parameters: { step: WorkflowTrigger | WorkflowAction; }) => { diff --git a/packages/twenty-server/test/integration/metadata/suites/agent/utils/agent-tool-test-utils.ts b/packages/twenty-server/test/integration/metadata/suites/agent/utils/agent-tool-test-utils.ts index dd1dbd51c4c..99055b7d590 100644 --- a/packages/twenty-server/test/integration/metadata/suites/agent/utils/agent-tool-test-utils.ts +++ b/packages/twenty-server/test/integration/metadata/suites/agent/utils/agent-tool-test-utils.ts @@ -109,7 +109,7 @@ export const createAgentToolTestModule = provide: SendEmailTool, useValue: { description: 'mock', - parameters: {}, + inputSchema: {}, execute: jest.fn(), }, }, diff --git a/packages/twenty-shared/package.json b/packages/twenty-shared/package.json index 2c9d9e5612c..8e2d1f9af48 100644 --- a/packages/twenty-shared/package.json +++ b/packages/twenty-shared/package.json @@ -38,6 +38,11 @@ "import": "./dist/index.mjs", "require": "./dist/index.cjs" }, + "./ai": { + "types": "./dist/ai/index.d.ts", + "import": "./dist/ai.mjs", + "require": "./dist/ai.cjs" + }, "./constants": { "types": "./dist/constants/index.d.ts", "import": "./dist/constants.mjs", @@ -76,6 +81,7 @@ }, "files": [ "dist", + "ai", "constants", "testing", "translations", @@ -86,6 +92,9 @@ ], "typesVersions": { "*": { + "ai": [ + "dist/ai/index.d.ts" + ], "constants": [ "dist/constants/index.d.ts" ], diff --git a/packages/twenty-shared/project.json b/packages/twenty-shared/project.json index 712678500b6..931647642bf 100644 --- a/packages/twenty-shared/project.json +++ b/packages/twenty-shared/project.json @@ -14,6 +14,8 @@ ], "outputs": [ "{projectRoot}/dist", + "{projectRoot}/ai/package.json", + "{projectRoot}/ai/dist", "{projectRoot}/constants/package.json", "{projectRoot}/constants/dist", "{projectRoot}/testing/package.json", diff --git a/packages/twenty-shared/src/ai/index.ts b/packages/twenty-shared/src/ai/index.ts new file mode 100644 index 00000000000..2a2fbcf659a --- /dev/null +++ b/packages/twenty-shared/src/ai/index.ts @@ -0,0 +1,19 @@ +/* + * _____ _ + *|_ _|_ _____ _ __ | |_ _ _ + * | | \ \ /\ / / _ \ '_ \| __| | | | Auto-generated file + * | | \ V V / __/ | | | |_| |_| | Any edits to this will be overridden + * |_| \_/\_/ \___|_| |_|\__|\__, | + * |___/ + */ + +export type { ErrorEvent } from './types/ErrorEvent'; +export type { ReasoningDeltaEvent } from './types/ReasoningDeltaEvent'; +export type { StreamEvent } from './types/StreamEvent'; +export type { TextBlock } from './types/TextBlock'; +export type { TextDeltaEvent } from './types/TextDeltaEvent'; +export type { ToolCallEvent } from './types/ToolCallEvent'; +export type { ToolEvent } from './types/ToolEvent'; +export type { ToolResultEvent } from './types/ToolResultEvent'; +export { parseStreamLine } from './utils/parseStreamLine'; +export { splitStreamIntoLines } from './utils/splitStreamIntoLines'; diff --git a/packages/twenty-shared/src/ai/types/ErrorEvent.ts b/packages/twenty-shared/src/ai/types/ErrorEvent.ts new file mode 100644 index 00000000000..6c95b025d78 --- /dev/null +++ b/packages/twenty-shared/src/ai/types/ErrorEvent.ts @@ -0,0 +1,5 @@ +export type ErrorEvent = { + type: 'error'; + message: string; + error?: unknown; +}; diff --git a/packages/twenty-shared/src/ai/types/ReasoningDeltaEvent.ts b/packages/twenty-shared/src/ai/types/ReasoningDeltaEvent.ts new file mode 100644 index 00000000000..e47257c93a7 --- /dev/null +++ b/packages/twenty-shared/src/ai/types/ReasoningDeltaEvent.ts @@ -0,0 +1,4 @@ +export type ReasoningDeltaEvent = { + type: 'reasoning-delta'; + text: string; +}; diff --git a/packages/twenty-shared/src/ai/types/StreamEvent.ts b/packages/twenty-shared/src/ai/types/StreamEvent.ts new file mode 100644 index 00000000000..b0c165d569e --- /dev/null +++ b/packages/twenty-shared/src/ai/types/StreamEvent.ts @@ -0,0 +1,21 @@ +import type { ErrorEvent } from './ErrorEvent'; +import type { ReasoningDeltaEvent } from './ReasoningDeltaEvent'; +import type { TextDeltaEvent } from './TextDeltaEvent'; +import type { ToolCallEvent } from './ToolCallEvent'; +import type { ToolResultEvent } from './ToolResultEvent'; + +export type StreamEvent = + | ToolCallEvent + | ToolResultEvent + | { + type: 'reasoning-start'; + } + | ReasoningDeltaEvent + | { + type: 'reasoning-end'; + } + | TextDeltaEvent + | { + type: 'step-finish'; + } + | ErrorEvent; diff --git a/packages/twenty-shared/src/ai/types/TextBlock.ts b/packages/twenty-shared/src/ai/types/TextBlock.ts new file mode 100644 index 00000000000..33557fc7ec7 --- /dev/null +++ b/packages/twenty-shared/src/ai/types/TextBlock.ts @@ -0,0 +1,4 @@ +export type TextBlock = + | { type: 'reasoning'; content: string; isThinking: boolean } + | { type: 'text'; content: string } + | null; diff --git a/packages/twenty-shared/src/ai/types/TextDeltaEvent.ts b/packages/twenty-shared/src/ai/types/TextDeltaEvent.ts new file mode 100644 index 00000000000..63a718989af --- /dev/null +++ b/packages/twenty-shared/src/ai/types/TextDeltaEvent.ts @@ -0,0 +1,4 @@ +export type TextDeltaEvent = { + type: 'text-delta'; + text: string; +}; diff --git a/packages/twenty-shared/src/ai/types/ToolCallEvent.ts b/packages/twenty-shared/src/ai/types/ToolCallEvent.ts new file mode 100644 index 00000000000..2562ea9029c --- /dev/null +++ b/packages/twenty-shared/src/ai/types/ToolCallEvent.ts @@ -0,0 +1,9 @@ +export type ToolCallEvent = { + type: 'tool-call'; + toolCallId: string; + toolName: string; + input: { + loadingMessage: string; + input: unknown; + }; +}; diff --git a/packages/twenty-shared/src/ai/types/ToolEvent.ts b/packages/twenty-shared/src/ai/types/ToolEvent.ts new file mode 100644 index 00000000000..879c26c2865 --- /dev/null +++ b/packages/twenty-shared/src/ai/types/ToolEvent.ts @@ -0,0 +1,4 @@ +import type { ToolCallEvent } from './ToolCallEvent'; +import type { ToolResultEvent } from './ToolResultEvent'; + +export type ToolEvent = ToolCallEvent | ToolResultEvent; diff --git a/packages/twenty-shared/src/ai/types/ToolResultEvent.ts b/packages/twenty-shared/src/ai/types/ToolResultEvent.ts new file mode 100644 index 00000000000..d5f8772ff03 --- /dev/null +++ b/packages/twenty-shared/src/ai/types/ToolResultEvent.ts @@ -0,0 +1,11 @@ +export type ToolResultEvent = { + type: 'tool-result'; + toolCallId: string; + toolName: string; + output: { + success: boolean; + result?: unknown; + error?: string; + message: string; + }; +}; diff --git a/packages/twenty-shared/src/ai/utils/parseStreamLine.ts b/packages/twenty-shared/src/ai/utils/parseStreamLine.ts new file mode 100644 index 00000000000..e94aee10731 --- /dev/null +++ b/packages/twenty-shared/src/ai/utils/parseStreamLine.ts @@ -0,0 +1,9 @@ +import type { StreamEvent } from '../types/StreamEvent'; + +export const parseStreamLine = (line: string): StreamEvent | null => { + try { + return JSON.parse(line) as StreamEvent; + } catch { + return null; + } +}; diff --git a/packages/twenty-shared/src/ai/utils/splitStreamIntoLines.ts b/packages/twenty-shared/src/ai/utils/splitStreamIntoLines.ts new file mode 100644 index 00000000000..845f3fe60f0 --- /dev/null +++ b/packages/twenty-shared/src/ai/utils/splitStreamIntoLines.ts @@ -0,0 +1,3 @@ +export const splitStreamIntoLines = (streamText: string): string[] => { + return streamText.trim().split('\n'); +}; diff --git a/packages/twenty-shared/src/workflow/index.ts b/packages/twenty-shared/src/workflow/index.ts index e1d5a23b007..7ce7f6b0b29 100644 --- a/packages/twenty-shared/src/workflow/index.ts +++ b/packages/twenty-shared/src/workflow/index.ts @@ -9,48 +9,6 @@ export { CONTENT_TYPE_VALUES_HTTP_REQUEST } from './constants/contentTypeValuesHttpRequest'; export { TRIGGER_STEP_ID } from './constants/TriggerStepId'; -export { - objectRecordSchema, - baseWorkflowActionSettingsSchema, - baseWorkflowActionSchema, - baseTriggerSchema, - workflowCodeActionSettingsSchema, - workflowSendEmailActionSettingsSchema, - workflowCreateRecordActionSettingsSchema, - workflowUpdateRecordActionSettingsSchema, - workflowDeleteRecordActionSettingsSchema, - workflowFindRecordsActionSettingsSchema, - workflowFormActionSettingsSchema, - workflowHttpRequestActionSettingsSchema, - workflowAiAgentActionSettingsSchema, - workflowFilterActionSettingsSchema, - workflowIteratorActionSettingsSchema, - workflowEmptyActionSettingsSchema, - workflowCodeActionSchema, - workflowSendEmailActionSchema, - workflowCreateRecordActionSchema, - workflowUpdateRecordActionSchema, - workflowDeleteRecordActionSchema, - workflowFindRecordsActionSchema, - workflowFormActionSchema, - workflowHttpRequestActionSchema, - workflowAiAgentActionSchema, - workflowFilterActionSchema, - workflowIteratorActionSchema, - workflowEmptyActionSchema, - workflowActionSchema, - workflowDatabaseEventTriggerSchema, - workflowManualTriggerSchema, - workflowCronTriggerSchema, - workflowWebhookTriggerSchema, - workflowTriggerSchema, - workflowRunStepStatusSchema, - workflowRunStateStepInfoSchema, - workflowRunStateStepInfosSchema, - workflowRunStateSchema, - workflowRunStatusSchema, - workflowRunSchema, -} from './schemas/workflow.schema'; export type { BodyType } from './types/workflowHttpRequestStep'; export type { WorkflowRunStepInfo, diff --git a/yarn.lock b/yarn.lock index d2e62c9e3f8..2a1b76af982 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24,118 +24,86 @@ __metadata: languageName: node linkType: hard -"@ai-sdk/anthropic@npm:^1.2.12": - version: 1.2.12 - resolution: "@ai-sdk/anthropic@npm:1.2.12" +"@ai-sdk/anthropic@npm:^2.0.17": + version: 2.0.17 + resolution: "@ai-sdk/anthropic@npm:2.0.17" dependencies: - "@ai-sdk/provider": "npm:1.1.3" - "@ai-sdk/provider-utils": "npm:2.2.8" + "@ai-sdk/provider": "npm:2.0.0" + "@ai-sdk/provider-utils": "npm:3.0.9" peerDependencies: - zod: ^3.0.0 - checksum: 10c0/da13e1ed3c03efe207dbb0fd5fe9f399e4119e6687ec1096418a33a7eeea3c5f912a51c74b185bba3c203b15ee0c1b9cdf649711815ff8e769e31af266ac00fb + zod: ^3.25.76 || ^4 + checksum: 10c0/783b6a953f3854c4303ad7c30dd56d4706486c7d1151adb17071d87933418c59c26bce53d5c26d34c4d4728eaac4a856ce49a336caed26a7216f982fea562814 languageName: node linkType: hard -"@ai-sdk/openai-compatible@npm:0.2.13": - version: 0.2.13 - resolution: "@ai-sdk/openai-compatible@npm:0.2.13" +"@ai-sdk/gateway@npm:1.0.23": + version: 1.0.23 + resolution: "@ai-sdk/gateway@npm:1.0.23" dependencies: - "@ai-sdk/provider": "npm:1.1.3" - "@ai-sdk/provider-utils": "npm:2.2.7" + "@ai-sdk/provider": "npm:2.0.0" + "@ai-sdk/provider-utils": "npm:3.0.9" peerDependencies: - zod: ^3.0.0 - checksum: 10c0/d06d46c319f5b846339aa3ba6144e27791fba4eaa7ba47a758cec52b0c9e578fae61a2ab5cc1f52adaf010a36b3c9309ef46c8ba5de77bea01d15d2d4eb7c27a + zod: ^3.25.76 || ^4 + checksum: 10c0/b1e1a6ab63b9191075eed92c586cd927696f8997ad24f056585aee3f5fffd283d981aa6b071a2560ecda4295445b80a4cfd321fa63c06e7ac54a06bc4c84887f languageName: node linkType: hard -"@ai-sdk/openai@npm:^1.3.22": - version: 1.3.22 - resolution: "@ai-sdk/openai@npm:1.3.22" +"@ai-sdk/openai-compatible@npm:1.0.18": + version: 1.0.18 + resolution: "@ai-sdk/openai-compatible@npm:1.0.18" dependencies: - "@ai-sdk/provider": "npm:1.1.3" - "@ai-sdk/provider-utils": "npm:2.2.8" + "@ai-sdk/provider": "npm:2.0.0" + "@ai-sdk/provider-utils": "npm:3.0.9" peerDependencies: - zod: ^3.0.0 - checksum: 10c0/bcc73a84bebd15aa54568c3c77cedd5f999e282c5be180d5e28ebc789f8873dd0a74d87f1ec4a0f16e3e61b658c3b0734835daf176ed910966246db73c72b468 + zod: ^3.25.76 || ^4 + checksum: 10c0/6fa84f6f1be07d13b1875c1ab3b009d0e659423dc9fc1c140bd0b3b99457356173c3b2d3289c535ac1abb542f7d56ff0a8538561a3e78c4b0bfa2de9d8a3e81c languageName: node linkType: hard -"@ai-sdk/provider-utils@npm:2.2.7": - version: 2.2.7 - resolution: "@ai-sdk/provider-utils@npm:2.2.7" +"@ai-sdk/openai@npm:^2.0.30": + version: 2.0.30 + resolution: "@ai-sdk/openai@npm:2.0.30" dependencies: - "@ai-sdk/provider": "npm:1.1.3" - nanoid: "npm:^3.3.8" - secure-json-parse: "npm:^2.7.0" + "@ai-sdk/provider": "npm:2.0.0" + "@ai-sdk/provider-utils": "npm:3.0.9" peerDependencies: - zod: ^3.23.8 - checksum: 10c0/e89a4e03be59df56bfb15e25e761955ffabd39b350527dd3e27da89c35332d1db6eeffc596d2aa3e18a2f5535d79e8ddc4ad7066d6f05f490f7d10082f427f00 + zod: ^3.25.76 || ^4 + checksum: 10c0/90a57c1b10dac46c0bbe7e16cf9202557fb250d9f0e94a2a5fb7d95b5ea77815a56add78b00238d3823f0313c9b2c42abe865478d28a6196f72b341d32dd40af languageName: node linkType: hard -"@ai-sdk/provider-utils@npm:2.2.8": - version: 2.2.8 - resolution: "@ai-sdk/provider-utils@npm:2.2.8" +"@ai-sdk/provider-utils@npm:3.0.9, @ai-sdk/provider-utils@npm:^3.0.9": + version: 3.0.9 + resolution: "@ai-sdk/provider-utils@npm:3.0.9" dependencies: - "@ai-sdk/provider": "npm:1.1.3" - nanoid: "npm:^3.3.8" - secure-json-parse: "npm:^2.7.0" + "@ai-sdk/provider": "npm:2.0.0" + "@standard-schema/spec": "npm:^1.0.0" + eventsource-parser: "npm:^3.0.5" peerDependencies: - zod: ^3.23.8 - checksum: 10c0/34c72bf5f23f2d3e7aef496da7099422ba3b3ff243c35511853e16c3f1528717500262eea32b19e3e09bc4452152a5f31e650512f53f08a5f5645d907bff429e + zod: ^3.25.76 || ^4 + checksum: 10c0/f8b659343d7e22ae099f7b6fc514591c0408012eb0aa00f7a912798b6d7d7305cafa8f18a07c7adec0bb5d39d9b6256b76d65c5393c3fc843d1361c52f1f8080 languageName: node linkType: hard -"@ai-sdk/provider@npm:1.1.3": - version: 1.1.3 - resolution: "@ai-sdk/provider@npm:1.1.3" +"@ai-sdk/provider@npm:2.0.0": + version: 2.0.0 + resolution: "@ai-sdk/provider@npm:2.0.0" dependencies: json-schema: "npm:^0.4.0" - checksum: 10c0/40e080e223328e7c89829865e9c48f4ce8442a6a59f7ed5dfbdb4f63e8d859a76641e2d31e91970dd389bddb910f32ec7c3dbb0ce583c119e5a1e614ea7b8bc4 + checksum: 10c0/e50e520016c9fc0a8b5009cadd47dae2f1c81ec05c1792b9e312d7d15479f024ca8039525813a33425c884e3449019fed21043b1bfabd6a2626152ca9a388199 languageName: node linkType: hard -"@ai-sdk/react@npm:1.2.12": - version: 1.2.12 - resolution: "@ai-sdk/react@npm:1.2.12" +"@ai-sdk/xai@npm:^2.0.19": + version: 2.0.19 + resolution: "@ai-sdk/xai@npm:2.0.19" dependencies: - "@ai-sdk/provider-utils": "npm:2.2.8" - "@ai-sdk/ui-utils": "npm:1.2.11" - swr: "npm:^2.2.5" - throttleit: "npm:2.1.0" + "@ai-sdk/openai-compatible": "npm:1.0.18" + "@ai-sdk/provider": "npm:2.0.0" + "@ai-sdk/provider-utils": "npm:3.0.9" peerDependencies: - react: ^18 || ^19 || ^19.0.0-rc - zod: ^3.23.8 - peerDependenciesMeta: - zod: - optional: true - checksum: 10c0/5422feb4ffeebd3287441cf658733e9ad7f9081fc279e85f57700d7fe9f4ed8a0504789c1be695790df44b28730e525cf12acf0f52bfa5adecc561ffd00cb2a5 - languageName: node - linkType: hard - -"@ai-sdk/ui-utils@npm:1.2.11": - version: 1.2.11 - resolution: "@ai-sdk/ui-utils@npm:1.2.11" - dependencies: - "@ai-sdk/provider": "npm:1.1.3" - "@ai-sdk/provider-utils": "npm:2.2.8" - zod-to-json-schema: "npm:^3.24.1" - peerDependencies: - zod: ^3.23.8 - checksum: 10c0/de0a10f9e16010126a21a1690aaf56d545b9c0f8d8b2cc33ffd22c2bb2e914949acb9b3f86e0e39a0e4b0d4f24db12e2b094045e34b311de0c8f84bfab48cc92 - languageName: node - linkType: hard - -"@ai-sdk/xai@npm:1.2.15": - version: 1.2.15 - resolution: "@ai-sdk/xai@npm:1.2.15" - dependencies: - "@ai-sdk/openai-compatible": "npm:0.2.13" - "@ai-sdk/provider": "npm:1.1.3" - "@ai-sdk/provider-utils": "npm:2.2.7" - peerDependencies: - zod: ^3.0.0 - checksum: 10c0/60bd4af83dd90e4b6b3a4149633c77bcfb11e976013f86fd3383f8f23f5fb3afa1d790607fefc34d6a33cfd80fd9680b5b01366a6016070792ab8055b204a55c + zod: ^3.25.76 || ^4 + checksum: 10c0/3a9aa297c98ea3f6a7305ab9c37c68a1e661f4358ef14668a82db27437334aa01aa632ebf656cd1770c221860ab6153a86363301387fab4acedeca10b6b8026e languageName: node linkType: hard @@ -17967,6 +17935,13 @@ __metadata: languageName: node linkType: hard +"@standard-schema/spec@npm:^1.0.0": + version: 1.0.0 + resolution: "@standard-schema/spec@npm:1.0.0" + checksum: 10c0/a1ab9a8bdc09b5b47aa8365d0e0ec40cc2df6437be02853696a0e377321653b0d3ac6f079a8c67d5ddbe9821025584b1fb71d9cc041a6666a96f1fadf2ece15f + languageName: node + linkType: hard + "@stitches/core@npm:^1.2.6": version: 1.2.8 resolution: "@stitches/core@npm:1.2.8" @@ -20037,13 +20012,6 @@ __metadata: languageName: node linkType: hard -"@types/diff-match-patch@npm:^1.0.36": - version: 1.0.36 - resolution: "@types/diff-match-patch@npm:1.0.36" - checksum: 10c0/0bad011ab138baa8bde94e7815064bb881f010452463272644ddbbb0590659cb93f7aa2776ff442c6721d70f202839e1053f8aa62d801cc4166f7a3ea9130055 - languageName: node - linkType: hard - "@types/doctrine@npm:^0.0.9": version: 0.0.9 resolution: "@types/doctrine@npm:0.0.9" @@ -23026,23 +22994,17 @@ __metadata: languageName: node linkType: hard -"ai@npm:^4.3.16": - version: 4.3.16 - resolution: "ai@npm:4.3.16" +"ai@npm:^5.0.44": + version: 5.0.44 + resolution: "ai@npm:5.0.44" dependencies: - "@ai-sdk/provider": "npm:1.1.3" - "@ai-sdk/provider-utils": "npm:2.2.8" - "@ai-sdk/react": "npm:1.2.12" - "@ai-sdk/ui-utils": "npm:1.2.11" + "@ai-sdk/gateway": "npm:1.0.23" + "@ai-sdk/provider": "npm:2.0.0" + "@ai-sdk/provider-utils": "npm:3.0.9" "@opentelemetry/api": "npm:1.9.0" - jsondiffpatch: "npm:0.6.0" peerDependencies: - react: ^18 || ^19 || ^19.0.0-rc - zod: ^3.23.8 - peerDependenciesMeta: - react: - optional: true - checksum: 10c0/befe761c9386cda6de33370a2590900352b444d81959255c624e2bfd40765f126d29269f0ef3e00bde07daf237004aa0b66d0b253664aa478c148e923ce78c41 + zod: ^3.25.76 || ^4 + checksum: 10c0/528c7e165f75715194204051ce0aa341d8dca7d5536c2abcf3df83ccda7399ed5d91deaa45a81340f93d2461b1c2fc5f740f7804dfd396927c71b0667403569b languageName: node linkType: hard @@ -29354,13 +29316,6 @@ __metadata: languageName: node linkType: hard -"diff-match-patch@npm:^1.0.5": - version: 1.0.5 - resolution: "diff-match-patch@npm:1.0.5" - checksum: 10c0/142b6fad627b9ef309d11bd935e82b84c814165a02500f046e2773f4ea894d10ed3017ac20454900d79d4a0322079f5b713cf0986aaf15fce0ec4a2479980c86 - languageName: node - linkType: hard - "diff-sequences@npm:^29.6.3": version: 29.6.3 resolution: "diff-sequences@npm:29.6.3" @@ -31600,6 +31555,13 @@ __metadata: languageName: node linkType: hard +"eventsource-parser@npm:^3.0.5": + version: 3.0.6 + resolution: "eventsource-parser@npm:3.0.6" + checksum: 10c0/70b8ccec7dac767ef2eca43f355e0979e70415701691382a042a2df8d6a68da6c2fca35363669821f3da876d29c02abe9b232964637c1b6635c940df05ada78a + languageName: node + linkType: hard + "evp_bytestokey@npm:^1.0.0, evp_bytestokey@npm:^1.0.3": version: 1.0.3 resolution: "evp_bytestokey@npm:1.0.3" @@ -38370,19 +38332,6 @@ __metadata: languageName: node linkType: hard -"jsondiffpatch@npm:0.6.0": - version: 0.6.0 - resolution: "jsondiffpatch@npm:0.6.0" - dependencies: - "@types/diff-match-patch": "npm:^1.0.36" - chalk: "npm:^5.3.0" - diff-match-patch: "npm:^1.0.5" - bin: - jsondiffpatch: bin/jsondiffpatch.js - checksum: 10c0/f7822e48a8ef8b9f7c6024cc59b7d3707a9fe6d84fd776d169de5a1803ad551ffe7cfdc7587f3900f224bc70897355884ed43eb1c8ccd02e7f7b43a7ebcfed4f - languageName: node - linkType: hard - "jsonfile@npm:^4.0.0": version: 4.0.0 resolution: "jsonfile@npm:4.0.0" @@ -42296,7 +42245,7 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:^3.3.11, nanoid@npm:^3.3.6, nanoid@npm:^3.3.7, nanoid@npm:^3.3.8": +"nanoid@npm:^3.3.11, nanoid@npm:^3.3.6, nanoid@npm:^3.3.7": version: 3.3.11 resolution: "nanoid@npm:3.3.11" bin: @@ -48408,13 +48357,6 @@ __metadata: languageName: node linkType: hard -"secure-json-parse@npm:^2.7.0": - version: 2.7.0 - resolution: "secure-json-parse@npm:2.7.0" - checksum: 10c0/f57eb6a44a38a3eeaf3548228585d769d788f59007454214fab9ed7f01fbf2e0f1929111da6db28cf0bcc1a2e89db5219a59e83eeaec3a54e413a0197ce879e4 - languageName: node - linkType: hard - "selderee@npm:^0.11.0": version: 0.11.0 resolution: "selderee@npm:0.11.0" @@ -50510,18 +50452,6 @@ __metadata: languageName: node linkType: hard -"swr@npm:^2.2.5": - version: 2.3.3 - resolution: "swr@npm:2.3.3" - dependencies: - dequal: "npm:^2.0.3" - use-sync-external-store: "npm:^1.4.0" - peerDependencies: - react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 10c0/882fc8291912860e0c50eae3470ebf0cd58b0144cb12adcc4b14c5cef913ea06479043830508d8b0b3d4061d99ad8dd52485c9c879fbd4e9b893484e6d8da9e3 - languageName: node - linkType: hard - "symbol-observable@npm:4.0.0, symbol-observable@npm:^4.0.0": version: 4.0.0 resolution: "symbol-observable@npm:4.0.0" @@ -50772,13 +50702,6 @@ __metadata: languageName: node linkType: hard -"throttleit@npm:2.1.0": - version: 2.1.0 - resolution: "throttleit@npm:2.1.0" - checksum: 10c0/1696ae849522cea6ba4f4f3beac1f6655d335e51b42d99215e196a718adced0069e48deaaf77f7e89f526ab31de5b5c91016027da182438e6f9280be2f3d5265 - languageName: node - linkType: hard - "through2@npm:4.0.2, through2@npm:^4.0.2": version: 4.0.2 resolution: "through2@npm:4.0.2" @@ -51760,9 +51683,10 @@ __metadata: version: 0.0.0-use.local resolution: "twenty-server@workspace:packages/twenty-server" dependencies: - "@ai-sdk/anthropic": "npm:^1.2.12" - "@ai-sdk/openai": "npm:^1.3.22" - "@ai-sdk/xai": "npm:1.2.15" + "@ai-sdk/anthropic": "npm:^2.0.17" + "@ai-sdk/openai": "npm:^2.0.30" + "@ai-sdk/provider-utils": "npm:^3.0.9" + "@ai-sdk/xai": "npm:^2.0.19" "@aws-sdk/client-lambda": "npm:3.825.0" "@aws-sdk/client-s3": "npm:3.825.0" "@aws-sdk/client-sts": "npm:3.825.0" @@ -51860,7 +51784,7 @@ __metadata: "@types/unzipper": "npm:^0" "@yarnpkg/types": "npm:^4.0.0" addressparser: "npm:1.0.1" - ai: "npm:^4.3.16" + ai: "npm:^5.0.44" apollo-server-core: "npm:3.13.0" archiver: "npm:7.0.1" axios: "npm:1.10.0" @@ -51972,7 +51896,7 @@ __metadata: unzipper: "npm:^0.12.3" uuid: "npm:9.0.1" vite-tsconfig-paths: "npm:4.3.2" - zod: "npm:3.23.8" + zod: "npm:^4.1.11" zod-to-json-schema: "npm:^3.23.1" languageName: unknown linkType: soft @@ -55484,7 +55408,7 @@ __metadata: languageName: node linkType: hard -"zod-to-json-schema@npm:^3.23.1, zod-to-json-schema@npm:^3.24.1": +"zod-to-json-schema@npm:^3.23.1": version: 3.24.5 resolution: "zod-to-json-schema@npm:3.24.5" peerDependencies: @@ -55507,6 +55431,13 @@ __metadata: languageName: node linkType: hard +"zod@npm:^4.1.11": + version: 4.1.11 + resolution: "zod@npm:4.1.11" + checksum: 10c0/ce6a4c4acfbf51d7dd0f2669c82f207d62a1f00264eef608994b94eb99d86a74c99f59b0dd3e61ef82909ee136631378b709e0908f0a02a2d5c21d0c497de5db + languageName: node + linkType: hard + "zustand@npm:^4.4.0": version: 4.5.4 resolution: "zustand@npm:4.5.4"