diff --git a/packages/instantsearch.js/src/lib/ai-lite/abstract-chat.ts b/packages/instantsearch.js/src/lib/ai-lite/abstract-chat.ts index d31307101e..8bfeff329d 100644 --- a/packages/instantsearch.js/src/lib/ai-lite/abstract-chat.ts +++ b/packages/instantsearch.js/src/lib/ai-lite/abstract-chat.ts @@ -11,7 +11,9 @@ import type { CreateUIMessage, FileUIPart, IdGenerator, + InferUIMessageChunk, InferUIMessageMetadata, + InferUIMessageToolCall, InferUIMessageTools, UIMessage, UIMessageChunk, @@ -21,9 +23,9 @@ import type { ChatOnDataCallback, } from './types'; -type ActiveResponse = { +type ActiveResponse = { abortController: AbortController; - stream?: ReadableStream; + stream?: ReadableStream; }; /** @@ -43,7 +45,9 @@ export abstract class AbstractChat { messages: TUIMessage[]; }) => boolean | PromiseLike; - private activeResponse: ActiveResponse | null = null; + private activeResponse: ActiveResponse< + InferUIMessageChunk + > | null = null; private jobExecutor = new SerialJobExecutor(); constructor({ @@ -423,7 +427,7 @@ export abstract class AbstractChat { } private processStreamWithCallbacks( - stream: ReadableStream + stream: ReadableStream> ): Promise { this.setStatus({ status: 'streaming' }); @@ -443,7 +447,7 @@ export abstract class AbstractChat { return new Promise((resolve) => { processStream( - stream, + stream as ReadableStream, // eslint-disable-next-line complexity (chunk) => { switch (chunk.type) { @@ -685,7 +689,8 @@ export abstract class AbstractChat { toolName: chunk.toolName, toolCallId: chunk.toolCallId, input: chunk.input, - } as any, + dynamic: 'dynamic' in chunk ? chunk.dynamic : undefined, + } as InferUIMessageToolCall, }); if (result && typeof result.then === 'function') { pendingToolCall = pendingToolCall.then(() => result); diff --git a/packages/instantsearch.js/src/lib/ai-lite/stream-parser.ts b/packages/instantsearch.js/src/lib/ai-lite/stream-parser.ts index 570536fb88..f4d9687756 100644 --- a/packages/instantsearch.js/src/lib/ai-lite/stream-parser.ts +++ b/packages/instantsearch.js/src/lib/ai-lite/stream-parser.ts @@ -11,13 +11,13 @@ import type { UIMessageChunk } from './types'; * @param stream - The input stream of raw bytes * @returns A ReadableStream of parsed UIMessageChunk events */ -export function parseJsonEventStream( - stream: ReadableStream -): ReadableStream { +export function parseJsonEventStream< + TChunk extends UIMessageChunk = UIMessageChunk +>(stream: ReadableStream): ReadableStream { const decoder = new TextDecoder(); let buffer = ''; - return new ReadableStream({ + return new ReadableStream({ start(controller) { const reader = stream.getReader(); @@ -30,7 +30,7 @@ export function parseJsonEventStream( const jsonData = extractJsonFromLine(buffer.trim()); if (jsonData) { try { - const chunk = JSON.parse(jsonData) as UIMessageChunk; + const chunk = JSON.parse(jsonData) as TChunk; controller.enqueue(chunk); } catch { // Ignore parsing errors for incomplete data at end @@ -60,7 +60,7 @@ export function parseJsonEventStream( if (!jsonData) continue; try { - const chunk = JSON.parse(jsonData) as UIMessageChunk; + const chunk = JSON.parse(jsonData) as TChunk; controller.enqueue(chunk); } catch { // Skip malformed lines diff --git a/packages/instantsearch.js/src/lib/ai-lite/transport.ts b/packages/instantsearch.js/src/lib/ai-lite/transport.ts index a05b2eccdc..3d8b70dee1 100644 --- a/packages/instantsearch.js/src/lib/ai-lite/transport.ts +++ b/packages/instantsearch.js/src/lib/ai-lite/transport.ts @@ -7,8 +7,8 @@ import { resolveValue } from './utils'; import type { ChatTransport, HttpChatTransportInitOptions, + InferUIMessageChunk, UIMessage, - UIMessageChunk, FetchFunction, PrepareSendMessagesRequest, PrepareReconnectToStreamRequest, @@ -57,7 +57,7 @@ export abstract class HttpChatTransport headers: requestHeaders, body: requestBody, }: Parameters['sendMessages']>[0]): Promise< - ReadableStream + ReadableStream> > { const fetchFn = this.fetch ?? fetch; @@ -151,7 +151,7 @@ export abstract class HttpChatTransport body: requestBody, }: Parameters< ChatTransport['reconnectToStream'] - >[0]): Promise | null> { + >[0]): Promise> | null> { const fetchFn = this.fetch ?? fetch; // Resolve configurable values @@ -230,7 +230,7 @@ export abstract class HttpChatTransport protected abstract processResponseStream( stream: ReadableStream - ): ReadableStream; + ): ReadableStream>; } /** @@ -245,7 +245,7 @@ export class DefaultChatTransport< protected processResponseStream( stream: ReadableStream - ): ReadableStream { - return parseJsonEventStream(stream); + ): ReadableStream> { + return parseJsonEventStream>(stream); } } diff --git a/packages/instantsearch.js/src/lib/ai-lite/types.ts b/packages/instantsearch.js/src/lib/ai-lite/types.ts index a317953ae4..fab94b9cbb 100644 --- a/packages/instantsearch.js/src/lib/ai-lite/types.ts +++ b/packages/instantsearch.js/src/lib/ai-lite/types.ts @@ -190,13 +190,15 @@ export type InferUIMessageData = T extends UIMessage< ? DATA_TYPES : UIDataTypes; -export type InferUIMessageTools = T extends UIMessage< - unknown, - UIDataTypes, - infer TOOLS -> - ? TOOLS - : UITools; +type _ExtractTools< + T extends UIMessage, + D extends UIDataTypes +> = T extends UIMessage ? TOOLS : UITools; + +export type InferUIMessageTools = _ExtractTools< + T, + InferUIMessageData +>; export type InferUIMessageToolCall = | ValueOf<{ @@ -227,27 +229,54 @@ type DataUIMessageChunk = ValueOf<{ }; }>; -export type UIMessageChunk< - METADATA = unknown, - DATA_TYPES extends UIDataTypes = UIDataTypes -> = - | { type: 'text-start'; id: string; providerMetadata?: ProviderMetadata } - | { - type: 'text-delta'; - delta: string; - id: string; - providerMetadata?: ProviderMetadata; - } - | { type: 'text-end'; id: string; providerMetadata?: ProviderMetadata } - | { type: 'reasoning-start'; id: string; providerMetadata?: ProviderMetadata } - | { - type: 'reasoning-delta'; - id: string; - delta: string; - providerMetadata?: ProviderMetadata; - } - | { type: 'reasoning-end'; id: string; providerMetadata?: ProviderMetadata } - | { type: 'error'; errorText: string } +type ToolUIMessageChunk = + | ValueOf<{ + [NAME in keyof TOOLS & string]: { + type: 'tool-input-available'; + toolName: NAME; + toolCallId: string; + input: TOOLS[NAME]['input']; + callProviderMetadata?: ProviderMetadata; + providerExecuted?: boolean; + }; + }> + | ValueOf<{ + [NAME in keyof TOOLS & string]: { + type: 'tool-input-start'; + toolName: NAME; + toolCallId: string; + input?: DeepPartial; + providerExecuted?: boolean; + }; + }> + | ValueOf<{ + [NAME in keyof TOOLS & string]: { + type: 'tool-input-delta'; + toolName: NAME; + toolCallId: string; + inputDelta: string; + }; + }> + | ValueOf<{ + [NAME in keyof TOOLS & string]: { + type: 'tool-output-available'; + toolName: NAME; + toolCallId: string; + output: TOOLS[NAME]['output']; + callProviderMetadata?: ProviderMetadata; + preliminary?: boolean; + }; + }> + | ValueOf<{ + [NAME in keyof TOOLS & string]: { + type: 'tool-error'; + toolName: NAME; + toolCallId: string; + errorText: string; + input?: TOOLS[NAME]['input']; + callProviderMetadata?: ProviderMetadata; + }; + }> | { type: 'tool-input-available'; toolName: string; @@ -255,6 +284,7 @@ export type UIMessageChunk< input: unknown; callProviderMetadata?: ProviderMetadata; providerExecuted?: boolean; + dynamic: true; } | { type: 'tool-input-start'; @@ -262,12 +292,14 @@ export type UIMessageChunk< toolCallId: string; input?: unknown; providerExecuted?: boolean; + dynamic: true; } | { type: 'tool-input-delta'; toolName: string; toolCallId: string; inputDelta: string; + dynamic: true; } | { type: 'tool-output-available'; @@ -276,6 +308,7 @@ export type UIMessageChunk< output: unknown; callProviderMetadata?: ProviderMetadata; preliminary?: boolean; + dynamic: true; } | { type: 'tool-error'; @@ -284,7 +317,32 @@ export type UIMessageChunk< errorText: string; input?: unknown; callProviderMetadata?: ProviderMetadata; + dynamic: true; + }; + +export type UIMessageChunk< + METADATA = unknown, + DATA_TYPES extends UIDataTypes = UIDataTypes, + TOOLS extends UITools = UITools +> = + | { type: 'text-start'; id: string; providerMetadata?: ProviderMetadata } + | { + type: 'text-delta'; + delta: string; + id: string; + providerMetadata?: ProviderMetadata; + } + | { type: 'text-end'; id: string; providerMetadata?: ProviderMetadata } + | { type: 'reasoning-start'; id: string; providerMetadata?: ProviderMetadata } + | { + type: 'reasoning-delta'; + id: string; + delta: string; + providerMetadata?: ProviderMetadata; } + | { type: 'reasoning-end'; id: string; providerMetadata?: ProviderMetadata } + | { type: 'error'; errorText: string } + | ToolUIMessageChunk | { type: 'source-url'; sourceId: string; url: string; title?: string } | { type: 'source-document'; @@ -305,7 +363,8 @@ export type UIMessageChunk< export type InferUIMessageChunk = UIMessageChunk< InferUIMessageMetadata, - InferUIMessageData + InferUIMessageData, + InferUIMessageTools >; export interface ChatState { @@ -334,13 +393,13 @@ export interface ChatTransport { trigger: 'submit-message' | 'regenerate-message'; messageId?: string; } & ChatRequestOptions - ) => Promise>; + ) => Promise>>; reconnectToStream: ( options: { chatId: string; } & ChatRequestOptions - ) => Promise | null>; + ) => Promise> | null>; } export type PrepareSendMessagesRequest = (