mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
AI SDK v5 migration (#14549)
Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
parent
e8121919bd
commit
216d72b5d7
75 changed files with 1347 additions and 841 deletions
9
.github/workflows/ci-breaking-changes.yaml
vendored
9
.github/workflows/ci-breaking-changes.yaml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 <LoadingDotsIcon />;
|
||||
}
|
||||
|
||||
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 <LazyMarkdownRenderer text={streamData} />;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = ({
|
|||
<IconAlertCircle size={theme.icon.size.md} />
|
||||
</StyledIconContainer>
|
||||
<StyledContent>
|
||||
<StyledTitle>Error</StyledTitle>
|
||||
<StyledTitle>{t`Error`}</StyledTitle>
|
||||
<StyledMessage>{errorMessage}</StyledMessage>
|
||||
</StyledContent>
|
||||
</StyledContainer>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<StyledContainer>
|
||||
<StyledLoadingContainer>
|
||||
<ShimmeringText>
|
||||
<StyledDisplayMessage>
|
||||
{toolCall.args.loadingMessage}
|
||||
{toolCallEvent.input.loadingMessage}
|
||||
</StyledDisplayMessage>
|
||||
</ShimmeringText>
|
||||
</StyledLoadingContainer>
|
||||
|
|
@ -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 (
|
||||
<StyledContainer>
|
||||
|
|
@ -137,20 +132,7 @@ export const ToolStepRenderer = ({ events }: { events: ToolEvent[] }) => {
|
|||
{isExpandable && (
|
||||
<AnimatedExpandableContainer isExpanded={isExpanded}>
|
||||
<StyledContentContainer>
|
||||
{isStandardizedFormat ? (
|
||||
<>
|
||||
{hasError && <div>{extractErrorMessage(toolOutput.error)}</div>}
|
||||
{hasResult && (
|
||||
<div>
|
||||
<StyledPre>
|
||||
{JSON.stringify(toolOutput.result, null, 2)}
|
||||
</StyledPre>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : toolResult?.result ? (
|
||||
JSON.stringify(toolResult.result, null, 2)
|
||||
) : undefined}
|
||||
<StyledPre>{JSON.stringify(toolOutput, null, 2)}</StyledPre>
|
||||
</StyledContentContainer>
|
||||
</AnimatedExpandableContainer>
|
||||
)}
|
||||
|
|
|
|||
7
packages/twenty-front/src/modules/ai/types/ParsedStep.ts
Normal file
7
packages/twenty-front/src/modules/ai/types/ParsedStep.ts
Normal file
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<typeof workflowCodeActionSchema>;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -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', () => ({
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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<AiService>;
|
||||
let featureFlagService: jest.Mocked<FeatureFlagService>;
|
||||
let aiBillingService: jest.Mocked<AIBillingService>;
|
||||
let aiModelRegistryService: jest.Mocked<AiModelRegistryService>;
|
||||
|
||||
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');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ describe('AIBillingService', () => {
|
|||
let mockWorkspaceEventEmitter: jest.Mocked<WorkspaceEventEmitter>;
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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: {} }),
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<number> {
|
||||
async calculateCost(
|
||||
modelId: ModelId,
|
||||
usage: LanguageModelUsage,
|
||||
): Promise<number> {
|
||||
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<void> {
|
||||
const costInCents = await this.calculateCost(modelId, usage);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<
|
||||
|
|
|
|||
|
|
@ -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<
|
||||
|
|
|
|||
|
|
@ -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<
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<typeof monitoringSchema>;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<typeof userSignupSchema>;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<typeof webhookResponseSchema>;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(', ');
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<ToolOutput> {
|
||||
const { url, method, headers, body } = parameters as HttpRequestInput;
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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<unknown>;
|
||||
execute(input: ToolInput): Promise<ToolOutput>;
|
||||
flag?: PermissionFlagType;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -33,33 +33,28 @@ export type TOTPStrategyConfig = z.infer<typeof TOTP_STRATEGY_CONFIG_SCHEMA>;
|
|||
|
||||
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(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<unknown, TOTPStrategyConfig> | undefined;
|
||||
let result: ZodSafeParseResult<TOTPStrategyConfig> | undefined;
|
||||
|
||||
if (isDefined(options)) {
|
||||
result = TOTP_STRATEGY_CONFIG_SCHEMA.safeParse(options);
|
||||
|
|
|
|||
|
|
@ -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<CoreUserMessage> {
|
||||
): Promise<UserModelMessage> {
|
||||
const content: Exclude<UserContent, string> = [
|
||||
{
|
||||
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 = '';
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
}),
|
||||
]),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<TextPart | ReasoningPart> = [];
|
||||
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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
});
|
||||
|
|
@ -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;
|
||||
}) => {
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ export const createAgentToolTestModule =
|
|||
provide: SendEmailTool,
|
||||
useValue: {
|
||||
description: 'mock',
|
||||
parameters: {},
|
||||
inputSchema: {},
|
||||
execute: jest.fn(),
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
19
packages/twenty-shared/src/ai/index.ts
Normal file
19
packages/twenty-shared/src/ai/index.ts
Normal file
|
|
@ -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';
|
||||
5
packages/twenty-shared/src/ai/types/ErrorEvent.ts
Normal file
5
packages/twenty-shared/src/ai/types/ErrorEvent.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export type ErrorEvent = {
|
||||
type: 'error';
|
||||
message: string;
|
||||
error?: unknown;
|
||||
};
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export type ReasoningDeltaEvent = {
|
||||
type: 'reasoning-delta';
|
||||
text: string;
|
||||
};
|
||||
21
packages/twenty-shared/src/ai/types/StreamEvent.ts
Normal file
21
packages/twenty-shared/src/ai/types/StreamEvent.ts
Normal file
|
|
@ -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;
|
||||
4
packages/twenty-shared/src/ai/types/TextBlock.ts
Normal file
4
packages/twenty-shared/src/ai/types/TextBlock.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export type TextBlock =
|
||||
| { type: 'reasoning'; content: string; isThinking: boolean }
|
||||
| { type: 'text'; content: string }
|
||||
| null;
|
||||
4
packages/twenty-shared/src/ai/types/TextDeltaEvent.ts
Normal file
4
packages/twenty-shared/src/ai/types/TextDeltaEvent.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export type TextDeltaEvent = {
|
||||
type: 'text-delta';
|
||||
text: string;
|
||||
};
|
||||
9
packages/twenty-shared/src/ai/types/ToolCallEvent.ts
Normal file
9
packages/twenty-shared/src/ai/types/ToolCallEvent.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export type ToolCallEvent = {
|
||||
type: 'tool-call';
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
input: {
|
||||
loadingMessage: string;
|
||||
input: unknown;
|
||||
};
|
||||
};
|
||||
4
packages/twenty-shared/src/ai/types/ToolEvent.ts
Normal file
4
packages/twenty-shared/src/ai/types/ToolEvent.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import type { ToolCallEvent } from './ToolCallEvent';
|
||||
import type { ToolResultEvent } from './ToolResultEvent';
|
||||
|
||||
export type ToolEvent = ToolCallEvent | ToolResultEvent;
|
||||
11
packages/twenty-shared/src/ai/types/ToolResultEvent.ts
Normal file
11
packages/twenty-shared/src/ai/types/ToolResultEvent.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export type ToolResultEvent = {
|
||||
type: 'tool-result';
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
output: {
|
||||
success: boolean;
|
||||
result?: unknown;
|
||||
error?: string;
|
||||
message: string;
|
||||
};
|
||||
};
|
||||
9
packages/twenty-shared/src/ai/utils/parseStreamLine.ts
Normal file
9
packages/twenty-shared/src/ai/utils/parseStreamLine.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export const splitStreamIntoLines = (streamText: string): string[] => {
|
||||
return streamText.trim().split('\n');
|
||||
};
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
239
yarn.lock
239
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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue