Add tool calls with agentic-gpt
This commit is contained in:
@@ -1,14 +1,82 @@
|
||||
import type { SSEvent } from './sse-session.js';
|
||||
|
||||
export type MessageChunk = {
|
||||
type: 'reasoning' | 'content';
|
||||
reasoning_details?: string;
|
||||
content: string;
|
||||
}
|
||||
/**
|
||||
* JSON Schema type for tool parameter definitions
|
||||
*/
|
||||
export type JSONSchema = {
|
||||
type: 'object' | 'string' | 'number' | 'boolean' | 'array' | 'null';
|
||||
properties?: Record<string, JSONSchema>;
|
||||
items?: JSONSchema;
|
||||
required?: string[];
|
||||
enum?: (string | number)[];
|
||||
description?: string;
|
||||
additionalProperties?: boolean;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* OpenAI-format tool definition using JSON Schema
|
||||
*/
|
||||
export type ToolDefinition = {
|
||||
type: 'function';
|
||||
function: {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: JSONSchema;
|
||||
strict?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a complete tool call from the LLM
|
||||
*/
|
||||
export type ToolCall = {
|
||||
id: string;
|
||||
type: 'function';
|
||||
function: {
|
||||
name: string;
|
||||
arguments: string; // JSON string
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Delta format for streaming tool calls
|
||||
*/
|
||||
export type ToolCallDelta = {
|
||||
index: number;
|
||||
id?: string;
|
||||
type?: 'function';
|
||||
function?: {
|
||||
name?: string;
|
||||
arguments?: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Message chunk types that can be yielded during streaming
|
||||
*/
|
||||
export type MessageChunk =
|
||||
| {
|
||||
type: 'reasoning';
|
||||
reasoning_details?: string;
|
||||
content: string;
|
||||
}
|
||||
| {
|
||||
type: 'content';
|
||||
content: string;
|
||||
}
|
||||
| {
|
||||
type: 'tool_call';
|
||||
toolCall: ToolCall;
|
||||
};
|
||||
|
||||
/**
|
||||
* Final result after consuming all chunks
|
||||
*/
|
||||
export type FinalResult = {
|
||||
reasoning: string;
|
||||
content: string;
|
||||
toolCalls: ToolCall[];
|
||||
}
|
||||
|
||||
export type GPTResponse = {
|
||||
@@ -20,18 +88,20 @@ export type GPTResponse = {
|
||||
choices: {
|
||||
index: number;
|
||||
delta: {
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
reasoning: string;
|
||||
reasoning_details: {
|
||||
role?: 'user' | 'assistant' | 'system';
|
||||
content?: string | null;
|
||||
reasoning?: string;
|
||||
reasoning_details?: {
|
||||
type: string;
|
||||
summary: string;
|
||||
}
|
||||
};
|
||||
tool_calls?: ToolCallDelta[];
|
||||
};
|
||||
finish_reason?: 'stop' | 'tool_calls' | 'length' | 'content_filter' | null;
|
||||
}[];
|
||||
finish_reason: 'stop' | 'tool_calls' | 'length' | 'content_filter' | null;
|
||||
native_finish_reason: string | null;
|
||||
usage: {
|
||||
finish_reason?: 'stop' | 'tool_calls' | 'length' | 'content_filter' | null;
|
||||
native_finish_reason?: string | null;
|
||||
usage?: {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
@@ -57,6 +127,12 @@ export class MessageResponse implements PromiseLike<FinalResult> {
|
||||
private resolveResult!: (value: FinalResult) => void;
|
||||
private resultPromise: Promise<FinalResult>;
|
||||
private iterator: AsyncIterable<SSEvent>;
|
||||
|
||||
/**
|
||||
* Accumulates tool calls as they stream in from the API
|
||||
* Key is the tool call index, value is the partially completed tool call
|
||||
*/
|
||||
private toolCallsInProgress: Map<number, Partial<ToolCall>> = new Map();
|
||||
|
||||
constructor(iterator: AsyncIterable<SSEvent>) {
|
||||
this.iterator = iterator;
|
||||
@@ -73,9 +149,13 @@ export class MessageResponse implements PromiseLike<FinalResult> {
|
||||
this.iteratorConsumed = true;
|
||||
|
||||
for await (const rawChunk of this.iterator) {
|
||||
const chunk = this.parseChunk(rawChunk);
|
||||
this.chunks.push(chunk);
|
||||
yield chunk;
|
||||
const chunks = this.parseChunk(rawChunk);
|
||||
|
||||
// parseChunk may return multiple chunks (e.g., when tool calls complete)
|
||||
for (const chunk of chunks) {
|
||||
this.chunks.push(chunk);
|
||||
yield chunk;
|
||||
}
|
||||
}
|
||||
|
||||
this.resolveResult(this.buildResult());
|
||||
@@ -91,8 +171,12 @@ export class MessageResponse implements PromiseLike<FinalResult> {
|
||||
|
||||
(async () => {
|
||||
for await (const rawChunk of this.iterator) {
|
||||
const chunk = this.parseChunk(rawChunk);
|
||||
this.chunks.push(chunk);
|
||||
const chunks = this.parseChunk(rawChunk);
|
||||
|
||||
// parseChunk may return multiple chunks
|
||||
for (const chunk of chunks) {
|
||||
this.chunks.push(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
this.resolveResult(this.buildResult());
|
||||
@@ -114,22 +198,37 @@ export class MessageResponse implements PromiseLike<FinalResult> {
|
||||
return {
|
||||
reasoning: this.chunks
|
||||
.filter(c => c.type === 'reasoning')
|
||||
.map(c => c.content)
|
||||
.map(c => 'content' in c ? c.content : '')
|
||||
.join(''),
|
||||
content: this.chunks
|
||||
.filter(c => c.type === 'content')
|
||||
.map(c => c.content)
|
||||
.map(c => 'content' in c ? c.content : '')
|
||||
.join(''),
|
||||
toolCalls: this.chunks
|
||||
.filter(c => c.type === 'tool_call')
|
||||
.map(c => 'toolCall' in c ? c.toolCall : null)
|
||||
.filter((tc): tc is ToolCall => tc !== null),
|
||||
};
|
||||
}
|
||||
|
||||
private parseChunk(rawChunk: SSEvent) {
|
||||
/**
|
||||
* Parses a raw SSE chunk and returns one or more MessageChunks
|
||||
* May return multiple chunks when tool calls complete
|
||||
*/
|
||||
private parseChunk(rawChunk: SSEvent): MessageChunk[] {
|
||||
// console.log('Raw Chunk:', rawChunk);
|
||||
if (rawChunk.data === '[DONE]') {
|
||||
return {
|
||||
// When stream ends, flush any pending tool calls
|
||||
const completedToolCalls = this.flushToolCalls();
|
||||
|
||||
if (completedToolCalls.length > 0) {
|
||||
return completedToolCalls;
|
||||
}
|
||||
|
||||
return [{
|
||||
type: 'content',
|
||||
content: '',
|
||||
} as const;
|
||||
}];
|
||||
}
|
||||
|
||||
const data = JSON.parse(rawChunk.data) as GPTResponse;
|
||||
@@ -140,18 +239,104 @@ export class MessageResponse implements PromiseLike<FinalResult> {
|
||||
}
|
||||
|
||||
const delta = choice.delta;
|
||||
const finishReason = choice.finish_reason || data.finish_reason;
|
||||
|
||||
// Handle tool calls
|
||||
if (delta.tool_calls) {
|
||||
this.processToolCallDeltas(delta.tool_calls);
|
||||
|
||||
// If finish_reason is 'tool_calls', all tool calls are complete
|
||||
if (finishReason === 'tool_calls') {
|
||||
return this.flushToolCalls();
|
||||
}
|
||||
|
||||
// Otherwise, don't yield anything yet (still accumulating)
|
||||
return [];
|
||||
}
|
||||
|
||||
// Handle reasoning chunks
|
||||
if (delta.reasoning) {
|
||||
return {
|
||||
const chunk: MessageChunk = {
|
||||
type: 'reasoning',
|
||||
content: delta.reasoning,
|
||||
reasoning_details: delta.reasoning_details.summary,
|
||||
} as const;
|
||||
} else {
|
||||
return {
|
||||
};
|
||||
|
||||
// Add reasoning_details if present
|
||||
if (delta.reasoning_details?.summary) {
|
||||
chunk.reasoning_details = delta.reasoning_details.summary;
|
||||
}
|
||||
|
||||
return [chunk];
|
||||
}
|
||||
|
||||
// Handle content chunks
|
||||
if (delta.content !== undefined && delta.content !== null) {
|
||||
return [{
|
||||
type: 'content',
|
||||
content: delta.content,
|
||||
} as const;
|
||||
}];
|
||||
}
|
||||
|
||||
// Empty chunk (e.g., role assignment)
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes tool call deltas and accumulates them
|
||||
*/
|
||||
private processToolCallDeltas(deltas: ToolCallDelta[]): void {
|
||||
for (const delta of deltas) {
|
||||
const index = delta.index;
|
||||
|
||||
if (!this.toolCallsInProgress.has(index)) {
|
||||
// Start a new tool call
|
||||
this.toolCallsInProgress.set(index, {
|
||||
id: delta.id || '',
|
||||
type: 'function',
|
||||
function: {
|
||||
name: delta.function?.name || '',
|
||||
arguments: delta.function?.arguments || '',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Accumulate arguments for existing tool call
|
||||
const existing = this.toolCallsInProgress.get(index)!;
|
||||
|
||||
if (delta.function?.arguments) {
|
||||
existing.function!.arguments += delta.function.arguments;
|
||||
}
|
||||
|
||||
// Update other fields if provided
|
||||
if (delta.id) {
|
||||
existing.id = delta.id;
|
||||
}
|
||||
if (delta.function?.name) {
|
||||
existing.function!.name = delta.function.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes all accumulated tool calls and returns them as chunks
|
||||
*/
|
||||
private flushToolCalls(): MessageChunk[] {
|
||||
const chunks: MessageChunk[] = [];
|
||||
|
||||
// Convert accumulated tool calls to chunks
|
||||
for (const [index, toolCall] of this.toolCallsInProgress.entries()) {
|
||||
// Validate that the tool call is complete
|
||||
if (toolCall.id && toolCall.function?.name && toolCall.function?.arguments !== undefined) {
|
||||
chunks.push({
|
||||
type: 'tool_call',
|
||||
toolCall: toolCall as ToolCall,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the accumulator
|
||||
this.toolCallsInProgress.clear();
|
||||
|
||||
return chunks;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user