AbstractChatCompletionRunner.mjs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. var _AbstractChatCompletionRunner_instances, _AbstractChatCompletionRunner_getFinalContent, _AbstractChatCompletionRunner_getFinalMessage, _AbstractChatCompletionRunner_getFinalFunctionToolCall, _AbstractChatCompletionRunner_getFinalFunctionToolCallResult, _AbstractChatCompletionRunner_calculateTotalUsage, _AbstractChatCompletionRunner_validateParams, _AbstractChatCompletionRunner_stringifyFunctionCallResult;
  2. import { __classPrivateFieldGet } from "../internal/tslib.mjs";
  3. import { OpenAIError } from "../error.mjs";
  4. import { isAutoParsableTool, parseChatCompletion } from "../lib/parser.mjs";
  5. import { isAssistantMessage, isToolMessage } from "./chatCompletionUtils.mjs";
  6. import { EventStream } from "./EventStream.mjs";
  7. import { isRunnableFunctionWithParse, } from "./RunnableFunction.mjs";
  8. const DEFAULT_MAX_CHAT_COMPLETIONS = 10;
  9. export class AbstractChatCompletionRunner extends EventStream {
  10. constructor() {
  11. super(...arguments);
  12. _AbstractChatCompletionRunner_instances.add(this);
  13. this._chatCompletions = [];
  14. this.messages = [];
  15. }
  16. _addChatCompletion(chatCompletion) {
  17. this._chatCompletions.push(chatCompletion);
  18. this._emit('chatCompletion', chatCompletion);
  19. const message = chatCompletion.choices[0]?.message;
  20. if (message)
  21. this._addMessage(message);
  22. return chatCompletion;
  23. }
  24. _addMessage(message, emit = true) {
  25. if (!('content' in message))
  26. message.content = null;
  27. this.messages.push(message);
  28. if (emit) {
  29. this._emit('message', message);
  30. if (isToolMessage(message) && message.content) {
  31. // Note, this assumes that {role: 'tool', content: …} is always the result of a call of tool of type=function.
  32. this._emit('functionToolCallResult', message.content);
  33. }
  34. else if (isAssistantMessage(message) && message.tool_calls) {
  35. for (const tool_call of message.tool_calls) {
  36. if (tool_call.type === 'function') {
  37. this._emit('functionToolCall', tool_call.function);
  38. }
  39. }
  40. }
  41. }
  42. }
  43. /**
  44. * @returns a promise that resolves with the final ChatCompletion, or rejects
  45. * if an error occurred or the stream ended prematurely without producing a ChatCompletion.
  46. */
  47. async finalChatCompletion() {
  48. await this.done();
  49. const completion = this._chatCompletions[this._chatCompletions.length - 1];
  50. if (!completion)
  51. throw new OpenAIError('stream ended without producing a ChatCompletion');
  52. return completion;
  53. }
  54. /**
  55. * @returns a promise that resolves with the content of the final ChatCompletionMessage, or rejects
  56. * if an error occurred or the stream ended prematurely without producing a ChatCompletionMessage.
  57. */
  58. async finalContent() {
  59. await this.done();
  60. return __classPrivateFieldGet(this, _AbstractChatCompletionRunner_instances, "m", _AbstractChatCompletionRunner_getFinalContent).call(this);
  61. }
  62. /**
  63. * @returns a promise that resolves with the the final assistant ChatCompletionMessage response,
  64. * or rejects if an error occurred or the stream ended prematurely without producing a ChatCompletionMessage.
  65. */
  66. async finalMessage() {
  67. await this.done();
  68. return __classPrivateFieldGet(this, _AbstractChatCompletionRunner_instances, "m", _AbstractChatCompletionRunner_getFinalMessage).call(this);
  69. }
  70. /**
  71. * @returns a promise that resolves with the content of the final FunctionCall, or rejects
  72. * if an error occurred or the stream ended prematurely without producing a ChatCompletionMessage.
  73. */
  74. async finalFunctionToolCall() {
  75. await this.done();
  76. return __classPrivateFieldGet(this, _AbstractChatCompletionRunner_instances, "m", _AbstractChatCompletionRunner_getFinalFunctionToolCall).call(this);
  77. }
  78. async finalFunctionToolCallResult() {
  79. await this.done();
  80. return __classPrivateFieldGet(this, _AbstractChatCompletionRunner_instances, "m", _AbstractChatCompletionRunner_getFinalFunctionToolCallResult).call(this);
  81. }
  82. async totalUsage() {
  83. await this.done();
  84. return __classPrivateFieldGet(this, _AbstractChatCompletionRunner_instances, "m", _AbstractChatCompletionRunner_calculateTotalUsage).call(this);
  85. }
  86. allChatCompletions() {
  87. return [...this._chatCompletions];
  88. }
  89. _emitFinal() {
  90. const completion = this._chatCompletions[this._chatCompletions.length - 1];
  91. if (completion)
  92. this._emit('finalChatCompletion', completion);
  93. const finalMessage = __classPrivateFieldGet(this, _AbstractChatCompletionRunner_instances, "m", _AbstractChatCompletionRunner_getFinalMessage).call(this);
  94. if (finalMessage)
  95. this._emit('finalMessage', finalMessage);
  96. const finalContent = __classPrivateFieldGet(this, _AbstractChatCompletionRunner_instances, "m", _AbstractChatCompletionRunner_getFinalContent).call(this);
  97. if (finalContent)
  98. this._emit('finalContent', finalContent);
  99. const finalFunctionCall = __classPrivateFieldGet(this, _AbstractChatCompletionRunner_instances, "m", _AbstractChatCompletionRunner_getFinalFunctionToolCall).call(this);
  100. if (finalFunctionCall)
  101. this._emit('finalFunctionToolCall', finalFunctionCall);
  102. const finalFunctionCallResult = __classPrivateFieldGet(this, _AbstractChatCompletionRunner_instances, "m", _AbstractChatCompletionRunner_getFinalFunctionToolCallResult).call(this);
  103. if (finalFunctionCallResult != null)
  104. this._emit('finalFunctionToolCallResult', finalFunctionCallResult);
  105. if (this._chatCompletions.some((c) => c.usage)) {
  106. this._emit('totalUsage', __classPrivateFieldGet(this, _AbstractChatCompletionRunner_instances, "m", _AbstractChatCompletionRunner_calculateTotalUsage).call(this));
  107. }
  108. }
  109. async _createChatCompletion(client, params, options) {
  110. const signal = options?.signal;
  111. if (signal) {
  112. if (signal.aborted)
  113. this.controller.abort();
  114. signal.addEventListener('abort', () => this.controller.abort());
  115. }
  116. __classPrivateFieldGet(this, _AbstractChatCompletionRunner_instances, "m", _AbstractChatCompletionRunner_validateParams).call(this, params);
  117. const chatCompletion = await client.chat.completions.create({ ...params, stream: false }, { ...options, signal: this.controller.signal });
  118. this._connected();
  119. return this._addChatCompletion(parseChatCompletion(chatCompletion, params));
  120. }
  121. async _runChatCompletion(client, params, options) {
  122. for (const message of params.messages) {
  123. this._addMessage(message, false);
  124. }
  125. return await this._createChatCompletion(client, params, options);
  126. }
  127. async _runTools(client, params, options) {
  128. const role = 'tool';
  129. const { tool_choice = 'auto', stream, ...restParams } = params;
  130. const singleFunctionToCall = typeof tool_choice !== 'string' && tool_choice.type === 'function' && tool_choice?.function?.name;
  131. const { maxChatCompletions = DEFAULT_MAX_CHAT_COMPLETIONS } = options || {};
  132. // TODO(someday): clean this logic up
  133. const inputTools = params.tools.map((tool) => {
  134. if (isAutoParsableTool(tool)) {
  135. if (!tool.$callback) {
  136. throw new OpenAIError('Tool given to `.runTools()` that does not have an associated function');
  137. }
  138. return {
  139. type: 'function',
  140. function: {
  141. function: tool.$callback,
  142. name: tool.function.name,
  143. description: tool.function.description || '',
  144. parameters: tool.function.parameters,
  145. parse: tool.$parseRaw,
  146. strict: true,
  147. },
  148. };
  149. }
  150. return tool;
  151. });
  152. const functionsByName = {};
  153. for (const f of inputTools) {
  154. if (f.type === 'function') {
  155. functionsByName[f.function.name || f.function.function.name] = f.function;
  156. }
  157. }
  158. const tools = 'tools' in params ?
  159. inputTools.map((t) => t.type === 'function' ?
  160. {
  161. type: 'function',
  162. function: {
  163. name: t.function.name || t.function.function.name,
  164. parameters: t.function.parameters,
  165. description: t.function.description,
  166. strict: t.function.strict,
  167. },
  168. }
  169. : t)
  170. : undefined;
  171. for (const message of params.messages) {
  172. this._addMessage(message, false);
  173. }
  174. for (let i = 0; i < maxChatCompletions; ++i) {
  175. const chatCompletion = await this._createChatCompletion(client, {
  176. ...restParams,
  177. tool_choice,
  178. tools,
  179. messages: [...this.messages],
  180. }, options);
  181. const message = chatCompletion.choices[0]?.message;
  182. if (!message) {
  183. throw new OpenAIError(`missing message in ChatCompletion response`);
  184. }
  185. if (!message.tool_calls?.length) {
  186. return;
  187. }
  188. for (const tool_call of message.tool_calls) {
  189. if (tool_call.type !== 'function')
  190. continue;
  191. const tool_call_id = tool_call.id;
  192. const { name, arguments: args } = tool_call.function;
  193. const fn = functionsByName[name];
  194. if (!fn) {
  195. const content = `Invalid tool_call: ${JSON.stringify(name)}. Available options are: ${Object.keys(functionsByName)
  196. .map((name) => JSON.stringify(name))
  197. .join(', ')}. Please try again`;
  198. this._addMessage({ role, tool_call_id, content });
  199. continue;
  200. }
  201. else if (singleFunctionToCall && singleFunctionToCall !== name) {
  202. const content = `Invalid tool_call: ${JSON.stringify(name)}. ${JSON.stringify(singleFunctionToCall)} requested. Please try again`;
  203. this._addMessage({ role, tool_call_id, content });
  204. continue;
  205. }
  206. let parsed;
  207. try {
  208. parsed = isRunnableFunctionWithParse(fn) ? await fn.parse(args) : args;
  209. }
  210. catch (error) {
  211. const content = error instanceof Error ? error.message : String(error);
  212. this._addMessage({ role, tool_call_id, content });
  213. continue;
  214. }
  215. // @ts-expect-error it can't rule out `never` type.
  216. const rawContent = await fn.function(parsed, this);
  217. const content = __classPrivateFieldGet(this, _AbstractChatCompletionRunner_instances, "m", _AbstractChatCompletionRunner_stringifyFunctionCallResult).call(this, rawContent);
  218. this._addMessage({ role, tool_call_id, content });
  219. if (singleFunctionToCall) {
  220. return;
  221. }
  222. }
  223. }
  224. return;
  225. }
  226. }
  227. _AbstractChatCompletionRunner_instances = new WeakSet(), _AbstractChatCompletionRunner_getFinalContent = function _AbstractChatCompletionRunner_getFinalContent() {
  228. return __classPrivateFieldGet(this, _AbstractChatCompletionRunner_instances, "m", _AbstractChatCompletionRunner_getFinalMessage).call(this).content ?? null;
  229. }, _AbstractChatCompletionRunner_getFinalMessage = function _AbstractChatCompletionRunner_getFinalMessage() {
  230. let i = this.messages.length;
  231. while (i-- > 0) {
  232. const message = this.messages[i];
  233. if (isAssistantMessage(message)) {
  234. // TODO: support audio here
  235. const ret = {
  236. ...message,
  237. content: message.content ?? null,
  238. refusal: message.refusal ?? null,
  239. };
  240. return ret;
  241. }
  242. }
  243. throw new OpenAIError('stream ended without producing a ChatCompletionMessage with role=assistant');
  244. }, _AbstractChatCompletionRunner_getFinalFunctionToolCall = function _AbstractChatCompletionRunner_getFinalFunctionToolCall() {
  245. for (let i = this.messages.length - 1; i >= 0; i--) {
  246. const message = this.messages[i];
  247. if (isAssistantMessage(message) && message?.tool_calls?.length) {
  248. return message.tool_calls.filter((x) => x.type === 'function').at(-1)?.function;
  249. }
  250. }
  251. return;
  252. }, _AbstractChatCompletionRunner_getFinalFunctionToolCallResult = function _AbstractChatCompletionRunner_getFinalFunctionToolCallResult() {
  253. for (let i = this.messages.length - 1; i >= 0; i--) {
  254. const message = this.messages[i];
  255. if (isToolMessage(message) &&
  256. message.content != null &&
  257. typeof message.content === 'string' &&
  258. this.messages.some((x) => x.role === 'assistant' &&
  259. x.tool_calls?.some((y) => y.type === 'function' && y.id === message.tool_call_id))) {
  260. return message.content;
  261. }
  262. }
  263. return;
  264. }, _AbstractChatCompletionRunner_calculateTotalUsage = function _AbstractChatCompletionRunner_calculateTotalUsage() {
  265. const total = {
  266. completion_tokens: 0,
  267. prompt_tokens: 0,
  268. total_tokens: 0,
  269. };
  270. for (const { usage } of this._chatCompletions) {
  271. if (usage) {
  272. total.completion_tokens += usage.completion_tokens;
  273. total.prompt_tokens += usage.prompt_tokens;
  274. total.total_tokens += usage.total_tokens;
  275. }
  276. }
  277. return total;
  278. }, _AbstractChatCompletionRunner_validateParams = function _AbstractChatCompletionRunner_validateParams(params) {
  279. if (params.n != null && params.n > 1) {
  280. throw new OpenAIError('ChatCompletion convenience helpers only support n=1 at this time. To use n>1, please use chat.completions.create() directly.');
  281. }
  282. }, _AbstractChatCompletionRunner_stringifyFunctionCallResult = function _AbstractChatCompletionRunner_stringifyFunctionCallResult(rawContent) {
  283. return (typeof rawContent === 'string' ? rawContent
  284. : rawContent === undefined ? 'undefined'
  285. : JSON.stringify(rawContent));
  286. };
  287. //# sourceMappingURL=AbstractChatCompletionRunner.mjs.map