@stackfactor/agent-utils 1.0.10 → 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.
@@ -1,5 +1,5 @@
1
1
  declare const _default: {
2
- createAgent: (name: string, modelName: string, systemPrompt: string, tools: any[], responseFormat: any, config: any, onReportProgress?: Function | null, minPercent?: number, maxPercent?: number) => 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":";wBAsaQ,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,qBACO,QAAQ,GAAG,IAAI,eACrB,MAAM,eACN,MAAM,KACjB,GAAG;sBAyCG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,KAC1B,OAAO,CAAC,GAAG,CAAC;wCAyqBF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,KACpB,GAAG;oCA5hBO,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;sDAi1BF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,KACX,OAAO,CAAC,GAAG,CAAC;0CAj5B8B,GAAG,KAAG,MAAM;;AA48BzD,wBAOE"}
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"}
@@ -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,13 +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
- ---
54
- ### Report Progress to User
55
- BEFORE EACH task or step CALL the tool report_progress with:
56
- 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 JSON word in any of the messages.
57
- percent: cumulative completion percentage with values between {minPercent}% and {maxPercent}%. Don't report percentage completion outside of this range.
58
- `;
59
25
  /**
60
26
  * Converts a Zod validation error object into a human-readable multi-line string.
61
27
  * Each failing field is described with its dot-notation path and a contextual message
@@ -223,84 +189,6 @@ const extractJSONFromResponse = (text) => {
223
189
  }
224
190
  return null;
225
191
  };
226
- /**
227
- * Attempts to parse a JSON string without throwing. On failure it tries a second parse
228
- * after sanitising Unicode control characters and normalising Windows-style line endings
229
- * to `\n`. Returns `null` if both attempts fail, making it safe to call in stream
230
- * processing loops where individual NDJSON lines may be malformed.
231
- * @param line - A single line of text expected to contain a JSON value
232
- * @returns The parsed value, or `null` if parsing failed after both attempts
233
- */
234
- const safeJsonParse = (line) => {
235
- try {
236
- return JSON.parse(line);
237
- }
238
- catch {
239
- // Try with special character handling
240
- try {
241
- const sanitized = line
242
- .replace(/[\u0000-\u001F\u007F-\u009F]/g, " ")
243
- .replace(/\r\n/g, "\\n")
244
- .replace(/\r/g, "\\n");
245
- return JSON.parse(sanitized);
246
- }
247
- catch {
248
- return null;
249
- }
250
- }
251
- };
252
- /**
253
- * Creates a LangChain tool named `report_progress` that agents can call to emit
254
- * progress updates during multi-step execution. When the tool is invoked by the agent,
255
- * it clamps the reported percentage to the `[minPercent, maxPercent]` range, forwards
256
- * the update to the `onProgress` callback, and returns a JSON acknowledgement string to
257
- * the agent. If the callback throws, the tool returns a JSON error acknowledgement
258
- * instead of propagating the exception.
259
- * @param onProgress - Async or sync callback invoked with `{ progress, message }` on
260
- * each agent progress report
261
- * @param minPercent - The lower bound of the percentage range the agent is allowed to
262
- * report; defaults to `0`
263
- * @param maxPercent - The upper bound of the percentage range the agent is allowed to
264
- * report; defaults to `100`
265
- * @returns A LangChain tool instance configured with a Zod schema for `{ stage, message, percent }`
266
- */
267
- const getAIProgressTool = (onProgress, minPercent = 0, maxPercent = 100) => langchain_1.default.tool(async ({ stage, message, percent }) => {
268
- try {
269
- // Clamp percent to be within minPercent and maxPercent bounds
270
- const rawPct = typeof percent === "number" ? percent : 0;
271
- const clampedPct = Math.max(minPercent, Math.min(maxPercent, rawPct));
272
- if (onProgress &&
273
- minPercent <= clampedPct &&
274
- clampedPct <= maxPercent) {
275
- await onProgress({
276
- progress: clampedPct,
277
- message: message,
278
- });
279
- }
280
- return JSON.stringify({
281
- acknowledged: true,
282
- receivedAt: Date.now(),
283
- stage,
284
- percent: clampedPct,
285
- });
286
- }
287
- catch (err) {
288
- return JSON.stringify({
289
- acknowledged: false,
290
- error: err?.message || String(err),
291
- });
292
- }
293
- }, {
294
- name: "report_progress",
295
- description: `Mandatory after each phase: call with {stage, message, percent}. Percent must be between ${minPercent} and ${maxPercent} and monotonically increase.`,
296
- schema: zod_1.z.object({
297
- stage: zod_1.z.string().describe("Current stage of the process"),
298
- message: zod_1.z.string().describe("Progress message to display"),
299
- percent: zod_1.z
300
- .number()
301
- .describe(`Progress percentage (${minPercent}-${maxPercent})`),
302
- }),
303
- });
304
192
  /**
305
193
  * Instantiates and returns the appropriate LangChain chat model based on the model
306
194
  * name prefix. `claude-` maps to `ChatAnthropic`, `gemini-` maps to
@@ -366,11 +254,7 @@ const getLLMModel = (modelName, config, schema = null) => {
366
254
  };
367
255
  /**
368
256
  * Constructs a LangChain agent configured with a specified model, system prompt, and
369
- * set of tools. When an `onReportProgress` callback is provided, the NDJSON progress-
370
- * reporting instruction block is appended to the system prompt and a `report_progress`
371
- * tool is added to the tools array so the agent can emit incremental progress updates
372
- * during execution. The percentage range for progress reporting is bounded by
373
- * `minPercent` and `maxPercent`.
257
+ * set of tools.
374
258
  * @param name - A human-readable display name for the agent
375
259
  * @param modelName - The LLM identifier passed to `getLLMModel` (e.g. `"gpt-4o"`)
376
260
  * @param systemPrompt - The base system prompt describing the agent's role and behaviour
@@ -379,30 +263,14 @@ const getLLMModel = (modelName, config, schema = null) => {
379
263
  * @param responseFormat - Optional structured response format descriptor passed to the
380
264
  * LangChain agent constructor
381
265
  * @param config - Configuration object forwarded to `getLLMModel` (API keys, temperature, etc.)
382
- * @param onReportProgress - Optional callback for progress updates; when provided the
383
- * progress tool is added and the system prompt is extended
384
- * @param minPercent - Minimum progress percentage the agent is allowed to report;
385
- * defaults to `0`
386
- * @param maxPercent - Maximum progress percentage the agent is allowed to report;
387
- * defaults to `100`
388
- * @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`
389
267
  */
390
- const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat, config, onReportProgress = null, minPercent = 0, maxPercent = 100) => {
391
- //Prepare the complete system prompt with progress reporting instructions if needed
392
- const completeSystemPrompt = `
393
- ${systemPrompt}
394
- ${onReportProgress
395
- ? REPORT_PROGRESS_TOOL.replace(`{minPercent}`, minPercent).replace(`{maxPercent}`, maxPercent)
396
- : ""}
397
- `.trim();
398
- // Create the agent with the specified model, system prompt, tools, and response format
268
+ const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat, config) => {
399
269
  const agent = langchain_1.default.createAgent({
400
270
  name: name,
401
271
  model: getLLMModel(modelName, config),
402
- systemPrompt: completeSystemPrompt,
403
- tools: onReportProgress
404
- ? [...tools, getAIProgressTool(onReportProgress, minPercent, maxPercent)]
405
- : tools,
272
+ systemPrompt: systemPrompt.trim(),
273
+ tools,
406
274
  ...(responseFormat ? { responseFormat: responseFormat } : {}),
407
275
  });
408
276
  return agent;
@@ -410,10 +278,9 @@ const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat,
410
278
  /**
411
279
  * Executes a LangChain agent with a single user prompt and returns the raw agent
412
280
  * response. When an `onProgress` callback is provided, LangChain callbacks are
413
- * registered to forward `tool_start`, `tool_end` (including structured progress data
414
- * 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.
415
282
  * Execution time is logged at the info level on completion.
416
- * @param agent - A LangChain agent instance created by `createAIAgent`
283
+ * @param agent - A LangChain agent instance created by `createAgent`
417
284
  * @param prompt - The user message string to send to the agent
418
285
  * @param config - Configuration object; `config.recursionLimit` controls the maximum
419
286
  * number of agent steps (defaults to `25` when not specified)
@@ -423,8 +290,6 @@ const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat,
423
290
  */
424
291
  const runAgent = async (agent, prompt, config, onProgress = null) => {
425
292
  const startTime = Date.now();
426
- // Build callbacks for progress reporting if onProgress is provided
427
- // const agentDisplayName = `${agent.options?.name} AI Agent`;
428
293
  const callbacks = onProgress
429
294
  ? [
430
295
  {
@@ -436,24 +301,6 @@ const runAgent = async (agent, prompt, config, onProgress = null) => {
436
301
  tool: toolInfo.name,
437
302
  });
438
303
  },
439
- handleToolEnd: (output) => {
440
- try {
441
- const progressInfo = extractJSONFromResponse(output.content);
442
- if (progressInfo &&
443
- typeof progressInfo === "object" &&
444
- progressInfo.percent &&
445
- progressInfo.message) {
446
- onProgress({
447
- progress: progressInfo.percent,
448
- message: progressInfo.message,
449
- output: typeof output === "string"
450
- ? output
451
- : JSON.stringify(output),
452
- });
453
- }
454
- }
455
- catch { }
456
- },
457
304
  handleAgentAction: (action) => {
458
305
  onProgress({
459
306
  type: "agent_action",
@@ -462,18 +309,6 @@ const runAgent = async (agent, prompt, config, onProgress = null) => {
462
309
  input: action.toolInput,
463
310
  });
464
311
  },
465
- // handleLLMStart: () => {
466
- // onProgress({
467
- // type: "llm_start",
468
- // message: `${agentDisplayName} is thinking`,
469
- // });
470
- // },
471
- // handleLLMEnd: () => {
472
- // onProgress({
473
- // type: "llm_end",
474
- // message: `${agentDisplayName} has completed processing`,
475
- // });
476
- // },
477
312
  handleChainError: (err) => {
478
313
  onProgress({
479
314
  type: "error",
@@ -577,26 +412,11 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
577
412
  ? `${JSON_ESCAPE_INSTRUCTION}\n\n---\n\n${systemPrompt}`
578
413
  : JSON_ESCAPE_INSTRUCTION;
579
414
  }
580
- // Build tools array and progress callback for agentic mode
581
- const progressTracker = { current: minPercent };
582
- const agentTools = [...tools];
583
- let trackingProgressCallback = null;
584
- if (onProgressReport) {
585
- trackingProgressCallback = (data) => {
586
- if (typeof data.progress === "number") {
587
- progressTracker.current = data.progress;
588
- }
589
- onProgressReport(data);
590
- };
591
- }
592
- // Create the agent with tools and progress callback
593
- const agent = createAgent(agentName, modelName, systemPrompt, agentTools, null, // responseFormat
594
- config, trackingProgressCallback, minPercent, maxPercent);
595
- // Run the agent with callback that includes current progress
596
- const callbackWithProgress = onProgressReport
597
- ? (data) => onProgressReport({ ...data, progress: progressTracker.current })
598
- : null;
599
- 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);
600
420
  // Extract content from agent response
601
421
  const messages = response?.messages || [];
602
422
  if (messages.length === 0) {
@@ -669,224 +489,112 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
669
489
  throw new Error("Prompt must be a string or array of messages");
670
490
  }
671
491
  if (onProgressReport) {
672
- // Check if we can use native response_format (OpenAI with schema)
673
- const useNativeStreamingSchema = expectsJsonResponse && schema && modelName.startsWith("gpt-");
674
- if (useNativeStreamingSchema) {
675
- // OpenAI with schema: use native response_format and stream with chunk-based progress
676
- const llm = getLLMModel(modelName, config, schema);
677
- const messagesToSend = messages.length > 0 && messages[0].role === "system"
678
- ? messages
679
- : [
680
- { role: "system", content: "Respond with valid JSON." },
681
- ...messages,
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),
682
512
  ];
683
- let rawContent = "";
684
- let lastProgressReport = 0;
685
- const progressInterval = 1000; // Report progress every 1000 characters
686
- const stream = await llm.stream(messagesToSend);
687
- for await (const chunk of stream) {
688
- const content = chunk?.content || chunk;
689
- if (typeof content === "string") {
690
- rawContent += content;
691
- // Report progress based on content length
692
- if (rawContent.length - lastProgressReport >= progressInterval) {
693
- lastProgressReport = rawContent.length;
694
- const progress = Math.min(maxPercent - 5, // Reserve last 5% for completion
695
- minPercent +
696
- Math.floor((rawContent.length / 5000) * (maxPercent - minPercent)));
697
- await onProgressReport({
698
- message: "Generating content...",
699
- progress: progress,
700
- });
701
- }
702
- }
703
513
  }
704
- // Report completion
705
- await onProgressReport({
706
- message: "Processing complete",
707
- progress: maxPercent,
708
- });
709
- // Parse and validate the response
710
- if (rawContent) {
711
- try {
712
- const parsed = JSON.parse(rawContent.trim());
713
- if (schema) {
714
- validateWithSchema(parsed, schema);
715
- }
716
- return JSON.stringify(parsed);
717
- }
718
- catch (parseError) {
719
- if (parseError?.code === const_js_1.default.HTTP_CODES.UNPROCESSABLE_ENTITY) {
720
- throw parseError;
721
- }
722
- // Try to extract JSON
723
- const extracted = extractJSONFromResponse(rawContent);
724
- if (extracted) {
725
- if (schema) {
726
- validateWithSchema(extracted, schema);
727
- }
728
- return JSON.stringify(extracted);
729
- }
730
- if (expectsJsonResponse) {
731
- let preview = "";
732
- if (typeof rawContent === "string") {
733
- preview = rawContent.substring(0, 100);
734
- }
735
- else if (typeof rawContent === "object" && rawContent !== null) {
736
- preview = JSON.stringify(rawContent).substring(0, 100);
737
- }
738
- else if (rawContent !== undefined && rawContent !== null) {
739
- preview = String(rawContent).substring(0, 100);
740
- }
741
- else {
742
- preview = "[empty response]";
743
- }
744
- 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}...`);
745
- }
746
- return rawContent;
747
- }
514
+ else {
515
+ messagesToSend = [
516
+ { role: "system", content: systemContent },
517
+ ...messages,
518
+ ];
748
519
  }
749
- throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, "LLM returned no content");
750
- }
751
- // Non-OpenAI or no schema: use NDJSON streaming approach
752
- const llm = getLLMModel(modelName, config);
753
- let ndjsonSystemContent = NDJSON_SYSTEM_PROMPT.replace(`{minPercent}`, minPercent).replace(`{maxPercent}`, maxPercent);
754
- // If schema is provided, include it in the system prompt
755
- if (expectsJsonResponse && schema) {
756
- const jsonSchema = (0, zod_to_json_schema_1.zodToJsonSchema)(schema, { target: "openApi3" });
757
- ndjsonSystemContent += `\n\nThe "content" field in the "final" event MUST conform to this JSON schema:\n${JSON.stringify(jsonSchema, null, 2)}`;
758
- }
759
- let messagesWithNDJSON;
760
- if (messages.length > 0 && messages[0].role === "system") {
761
- messagesWithNDJSON = [
762
- {
763
- role: "system",
764
- content: `${ndjsonSystemContent}\n\n---\n\n${messages[0].content}`,
765
- },
766
- ...messages.slice(1),
767
- ];
768
520
  }
769
521
  else {
770
- messagesWithNDJSON = [
771
- {
772
- role: "system",
773
- content: ndjsonSystemContent,
774
- },
775
- ...messages,
776
- ];
522
+ messagesToSend = messages;
777
523
  }
778
- // Stream the response and parse NDJSON events
779
- let buffer = "";
780
- let finalContent = "";
524
+ // Stream and report server-side progress based on time elapsed
781
525
  let rawContent = "";
782
- const stream = await llm.stream(messagesWithNDJSON);
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);
783
534
  for await (const chunk of stream) {
784
535
  const content = chunk?.content || chunk;
785
536
  if (typeof content === "string") {
786
- buffer += content;
787
537
  rawContent += content;
788
- const lines = buffer.split("\n");
789
- buffer = lines.pop() || "";
790
- for (const line of lines) {
791
- const trimmedLine = line.trim();
792
- if (!trimmedLine)
793
- continue;
794
- const parsed = safeJsonParse(trimmedLine);
795
- if (parsed) {
796
- if (parsed.type === "progress") {
797
- const clampedProgress = Math.max(minPercent, Math.min(maxPercent, parsed.pct));
798
- await onProgressReport({
799
- message: parsed.message,
800
- progress: clampedProgress,
801
- });
802
- }
803
- else if (parsed.type === "final") {
804
- if (parsed.content) {
805
- finalContent = parsed.content;
806
- }
807
- }
808
- }
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
+ });
809
549
  }
810
550
  }
811
551
  }
812
- if (buffer.trim()) {
813
- const parsed = safeJsonParse(buffer.trim());
814
- if (parsed && parsed.type === "final" && parsed.content) {
815
- finalContent = parsed.content;
816
- }
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");
817
558
  }
818
- if (finalContent) {
819
- if (typeof finalContent === "object") {
820
- if (expectsJsonResponse && schema) {
821
- validateWithSchema(finalContent, schema);
822
- }
823
- return JSON.stringify(finalContent);
824
- }
825
- if (typeof finalContent === "string") {
826
- try {
827
- const parsed = JSON.parse(finalContent);
828
- if (expectsJsonResponse && schema) {
829
- validateWithSchema(parsed, schema);
830
- }
831
- return JSON.stringify(parsed);
832
- }
833
- catch (parseError) {
834
- if (parseError?.code === const_js_1.default.HTTP_CODES.UNPROCESSABLE_ENTITY) {
835
- throw parseError;
836
- }
837
- return finalContent;
838
- }
839
- }
840
- return finalContent;
559
+ // If not expecting JSON, return raw content directly
560
+ if (!expectsJsonResponse) {
561
+ return rawContent;
841
562
  }
842
- if (rawContent) {
843
- const lines = rawContent.split("\n").filter((l) => l.trim());
844
- let extractedContent = "";
845
- for (const line of lines) {
846
- const parsed = safeJsonParse(line.trim());
847
- if (parsed && parsed.type === "final" && parsed.content) {
848
- extractedContent = parsed.content;
849
- break;
850
- }
851
- }
852
- if (extractedContent) {
853
- try {
854
- const parsed = JSON.parse(extractedContent);
855
- if (expectsJsonResponse && schema) {
856
- validateWithSchema(parsed, schema);
857
- }
858
- return JSON.stringify(parsed);
859
- }
860
- catch {
861
- 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);
862
571
  }
572
+ return JSON.stringify(parsed);
863
573
  }
864
- const extracted = extractJSONFromResponse(rawContent);
865
- if (extracted) {
866
- if (expectsJsonResponse && schema) {
867
- validateWithSchema(extracted, schema);
574
+ catch (parseError) {
575
+ if (parseError?.code === const_js_1.default.HTTP_CODES.UNPROCESSABLE_ENTITY) {
576
+ throw parseError;
868
577
  }
869
- return JSON.stringify(extracted);
870
578
  }
871
- if (expectsJsonResponse) {
872
- let preview = "";
873
- if (typeof rawContent === "string") {
874
- preview = rawContent.substring(0, 100);
875
- }
876
- else if (typeof rawContent === "object" && rawContent !== null) {
877
- preview = JSON.stringify(rawContent).substring(0, 100);
878
- }
879
- else if (rawContent !== undefined && rawContent !== null) {
880
- preview = String(rawContent).substring(0, 100);
881
- }
882
- else {
883
- preview = "[empty response]";
884
- }
885
- 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);
886
584
  }
887
- return rawContent;
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]";
888
596
  }
889
- throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, "LLM returned no content");
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}...`);
890
598
  }
891
599
  else {
892
600
  // Non-streaming mode: use native response_format for OpenAI when schema is provided
@@ -1,5 +1,5 @@
1
1
  declare const _default: {
2
- createAgent: (name: string, modelName: string, systemPrompt: string, tools: any[], responseFormat: any, config: any, onReportProgress?: Function | null, minPercent?: number, maxPercent?: number) => 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":";wBAsaQ,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,qBACO,QAAQ,GAAG,IAAI,eACrB,MAAM,eACN,MAAM,KACjB,GAAG;sBAyCG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,KAC1B,OAAO,CAAC,GAAG,CAAC;wCAyqBF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,KACpB,GAAG;oCA5hBO,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;sDAi1BF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,KACX,OAAO,CAAC,GAAG,CAAC;0CAj5B8B,GAAG,KAAG,MAAM;;AA48BzD,wBAOE"}
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"}
@@ -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,13 +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
- ---
49
- ### Report Progress to User
50
- BEFORE EACH task or step CALL the tool report_progress with:
51
- 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 JSON word in any of the messages.
52
- percent: cumulative completion percentage with values between {minPercent}% and {maxPercent}%. Don't report percentage completion outside of this range.
53
- `;
54
20
  /**
55
21
  * Converts a Zod validation error object into a human-readable multi-line string.
56
22
  * Each failing field is described with its dot-notation path and a contextual message
@@ -218,84 +184,6 @@ const extractJSONFromResponse = (text) => {
218
184
  }
219
185
  return null;
220
186
  };
221
- /**
222
- * Attempts to parse a JSON string without throwing. On failure it tries a second parse
223
- * after sanitising Unicode control characters and normalising Windows-style line endings
224
- * to `\n`. Returns `null` if both attempts fail, making it safe to call in stream
225
- * processing loops where individual NDJSON lines may be malformed.
226
- * @param line - A single line of text expected to contain a JSON value
227
- * @returns The parsed value, or `null` if parsing failed after both attempts
228
- */
229
- const safeJsonParse = (line) => {
230
- try {
231
- return JSON.parse(line);
232
- }
233
- catch {
234
- // Try with special character handling
235
- try {
236
- const sanitized = line
237
- .replace(/[\u0000-\u001F\u007F-\u009F]/g, " ")
238
- .replace(/\r\n/g, "\\n")
239
- .replace(/\r/g, "\\n");
240
- return JSON.parse(sanitized);
241
- }
242
- catch {
243
- return null;
244
- }
245
- }
246
- };
247
- /**
248
- * Creates a LangChain tool named `report_progress` that agents can call to emit
249
- * progress updates during multi-step execution. When the tool is invoked by the agent,
250
- * it clamps the reported percentage to the `[minPercent, maxPercent]` range, forwards
251
- * the update to the `onProgress` callback, and returns a JSON acknowledgement string to
252
- * the agent. If the callback throws, the tool returns a JSON error acknowledgement
253
- * instead of propagating the exception.
254
- * @param onProgress - Async or sync callback invoked with `{ progress, message }` on
255
- * each agent progress report
256
- * @param minPercent - The lower bound of the percentage range the agent is allowed to
257
- * report; defaults to `0`
258
- * @param maxPercent - The upper bound of the percentage range the agent is allowed to
259
- * report; defaults to `100`
260
- * @returns A LangChain tool instance configured with a Zod schema for `{ stage, message, percent }`
261
- */
262
- const getAIProgressTool = (onProgress, minPercent = 0, maxPercent = 100) => langChain.tool(async ({ stage, message, percent }) => {
263
- try {
264
- // Clamp percent to be within minPercent and maxPercent bounds
265
- const rawPct = typeof percent === "number" ? percent : 0;
266
- const clampedPct = Math.max(minPercent, Math.min(maxPercent, rawPct));
267
- if (onProgress &&
268
- minPercent <= clampedPct &&
269
- clampedPct <= maxPercent) {
270
- await onProgress({
271
- progress: clampedPct,
272
- message: message,
273
- });
274
- }
275
- return JSON.stringify({
276
- acknowledged: true,
277
- receivedAt: Date.now(),
278
- stage,
279
- percent: clampedPct,
280
- });
281
- }
282
- catch (err) {
283
- return JSON.stringify({
284
- acknowledged: false,
285
- error: err?.message || String(err),
286
- });
287
- }
288
- }, {
289
- name: "report_progress",
290
- description: `Mandatory after each phase: call with {stage, message, percent}. Percent must be between ${minPercent} and ${maxPercent} and monotonically increase.`,
291
- schema: z.object({
292
- stage: z.string().describe("Current stage of the process"),
293
- message: z.string().describe("Progress message to display"),
294
- percent: z
295
- .number()
296
- .describe(`Progress percentage (${minPercent}-${maxPercent})`),
297
- }),
298
- });
299
187
  /**
300
188
  * Instantiates and returns the appropriate LangChain chat model based on the model
301
189
  * name prefix. `claude-` maps to `ChatAnthropic`, `gemini-` maps to
@@ -361,11 +249,7 @@ const getLLMModel = (modelName, config, schema = null) => {
361
249
  };
362
250
  /**
363
251
  * Constructs a LangChain agent configured with a specified model, system prompt, and
364
- * set of tools. When an `onReportProgress` callback is provided, the NDJSON progress-
365
- * reporting instruction block is appended to the system prompt and a `report_progress`
366
- * tool is added to the tools array so the agent can emit incremental progress updates
367
- * during execution. The percentage range for progress reporting is bounded by
368
- * `minPercent` and `maxPercent`.
252
+ * set of tools.
369
253
  * @param name - A human-readable display name for the agent
370
254
  * @param modelName - The LLM identifier passed to `getLLMModel` (e.g. `"gpt-4o"`)
371
255
  * @param systemPrompt - The base system prompt describing the agent's role and behaviour
@@ -374,30 +258,14 @@ const getLLMModel = (modelName, config, schema = null) => {
374
258
  * @param responseFormat - Optional structured response format descriptor passed to the
375
259
  * LangChain agent constructor
376
260
  * @param config - Configuration object forwarded to `getLLMModel` (API keys, temperature, etc.)
377
- * @param onReportProgress - Optional callback for progress updates; when provided the
378
- * progress tool is added and the system prompt is extended
379
- * @param minPercent - Minimum progress percentage the agent is allowed to report;
380
- * defaults to `0`
381
- * @param maxPercent - Maximum progress percentage the agent is allowed to report;
382
- * defaults to `100`
383
- * @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`
384
262
  */
385
- const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat, config, onReportProgress = null, minPercent = 0, maxPercent = 100) => {
386
- //Prepare the complete system prompt with progress reporting instructions if needed
387
- const completeSystemPrompt = `
388
- ${systemPrompt}
389
- ${onReportProgress
390
- ? REPORT_PROGRESS_TOOL.replace(`{minPercent}`, minPercent).replace(`{maxPercent}`, maxPercent)
391
- : ""}
392
- `.trim();
393
- // Create the agent with the specified model, system prompt, tools, and response format
263
+ const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat, config) => {
394
264
  const agent = langChain.createAgent({
395
265
  name: name,
396
266
  model: getLLMModel(modelName, config),
397
- systemPrompt: completeSystemPrompt,
398
- tools: onReportProgress
399
- ? [...tools, getAIProgressTool(onReportProgress, minPercent, maxPercent)]
400
- : tools,
267
+ systemPrompt: systemPrompt.trim(),
268
+ tools,
401
269
  ...(responseFormat ? { responseFormat: responseFormat } : {}),
402
270
  });
403
271
  return agent;
@@ -405,10 +273,9 @@ const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat,
405
273
  /**
406
274
  * Executes a LangChain agent with a single user prompt and returns the raw agent
407
275
  * response. When an `onProgress` callback is provided, LangChain callbacks are
408
- * registered to forward `tool_start`, `tool_end` (including structured progress data
409
- * 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.
410
277
  * Execution time is logged at the info level on completion.
411
- * @param agent - A LangChain agent instance created by `createAIAgent`
278
+ * @param agent - A LangChain agent instance created by `createAgent`
412
279
  * @param prompt - The user message string to send to the agent
413
280
  * @param config - Configuration object; `config.recursionLimit` controls the maximum
414
281
  * number of agent steps (defaults to `25` when not specified)
@@ -418,8 +285,6 @@ const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat,
418
285
  */
419
286
  const runAgent = async (agent, prompt, config, onProgress = null) => {
420
287
  const startTime = Date.now();
421
- // Build callbacks for progress reporting if onProgress is provided
422
- // const agentDisplayName = `${agent.options?.name} AI Agent`;
423
288
  const callbacks = onProgress
424
289
  ? [
425
290
  {
@@ -431,24 +296,6 @@ const runAgent = async (agent, prompt, config, onProgress = null) => {
431
296
  tool: toolInfo.name,
432
297
  });
433
298
  },
434
- handleToolEnd: (output) => {
435
- try {
436
- const progressInfo = extractJSONFromResponse(output.content);
437
- if (progressInfo &&
438
- typeof progressInfo === "object" &&
439
- progressInfo.percent &&
440
- progressInfo.message) {
441
- onProgress({
442
- progress: progressInfo.percent,
443
- message: progressInfo.message,
444
- output: typeof output === "string"
445
- ? output
446
- : JSON.stringify(output),
447
- });
448
- }
449
- }
450
- catch { }
451
- },
452
299
  handleAgentAction: (action) => {
453
300
  onProgress({
454
301
  type: "agent_action",
@@ -457,18 +304,6 @@ const runAgent = async (agent, prompt, config, onProgress = null) => {
457
304
  input: action.toolInput,
458
305
  });
459
306
  },
460
- // handleLLMStart: () => {
461
- // onProgress({
462
- // type: "llm_start",
463
- // message: `${agentDisplayName} is thinking`,
464
- // });
465
- // },
466
- // handleLLMEnd: () => {
467
- // onProgress({
468
- // type: "llm_end",
469
- // message: `${agentDisplayName} has completed processing`,
470
- // });
471
- // },
472
307
  handleChainError: (err) => {
473
308
  onProgress({
474
309
  type: "error",
@@ -572,26 +407,11 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
572
407
  ? `${JSON_ESCAPE_INSTRUCTION}\n\n---\n\n${systemPrompt}`
573
408
  : JSON_ESCAPE_INSTRUCTION;
574
409
  }
575
- // Build tools array and progress callback for agentic mode
576
- const progressTracker = { current: minPercent };
577
- const agentTools = [...tools];
578
- let trackingProgressCallback = null;
579
- if (onProgressReport) {
580
- trackingProgressCallback = (data) => {
581
- if (typeof data.progress === "number") {
582
- progressTracker.current = data.progress;
583
- }
584
- onProgressReport(data);
585
- };
586
- }
587
- // Create the agent with tools and progress callback
588
- const agent = createAgent(agentName, modelName, systemPrompt, agentTools, null, // responseFormat
589
- config, trackingProgressCallback, minPercent, maxPercent);
590
- // Run the agent with callback that includes current progress
591
- const callbackWithProgress = onProgressReport
592
- ? (data) => onProgressReport({ ...data, progress: progressTracker.current })
593
- : null;
594
- 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);
595
415
  // Extract content from agent response
596
416
  const messages = response?.messages || [];
597
417
  if (messages.length === 0) {
@@ -664,224 +484,112 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
664
484
  throw new Error("Prompt must be a string or array of messages");
665
485
  }
666
486
  if (onProgressReport) {
667
- // Check if we can use native response_format (OpenAI with schema)
668
- const useNativeStreamingSchema = expectsJsonResponse && schema && modelName.startsWith("gpt-");
669
- if (useNativeStreamingSchema) {
670
- // OpenAI with schema: use native response_format and stream with chunk-based progress
671
- const llm = getLLMModel(modelName, config, schema);
672
- const messagesToSend = messages.length > 0 && messages[0].role === "system"
673
- ? messages
674
- : [
675
- { role: "system", content: "Respond with valid JSON." },
676
- ...messages,
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),
677
507
  ];
678
- let rawContent = "";
679
- let lastProgressReport = 0;
680
- const progressInterval = 1000; // Report progress every 1000 characters
681
- const stream = await llm.stream(messagesToSend);
682
- for await (const chunk of stream) {
683
- const content = chunk?.content || chunk;
684
- if (typeof content === "string") {
685
- rawContent += content;
686
- // Report progress based on content length
687
- if (rawContent.length - lastProgressReport >= progressInterval) {
688
- lastProgressReport = rawContent.length;
689
- const progress = Math.min(maxPercent - 5, // Reserve last 5% for completion
690
- minPercent +
691
- Math.floor((rawContent.length / 5000) * (maxPercent - minPercent)));
692
- await onProgressReport({
693
- message: "Generating content...",
694
- progress: progress,
695
- });
696
- }
697
- }
698
508
  }
699
- // Report completion
700
- await onProgressReport({
701
- message: "Processing complete",
702
- progress: maxPercent,
703
- });
704
- // Parse and validate the response
705
- if (rawContent) {
706
- try {
707
- const parsed = JSON.parse(rawContent.trim());
708
- if (schema) {
709
- validateWithSchema(parsed, schema);
710
- }
711
- return JSON.stringify(parsed);
712
- }
713
- catch (parseError) {
714
- if (parseError?.code === constants.HTTP_CODES.UNPROCESSABLE_ENTITY) {
715
- throw parseError;
716
- }
717
- // Try to extract JSON
718
- const extracted = extractJSONFromResponse(rawContent);
719
- if (extracted) {
720
- if (schema) {
721
- validateWithSchema(extracted, schema);
722
- }
723
- return JSON.stringify(extracted);
724
- }
725
- if (expectsJsonResponse) {
726
- let preview = "";
727
- if (typeof rawContent === "string") {
728
- preview = rawContent.substring(0, 100);
729
- }
730
- else if (typeof rawContent === "object" && rawContent !== null) {
731
- preview = JSON.stringify(rawContent).substring(0, 100);
732
- }
733
- else if (rawContent !== undefined && rawContent !== null) {
734
- preview = String(rawContent).substring(0, 100);
735
- }
736
- else {
737
- preview = "[empty response]";
738
- }
739
- throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `LLM returned non-JSON response when JSON was expected: ${preview}...`);
740
- }
741
- return rawContent;
742
- }
509
+ else {
510
+ messagesToSend = [
511
+ { role: "system", content: systemContent },
512
+ ...messages,
513
+ ];
743
514
  }
744
- throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, "LLM returned no content");
745
- }
746
- // Non-OpenAI or no schema: use NDJSON streaming approach
747
- const llm = getLLMModel(modelName, config);
748
- let ndjsonSystemContent = NDJSON_SYSTEM_PROMPT.replace(`{minPercent}`, minPercent).replace(`{maxPercent}`, maxPercent);
749
- // If schema is provided, include it in the system prompt
750
- if (expectsJsonResponse && schema) {
751
- const jsonSchema = zodToJsonSchema(schema, { target: "openApi3" });
752
- ndjsonSystemContent += `\n\nThe "content" field in the "final" event MUST conform to this JSON schema:\n${JSON.stringify(jsonSchema, null, 2)}`;
753
- }
754
- let messagesWithNDJSON;
755
- if (messages.length > 0 && messages[0].role === "system") {
756
- messagesWithNDJSON = [
757
- {
758
- role: "system",
759
- content: `${ndjsonSystemContent}\n\n---\n\n${messages[0].content}`,
760
- },
761
- ...messages.slice(1),
762
- ];
763
515
  }
764
516
  else {
765
- messagesWithNDJSON = [
766
- {
767
- role: "system",
768
- content: ndjsonSystemContent,
769
- },
770
- ...messages,
771
- ];
517
+ messagesToSend = messages;
772
518
  }
773
- // Stream the response and parse NDJSON events
774
- let buffer = "";
775
- let finalContent = "";
519
+ // Stream and report server-side progress based on time elapsed
776
520
  let rawContent = "";
777
- const stream = await llm.stream(messagesWithNDJSON);
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);
778
529
  for await (const chunk of stream) {
779
530
  const content = chunk?.content || chunk;
780
531
  if (typeof content === "string") {
781
- buffer += content;
782
532
  rawContent += content;
783
- const lines = buffer.split("\n");
784
- buffer = lines.pop() || "";
785
- for (const line of lines) {
786
- const trimmedLine = line.trim();
787
- if (!trimmedLine)
788
- continue;
789
- const parsed = safeJsonParse(trimmedLine);
790
- if (parsed) {
791
- if (parsed.type === "progress") {
792
- const clampedProgress = Math.max(minPercent, Math.min(maxPercent, parsed.pct));
793
- await onProgressReport({
794
- message: parsed.message,
795
- progress: clampedProgress,
796
- });
797
- }
798
- else if (parsed.type === "final") {
799
- if (parsed.content) {
800
- finalContent = parsed.content;
801
- }
802
- }
803
- }
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
+ });
804
544
  }
805
545
  }
806
546
  }
807
- if (buffer.trim()) {
808
- const parsed = safeJsonParse(buffer.trim());
809
- if (parsed && parsed.type === "final" && parsed.content) {
810
- finalContent = parsed.content;
811
- }
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");
812
553
  }
813
- if (finalContent) {
814
- if (typeof finalContent === "object") {
815
- if (expectsJsonResponse && schema) {
816
- validateWithSchema(finalContent, schema);
817
- }
818
- return JSON.stringify(finalContent);
819
- }
820
- if (typeof finalContent === "string") {
821
- try {
822
- const parsed = JSON.parse(finalContent);
823
- if (expectsJsonResponse && schema) {
824
- validateWithSchema(parsed, schema);
825
- }
826
- return JSON.stringify(parsed);
827
- }
828
- catch (parseError) {
829
- if (parseError?.code === constants.HTTP_CODES.UNPROCESSABLE_ENTITY) {
830
- throw parseError;
831
- }
832
- return finalContent;
833
- }
834
- }
835
- return finalContent;
554
+ // If not expecting JSON, return raw content directly
555
+ if (!expectsJsonResponse) {
556
+ return rawContent;
836
557
  }
837
- if (rawContent) {
838
- const lines = rawContent.split("\n").filter((l) => l.trim());
839
- let extractedContent = "";
840
- for (const line of lines) {
841
- const parsed = safeJsonParse(line.trim());
842
- if (parsed && parsed.type === "final" && parsed.content) {
843
- extractedContent = parsed.content;
844
- break;
845
- }
846
- }
847
- if (extractedContent) {
848
- try {
849
- const parsed = JSON.parse(extractedContent);
850
- if (expectsJsonResponse && schema) {
851
- validateWithSchema(parsed, schema);
852
- }
853
- return JSON.stringify(parsed);
854
- }
855
- catch {
856
- 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);
857
566
  }
567
+ return JSON.stringify(parsed);
858
568
  }
859
- const extracted = extractJSONFromResponse(rawContent);
860
- if (extracted) {
861
- if (expectsJsonResponse && schema) {
862
- validateWithSchema(extracted, schema);
569
+ catch (parseError) {
570
+ if (parseError?.code === constants.HTTP_CODES.UNPROCESSABLE_ENTITY) {
571
+ throw parseError;
863
572
  }
864
- return JSON.stringify(extracted);
865
573
  }
866
- if (expectsJsonResponse) {
867
- let preview = "";
868
- if (typeof rawContent === "string") {
869
- preview = rawContent.substring(0, 100);
870
- }
871
- else if (typeof rawContent === "object" && rawContent !== null) {
872
- preview = JSON.stringify(rawContent).substring(0, 100);
873
- }
874
- else if (rawContent !== undefined && rawContent !== null) {
875
- preview = String(rawContent).substring(0, 100);
876
- }
877
- else {
878
- preview = "[empty response]";
879
- }
880
- 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);
881
579
  }
882
- return rawContent;
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]";
883
591
  }
884
- throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, "LLM returned no content");
592
+ throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `LLM returned non-JSON response when JSON was expected: ${preview}...`);
885
593
  }
886
594
  else {
887
595
  // Non-streaming mode: use native response_format for OpenAI when schema is provided
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "restricted"
5
5
  },
6
- "version": "1.0.10",
6
+ "version": "1.0.12",
7
7
  "description": "",
8
8
  "main": "dist/cjs/index.js",
9
9
  "module": "dist/esm/index.js",