@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 +647 -376
- package/dist/index.js.map +1 -1
- package/dist/providers/openai/OpenAiProvider.class.d.ts +9 -2
- package/dist/providers/openai/operate.d.ts +21 -7
- package/dist/providers/openai/types.d.ts +21 -2
- package/dist/types/LlmProvider.interface.d.ts +116 -5
- package/dist/types/LlmTool.interface.d.ts +2 -2
- package/dist/util/formatOperateInput.d.ts +24 -0
- package/dist/util/formatOperateMessage.d.ts +24 -0
- package/dist/util/index.d.ts +7 -0
- package/dist/util/logger.d.ts +71 -0
- package/dist/util/maxTurnsFromOptions.d.ts +10 -0
- package/dist/util/naturalZodSchema.d.ts +1 -1
- package/dist/util/random.d.ts +2 -2
- package/dist/util/tryParseNumber.d.ts +22 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { BadGatewayError,
|
|
2
|
-
import { log, JAYPIE,
|
|
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
|
-
//
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
221
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
//
|
|
479
|
+
const ERROR = {
|
|
480
|
+
BAD_FUNCTION_CALL: "Bad Function Call",
|
|
481
|
+
};
|
|
257
482
|
//
|
|
258
|
-
// Helpers
|
|
259
483
|
//
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
502
|
+
// Add any provider-specific options
|
|
503
|
+
if (options?.providerOptions) {
|
|
504
|
+
Object.assign(requestOptions, options.providerOptions);
|
|
270
505
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
|
277
|
-
//
|
|
278
|
-
|
|
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
|
-
//
|
|
284
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
312
|
-
|
|
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
|
-
//
|
|
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(
|
|
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
|
|
408
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
469
|
-
|
|
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
|
-
//
|
|
517
|
-
|
|
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
|
-
|
|
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
|
-
|
|
806
|
-
|
|
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}`);
|