@stackfactor/agent-utils 1.0.21 → 1.0.23
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/README.md +0 -26
- package/dist/cjs/langChain.d.ts +1 -1
- package/dist/cjs/langChain.d.ts.map +1 -1
- package/dist/cjs/langChain.js +231 -68
- package/dist/esm/langChain.d.ts +1 -1
- package/dist/esm/langChain.d.ts.map +1 -1
- package/dist/esm/langChain.js +231 -68
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,32 +62,6 @@ const result = await langChain.runPromptWithModel(
|
|
|
62
62
|
|
|
63
63
|
---
|
|
64
64
|
|
|
65
|
-
### `langChain.runChatPromptWithModel(modelName, config, prompt, onProgressReport?)`
|
|
66
|
-
|
|
67
|
-
Sends a conversational chat prompt to an LLM and returns a plain HTML response. Built for the StackFactor Mentor chat feature — system messages are wrapped with topic-constraint instructions that decline off-topic questions.
|
|
68
|
-
|
|
69
|
-
| Parameter | Type | Default | Description |
|
|
70
|
-
| ------------------ | -------------------- | ------- | ----------------------------------------------------- |
|
|
71
|
-
| `modelName` | `string` | — | Model identifier |
|
|
72
|
-
| `config` | `object` | — | API keys and optional `temperature` |
|
|
73
|
-
| `prompt` | `string \| object[]` | — | Plain string or array of `{ role, content }` messages |
|
|
74
|
-
| `onProgressReport` | `function \| null` | `null` | Streaming progress callback |
|
|
75
|
-
|
|
76
|
-
**Returns:** Raw HTML string.
|
|
77
|
-
|
|
78
|
-
```typescript
|
|
79
|
-
const html = await langChain.runChatPromptWithModel(
|
|
80
|
-
"claude-3-5-sonnet",
|
|
81
|
-
{ anthropicAPIKey: "sk-ant-..." },
|
|
82
|
-
[
|
|
83
|
-
{ role: "system", content: "Topic: JavaScript closures" },
|
|
84
|
-
{ role: "user", content: "Explain closures with an example" },
|
|
85
|
-
],
|
|
86
|
-
);
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
---
|
|
90
|
-
|
|
91
65
|
### `langChain.createAIAgent(name, modelName, systemPrompt, tools?, responseFormat?, config, onReportProgress?, minPercent?, maxPercent?)`
|
|
92
66
|
|
|
93
67
|
Constructs a LangChain agent with a model, system prompt, and tools. When `onReportProgress` is provided, a `report_progress` tool is automatically added.
|
package/dist/cjs/langChain.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
declare const _default: {
|
|
2
|
+
checkIfAIProviderConfigured: (config: any) => void;
|
|
2
3
|
createAgent: (name: string, modelName: string, systemPrompt: string, tools: any[], responseFormat: any, config: any) => any;
|
|
3
4
|
runAgent: (agent: any, prompt: string, config: any, onProgress?: Function | null) => Promise<any>;
|
|
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>;
|
|
6
6
|
runPromptWithModelForImageGeneration: (modelName: string, config: any, prompt: string, options?: any) => Promise<any>;
|
|
7
7
|
throwErrorIfNotSuccessful: (response: any) => string;
|
|
@@ -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":";0CAgD6C,GAAG,KAAG,IAAI;wBAke/C,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;oCAqWF,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;sDA6rBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,KACX,OAAO,CAAC,GAAG,CAAC;0CA18B8B,GAAG,KAAG,MAAM;;AAqgCzD,wBAOE"}
|
package/dist/cjs/langChain.js
CHANGED
|
@@ -36,6 +36,14 @@ const assertQuotaAvailable = () => {
|
|
|
36
36
|
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.PAYMENT_REQUIRED, const_js_1.default.ERROR.QUOTA_EXHAUSTED);
|
|
37
37
|
}
|
|
38
38
|
};
|
|
39
|
+
const checkIfAIProviderConfigured = (config) => {
|
|
40
|
+
if (!config ||
|
|
41
|
+
!config.openAIAPIKey ||
|
|
42
|
+
!config.googleAPIKey ||
|
|
43
|
+
!config.anthropicAPIKey) {
|
|
44
|
+
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, "The integration is not properly configured with all AI providers.");
|
|
45
|
+
}
|
|
46
|
+
};
|
|
39
47
|
/**
|
|
40
48
|
* Subtracts the given USD cost from `global.quota.remaining` and adds it to
|
|
41
49
|
* `global.quota.usedThisSession`. Silently no-ops when the quota globals are not
|
|
@@ -522,14 +530,44 @@ const runAgent = async (agent, prompt, config, onProgress = null) => {
|
|
|
522
530
|
},
|
|
523
531
|
]
|
|
524
532
|
: undefined;
|
|
525
|
-
assertQuotaAvailable();
|
|
526
|
-
const response = await agent.invoke({
|
|
527
|
-
messages: [{ role: "user", content: prompt }],
|
|
528
|
-
}, {
|
|
529
|
-
recursionLimit: config.recursionLimit || 25,
|
|
530
|
-
...(callbacks ? { callbacks } : {}),
|
|
531
|
-
});
|
|
532
533
|
const modelName = agent.options?.model?.modelName || agent.options?.model?.model || "";
|
|
534
|
+
// Wait + retry on 429 around the full agent run. The agent loop may issue
|
|
535
|
+
// many internal LLM calls, but a rate limit surfaces as a thrown error from
|
|
536
|
+
// the wrapping invoke. On retry we restart the agent run from scratch — any
|
|
537
|
+
// partial progress (tool calls, intermediate messages) is discarded, since
|
|
538
|
+
// the agent state isn't externally checkpointed. Cost is only recorded on a
|
|
539
|
+
// successful completion. Matches the behavior of the non-agentic paths.
|
|
540
|
+
let response;
|
|
541
|
+
let rateLimitAttempt = 0;
|
|
542
|
+
while (true) {
|
|
543
|
+
try {
|
|
544
|
+
assertQuotaAvailable();
|
|
545
|
+
response = await agent.invoke({
|
|
546
|
+
messages: [{ role: "user", content: prompt }],
|
|
547
|
+
}, {
|
|
548
|
+
recursionLimit: config.recursionLimit || 25,
|
|
549
|
+
...(callbacks ? { callbacks } : {}),
|
|
550
|
+
});
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
catch (err) {
|
|
554
|
+
if (isRateLimitError(err) && rateLimitAttempt < MAX_RATE_LIMIT_RETRIES) {
|
|
555
|
+
rateLimitAttempt++;
|
|
556
|
+
const { waitMs, source } = getRateLimitWaitMs(err);
|
|
557
|
+
const waitSeconds = Math.round(waitMs / 1000);
|
|
558
|
+
logger_js_1.default.log(null, logger_js_1.default.levels.warn, `Rate limited by "${modelName}" (agent), waiting ${waitSeconds}s before retry (${rateLimitAttempt}/${MAX_RATE_LIMIT_RETRIES}, source=${source}): ${err?.message ?? err}`);
|
|
559
|
+
if (onProgress) {
|
|
560
|
+
onProgress({
|
|
561
|
+
type: "rate_limit",
|
|
562
|
+
message: `AI service is busy. Retrying in ${waitSeconds}s...`,
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
await sleep(waitMs);
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
throw err;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
533
571
|
recordCost(calculateTextCost(modelName, sumAgentResponseUsage(response), config));
|
|
534
572
|
const endTime = Date.now();
|
|
535
573
|
const duration = endTime - startTime;
|
|
@@ -552,6 +590,122 @@ const throwErrorIfNotSuccessful = (response) => {
|
|
|
552
590
|
}
|
|
553
591
|
};
|
|
554
592
|
const MAX_VALIDATION_RETRIES = 3;
|
|
593
|
+
const MAX_RATE_LIMIT_RETRIES = 2;
|
|
594
|
+
const RATE_LIMIT_MIN_WAIT_MS = 30_000;
|
|
595
|
+
const RATE_LIMIT_MAX_WAIT_MS = 60_000;
|
|
596
|
+
// Cap on how long we'll honor a provider Retry-After hint. If a provider asks
|
|
597
|
+
// for longer than this, treat it as a signal that the request shouldn't be
|
|
598
|
+
// retried in-flight and fail fast instead of holding the caller open.
|
|
599
|
+
const RATE_LIMIT_MAX_HONORED_WAIT_MS = 5 * 60 * 1000;
|
|
600
|
+
// Small jitter added on top of an honored Retry-After so concurrent callers
|
|
601
|
+
// that all received the same hint don't wake at exactly the same instant.
|
|
602
|
+
const RATE_LIMIT_HONORED_JITTER_MS = 2_000;
|
|
603
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
604
|
+
/**
|
|
605
|
+
* Extracts a Retry-After hint (in milliseconds) from a provider 429 error.
|
|
606
|
+
* Different SDKs put headers in different places (top-level `headers`, nested
|
|
607
|
+
* `response.headers`, `responseHeaders`, or under `cause`), and some include a
|
|
608
|
+
* millisecond-precision `retry-after-ms` variant. Also checks the response body
|
|
609
|
+
* for `retry_after` / `retry_after_ms` fields (some providers put it there
|
|
610
|
+
* instead of headers). Standard `Retry-After` may be seconds or an HTTP date.
|
|
611
|
+
* Returns `null` when no usable hint is present.
|
|
612
|
+
*/
|
|
613
|
+
const parseRetryAfterFromError = (err) => {
|
|
614
|
+
const headerSources = [
|
|
615
|
+
err?.headers,
|
|
616
|
+
err?.response?.headers,
|
|
617
|
+
err?.responseHeaders,
|
|
618
|
+
err?.cause?.headers,
|
|
619
|
+
err?.cause?.response?.headers,
|
|
620
|
+
];
|
|
621
|
+
for (const headers of headerSources) {
|
|
622
|
+
if (!headers || typeof headers !== "object")
|
|
623
|
+
continue;
|
|
624
|
+
const retryAfterMs = headers["retry-after-ms"] ?? headers["Retry-After-Ms"];
|
|
625
|
+
if (retryAfterMs != null) {
|
|
626
|
+
const ms = Number(retryAfterMs);
|
|
627
|
+
if (Number.isFinite(ms) && ms > 0)
|
|
628
|
+
return ms;
|
|
629
|
+
}
|
|
630
|
+
const retryAfter = headers["retry-after"] ?? headers["Retry-After"];
|
|
631
|
+
if (retryAfter != null) {
|
|
632
|
+
const asNumber = Number(retryAfter);
|
|
633
|
+
if (Number.isFinite(asNumber) && asNumber > 0) {
|
|
634
|
+
return asNumber * 1000;
|
|
635
|
+
}
|
|
636
|
+
const dateMs = Date.parse(String(retryAfter));
|
|
637
|
+
if (!Number.isNaN(dateMs)) {
|
|
638
|
+
const delta = dateMs - Date.now();
|
|
639
|
+
if (delta > 0)
|
|
640
|
+
return delta;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const bodySources = [err?.error, err?.response?.data, err?.body];
|
|
645
|
+
for (const body of bodySources) {
|
|
646
|
+
if (!body || typeof body !== "object")
|
|
647
|
+
continue;
|
|
648
|
+
const retryAfterMs = body.retry_after_ms ?? body.retryAfterMs;
|
|
649
|
+
if (typeof retryAfterMs === "number" && retryAfterMs > 0) {
|
|
650
|
+
return retryAfterMs;
|
|
651
|
+
}
|
|
652
|
+
const retryAfterS = body.retry_after ?? body.retryAfter;
|
|
653
|
+
if (typeof retryAfterS === "number" && retryAfterS > 0) {
|
|
654
|
+
return retryAfterS * 1000;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return null;
|
|
658
|
+
};
|
|
659
|
+
/**
|
|
660
|
+
* Picks the back-off duration before retrying a 429. When the provider supplied
|
|
661
|
+
* a Retry-After hint, that value is honored (capped at
|
|
662
|
+
* RATE_LIMIT_MAX_HONORED_WAIT_MS, with small jitter added to avoid thundering
|
|
663
|
+
* herd on the same wake instant). When no hint is present, falls back to a
|
|
664
|
+
* uniform random wait in [RATE_LIMIT_MIN_WAIT_MS, RATE_LIMIT_MAX_WAIT_MS] —
|
|
665
|
+
* which is the right default for "we don't know how long this will last."
|
|
666
|
+
* Returns both the chosen ms and the source so callers can log it.
|
|
667
|
+
*/
|
|
668
|
+
const getRateLimitWaitMs = (err) => {
|
|
669
|
+
const hint = parseRetryAfterFromError(err);
|
|
670
|
+
if (hint != null) {
|
|
671
|
+
const honored = Math.min(hint, RATE_LIMIT_MAX_HONORED_WAIT_MS);
|
|
672
|
+
const jitter = Math.floor(Math.random() * RATE_LIMIT_HONORED_JITTER_MS);
|
|
673
|
+
return {
|
|
674
|
+
waitMs: Math.max(honored + jitter, 1_000),
|
|
675
|
+
source: "retry-after",
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
const waitMs = RATE_LIMIT_MIN_WAIT_MS +
|
|
679
|
+
Math.floor(Math.random() * (RATE_LIMIT_MAX_WAIT_MS - RATE_LIMIT_MIN_WAIT_MS + 1));
|
|
680
|
+
return { waitMs, source: "random" };
|
|
681
|
+
};
|
|
682
|
+
/**
|
|
683
|
+
* Detects rate-limit (HTTP 429) errors across LangChain and provider SDK shapes.
|
|
684
|
+
* Different SDKs surface the status in different places (status, statusCode,
|
|
685
|
+
* response.status, code) and sometimes only in the message text, so this checks
|
|
686
|
+
* each known shape rather than assuming a single field.
|
|
687
|
+
*/
|
|
688
|
+
const isRateLimitError = (err) => {
|
|
689
|
+
if (!err)
|
|
690
|
+
return false;
|
|
691
|
+
const statusCandidates = [
|
|
692
|
+
err.status,
|
|
693
|
+
err.statusCode,
|
|
694
|
+
err.response?.status,
|
|
695
|
+
err.response?.statusCode,
|
|
696
|
+
err.cause?.status,
|
|
697
|
+
err.cause?.statusCode,
|
|
698
|
+
err.code,
|
|
699
|
+
];
|
|
700
|
+
if (statusCandidates.some((c) => c === 429 || c === "429"))
|
|
701
|
+
return true;
|
|
702
|
+
if (typeof err.code === "string" && /rate.?limit/i.test(err.code))
|
|
703
|
+
return true;
|
|
704
|
+
const message = typeof err.message === "string" ? err.message : "";
|
|
705
|
+
return (/\b429\b/.test(message) ||
|
|
706
|
+
/rate.?limit/i.test(message) ||
|
|
707
|
+
/too many requests/i.test(message));
|
|
708
|
+
};
|
|
555
709
|
/**
|
|
556
710
|
* Parses raw LLM content as JSON and validates against an optional Zod schema,
|
|
557
711
|
* returning the canonical JSON string. Tries a direct parse of the trimmed text
|
|
@@ -812,29 +966,59 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
812
966
|
const expectedDurationMs = 15_000; // Tune: expected typical response time
|
|
813
967
|
let activeMessages = messagesToSend;
|
|
814
968
|
let attempt = 0;
|
|
969
|
+
const calcCurrentProgress = () => {
|
|
970
|
+
const elapsed = Date.now() - overallStartTime;
|
|
971
|
+
return Math.round(minPercent +
|
|
972
|
+
(maxPercent - minPercent - 5) *
|
|
973
|
+
(1 - Math.exp(-elapsed / expectedDurationMs)));
|
|
974
|
+
};
|
|
815
975
|
while (true) {
|
|
816
976
|
let rawContent = "";
|
|
817
977
|
let chunkCount = 0;
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
978
|
+
let streamUsage = { input_tokens: 0, output_tokens: 0 };
|
|
979
|
+
// Inner loop: wait + retry on 429 around stream setup and consumption.
|
|
980
|
+
// Cost is only recorded on a successful stream — partial streams that error
|
|
981
|
+
// out with a rate limit are not billed. A 429 fired mid-stream simply
|
|
982
|
+
// discards the partial output and restarts cleanly.
|
|
983
|
+
let rateLimitAttempt = 0;
|
|
984
|
+
while (true) {
|
|
985
|
+
rawContent = "";
|
|
986
|
+
chunkCount = 0;
|
|
987
|
+
streamUsage = { input_tokens: 0, output_tokens: 0 };
|
|
988
|
+
try {
|
|
989
|
+
assertQuotaAvailable();
|
|
990
|
+
const stream = await llm.stream(activeMessages);
|
|
991
|
+
for await (const chunk of stream) {
|
|
992
|
+
accumulateChunkUsage(streamUsage, chunk);
|
|
993
|
+
const content = chunk?.content || chunk;
|
|
994
|
+
if (typeof content === "string") {
|
|
995
|
+
rawContent += content;
|
|
996
|
+
chunkCount++;
|
|
997
|
+
if (chunkCount % progressReportInterval === 0) {
|
|
998
|
+
await onProgressReport({
|
|
999
|
+
message: "Generating content...",
|
|
1000
|
+
progress: Math.min(calcCurrentProgress(), maxPercent - 5),
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
break; // stream completed without 429
|
|
1006
|
+
}
|
|
1007
|
+
catch (err) {
|
|
1008
|
+
if (isRateLimitError(err) &&
|
|
1009
|
+
rateLimitAttempt < MAX_RATE_LIMIT_RETRIES) {
|
|
1010
|
+
rateLimitAttempt++;
|
|
1011
|
+
const { waitMs, source } = getRateLimitWaitMs(err);
|
|
1012
|
+
const waitSeconds = Math.round(waitMs / 1000);
|
|
1013
|
+
logger_js_1.default.log(null, logger_js_1.default.levels.warn, `Rate limited by "${modelName}", waiting ${waitSeconds}s before retry (${rateLimitAttempt}/${MAX_RATE_LIMIT_RETRIES}, source=${source}): ${err?.message ?? err}`);
|
|
833
1014
|
await onProgressReport({
|
|
834
|
-
message:
|
|
835
|
-
progress: Math.min(
|
|
1015
|
+
message: `AI service is busy. Retrying in ${waitSeconds}s...`,
|
|
1016
|
+
progress: Math.min(calcCurrentProgress(), maxPercent - 5),
|
|
836
1017
|
});
|
|
1018
|
+
await sleep(waitMs);
|
|
1019
|
+
continue;
|
|
837
1020
|
}
|
|
1021
|
+
throw err;
|
|
838
1022
|
}
|
|
839
1023
|
}
|
|
840
1024
|
recordCost(calculateTextCost(modelName, streamUsage, config));
|
|
@@ -915,8 +1099,28 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
915
1099
|
let activeMessages = messagesToSend;
|
|
916
1100
|
let attempt = 0;
|
|
917
1101
|
while (true) {
|
|
918
|
-
|
|
919
|
-
|
|
1102
|
+
// Inner loop: wait + retry on 429. No progress callback in this branch, so
|
|
1103
|
+
// the wait is silent to the caller — only the warn log is emitted.
|
|
1104
|
+
let response;
|
|
1105
|
+
let rateLimitAttempt = 0;
|
|
1106
|
+
while (true) {
|
|
1107
|
+
try {
|
|
1108
|
+
assertQuotaAvailable();
|
|
1109
|
+
response = await llm.invoke(activeMessages);
|
|
1110
|
+
break;
|
|
1111
|
+
}
|
|
1112
|
+
catch (err) {
|
|
1113
|
+
if (isRateLimitError(err) &&
|
|
1114
|
+
rateLimitAttempt < MAX_RATE_LIMIT_RETRIES) {
|
|
1115
|
+
rateLimitAttempt++;
|
|
1116
|
+
const { waitMs, source } = getRateLimitWaitMs(err);
|
|
1117
|
+
logger_js_1.default.log(null, logger_js_1.default.levels.warn, `Rate limited by "${modelName}", waiting ${Math.round(waitMs / 1000)}s before retry (${rateLimitAttempt}/${MAX_RATE_LIMIT_RETRIES}, source=${source}): ${err?.message ?? err}`);
|
|
1118
|
+
await sleep(waitMs);
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
throw err;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
920
1124
|
recordCost(calculateTextCost(modelName, extractUsageFromInvoke(response), config));
|
|
921
1125
|
const rawContent = response?.content || response;
|
|
922
1126
|
// If not expecting JSON, return raw content directly
|
|
@@ -941,47 +1145,6 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
941
1145
|
}
|
|
942
1146
|
}
|
|
943
1147
|
};
|
|
944
|
-
/**
|
|
945
|
-
* Sends a conversational chat prompt to an LLM and returns a plain HTML response
|
|
946
|
-
* intended for end-user display. This function is purpose-built for the StackFactor
|
|
947
|
-
* Mentor chat feature: when the prompt is an array of messages, each `system` message
|
|
948
|
-
* is wrapped in an enhanced system prompt that instructs the model to answer only
|
|
949
|
-
* questions related to the provided topic, decline off-topic questions, and return
|
|
950
|
-
* results as simple HTML without markdown code-block notation. The call is delegated to
|
|
951
|
-
* `runPromptWithModel` with `expectsJsonResponse` set to `false`.
|
|
952
|
-
* @param modelName - The model identifier, e.g. `"gpt-4o"`, `"claude-3-5-sonnet"`
|
|
953
|
-
* @param config - Configuration object with API keys and optional `temperature`
|
|
954
|
-
* @param prompt - A plain string user message, or an array of `{ role, content }`
|
|
955
|
-
* message objects; `system` messages have the topic-constraint wrapper applied
|
|
956
|
-
* @param onProgressReport - Optional callback for streaming progress updates passed
|
|
957
|
-
* through to `runPromptWithModel`
|
|
958
|
-
* @returns The model's response as a raw HTML string
|
|
959
|
-
*/
|
|
960
|
-
const runChatPromptWithModel = (modelName, config, prompt, onProgressReport) => {
|
|
961
|
-
let messages = prompt;
|
|
962
|
-
// If prompt is an array, check for system message and enhance it
|
|
963
|
-
if (Array.isArray(prompt)) {
|
|
964
|
-
messages = prompt.map((msg) => {
|
|
965
|
-
if (msg.role === "system") {
|
|
966
|
-
return {
|
|
967
|
-
...msg,
|
|
968
|
-
content: `You are StackFactor Mentor, an AI assistant that helps users by providing information related to the specified topic.
|
|
969
|
-
|
|
970
|
-
### Objective:
|
|
971
|
-
Respond to the user question considering just related to the selected topic and all previous interactions:
|
|
972
|
-
- If the question is unrelated decline to respond.
|
|
973
|
-
- Return the results as a simple HMTL but don't include notations for formatting blocks.
|
|
974
|
-
|
|
975
|
-
### TOPIC INFORMATION:\n
|
|
976
|
-
${msg.content}
|
|
977
|
-
`,
|
|
978
|
-
};
|
|
979
|
-
}
|
|
980
|
-
return msg;
|
|
981
|
-
});
|
|
982
|
-
}
|
|
983
|
-
return runPromptWithModel(modelName, config, messages, onProgressReport, 0, 100, false);
|
|
984
|
-
};
|
|
985
1148
|
/**
|
|
986
1149
|
* Determines the image generation provider for a given model name based on its prefix.
|
|
987
1150
|
* `dall-e-` and `gpt-image-` prefixes map to `"openai"`. `gemini-` and `imagen-`
|
|
@@ -1243,9 +1406,9 @@ const runPromptWithModelForImageGeneration = async (modelName, config, prompt, o
|
|
|
1243
1406
|
}
|
|
1244
1407
|
};
|
|
1245
1408
|
exports.default = {
|
|
1409
|
+
checkIfAIProviderConfigured,
|
|
1246
1410
|
createAgent,
|
|
1247
1411
|
runAgent,
|
|
1248
|
-
runChatPromptWithModel,
|
|
1249
1412
|
runPromptWithModel,
|
|
1250
1413
|
runPromptWithModelForImageGeneration,
|
|
1251
1414
|
throwErrorIfNotSuccessful,
|
package/dist/esm/langChain.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
declare const _default: {
|
|
2
|
+
checkIfAIProviderConfigured: (config: any) => void;
|
|
2
3
|
createAgent: (name: string, modelName: string, systemPrompt: string, tools: any[], responseFormat: any, config: any) => any;
|
|
3
4
|
runAgent: (agent: any, prompt: string, config: any, onProgress?: Function | null) => Promise<any>;
|
|
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>;
|
|
6
6
|
runPromptWithModelForImageGeneration: (modelName: string, config: any, prompt: string, options?: any) => Promise<any>;
|
|
7
7
|
throwErrorIfNotSuccessful: (response: any) => string;
|
|
@@ -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":";0CAgD6C,GAAG,KAAG,IAAI;wBAke/C,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;oCAqWF,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;sDA6rBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,KACX,OAAO,CAAC,GAAG,CAAC;0CA18B8B,GAAG,KAAG,MAAM;;AAqgCzD,wBAOE"}
|
package/dist/esm/langChain.js
CHANGED
|
@@ -31,6 +31,14 @@ const assertQuotaAvailable = () => {
|
|
|
31
31
|
throw errorHandlingHelper.create(constants.HTTP_CODES.PAYMENT_REQUIRED, constants.ERROR.QUOTA_EXHAUSTED);
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
|
+
const checkIfAIProviderConfigured = (config) => {
|
|
35
|
+
if (!config ||
|
|
36
|
+
!config.openAIAPIKey ||
|
|
37
|
+
!config.googleAPIKey ||
|
|
38
|
+
!config.anthropicAPIKey) {
|
|
39
|
+
throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, "The integration is not properly configured with all AI providers.");
|
|
40
|
+
}
|
|
41
|
+
};
|
|
34
42
|
/**
|
|
35
43
|
* Subtracts the given USD cost from `global.quota.remaining` and adds it to
|
|
36
44
|
* `global.quota.usedThisSession`. Silently no-ops when the quota globals are not
|
|
@@ -517,14 +525,44 @@ const runAgent = async (agent, prompt, config, onProgress = null) => {
|
|
|
517
525
|
},
|
|
518
526
|
]
|
|
519
527
|
: undefined;
|
|
520
|
-
assertQuotaAvailable();
|
|
521
|
-
const response = await agent.invoke({
|
|
522
|
-
messages: [{ role: "user", content: prompt }],
|
|
523
|
-
}, {
|
|
524
|
-
recursionLimit: config.recursionLimit || 25,
|
|
525
|
-
...(callbacks ? { callbacks } : {}),
|
|
526
|
-
});
|
|
527
528
|
const modelName = agent.options?.model?.modelName || agent.options?.model?.model || "";
|
|
529
|
+
// Wait + retry on 429 around the full agent run. The agent loop may issue
|
|
530
|
+
// many internal LLM calls, but a rate limit surfaces as a thrown error from
|
|
531
|
+
// the wrapping invoke. On retry we restart the agent run from scratch — any
|
|
532
|
+
// partial progress (tool calls, intermediate messages) is discarded, since
|
|
533
|
+
// the agent state isn't externally checkpointed. Cost is only recorded on a
|
|
534
|
+
// successful completion. Matches the behavior of the non-agentic paths.
|
|
535
|
+
let response;
|
|
536
|
+
let rateLimitAttempt = 0;
|
|
537
|
+
while (true) {
|
|
538
|
+
try {
|
|
539
|
+
assertQuotaAvailable();
|
|
540
|
+
response = await agent.invoke({
|
|
541
|
+
messages: [{ role: "user", content: prompt }],
|
|
542
|
+
}, {
|
|
543
|
+
recursionLimit: config.recursionLimit || 25,
|
|
544
|
+
...(callbacks ? { callbacks } : {}),
|
|
545
|
+
});
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
catch (err) {
|
|
549
|
+
if (isRateLimitError(err) && rateLimitAttempt < MAX_RATE_LIMIT_RETRIES) {
|
|
550
|
+
rateLimitAttempt++;
|
|
551
|
+
const { waitMs, source } = getRateLimitWaitMs(err);
|
|
552
|
+
const waitSeconds = Math.round(waitMs / 1000);
|
|
553
|
+
logger.log(null, logger.levels.warn, `Rate limited by "${modelName}" (agent), waiting ${waitSeconds}s before retry (${rateLimitAttempt}/${MAX_RATE_LIMIT_RETRIES}, source=${source}): ${err?.message ?? err}`);
|
|
554
|
+
if (onProgress) {
|
|
555
|
+
onProgress({
|
|
556
|
+
type: "rate_limit",
|
|
557
|
+
message: `AI service is busy. Retrying in ${waitSeconds}s...`,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
await sleep(waitMs);
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
throw err;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
528
566
|
recordCost(calculateTextCost(modelName, sumAgentResponseUsage(response), config));
|
|
529
567
|
const endTime = Date.now();
|
|
530
568
|
const duration = endTime - startTime;
|
|
@@ -547,6 +585,122 @@ const throwErrorIfNotSuccessful = (response) => {
|
|
|
547
585
|
}
|
|
548
586
|
};
|
|
549
587
|
const MAX_VALIDATION_RETRIES = 3;
|
|
588
|
+
const MAX_RATE_LIMIT_RETRIES = 2;
|
|
589
|
+
const RATE_LIMIT_MIN_WAIT_MS = 30_000;
|
|
590
|
+
const RATE_LIMIT_MAX_WAIT_MS = 60_000;
|
|
591
|
+
// Cap on how long we'll honor a provider Retry-After hint. If a provider asks
|
|
592
|
+
// for longer than this, treat it as a signal that the request shouldn't be
|
|
593
|
+
// retried in-flight and fail fast instead of holding the caller open.
|
|
594
|
+
const RATE_LIMIT_MAX_HONORED_WAIT_MS = 5 * 60 * 1000;
|
|
595
|
+
// Small jitter added on top of an honored Retry-After so concurrent callers
|
|
596
|
+
// that all received the same hint don't wake at exactly the same instant.
|
|
597
|
+
const RATE_LIMIT_HONORED_JITTER_MS = 2_000;
|
|
598
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
599
|
+
/**
|
|
600
|
+
* Extracts a Retry-After hint (in milliseconds) from a provider 429 error.
|
|
601
|
+
* Different SDKs put headers in different places (top-level `headers`, nested
|
|
602
|
+
* `response.headers`, `responseHeaders`, or under `cause`), and some include a
|
|
603
|
+
* millisecond-precision `retry-after-ms` variant. Also checks the response body
|
|
604
|
+
* for `retry_after` / `retry_after_ms` fields (some providers put it there
|
|
605
|
+
* instead of headers). Standard `Retry-After` may be seconds or an HTTP date.
|
|
606
|
+
* Returns `null` when no usable hint is present.
|
|
607
|
+
*/
|
|
608
|
+
const parseRetryAfterFromError = (err) => {
|
|
609
|
+
const headerSources = [
|
|
610
|
+
err?.headers,
|
|
611
|
+
err?.response?.headers,
|
|
612
|
+
err?.responseHeaders,
|
|
613
|
+
err?.cause?.headers,
|
|
614
|
+
err?.cause?.response?.headers,
|
|
615
|
+
];
|
|
616
|
+
for (const headers of headerSources) {
|
|
617
|
+
if (!headers || typeof headers !== "object")
|
|
618
|
+
continue;
|
|
619
|
+
const retryAfterMs = headers["retry-after-ms"] ?? headers["Retry-After-Ms"];
|
|
620
|
+
if (retryAfterMs != null) {
|
|
621
|
+
const ms = Number(retryAfterMs);
|
|
622
|
+
if (Number.isFinite(ms) && ms > 0)
|
|
623
|
+
return ms;
|
|
624
|
+
}
|
|
625
|
+
const retryAfter = headers["retry-after"] ?? headers["Retry-After"];
|
|
626
|
+
if (retryAfter != null) {
|
|
627
|
+
const asNumber = Number(retryAfter);
|
|
628
|
+
if (Number.isFinite(asNumber) && asNumber > 0) {
|
|
629
|
+
return asNumber * 1000;
|
|
630
|
+
}
|
|
631
|
+
const dateMs = Date.parse(String(retryAfter));
|
|
632
|
+
if (!Number.isNaN(dateMs)) {
|
|
633
|
+
const delta = dateMs - Date.now();
|
|
634
|
+
if (delta > 0)
|
|
635
|
+
return delta;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
const bodySources = [err?.error, err?.response?.data, err?.body];
|
|
640
|
+
for (const body of bodySources) {
|
|
641
|
+
if (!body || typeof body !== "object")
|
|
642
|
+
continue;
|
|
643
|
+
const retryAfterMs = body.retry_after_ms ?? body.retryAfterMs;
|
|
644
|
+
if (typeof retryAfterMs === "number" && retryAfterMs > 0) {
|
|
645
|
+
return retryAfterMs;
|
|
646
|
+
}
|
|
647
|
+
const retryAfterS = body.retry_after ?? body.retryAfter;
|
|
648
|
+
if (typeof retryAfterS === "number" && retryAfterS > 0) {
|
|
649
|
+
return retryAfterS * 1000;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return null;
|
|
653
|
+
};
|
|
654
|
+
/**
|
|
655
|
+
* Picks the back-off duration before retrying a 429. When the provider supplied
|
|
656
|
+
* a Retry-After hint, that value is honored (capped at
|
|
657
|
+
* RATE_LIMIT_MAX_HONORED_WAIT_MS, with small jitter added to avoid thundering
|
|
658
|
+
* herd on the same wake instant). When no hint is present, falls back to a
|
|
659
|
+
* uniform random wait in [RATE_LIMIT_MIN_WAIT_MS, RATE_LIMIT_MAX_WAIT_MS] —
|
|
660
|
+
* which is the right default for "we don't know how long this will last."
|
|
661
|
+
* Returns both the chosen ms and the source so callers can log it.
|
|
662
|
+
*/
|
|
663
|
+
const getRateLimitWaitMs = (err) => {
|
|
664
|
+
const hint = parseRetryAfterFromError(err);
|
|
665
|
+
if (hint != null) {
|
|
666
|
+
const honored = Math.min(hint, RATE_LIMIT_MAX_HONORED_WAIT_MS);
|
|
667
|
+
const jitter = Math.floor(Math.random() * RATE_LIMIT_HONORED_JITTER_MS);
|
|
668
|
+
return {
|
|
669
|
+
waitMs: Math.max(honored + jitter, 1_000),
|
|
670
|
+
source: "retry-after",
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
const waitMs = RATE_LIMIT_MIN_WAIT_MS +
|
|
674
|
+
Math.floor(Math.random() * (RATE_LIMIT_MAX_WAIT_MS - RATE_LIMIT_MIN_WAIT_MS + 1));
|
|
675
|
+
return { waitMs, source: "random" };
|
|
676
|
+
};
|
|
677
|
+
/**
|
|
678
|
+
* Detects rate-limit (HTTP 429) errors across LangChain and provider SDK shapes.
|
|
679
|
+
* Different SDKs surface the status in different places (status, statusCode,
|
|
680
|
+
* response.status, code) and sometimes only in the message text, so this checks
|
|
681
|
+
* each known shape rather than assuming a single field.
|
|
682
|
+
*/
|
|
683
|
+
const isRateLimitError = (err) => {
|
|
684
|
+
if (!err)
|
|
685
|
+
return false;
|
|
686
|
+
const statusCandidates = [
|
|
687
|
+
err.status,
|
|
688
|
+
err.statusCode,
|
|
689
|
+
err.response?.status,
|
|
690
|
+
err.response?.statusCode,
|
|
691
|
+
err.cause?.status,
|
|
692
|
+
err.cause?.statusCode,
|
|
693
|
+
err.code,
|
|
694
|
+
];
|
|
695
|
+
if (statusCandidates.some((c) => c === 429 || c === "429"))
|
|
696
|
+
return true;
|
|
697
|
+
if (typeof err.code === "string" && /rate.?limit/i.test(err.code))
|
|
698
|
+
return true;
|
|
699
|
+
const message = typeof err.message === "string" ? err.message : "";
|
|
700
|
+
return (/\b429\b/.test(message) ||
|
|
701
|
+
/rate.?limit/i.test(message) ||
|
|
702
|
+
/too many requests/i.test(message));
|
|
703
|
+
};
|
|
550
704
|
/**
|
|
551
705
|
* Parses raw LLM content as JSON and validates against an optional Zod schema,
|
|
552
706
|
* returning the canonical JSON string. Tries a direct parse of the trimmed text
|
|
@@ -807,29 +961,59 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
807
961
|
const expectedDurationMs = 15_000; // Tune: expected typical response time
|
|
808
962
|
let activeMessages = messagesToSend;
|
|
809
963
|
let attempt = 0;
|
|
964
|
+
const calcCurrentProgress = () => {
|
|
965
|
+
const elapsed = Date.now() - overallStartTime;
|
|
966
|
+
return Math.round(minPercent +
|
|
967
|
+
(maxPercent - minPercent - 5) *
|
|
968
|
+
(1 - Math.exp(-elapsed / expectedDurationMs)));
|
|
969
|
+
};
|
|
810
970
|
while (true) {
|
|
811
971
|
let rawContent = "";
|
|
812
972
|
let chunkCount = 0;
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
973
|
+
let streamUsage = { input_tokens: 0, output_tokens: 0 };
|
|
974
|
+
// Inner loop: wait + retry on 429 around stream setup and consumption.
|
|
975
|
+
// Cost is only recorded on a successful stream — partial streams that error
|
|
976
|
+
// out with a rate limit are not billed. A 429 fired mid-stream simply
|
|
977
|
+
// discards the partial output and restarts cleanly.
|
|
978
|
+
let rateLimitAttempt = 0;
|
|
979
|
+
while (true) {
|
|
980
|
+
rawContent = "";
|
|
981
|
+
chunkCount = 0;
|
|
982
|
+
streamUsage = { input_tokens: 0, output_tokens: 0 };
|
|
983
|
+
try {
|
|
984
|
+
assertQuotaAvailable();
|
|
985
|
+
const stream = await llm.stream(activeMessages);
|
|
986
|
+
for await (const chunk of stream) {
|
|
987
|
+
accumulateChunkUsage(streamUsage, chunk);
|
|
988
|
+
const content = chunk?.content || chunk;
|
|
989
|
+
if (typeof content === "string") {
|
|
990
|
+
rawContent += content;
|
|
991
|
+
chunkCount++;
|
|
992
|
+
if (chunkCount % progressReportInterval === 0) {
|
|
993
|
+
await onProgressReport({
|
|
994
|
+
message: "Generating content...",
|
|
995
|
+
progress: Math.min(calcCurrentProgress(), maxPercent - 5),
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
break; // stream completed without 429
|
|
1001
|
+
}
|
|
1002
|
+
catch (err) {
|
|
1003
|
+
if (isRateLimitError(err) &&
|
|
1004
|
+
rateLimitAttempt < MAX_RATE_LIMIT_RETRIES) {
|
|
1005
|
+
rateLimitAttempt++;
|
|
1006
|
+
const { waitMs, source } = getRateLimitWaitMs(err);
|
|
1007
|
+
const waitSeconds = Math.round(waitMs / 1000);
|
|
1008
|
+
logger.log(null, logger.levels.warn, `Rate limited by "${modelName}", waiting ${waitSeconds}s before retry (${rateLimitAttempt}/${MAX_RATE_LIMIT_RETRIES}, source=${source}): ${err?.message ?? err}`);
|
|
828
1009
|
await onProgressReport({
|
|
829
|
-
message:
|
|
830
|
-
progress: Math.min(
|
|
1010
|
+
message: `AI service is busy. Retrying in ${waitSeconds}s...`,
|
|
1011
|
+
progress: Math.min(calcCurrentProgress(), maxPercent - 5),
|
|
831
1012
|
});
|
|
1013
|
+
await sleep(waitMs);
|
|
1014
|
+
continue;
|
|
832
1015
|
}
|
|
1016
|
+
throw err;
|
|
833
1017
|
}
|
|
834
1018
|
}
|
|
835
1019
|
recordCost(calculateTextCost(modelName, streamUsage, config));
|
|
@@ -910,8 +1094,28 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
910
1094
|
let activeMessages = messagesToSend;
|
|
911
1095
|
let attempt = 0;
|
|
912
1096
|
while (true) {
|
|
913
|
-
|
|
914
|
-
|
|
1097
|
+
// Inner loop: wait + retry on 429. No progress callback in this branch, so
|
|
1098
|
+
// the wait is silent to the caller — only the warn log is emitted.
|
|
1099
|
+
let response;
|
|
1100
|
+
let rateLimitAttempt = 0;
|
|
1101
|
+
while (true) {
|
|
1102
|
+
try {
|
|
1103
|
+
assertQuotaAvailable();
|
|
1104
|
+
response = await llm.invoke(activeMessages);
|
|
1105
|
+
break;
|
|
1106
|
+
}
|
|
1107
|
+
catch (err) {
|
|
1108
|
+
if (isRateLimitError(err) &&
|
|
1109
|
+
rateLimitAttempt < MAX_RATE_LIMIT_RETRIES) {
|
|
1110
|
+
rateLimitAttempt++;
|
|
1111
|
+
const { waitMs, source } = getRateLimitWaitMs(err);
|
|
1112
|
+
logger.log(null, logger.levels.warn, `Rate limited by "${modelName}", waiting ${Math.round(waitMs / 1000)}s before retry (${rateLimitAttempt}/${MAX_RATE_LIMIT_RETRIES}, source=${source}): ${err?.message ?? err}`);
|
|
1113
|
+
await sleep(waitMs);
|
|
1114
|
+
continue;
|
|
1115
|
+
}
|
|
1116
|
+
throw err;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
915
1119
|
recordCost(calculateTextCost(modelName, extractUsageFromInvoke(response), config));
|
|
916
1120
|
const rawContent = response?.content || response;
|
|
917
1121
|
// If not expecting JSON, return raw content directly
|
|
@@ -936,47 +1140,6 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
936
1140
|
}
|
|
937
1141
|
}
|
|
938
1142
|
};
|
|
939
|
-
/**
|
|
940
|
-
* Sends a conversational chat prompt to an LLM and returns a plain HTML response
|
|
941
|
-
* intended for end-user display. This function is purpose-built for the StackFactor
|
|
942
|
-
* Mentor chat feature: when the prompt is an array of messages, each `system` message
|
|
943
|
-
* is wrapped in an enhanced system prompt that instructs the model to answer only
|
|
944
|
-
* questions related to the provided topic, decline off-topic questions, and return
|
|
945
|
-
* results as simple HTML without markdown code-block notation. The call is delegated to
|
|
946
|
-
* `runPromptWithModel` with `expectsJsonResponse` set to `false`.
|
|
947
|
-
* @param modelName - The model identifier, e.g. `"gpt-4o"`, `"claude-3-5-sonnet"`
|
|
948
|
-
* @param config - Configuration object with API keys and optional `temperature`
|
|
949
|
-
* @param prompt - A plain string user message, or an array of `{ role, content }`
|
|
950
|
-
* message objects; `system` messages have the topic-constraint wrapper applied
|
|
951
|
-
* @param onProgressReport - Optional callback for streaming progress updates passed
|
|
952
|
-
* through to `runPromptWithModel`
|
|
953
|
-
* @returns The model's response as a raw HTML string
|
|
954
|
-
*/
|
|
955
|
-
const runChatPromptWithModel = (modelName, config, prompt, onProgressReport) => {
|
|
956
|
-
let messages = prompt;
|
|
957
|
-
// If prompt is an array, check for system message and enhance it
|
|
958
|
-
if (Array.isArray(prompt)) {
|
|
959
|
-
messages = prompt.map((msg) => {
|
|
960
|
-
if (msg.role === "system") {
|
|
961
|
-
return {
|
|
962
|
-
...msg,
|
|
963
|
-
content: `You are StackFactor Mentor, an AI assistant that helps users by providing information related to the specified topic.
|
|
964
|
-
|
|
965
|
-
### Objective:
|
|
966
|
-
Respond to the user question considering just related to the selected topic and all previous interactions:
|
|
967
|
-
- If the question is unrelated decline to respond.
|
|
968
|
-
- Return the results as a simple HMTL but don't include notations for formatting blocks.
|
|
969
|
-
|
|
970
|
-
### TOPIC INFORMATION:\n
|
|
971
|
-
${msg.content}
|
|
972
|
-
`,
|
|
973
|
-
};
|
|
974
|
-
}
|
|
975
|
-
return msg;
|
|
976
|
-
});
|
|
977
|
-
}
|
|
978
|
-
return runPromptWithModel(modelName, config, messages, onProgressReport, 0, 100, false);
|
|
979
|
-
};
|
|
980
1143
|
/**
|
|
981
1144
|
* Determines the image generation provider for a given model name based on its prefix.
|
|
982
1145
|
* `dall-e-` and `gpt-image-` prefixes map to `"openai"`. `gemini-` and `imagen-`
|
|
@@ -1238,9 +1401,9 @@ const runPromptWithModelForImageGeneration = async (modelName, config, prompt, o
|
|
|
1238
1401
|
}
|
|
1239
1402
|
};
|
|
1240
1403
|
export default {
|
|
1404
|
+
checkIfAIProviderConfigured,
|
|
1241
1405
|
createAgent,
|
|
1242
1406
|
runAgent,
|
|
1243
|
-
runChatPromptWithModel,
|
|
1244
1407
|
runPromptWithModel,
|
|
1245
1408
|
runPromptWithModelForImageGeneration,
|
|
1246
1409
|
throwErrorIfNotSuccessful,
|