@proteinjs/conversation 1.5.2 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,157 @@
1
+ import { ChatCompletionMessageToolCall, ChatCompletionChunk } from 'openai/resources/chat';
2
+ import { LogLevel, Logger } from '@proteinjs/util';
3
+ import { Stream } from 'openai/streaming';
4
+ import { Readable, Transform, TransformCallback, PassThrough } from 'stream';
5
+
6
+ /**
7
+ * Processes streaming responses from OpenAI's `ChatCompletions` api.
8
+ * - When a tool call is received, it delegates processing to `onToolCalls`; this can happen recursively
9
+ * - When a response to the user is received, it writes to `outputStream`
10
+ */
11
+ export class OpenAiStreamProcessor {
12
+ private logger: Logger;
13
+ private accumulatedToolCalls: Partial<ChatCompletionMessageToolCall>[] = [];
14
+ private toolCallsExecuted = 0;
15
+ private currentToolCall: Partial<ChatCompletionMessageToolCall> | null = null;
16
+ private inputStream: Readable;
17
+ private controlStream: Transform;
18
+ private outputStream: Readable;
19
+
20
+ constructor(
21
+ inputStream: Stream<ChatCompletionChunk>,
22
+ private onToolCalls: (
23
+ toolCalls: ChatCompletionMessageToolCall[],
24
+ currentFunctionCalls: number
25
+ ) => Promise<Readable>,
26
+ logLevel: LogLevel
27
+ ) {
28
+ this.logger = new Logger(this.constructor.name, logLevel);
29
+ this.inputStream = Readable.from(inputStream);
30
+ this.controlStream = this.createControlStream();
31
+ this.outputStream = new PassThrough();
32
+ this.inputStream.pipe(this.controlStream);
33
+ }
34
+
35
+ /**
36
+ * @returns a `Readable` stream that will receive the assistant's text response to the user
37
+ */
38
+ getOutputStream(): Readable {
39
+ return this.outputStream;
40
+ }
41
+
42
+ /**
43
+ * @returns a `Transform` that parses the input stream and delegates to tool calls or writes a text response to the user
44
+ */
45
+ private createControlStream(): Transform {
46
+ return new Transform({
47
+ objectMode: true,
48
+ transform: (chunk: ChatCompletionChunk, encoding: string, callback: TransformCallback) => {
49
+ try {
50
+ if (this.outputStream.destroyed) {
51
+ this.logger.warn(`Destroying input and control streams since output stream is destroyed`);
52
+ this.inputStream.destroy();
53
+ this.controlStream.destroy();
54
+ return;
55
+ }
56
+
57
+ if (!chunk || !chunk.choices) {
58
+ throw new Error(`Received invalid chunk:\n${JSON.stringify(chunk, null, 2)}`);
59
+ } else if (typeof chunk.choices[0]?.delta?.content === 'string') {
60
+ this.outputStream.push(chunk.choices[0].delta.content);
61
+ } else if (chunk.choices[0]?.delta?.tool_calls) {
62
+ this.handleToolCallDelta(chunk.choices[0].delta.tool_calls);
63
+ } else if (chunk.choices[0]?.finish_reason === 'tool_calls') {
64
+ this.handleToolCalls();
65
+ } else if (chunk.choices[0]?.finish_reason === 'stop') {
66
+ this.outputStream.push(null);
67
+ } else if (chunk.choices[0]?.finish_reason === 'length') {
68
+ this.logger.warn(`The maximum number of tokens specified in the request was reached`);
69
+ this.outputStream.push(null);
70
+ } else if (chunk.choices[0]?.finish_reason === 'content_filter') {
71
+ this.logger.error(`Content was omitted due to a flag from OpenAI's content filters`);
72
+ this.outputStream.push(null);
73
+ } else {
74
+ throw new Error(`Received invalid finish reason or delta:\n${JSON.stringify(chunk, null, 2)}`);
75
+ }
76
+ callback();
77
+ } catch (error: any) {
78
+ this.logger.error('Error tranforming chunk', error);
79
+ this.destroyStreams(error);
80
+ }
81
+ },
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Destroy all streams used by `OpenAiStreamProcessor`
87
+ */
88
+ private destroyStreams(error?: Error) {
89
+ this.inputStream.destroy();
90
+ this.controlStream.destroy();
91
+ this.outputStream.emit('error', error);
92
+ this.outputStream.destroy();
93
+ }
94
+
95
+ /**
96
+ * Accumulates tool call deltas into complete tool calls.
97
+ * @param toolCallDeltas `ChatCompletionChunk.Choice.Delta.ToolCall` objects that contain part of a complete `ChatCompletionMessageToolCall`
98
+ */
99
+ private handleToolCallDelta(toolCallDeltas: ChatCompletionChunk.Choice.Delta.ToolCall[]) {
100
+ for (const delta of toolCallDeltas) {
101
+ if (delta.id) {
102
+ // Start of a new tool call
103
+ if (this.currentToolCall) {
104
+ this.accumulatedToolCalls.push(this.currentToolCall);
105
+ }
106
+ this.currentToolCall = {
107
+ id: delta.id,
108
+ type: delta.type || 'function',
109
+ function: {
110
+ name: delta.function?.name || '',
111
+ arguments: delta.function?.arguments || '',
112
+ },
113
+ };
114
+ } else {
115
+ // Continue building the current tool call
116
+ if (delta.function?.name) {
117
+ this.currentToolCall!.function!.name += delta.function.name;
118
+ }
119
+ if (delta.function?.arguments) {
120
+ this.currentToolCall!.function!.arguments += delta.function.arguments;
121
+ }
122
+ }
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Delegates `ChatCompletionMessageToolCall`s to `onToolCalls`.
128
+ * - Manages refreshing the `inputStream` and `controlStream`
129
+ * - Manages tool call state (such as keeping track of the number of tool calls made)
130
+ */
131
+ private async handleToolCalls() {
132
+ if (this.currentToolCall) {
133
+ this.accumulatedToolCalls.push(this.currentToolCall);
134
+ this.currentToolCall = null;
135
+ }
136
+
137
+ const completedToolCalls = this.accumulatedToolCalls.filter(
138
+ (tc): tc is ChatCompletionMessageToolCall =>
139
+ tc.id !== undefined && tc.function !== undefined && tc.type !== undefined
140
+ );
141
+
142
+ this.accumulatedToolCalls = [];
143
+ this.inputStream.destroy();
144
+ this.controlStream.destroy();
145
+ this.controlStream = this.createControlStream();
146
+
147
+ try {
148
+ this.inputStream = await this.onToolCalls(completedToolCalls, this.toolCallsExecuted);
149
+ this.inputStream.on('error', (error) => this.destroyStreams(error));
150
+ this.inputStream.pipe(this.controlStream);
151
+ this.toolCallsExecuted += completedToolCalls.length;
152
+ } catch (error: any) {
153
+ this.logger.error('Error processing tool calls:', error);
154
+ this.destroyStreams(error);
155
+ }
156
+ }
157
+ }