@stackfactor/agent-utils 1.0.11 → 1.0.12
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/cjs/langChain.d.ts +1 -1
- package/dist/cjs/langChain.d.ts.map +1 -1
- package/dist/cjs/langChain.js +96 -385
- package/dist/esm/langChain.d.ts +1 -1
- package/dist/esm/langChain.d.ts.map +1 -1
- package/dist/esm/langChain.js +96 -385
- package/package.json +1 -1
package/dist/cjs/langChain.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
declare const _default: {
|
|
2
|
-
createAgent: (name: string, modelName: string, systemPrompt: string, tools: any[], responseFormat: any, config: any
|
|
2
|
+
createAgent: (name: string, modelName: string, systemPrompt: string, tools: any[], responseFormat: any, config: any) => any;
|
|
3
3
|
runAgent: (agent: any, prompt: string, config: any, onProgress?: Function | null) => Promise<any>;
|
|
4
4
|
runChatPromptWithModel: (modelName: string, config: any, prompt: any, onProgressReport: any) => any;
|
|
5
5
|
runPromptWithModel: (modelName: string, config: any, prompt: any, onProgressReport: any, minPercent?: number, maxPercent?: number, expectsJsonResponse?: boolean, schema?: any, agentName?: string, tools?: any[]) => Promise<any>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":";wBAkSQ,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,KACV,GAAG;sBAyBG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,KAC1B,OAAO,CAAC,GAAG,CAAC;wCAufF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,KACpB,GAAG;oCA5YO,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,eACT,MAAM,eACN,MAAM,wBACG,OAAO,WACpB,GAAG,cACA,MAAM,UACV,GAAG,EAAE,KACX,OAAO,CAAC,GAAG,CAAC;sDAisBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,KACX,OAAO,CAAC,GAAG,CAAC;0CAjwB8B,GAAG,KAAG,MAAM;;AA4zBzD,wBAOE"}
|
package/dist/cjs/langChain.js
CHANGED
|
@@ -12,34 +12,7 @@ const const_js_1 = __importDefault(require("./const.js"));
|
|
|
12
12
|
const langchain_1 = __importDefault(require("langchain"));
|
|
13
13
|
const errorHandling_js_1 = __importDefault(require("./errorHandling.js"));
|
|
14
14
|
const logger_js_1 = __importDefault(require("./logger.js"));
|
|
15
|
-
const zod_1 = require("zod");
|
|
16
15
|
const zod_to_json_schema_1 = require("zod-to-json-schema");
|
|
17
|
-
const NDJSON_SYSTEM_PROMPT = `
|
|
18
|
-
You are a streaming JSON generator.
|
|
19
|
-
|
|
20
|
-
You MUST output NDJSON (newline-delimited JSON).
|
|
21
|
-
That means: ONE valid JSON object per line.
|
|
22
|
-
|
|
23
|
-
Allowed event types:
|
|
24
|
-
- "progress": short status update for the user
|
|
25
|
-
- "final": final complete result
|
|
26
|
-
|
|
27
|
-
Rules:
|
|
28
|
-
- Output ONLY NDJSON lines (no markdown, no extra text).
|
|
29
|
-
- Each line must be valid JSON.
|
|
30
|
-
- "progress" must include: type, message, pct (with values between {minPercent} and {maxPercent})
|
|
31
|
-
- "final" must include: type, content
|
|
32
|
-
- End with EXACTLY ONE "final" event.
|
|
33
|
-
- Do NOT reveal hidden chain-of-thought.
|
|
34
|
-
|
|
35
|
-
CRITICAL - Escape special characters in ALL string values:
|
|
36
|
-
- Newlines must be written as \\n (two characters: backslash + n)
|
|
37
|
-
- Tabs must be written as \\t (two characters: backslash + t)
|
|
38
|
-
- Carriage returns must be written as \\r (two characters: backslash + r)
|
|
39
|
-
- Double quotes inside strings must be written as \\"
|
|
40
|
-
- Backslashes must be written as \\\\
|
|
41
|
-
This ensures valid JSON output.
|
|
42
|
-
`.trim();
|
|
43
16
|
const JSON_ESCAPE_INSTRUCTION = `
|
|
44
17
|
CRITICAL - Your response must be valid JSON. Escape ALL special characters in string values:
|
|
45
18
|
- Newlines → \\n
|
|
@@ -49,12 +22,6 @@ CRITICAL - Your response must be valid JSON. Escape ALL special characters in st
|
|
|
49
22
|
- Backslashes → \\\\
|
|
50
23
|
Do NOT include raw newlines, tabs, or unescaped quotes inside JSON string values.
|
|
51
24
|
`.trim();
|
|
52
|
-
const REPORT_PROGRESS_TOOL = `
|
|
53
|
-
Report Progress to User
|
|
54
|
-
Before each task or step, call the tool report_progress with:
|
|
55
|
-
message: brief description of work you plan to do that makes sense for individuals in Learning and Development or Business Strategy roles. Don't include the word JSON in any of the messages.
|
|
56
|
-
percent: cumulative completion percentage with values between {minPercent}% and {maxPercent}%. Don't report percentage completion outside of this range.
|
|
57
|
-
`;
|
|
58
25
|
/**
|
|
59
26
|
* Converts a Zod validation error object into a human-readable multi-line string.
|
|
60
27
|
* Each failing field is described with its dot-notation path and a contextual message
|
|
@@ -222,82 +189,6 @@ const extractJSONFromResponse = (text) => {
|
|
|
222
189
|
}
|
|
223
190
|
return null;
|
|
224
191
|
};
|
|
225
|
-
/**
|
|
226
|
-
* Attempts to parse a JSON string without throwing. On failure it tries a second parse
|
|
227
|
-
* after sanitising Unicode control characters and normalising Windows-style line endings
|
|
228
|
-
* to `\n`. Returns `null` if both attempts fail, making it safe to call in stream
|
|
229
|
-
* processing loops where individual NDJSON lines may be malformed.
|
|
230
|
-
* @param line - A single line of text expected to contain a JSON value
|
|
231
|
-
* @returns The parsed value, or `null` if parsing failed after both attempts
|
|
232
|
-
*/
|
|
233
|
-
const safeJsonParse = (line) => {
|
|
234
|
-
try {
|
|
235
|
-
return JSON.parse(line);
|
|
236
|
-
}
|
|
237
|
-
catch {
|
|
238
|
-
// Try with special character handling
|
|
239
|
-
try {
|
|
240
|
-
const sanitized = line
|
|
241
|
-
.replace(/[\u0000-\u001F\u007F-\u009F]/g, " ")
|
|
242
|
-
.replace(/\r\n/g, "\\n")
|
|
243
|
-
.replace(/\r/g, "\\n");
|
|
244
|
-
return JSON.parse(sanitized);
|
|
245
|
-
}
|
|
246
|
-
catch {
|
|
247
|
-
return null;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
};
|
|
251
|
-
/**
|
|
252
|
-
* Creates a LangChain tool named `report_progress` that agents can call to emit
|
|
253
|
-
* progress updates during multi-step execution. When the tool is invoked by the agent,
|
|
254
|
-
* it clamps the reported percentage to the `[minPercent, maxPercent]` range, forwards
|
|
255
|
-
* the update to the `onProgress` callback, and returns a JSON acknowledgement string to
|
|
256
|
-
* the agent. If the callback throws, the tool returns a JSON error acknowledgement
|
|
257
|
-
* instead of propagating the exception.
|
|
258
|
-
* @param onProgress - Async or sync callback invoked with `{ progress, message }` on
|
|
259
|
-
* each agent progress report
|
|
260
|
-
* @param minPercent - The lower bound of the percentage range the agent is allowed to
|
|
261
|
-
* report; defaults to `0`
|
|
262
|
-
* @param maxPercent - The upper bound of the percentage range the agent is allowed to
|
|
263
|
-
* report; defaults to `100`
|
|
264
|
-
* @returns A LangChain tool instance configured with a Zod schema for `{ stage, message, percent }`
|
|
265
|
-
*/
|
|
266
|
-
const getAIProgressTool = (onProgress, minPercent = 0, maxPercent = 100) => langchain_1.default.tool(async ({ stage, message, percent }) => {
|
|
267
|
-
let clampedPct = 0;
|
|
268
|
-
try {
|
|
269
|
-
// Coerce percent to number and clamp to be within minPercent and maxPercent bounds
|
|
270
|
-
const rawPct = typeof percent === "number" ? percent : parseFloat(percent) || 0;
|
|
271
|
-
clampedPct = Math.max(minPercent, Math.min(maxPercent, rawPct));
|
|
272
|
-
// NOTE: Monotonicity is not enforced here; implement if needed per session/context.
|
|
273
|
-
if (onProgress &&
|
|
274
|
-
minPercent <= clampedPct &&
|
|
275
|
-
clampedPct <= maxPercent) {
|
|
276
|
-
await onProgress({
|
|
277
|
-
progress: clampedPct,
|
|
278
|
-
message: message,
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
finally {
|
|
283
|
-
return JSON.stringify({
|
|
284
|
-
acknowledged: true,
|
|
285
|
-
receivedAt: Date.now(),
|
|
286
|
-
stage,
|
|
287
|
-
percent: clampedPct,
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
}, {
|
|
291
|
-
name: "report_progress",
|
|
292
|
-
description: `Mandatory after each phase: call with {stage, message, percent}. Percent must be between ${minPercent} and ${maxPercent} and monotonically increase.`,
|
|
293
|
-
schema: zod_1.z.object({
|
|
294
|
-
stage: zod_1.z.string().describe("Current stage of the process"),
|
|
295
|
-
message: zod_1.z.string().describe("Progress message to display"),
|
|
296
|
-
percent: zod_1.z
|
|
297
|
-
.number()
|
|
298
|
-
.describe(`Progress percentage (${minPercent}-${maxPercent})`),
|
|
299
|
-
}),
|
|
300
|
-
});
|
|
301
192
|
/**
|
|
302
193
|
* Instantiates and returns the appropriate LangChain chat model based on the model
|
|
303
194
|
* name prefix. `claude-` maps to `ChatAnthropic`, `gemini-` maps to
|
|
@@ -363,11 +254,7 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
363
254
|
};
|
|
364
255
|
/**
|
|
365
256
|
* Constructs a LangChain agent configured with a specified model, system prompt, and
|
|
366
|
-
* set of tools.
|
|
367
|
-
* reporting instruction block is appended to the system prompt and a `report_progress`
|
|
368
|
-
* tool is added to the tools array so the agent can emit incremental progress updates
|
|
369
|
-
* during execution. The percentage range for progress reporting is bounded by
|
|
370
|
-
* `minPercent` and `maxPercent`.
|
|
257
|
+
* set of tools.
|
|
371
258
|
* @param name - A human-readable display name for the agent
|
|
372
259
|
* @param modelName - The LLM identifier passed to `getLLMModel` (e.g. `"gpt-4o"`)
|
|
373
260
|
* @param systemPrompt - The base system prompt describing the agent's role and behaviour
|
|
@@ -376,30 +263,14 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
376
263
|
* @param responseFormat - Optional structured response format descriptor passed to the
|
|
377
264
|
* LangChain agent constructor
|
|
378
265
|
* @param config - Configuration object forwarded to `getLLMModel` (API keys, temperature, etc.)
|
|
379
|
-
* @
|
|
380
|
-
* progress tool is added and the system prompt is extended
|
|
381
|
-
* @param minPercent - Minimum progress percentage the agent is allowed to report;
|
|
382
|
-
* defaults to `0`
|
|
383
|
-
* @param maxPercent - Maximum progress percentage the agent is allowed to report;
|
|
384
|
-
* defaults to `100`
|
|
385
|
-
* @returns A configured LangChain agent instance ready to be run with `runAIAgent`
|
|
266
|
+
* @returns A configured LangChain agent instance ready to be run with `runAgent`
|
|
386
267
|
*/
|
|
387
|
-
const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat, config
|
|
388
|
-
//Prepare the complete system prompt with progress reporting instructions if needed
|
|
389
|
-
const completeSystemPrompt = `
|
|
390
|
-
${systemPrompt}
|
|
391
|
-
${onReportProgress
|
|
392
|
-
? REPORT_PROGRESS_TOOL.replace(`{minPercent}`, minPercent).replace(`{maxPercent}`, maxPercent)
|
|
393
|
-
: ""}
|
|
394
|
-
`.trim();
|
|
395
|
-
// Create the agent with the specified model, system prompt, tools, and response format
|
|
268
|
+
const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat, config) => {
|
|
396
269
|
const agent = langchain_1.default.createAgent({
|
|
397
270
|
name: name,
|
|
398
271
|
model: getLLMModel(modelName, config),
|
|
399
|
-
systemPrompt:
|
|
400
|
-
tools
|
|
401
|
-
? [...tools, getAIProgressTool(onReportProgress, minPercent, maxPercent)]
|
|
402
|
-
: tools,
|
|
272
|
+
systemPrompt: systemPrompt.trim(),
|
|
273
|
+
tools,
|
|
403
274
|
...(responseFormat ? { responseFormat: responseFormat } : {}),
|
|
404
275
|
});
|
|
405
276
|
return agent;
|
|
@@ -407,10 +278,9 @@ const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat,
|
|
|
407
278
|
/**
|
|
408
279
|
* Executes a LangChain agent with a single user prompt and returns the raw agent
|
|
409
280
|
* response. When an `onProgress` callback is provided, LangChain callbacks are
|
|
410
|
-
* registered to forward `tool_start`, `
|
|
411
|
-
* from the `report_progress` tool), `agent_action`, and error events to the caller.
|
|
281
|
+
* registered to forward `tool_start`, `agent_action`, and error events to the caller.
|
|
412
282
|
* Execution time is logged at the info level on completion.
|
|
413
|
-
* @param agent - A LangChain agent instance created by `
|
|
283
|
+
* @param agent - A LangChain agent instance created by `createAgent`
|
|
414
284
|
* @param prompt - The user message string to send to the agent
|
|
415
285
|
* @param config - Configuration object; `config.recursionLimit` controls the maximum
|
|
416
286
|
* number of agent steps (defaults to `25` when not specified)
|
|
@@ -420,8 +290,6 @@ const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat,
|
|
|
420
290
|
*/
|
|
421
291
|
const runAgent = async (agent, prompt, config, onProgress = null) => {
|
|
422
292
|
const startTime = Date.now();
|
|
423
|
-
// Build callbacks for progress reporting if onProgress is provided
|
|
424
|
-
// const agentDisplayName = `${agent.options?.name} AI Agent`;
|
|
425
293
|
const callbacks = onProgress
|
|
426
294
|
? [
|
|
427
295
|
{
|
|
@@ -433,24 +301,6 @@ const runAgent = async (agent, prompt, config, onProgress = null) => {
|
|
|
433
301
|
tool: toolInfo.name,
|
|
434
302
|
});
|
|
435
303
|
},
|
|
436
|
-
handleToolEnd: (output) => {
|
|
437
|
-
try {
|
|
438
|
-
const progressInfo = extractJSONFromResponse(output.content);
|
|
439
|
-
if (progressInfo &&
|
|
440
|
-
typeof progressInfo === "object" &&
|
|
441
|
-
progressInfo.percent &&
|
|
442
|
-
progressInfo.message) {
|
|
443
|
-
onProgress({
|
|
444
|
-
progress: progressInfo.percent,
|
|
445
|
-
message: progressInfo.message,
|
|
446
|
-
output: typeof output === "string"
|
|
447
|
-
? output
|
|
448
|
-
: JSON.stringify(output),
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
catch { }
|
|
453
|
-
},
|
|
454
304
|
handleAgentAction: (action) => {
|
|
455
305
|
onProgress({
|
|
456
306
|
type: "agent_action",
|
|
@@ -459,18 +309,6 @@ const runAgent = async (agent, prompt, config, onProgress = null) => {
|
|
|
459
309
|
input: action.toolInput,
|
|
460
310
|
});
|
|
461
311
|
},
|
|
462
|
-
// handleLLMStart: () => {
|
|
463
|
-
// onProgress({
|
|
464
|
-
// type: "llm_start",
|
|
465
|
-
// message: `${agentDisplayName} is thinking`,
|
|
466
|
-
// });
|
|
467
|
-
// },
|
|
468
|
-
// handleLLMEnd: () => {
|
|
469
|
-
// onProgress({
|
|
470
|
-
// type: "llm_end",
|
|
471
|
-
// message: `${agentDisplayName} has completed processing`,
|
|
472
|
-
// });
|
|
473
|
-
// },
|
|
474
312
|
handleChainError: (err) => {
|
|
475
313
|
onProgress({
|
|
476
314
|
type: "error",
|
|
@@ -574,26 +412,11 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
574
412
|
? `${JSON_ESCAPE_INSTRUCTION}\n\n---\n\n${systemPrompt}`
|
|
575
413
|
: JSON_ESCAPE_INSTRUCTION;
|
|
576
414
|
}
|
|
577
|
-
//
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
trackingProgressCallback = (data) => {
|
|
583
|
-
if (typeof data.progress === "number") {
|
|
584
|
-
progressTracker.current = data.progress;
|
|
585
|
-
}
|
|
586
|
-
onProgressReport(data);
|
|
587
|
-
};
|
|
588
|
-
}
|
|
589
|
-
// Create the agent with tools and progress callback
|
|
590
|
-
const agent = createAgent(agentName, modelName, systemPrompt, agentTools, null, // responseFormat
|
|
591
|
-
config, trackingProgressCallback, minPercent, maxPercent);
|
|
592
|
-
// Run the agent with callback that includes current progress
|
|
593
|
-
const callbackWithProgress = onProgressReport
|
|
594
|
-
? (data) => onProgressReport({ ...data, progress: progressTracker.current })
|
|
595
|
-
: null;
|
|
596
|
-
const response = await runAgent(agent, userPrompt, config, callbackWithProgress);
|
|
415
|
+
// Create the agent with tools
|
|
416
|
+
const agent = createAgent(agentName, modelName, systemPrompt, [...tools], null, // responseFormat
|
|
417
|
+
config);
|
|
418
|
+
// Run the agent with progress callback
|
|
419
|
+
const response = await runAgent(agent, userPrompt, config, onProgressReport || null);
|
|
597
420
|
// Extract content from agent response
|
|
598
421
|
const messages = response?.messages || [];
|
|
599
422
|
if (messages.length === 0) {
|
|
@@ -666,224 +489,112 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
666
489
|
throw new Error("Prompt must be a string or array of messages");
|
|
667
490
|
}
|
|
668
491
|
if (onProgressReport) {
|
|
669
|
-
//
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
492
|
+
// Streaming mode: use server-side chunk-based progress for all models
|
|
493
|
+
const useNativeSchema = expectsJsonResponse && schema && modelName.startsWith("gpt-");
|
|
494
|
+
const llm = getLLMModel(modelName, config, useNativeSchema ? schema : null);
|
|
495
|
+
// Build messages with JSON instructions if needed
|
|
496
|
+
let messagesToSend;
|
|
497
|
+
if (expectsJsonResponse) {
|
|
498
|
+
let systemContent = useNativeSchema
|
|
499
|
+
? "Respond with valid JSON."
|
|
500
|
+
: JSON_ESCAPE_INSTRUCTION;
|
|
501
|
+
if (schema && !useNativeSchema) {
|
|
502
|
+
const jsonSchema = (0, zod_to_json_schema_1.zodToJsonSchema)(schema, { target: "openApi3" });
|
|
503
|
+
systemContent += `\n\nYour response MUST conform to this JSON schema:\n${JSON.stringify(jsonSchema, null, 2)}`;
|
|
504
|
+
}
|
|
505
|
+
if (messages.length > 0 && messages[0].role === "system") {
|
|
506
|
+
messagesToSend = [
|
|
507
|
+
{
|
|
508
|
+
role: "system",
|
|
509
|
+
content: `${systemContent}\n\n---\n\n${messages[0].content}`,
|
|
510
|
+
},
|
|
511
|
+
...messages.slice(1),
|
|
679
512
|
];
|
|
680
|
-
let rawContent = "";
|
|
681
|
-
let lastProgressReport = 0;
|
|
682
|
-
const progressInterval = 1000; // Report progress every 1000 characters
|
|
683
|
-
const stream = await llm.stream(messagesToSend);
|
|
684
|
-
for await (const chunk of stream) {
|
|
685
|
-
const content = chunk?.content || chunk;
|
|
686
|
-
if (typeof content === "string") {
|
|
687
|
-
rawContent += content;
|
|
688
|
-
// Report progress based on content length
|
|
689
|
-
if (rawContent.length - lastProgressReport >= progressInterval) {
|
|
690
|
-
lastProgressReport = rawContent.length;
|
|
691
|
-
const progress = Math.min(maxPercent - 5, // Reserve last 5% for completion
|
|
692
|
-
minPercent +
|
|
693
|
-
Math.floor((rawContent.length / 5000) * (maxPercent - minPercent)));
|
|
694
|
-
await onProgressReport({
|
|
695
|
-
message: "Generating content...",
|
|
696
|
-
progress: progress,
|
|
697
|
-
});
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
513
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
// Parse and validate the response
|
|
707
|
-
if (rawContent) {
|
|
708
|
-
try {
|
|
709
|
-
const parsed = JSON.parse(rawContent.trim());
|
|
710
|
-
if (schema) {
|
|
711
|
-
validateWithSchema(parsed, schema);
|
|
712
|
-
}
|
|
713
|
-
return JSON.stringify(parsed);
|
|
714
|
-
}
|
|
715
|
-
catch (parseError) {
|
|
716
|
-
if (parseError?.code === const_js_1.default.HTTP_CODES.UNPROCESSABLE_ENTITY) {
|
|
717
|
-
throw parseError;
|
|
718
|
-
}
|
|
719
|
-
// Try to extract JSON
|
|
720
|
-
const extracted = extractJSONFromResponse(rawContent);
|
|
721
|
-
if (extracted) {
|
|
722
|
-
if (schema) {
|
|
723
|
-
validateWithSchema(extracted, schema);
|
|
724
|
-
}
|
|
725
|
-
return JSON.stringify(extracted);
|
|
726
|
-
}
|
|
727
|
-
if (expectsJsonResponse) {
|
|
728
|
-
let preview = "";
|
|
729
|
-
if (typeof rawContent === "string") {
|
|
730
|
-
preview = rawContent.substring(0, 100);
|
|
731
|
-
}
|
|
732
|
-
else if (typeof rawContent === "object" && rawContent !== null) {
|
|
733
|
-
preview = JSON.stringify(rawContent).substring(0, 100);
|
|
734
|
-
}
|
|
735
|
-
else if (rawContent !== undefined && rawContent !== null) {
|
|
736
|
-
preview = String(rawContent).substring(0, 100);
|
|
737
|
-
}
|
|
738
|
-
else {
|
|
739
|
-
preview = "[empty response]";
|
|
740
|
-
}
|
|
741
|
-
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, `LLM returned non-JSON response when JSON was expected: ${preview}...`);
|
|
742
|
-
}
|
|
743
|
-
return rawContent;
|
|
744
|
-
}
|
|
514
|
+
else {
|
|
515
|
+
messagesToSend = [
|
|
516
|
+
{ role: "system", content: systemContent },
|
|
517
|
+
...messages,
|
|
518
|
+
];
|
|
745
519
|
}
|
|
746
|
-
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, "LLM returned no content");
|
|
747
|
-
}
|
|
748
|
-
// Non-OpenAI or no schema: use NDJSON streaming approach
|
|
749
|
-
const llm = getLLMModel(modelName, config);
|
|
750
|
-
let ndjsonSystemContent = NDJSON_SYSTEM_PROMPT.replace(`{minPercent}`, minPercent).replace(`{maxPercent}`, maxPercent);
|
|
751
|
-
// If schema is provided, include it in the system prompt
|
|
752
|
-
if (expectsJsonResponse && schema) {
|
|
753
|
-
const jsonSchema = (0, zod_to_json_schema_1.zodToJsonSchema)(schema, { target: "openApi3" });
|
|
754
|
-
ndjsonSystemContent += `\n\nThe "content" field in the "final" event MUST conform to this JSON schema:\n${JSON.stringify(jsonSchema, null, 2)}`;
|
|
755
|
-
}
|
|
756
|
-
let messagesWithNDJSON;
|
|
757
|
-
if (messages.length > 0 && messages[0].role === "system") {
|
|
758
|
-
messagesWithNDJSON = [
|
|
759
|
-
{
|
|
760
|
-
role: "system",
|
|
761
|
-
content: `${ndjsonSystemContent}\n\n---\n\n${messages[0].content}`,
|
|
762
|
-
},
|
|
763
|
-
...messages.slice(1),
|
|
764
|
-
];
|
|
765
520
|
}
|
|
766
521
|
else {
|
|
767
|
-
|
|
768
|
-
{
|
|
769
|
-
role: "system",
|
|
770
|
-
content: ndjsonSystemContent,
|
|
771
|
-
},
|
|
772
|
-
...messages,
|
|
773
|
-
];
|
|
522
|
+
messagesToSend = messages;
|
|
774
523
|
}
|
|
775
|
-
// Stream
|
|
776
|
-
let buffer = "";
|
|
777
|
-
let finalContent = "";
|
|
524
|
+
// Stream and report server-side progress based on time elapsed
|
|
778
525
|
let rawContent = "";
|
|
779
|
-
|
|
526
|
+
let chunkCount = 0;
|
|
527
|
+
const progressReportInterval = 10; // Report every N chunks
|
|
528
|
+
const startTime = Date.now();
|
|
529
|
+
// Use a time-based asymptotic curve: progress approaches maxPercent but never
|
|
530
|
+
// overshoots. This avoids the magic "expected length" constant — longer responses
|
|
531
|
+
// simply slow the curve down rather than exceeding the range.
|
|
532
|
+
const expectedDurationMs = 15_000; // Tune: expected typical response time
|
|
533
|
+
const stream = await llm.stream(messagesToSend);
|
|
780
534
|
for await (const chunk of stream) {
|
|
781
535
|
const content = chunk?.content || chunk;
|
|
782
536
|
if (typeof content === "string") {
|
|
783
|
-
buffer += content;
|
|
784
537
|
rawContent += content;
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
message: parsed.message,
|
|
797
|
-
progress: clampedProgress,
|
|
798
|
-
});
|
|
799
|
-
}
|
|
800
|
-
else if (parsed.type === "final") {
|
|
801
|
-
if (parsed.content) {
|
|
802
|
-
finalContent = parsed.content;
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
}
|
|
538
|
+
chunkCount++;
|
|
539
|
+
if (chunkCount % progressReportInterval === 0) {
|
|
540
|
+
const elapsed = Date.now() - startTime;
|
|
541
|
+
// Asymptotic curve: fast early progress that slows as it approaches max
|
|
542
|
+
const progress = Math.round(minPercent +
|
|
543
|
+
(maxPercent - minPercent - 5) *
|
|
544
|
+
(1 - Math.exp(-elapsed / expectedDurationMs)));
|
|
545
|
+
await onProgressReport({
|
|
546
|
+
message: "Generating content...",
|
|
547
|
+
progress: Math.min(progress, maxPercent - 5),
|
|
548
|
+
});
|
|
806
549
|
}
|
|
807
550
|
}
|
|
808
551
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
552
|
+
await onProgressReport({
|
|
553
|
+
message: "Processing complete",
|
|
554
|
+
progress: maxPercent,
|
|
555
|
+
});
|
|
556
|
+
if (!rawContent) {
|
|
557
|
+
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, "LLM returned no content");
|
|
814
558
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
validateWithSchema(finalContent, schema);
|
|
819
|
-
}
|
|
820
|
-
return JSON.stringify(finalContent);
|
|
821
|
-
}
|
|
822
|
-
if (typeof finalContent === "string") {
|
|
823
|
-
try {
|
|
824
|
-
const parsed = JSON.parse(finalContent);
|
|
825
|
-
if (expectsJsonResponse && schema) {
|
|
826
|
-
validateWithSchema(parsed, schema);
|
|
827
|
-
}
|
|
828
|
-
return JSON.stringify(parsed);
|
|
829
|
-
}
|
|
830
|
-
catch (parseError) {
|
|
831
|
-
if (parseError?.code === const_js_1.default.HTTP_CODES.UNPROCESSABLE_ENTITY) {
|
|
832
|
-
throw parseError;
|
|
833
|
-
}
|
|
834
|
-
return finalContent;
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
return finalContent;
|
|
559
|
+
// If not expecting JSON, return raw content directly
|
|
560
|
+
if (!expectsJsonResponse) {
|
|
561
|
+
return rawContent;
|
|
838
562
|
}
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
if (extractedContent) {
|
|
850
|
-
try {
|
|
851
|
-
const parsed = JSON.parse(extractedContent);
|
|
852
|
-
if (expectsJsonResponse && schema) {
|
|
853
|
-
validateWithSchema(parsed, schema);
|
|
854
|
-
}
|
|
855
|
-
return JSON.stringify(parsed);
|
|
856
|
-
}
|
|
857
|
-
catch {
|
|
858
|
-
return extractedContent;
|
|
563
|
+
// Parse and validate JSON response
|
|
564
|
+
const trimmed = rawContent.trim();
|
|
565
|
+
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
|
|
566
|
+
(trimmed.startsWith("[") && trimmed.endsWith("]"))) {
|
|
567
|
+
try {
|
|
568
|
+
const parsed = JSON.parse(trimmed);
|
|
569
|
+
if (schema) {
|
|
570
|
+
validateWithSchema(parsed, schema);
|
|
859
571
|
}
|
|
572
|
+
return JSON.stringify(parsed);
|
|
860
573
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
validateWithSchema(extracted, schema);
|
|
574
|
+
catch (parseError) {
|
|
575
|
+
if (parseError?.code === const_js_1.default.HTTP_CODES.UNPROCESSABLE_ENTITY) {
|
|
576
|
+
throw parseError;
|
|
865
577
|
}
|
|
866
|
-
return JSON.stringify(extracted);
|
|
867
578
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
else if (typeof rawContent === "object" && rawContent !== null) {
|
|
874
|
-
preview = JSON.stringify(rawContent).substring(0, 100);
|
|
875
|
-
}
|
|
876
|
-
else if (rawContent !== undefined && rawContent !== null) {
|
|
877
|
-
preview = String(rawContent).substring(0, 100);
|
|
878
|
-
}
|
|
879
|
-
else {
|
|
880
|
-
preview = "[empty response]";
|
|
881
|
-
}
|
|
882
|
-
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, `LLM returned non-JSON response when JSON was expected: ${preview}...`);
|
|
579
|
+
}
|
|
580
|
+
const extracted = extractJSONFromResponse(rawContent);
|
|
581
|
+
if (extracted) {
|
|
582
|
+
if (schema) {
|
|
583
|
+
validateWithSchema(extracted, schema);
|
|
883
584
|
}
|
|
884
|
-
return
|
|
585
|
+
return JSON.stringify(extracted);
|
|
586
|
+
}
|
|
587
|
+
let preview = "";
|
|
588
|
+
if (typeof rawContent === "string") {
|
|
589
|
+
preview = rawContent.substring(0, 100);
|
|
590
|
+
}
|
|
591
|
+
else if (typeof rawContent === "object" && rawContent !== null) {
|
|
592
|
+
preview = JSON.stringify(rawContent).substring(0, 100);
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
preview = "[empty response]";
|
|
885
596
|
}
|
|
886
|
-
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR,
|
|
597
|
+
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, `LLM returned non-JSON response when JSON was expected: ${preview}...`);
|
|
887
598
|
}
|
|
888
599
|
else {
|
|
889
600
|
// Non-streaming mode: use native response_format for OpenAI when schema is provided
|
package/dist/esm/langChain.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
declare const _default: {
|
|
2
|
-
createAgent: (name: string, modelName: string, systemPrompt: string, tools: any[], responseFormat: any, config: any
|
|
2
|
+
createAgent: (name: string, modelName: string, systemPrompt: string, tools: any[], responseFormat: any, config: any) => any;
|
|
3
3
|
runAgent: (agent: any, prompt: string, config: any, onProgress?: Function | null) => Promise<any>;
|
|
4
4
|
runChatPromptWithModel: (modelName: string, config: any, prompt: any, onProgressReport: any) => any;
|
|
5
5
|
runPromptWithModel: (modelName: string, config: any, prompt: any, onProgressReport: any, minPercent?: number, maxPercent?: number, expectsJsonResponse?: boolean, schema?: any, agentName?: string, tools?: any[]) => Promise<any>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":";wBAkSQ,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,KACV,GAAG;sBAyBG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,KAC1B,OAAO,CAAC,GAAG,CAAC;wCAufF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,KACpB,GAAG;oCA5YO,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,eACT,MAAM,eACN,MAAM,wBACG,OAAO,WACpB,GAAG,cACA,MAAM,UACV,GAAG,EAAE,KACX,OAAO,CAAC,GAAG,CAAC;sDAisBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,KACX,OAAO,CAAC,GAAG,CAAC;0CAjwB8B,GAAG,KAAG,MAAM;;AA4zBzD,wBAOE"}
|
package/dist/esm/langChain.js
CHANGED
|
@@ -7,34 +7,7 @@ import constants from "./const.js";
|
|
|
7
7
|
import langChain from "langchain";
|
|
8
8
|
import errorHandlingHelper from "./errorHandling.js";
|
|
9
9
|
import logger from "./logger.js";
|
|
10
|
-
import { z } from "zod";
|
|
11
10
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
12
|
-
const NDJSON_SYSTEM_PROMPT = `
|
|
13
|
-
You are a streaming JSON generator.
|
|
14
|
-
|
|
15
|
-
You MUST output NDJSON (newline-delimited JSON).
|
|
16
|
-
That means: ONE valid JSON object per line.
|
|
17
|
-
|
|
18
|
-
Allowed event types:
|
|
19
|
-
- "progress": short status update for the user
|
|
20
|
-
- "final": final complete result
|
|
21
|
-
|
|
22
|
-
Rules:
|
|
23
|
-
- Output ONLY NDJSON lines (no markdown, no extra text).
|
|
24
|
-
- Each line must be valid JSON.
|
|
25
|
-
- "progress" must include: type, message, pct (with values between {minPercent} and {maxPercent})
|
|
26
|
-
- "final" must include: type, content
|
|
27
|
-
- End with EXACTLY ONE "final" event.
|
|
28
|
-
- Do NOT reveal hidden chain-of-thought.
|
|
29
|
-
|
|
30
|
-
CRITICAL - Escape special characters in ALL string values:
|
|
31
|
-
- Newlines must be written as \\n (two characters: backslash + n)
|
|
32
|
-
- Tabs must be written as \\t (two characters: backslash + t)
|
|
33
|
-
- Carriage returns must be written as \\r (two characters: backslash + r)
|
|
34
|
-
- Double quotes inside strings must be written as \\"
|
|
35
|
-
- Backslashes must be written as \\\\
|
|
36
|
-
This ensures valid JSON output.
|
|
37
|
-
`.trim();
|
|
38
11
|
const JSON_ESCAPE_INSTRUCTION = `
|
|
39
12
|
CRITICAL - Your response must be valid JSON. Escape ALL special characters in string values:
|
|
40
13
|
- Newlines → \\n
|
|
@@ -44,12 +17,6 @@ CRITICAL - Your response must be valid JSON. Escape ALL special characters in st
|
|
|
44
17
|
- Backslashes → \\\\
|
|
45
18
|
Do NOT include raw newlines, tabs, or unescaped quotes inside JSON string values.
|
|
46
19
|
`.trim();
|
|
47
|
-
const REPORT_PROGRESS_TOOL = `
|
|
48
|
-
Report Progress to User
|
|
49
|
-
Before each task or step, call the tool report_progress with:
|
|
50
|
-
message: brief description of work you plan to do that makes sense for individuals in Learning and Development or Business Strategy roles. Don't include the word JSON in any of the messages.
|
|
51
|
-
percent: cumulative completion percentage with values between {minPercent}% and {maxPercent}%. Don't report percentage completion outside of this range.
|
|
52
|
-
`;
|
|
53
20
|
/**
|
|
54
21
|
* Converts a Zod validation error object into a human-readable multi-line string.
|
|
55
22
|
* Each failing field is described with its dot-notation path and a contextual message
|
|
@@ -217,82 +184,6 @@ const extractJSONFromResponse = (text) => {
|
|
|
217
184
|
}
|
|
218
185
|
return null;
|
|
219
186
|
};
|
|
220
|
-
/**
|
|
221
|
-
* Attempts to parse a JSON string without throwing. On failure it tries a second parse
|
|
222
|
-
* after sanitising Unicode control characters and normalising Windows-style line endings
|
|
223
|
-
* to `\n`. Returns `null` if both attempts fail, making it safe to call in stream
|
|
224
|
-
* processing loops where individual NDJSON lines may be malformed.
|
|
225
|
-
* @param line - A single line of text expected to contain a JSON value
|
|
226
|
-
* @returns The parsed value, or `null` if parsing failed after both attempts
|
|
227
|
-
*/
|
|
228
|
-
const safeJsonParse = (line) => {
|
|
229
|
-
try {
|
|
230
|
-
return JSON.parse(line);
|
|
231
|
-
}
|
|
232
|
-
catch {
|
|
233
|
-
// Try with special character handling
|
|
234
|
-
try {
|
|
235
|
-
const sanitized = line
|
|
236
|
-
.replace(/[\u0000-\u001F\u007F-\u009F]/g, " ")
|
|
237
|
-
.replace(/\r\n/g, "\\n")
|
|
238
|
-
.replace(/\r/g, "\\n");
|
|
239
|
-
return JSON.parse(sanitized);
|
|
240
|
-
}
|
|
241
|
-
catch {
|
|
242
|
-
return null;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
};
|
|
246
|
-
/**
|
|
247
|
-
* Creates a LangChain tool named `report_progress` that agents can call to emit
|
|
248
|
-
* progress updates during multi-step execution. When the tool is invoked by the agent,
|
|
249
|
-
* it clamps the reported percentage to the `[minPercent, maxPercent]` range, forwards
|
|
250
|
-
* the update to the `onProgress` callback, and returns a JSON acknowledgement string to
|
|
251
|
-
* the agent. If the callback throws, the tool returns a JSON error acknowledgement
|
|
252
|
-
* instead of propagating the exception.
|
|
253
|
-
* @param onProgress - Async or sync callback invoked with `{ progress, message }` on
|
|
254
|
-
* each agent progress report
|
|
255
|
-
* @param minPercent - The lower bound of the percentage range the agent is allowed to
|
|
256
|
-
* report; defaults to `0`
|
|
257
|
-
* @param maxPercent - The upper bound of the percentage range the agent is allowed to
|
|
258
|
-
* report; defaults to `100`
|
|
259
|
-
* @returns A LangChain tool instance configured with a Zod schema for `{ stage, message, percent }`
|
|
260
|
-
*/
|
|
261
|
-
const getAIProgressTool = (onProgress, minPercent = 0, maxPercent = 100) => langChain.tool(async ({ stage, message, percent }) => {
|
|
262
|
-
let clampedPct = 0;
|
|
263
|
-
try {
|
|
264
|
-
// Coerce percent to number and clamp to be within minPercent and maxPercent bounds
|
|
265
|
-
const rawPct = typeof percent === "number" ? percent : parseFloat(percent) || 0;
|
|
266
|
-
clampedPct = Math.max(minPercent, Math.min(maxPercent, rawPct));
|
|
267
|
-
// NOTE: Monotonicity is not enforced here; implement if needed per session/context.
|
|
268
|
-
if (onProgress &&
|
|
269
|
-
minPercent <= clampedPct &&
|
|
270
|
-
clampedPct <= maxPercent) {
|
|
271
|
-
await onProgress({
|
|
272
|
-
progress: clampedPct,
|
|
273
|
-
message: message,
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
finally {
|
|
278
|
-
return JSON.stringify({
|
|
279
|
-
acknowledged: true,
|
|
280
|
-
receivedAt: Date.now(),
|
|
281
|
-
stage,
|
|
282
|
-
percent: clampedPct,
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
}, {
|
|
286
|
-
name: "report_progress",
|
|
287
|
-
description: `Mandatory after each phase: call with {stage, message, percent}. Percent must be between ${minPercent} and ${maxPercent} and monotonically increase.`,
|
|
288
|
-
schema: z.object({
|
|
289
|
-
stage: z.string().describe("Current stage of the process"),
|
|
290
|
-
message: z.string().describe("Progress message to display"),
|
|
291
|
-
percent: z
|
|
292
|
-
.number()
|
|
293
|
-
.describe(`Progress percentage (${minPercent}-${maxPercent})`),
|
|
294
|
-
}),
|
|
295
|
-
});
|
|
296
187
|
/**
|
|
297
188
|
* Instantiates and returns the appropriate LangChain chat model based on the model
|
|
298
189
|
* name prefix. `claude-` maps to `ChatAnthropic`, `gemini-` maps to
|
|
@@ -358,11 +249,7 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
358
249
|
};
|
|
359
250
|
/**
|
|
360
251
|
* Constructs a LangChain agent configured with a specified model, system prompt, and
|
|
361
|
-
* set of tools.
|
|
362
|
-
* reporting instruction block is appended to the system prompt and a `report_progress`
|
|
363
|
-
* tool is added to the tools array so the agent can emit incremental progress updates
|
|
364
|
-
* during execution. The percentage range for progress reporting is bounded by
|
|
365
|
-
* `minPercent` and `maxPercent`.
|
|
252
|
+
* set of tools.
|
|
366
253
|
* @param name - A human-readable display name for the agent
|
|
367
254
|
* @param modelName - The LLM identifier passed to `getLLMModel` (e.g. `"gpt-4o"`)
|
|
368
255
|
* @param systemPrompt - The base system prompt describing the agent's role and behaviour
|
|
@@ -371,30 +258,14 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
371
258
|
* @param responseFormat - Optional structured response format descriptor passed to the
|
|
372
259
|
* LangChain agent constructor
|
|
373
260
|
* @param config - Configuration object forwarded to `getLLMModel` (API keys, temperature, etc.)
|
|
374
|
-
* @
|
|
375
|
-
* progress tool is added and the system prompt is extended
|
|
376
|
-
* @param minPercent - Minimum progress percentage the agent is allowed to report;
|
|
377
|
-
* defaults to `0`
|
|
378
|
-
* @param maxPercent - Maximum progress percentage the agent is allowed to report;
|
|
379
|
-
* defaults to `100`
|
|
380
|
-
* @returns A configured LangChain agent instance ready to be run with `runAIAgent`
|
|
261
|
+
* @returns A configured LangChain agent instance ready to be run with `runAgent`
|
|
381
262
|
*/
|
|
382
|
-
const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat, config
|
|
383
|
-
//Prepare the complete system prompt with progress reporting instructions if needed
|
|
384
|
-
const completeSystemPrompt = `
|
|
385
|
-
${systemPrompt}
|
|
386
|
-
${onReportProgress
|
|
387
|
-
? REPORT_PROGRESS_TOOL.replace(`{minPercent}`, minPercent).replace(`{maxPercent}`, maxPercent)
|
|
388
|
-
: ""}
|
|
389
|
-
`.trim();
|
|
390
|
-
// Create the agent with the specified model, system prompt, tools, and response format
|
|
263
|
+
const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat, config) => {
|
|
391
264
|
const agent = langChain.createAgent({
|
|
392
265
|
name: name,
|
|
393
266
|
model: getLLMModel(modelName, config),
|
|
394
|
-
systemPrompt:
|
|
395
|
-
tools
|
|
396
|
-
? [...tools, getAIProgressTool(onReportProgress, minPercent, maxPercent)]
|
|
397
|
-
: tools,
|
|
267
|
+
systemPrompt: systemPrompt.trim(),
|
|
268
|
+
tools,
|
|
398
269
|
...(responseFormat ? { responseFormat: responseFormat } : {}),
|
|
399
270
|
});
|
|
400
271
|
return agent;
|
|
@@ -402,10 +273,9 @@ const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat,
|
|
|
402
273
|
/**
|
|
403
274
|
* Executes a LangChain agent with a single user prompt and returns the raw agent
|
|
404
275
|
* response. When an `onProgress` callback is provided, LangChain callbacks are
|
|
405
|
-
* registered to forward `tool_start`, `
|
|
406
|
-
* from the `report_progress` tool), `agent_action`, and error events to the caller.
|
|
276
|
+
* registered to forward `tool_start`, `agent_action`, and error events to the caller.
|
|
407
277
|
* Execution time is logged at the info level on completion.
|
|
408
|
-
* @param agent - A LangChain agent instance created by `
|
|
278
|
+
* @param agent - A LangChain agent instance created by `createAgent`
|
|
409
279
|
* @param prompt - The user message string to send to the agent
|
|
410
280
|
* @param config - Configuration object; `config.recursionLimit` controls the maximum
|
|
411
281
|
* number of agent steps (defaults to `25` when not specified)
|
|
@@ -415,8 +285,6 @@ const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat,
|
|
|
415
285
|
*/
|
|
416
286
|
const runAgent = async (agent, prompt, config, onProgress = null) => {
|
|
417
287
|
const startTime = Date.now();
|
|
418
|
-
// Build callbacks for progress reporting if onProgress is provided
|
|
419
|
-
// const agentDisplayName = `${agent.options?.name} AI Agent`;
|
|
420
288
|
const callbacks = onProgress
|
|
421
289
|
? [
|
|
422
290
|
{
|
|
@@ -428,24 +296,6 @@ const runAgent = async (agent, prompt, config, onProgress = null) => {
|
|
|
428
296
|
tool: toolInfo.name,
|
|
429
297
|
});
|
|
430
298
|
},
|
|
431
|
-
handleToolEnd: (output) => {
|
|
432
|
-
try {
|
|
433
|
-
const progressInfo = extractJSONFromResponse(output.content);
|
|
434
|
-
if (progressInfo &&
|
|
435
|
-
typeof progressInfo === "object" &&
|
|
436
|
-
progressInfo.percent &&
|
|
437
|
-
progressInfo.message) {
|
|
438
|
-
onProgress({
|
|
439
|
-
progress: progressInfo.percent,
|
|
440
|
-
message: progressInfo.message,
|
|
441
|
-
output: typeof output === "string"
|
|
442
|
-
? output
|
|
443
|
-
: JSON.stringify(output),
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
catch { }
|
|
448
|
-
},
|
|
449
299
|
handleAgentAction: (action) => {
|
|
450
300
|
onProgress({
|
|
451
301
|
type: "agent_action",
|
|
@@ -454,18 +304,6 @@ const runAgent = async (agent, prompt, config, onProgress = null) => {
|
|
|
454
304
|
input: action.toolInput,
|
|
455
305
|
});
|
|
456
306
|
},
|
|
457
|
-
// handleLLMStart: () => {
|
|
458
|
-
// onProgress({
|
|
459
|
-
// type: "llm_start",
|
|
460
|
-
// message: `${agentDisplayName} is thinking`,
|
|
461
|
-
// });
|
|
462
|
-
// },
|
|
463
|
-
// handleLLMEnd: () => {
|
|
464
|
-
// onProgress({
|
|
465
|
-
// type: "llm_end",
|
|
466
|
-
// message: `${agentDisplayName} has completed processing`,
|
|
467
|
-
// });
|
|
468
|
-
// },
|
|
469
307
|
handleChainError: (err) => {
|
|
470
308
|
onProgress({
|
|
471
309
|
type: "error",
|
|
@@ -569,26 +407,11 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
569
407
|
? `${JSON_ESCAPE_INSTRUCTION}\n\n---\n\n${systemPrompt}`
|
|
570
408
|
: JSON_ESCAPE_INSTRUCTION;
|
|
571
409
|
}
|
|
572
|
-
//
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
trackingProgressCallback = (data) => {
|
|
578
|
-
if (typeof data.progress === "number") {
|
|
579
|
-
progressTracker.current = data.progress;
|
|
580
|
-
}
|
|
581
|
-
onProgressReport(data);
|
|
582
|
-
};
|
|
583
|
-
}
|
|
584
|
-
// Create the agent with tools and progress callback
|
|
585
|
-
const agent = createAgent(agentName, modelName, systemPrompt, agentTools, null, // responseFormat
|
|
586
|
-
config, trackingProgressCallback, minPercent, maxPercent);
|
|
587
|
-
// Run the agent with callback that includes current progress
|
|
588
|
-
const callbackWithProgress = onProgressReport
|
|
589
|
-
? (data) => onProgressReport({ ...data, progress: progressTracker.current })
|
|
590
|
-
: null;
|
|
591
|
-
const response = await runAgent(agent, userPrompt, config, callbackWithProgress);
|
|
410
|
+
// Create the agent with tools
|
|
411
|
+
const agent = createAgent(agentName, modelName, systemPrompt, [...tools], null, // responseFormat
|
|
412
|
+
config);
|
|
413
|
+
// Run the agent with progress callback
|
|
414
|
+
const response = await runAgent(agent, userPrompt, config, onProgressReport || null);
|
|
592
415
|
// Extract content from agent response
|
|
593
416
|
const messages = response?.messages || [];
|
|
594
417
|
if (messages.length === 0) {
|
|
@@ -661,224 +484,112 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
661
484
|
throw new Error("Prompt must be a string or array of messages");
|
|
662
485
|
}
|
|
663
486
|
if (onProgressReport) {
|
|
664
|
-
//
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
487
|
+
// Streaming mode: use server-side chunk-based progress for all models
|
|
488
|
+
const useNativeSchema = expectsJsonResponse && schema && modelName.startsWith("gpt-");
|
|
489
|
+
const llm = getLLMModel(modelName, config, useNativeSchema ? schema : null);
|
|
490
|
+
// Build messages with JSON instructions if needed
|
|
491
|
+
let messagesToSend;
|
|
492
|
+
if (expectsJsonResponse) {
|
|
493
|
+
let systemContent = useNativeSchema
|
|
494
|
+
? "Respond with valid JSON."
|
|
495
|
+
: JSON_ESCAPE_INSTRUCTION;
|
|
496
|
+
if (schema && !useNativeSchema) {
|
|
497
|
+
const jsonSchema = zodToJsonSchema(schema, { target: "openApi3" });
|
|
498
|
+
systemContent += `\n\nYour response MUST conform to this JSON schema:\n${JSON.stringify(jsonSchema, null, 2)}`;
|
|
499
|
+
}
|
|
500
|
+
if (messages.length > 0 && messages[0].role === "system") {
|
|
501
|
+
messagesToSend = [
|
|
502
|
+
{
|
|
503
|
+
role: "system",
|
|
504
|
+
content: `${systemContent}\n\n---\n\n${messages[0].content}`,
|
|
505
|
+
},
|
|
506
|
+
...messages.slice(1),
|
|
674
507
|
];
|
|
675
|
-
let rawContent = "";
|
|
676
|
-
let lastProgressReport = 0;
|
|
677
|
-
const progressInterval = 1000; // Report progress every 1000 characters
|
|
678
|
-
const stream = await llm.stream(messagesToSend);
|
|
679
|
-
for await (const chunk of stream) {
|
|
680
|
-
const content = chunk?.content || chunk;
|
|
681
|
-
if (typeof content === "string") {
|
|
682
|
-
rawContent += content;
|
|
683
|
-
// Report progress based on content length
|
|
684
|
-
if (rawContent.length - lastProgressReport >= progressInterval) {
|
|
685
|
-
lastProgressReport = rawContent.length;
|
|
686
|
-
const progress = Math.min(maxPercent - 5, // Reserve last 5% for completion
|
|
687
|
-
minPercent +
|
|
688
|
-
Math.floor((rawContent.length / 5000) * (maxPercent - minPercent)));
|
|
689
|
-
await onProgressReport({
|
|
690
|
-
message: "Generating content...",
|
|
691
|
-
progress: progress,
|
|
692
|
-
});
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
508
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
// Parse and validate the response
|
|
702
|
-
if (rawContent) {
|
|
703
|
-
try {
|
|
704
|
-
const parsed = JSON.parse(rawContent.trim());
|
|
705
|
-
if (schema) {
|
|
706
|
-
validateWithSchema(parsed, schema);
|
|
707
|
-
}
|
|
708
|
-
return JSON.stringify(parsed);
|
|
709
|
-
}
|
|
710
|
-
catch (parseError) {
|
|
711
|
-
if (parseError?.code === constants.HTTP_CODES.UNPROCESSABLE_ENTITY) {
|
|
712
|
-
throw parseError;
|
|
713
|
-
}
|
|
714
|
-
// Try to extract JSON
|
|
715
|
-
const extracted = extractJSONFromResponse(rawContent);
|
|
716
|
-
if (extracted) {
|
|
717
|
-
if (schema) {
|
|
718
|
-
validateWithSchema(extracted, schema);
|
|
719
|
-
}
|
|
720
|
-
return JSON.stringify(extracted);
|
|
721
|
-
}
|
|
722
|
-
if (expectsJsonResponse) {
|
|
723
|
-
let preview = "";
|
|
724
|
-
if (typeof rawContent === "string") {
|
|
725
|
-
preview = rawContent.substring(0, 100);
|
|
726
|
-
}
|
|
727
|
-
else if (typeof rawContent === "object" && rawContent !== null) {
|
|
728
|
-
preview = JSON.stringify(rawContent).substring(0, 100);
|
|
729
|
-
}
|
|
730
|
-
else if (rawContent !== undefined && rawContent !== null) {
|
|
731
|
-
preview = String(rawContent).substring(0, 100);
|
|
732
|
-
}
|
|
733
|
-
else {
|
|
734
|
-
preview = "[empty response]";
|
|
735
|
-
}
|
|
736
|
-
throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `LLM returned non-JSON response when JSON was expected: ${preview}...`);
|
|
737
|
-
}
|
|
738
|
-
return rawContent;
|
|
739
|
-
}
|
|
509
|
+
else {
|
|
510
|
+
messagesToSend = [
|
|
511
|
+
{ role: "system", content: systemContent },
|
|
512
|
+
...messages,
|
|
513
|
+
];
|
|
740
514
|
}
|
|
741
|
-
throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, "LLM returned no content");
|
|
742
|
-
}
|
|
743
|
-
// Non-OpenAI or no schema: use NDJSON streaming approach
|
|
744
|
-
const llm = getLLMModel(modelName, config);
|
|
745
|
-
let ndjsonSystemContent = NDJSON_SYSTEM_PROMPT.replace(`{minPercent}`, minPercent).replace(`{maxPercent}`, maxPercent);
|
|
746
|
-
// If schema is provided, include it in the system prompt
|
|
747
|
-
if (expectsJsonResponse && schema) {
|
|
748
|
-
const jsonSchema = zodToJsonSchema(schema, { target: "openApi3" });
|
|
749
|
-
ndjsonSystemContent += `\n\nThe "content" field in the "final" event MUST conform to this JSON schema:\n${JSON.stringify(jsonSchema, null, 2)}`;
|
|
750
|
-
}
|
|
751
|
-
let messagesWithNDJSON;
|
|
752
|
-
if (messages.length > 0 && messages[0].role === "system") {
|
|
753
|
-
messagesWithNDJSON = [
|
|
754
|
-
{
|
|
755
|
-
role: "system",
|
|
756
|
-
content: `${ndjsonSystemContent}\n\n---\n\n${messages[0].content}`,
|
|
757
|
-
},
|
|
758
|
-
...messages.slice(1),
|
|
759
|
-
];
|
|
760
515
|
}
|
|
761
516
|
else {
|
|
762
|
-
|
|
763
|
-
{
|
|
764
|
-
role: "system",
|
|
765
|
-
content: ndjsonSystemContent,
|
|
766
|
-
},
|
|
767
|
-
...messages,
|
|
768
|
-
];
|
|
517
|
+
messagesToSend = messages;
|
|
769
518
|
}
|
|
770
|
-
// Stream
|
|
771
|
-
let buffer = "";
|
|
772
|
-
let finalContent = "";
|
|
519
|
+
// Stream and report server-side progress based on time elapsed
|
|
773
520
|
let rawContent = "";
|
|
774
|
-
|
|
521
|
+
let chunkCount = 0;
|
|
522
|
+
const progressReportInterval = 10; // Report every N chunks
|
|
523
|
+
const startTime = Date.now();
|
|
524
|
+
// Use a time-based asymptotic curve: progress approaches maxPercent but never
|
|
525
|
+
// overshoots. This avoids the magic "expected length" constant — longer responses
|
|
526
|
+
// simply slow the curve down rather than exceeding the range.
|
|
527
|
+
const expectedDurationMs = 15_000; // Tune: expected typical response time
|
|
528
|
+
const stream = await llm.stream(messagesToSend);
|
|
775
529
|
for await (const chunk of stream) {
|
|
776
530
|
const content = chunk?.content || chunk;
|
|
777
531
|
if (typeof content === "string") {
|
|
778
|
-
buffer += content;
|
|
779
532
|
rawContent += content;
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
message: parsed.message,
|
|
792
|
-
progress: clampedProgress,
|
|
793
|
-
});
|
|
794
|
-
}
|
|
795
|
-
else if (parsed.type === "final") {
|
|
796
|
-
if (parsed.content) {
|
|
797
|
-
finalContent = parsed.content;
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
}
|
|
533
|
+
chunkCount++;
|
|
534
|
+
if (chunkCount % progressReportInterval === 0) {
|
|
535
|
+
const elapsed = Date.now() - startTime;
|
|
536
|
+
// Asymptotic curve: fast early progress that slows as it approaches max
|
|
537
|
+
const progress = Math.round(minPercent +
|
|
538
|
+
(maxPercent - minPercent - 5) *
|
|
539
|
+
(1 - Math.exp(-elapsed / expectedDurationMs)));
|
|
540
|
+
await onProgressReport({
|
|
541
|
+
message: "Generating content...",
|
|
542
|
+
progress: Math.min(progress, maxPercent - 5),
|
|
543
|
+
});
|
|
801
544
|
}
|
|
802
545
|
}
|
|
803
546
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
547
|
+
await onProgressReport({
|
|
548
|
+
message: "Processing complete",
|
|
549
|
+
progress: maxPercent,
|
|
550
|
+
});
|
|
551
|
+
if (!rawContent) {
|
|
552
|
+
throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, "LLM returned no content");
|
|
809
553
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
validateWithSchema(finalContent, schema);
|
|
814
|
-
}
|
|
815
|
-
return JSON.stringify(finalContent);
|
|
816
|
-
}
|
|
817
|
-
if (typeof finalContent === "string") {
|
|
818
|
-
try {
|
|
819
|
-
const parsed = JSON.parse(finalContent);
|
|
820
|
-
if (expectsJsonResponse && schema) {
|
|
821
|
-
validateWithSchema(parsed, schema);
|
|
822
|
-
}
|
|
823
|
-
return JSON.stringify(parsed);
|
|
824
|
-
}
|
|
825
|
-
catch (parseError) {
|
|
826
|
-
if (parseError?.code === constants.HTTP_CODES.UNPROCESSABLE_ENTITY) {
|
|
827
|
-
throw parseError;
|
|
828
|
-
}
|
|
829
|
-
return finalContent;
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
return finalContent;
|
|
554
|
+
// If not expecting JSON, return raw content directly
|
|
555
|
+
if (!expectsJsonResponse) {
|
|
556
|
+
return rawContent;
|
|
833
557
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
if (extractedContent) {
|
|
845
|
-
try {
|
|
846
|
-
const parsed = JSON.parse(extractedContent);
|
|
847
|
-
if (expectsJsonResponse && schema) {
|
|
848
|
-
validateWithSchema(parsed, schema);
|
|
849
|
-
}
|
|
850
|
-
return JSON.stringify(parsed);
|
|
851
|
-
}
|
|
852
|
-
catch {
|
|
853
|
-
return extractedContent;
|
|
558
|
+
// Parse and validate JSON response
|
|
559
|
+
const trimmed = rawContent.trim();
|
|
560
|
+
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
|
|
561
|
+
(trimmed.startsWith("[") && trimmed.endsWith("]"))) {
|
|
562
|
+
try {
|
|
563
|
+
const parsed = JSON.parse(trimmed);
|
|
564
|
+
if (schema) {
|
|
565
|
+
validateWithSchema(parsed, schema);
|
|
854
566
|
}
|
|
567
|
+
return JSON.stringify(parsed);
|
|
855
568
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
validateWithSchema(extracted, schema);
|
|
569
|
+
catch (parseError) {
|
|
570
|
+
if (parseError?.code === constants.HTTP_CODES.UNPROCESSABLE_ENTITY) {
|
|
571
|
+
throw parseError;
|
|
860
572
|
}
|
|
861
|
-
return JSON.stringify(extracted);
|
|
862
573
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
else if (typeof rawContent === "object" && rawContent !== null) {
|
|
869
|
-
preview = JSON.stringify(rawContent).substring(0, 100);
|
|
870
|
-
}
|
|
871
|
-
else if (rawContent !== undefined && rawContent !== null) {
|
|
872
|
-
preview = String(rawContent).substring(0, 100);
|
|
873
|
-
}
|
|
874
|
-
else {
|
|
875
|
-
preview = "[empty response]";
|
|
876
|
-
}
|
|
877
|
-
throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `LLM returned non-JSON response when JSON was expected: ${preview}...`);
|
|
574
|
+
}
|
|
575
|
+
const extracted = extractJSONFromResponse(rawContent);
|
|
576
|
+
if (extracted) {
|
|
577
|
+
if (schema) {
|
|
578
|
+
validateWithSchema(extracted, schema);
|
|
878
579
|
}
|
|
879
|
-
return
|
|
580
|
+
return JSON.stringify(extracted);
|
|
581
|
+
}
|
|
582
|
+
let preview = "";
|
|
583
|
+
if (typeof rawContent === "string") {
|
|
584
|
+
preview = rawContent.substring(0, 100);
|
|
585
|
+
}
|
|
586
|
+
else if (typeof rawContent === "object" && rawContent !== null) {
|
|
587
|
+
preview = JSON.stringify(rawContent).substring(0, 100);
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
preview = "[empty response]";
|
|
880
591
|
}
|
|
881
|
-
throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR,
|
|
592
|
+
throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `LLM returned non-JSON response when JSON was expected: ${preview}...`);
|
|
882
593
|
}
|
|
883
594
|
else {
|
|
884
595
|
// Non-streaming mode: use native response_format for OpenAI when schema is provided
|