@proteinjs/conversation 1.5.2 → 1.6.1

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,202 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __generator = (this && this.__generator) || function (thisArg, body) {
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
13
+ return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
+ function verb(n) { return function (v) { return step([n, v]); }; }
15
+ function step(op) {
16
+ if (f) throw new TypeError("Generator is already executing.");
17
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19
+ if (y = 0, t) op = [op[0] & 2, t.value];
20
+ switch (op[0]) {
21
+ case 0: case 1: t = op; break;
22
+ case 4: _.label++; return { value: op[1], done: false };
23
+ case 5: _.label++; y = op[1]; op = [0]; continue;
24
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
25
+ default:
26
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30
+ if (t[2]) _.ops.pop();
31
+ _.trys.pop(); continue;
32
+ }
33
+ op = body.call(thisArg, _);
34
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
+ }
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.OpenAiStreamProcessor = void 0;
40
+ var util_1 = require("@proteinjs/util");
41
+ var stream_1 = require("stream");
42
+ /**
43
+ * Processes streaming responses from OpenAI's `ChatCompletions` api.
44
+ * - When a tool call is received, it delegates processing to `onToolCalls`; this can happen recursively
45
+ * - When a response to the user is received, it writes to `outputStream`
46
+ */
47
+ var OpenAiStreamProcessor = /** @class */ (function () {
48
+ function OpenAiStreamProcessor(inputStream, onToolCalls, logLevel) {
49
+ this.onToolCalls = onToolCalls;
50
+ this.accumulatedToolCalls = [];
51
+ this.toolCallsExecuted = 0;
52
+ this.currentToolCall = null;
53
+ this.logger = new util_1.Logger(this.constructor.name, logLevel);
54
+ this.inputStream = stream_1.Readable.from(inputStream);
55
+ this.controlStream = this.createControlStream();
56
+ this.outputStream = new stream_1.PassThrough();
57
+ this.inputStream.pipe(this.controlStream);
58
+ }
59
+ /**
60
+ * @returns a `Readable` stream that will receive the assistant's text response to the user
61
+ */
62
+ OpenAiStreamProcessor.prototype.getOutputStream = function () {
63
+ return this.outputStream;
64
+ };
65
+ /**
66
+ * @returns a `Transform` that parses the input stream and delegates to tool calls or writes a text response to the user
67
+ */
68
+ OpenAiStreamProcessor.prototype.createControlStream = function () {
69
+ var _this = this;
70
+ return new stream_1.Transform({
71
+ objectMode: true,
72
+ transform: function (chunk, encoding, callback) {
73
+ var _a, _b, _c, _d, _e, _f, _g, _h;
74
+ try {
75
+ if (_this.outputStream.destroyed) {
76
+ _this.logger.warn("Destroying input and control streams since output stream is destroyed");
77
+ _this.inputStream.destroy();
78
+ _this.controlStream.destroy();
79
+ return;
80
+ }
81
+ if (!chunk || !chunk.choices) {
82
+ throw new Error("Received invalid chunk:\n".concat(JSON.stringify(chunk, null, 2)));
83
+ }
84
+ else if ((_b = (_a = chunk.choices[0]) === null || _a === void 0 ? void 0 : _a.delta) === null || _b === void 0 ? void 0 : _b.content) {
85
+ _this.outputStream.push(chunk.choices[0].delta.content);
86
+ }
87
+ else if ((_d = (_c = chunk.choices[0]) === null || _c === void 0 ? void 0 : _c.delta) === null || _d === void 0 ? void 0 : _d.tool_calls) {
88
+ _this.handleToolCallDelta(chunk.choices[0].delta.tool_calls);
89
+ }
90
+ else if (((_e = chunk.choices[0]) === null || _e === void 0 ? void 0 : _e.finish_reason) === 'tool_calls') {
91
+ _this.handleToolCalls();
92
+ }
93
+ else if (((_f = chunk.choices[0]) === null || _f === void 0 ? void 0 : _f.finish_reason) === 'stop') {
94
+ _this.outputStream.push(null);
95
+ }
96
+ else if (((_g = chunk.choices[0]) === null || _g === void 0 ? void 0 : _g.finish_reason) === 'length') {
97
+ _this.logger.warn("The maximum number of tokens specified in the request was reached");
98
+ _this.outputStream.push(null);
99
+ }
100
+ else if (((_h = chunk.choices[0]) === null || _h === void 0 ? void 0 : _h.finish_reason) === 'content_filter') {
101
+ _this.logger.error("Content was omitted due to a flag from OpenAI's content filters");
102
+ _this.outputStream.push(null);
103
+ }
104
+ callback();
105
+ }
106
+ catch (error) {
107
+ _this.logger.error('Error tranforming chunk', error);
108
+ _this.destroyStreams(error);
109
+ }
110
+ },
111
+ });
112
+ };
113
+ /**
114
+ * Destroy all streams used by `OpenAiStreamProcessor`
115
+ */
116
+ OpenAiStreamProcessor.prototype.destroyStreams = function (error) {
117
+ this.inputStream.destroy();
118
+ this.controlStream.destroy();
119
+ this.outputStream.emit('error', error);
120
+ this.outputStream.destroy();
121
+ };
122
+ /**
123
+ * Accumulates tool call deltas into complete tool calls.
124
+ * @param toolCallDeltas `ChatCompletionChunk.Choice.Delta.ToolCall` objects that contain part of a complete `ChatCompletionMessageToolCall`
125
+ */
126
+ OpenAiStreamProcessor.prototype.handleToolCallDelta = function (toolCallDeltas) {
127
+ var _a, _b, _c, _d;
128
+ for (var _i = 0, toolCallDeltas_1 = toolCallDeltas; _i < toolCallDeltas_1.length; _i++) {
129
+ var delta = toolCallDeltas_1[_i];
130
+ if (delta.id) {
131
+ // Start of a new tool call
132
+ if (this.currentToolCall) {
133
+ this.accumulatedToolCalls.push(this.currentToolCall);
134
+ }
135
+ this.currentToolCall = {
136
+ id: delta.id,
137
+ type: delta.type || 'function',
138
+ function: {
139
+ name: ((_a = delta.function) === null || _a === void 0 ? void 0 : _a.name) || '',
140
+ arguments: ((_b = delta.function) === null || _b === void 0 ? void 0 : _b.arguments) || '',
141
+ },
142
+ };
143
+ }
144
+ else {
145
+ // Continue building the current tool call
146
+ if ((_c = delta.function) === null || _c === void 0 ? void 0 : _c.name) {
147
+ this.currentToolCall.function.name += delta.function.name;
148
+ }
149
+ if ((_d = delta.function) === null || _d === void 0 ? void 0 : _d.arguments) {
150
+ this.currentToolCall.function.arguments += delta.function.arguments;
151
+ }
152
+ }
153
+ }
154
+ };
155
+ /**
156
+ * Delegates `ChatCompletionMessageToolCall`s to `onToolCalls`.
157
+ * - Manages refreshing the `inputStream` and `controlStream`
158
+ * - Manages tool call state (such as keeping track of the number of tool calls made)
159
+ */
160
+ OpenAiStreamProcessor.prototype.handleToolCalls = function () {
161
+ return __awaiter(this, void 0, void 0, function () {
162
+ var completedToolCalls, _a, error_1;
163
+ var _this = this;
164
+ return __generator(this, function (_b) {
165
+ switch (_b.label) {
166
+ case 0:
167
+ if (this.currentToolCall) {
168
+ this.accumulatedToolCalls.push(this.currentToolCall);
169
+ this.currentToolCall = null;
170
+ }
171
+ completedToolCalls = this.accumulatedToolCalls.filter(function (tc) {
172
+ return tc.id !== undefined && tc.function !== undefined && tc.type !== undefined;
173
+ });
174
+ this.accumulatedToolCalls = [];
175
+ this.inputStream.destroy();
176
+ this.controlStream.destroy();
177
+ this.controlStream = this.createControlStream();
178
+ _b.label = 1;
179
+ case 1:
180
+ _b.trys.push([1, 3, , 4]);
181
+ _a = this;
182
+ return [4 /*yield*/, this.onToolCalls(completedToolCalls, this.toolCallsExecuted)];
183
+ case 2:
184
+ _a.inputStream = _b.sent();
185
+ this.inputStream.on('error', function (error) { return _this.destroyStreams(error); });
186
+ this.inputStream.pipe(this.controlStream);
187
+ this.toolCallsExecuted += completedToolCalls.length;
188
+ return [3 /*break*/, 4];
189
+ case 3:
190
+ error_1 = _b.sent();
191
+ this.logger.error('Error processing tool calls:', error_1);
192
+ this.destroyStreams(error_1);
193
+ return [3 /*break*/, 4];
194
+ case 4: return [2 /*return*/];
195
+ }
196
+ });
197
+ });
198
+ };
199
+ return OpenAiStreamProcessor;
200
+ }());
201
+ exports.OpenAiStreamProcessor = OpenAiStreamProcessor;
202
+ //# sourceMappingURL=OpenAiStreamProcessor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OpenAiStreamProcessor.js","sourceRoot":"","sources":["../../src/OpenAiStreamProcessor.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACA,wCAAmD;AAEnD,iCAA6E;AAE7E;;;;GAIG;AACH;IASE,+BACE,WAAwC,EAChC,WAGc,EACtB,QAAkB;QAJV,gBAAW,GAAX,WAAW,CAGG;QAZhB,yBAAoB,GAA6C,EAAE,CAAC;QACpE,sBAAiB,GAAG,CAAC,CAAC;QACtB,oBAAe,GAAkD,IAAI,CAAC;QAa5E,IAAI,CAAC,MAAM,GAAG,IAAI,aAAM,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QAC1D,IAAI,CAAC,WAAW,GAAG,iBAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAC9C,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,mBAAmB,EAAE,CAAC;QAChD,IAAI,CAAC,YAAY,GAAG,IAAI,oBAAW,EAAE,CAAC;QACtC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC5C,CAAC;IAED;;OAEG;IACH,+CAAe,GAAf;QACE,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;IAED;;OAEG;IACK,mDAAmB,GAA3B;QAAA,iBAoCC;QAnCC,OAAO,IAAI,kBAAS,CAAC;YACnB,UAAU,EAAE,IAAI;YAChB,SAAS,EAAE,UAAC,KAA0B,EAAE,QAAgB,EAAE,QAA2B;;gBACnF,IAAI;oBACF,IAAI,KAAI,CAAC,YAAY,CAAC,SAAS,EAAE;wBAC/B,KAAI,CAAC,MAAM,CAAC,IAAI,CAAC,uEAAuE,CAAC,CAAC;wBAC1F,KAAI,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC;wBAC3B,KAAI,CAAC,aAAa,CAAC,OAAO,EAAE,CAAC;wBAC7B,OAAO;qBACR;oBAED,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE;wBAC5B,MAAM,IAAI,KAAK,CAAC,mCAA4B,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAE,CAAC,CAAC;qBAC/E;yBAAM,IAAI,MAAA,MAAA,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,0CAAE,KAAK,0CAAE,OAAO,EAAE;wBAC3C,KAAI,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;qBACxD;yBAAM,IAAI,MAAA,MAAA,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,0CAAE,KAAK,0CAAE,UAAU,EAAE;wBAC9C,KAAI,CAAC,mBAAmB,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;qBAC7D;yBAAM,IAAI,CAAA,MAAA,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,0CAAE,aAAa,MAAK,YAAY,EAAE;wBAC3D,KAAI,CAAC,eAAe,EAAE,CAAC;qBACxB;yBAAM,IAAI,CAAA,MAAA,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,0CAAE,aAAa,MAAK,MAAM,EAAE;wBACrD,KAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;qBAC9B;yBAAM,IAAI,CAAA,MAAA,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,0CAAE,aAAa,MAAK,QAAQ,EAAE;wBACvD,KAAI,CAAC,MAAM,CAAC,IAAI,CAAC,mEAAmE,CAAC,CAAC;wBACtF,KAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;qBAC9B;yBAAM,IAAI,CAAA,MAAA,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,0CAAE,aAAa,MAAK,gBAAgB,EAAE;wBAC/D,KAAI,CAAC,MAAM,CAAC,KAAK,CAAC,iEAAiE,CAAC,CAAC;wBACrF,KAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;qBAC9B;oBACD,QAAQ,EAAE,CAAC;iBACZ;gBAAC,OAAO,KAAU,EAAE;oBACnB,KAAI,CAAC,MAAM,CAAC,KAAK,CAAC,yBAAyB,EAAE,KAAK,CAAC,CAAC;oBACpD,KAAI,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;iBAC5B;YACH,CAAC;SACF,CAAC,CAAC;IACL,CAAC;IAED;;OAEG;IACK,8CAAc,GAAtB,UAAuB,KAAa;QAClC,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC;QAC3B,IAAI,CAAC,aAAa,CAAC,OAAO,EAAE,CAAC;QAC7B,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACvC,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,CAAC;IAC9B,CAAC;IAED;;;OAGG;IACK,mDAAmB,GAA3B,UAA4B,cAA2D;;QACrF,KAAoB,UAAc,EAAd,iCAAc,EAAd,4BAAc,EAAd,IAAc,EAAE;YAA/B,IAAM,KAAK,uBAAA;YACd,IAAI,KAAK,CAAC,EAAE,EAAE;gBACZ,2BAA2B;gBAC3B,IAAI,IAAI,CAAC,eAAe,EAAE;oBACxB,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;iBACtD;gBACD,IAAI,CAAC,eAAe,GAAG;oBACrB,EAAE,EAAE,KAAK,CAAC,EAAE;oBACZ,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,UAAU;oBAC9B,QAAQ,EAAE;wBACR,IAAI,EAAE,CAAA,MAAA,KAAK,CAAC,QAAQ,0CAAE,IAAI,KAAI,EAAE;wBAChC,SAAS,EAAE,CAAA,MAAA,KAAK,CAAC,QAAQ,0CAAE,SAAS,KAAI,EAAE;qBAC3C;iBACF,CAAC;aACH;iBAAM;gBACL,0CAA0C;gBAC1C,IAAI,MAAA,KAAK,CAAC,QAAQ,0CAAE,IAAI,EAAE;oBACxB,IAAI,CAAC,eAAgB,CAAC,QAAS,CAAC,IAAI,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;iBAC7D;gBACD,IAAI,MAAA,KAAK,CAAC,QAAQ,0CAAE,SAAS,EAAE;oBAC7B,IAAI,CAAC,eAAgB,CAAC,QAAS,CAAC,SAAS,IAAI,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC;iBACvE;aACF;SACF;IACH,CAAC;IAED;;;;OAIG;IACW,+CAAe,GAA7B;;;;;;;wBACE,IAAI,IAAI,CAAC,eAAe,EAAE;4BACxB,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;4BACrD,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;yBAC7B;wBAEK,kBAAkB,GAAG,IAAI,CAAC,oBAAoB,CAAC,MAAM,CACzD,UAAC,EAAE;4BACD,OAAA,EAAE,CAAC,EAAE,KAAK,SAAS,IAAI,EAAE,CAAC,QAAQ,KAAK,SAAS,IAAI,EAAE,CAAC,IAAI,KAAK,SAAS;wBAAzE,CAAyE,CAC5E,CAAC;wBAEF,IAAI,CAAC,oBAAoB,GAAG,EAAE,CAAC;wBAC/B,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC;wBAC3B,IAAI,CAAC,aAAa,CAAC,OAAO,EAAE,CAAC;wBAC7B,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,mBAAmB,EAAE,CAAC;;;;wBAG9C,KAAA,IAAI,CAAA;wBAAe,qBAAM,IAAI,CAAC,WAAW,CAAC,kBAAkB,EAAE,IAAI,CAAC,iBAAiB,CAAC,EAAA;;wBAArF,GAAK,WAAW,GAAG,SAAkE,CAAC;wBACtF,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,UAAC,KAAK,IAAK,OAAA,KAAI,CAAC,cAAc,CAAC,KAAK,CAAC,EAA1B,CAA0B,CAAC,CAAC;wBACpE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;wBAC1C,IAAI,CAAC,iBAAiB,IAAI,kBAAkB,CAAC,MAAM,CAAC;;;;wBAEpD,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,8BAA8B,EAAE,OAAK,CAAC,CAAC;wBACzD,IAAI,CAAC,cAAc,CAAC,OAAK,CAAC,CAAC;;;;;;KAE9B;IACH,4BAAC;AAAD,CAAC,AAhJD,IAgJC;AAhJY,sDAAqB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proteinjs/conversation",
3
- "version": "1.5.2",
3
+ "version": "1.6.1",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "publishConfig": {
@@ -36,5 +36,5 @@
36
36
  "tiktoken": "1.0.15",
37
37
  "typescript": "5.2.2"
38
38
  },
39
- "gitHead": "19aa49a81de90688b40224120ec611cef65e77f4"
39
+ "gitHead": "e0bc39f8f16e8e9f062194234cee2c4703336da5"
40
40
  }
@@ -209,6 +209,18 @@ export class Conversation {
209
209
  );
210
210
  }
211
211
 
212
+ async generateStreamingResponse(messages: (string | ChatCompletionMessageParam)[], model?: TiktokenModel) {
213
+ await this.enforceTokenLimit(messages, model);
214
+ return await OpenAi.generateStreamingResponse(
215
+ messages,
216
+ model,
217
+ this.history,
218
+ this.functions,
219
+ this.messageModerators,
220
+ this.params.logLevel
221
+ );
222
+ }
223
+
212
224
  async generateCode(description: string[], model?: TiktokenModel) {
213
225
  this.logger.info(`Generating code for description:\n${description.join('\n')}`);
214
226
  const code = await OpenAi.generateCode(
package/src/OpenAi.ts CHANGED
@@ -1,11 +1,19 @@
1
1
  import { OpenAI as OpenAIApi } from 'openai';
2
- import { ChatCompletionMessageParam, ChatCompletion, ChatCompletionMessageToolCall } from 'openai/resources/chat';
2
+ import {
3
+ ChatCompletionMessageParam,
4
+ ChatCompletion,
5
+ ChatCompletionMessageToolCall,
6
+ ChatCompletionChunk,
7
+ } from 'openai/resources/chat';
3
8
  import { LogLevel, Logger, isInstanceOf } from '@proteinjs/util';
4
9
  import { MessageModerator } from './history/MessageModerator';
5
10
  import { Function } from './Function';
6
11
  import { MessageHistory } from './history/MessageHistory';
7
12
  import { TiktokenModel } from 'tiktoken';
8
13
  import { ChatCompletionMessageParamFactory } from './ChatCompletionMessageParamFactory';
14
+ import { Stream } from 'openai/streaming';
15
+ import { Readable } from 'stream';
16
+ import { OpenAiStreamProcessor } from './OpenAiStreamProcessor';
9
17
 
10
18
  function delay(ms: number) {
11
19
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -22,8 +30,9 @@ export class OpenAi {
22
30
  logLevel: LogLevel = 'info',
23
31
  maxFunctionCalls: number = 50
24
32
  ): Promise<string> {
25
- return await this.generateResponseHelper(
33
+ return (await this.generateResponseHelper(
26
34
  messages,
35
+ false,
27
36
  0,
28
37
  model,
29
38
  history,
@@ -31,11 +40,34 @@ export class OpenAi {
31
40
  messageModerators,
32
41
  logLevel,
33
42
  maxFunctionCalls
34
- );
43
+ )) as string;
44
+ }
45
+
46
+ static async generateStreamingResponse(
47
+ messages: (string | ChatCompletionMessageParam)[],
48
+ model?: string,
49
+ history?: MessageHistory,
50
+ functions?: Omit<Function, 'instructions'>[],
51
+ messageModerators?: MessageModerator[],
52
+ logLevel: LogLevel = 'info',
53
+ maxFunctionCalls: number = 50
54
+ ): Promise<Readable> {
55
+ return (await this.generateResponseHelper(
56
+ messages,
57
+ true,
58
+ 0,
59
+ model,
60
+ history,
61
+ functions,
62
+ messageModerators,
63
+ logLevel,
64
+ maxFunctionCalls
65
+ )) as Readable;
35
66
  }
36
67
 
37
68
  static async generateResponseHelper(
38
69
  messages: (string | ChatCompletionMessageParam)[],
70
+ stream: boolean,
39
71
  currentFunctionCalls: number,
40
72
  model?: string,
41
73
  history?: MessageHistory,
@@ -43,38 +75,44 @@ export class OpenAi {
43
75
  messageModerators?: MessageModerator[],
44
76
  logLevel: LogLevel = 'info',
45
77
  maxFunctionCalls: number = 50
46
- ): Promise<string> {
47
- const logger = new Logger('OpenAi.generateResponse', logLevel);
48
- const messageParams: ChatCompletionMessageParam[] = messages.map((message) => {
49
- if (typeof message === 'string') {
50
- return { role: 'user', content: message };
51
- }
78
+ ): Promise<string | Readable> {
79
+ const logger = new Logger('OpenAi.generateResponseHelper', logLevel);
80
+ const updatedHistory = OpenAi.getUpdatedMessageHistory(messages, history, messageModerators);
81
+ const response = await OpenAi.executeRequest(updatedHistory, stream, logLevel, functions, model);
82
+ if (stream) {
83
+ logger.info(`Processing response stream`);
84
+ const inputStream = response as Stream<ChatCompletionChunk>;
52
85
 
53
- return message;
54
- });
55
- if (history) {
56
- history.push(messageParams);
57
- }
58
- let messageParamsWithHistory = history ? history : new MessageHistory().push(messageParams);
59
- if (messageModerators) {
60
- messageParamsWithHistory = OpenAi.moderateHistory(messageParamsWithHistory, messageModerators);
61
- }
62
- const response = await OpenAi.executeRequest(messageParamsWithHistory, logLevel, functions, model);
63
- const responseMessage = response.choices[0].message;
64
- if (responseMessage.tool_calls) {
65
- if (currentFunctionCalls >= maxFunctionCalls) {
66
- throw new Error(`Max function calls (${maxFunctionCalls}) reached. Stopping execution.`);
86
+ // For subsequent tool calls, return the raw OpenAI stream to `OpenAiStreamProcessor`
87
+ if (currentFunctionCalls > 0) {
88
+ return Readable.from(inputStream);
67
89
  }
68
90
 
69
- messageParamsWithHistory.push([responseMessage]);
70
- const toolMessageParams = await this.callTools(logLevel, responseMessage.tool_calls, functions);
71
- messageParamsWithHistory.push([...toolMessageParams]);
91
+ // For the initial call to `generateResponseHelper`, return the `OpenAiStreamProcessor` output stream
92
+ const onToolCalls = ((toolCalls, currentFunctionCalls) =>
93
+ OpenAi.handleToolCalls(
94
+ toolCalls,
95
+ true,
96
+ currentFunctionCalls,
97
+ updatedHistory,
98
+ model,
99
+ functions,
100
+ messageModerators,
101
+ logLevel,
102
+ maxFunctionCalls
103
+ )) as (toolCalls: ChatCompletionMessageToolCall[], currentFunctionCalls: number) => Promise<Readable>;
104
+ const streamProcessor = new OpenAiStreamProcessor(inputStream, onToolCalls, logLevel);
105
+ return streamProcessor.getOutputStream();
106
+ }
72
107
 
73
- return await this.generateResponseHelper(
74
- [],
75
- currentFunctionCalls + responseMessage.tool_calls.length,
108
+ const responseMessage = (response as ChatCompletion).choices[0].message;
109
+ if (responseMessage.tool_calls) {
110
+ return await OpenAi.handleToolCalls(
111
+ responseMessage.tool_calls,
112
+ stream,
113
+ currentFunctionCalls,
114
+ updatedHistory,
76
115
  model,
77
- messageParamsWithHistory,
78
116
  functions,
79
117
  messageModerators,
80
118
  logLevel,
@@ -87,10 +125,33 @@ export class OpenAi {
87
125
  throw new Error(`Response was empty for messages: ${messages.join('\n')}`);
88
126
  }
89
127
 
90
- messageParamsWithHistory.push([responseMessage]);
128
+ updatedHistory.push([responseMessage]);
91
129
  return responseText;
92
130
  }
93
131
 
132
+ private static getUpdatedMessageHistory(
133
+ messages: (string | ChatCompletionMessageParam)[],
134
+ history?: MessageHistory,
135
+ messageModerators?: MessageModerator[]
136
+ ) {
137
+ const messageParams: ChatCompletionMessageParam[] = messages.map((message) => {
138
+ if (typeof message === 'string') {
139
+ return { role: 'user', content: message };
140
+ }
141
+
142
+ return message;
143
+ });
144
+ if (history) {
145
+ history.push(messageParams);
146
+ }
147
+ let messageParamsWithHistory = history ? history : new MessageHistory().push(messageParams);
148
+ if (messageModerators) {
149
+ messageParamsWithHistory = OpenAi.moderateHistory(messageParamsWithHistory, messageModerators);
150
+ }
151
+
152
+ return messageParamsWithHistory;
153
+ }
154
+
94
155
  private static moderateHistory(history: MessageHistory, messageModerators: MessageModerator[]) {
95
156
  for (const messageModerator of messageModerators) {
96
157
  history.setMessages(messageModerator.observe(history.getMessages()));
@@ -101,24 +162,18 @@ export class OpenAi {
101
162
 
102
163
  private static async executeRequest(
103
164
  messageParamsWithHistory: MessageHistory,
165
+ stream: boolean,
104
166
  logLevel: LogLevel,
105
167
  functions?: Omit<Function, 'instructions'>[],
106
168
  model?: string
107
- ): Promise<ChatCompletion> {
169
+ ): Promise<ChatCompletion | Stream<ChatCompletionChunk>> {
108
170
  const logger = new Logger('OpenAi.executeRequest', logLevel);
109
171
  const openaiApi = new OpenAIApi();
110
- let response: ChatCompletion;
111
172
  try {
112
173
  const latestMessage = messageParamsWithHistory.getMessages()[messageParamsWithHistory.getMessages().length - 1];
113
- if (latestMessage.content) {
114
- logger.info(`Sending request: ${latestMessage.content}`);
115
- } else if (latestMessage.role == 'function') {
116
- logger.info(`Sending request: returning output of ${latestMessage.name} function`);
117
- } else {
118
- logger.info(`Sending request`);
119
- }
120
- logger.debug(`Sending messages: ${JSON.stringify(messageParamsWithHistory.getMessages(), null, 2)}`, true);
121
- response = await openaiApi.chat.completions.create({
174
+ this.logRequestDetails(logger, logLevel, latestMessage, messageParamsWithHistory);
175
+
176
+ const response = await openaiApi.chat.completions.create({
122
177
  model: model ? model : DEFAULT_MODEL,
123
178
  temperature: 0,
124
179
  messages: messageParamsWithHistory.getMessages(),
@@ -126,41 +181,130 @@ export class OpenAi {
126
181
  type: 'function',
127
182
  function: f.definition,
128
183
  })),
184
+ stream: stream,
129
185
  });
130
- const responseMessage = response.choices[0].message;
131
- if (responseMessage.content) {
132
- logger.info(`Received response: ${responseMessage.content}`);
133
- } else if (responseMessage.tool_calls) {
134
- logger.info(
135
- `Received response: call functions: ${JSON.stringify(responseMessage.tool_calls.map((toolCall) => toolCall.function.name))}`
136
- );
137
- } else {
138
- logger.info(`Received response`);
139
- }
140
- if (response.usage) {
141
- logger.info(JSON.stringify(response.usage));
142
- } else {
143
- logger.info(JSON.stringify(`Usage data missing`));
186
+
187
+ if (!stream) {
188
+ this.logResponseDetails(logger, response as ChatCompletion);
144
189
  }
190
+
191
+ return response;
145
192
  } catch (error: any) {
146
- logger.info(`Received error response, error type: ${error.type}`);
147
- if (typeof error.status !== 'undefined' && error.status == 429) {
148
- if (error.type == 'tokens' && typeof error.headers['x-ratelimit-reset-tokens'] === 'string') {
149
- const waitTime = parseInt(error.headers['x-ratelimit-reset-tokens']);
150
- const remainingTokens = error.headers['x-ratelimit-remaining-tokens'];
151
- const delayMs = 15000;
152
- logger.warn(
153
- `Waiting to retry in ${delayMs / 1000}s, token reset in: ${waitTime}s, remaining tokens: ${remainingTokens}`
154
- );
155
- await delay(delayMs);
156
- return await OpenAi.executeRequest(messageParamsWithHistory, logLevel, functions, model);
157
- }
193
+ return this.handleRequestError(logger, error, messageParamsWithHistory, stream, logLevel, functions, model);
194
+ }
195
+ }
196
+
197
+ private static logRequestDetails(
198
+ logger: Logger,
199
+ logLevel: LogLevel,
200
+ latestMessage: ChatCompletionMessageParam,
201
+ messageParamsWithHistory: MessageHistory
202
+ ) {
203
+ if (latestMessage.role == 'tool') {
204
+ logger.info(`Sending request: returning output of tool call (${latestMessage.tool_call_id})`);
205
+ } else if (latestMessage.content) {
206
+ const requestContent =
207
+ typeof latestMessage.content === 'string'
208
+ ? latestMessage.content
209
+ : latestMessage.content[0].type === 'text'
210
+ ? latestMessage.content[0].text
211
+ : 'image';
212
+ logger.info(`Sending request: ${requestContent}`);
213
+ } else {
214
+ logger.info(`Sending request`);
215
+ }
216
+
217
+ if (logLevel === 'debug') {
218
+ logger.debug(`Sending messages: ${JSON.stringify(messageParamsWithHistory.getMessages(), null, 2)}`, true);
219
+ }
220
+ }
221
+
222
+ private static logResponseDetails(logger: Logger, response: ChatCompletion) {
223
+ const responseMessage = response.choices[0].message;
224
+ if (responseMessage.content) {
225
+ logger.info(`Received response: ${responseMessage.content}`);
226
+ } else if (responseMessage.tool_calls) {
227
+ logger.info(
228
+ `Received response: call functions: ${JSON.stringify(responseMessage.tool_calls.map((toolCall) => toolCall.function.name))}`
229
+ );
230
+ } else {
231
+ logger.info(`Received response`);
232
+ }
233
+ if (response.usage) {
234
+ logger.info(JSON.stringify(response.usage));
235
+ } else {
236
+ logger.info(JSON.stringify(`Usage data missing`));
237
+ }
238
+ }
239
+
240
+ private static async handleRequestError(
241
+ logger: Logger,
242
+ error: any,
243
+ messageParamsWithHistory: MessageHistory,
244
+ stream: boolean,
245
+ logLevel: LogLevel,
246
+ functions?: Omit<Function, 'instructions'>[],
247
+ model?: string
248
+ ): Promise<ChatCompletion | Stream<ChatCompletionChunk>> {
249
+ logger.info(`Received error response, error type: ${error.type}`);
250
+ if (typeof error.status !== 'undefined' && error.status == 429) {
251
+ if (error.type == 'tokens' && typeof error.headers['x-ratelimit-reset-tokens'] === 'string') {
252
+ const waitTime = parseInt(error.headers['x-ratelimit-reset-tokens']);
253
+ const remainingTokens = error.headers['x-ratelimit-remaining-tokens'];
254
+ const delayMs = 15000;
255
+ logger.warn(
256
+ `Waiting to retry in ${delayMs / 1000}s, token reset in: ${waitTime}s, remaining tokens: ${remainingTokens}`
257
+ );
258
+ await delay(delayMs);
259
+ return await OpenAi.executeRequest(messageParamsWithHistory, stream, logLevel, functions, model);
158
260
  }
261
+ }
262
+ throw error;
263
+ }
159
264
 
160
- throw error;
265
+ private static async handleToolCalls(
266
+ toolCalls: ChatCompletionMessageToolCall[],
267
+ stream: boolean,
268
+ currentFunctionCalls: number,
269
+ history: MessageHistory,
270
+ model?: string,
271
+ functions?: Omit<Function, 'instructions'>[],
272
+ messageModerators?: MessageModerator[],
273
+ logLevel: LogLevel = 'info',
274
+ maxFunctionCalls: number = 50
275
+ ): Promise<string | Readable> {
276
+ if (currentFunctionCalls >= maxFunctionCalls) {
277
+ throw new Error(`Max function calls (${maxFunctionCalls}) reached. Stopping execution.`);
161
278
  }
162
279
 
163
- return response;
280
+ // Create a message for the tool calls
281
+ const toolCallMessage: ChatCompletionMessageParam = {
282
+ role: 'assistant',
283
+ content: null,
284
+ tool_calls: toolCalls,
285
+ };
286
+
287
+ // Add the tool call message to the history
288
+ history.push([toolCallMessage]);
289
+
290
+ // Call the tools and get the responses
291
+ const toolMessageParams = await this.callTools(logLevel, toolCalls, functions);
292
+
293
+ // Add the tool responses to the history
294
+ history.push(toolMessageParams);
295
+
296
+ // Generate the next response
297
+ return this.generateResponseHelper(
298
+ [],
299
+ stream,
300
+ currentFunctionCalls + toolCalls.length,
301
+ model,
302
+ history,
303
+ functions,
304
+ messageModerators,
305
+ logLevel,
306
+ maxFunctionCalls
307
+ );
164
308
  }
165
309
 
166
310
  private static async callTools(
@@ -201,8 +345,12 @@ export class OpenAi {
201
345
  }
202
346
 
203
347
  try {
204
- logger.info(`Assistant calling function: (${toolCallId}) ${f.definition.name}(${functionCall.arguments})`, 1000);
205
- const returnObject = await f.call(JSON.parse(functionCall.arguments));
348
+ const parsedArguments = JSON.parse(functionCall.arguments);
349
+ logger.info(
350
+ `Assistant calling function: (${toolCallId}) ${f.definition.name}(${JSON.stringify(parsedArguments, null, 2)})`,
351
+ 1000
352
+ );
353
+ const returnObject = await f.call(parsedArguments);
206
354
 
207
355
  const returnObjectCompletionParams: ChatCompletionMessageParam[] = [];
208
356
  if (isInstanceOf(returnObject, ChatCompletionMessageParamFactory)) {
@@ -216,7 +364,7 @@ export class OpenAi {
216
364
  };
217
365
  returnObjectCompletionParams.push(instructionMessageParam, ...messageParams);
218
366
  logger.info(
219
- `Assistant called function: (${toolCallId}) ${f.definition.name} => ${JSON.stringify(messageParams)}`,
367
+ `Assistant called function: (${toolCallId}) ${f.definition.name} => ${JSON.stringify(messageParams, null, 2)}`,
220
368
  500
221
369
  );
222
370
  } else {
@@ -228,7 +376,7 @@ export class OpenAi {
228
376
  content: serializedReturnObject,
229
377
  });
230
378
  logger.info(
231
- `Assistant called function: (${toolCallId}) ${f.definition.name} => ${serializedReturnObject}`,
379
+ `Assistant called function: (${toolCallId}) ${f.definition.name} => ${JSON.stringify(returnObject, null, 2)}`,
232
380
  1000
233
381
  );
234
382
  }
@@ -245,9 +393,9 @@ export class OpenAi {
245
393
 
246
394
  return returnObjectCompletionParams;
247
395
  } catch (error: any) {
248
- const errorMessage = `Error occurred while executing function ${f.definition.name}: ${error.message}`;
396
+ const errorMessage = `Error occurred while executing function ${f.definition.name}: (${toolCallId}) ${error.message}`;
249
397
  logger.error(errorMessage);
250
- return [{ role: 'tool', tool_call_id: toolCallId, content: JSON.stringify({ error: errorMessage }) }];
398
+ throw error;
251
399
  }
252
400
  }
253
401