AI SDK v5 migration (#14549)
Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
+13
-18
@@ -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 = '';
|
||||
|
||||
|
||||
+2
-2
@@ -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()
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+11
-8
@@ -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 },
|
||||
);
|
||||
|
||||
+10
-22
@@ -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()),
|
||||
}),
|
||||
]),
|
||||
),
|
||||
|
||||
+66
-30
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user