From 216d72b5d7d512ac9be705fa1a323c6d8013a05e Mon Sep 17 00:00:00 2001 From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com> Date: Tue, 23 Sep 2025 01:43:43 +0530 Subject: [PATCH] AI SDK v5 migration (#14549) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Félix Malfait --- .github/workflows/ci-breaking-changes.yaml | 9 +- .../AIChatAssistantMessageRenderer.tsx | 19 +- .../ai/components/ErrorStepRenderer.tsx | 3 +- .../ai/components/ToolStepRenderer.tsx | 52 +-- .../src/modules/ai/types/ParsedStep.ts | 7 + .../src/modules/ai/types/streamTypes.ts | 36 -- .../ai/utils/__tests__/parseStream.test.ts | 77 ++-- .../modules/ai/utils/extractErrorMessage.ts | 64 ++- .../ai/utils/hasStructuredStreamData.ts | 14 + .../src/modules/ai/utils/parseStream.ts | 229 ++++++---- .../meta-types/utils/orderWorkflowRunState.ts | 2 +- .../modules/workflow/hooks/useWorkflowRun.ts | 2 +- .../src/modules/workflow/types/Workflow.ts | 2 +- .../validation-schemas/workflowSchema.ts | 390 ++++++++++++++++++ .../useWorkflowActionHeader.test.tsx | 6 +- packages/twenty-server/package.json | 11 +- .../ai/controllers/ai.controller.spec.ts | 46 ++- .../ai/controllers/ai.controller.ts | 23 +- .../__tests__/ai-billing.service.spec.ts | 8 +- .../ai/services/__tests__/mcp.service.spec.ts | 22 +- .../__tests__/tool-adapter.service.spec.ts | 6 +- .../ai/services/ai-billing.service.ts | 19 +- .../ai/services/ai-model-registry.service.ts | 2 +- .../core-modules/ai/services/ai.service.ts | 10 +- .../core-modules/ai/services/mcp.service.ts | 4 +- .../ai/services/tool-adapter.service.ts | 2 +- .../core-modules/ai/services/tool.service.ts | 12 +- .../audit/services/audit.service.spec.ts | 3 +- .../audit/utils/events/common/base-schemas.ts | 14 +- .../object-event/object-record-created.ts | 2 +- .../object-event/object-record-delete.ts | 2 +- .../object-event/object-record-updated.ts | 2 +- .../custom-domain/custom-domain-activated.ts | 10 +- .../custom-domain-deactivated.ts | 10 +- .../workspace-event/monitoring/monitoring.ts | 22 +- .../serverless-function-executed.ts | 24 +- .../workspace-event/user/user-signup.ts | 10 +- .../webhook/webhook-response.ts | 24 +- .../workspace-entity-created.ts | 14 +- ...mtp-caldav-connection-validator.service.ts | 4 +- .../tool/services/tool-registry.service.ts | 2 +- .../tool/tools/http-tool/http-tool.schema.ts | 2 +- .../tool/tools/http-tool/http-tool.ts | 2 +- .../send-email-tool/send-email-tool.schema.ts | 3 +- .../tools/send-email-tool/send-email-tool.ts | 7 +- .../core-modules/tool/types/tool.type.ts | 5 +- .../totp/constants/totp.strategy.constants.ts | 37 +- .../strategies/otp/totp/totp.strategy.ts | 4 +- .../agent/agent-execution.service.ts | 31 +- .../agent/agent-handoff-executor.service.ts | 4 +- .../agent/agent-handoff-tool.service.ts | 2 +- .../agent/agent-streaming.service.ts | 19 +- .../constants/agent-handoff-schema.const.ts | 32 +- ...structAssistantMessageContentFromStream.ts | 96 +++-- .../services/blocklist-validation.service.ts | 4 +- .../services/ai-agent-executor.service.ts | 16 +- .../schemas/workflow-tool-schemas.ts | 6 +- .../schemas/workflow.schema.ts | 289 +++++++------ .../workflow-tool.workspace-service.ts | 22 +- .../agent/utils/agent-tool-test-utils.ts | 2 +- packages/twenty-shared/package.json | 9 + packages/twenty-shared/project.json | 2 + packages/twenty-shared/src/ai/index.ts | 19 + .../twenty-shared/src/ai/types/ErrorEvent.ts | 5 + .../src/ai/types/ReasoningDeltaEvent.ts | 4 + .../twenty-shared/src/ai/types/StreamEvent.ts | 21 + .../twenty-shared/src/ai/types/TextBlock.ts | 4 + .../src/ai/types/TextDeltaEvent.ts | 4 + .../src/ai/types/ToolCallEvent.ts | 9 + .../twenty-shared/src/ai/types/ToolEvent.ts | 4 + .../src/ai/types/ToolResultEvent.ts | 11 + .../src/ai/utils/parseStreamLine.ts | 9 + .../src/ai/utils/splitStreamIntoLines.ts | 3 + packages/twenty-shared/src/workflow/index.ts | 42 -- yarn.lock | 239 ++++------- 75 files changed, 1347 insertions(+), 841 deletions(-) create mode 100644 packages/twenty-front/src/modules/ai/types/ParsedStep.ts delete mode 100644 packages/twenty-front/src/modules/ai/types/streamTypes.ts create mode 100644 packages/twenty-front/src/modules/ai/utils/hasStructuredStreamData.ts create mode 100644 packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts rename packages/{twenty-shared/src/workflow => twenty-server/src/modules/workflow/workflow-tools}/schemas/workflow.schema.ts (61%) create mode 100644 packages/twenty-shared/src/ai/index.ts create mode 100644 packages/twenty-shared/src/ai/types/ErrorEvent.ts create mode 100644 packages/twenty-shared/src/ai/types/ReasoningDeltaEvent.ts create mode 100644 packages/twenty-shared/src/ai/types/StreamEvent.ts create mode 100644 packages/twenty-shared/src/ai/types/TextBlock.ts create mode 100644 packages/twenty-shared/src/ai/types/TextDeltaEvent.ts create mode 100644 packages/twenty-shared/src/ai/types/ToolCallEvent.ts create mode 100644 packages/twenty-shared/src/ai/types/ToolEvent.ts create mode 100644 packages/twenty-shared/src/ai/types/ToolResultEvent.ts create mode 100644 packages/twenty-shared/src/ai/utils/parseStreamLine.ts create mode 100644 packages/twenty-shared/src/ai/utils/splitStreamIntoLines.ts diff --git a/.github/workflows/ci-breaking-changes.yaml b/.github/workflows/ci-breaking-changes.yaml index fc6d4246eb..ba4c4b1b69 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 aa5c0129c1..27c47f753f 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 0ad53d105f..628c4a11b9 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 d1d868221b..036f7c66a8 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 0000000000..037d2af559 --- /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 2d580e999d..0000000000 --- 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 79ec1d28bc..26db8c1415 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 3e2c9de151..0693074314 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 0000000000..ccd61fe688 --- /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 6f66f93270..5b095e9998 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 71f22435a4..8d74e6cef8 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 cb0424c2d9..abf8293760 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 bc94c37b1f..4c9eb36b9b 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 0000000000..69e2a81e2b --- /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 93293eed01..3d83c4382b 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 11848be100..dc8ba1e168 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 e049083e40..b26afd8e57 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 80c001303c..a0efc1f629 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 969c3141f8..00fdcbd235 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 1ce4590038..be552b65a8 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 a2ea3d2050..51bc7bb26d 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 c569e0166d..b377748ba9 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 c775371f3a..9ecd75fe50 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 d6508aecd8..52d6c07002 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 264244afbb..3b3564296e 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 a99ac215b3..26a2545d23 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 bfccb6985c..150b77a40c 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 8a39b90b88..a60594ba42 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 e56283daf4..c4f113e4d2 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 00b077c551..5daccf50ad 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 9580986934..143c25c140 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 01c0f12bb5..c19d667a9e 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 15841e8395..1c236fa9ab 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 a5015f63d0..1a20aaf1f0 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 bc8f29cdf8..5ab0e96809 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 50b084c99b..202bba6ce7 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 7453bf2a0d..231c7a52c9 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 5806eda949..f0c4411424 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 5d921ab958..d9ed163849 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 cac0c24aaa..7408f5a832 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 f5fff4d3b9..e7d0ee3e5c 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 e3bbdfece1..0feb1e97cb 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 da6d4ab8d1..e010a34b05 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 d9ef21756e..438e7cd158 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 f09ef2e7f9..3aa0554ca9 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 44f6b9a15d..9880c284ed 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 be5293e7dc..a0d74ed20a 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 aa83a8dd95..d21fa98cb6 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 7d17a8dcba..c82437be79 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 423c8a97e6..f2c20db802 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 0bc32ed10f..2531cd733d 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 3e4eb18317..e462ed02e7 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 abc46db9ff..0213d6b862 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 3127e455f1..4a23e93b1a 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 bf2b6ce79f..821b85944e 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 2fc6d33794..9c67fbd226 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 3139481810..97951f4c1b 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 3298979f5d..9a6e9b2f19 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 3cfa89279e..9b7639503b 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 dd1dbd51c4..99055b7d59 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 2c9d9e5612..8e2d1f9af4 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 712678500b..931647642b 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 0000000000..2a2fbcf659 --- /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 0000000000..6c95b025d7 --- /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 0000000000..e47257c93a --- /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 0000000000..b0c165d569 --- /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 0000000000..33557fc7ec --- /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 0000000000..63a718989a --- /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 0000000000..2562ea9029 --- /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 0000000000..879c26c286 --- /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 0000000000..d5f8772ff0 --- /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 0000000000..e94aee1073 --- /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 0000000000..845f3fe60f --- /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 e1d5a23b00..7ce7f6b0b2 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 d2e62c9e3f..2a1b76af98 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"