@jaypie/llm 1.1.6 → 1.1.8

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.
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import { BadGatewayError, NotImplementedError, ConfigurationError as ConfigurationError$1 } from '@jaypie/errors';
2
- import { log, JAYPIE, ConfigurationError, placeholders, sleep } from '@jaypie/core';
1
+ import { ConfigurationError, BadGatewayError, TooManyRequestsError, NotImplementedError } from '@jaypie/errors';
2
+ import { placeholders, log as log$1, JAYPIE, sleep, ConfigurationError as ConfigurationError$1 } from '@jaypie/core';
3
3
  import { OpenAI, APIUserAbortError, AuthenticationError, BadRequestError, ConflictError, NotFoundError, PermissionDeniedError, RateLimitError, UnprocessableEntityError, APIConnectionError, APIConnectionTimeoutError, InternalServerError } from 'openai';
4
4
  import { zodResponseFormat } from 'openai/helpers/zod';
5
5
  import { z } from 'zod';
6
- import { getEnvSecret } from '@jaypie/aws';
7
6
  import RandomLib from 'random';
7
+ import { getEnvSecret } from '@jaypie/aws';
8
8
  import { fetchWeatherApi } from 'openmeteo';
9
9
 
10
10
  const PROVIDER = {
@@ -44,6 +44,171 @@ var constants = /*#__PURE__*/Object.freeze({
44
44
  PROVIDER: PROVIDER
45
45
  });
46
46
 
47
+ const DEFAULT_TOOL_TYPE = "function";
48
+ class Toolkit {
49
+ constructor(tools, options) {
50
+ this._tools = tools;
51
+ this._options = options || {};
52
+ this.explain = this._options.explain ? true : false;
53
+ }
54
+ get tools() {
55
+ return this._tools.map((tool) => {
56
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
+ const toolCopy = { ...tool };
58
+ delete toolCopy.call;
59
+ if (this.explain && toolCopy.parameters?.type === "object") {
60
+ if (toolCopy.parameters?.properties) {
61
+ if (!toolCopy.parameters.properties.__Explanation) {
62
+ toolCopy.parameters.properties.__Explanation = {
63
+ type: "string",
64
+ description: "Explain the reasoning behind this function call. What is the expected result? Describe possible next steps and the conditions under which they should be taken.",
65
+ };
66
+ }
67
+ }
68
+ }
69
+ // Set default type if not provided
70
+ if (!toolCopy.type) {
71
+ toolCopy.type = DEFAULT_TOOL_TYPE;
72
+ }
73
+ return toolCopy;
74
+ });
75
+ }
76
+ async call({ name, arguments: args }) {
77
+ const tool = this._tools.find((t) => t.name === name);
78
+ if (!tool) {
79
+ throw new Error(`Tool '${name}' not found`);
80
+ }
81
+ let parsedArgs;
82
+ try {
83
+ parsedArgs = JSON.parse(args);
84
+ if (this.explain) {
85
+ delete parsedArgs.__Explanation;
86
+ }
87
+ }
88
+ catch {
89
+ parsedArgs = args;
90
+ }
91
+ const result = tool.call(parsedArgs);
92
+ if (result instanceof Promise) {
93
+ return await result;
94
+ }
95
+ return result;
96
+ }
97
+ }
98
+
99
+ // Enums
100
+ var LlmMessageRole;
101
+ (function (LlmMessageRole) {
102
+ LlmMessageRole["Assistant"] = "assistant";
103
+ LlmMessageRole["Developer"] = "developer";
104
+ LlmMessageRole["System"] = "system";
105
+ LlmMessageRole["User"] = "user";
106
+ })(LlmMessageRole || (LlmMessageRole = {}));
107
+ var LlmMessageType;
108
+ (function (LlmMessageType) {
109
+ LlmMessageType["FunctionCall"] = "function_call";
110
+ LlmMessageType["FunctionCallOutput"] = "function_call_output";
111
+ LlmMessageType["InputFile"] = "input_file";
112
+ LlmMessageType["InputImage"] = "input_image";
113
+ LlmMessageType["InputText"] = "input_text";
114
+ LlmMessageType["ItemReference"] = "item_reference";
115
+ LlmMessageType["Message"] = "message";
116
+ LlmMessageType["OutputText"] = "output_text";
117
+ LlmMessageType["Refusal"] = "refusal";
118
+ })(LlmMessageType || (LlmMessageType = {}));
119
+ var LlmResponseStatus;
120
+ (function (LlmResponseStatus) {
121
+ LlmResponseStatus["Completed"] = "completed";
122
+ LlmResponseStatus["Incomplete"] = "incomplete";
123
+ LlmResponseStatus["InProgress"] = "in_progress";
124
+ })(LlmResponseStatus || (LlmResponseStatus = {}));
125
+
126
+ /**
127
+ * Converts a string to a standardized LlmInputMessage
128
+ * @param input - String to format
129
+ * @param options - Optional configuration
130
+ * @param options.data - Data to use for placeholder substitution
131
+ * @param options.role - Role to use for the message (defaults to User)
132
+ * @returns LlmInputMessage
133
+ */
134
+ function formatOperateMessage(input, options) {
135
+ const content = options?.data ? placeholders(input, options.data) : input;
136
+ return {
137
+ content,
138
+ role: options?.role || LlmMessageRole.User,
139
+ type: LlmMessageType.Message,
140
+ };
141
+ }
142
+
143
+ /**
144
+ * Converts various input types to a standardized LlmHistory format
145
+ * @param input - String, LlmInputMessage, or LlmHistory to format
146
+ * @param options - Optional configuration
147
+ * @param options.data - Data to use for placeholder substitution
148
+ * @param options.role - Role to use when converting a string to LlmInputMessage (defaults to User)
149
+ * @returns Standardized LlmHistory array
150
+ */
151
+ function formatOperateInput(input, options) {
152
+ // If input is already LlmHistory, return it
153
+ if (Array.isArray(input)) {
154
+ return input;
155
+ }
156
+ // If input is a string, convert it to LlmInputMessage
157
+ if (typeof input === "string") {
158
+ return [formatOperateMessage(input, options)];
159
+ }
160
+ // If input is LlmInputMessage, apply placeholders if data is provided
161
+ if (options?.data && typeof input.content === "string") {
162
+ return [
163
+ formatOperateMessage(input.content, {
164
+ data: options.data,
165
+ role: input.role || options?.role,
166
+ }),
167
+ ];
168
+ }
169
+ // Otherwise, just wrap the input in an array
170
+ return [input];
171
+ }
172
+
173
+ const getLogger$1 = () => log$1.lib({ lib: JAYPIE.LIB.LLM });
174
+ const log = getLogger$1();
175
+
176
+ // Turn policy constants
177
+ const MAX_TURNS_ABSOLUTE_LIMIT = 72;
178
+ const MAX_TURNS_DEFAULT_LIMIT = 12;
179
+ /**
180
+ * Determines the maximum number of turns based on the provided options
181
+ *
182
+ * @param options - The LLM operate options
183
+ * @returns The maximum number of turns
184
+ */
185
+ function maxTurnsFromOptions(options) {
186
+ // Default to single turn (1) when turns are disabled
187
+ // Handle the turns parameter
188
+ if (options.turns === undefined) {
189
+ // Default to default limit when undefined
190
+ return MAX_TURNS_DEFAULT_LIMIT;
191
+ }
192
+ else if (options.turns === true) {
193
+ // Explicitly set to true
194
+ return MAX_TURNS_DEFAULT_LIMIT;
195
+ }
196
+ else if (typeof options.turns === "number") {
197
+ if (options.turns > 0) {
198
+ // Positive number - use that limit (capped at absolute limit)
199
+ return Math.min(options.turns, MAX_TURNS_ABSOLUTE_LIMIT);
200
+ }
201
+ else if (options.turns < 0) {
202
+ // Negative number - use default limit
203
+ return MAX_TURNS_DEFAULT_LIMIT;
204
+ }
205
+ // If turns is 0, return 1 (disabled)
206
+ return 1;
207
+ }
208
+ // All other values (false, null, etc.) will return 1 (disabled)
209
+ return 1;
210
+ }
211
+
47
212
  function naturalZodSchema(definition) {
48
213
  if (Array.isArray(definition)) {
49
214
  if (definition.length === 0) {
@@ -106,129 +271,190 @@ function naturalZodSchema(definition) {
106
271
  }
107
272
  }
108
273
 
109
- // Logger
110
- const getLogger = () => log.lib({ lib: JAYPIE.LIB.LLM });
111
- // Client initialization
112
- async function initializeClient({ apiKey, } = {}) {
113
- const logger = getLogger();
114
- const resolvedApiKey = apiKey || (await getEnvSecret("OPENAI_API_KEY"));
115
- if (!resolvedApiKey) {
116
- throw new ConfigurationError("The application could not resolve the requested keys");
274
+ //
275
+ // Constants
276
+ //
277
+ const DEFAULT_MIN = 0;
278
+ const DEFAULT_MAX = 1;
279
+ //
280
+ // Helper Functions
281
+ //
282
+ /**
283
+ * Clamps a number between optional minimum and maximum values
284
+ * @param num - The number to clamp
285
+ * @param min - Optional minimum value
286
+ * @param max - Optional maximum value
287
+ * @returns The clamped number
288
+ */
289
+ const clamp = (num, min, max) => {
290
+ let result = num;
291
+ if (typeof min === "number")
292
+ result = Math.max(result, min);
293
+ if (typeof max === "number")
294
+ result = Math.min(result, max);
295
+ return result;
296
+ };
297
+ /**
298
+ * Validates that min is not greater than max
299
+ * @param min - Optional minimum value
300
+ * @param max - Optional maximum value
301
+ * @throws {ConfigurationError} If min is greater than max
302
+ */
303
+ const validateBounds = (min, max) => {
304
+ if (typeof min === "number" && typeof max === "number" && min > max) {
305
+ throw new ConfigurationError(`Invalid bounds: min (${min}) cannot be greater than max (${max})`);
117
306
  }
118
- const client = new OpenAI({ apiKey: resolvedApiKey });
119
- logger.trace("Initialized OpenAI client");
120
- return client;
121
- }
122
- function formatSystemMessage(systemPrompt, { data, placeholders: placeholders$1 } = {}) {
123
- const content = placeholders$1?.system === false
124
- ? systemPrompt
125
- : placeholders(systemPrompt, data);
126
- return {
127
- role: "developer",
128
- content,
129
- };
130
- }
131
- function formatUserMessage(message, { data, placeholders: placeholders$1 } = {}) {
132
- const content = placeholders$1?.message === false
133
- ? message
134
- : placeholders(message, data);
135
- return {
136
- role: "user",
137
- content,
307
+ };
308
+ //
309
+ // Main
310
+ //
311
+ /**
312
+ * Creates a random number generator with optional seeding
313
+ *
314
+ * @param defaultSeed - Seed string for the default RNG
315
+ * @returns A function that generates random numbers based on provided options
316
+ *
317
+ * @example
318
+ * const rng = random("default-seed");
319
+ *
320
+ * // Generate a random float between 0 and 1
321
+ * const basic = rng();
322
+ *
323
+ * // Generate an integer between 1 and 10
324
+ * const integer = rng({ min: 1, max: 10, integer: true });
325
+ *
326
+ * // Generate from normal distribution
327
+ * const normal = rng({ mean: 50, stddev: 10 });
328
+ *
329
+ * // Use consistent seeding
330
+ * const seeded = rng({ seed: "my-seed" });
331
+ */
332
+ function random$1(defaultSeed) {
333
+ // Initialize default seeded RNG
334
+ const defaultRng = RandomLib.clone(defaultSeed);
335
+ // Store per-seed RNGs
336
+ const seedMap = new Map();
337
+ const rngFn = ({ min, max: providedMax, mean, stddev, integer = false, start = 0, seed, precision, currency = false, } = {}) => {
338
+ // Select the appropriate RNG based on seed
339
+ const rng = seed
340
+ ? seedMap.get(seed) ||
341
+ (() => {
342
+ const newRng = RandomLib.clone(seed);
343
+ seedMap.set(seed, newRng);
344
+ return newRng;
345
+ })()
346
+ : defaultRng;
347
+ // If only min is set, set max to min*2, but keep track of whether max was provided
348
+ let max = providedMax;
349
+ if (typeof min === "number" && typeof max !== "number") {
350
+ max = min * 2;
351
+ }
352
+ validateBounds(min, max);
353
+ // Determine effective precision based on parameters
354
+ const getEffectivePrecision = () => {
355
+ if (integer)
356
+ return 0;
357
+ const precisions = [];
358
+ if (typeof precision === "number" && precision >= 0) {
359
+ precisions.push(precision);
360
+ }
361
+ if (currency) {
362
+ precisions.push(2);
363
+ }
364
+ // Return the lowest precision if any are set, undefined otherwise
365
+ return precisions.length > 0 ? Math.min(...precisions) : undefined;
366
+ };
367
+ // Helper function to apply precision
368
+ const applyPrecision = (value) => {
369
+ const effectivePrecision = getEffectivePrecision();
370
+ if (typeof effectivePrecision === "number") {
371
+ const factor = Math.pow(10, effectivePrecision);
372
+ return Math.round(value * factor) / factor;
373
+ }
374
+ return value;
375
+ };
376
+ // Use normal distribution if both mean and stddev are provided
377
+ if (typeof mean === "number" && typeof stddev === "number") {
378
+ const normalDist = rng.normal(mean, stddev);
379
+ const value = normalDist();
380
+ // Only clamp if min/max are defined
381
+ const clampedValue = clamp(value, min, providedMax);
382
+ // Switch to uniform distribution only if both bounds were explicitly provided and exceeded
383
+ if (typeof min === "number" &&
384
+ typeof providedMax === "number" &&
385
+ clampedValue !== value) {
386
+ const baseValue = integer ? rng.int(min, max) : rng.float(min, max);
387
+ return applyPrecision(start + baseValue);
388
+ }
389
+ return applyPrecision(start + (integer ? Math.round(clampedValue) : clampedValue));
390
+ }
391
+ // For uniform distribution, use defaults if min/max are undefined
392
+ const uniformMin = typeof min === "number" ? min : DEFAULT_MIN;
393
+ const uniformMax = typeof max === "number" ? max : DEFAULT_MAX;
394
+ const baseValue = integer
395
+ ? rng.int(uniformMin, uniformMax)
396
+ : rng.float(uniformMin, uniformMax);
397
+ return applyPrecision(start + baseValue);
138
398
  };
399
+ return rngFn;
139
400
  }
140
- function prepareMessages(message, { system, data, placeholders } = {}) {
141
- const logger = getLogger();
142
- const messages = [];
143
- if (system) {
144
- const systemMessage = formatSystemMessage(system, { data, placeholders });
145
- messages.push(systemMessage);
146
- logger.trace(`System message: ${systemMessage.content?.length} characters`);
401
+
402
+ /**
403
+ * Helper function to safely call a function that might throw
404
+ * @param fn Function to call safely
405
+ */
406
+ function callSafely(fn) {
407
+ try {
408
+ fn();
409
+ }
410
+ catch {
411
+ // Silently catch any errors from the function
147
412
  }
148
- const userMessage = formatUserMessage(message, { data, placeholders });
149
- messages.push(userMessage);
150
- logger.trace(`User message: ${userMessage.content?.length} characters`);
151
- return messages;
152
- }
153
- // Completion requests
154
- async function createStructuredCompletion(client, { messages, responseSchema, model, }) {
155
- const logger = getLogger();
156
- logger.trace("Using structured output");
157
- const zodSchema = responseSchema instanceof z.ZodType
158
- ? responseSchema
159
- : naturalZodSchema(responseSchema);
160
- const completion = await client.beta.chat.completions.parse({
161
- messages,
162
- model,
163
- response_format: zodResponseFormat(zodSchema, "response"),
164
- });
165
- logger.var({ assistantReply: completion.choices[0].message.parsed });
166
- return completion.choices[0].message.parsed;
167
- }
168
- async function createTextCompletion(client, { messages, model, }) {
169
- const logger = getLogger();
170
- logger.trace("Using text output (unstructured)");
171
- const completion = await client.chat.completions.create({
172
- messages,
173
- model,
174
- });
175
- logger.trace(`Assistant reply: ${completion.choices[0]?.message?.content?.length} characters`);
176
- return completion.choices[0]?.message?.content || "";
177
413
  }
178
-
179
- const DEFAULT_TOOL_TYPE = "function";
180
- class Toolkit {
181
- constructor(tools, options) {
182
- this._tools = tools;
183
- this._options = options || {};
184
- this.explain = this._options.explain ? true : false;
414
+ /**
415
+ * Attempts to parse a value as a number. Returns the original input if parsing fails or results in NaN.
416
+ * @param input - The value to attempt to parse as a number
417
+ * @param options - Optional configuration
418
+ * @param options.defaultValue - Default value to return if parsing fails or results in NaN
419
+ * @param options.warnFunction - Function to call with warning message if parsing fails or results in NaN
420
+ * @returns The parsed number, defaultValue (if specified and parsing fails), or the original input
421
+ */
422
+ function tryParseNumber(input, options) {
423
+ if (input === null || input === undefined) {
424
+ return input;
185
425
  }
186
- get tools() {
187
- return this._tools.map((tool) => {
188
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
189
- const toolCopy = { ...tool };
190
- delete toolCopy.call;
191
- if (this.explain && toolCopy.parameters?.type === "object") {
192
- if (toolCopy.parameters?.properties) {
193
- if (!toolCopy.parameters.properties.__Explanation) {
194
- toolCopy.parameters.properties.__Explanation = {
195
- type: "string",
196
- description: "Explain the reasoning behind this function call. What is the expected result? Describe possible next steps and the conditions under which they should be taken.",
197
- };
198
- }
199
- }
426
+ try {
427
+ const parsed = Number(input);
428
+ if (Number.isNaN(parsed)) {
429
+ if (options?.warnFunction) {
430
+ const warningMessage = `Failed to parse "${String(input)}" as number`;
431
+ callSafely(() => options.warnFunction(warningMessage));
200
432
  }
201
- // Set default type if not provided
202
- if (!toolCopy.type) {
203
- toolCopy.type = DEFAULT_TOOL_TYPE;
204
- }
205
- return toolCopy;
206
- });
207
- }
208
- async call({ name, arguments: args }) {
209
- const tool = this._tools.find((t) => t.name === name);
210
- if (!tool) {
211
- throw new Error(`Tool '${name}' not found`);
433
+ return typeof options?.defaultValue === "number"
434
+ ? options.defaultValue
435
+ : input;
212
436
  }
213
- let parsedArgs;
214
- try {
215
- parsedArgs = JSON.parse(args);
216
- if (this.explain) {
217
- delete parsedArgs.__Explanation;
437
+ return parsed;
438
+ }
439
+ catch (error) {
440
+ if (options?.warnFunction) {
441
+ let errorMessage = "";
442
+ if (error instanceof Error) {
443
+ errorMessage = error.message;
218
444
  }
445
+ const warningMessage = `Error parsing "${String(input)}" as number${errorMessage ? "; " + errorMessage : ""}`;
446
+ callSafely(() => options.warnFunction(warningMessage));
219
447
  }
220
- catch {
221
- parsedArgs = args;
222
- }
223
- const result = tool.call(parsedArgs);
224
- if (result instanceof Promise) {
225
- return await result;
226
- }
227
- return result;
448
+ return typeof options?.defaultValue === "number"
449
+ ? options.defaultValue
450
+ : input;
228
451
  }
229
452
  }
230
453
 
454
+ //
455
+ //
231
456
  // Constants
457
+ //
232
458
  const MAX_RETRIES_ABSOLUTE_LIMIT = 72;
233
459
  const MAX_RETRIES_DEFAULT_LIMIT = 6;
234
460
  // Retry policy constants
@@ -250,71 +476,163 @@ const NOT_RETRYABLE_ERRORS = [
250
476
  RateLimitError,
251
477
  UnprocessableEntityError,
252
478
  ];
253
- // Turn policy constants
254
- const MAX_TURNS_ABSOLUTE_LIMIT = 72;
255
- const MAX_TURNS_DEFAULT_LIMIT = 12;
256
- //
479
+ const ERROR = {
480
+ BAD_FUNCTION_CALL: "Bad Function Call",
481
+ };
257
482
  //
258
- // Helpers
259
483
  //
260
- function maxTurnsFromOptions(options) {
261
- // Default to single turn (1) when turns are disabled
262
- // Handle the turns parameter
263
- if (options.turns === undefined) {
264
- // Default to default limit when undefined
265
- return MAX_TURNS_DEFAULT_LIMIT;
484
+ // Helpers
485
+ //
486
+ /**
487
+ * Creates the request options for the OpenAI API call
488
+ *
489
+ * @param input - The formatted input messages
490
+ * @param options - The LLM operation options
491
+ * @returns The request options for the OpenAI API
492
+ */
493
+ function createRequestOptions(input, options = {}) {
494
+ const requestOptions = {
495
+ model: options?.model || PROVIDER.OPENAI.MODEL.DEFAULT,
496
+ input,
497
+ };
498
+ // Add user if provided
499
+ if (options?.user) {
500
+ requestOptions.user = options.user;
266
501
  }
267
- else if (options.turns === true) {
268
- // Explicitly set to true
269
- return MAX_TURNS_DEFAULT_LIMIT;
502
+ // Add any provider-specific options
503
+ if (options?.providerOptions) {
504
+ Object.assign(requestOptions, options.providerOptions);
270
505
  }
271
- else if (typeof options.turns === "number") {
272
- if (options.turns > 0) {
273
- // Positive number - use that limit (capped at absolute limit)
274
- return Math.min(options.turns, MAX_TURNS_ABSOLUTE_LIMIT);
506
+ // Handle instructions or system message
507
+ if (options?.instructions) {
508
+ // Apply placeholders to instructions if data is provided and placeholders.instructions is undefined or true
509
+ requestOptions.instructions =
510
+ options.data &&
511
+ (options.placeholders?.instructions === undefined ||
512
+ options.placeholders?.instructions)
513
+ ? placeholders(options.instructions, options.data)
514
+ : options.instructions;
515
+ }
516
+ // Handle developer message as system message
517
+ // @ts-expect-error Testing invalid option
518
+ if (options?.developer) {
519
+ log.warn("Developer message provided but not supported. Using as system message.");
520
+ // @ts-expect-error Testing invalid option
521
+ requestOptions.system = options.developer;
522
+ }
523
+ // Handle structured output format
524
+ if (options?.format) {
525
+ // Check if format is a JsonObject with type "json_schema"
526
+ if (typeof options.format === "object" &&
527
+ options.format !== null &&
528
+ !Array.isArray(options.format) &&
529
+ options.format.type === "json_schema") {
530
+ // Direct pass-through for JsonObject with type "json_schema"
531
+ requestOptions.text = {
532
+ format: options.format,
533
+ };
275
534
  }
276
- else if (options.turns < 0) {
277
- // Negative number - use default limit
278
- return MAX_TURNS_DEFAULT_LIMIT;
535
+ else {
536
+ // Convert NaturalSchema to Zod schema if needed
537
+ const zodSchema = options.format instanceof z.ZodType
538
+ ? options.format
539
+ : naturalZodSchema(options.format);
540
+ const responseFormat = zodResponseFormat(zodSchema, "response");
541
+ // Set up structured output format in the format expected by the test
542
+ requestOptions.text = {
543
+ format: {
544
+ name: responseFormat.json_schema.name,
545
+ schema: responseFormat.json_schema.schema,
546
+ strict: responseFormat.json_schema.strict,
547
+ type: responseFormat.type,
548
+ },
549
+ };
279
550
  }
280
- // If turns is 0, return 1 (disabled)
281
- return 1;
282
551
  }
283
- // All other values (false, null, etc.) will return 1 (disabled)
284
- return 1;
552
+ // Create toolkit and add tools if provided
553
+ if (options.tools?.length) {
554
+ const explain = options?.explain ?? false;
555
+ const toolkit = new Toolkit(options.tools, { explain });
556
+ requestOptions.tools = toolkit.tools;
557
+ }
558
+ return requestOptions;
285
559
  }
286
560
  //
287
561
  //
288
562
  // Main
289
563
  //
290
- async function operate(
291
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
292
- input, options = {}, context = {
564
+ async function operate(input, options = {}, context = {
293
565
  client: new OpenAI(),
294
566
  }) {
295
- const log = getLogger();
567
+ //
568
+ //
569
+ // Setup
570
+ //
296
571
  const openai = context.client;
297
- // Validate
298
572
  if (!context.maxRetries) {
299
573
  context.maxRetries = MAX_RETRIES_DEFAULT_LIMIT;
300
574
  }
301
- const model = options?.model || PROVIDER.OPENAI.MODEL.DEFAULT;
302
- // Setup
303
575
  let retryCount = 0;
304
576
  let retryDelay = INITIAL_RETRY_DELAY_MS;
305
577
  const maxRetries = Math.min(context.maxRetries, MAX_RETRIES_ABSOLUTE_LIMIT);
306
- const allResponses = [];
578
+ const returnResponse = {
579
+ history: [],
580
+ output: [],
581
+ responses: [],
582
+ status: LlmResponseStatus.InProgress,
583
+ usage: {
584
+ input: 0,
585
+ output: 0,
586
+ reasoning: 0,
587
+ total: 0,
588
+ },
589
+ };
590
+ // Convert string input to array format with placeholders if needed
591
+ let currentInput = formatOperateInput(input);
592
+ if (options?.data &&
593
+ (options.placeholders?.input === undefined || options.placeholders?.input)) {
594
+ currentInput = formatOperateInput(input, {
595
+ data: options?.data,
596
+ });
597
+ }
598
+ // If history is provided, merge it with currentInput
599
+ if (options.history) {
600
+ currentInput = [...options.history, ...currentInput];
601
+ }
602
+ // If system message is provided, add it to the beginning of the input
603
+ if (options?.system) {
604
+ const systemMessage = options.data && options.placeholders?.system !== false
605
+ ? placeholders(options.system, options.data)
606
+ : options.system;
607
+ // Create system message
608
+ const systemInputMessage = {
609
+ content: systemMessage,
610
+ role: LlmMessageRole.System,
611
+ type: LlmMessageType.Message,
612
+ };
613
+ // Check if history starts with an identical system message
614
+ const firstMessage = currentInput[0];
615
+ const isIdenticalSystemMessage = firstMessage?.type === LlmMessageType.Message &&
616
+ firstMessage?.role === LlmMessageRole.System &&
617
+ firstMessage?.content === systemMessage;
618
+ // Only prepend if not identical
619
+ if (!isIdenticalSystemMessage) {
620
+ // Remove any existing system message from the beginning
621
+ if (currentInput[0]?.type === LlmMessageType.Message &&
622
+ currentInput[0]?.role === LlmMessageRole.System) {
623
+ currentInput = currentInput.slice(1);
624
+ }
625
+ currentInput = [systemInputMessage, ...currentInput];
626
+ }
627
+ }
628
+ // Initialize history with currentInput
629
+ returnResponse.history = [...currentInput];
307
630
  // Determine max turns from options
308
631
  const maxTurns = maxTurnsFromOptions(options);
309
632
  const enableMultipleTurns = maxTurns > 1;
310
633
  let currentTurn = 0;
311
- let currentInput = input;
312
- let toolkit;
313
- const explain = options?.explain ?? false;
314
- // Initialize toolkit if tools are provided
315
- if (options.tools?.length) {
316
- toolkit = new Toolkit(options.tools, { explain });
317
- }
634
+ // Build request options outside the retry loop
635
+ const requestOptions = createRequestOptions(currentInput, options);
318
636
  // OpenAI Multi-turn Loop
319
637
  while (currentTurn < maxTurns) {
320
638
  currentTurn++;
@@ -323,126 +641,88 @@ input, options = {}, context = {
323
641
  // OpenAI Retry Loop
324
642
  while (true) {
325
643
  try {
326
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
327
- const requestOptions = {
328
- model,
329
- input: typeof currentInput === "string" &&
330
- options?.data &&
331
- (options.placeholders?.input === undefined ||
332
- options.placeholders?.input)
333
- ? placeholders(currentInput, options.data)
334
- : currentInput,
335
- };
336
- if (options?.user) {
337
- requestOptions.user = options.user;
338
- }
339
- // Add any provider-specific options
340
- if (options?.providerOptions) {
341
- Object.assign(requestOptions, options.providerOptions);
342
- }
343
- if (options?.instructions) {
344
- // Apply placeholders to instructions if data is provided and placeholders.instructions is undefined or true
345
- requestOptions.instructions =
346
- options.data &&
347
- (options.placeholders?.instructions === undefined ||
348
- options.placeholders?.instructions)
349
- ? placeholders(options.instructions, options.data)
350
- : options.instructions;
351
- }
352
- else if (options?.system) {
353
- // Check for illegal system option, use it as instructions, and log a warning
354
- log.warn("[operate] Use 'instructions' instead of 'system'.");
355
- // Apply placeholders to system if data is provided and placeholders.instructions is undefined or true
356
- requestOptions.instructions =
357
- options.data &&
358
- (options.placeholders?.instructions === undefined ||
359
- options.placeholders?.instructions)
360
- ? placeholders(options.system, options.data)
361
- : options.system;
362
- }
363
- if (options?.format) {
364
- // Check if format is a JsonObject with type "json_schema"
365
- if (typeof options.format === "object" &&
366
- options.format !== null &&
367
- !Array.isArray(options.format) &&
368
- options.format.type === "json_schema") {
369
- // Direct pass-through for JsonObject with type "json_schema"
370
- requestOptions.text = {
371
- format: options.format,
372
- };
373
- }
374
- else {
375
- // Convert NaturalSchema to Zod schema if needed
376
- const zodSchema = options.format instanceof z.ZodType
377
- ? options.format
378
- : naturalZodSchema(options.format);
379
- const responseFormat = zodResponseFormat(zodSchema, "response");
380
- // Set up structured output format in the format expected by the test
381
- requestOptions.text = {
382
- format: {
383
- name: responseFormat.json_schema.name,
384
- schema: responseFormat.json_schema.schema,
385
- strict: responseFormat.json_schema.strict,
386
- type: responseFormat.type,
387
- },
388
- };
389
- }
390
- }
391
- // Add tools if toolkit is initialized
392
- if (toolkit) {
393
- requestOptions.tools = toolkit.tools;
394
- }
644
+ // Log appropriate message based on turn number
395
645
  if (currentTurn > 1) {
396
646
  log.trace(`[operate] Calling OpenAI Responses API - ${currentTurn}`);
397
647
  }
398
648
  else {
399
649
  log.trace("[operate] Calling OpenAI Responses API");
400
650
  }
401
- log.var({ requestOptions });
402
651
  // Use type assertion to handle the OpenAI SDK response type
403
- const currentResponse = (await openai.responses.create(requestOptions));
652
+ const currentResponse = (await openai.responses.create(
653
+ // @ts-expect-error error claims missing non-required id, status
654
+ requestOptions));
404
655
  if (retryCount > 0) {
405
656
  log.debug(`OpenAI API call succeeded after ${retryCount} retries`);
406
657
  }
407
- // Add the entire response to allResponses
408
- allResponses.push(currentResponse);
658
+ // Add the response to the responses array
659
+ returnResponse.responses.push(currentResponse);
660
+ // Accumulate token usage from the current response
661
+ if (currentResponse.usage) {
662
+ returnResponse.usage.input += currentResponse.usage.input_tokens || 0;
663
+ returnResponse.usage.output +=
664
+ currentResponse.usage.output_tokens || 0;
665
+ returnResponse.usage.total += currentResponse.usage.total_tokens || 0;
666
+ if (currentResponse.usage.output_tokens_details?.reasoning_tokens) {
667
+ returnResponse.usage.reasoning =
668
+ (returnResponse.usage.reasoning || 0) +
669
+ currentResponse.usage.output_tokens_details.reasoning_tokens;
670
+ }
671
+ }
409
672
  // Check if we need to process function calls for multi-turn conversations
410
673
  let hasFunctionCall = false;
411
674
  try {
412
675
  if (currentResponse.output && Array.isArray(currentResponse.output)) {
413
676
  // New OpenAI API format with output array
414
677
  for (const output of currentResponse.output) {
415
- if (output.type === "function_call") {
678
+ returnResponse.output.push(output);
679
+ returnResponse.history.push(output);
680
+ if (output.type === LlmMessageType.FunctionCall) {
416
681
  hasFunctionCall = true;
682
+ let toolkit;
683
+ const explain = options?.explain ?? false;
684
+ // Initialize toolkit if tools are provided for multi-turn function calling
685
+ if (options.tools?.length) {
686
+ toolkit = new Toolkit(options.tools, { explain });
687
+ }
417
688
  if (toolkit && enableMultipleTurns) {
418
689
  try {
419
- // Parse arguments for validation
420
- JSON.parse(output.arguments);
421
690
  // Call the tool and ensure the result is resolved if it's a Promise
422
691
  log.trace(`[operate] Calling tool - ${output.name}`);
692
+ returnResponse.content = `${LlmMessageType.FunctionCall}:${output.name}${output.arguments}#${output.call_id}`;
423
693
  const result = await toolkit.call({
424
694
  name: output.name,
425
695
  arguments: output.arguments,
426
696
  });
427
- // Prepare for next turn by adding function call and result
428
- // Add the function call to the input for the next turn
429
- if (typeof currentInput === "string") {
430
- // Convert string input to array format for the first turn
431
- currentInput = [{ content: currentInput, role: "user" }];
432
- }
433
697
  // Add model's function call and result
434
698
  if (Array.isArray(currentInput)) {
435
699
  currentInput.push(output);
436
700
  // Add function call result
437
- currentInput.push({
438
- type: "function_call_output",
701
+ const functionCallOutput = {
439
702
  call_id: output.call_id,
440
703
  output: JSON.stringify(result),
441
- });
704
+ type: LlmMessageType.FunctionCallOutput,
705
+ };
706
+ currentInput.push(functionCallOutput);
707
+ returnResponse.output.push(functionCallOutput);
708
+ returnResponse.history.push(functionCallOutput);
709
+ returnResponse.content = `${LlmMessageType.FunctionCallOutput}:${functionCallOutput.output}#${functionCallOutput.call_id}`;
442
710
  }
443
711
  }
444
712
  catch (error) {
445
- log.error(`Error executing function call ${output.name}:`, error);
713
+ // TODO: but I do need to tell the model that something went wrong, right?
714
+ const jaypieError = new BadGatewayError();
715
+ const detail = [
716
+ `Error executing function call ${output.name}.`,
717
+ error.message,
718
+ ].join("\n");
719
+ returnResponse.error = {
720
+ detail,
721
+ status: jaypieError.status,
722
+ title: ERROR.BAD_FUNCTION_CALL,
723
+ };
724
+ log.error(`Error executing function call ${output.name}`);
725
+ log.var({ error });
446
726
  // We don't add error messages to allResponses here as we want to keep the original response objects
447
727
  }
448
728
  }
@@ -450,6 +730,12 @@ input, options = {}, context = {
450
730
  log.warn("Model requested function call but no toolkit available");
451
731
  }
452
732
  }
733
+ if (output.type === LlmMessageType.Message) {
734
+ if (output.content?.[0] &&
735
+ output.content[0].type === LlmMessageType.OutputText) {
736
+ returnResponse.content = output.content[0].text;
737
+ }
738
+ }
453
739
  }
454
740
  }
455
741
  }
@@ -461,12 +747,21 @@ input, options = {}, context = {
461
747
  }
462
748
  // If there's no function call or we can't take another turn, exit the loop
463
749
  if (!hasFunctionCall || !enableMultipleTurns) {
464
- return allResponses;
750
+ returnResponse.status = LlmResponseStatus.Completed;
751
+ return returnResponse;
465
752
  }
466
753
  // If we've reached the maximum number of turns, exit the loop
467
754
  if (currentTurn >= maxTurns) {
468
- log.warn(`Model requested function call but exceeded ${maxTurns} turns`);
469
- return allResponses;
755
+ const error = new TooManyRequestsError();
756
+ const detail = `Model requested function call but exceeded ${maxTurns} turns`;
757
+ log.warn(detail);
758
+ returnResponse.status = LlmResponseStatus.Incomplete;
759
+ returnResponse.error = {
760
+ detail,
761
+ status: error.status,
762
+ title: error.title,
763
+ };
764
+ return returnResponse;
470
765
  }
471
766
  // Continue to next turn
472
767
  break;
@@ -513,13 +808,88 @@ input, options = {}, context = {
513
808
  }
514
809
  }
515
810
  }
516
- // If we've reached the maximum number of turns, return all responses
517
- return allResponses;
811
+ // * All possible paths should return a response; getting here is an error
812
+ // The main loop is `currentTurn < maxTurns` and `currentTurn >= maxTurns` within the loop returns
813
+ log.warn("This should never happen");
814
+ returnResponse.status = LlmResponseStatus.Incomplete;
815
+ // Always return the full LlmOperateResponse object for consistency
816
+ return returnResponse;
817
+ }
818
+
819
+ // Logger
820
+ const getLogger = () => log$1.lib({ lib: JAYPIE.LIB.LLM });
821
+ // Client initialization
822
+ async function initializeClient({ apiKey, } = {}) {
823
+ const logger = getLogger();
824
+ const resolvedApiKey = apiKey || (await getEnvSecret("OPENAI_API_KEY"));
825
+ if (!resolvedApiKey) {
826
+ throw new ConfigurationError$1("The application could not resolve the requested keys");
827
+ }
828
+ const client = new OpenAI({ apiKey: resolvedApiKey });
829
+ logger.trace("Initialized OpenAI client");
830
+ return client;
831
+ }
832
+ function formatSystemMessage(systemPrompt, { data, placeholders: placeholders$1 } = {}) {
833
+ const content = placeholders$1?.system === false
834
+ ? systemPrompt
835
+ : placeholders(systemPrompt, data);
836
+ return {
837
+ role: "developer",
838
+ content,
839
+ };
840
+ }
841
+ function formatUserMessage(message, { data, placeholders: placeholders$1 } = {}) {
842
+ const content = placeholders$1?.message === false
843
+ ? message
844
+ : placeholders(message, data);
845
+ return {
846
+ role: "user",
847
+ content,
848
+ };
849
+ }
850
+ function prepareMessages(message, { system, data, placeholders } = {}) {
851
+ const logger = getLogger();
852
+ const messages = [];
853
+ if (system) {
854
+ const systemMessage = formatSystemMessage(system, { data, placeholders });
855
+ messages.push(systemMessage);
856
+ logger.trace(`System message: ${systemMessage.content?.length} characters`);
857
+ }
858
+ const userMessage = formatUserMessage(message, { data, placeholders });
859
+ messages.push(userMessage);
860
+ logger.trace(`User message: ${userMessage.content?.length} characters`);
861
+ return messages;
862
+ }
863
+ // Completion requests
864
+ async function createStructuredCompletion(client, { messages, responseSchema, model, }) {
865
+ const logger = getLogger();
866
+ logger.trace("Using structured output");
867
+ const zodSchema = responseSchema instanceof z.ZodType
868
+ ? responseSchema
869
+ : naturalZodSchema(responseSchema);
870
+ const completion = await client.beta.chat.completions.parse({
871
+ messages,
872
+ model,
873
+ response_format: zodResponseFormat(zodSchema, "response"),
874
+ });
875
+ logger.var({ assistantReply: completion.choices[0].message.parsed });
876
+ return completion.choices[0].message.parsed;
877
+ }
878
+ async function createTextCompletion(client, { messages, model, }) {
879
+ const logger = getLogger();
880
+ logger.trace("Using text output (unstructured)");
881
+ const completion = await client.chat.completions.create({
882
+ messages,
883
+ model,
884
+ });
885
+ logger.trace(`Assistant reply: ${completion.choices[0]?.message?.content?.length} characters`);
886
+ return completion.choices[0]?.message?.content || "";
518
887
  }
519
888
 
520
889
  class OpenAiProvider {
521
890
  constructor(model = PROVIDER.OPENAI.MODEL.DEFAULT, { apiKey } = {}) {
522
891
  this.log = getLogger();
892
+ this.conversationHistory = [];
523
893
  this.model = model;
524
894
  this.apiKey = apiKey;
525
895
  }
@@ -549,7 +919,28 @@ class OpenAiProvider {
549
919
  async operate(input, options = {}) {
550
920
  const client = await this.getClient();
551
921
  options.model = options?.model || this.model;
552
- return operate(input, options, { client });
922
+ // TODO: Create a merged history including both the tracked history and any explicitly provided history
923
+ // Call operate with the updated options
924
+ const response = await operate(input, options, { client });
925
+ // TODO: Update conversation history with the input and response
926
+ return response;
927
+ }
928
+ /**
929
+ * Updates the conversation history with the latest input and response
930
+ * @param input The formatted input messages
931
+ * @param response The response from the model
932
+ */
933
+ updateConversationHistory(input, response) {
934
+ // Add the input to history
935
+ this.conversationHistory.push(...input);
936
+ // Add the response to history if it exists and has content
937
+ if (response && response.length > 0) {
938
+ // Extract the last response item and add it to history
939
+ const lastResponse = response[response.length - 1];
940
+ if (lastResponse) {
941
+ this.conversationHistory.push(lastResponse);
942
+ }
943
+ }
553
944
  }
554
945
  }
555
946
 
@@ -604,134 +995,6 @@ class Llm {
604
995
  }
605
996
  }
606
997
 
607
- //
608
- // Constants
609
- //
610
- const DEFAULT_MIN = 0;
611
- const DEFAULT_MAX = 1;
612
- //
613
- // Helper Functions
614
- //
615
- /**
616
- * Clamps a number between optional minimum and maximum values
617
- * @param num - The number to clamp
618
- * @param min - Optional minimum value
619
- * @param max - Optional maximum value
620
- * @returns The clamped number
621
- */
622
- const clamp = (num, min, max) => {
623
- let result = num;
624
- if (typeof min === "number")
625
- result = Math.max(result, min);
626
- if (typeof max === "number")
627
- result = Math.min(result, max);
628
- return result;
629
- };
630
- /**
631
- * Validates that min is not greater than max
632
- * @param min - Optional minimum value
633
- * @param max - Optional maximum value
634
- * @throws {ConfigurationError} If min is greater than max
635
- */
636
- const validateBounds = (min, max) => {
637
- if (typeof min === "number" && typeof max === "number" && min > max) {
638
- throw new ConfigurationError$1(`Invalid bounds: min (${min}) cannot be greater than max (${max})`);
639
- }
640
- };
641
- //
642
- // Main
643
- //
644
- /**
645
- * Creates a random number generator with optional seeding
646
- *
647
- * @param defaultSeed - Seed string for the default RNG
648
- * @returns A function that generates random numbers based on provided options
649
- *
650
- * @example
651
- * const rng = random("default-seed");
652
- *
653
- * // Generate a random float between 0 and 1
654
- * const basic = rng();
655
- *
656
- * // Generate an integer between 1 and 10
657
- * const integer = rng({ min: 1, max: 10, integer: true });
658
- *
659
- * // Generate from normal distribution
660
- * const normal = rng({ mean: 50, stddev: 10 });
661
- *
662
- * // Use consistent seeding
663
- * const seeded = rng({ seed: "my-seed" });
664
- */
665
- const random$1 = (defaultSeed) => {
666
- // Initialize default seeded RNG
667
- const defaultRng = RandomLib.clone(defaultSeed);
668
- // Store per-seed RNGs
669
- const seedMap = new Map();
670
- const rngFn = ({ min, max: providedMax, mean, stddev, integer = false, start = 0, seed, precision, currency = false, } = {}) => {
671
- // Select the appropriate RNG based on seed
672
- const rng = seed
673
- ? seedMap.get(seed) ||
674
- (() => {
675
- const newRng = RandomLib.clone(seed);
676
- seedMap.set(seed, newRng);
677
- return newRng;
678
- })()
679
- : defaultRng;
680
- // If only min is set, set max to min*2, but keep track of whether max was provided
681
- let max = providedMax;
682
- if (typeof min === "number" && typeof max !== "number") {
683
- max = min * 2;
684
- }
685
- validateBounds(min, max);
686
- // Determine effective precision based on parameters
687
- const getEffectivePrecision = () => {
688
- if (integer)
689
- return 0;
690
- const precisions = [];
691
- if (typeof precision === "number" && precision >= 0) {
692
- precisions.push(precision);
693
- }
694
- if (currency) {
695
- precisions.push(2);
696
- }
697
- // Return the lowest precision if any are set, undefined otherwise
698
- return precisions.length > 0 ? Math.min(...precisions) : undefined;
699
- };
700
- // Helper function to apply precision
701
- const applyPrecision = (value) => {
702
- const effectivePrecision = getEffectivePrecision();
703
- if (typeof effectivePrecision === "number") {
704
- const factor = Math.pow(10, effectivePrecision);
705
- return Math.round(value * factor) / factor;
706
- }
707
- return value;
708
- };
709
- // Use normal distribution if both mean and stddev are provided
710
- if (typeof mean === "number" && typeof stddev === "number") {
711
- const normalDist = rng.normal(mean, stddev);
712
- const value = normalDist();
713
- // Only clamp if min/max are defined
714
- const clampedValue = clamp(value, min, providedMax);
715
- // Switch to uniform distribution only if both bounds were explicitly provided and exceeded
716
- if (typeof min === "number" &&
717
- typeof providedMax === "number" &&
718
- clampedValue !== value) {
719
- const baseValue = integer ? rng.int(min, max) : rng.float(min, max);
720
- return applyPrecision(start + baseValue);
721
- }
722
- return applyPrecision(start + (integer ? Math.round(clampedValue) : clampedValue));
723
- }
724
- // For uniform distribution, use defaults if min/max are undefined
725
- const uniformMin = typeof min === "number" ? min : DEFAULT_MIN;
726
- const uniformMax = typeof max === "number" ? max : DEFAULT_MAX;
727
- const baseValue = integer
728
- ? rng.int(uniformMin, uniformMax)
729
- : rng.float(uniformMin, uniformMax);
730
- return applyPrecision(start + baseValue);
731
- };
732
- return rngFn;
733
- };
734
-
735
998
  const random = {
736
999
  description: "Generate a random number with optional distribution, precision, range, and seeding",
737
1000
  name: "random",
@@ -798,12 +1061,20 @@ const roll = {
798
1061
  required: ["number", "sides"],
799
1062
  },
800
1063
  type: "function",
801
- call: ({ number = 1, sides = 6 }) => {
1064
+ call: ({ number = 1, sides = 6 } = {}) => {
802
1065
  const rng = random$1();
803
1066
  const rolls = [];
804
1067
  let total = 0;
805
- for (let i = 0; i < number; i++) {
806
- const rollValue = rng({ min: 1, max: sides, integer: true });
1068
+ const parsedNumber = tryParseNumber(number, {
1069
+ defaultValue: 1,
1070
+ warnFunction: log.warn,
1071
+ });
1072
+ const parsedSides = tryParseNumber(sides, {
1073
+ defaultValue: 6,
1074
+ warnFunction: log.warn,
1075
+ });
1076
+ for (let i = 0; i < parsedNumber; i++) {
1077
+ const rollValue = rng({ min: 1, max: parsedSides, integer: true });
807
1078
  rolls.push(rollValue);
808
1079
  total += rollValue;
809
1080
  }
@@ -826,7 +1097,7 @@ const time = {
826
1097
  },
827
1098
  type: "function",
828
1099
  call: ({ date } = {}) => {
829
- if (date) {
1100
+ if (typeof date === "number" || typeof date === "string") {
830
1101
  const parsedDate = new Date(date);
831
1102
  if (isNaN(parsedDate.getTime())) {
832
1103
  throw new Error(`Invalid date format: ${date}`);