Compare commits
2 Commits
af440dcbc7
...
agentic-gp
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ed0977553 | |||
| e68427e53d |
@@ -1,4 +1,6 @@
|
||||
import type { SSEvent } from './sse-session.js';
|
||||
import type { SSEvent } from '../utils/sse-session.js';
|
||||
|
||||
import { ThenableIterator } from '../utils/thenable-iterator.js';
|
||||
|
||||
/**
|
||||
* JSON Schema type for tool parameter definitions
|
||||
@@ -121,13 +123,7 @@ export type GPTResponse = {
|
||||
};
|
||||
}
|
||||
|
||||
export class MessageResponse implements PromiseLike<FinalResult> {
|
||||
private chunks: MessageChunk[] = [];
|
||||
private iteratorConsumed = false;
|
||||
private resolveResult!: (value: FinalResult) => void;
|
||||
private resultPromise: Promise<FinalResult>;
|
||||
private iterator: AsyncIterable<SSEvent>;
|
||||
|
||||
export class GPTResponseIterator extends ThenableIterator<SSEvent, MessageChunk, FinalResult> {
|
||||
/**
|
||||
* Accumulates tool calls as they stream in from the API
|
||||
* Key is the tool call index, value is the partially completed tool call
|
||||
@@ -135,118 +131,54 @@ export class MessageResponse implements PromiseLike<FinalResult> {
|
||||
private toolCallsInProgress: Map<number, Partial<ToolCall>> = new Map();
|
||||
|
||||
constructor(iterator: AsyncIterable<SSEvent>) {
|
||||
this.iterator = iterator;
|
||||
this.resultPromise = new Promise(resolve => {
|
||||
this.resolveResult = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
async *[Symbol.asyncIterator]() {
|
||||
if (this.iteratorConsumed) {
|
||||
throw new Error('GPTResponse can only be iterated once');
|
||||
}
|
||||
|
||||
this.iteratorConsumed = true;
|
||||
|
||||
for await (const rawChunk of this.iterator) {
|
||||
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());
|
||||
}
|
||||
|
||||
then<TResult1 = FinalResult, TResult2 = never>(
|
||||
onfulfilled?: ((value: FinalResult) => TResult1 | PromiseLike<TResult1>) | null,
|
||||
onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
|
||||
): Promise<TResult1 | TResult2> {
|
||||
// If not yet iterated, consume the iterator to get the result
|
||||
if (!this.iteratorConsumed) {
|
||||
this.iteratorConsumed = true;
|
||||
|
||||
(async () => {
|
||||
for await (const rawChunk of this.iterator) {
|
||||
const chunks = this.parseChunk(rawChunk);
|
||||
|
||||
// parseChunk may return multiple chunks
|
||||
for (const chunk of chunks) {
|
||||
this.chunks.push(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
this.resolveResult(this.buildResult());
|
||||
})();
|
||||
}
|
||||
|
||||
return this.resultPromise.then(onfulfilled, onrejected);
|
||||
}
|
||||
|
||||
catch(onrejected?: ((reason: unknown) => never) | null): Promise<FinalResult> {
|
||||
return this.resultPromise.catch(onrejected);
|
||||
}
|
||||
|
||||
finally(onfinally?: (() => void) | undefined): Promise<FinalResult> {
|
||||
return this.resultPromise.finally(onfinally);
|
||||
}
|
||||
|
||||
private buildResult(): FinalResult {
|
||||
return {
|
||||
reasoning: this.chunks
|
||||
.filter(c => c.type === 'reasoning')
|
||||
.map(c => 'content' in c ? c.content : '')
|
||||
.join(''),
|
||||
content: this.chunks
|
||||
.filter(c => c.type === '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),
|
||||
};
|
||||
super(iterator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a raw SSE chunk and returns one or more MessageChunks
|
||||
* May return multiple chunks when tool calls complete
|
||||
*/
|
||||
private parseChunk(rawChunk: SSEvent): MessageChunk[] {
|
||||
parseChunk(rawChunk: SSEvent): MessageChunk[] {
|
||||
// console.log('Raw Chunk:', rawChunk);
|
||||
if (rawChunk.data === '[DONE]') {
|
||||
// When stream ends, flush any pending tool calls
|
||||
const completedToolCalls = this.flushToolCalls();
|
||||
|
||||
// If there are completed tool calls, return them
|
||||
if (completedToolCalls.length > 0) {
|
||||
return completedToolCalls;
|
||||
}
|
||||
|
||||
// Otherwise, return an empty content chunk so we don't crash trying to parse invalid data
|
||||
return [{
|
||||
type: 'content',
|
||||
content: '',
|
||||
}];
|
||||
}
|
||||
|
||||
// Parse the chunk data as a GPTResponse
|
||||
const data = JSON.parse(rawChunk.data) as GPTResponse;
|
||||
// Get the first choice
|
||||
const choice = data.choices[0];
|
||||
|
||||
// If no choice found, throw an error
|
||||
if (!choice) {
|
||||
throw new Error('No choice found in chunk');
|
||||
}
|
||||
|
||||
// Get the delta from the choice
|
||||
const delta = choice.delta;
|
||||
// Get the finish reason from the choice or the response
|
||||
const finishReason = choice.finish_reason || data.finish_reason;
|
||||
|
||||
// Handle tool calls
|
||||
if (delta.tool_calls) {
|
||||
// Process the tool call deltas
|
||||
this.processToolCallDeltas(delta.tool_calls);
|
||||
|
||||
// If finish_reason is 'tool_calls', all tool calls are complete
|
||||
if (finishReason === 'tool_calls') {
|
||||
// If all tool calls are complete, flush them and return them as chunks
|
||||
return this.flushToolCalls();
|
||||
}
|
||||
|
||||
@@ -281,6 +213,29 @@ export class MessageResponse implements PromiseLike<FinalResult> {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a final result from the output chunks
|
||||
*
|
||||
* @param chunks - The output chunks to parse
|
||||
* @returns The parsed final result
|
||||
*/
|
||||
parseFinal(chunks: MessageChunk[]): FinalResult {
|
||||
return {
|
||||
reasoning: chunks
|
||||
.filter(c => c.type === 'reasoning')
|
||||
.map(c => 'content' in c ? c.content : '')
|
||||
.join(''),
|
||||
content: chunks
|
||||
.filter(c => c.type === 'content')
|
||||
.map(c => 'content' in c ? c.content : '')
|
||||
.join(''),
|
||||
toolCalls: chunks
|
||||
.filter(c => c.type === 'tool_call')
|
||||
.map(c => 'toolCall' in c ? c.toolCall : null)
|
||||
.filter((tc): tc is ToolCall => tc !== null),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes tool call deltas and accumulates them
|
||||
*/
|
||||
@@ -339,4 +294,4 @@ export class MessageResponse implements PromiseLike<FinalResult> {
|
||||
|
||||
return chunks;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { SSESession } from './sse-session.js';
|
||||
import { EventEmitter } from './utils/event-emitter.js';
|
||||
import { MessageResponse } from './gpt-response.js';
|
||||
import type { ToolDefinition, ToolCall } from './gpt-response.js';
|
||||
import { SSESession } from '../utils/sse-session.js';
|
||||
import { EventEmitter } from '../utils/event-emitter.js';
|
||||
import { GPTResponseIterator, type ToolCall, type ToolDefinition } from './gpt-response.js';
|
||||
|
||||
|
||||
export type GPTEventMap = {
|
||||
@@ -93,10 +92,10 @@ export class GPT extends EventEmitter<GPTEventMap> {
|
||||
|
||||
/**
|
||||
* Sends a message to the GPT API
|
||||
* @param request - The request configuration including messages and optional tools
|
||||
* @param message - The message to send
|
||||
* @returns The response from the GPT API
|
||||
*/
|
||||
send(request: GPTRequest): MessageResponse {
|
||||
send(request: GPTRequest): GPTResponseIterator {
|
||||
const config = this.config;
|
||||
|
||||
const lazyIterator = (async function* () {
|
||||
@@ -132,6 +131,6 @@ export class GPT extends EventEmitter<GPTEventMap> {
|
||||
yield* session.messages;
|
||||
})();
|
||||
|
||||
return new MessageResponse(lazyIterator);
|
||||
return new GPTResponseIterator(lazyIterator);
|
||||
}
|
||||
}
|
||||
48
src/index.ts
48
src/index.ts
@@ -1,24 +1,32 @@
|
||||
/**
|
||||
* Export all public types and classes
|
||||
*/
|
||||
export { GPT } from './gpt.js';
|
||||
export type { GPTConfig, GPTRequest, Message, GPTEventMap } from './gpt.js';
|
||||
export { AgenticGPT } from './agentic-gpt.js';
|
||||
export type { ToolFunction, ToolRegistration, AgenticOptions } from './agentic-gpt.js';
|
||||
export { MessageResponse } from './gpt-response.js';
|
||||
export type {
|
||||
MessageChunk,
|
||||
FinalResult,
|
||||
ToolDefinition,
|
||||
ToolCall,
|
||||
ToolCallDelta,
|
||||
JSONSchema,
|
||||
GPTResponse
|
||||
} from './gpt-response.js';
|
||||
export {
|
||||
GPT,
|
||||
type GPTConfig,
|
||||
type GPTRequest,
|
||||
type Message,
|
||||
type GPTEventMap,
|
||||
} from './gpt/gpt.js'
|
||||
|
||||
import { GPT } from './gpt.js';
|
||||
import { AgenticGPT } from './agentic-gpt.js';
|
||||
import type { ToolDefinition } from './gpt-response.js';
|
||||
export {
|
||||
AgenticGPT,
|
||||
type ToolFunction,
|
||||
type ToolRegistration,
|
||||
type AgenticOptions,
|
||||
} from './gpt/agentic-gpt.js';
|
||||
|
||||
export {
|
||||
GPTResponseIterator,
|
||||
type MessageChunk,
|
||||
type FinalResult,
|
||||
type ToolDefinition,
|
||||
type ToolCall,
|
||||
type ToolCallDelta,
|
||||
type JSONSchema,
|
||||
type GPTResponse,
|
||||
} from './gpt/gpt-response.js';
|
||||
|
||||
import { GPT } from './gpt/gpt.js';
|
||||
import { AgenticGPT } from './gpt/agentic-gpt.js';
|
||||
import type { ToolDefinition } from './gpt/gpt-response.js';
|
||||
|
||||
/**
|
||||
* Examples demonstrating the different usage patterns
|
||||
|
||||
133
src/utils/thenable-iterator.ts
Normal file
133
src/utils/thenable-iterator.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
|
||||
|
||||
export class ThenableIterator<
|
||||
InputChunk,
|
||||
OutputChunk,
|
||||
Output,
|
||||
> implements PromiseLike<Output> {
|
||||
/** Iterator to be consumed */
|
||||
protected iterator: AsyncIterable<InputChunk>;
|
||||
|
||||
/** Chunks to be parsed */
|
||||
protected chunks: OutputChunk[] = [];
|
||||
|
||||
/** Whether the iterator has been consumed */
|
||||
protected iteratorConsumed = false;
|
||||
|
||||
/** Promise to resolve the result */
|
||||
protected resultPromise: Promise<Output>;
|
||||
|
||||
/** Resolver function to resolve the result promise */
|
||||
protected resolveResult!: (value: Output) => void;
|
||||
|
||||
/**
|
||||
* Creates a new ThenableIterator instance
|
||||
* @param iterator - The iterator to be consumed
|
||||
*/
|
||||
constructor(iterator: AsyncIterable<InputChunk>) {
|
||||
// Store the iterator to be consumed
|
||||
this.iterator = iterator;
|
||||
|
||||
// Create a promise that we can bind to the thenable pattern
|
||||
this.resultPromise = new Promise((resolve) => this.resolveResult = resolve);
|
||||
}
|
||||
|
||||
/**
|
||||
* `Symbol.asyncIterator` method to implement the async iterator pattern
|
||||
* Consumes the iterator if it has not yet been consumed, then yields the parsed chunks
|
||||
*
|
||||
* @returns An async iterator that yields the parsed chunks
|
||||
* @throws An error if the iterator has already been consumed
|
||||
*/
|
||||
async *[Symbol.asyncIterator]() {
|
||||
if (this.iteratorConsumed) {
|
||||
throw new Error('Iterator can only be iterated once');
|
||||
}
|
||||
|
||||
this.iteratorConsumed = true;
|
||||
|
||||
for await (const chunk of this.iterator) {
|
||||
const parsedChunks = this.parseChunk(chunk);
|
||||
|
||||
for (const parsedChunk of parsedChunks) {
|
||||
this.chunks.push(parsedChunk);
|
||||
yield parsedChunk;
|
||||
}
|
||||
}
|
||||
|
||||
this.resolveResult(this.parseFinal(this.chunks));
|
||||
}
|
||||
|
||||
/**
|
||||
* `then` method to implement the thenable pattern
|
||||
* Consumes the iterator if it has not yet been consumed, then returns the result as a resolved promise
|
||||
*
|
||||
* @param onfulfilled - The function to call when the promise is fulfilled
|
||||
* @param onrejected - The function to call when the promise is rejected
|
||||
* @returns A promise that resolves to the result
|
||||
*/
|
||||
then<TResult1 = Output, TResult2 = never>(
|
||||
onfulfilled?: ((value: Output) => TResult1 | PromiseLike<TResult1>) | null,
|
||||
onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
|
||||
): Promise<TResult1 | TResult2> {
|
||||
// If not yet iterated, consume the iterator to get the result
|
||||
if (!this.iteratorConsumed) {
|
||||
this.iteratorConsumed = true;
|
||||
|
||||
// Consume the iterator parts
|
||||
(async () => {
|
||||
for await (const rawChunk of this.iterator) {
|
||||
const chunks = this.parseChunk(rawChunk);
|
||||
this.chunks.push(...chunks);
|
||||
}
|
||||
|
||||
// Build the result from the chunks
|
||||
this.resolveResult(this.parseFinal(this.chunks));
|
||||
})();
|
||||
}
|
||||
|
||||
return this.resultPromise.then(onfulfilled, onrejected);
|
||||
}
|
||||
|
||||
/**
|
||||
* `catch` method to implement the promise pattern
|
||||
* Returns a promise that rejects with the reason
|
||||
*
|
||||
* @param onrejected - The function to call when the promise is rejected
|
||||
* @returns A promise that rejects with the reason
|
||||
*/
|
||||
catch(onrejected?: ((reason: unknown) => never) | null): Promise<Output> {
|
||||
return this.resultPromise.catch(onrejected);
|
||||
}
|
||||
|
||||
/**
|
||||
* `finally` method to implement the promise pattern
|
||||
* Returns a promise that resolves to the result
|
||||
*
|
||||
* @param onfinally - The function to call when the promise is fulfilled or rejected
|
||||
* @returns A promise that resolves to the result
|
||||
*/
|
||||
finally(onfinally?: (() => void) | undefined): Promise<Output> {
|
||||
return this.resultPromise.finally(onfinally);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a chunk of input into an output chunk
|
||||
*
|
||||
* @param chunk - The chunk of input to parse
|
||||
* @returns The parsed output chunk
|
||||
*/
|
||||
parseChunk(chunk: InputChunk): OutputChunk[] {
|
||||
return [chunk] as unknown as OutputChunk[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a final result from the output chunks
|
||||
*
|
||||
* @param chunks - The output chunks to parse
|
||||
* @returns The parsed final result
|
||||
*/
|
||||
parseFinal(chunks: OutputChunk[]): Output {
|
||||
return chunks as unknown as Output;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user