@superatomai/sdk-node 0.0.1-s → 0.0.2-dsp
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +1795 -1428
- package/dist/index.d.ts +1795 -1428
- package/dist/index.js +1767 -738
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1782 -758
- package/dist/index.mjs.map +1 -1
- package/dist/userResponse/scripts/script-bootstrap.js +103 -34
- package/dist/userResponse/scripts/script-bootstrap.js.map +1 -1
- package/dist/userResponse/scripts/script-bootstrap.mjs +103 -34
- package/dist/userResponse/scripts/script-bootstrap.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -37,6 +37,8 @@ __export(index_exports, {
|
|
|
37
37
|
LLM: () => LLM,
|
|
38
38
|
MainAgent: () => MainAgent,
|
|
39
39
|
STORAGE_CONFIG: () => STORAGE_CONFIG,
|
|
40
|
+
ScriptMatcher: () => ScriptMatcher,
|
|
41
|
+
ScriptStore: () => ScriptStore,
|
|
40
42
|
SuperatomSDK: () => SuperatomSDK,
|
|
41
43
|
Thread: () => Thread,
|
|
42
44
|
ThreadManager: () => ThreadManager,
|
|
@@ -50,10 +52,13 @@ __export(index_exports, {
|
|
|
50
52
|
hybridRerank: () => hybridRerank,
|
|
51
53
|
llmUsageLogger: () => llmUsageLogger,
|
|
52
54
|
logger: () => logger,
|
|
55
|
+
normalizeScriptBody: () => normalizeScriptBody,
|
|
53
56
|
openaiLLM: () => openaiLLM,
|
|
54
57
|
queryCache: () => queryCache,
|
|
55
58
|
rerankChromaResults: () => rerankChromaResults,
|
|
56
59
|
rerankConversationResults: () => rerankConversationResults,
|
|
60
|
+
resolveScriptRecipeStore: () => resolveScriptRecipeStore,
|
|
61
|
+
runScript: () => runScript,
|
|
57
62
|
userPromptErrorLogger: () => userPromptErrorLogger
|
|
58
63
|
});
|
|
59
64
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -379,6 +384,24 @@ var ComponentListResponseMessageSchema = import_zod3.z.object({
|
|
|
379
384
|
type: import_zod3.z.literal("COMPONENT_LIST_RES"),
|
|
380
385
|
payload: ComponentListResponsePayloadSchema
|
|
381
386
|
});
|
|
387
|
+
var WorkflowDescriptorSchema = import_zod3.z.object({
|
|
388
|
+
id: import_zod3.z.string(),
|
|
389
|
+
name: import_zod3.z.string(),
|
|
390
|
+
description: import_zod3.z.string(),
|
|
391
|
+
whenToUse: import_zod3.z.string(),
|
|
392
|
+
propsSchema: import_zod3.z.record(import_zod3.z.string()),
|
|
393
|
+
defaultProps: import_zod3.z.record(import_zod3.z.unknown()).optional()
|
|
394
|
+
});
|
|
395
|
+
var WorkflowsSchema = import_zod3.z.array(WorkflowDescriptorSchema);
|
|
396
|
+
var WorkflowListResponsePayloadSchema = import_zod3.z.object({
|
|
397
|
+
workflows: import_zod3.z.array(WorkflowDescriptorSchema)
|
|
398
|
+
});
|
|
399
|
+
var WorkflowListResponseMessageSchema = import_zod3.z.object({
|
|
400
|
+
id: import_zod3.z.string(),
|
|
401
|
+
from: MessageParticipantSchema,
|
|
402
|
+
type: import_zod3.z.literal("WORKFLOW_LIST_RES"),
|
|
403
|
+
payload: WorkflowListResponsePayloadSchema
|
|
404
|
+
});
|
|
382
405
|
var OutputFieldSchema = import_zod3.z.object({
|
|
383
406
|
name: import_zod3.z.string(),
|
|
384
407
|
// Field name (column name in the result)
|
|
@@ -402,8 +425,10 @@ var ToolSchema = import_zod3.z.object({
|
|
|
402
425
|
fullSchema: import_zod3.z.string().optional(),
|
|
403
426
|
params: import_zod3.z.record(import_zod3.z.string()),
|
|
404
427
|
fn: import_zod3.z.function().args(import_zod3.z.any()).returns(import_zod3.z.any()),
|
|
405
|
-
outputSchema: OutputSchema.optional()
|
|
428
|
+
outputSchema: OutputSchema.optional(),
|
|
406
429
|
// Optional: describes the data structure returned by this tool
|
|
430
|
+
/** Cache policy. `false` = never cache (live data, write ops). Mirrors HTTP `Cache-Control: no-store`. */
|
|
431
|
+
cache: import_zod3.z.union([import_zod3.z.literal(false), import_zod3.z.object({ ttlMs: import_zod3.z.number().optional() })]).optional()
|
|
407
432
|
});
|
|
408
433
|
var UserQueryFiltersSchema = import_zod3.z.object({
|
|
409
434
|
username: import_zod3.z.string().optional(),
|
|
@@ -1518,7 +1543,7 @@ var QueryCache = class {
|
|
|
1518
1543
|
this.cache = /* @__PURE__ */ new Map();
|
|
1519
1544
|
this.ttlMs = 10 * 60 * 1e3;
|
|
1520
1545
|
// Default: 10 minutes
|
|
1521
|
-
this.maxCacheSize =
|
|
1546
|
+
this.maxCacheSize = 5e3;
|
|
1522
1547
|
// Max data cache entries
|
|
1523
1548
|
this.cleanupInterval = null;
|
|
1524
1549
|
// Encryption for queryId tokens
|
|
@@ -1545,9 +1570,13 @@ var QueryCache = class {
|
|
|
1545
1570
|
return this.ttlMs / 60 / 1e3;
|
|
1546
1571
|
}
|
|
1547
1572
|
/**
|
|
1548
|
-
* Store query result in data cache
|
|
1573
|
+
* Store query result in data cache.
|
|
1574
|
+
* If the key already exists, it's removed first so the re-insert places it
|
|
1575
|
+
* at the back of the iteration order (LRU). Eviction only fires when adding
|
|
1576
|
+
* a genuinely new key past the size limit.
|
|
1549
1577
|
*/
|
|
1550
1578
|
set(query, data) {
|
|
1579
|
+
this.cache.delete(query);
|
|
1551
1580
|
if (this.cache.size >= this.maxCacheSize) {
|
|
1552
1581
|
const oldestKey = this.cache.keys().next().value;
|
|
1553
1582
|
if (oldestKey) this.cache.delete(oldestKey);
|
|
@@ -1560,7 +1589,9 @@ var QueryCache = class {
|
|
|
1560
1589
|
logger.debug(`[QueryCache] Stored result for query (${query.substring(0, 50)}...)`);
|
|
1561
1590
|
}
|
|
1562
1591
|
/**
|
|
1563
|
-
* Get cached result if exists and not expired
|
|
1592
|
+
* Get cached result if exists and not expired.
|
|
1593
|
+
* On hit, re-inserts the entry so it moves to the back of the Map's
|
|
1594
|
+
* iteration order — turning FIFO eviction into true LRU.
|
|
1564
1595
|
*/
|
|
1565
1596
|
get(query) {
|
|
1566
1597
|
const entry = this.cache.get(query);
|
|
@@ -1569,6 +1600,8 @@ var QueryCache = class {
|
|
|
1569
1600
|
this.cache.delete(query);
|
|
1570
1601
|
return null;
|
|
1571
1602
|
}
|
|
1603
|
+
this.cache.delete(query);
|
|
1604
|
+
this.cache.set(query, entry);
|
|
1572
1605
|
logger.info(`[QueryCache] Cache HIT for query (${query.substring(0, 50)}...)`);
|
|
1573
1606
|
return entry.data;
|
|
1574
1607
|
}
|
|
@@ -1710,6 +1743,21 @@ var QueryCache = class {
|
|
|
1710
1743
|
};
|
|
1711
1744
|
var queryCache = new QueryCache();
|
|
1712
1745
|
|
|
1746
|
+
// src/utils/surrogate.ts
|
|
1747
|
+
var LONE_SURROGATE_RE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
|
|
1748
|
+
function stripLoneSurrogates(value) {
|
|
1749
|
+
if (typeof value !== "string") return value;
|
|
1750
|
+
if (!/[\uD800-\uDFFF]/.test(value)) return value;
|
|
1751
|
+
return value.replace(LONE_SURROGATE_RE, "\uFFFD");
|
|
1752
|
+
}
|
|
1753
|
+
function safeTruncate(text, maxUnits) {
|
|
1754
|
+
if (typeof text !== "string" || text.length <= maxUnits || maxUnits < 0) return text;
|
|
1755
|
+
let end = maxUnits;
|
|
1756
|
+
const lastCode = text.charCodeAt(end - 1);
|
|
1757
|
+
if (lastCode >= 55296 && lastCode <= 56319) end -= 1;
|
|
1758
|
+
return text.slice(0, end);
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1713
1761
|
// src/userResponse/llm-result-truncator.ts
|
|
1714
1762
|
var DEFAULT_MAX_ROWS = 10;
|
|
1715
1763
|
var DEFAULT_MAX_CHARS_PER_FIELD = 500;
|
|
@@ -1752,12 +1800,12 @@ function isDateString(value) {
|
|
|
1752
1800
|
}
|
|
1753
1801
|
function truncateTextField(value, maxLength) {
|
|
1754
1802
|
if (value.length <= maxLength) {
|
|
1755
|
-
return { text: value, wasTruncated: false };
|
|
1803
|
+
return { text: stripLoneSurrogates(value), wasTruncated: false };
|
|
1756
1804
|
}
|
|
1757
|
-
const truncated = value
|
|
1758
|
-
const remaining = value.length -
|
|
1805
|
+
const truncated = safeTruncate(value, maxLength);
|
|
1806
|
+
const remaining = value.length - truncated.length;
|
|
1759
1807
|
return {
|
|
1760
|
-
text: `${truncated}... (${remaining} more chars)`,
|
|
1808
|
+
text: `${stripLoneSurrogates(truncated)}... (${remaining} more chars)`,
|
|
1761
1809
|
wasTruncated: true
|
|
1762
1810
|
};
|
|
1763
1811
|
}
|
|
@@ -2055,6 +2103,21 @@ function formatResultAsString(formattedResult) {
|
|
|
2055
2103
|
return JSON.stringify(formattedResult, null, 2);
|
|
2056
2104
|
}
|
|
2057
2105
|
|
|
2106
|
+
// src/utils/cache-key.ts
|
|
2107
|
+
function stableStringify(value) {
|
|
2108
|
+
if (value === null || typeof value !== "object") {
|
|
2109
|
+
return JSON.stringify(value);
|
|
2110
|
+
}
|
|
2111
|
+
if (Array.isArray(value)) {
|
|
2112
|
+
return "[" + value.map(stableStringify).join(",") + "]";
|
|
2113
|
+
}
|
|
2114
|
+
const keys = Object.keys(value).sort();
|
|
2115
|
+
return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(value[k])).join(",") + "}";
|
|
2116
|
+
}
|
|
2117
|
+
function buildDirectToolCacheKey(toolId, parameters) {
|
|
2118
|
+
return `et-direct:${toolId}:${stableStringify(parameters || {})}`;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2058
2121
|
// src/handlers/data-request.ts
|
|
2059
2122
|
function getQueryCacheKey(query) {
|
|
2060
2123
|
if (typeof query === "string") {
|
|
@@ -2068,7 +2131,7 @@ function getQueryCacheKey(query) {
|
|
|
2068
2131
|
}
|
|
2069
2132
|
return "";
|
|
2070
2133
|
}
|
|
2071
|
-
function getCacheKey(collection, op, params) {
|
|
2134
|
+
function getCacheKey(collection, op, params, tools) {
|
|
2072
2135
|
if (collection === "database" && op === "execute" && params?.sql) {
|
|
2073
2136
|
return getQueryCacheKey(params.sql);
|
|
2074
2137
|
}
|
|
@@ -2078,6 +2141,13 @@ function getCacheKey(collection, op, params) {
|
|
|
2078
2141
|
const paramsKey = params.params ? JSON.stringify(params.params) : "";
|
|
2079
2142
|
return sqlKey ? `et:${toolId}:${sqlKey}:${paramsKey}` : "";
|
|
2080
2143
|
}
|
|
2144
|
+
if (collection === "external-tools" && op === "execute" && params?.toolId && !params?.sql) {
|
|
2145
|
+
const tool = tools?.find((t) => t.id === params.toolId);
|
|
2146
|
+
if (tool?.cache === false) return "";
|
|
2147
|
+
const { toolId, toolName, ...rest } = params;
|
|
2148
|
+
const paramsKey = stableStringify(rest);
|
|
2149
|
+
return `et-direct:${toolId}:${paramsKey}`;
|
|
2150
|
+
}
|
|
2081
2151
|
if (collection === "external-tools" && op === "executeByQueryId" && params?.queryId) {
|
|
2082
2152
|
const toolId = params.toolId || "";
|
|
2083
2153
|
const filterKey = params.filterParams ? JSON.stringify(params.filterParams) : "";
|
|
@@ -2086,7 +2156,7 @@ function getCacheKey(collection, op, params) {
|
|
|
2086
2156
|
}
|
|
2087
2157
|
return "";
|
|
2088
2158
|
}
|
|
2089
|
-
async function handleDataRequest(data, collections, sendMessage) {
|
|
2159
|
+
async function handleDataRequest(data, collections, sendMessage, tools) {
|
|
2090
2160
|
let requestId;
|
|
2091
2161
|
let collection;
|
|
2092
2162
|
let op;
|
|
@@ -2112,7 +2182,7 @@ async function handleDataRequest(data, collections, sendMessage) {
|
|
|
2112
2182
|
const startTime = performance.now();
|
|
2113
2183
|
let result;
|
|
2114
2184
|
let fromCache = false;
|
|
2115
|
-
const cacheKey = getCacheKey(collection, op, params);
|
|
2185
|
+
const cacheKey = getCacheKey(collection, op, params, tools);
|
|
2116
2186
|
if (cacheKey) {
|
|
2117
2187
|
const cachedResult = queryCache.get(cacheKey);
|
|
2118
2188
|
if (cachedResult !== null) {
|
|
@@ -3777,6 +3847,12 @@ You MUST respond with ONLY a valid JSON object (no markdown, no code blocks):
|
|
|
3777
3847
|
---
|
|
3778
3848
|
|
|
3779
3849
|
## CONTEXT
|
|
3850
|
+
|
|
3851
|
+
### Global Knowledge Base
|
|
3852
|
+
{{GLOBAL_KNOWLEDGE_BASE}}
|
|
3853
|
+
|
|
3854
|
+
### Knowledge Base Context
|
|
3855
|
+
{{KNOWLEDGE_BASE_CONTEXT}}
|
|
3780
3856
|
`,
|
|
3781
3857
|
user: `{{USER_PROMPT}}`
|
|
3782
3858
|
},
|
|
@@ -4329,13 +4405,11 @@ var PromptLoader = class {
|
|
|
4329
4405
|
* @returns Processed string
|
|
4330
4406
|
*/
|
|
4331
4407
|
replaceVariables(template, variables) {
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
const
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
}
|
|
4338
|
-
return content;
|
|
4408
|
+
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
4409
|
+
if (!(key in variables)) return match;
|
|
4410
|
+
const value = variables[key];
|
|
4411
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
4412
|
+
});
|
|
4339
4413
|
}
|
|
4340
4414
|
/**
|
|
4341
4415
|
* Load both system and user prompts from cache and replace variables
|
|
@@ -4358,8 +4432,6 @@ var PromptLoader = class {
|
|
|
4358
4432
|
const [staticPart, contextPart] = template.system.split(contextMarker);
|
|
4359
4433
|
const processedStatic = this.replaceVariables(staticPart, variables);
|
|
4360
4434
|
const processedContext = this.replaceVariables(contextMarker + contextPart, variables);
|
|
4361
|
-
const staticLength = processedStatic.length;
|
|
4362
|
-
const contextLength = processedContext.length;
|
|
4363
4435
|
return {
|
|
4364
4436
|
system: [
|
|
4365
4437
|
{
|
|
@@ -5289,9 +5361,36 @@ var searchConversationsWithReranking = async (options) => {
|
|
|
5289
5361
|
return null;
|
|
5290
5362
|
}
|
|
5291
5363
|
};
|
|
5364
|
+
var findExactMatch = async ({
|
|
5365
|
+
userPrompt,
|
|
5366
|
+
collections,
|
|
5367
|
+
userId
|
|
5368
|
+
}) => {
|
|
5369
|
+
try {
|
|
5370
|
+
if (!collections || !collections["conversation-history"] || !collections["conversation-history"]["exactMatch"]) {
|
|
5371
|
+
logger.info("[ConversationSearch] conversation-history.exactMatch collection not registered, skipping");
|
|
5372
|
+
return null;
|
|
5373
|
+
}
|
|
5374
|
+
const result = await collections["conversation-history"]["exactMatch"]({
|
|
5375
|
+
userPrompt,
|
|
5376
|
+
userId
|
|
5377
|
+
});
|
|
5378
|
+
if (!result || !result.uiBlock) {
|
|
5379
|
+
logger.info("[ConversationSearch] No exact match found");
|
|
5380
|
+
return null;
|
|
5381
|
+
}
|
|
5382
|
+
logger.info(`[ConversationSearch] \u2713 Exact prompt match found for "${userPrompt.substring(0, 50)}..."`);
|
|
5383
|
+
return result;
|
|
5384
|
+
} catch (error) {
|
|
5385
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
5386
|
+
logger.warn(`[ConversationSearch] Error in exact match lookup: ${errorMsg}`);
|
|
5387
|
+
return null;
|
|
5388
|
+
}
|
|
5389
|
+
};
|
|
5292
5390
|
var ConversationSearch = {
|
|
5293
5391
|
searchConversations,
|
|
5294
|
-
searchConversationsWithReranking
|
|
5392
|
+
searchConversationsWithReranking,
|
|
5393
|
+
findExactMatch
|
|
5295
5394
|
};
|
|
5296
5395
|
var conversation_search_default = ConversationSearch;
|
|
5297
5396
|
|
|
@@ -5309,16 +5408,20 @@ var MAX_TOKENS_CLASSIFICATION = 1500;
|
|
|
5309
5408
|
var MAX_TOKENS_ADAPTATION = 8192;
|
|
5310
5409
|
var MAX_TOKENS_TEXT_RESPONSE = 4e3;
|
|
5311
5410
|
var MAX_TOKENS_NEXT_QUESTIONS = 1200;
|
|
5312
|
-
var
|
|
5411
|
+
var MAX_ROWS_FETCHED = 50;
|
|
5412
|
+
var MAX_ROWS_TO_LLM = 10;
|
|
5413
|
+
var LLM_SAMPLE_ROWS = 10;
|
|
5414
|
+
var MAX_ROWS_RENDERED = 250;
|
|
5313
5415
|
var DEFAULT_MAX_CHARS_PER_FIELD2 = 500;
|
|
5314
|
-
var STREAM_PREVIEW_MAX_ROWS = 10;
|
|
5315
5416
|
var STREAM_PREVIEW_MAX_CHARS = 200;
|
|
5316
|
-
var TOOL_TRACKING_MAX_ROWS = 5;
|
|
5317
5417
|
var TOOL_TRACKING_MAX_CHARS = 200;
|
|
5318
|
-
var
|
|
5418
|
+
var DEFAULT_MAX_ROWS_FOR_LLM = LLM_SAMPLE_ROWS;
|
|
5419
|
+
var STREAM_PREVIEW_MAX_ROWS = LLM_SAMPLE_ROWS;
|
|
5420
|
+
var TOOL_TRACKING_SAMPLE_ROWS = LLM_SAMPLE_ROWS;
|
|
5421
|
+
var TOOL_TRACKING_MAX_ROWS = 5;
|
|
5319
5422
|
var DEFAULT_QUERY_LIMIT = 24;
|
|
5320
|
-
var MAX_AGENT_QUERY_LIMIT =
|
|
5321
|
-
var MAX_COMPONENT_QUERY_LIMIT =
|
|
5423
|
+
var MAX_AGENT_QUERY_LIMIT = LLM_SAMPLE_ROWS;
|
|
5424
|
+
var MAX_COMPONENT_QUERY_LIMIT = MAX_ROWS_RENDERED;
|
|
5322
5425
|
var EXACT_MATCH_SIMILARITY_THRESHOLD = 0.99;
|
|
5323
5426
|
var DEFAULT_CONVERSATION_SIMILARITY_THRESHOLD = 0.8;
|
|
5324
5427
|
var MAX_TOOL_CALLING_ITERATIONS = 20;
|
|
@@ -5815,6 +5918,7 @@ var LLM = class {
|
|
|
5815
5918
|
/* Get a complete text response from an LLM (Anthropic or Groq) */
|
|
5816
5919
|
static async text(messages, options = {}) {
|
|
5817
5920
|
const [provider, modelName] = this._parseModel(options.model);
|
|
5921
|
+
messages = this._sanitizeMessages(messages);
|
|
5818
5922
|
if (provider === "anthropic") {
|
|
5819
5923
|
return this._anthropicText(messages, modelName, options);
|
|
5820
5924
|
} else if (provider === "groq") {
|
|
@@ -5830,6 +5934,7 @@ var LLM = class {
|
|
|
5830
5934
|
/* Stream response from an LLM (Anthropic or Groq) */
|
|
5831
5935
|
static async stream(messages, options = {}, json) {
|
|
5832
5936
|
const [provider, modelName] = this._parseModel(options.model);
|
|
5937
|
+
messages = this._sanitizeMessages(messages);
|
|
5833
5938
|
if (provider === "anthropic") {
|
|
5834
5939
|
return this._anthropicStream(messages, modelName, options, json);
|
|
5835
5940
|
} else if (provider === "groq") {
|
|
@@ -5845,6 +5950,7 @@ var LLM = class {
|
|
|
5845
5950
|
/* Stream response with tool calling support (Anthropic and Gemini) */
|
|
5846
5951
|
static async streamWithTools(messages, tools, toolHandler, options = {}, maxIterations = 3) {
|
|
5847
5952
|
const [provider, modelName] = this._parseModel(options.model);
|
|
5953
|
+
messages = this._sanitizeMessages(messages);
|
|
5848
5954
|
if (provider === "anthropic") {
|
|
5849
5955
|
return this._anthropicStreamWithTools(messages, tools, toolHandler, modelName, options, maxIterations);
|
|
5850
5956
|
} else if (provider === "gemini") {
|
|
@@ -5870,6 +5976,26 @@ var LLM = class {
|
|
|
5870
5976
|
}
|
|
5871
5977
|
return sys;
|
|
5872
5978
|
}
|
|
5979
|
+
/**
|
|
5980
|
+
* Strip unpaired UTF-16 surrogates from every text field of a message set.
|
|
5981
|
+
*
|
|
5982
|
+
* A lone surrogate (from mid-pair string slicing or corrupt source data)
|
|
5983
|
+
* serializes to a bare `\udXXX` escape that strict JSON parsers — including
|
|
5984
|
+
* the one on Anthropic's API — reject with "no low surrogate in string",
|
|
5985
|
+
* failing the whole request. Sanitizing here, at the single boundary every
|
|
5986
|
+
* provider call flows through, guarantees no request can carry one.
|
|
5987
|
+
*/
|
|
5988
|
+
static _sanitizeMessages(messages) {
|
|
5989
|
+
const sys = typeof messages.sys === "string" ? stripLoneSurrogates(messages.sys) : messages.sys.map(
|
|
5990
|
+
(block) => block?.type === "text" && typeof block.text === "string" ? { ...block, text: stripLoneSurrogates(block.text) } : block
|
|
5991
|
+
);
|
|
5992
|
+
return {
|
|
5993
|
+
...messages,
|
|
5994
|
+
sys,
|
|
5995
|
+
user: stripLoneSurrogates(messages.user),
|
|
5996
|
+
...messages.prefill !== void 0 && { prefill: stripLoneSurrogates(messages.prefill) }
|
|
5997
|
+
};
|
|
5998
|
+
}
|
|
5873
5999
|
/**
|
|
5874
6000
|
* Log cache usage metrics from Anthropic API response
|
|
5875
6001
|
* Shows cache hits, costs, and savings
|
|
@@ -6249,12 +6375,14 @@ var LLM = class {
|
|
|
6249
6375
|
let resultContent = typeof result === "string" ? result : JSON.stringify(result);
|
|
6250
6376
|
const MAX_RESULT_LENGTH = 5e4;
|
|
6251
6377
|
if (resultContent.length > MAX_RESULT_LENGTH) {
|
|
6252
|
-
resultContent = resultContent
|
|
6378
|
+
resultContent = safeTruncate(resultContent, MAX_RESULT_LENGTH) + "\n\n... [Result truncated - showing first 50000 characters of " + resultContent.length + " total]";
|
|
6253
6379
|
}
|
|
6254
6380
|
return {
|
|
6255
6381
|
type: "tool_result",
|
|
6256
6382
|
tool_use_id: toolUse.id,
|
|
6257
|
-
|
|
6383
|
+
// Final safety net: tool results carry source data and are built
|
|
6384
|
+
// mid-loop (after entry-point sanitize), so strip lone surrogates here.
|
|
6385
|
+
content: stripLoneSurrogates(resultContent)
|
|
6258
6386
|
};
|
|
6259
6387
|
} catch (error) {
|
|
6260
6388
|
return {
|
|
@@ -6769,11 +6897,12 @@ var LLM = class {
|
|
|
6769
6897
|
let resultContent = typeof result2 === "string" ? result2 : JSON.stringify(result2);
|
|
6770
6898
|
const MAX_RESULT_LENGTH = 5e4;
|
|
6771
6899
|
if (resultContent.length > MAX_RESULT_LENGTH) {
|
|
6772
|
-
resultContent = resultContent
|
|
6900
|
+
resultContent = safeTruncate(resultContent, MAX_RESULT_LENGTH) + "\n\n... [Result truncated - showing first 50000 characters of " + resultContent.length + " total]";
|
|
6773
6901
|
}
|
|
6774
6902
|
return {
|
|
6775
6903
|
name: fc.name,
|
|
6776
|
-
|
|
6904
|
+
// Final safety net: strip lone surrogates from source-data results.
|
|
6905
|
+
response: { result: stripLoneSurrogates(resultContent) }
|
|
6777
6906
|
};
|
|
6778
6907
|
} catch (error) {
|
|
6779
6908
|
return {
|
|
@@ -7046,12 +7175,12 @@ var LLM = class {
|
|
|
7046
7175
|
result = typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult);
|
|
7047
7176
|
const MAX_RESULT_LENGTH = 5e4;
|
|
7048
7177
|
if (result.length > MAX_RESULT_LENGTH) {
|
|
7049
|
-
result = result
|
|
7178
|
+
result = safeTruncate(result, MAX_RESULT_LENGTH) + "\n\n... [Result truncated - showing first 50000 characters of " + result.length + " total]";
|
|
7050
7179
|
}
|
|
7051
7180
|
} catch (error) {
|
|
7052
7181
|
result = JSON.stringify({ error: error instanceof Error ? error.message : String(error) });
|
|
7053
7182
|
}
|
|
7054
|
-
return { role: "tool", tool_call_id: tc.id, content: result };
|
|
7183
|
+
return { role: "tool", tool_call_id: tc.id, content: stripLoneSurrogates(result) };
|
|
7055
7184
|
}));
|
|
7056
7185
|
toolCallResults.forEach((r) => conversationMessages.push(r));
|
|
7057
7186
|
}
|
|
@@ -7190,6 +7319,130 @@ function extractObjectText(obj) {
|
|
|
7190
7319
|
return JSON.stringify(obj, null, 2);
|
|
7191
7320
|
}
|
|
7192
7321
|
|
|
7322
|
+
// src/userResponse/agents/data-summary.ts
|
|
7323
|
+
var MAIN_AGENT_COMPLETE_ROWS = MAX_ROWS_TO_LLM;
|
|
7324
|
+
function summarizeRows(rows) {
|
|
7325
|
+
if (!rows.length) return { totalRows: 0, columns: {} };
|
|
7326
|
+
const MAX_CATEGORIES = 20;
|
|
7327
|
+
const keys = Object.keys(rows[0]);
|
|
7328
|
+
const columns = {};
|
|
7329
|
+
const fmtDate = (ms) => {
|
|
7330
|
+
const d = new Date(ms);
|
|
7331
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
7332
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
|
7333
|
+
};
|
|
7334
|
+
for (const key of keys) {
|
|
7335
|
+
let nonNull = 0, zeros = 0, isNumeric = true, isDateLike = true, sum = 0;
|
|
7336
|
+
let min, max;
|
|
7337
|
+
let dMin, dMax;
|
|
7338
|
+
const counts = /* @__PURE__ */ new Map();
|
|
7339
|
+
let overflow = false;
|
|
7340
|
+
for (const row of rows) {
|
|
7341
|
+
const v = row[key];
|
|
7342
|
+
if (v === null || v === void 0 || v === "") continue;
|
|
7343
|
+
nonNull++;
|
|
7344
|
+
const num = typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" && !isNaN(Number(v)) ? Number(v) : NaN;
|
|
7345
|
+
if (!Number.isNaN(num) && typeof v !== "boolean") {
|
|
7346
|
+
if (num === 0) zeros++;
|
|
7347
|
+
min = min === void 0 ? num : Math.min(min, num);
|
|
7348
|
+
max = max === void 0 ? num : Math.max(max, num);
|
|
7349
|
+
sum += num;
|
|
7350
|
+
} else {
|
|
7351
|
+
isNumeric = false;
|
|
7352
|
+
}
|
|
7353
|
+
const t = v instanceof Date ? v.getTime() : typeof v === "string" ? Date.parse(v) : NaN;
|
|
7354
|
+
if (!Number.isNaN(t) && typeof v !== "number") {
|
|
7355
|
+
dMin = dMin === void 0 ? t : Math.min(dMin, t);
|
|
7356
|
+
dMax = dMax === void 0 ? t : Math.max(dMax, t);
|
|
7357
|
+
} else {
|
|
7358
|
+
isDateLike = false;
|
|
7359
|
+
}
|
|
7360
|
+
if (!overflow) {
|
|
7361
|
+
const sv = String(v);
|
|
7362
|
+
counts.set(sv, (counts.get(sv) || 0) + 1);
|
|
7363
|
+
if (counts.size > MAX_CATEGORIES * 5) overflow = true;
|
|
7364
|
+
}
|
|
7365
|
+
}
|
|
7366
|
+
const nulls = rows.length - nonNull;
|
|
7367
|
+
if (nonNull === 0) {
|
|
7368
|
+
columns[key] = { type: "empty", nulls };
|
|
7369
|
+
continue;
|
|
7370
|
+
}
|
|
7371
|
+
if (isNumeric) {
|
|
7372
|
+
columns[key] = {
|
|
7373
|
+
type: "number",
|
|
7374
|
+
nonNull,
|
|
7375
|
+
nulls,
|
|
7376
|
+
zeros,
|
|
7377
|
+
min,
|
|
7378
|
+
max,
|
|
7379
|
+
avg: Math.round(sum / nonNull * 100) / 100
|
|
7380
|
+
};
|
|
7381
|
+
} else if (isDateLike) {
|
|
7382
|
+
columns[key] = {
|
|
7383
|
+
type: "date",
|
|
7384
|
+
nonNull,
|
|
7385
|
+
min: fmtDate(dMin),
|
|
7386
|
+
max: fmtDate(dMax),
|
|
7387
|
+
distinct: overflow ? `${MAX_CATEGORIES * 5}+` : counts.size
|
|
7388
|
+
};
|
|
7389
|
+
} else if (!overflow && counts.size <= MAX_CATEGORIES) {
|
|
7390
|
+
const dist = {};
|
|
7391
|
+
for (const [k, c] of [...counts.entries()].sort((a, b) => b[1] - a[1])) dist[k] = c;
|
|
7392
|
+
columns[key] = { type: "category", distinct: counts.size, counts: dist };
|
|
7393
|
+
} else {
|
|
7394
|
+
columns[key] = {
|
|
7395
|
+
type: "text",
|
|
7396
|
+
distinct: overflow ? `${MAX_CATEGORIES * 5}+` : counts.size,
|
|
7397
|
+
examples: [...counts.keys()].slice(0, 3)
|
|
7398
|
+
};
|
|
7399
|
+
}
|
|
7400
|
+
}
|
|
7401
|
+
const dims = keys.filter((k) => columns[k]?.type === "category" && columns[k].distinct <= 6).sort((a, b) => columns[a].distinct - columns[b].distinct);
|
|
7402
|
+
const measures = keys.filter((k) => columns[k]?.type === "number");
|
|
7403
|
+
let groups;
|
|
7404
|
+
if (dims.length && measures.length) {
|
|
7405
|
+
const MAX_GROUPS = 30;
|
|
7406
|
+
let useDims = dims.slice(0, 2);
|
|
7407
|
+
const keyOf = (row) => useDims.map((d) => `${d}=${row[d]}`).join(" | ");
|
|
7408
|
+
if (new Set(rows.map(keyOf)).size > MAX_GROUPS) useDims = dims.slice(0, 1);
|
|
7409
|
+
const agg = /* @__PURE__ */ new Map();
|
|
7410
|
+
for (const row of rows) {
|
|
7411
|
+
const key = useDims.map((d) => `${d}=${row[d]}`).join(" | ");
|
|
7412
|
+
let g = agg.get(key);
|
|
7413
|
+
if (!g) {
|
|
7414
|
+
g = { count: 0, sums: {}, ns: {} };
|
|
7415
|
+
agg.set(key, g);
|
|
7416
|
+
}
|
|
7417
|
+
g.count++;
|
|
7418
|
+
for (const m of measures) {
|
|
7419
|
+
const v = row[m];
|
|
7420
|
+
if (typeof v === "number" && !Number.isNaN(v)) {
|
|
7421
|
+
g.sums[m] = (g.sums[m] || 0) + v;
|
|
7422
|
+
g.ns[m] = (g.ns[m] || 0) + 1;
|
|
7423
|
+
}
|
|
7424
|
+
}
|
|
7425
|
+
}
|
|
7426
|
+
if (agg.size <= MAX_GROUPS) {
|
|
7427
|
+
const isCountLike = (name) => /count|\bobs\b|observation|qty|quantity|\bnum|total|evidential|volume|units/i.test(name);
|
|
7428
|
+
groups = {
|
|
7429
|
+
by: useDims,
|
|
7430
|
+
rows: [...agg.entries()].map(([key, g]) => {
|
|
7431
|
+
const avg = {};
|
|
7432
|
+
const sum = {};
|
|
7433
|
+
for (const m of measures) {
|
|
7434
|
+
if (!g.ns[m]) continue;
|
|
7435
|
+
avg[m] = Math.round(g.sums[m] / g.ns[m] * 100) / 100;
|
|
7436
|
+
if (isCountLike(m)) sum[m] = Math.round(g.sums[m] * 100) / 100;
|
|
7437
|
+
}
|
|
7438
|
+
return Object.keys(sum).length ? { key, count: g.count, avg, sum } : { key, count: g.count, avg };
|
|
7439
|
+
})
|
|
7440
|
+
};
|
|
7441
|
+
}
|
|
7442
|
+
}
|
|
7443
|
+
return groups ? { totalRows: rows.length, columns, groups } : { totalRows: rows.length, columns };
|
|
7444
|
+
}
|
|
7445
|
+
|
|
7193
7446
|
// src/userResponse/agents/agent-prompt-builder.ts
|
|
7194
7447
|
function buildSourceSummaries(externalTools) {
|
|
7195
7448
|
return externalTools.map((tool) => {
|
|
@@ -7409,8 +7662,15 @@ ${result}`;
|
|
|
7409
7662
|
await streamDelay();
|
|
7410
7663
|
}
|
|
7411
7664
|
const cappedInput = { ...toolInput };
|
|
7412
|
-
if (cappedInput.limit === void 0 || cappedInput.limit > this.config.
|
|
7413
|
-
cappedInput.limit = this.config.
|
|
7665
|
+
if (cappedInput.limit === void 0 || cappedInput.limit > this.config.maxRowsFetched) {
|
|
7666
|
+
cappedInput.limit = this.config.maxRowsFetched;
|
|
7667
|
+
}
|
|
7668
|
+
const _st = this.extractSourceType();
|
|
7669
|
+
if (typeof cappedInput.sql === "string") {
|
|
7670
|
+
cappedInput.sql = ensureQueryLimit(cappedInput.sql, this.config.maxRowsFetched, this.config.maxRowsFetched, _st);
|
|
7671
|
+
}
|
|
7672
|
+
if (typeof cappedInput.query === "string") {
|
|
7673
|
+
cappedInput.query = ensureQueryLimit(cappedInput.query, this.config.maxRowsFetched, this.config.maxRowsFetched, _st);
|
|
7414
7674
|
}
|
|
7415
7675
|
queryExecuted = cappedInput.sql || cappedInput.query || JSON.stringify(cappedInput);
|
|
7416
7676
|
if (this.streamBuffer.hasCallback() && queryExecuted) {
|
|
@@ -7461,8 +7721,11 @@ Analyze the error and try again with a corrected query.`;
|
|
|
7461
7721
|
`);
|
|
7462
7722
|
if (resultData.length > 0) {
|
|
7463
7723
|
try {
|
|
7464
|
-
const
|
|
7465
|
-
|
|
7724
|
+
const preview = formatQueryResultForLLM(resultData, {
|
|
7725
|
+
maxRows: STREAM_PREVIEW_MAX_ROWS,
|
|
7726
|
+
maxCharsPerField: STREAM_PREVIEW_MAX_CHARS
|
|
7727
|
+
});
|
|
7728
|
+
this.streamBuffer.write(`<DataTable>${JSON.stringify(preview.data)}</DataTable>
|
|
7466
7729
|
|
|
7467
7730
|
`);
|
|
7468
7731
|
} catch {
|
|
@@ -7485,7 +7748,12 @@ Analyze the error and try again with a corrected query.`;
|
|
|
7485
7748
|
_totalRecords: totalRowsMatched,
|
|
7486
7749
|
_recordsShown: resultData.length,
|
|
7487
7750
|
_metadata: result.metadata,
|
|
7488
|
-
_sampleData: resultData.slice(0,
|
|
7751
|
+
_sampleData: resultData.slice(0, TOOL_TRACKING_SAMPLE_ROWS),
|
|
7752
|
+
// For the main agent: a bounded summary over the FULL fetched
|
|
7753
|
+
// result (complete structure regardless of size) + a complete
|
|
7754
|
+
// slice for small results (so lookups arrive whole, not biased).
|
|
7755
|
+
_summary: summarizeRows(resultData),
|
|
7756
|
+
_mainAgentRows: resultData.slice(0, MAIN_AGENT_COMPLETE_ROWS)
|
|
7489
7757
|
},
|
|
7490
7758
|
outputSchema: this.tool.outputSchema,
|
|
7491
7759
|
sourceSchema: this.tool.description,
|
|
@@ -7493,7 +7761,7 @@ Analyze the error and try again with a corrected query.`;
|
|
|
7493
7761
|
};
|
|
7494
7762
|
allExecutedTools.push(executedTool);
|
|
7495
7763
|
const formatted = typeof formattedResult === "string" ? formattedResult : JSON.stringify(formattedResult);
|
|
7496
|
-
const followUpNote = successfulQueries < 2 ? "
|
|
7764
|
+
const followUpNote = successfulQueries < 2 ? "STOP now and return this data \u2014 it satisfies the intent in the common case. Make a second query ONLY if a column or row the intent explicitly requires is verifiably MISSING from these results (not merely to widen, re-sort, or double-check). A redundant follow-up wastes a full round-trip." : "Maximum queries reached. Use the data you have.";
|
|
7497
7765
|
return `\u2705 Query executed successfully. ${resultData.length} rows returned (${totalRowsMatched} total matched). ${followUpNote}
|
|
7498
7766
|
|
|
7499
7767
|
${formatted}`;
|
|
@@ -7555,7 +7823,13 @@ Analyze the error and try again with a corrected query.`;
|
|
|
7555
7823
|
sourceId: this.tool.id,
|
|
7556
7824
|
sourceName: this.tool.name,
|
|
7557
7825
|
success: true,
|
|
7558
|
-
|
|
7826
|
+
// Don't retain the full fetched result — only the bounded slice the
|
|
7827
|
+
// main agent can actually use. The complete-structure summary and the
|
|
7828
|
+
// complete-small rows already live on each executedTool (`_summary` /
|
|
7829
|
+
// `_mainAgentRows`); keeping the full resultData here would hold up to
|
|
7830
|
+
// MAX_ROWS_FETCHED rows × every query in memory for the whole request
|
|
7831
|
+
// for no functional gain.
|
|
7832
|
+
data: resultData.slice(0, MAIN_AGENT_COMPLETE_ROWS),
|
|
7559
7833
|
metadata: {
|
|
7560
7834
|
totalRowsMatched,
|
|
7561
7835
|
rowsReturned: resultData.length,
|
|
@@ -7606,13 +7880,13 @@ Analyze the error and try again with a corrected query.`;
|
|
|
7606
7880
|
const fullSchema = this.options.preResolvedSchema || this.tool.fullSchema || this.tool.description || "No schema available";
|
|
7607
7881
|
const databaseRules = await promptLoader.loadDatabaseRulesForType(sourceType);
|
|
7608
7882
|
const rowLimitSyntax = {
|
|
7609
|
-
"mssql": `Use SELECT TOP ${this.config.
|
|
7610
|
-
"postgres": `Add LIMIT ${this.config.
|
|
7611
|
-
"mysql": `Add LIMIT ${this.config.
|
|
7612
|
-
"excel": `Add LIMIT ${this.config.
|
|
7613
|
-
"csv": `Add LIMIT ${this.config.
|
|
7883
|
+
"mssql": `Use SELECT TOP ${this.config.maxRowsFetched} in every SELECT statement`,
|
|
7884
|
+
"postgres": `Add LIMIT ${this.config.maxRowsFetched} at the end of every query`,
|
|
7885
|
+
"mysql": `Add LIMIT ${this.config.maxRowsFetched} at the end of every query`,
|
|
7886
|
+
"excel": `Add LIMIT ${this.config.maxRowsFetched} at the end of every query`,
|
|
7887
|
+
"csv": `Add LIMIT ${this.config.maxRowsFetched} at the end of every query`
|
|
7614
7888
|
};
|
|
7615
|
-
const rowLimitInstruction = rowLimitSyntax[sourceType] || `Limit results to ${this.config.
|
|
7889
|
+
const rowLimitInstruction = rowLimitSyntax[sourceType] || `Limit results to ${this.config.maxRowsFetched} rows`;
|
|
7616
7890
|
const hasSchemaSearch = !this.options.skipSchemaSearch && !!this.tool.schemaSearchFn;
|
|
7617
7891
|
const schemaTier = this.tool.schemaTier || "";
|
|
7618
7892
|
let schemaSearchInstructions = "";
|
|
@@ -7651,8 +7925,13 @@ Even if a table appears in the detailed schema above, search_schema returns samp
|
|
|
7651
7925
|
FULL_SCHEMA: fullSchema,
|
|
7652
7926
|
DATABASE_RULES: databaseRules,
|
|
7653
7927
|
ROW_LIMIT_SYNTAX: rowLimitInstruction,
|
|
7928
|
+
// NOTE: MAX_ROWS here governs the SQL row cap (the {{MAX_ROWS}} in the
|
|
7929
|
+
// "limit results to N rows" line), so it MUST match the fetch cap that
|
|
7930
|
+
// ROW_LIMIT_SYNTAX states — not the smaller LLM-show cap — or the prompt
|
|
7931
|
+
// contradicts itself ("max 10 rows" + "LIMIT 50") and the LLM picks its
|
|
7932
|
+
// own number. See maxRowsFetched override below.
|
|
7654
7933
|
SCHEMA_SEARCH_INSTRUCTIONS: schemaSearchInstructions,
|
|
7655
|
-
MAX_ROWS: String(this.config.
|
|
7934
|
+
MAX_ROWS: String(this.config.maxRowsFetched),
|
|
7656
7935
|
AGGREGATION_MODE: aggregation,
|
|
7657
7936
|
GLOBAL_KNOWLEDGE_BASE: this.config.globalKnowledgeBase || "No global knowledge base available.",
|
|
7658
7937
|
CURRENT_DATETIME: getCurrentDateTimeForPrompt(),
|
|
@@ -7774,13 +8053,27 @@ Even if a table appears in the detailed schema above, search_schema returns samp
|
|
|
7774
8053
|
// src/userResponse/scripts/script-runner.ts
|
|
7775
8054
|
var import_child_process = require("child_process");
|
|
7776
8055
|
var path5 = __toESM(require("path"));
|
|
8056
|
+
var os = __toESM(require("os"));
|
|
7777
8057
|
|
|
7778
8058
|
// src/userResponse/scripts/script-ipc.ts
|
|
7779
8059
|
function encodeMessage(msg) {
|
|
7780
8060
|
return JSON.stringify(msg) + "\n";
|
|
7781
8061
|
}
|
|
8062
|
+
var LineOverflowError = class extends Error {
|
|
8063
|
+
constructor(buffered, limit) {
|
|
8064
|
+
super(`IPC message exceeded ${limit} bytes without a newline (${buffered} buffered) \u2014 aborting to avoid unbounded memory.`);
|
|
8065
|
+
this.buffered = buffered;
|
|
8066
|
+
this.limit = limit;
|
|
8067
|
+
this.name = "LineOverflowError";
|
|
8068
|
+
}
|
|
8069
|
+
};
|
|
8070
|
+
var DEFAULT_MAX_IPC_BYTES = (() => {
|
|
8071
|
+
const envVal = Number(process.env.SCRIPT_MAX_IPC_BYTES);
|
|
8072
|
+
return Number.isFinite(envVal) && envVal > 0 ? Math.floor(envVal) : 64 * 1024 * 1024;
|
|
8073
|
+
})();
|
|
7782
8074
|
var LineSplitter = class {
|
|
7783
|
-
constructor() {
|
|
8075
|
+
constructor(maxBytes = DEFAULT_MAX_IPC_BYTES) {
|
|
8076
|
+
this.maxBytes = maxBytes;
|
|
7784
8077
|
this.buffer = "";
|
|
7785
8078
|
}
|
|
7786
8079
|
push(chunk) {
|
|
@@ -7792,6 +8085,11 @@ var LineSplitter = class {
|
|
|
7792
8085
|
this.buffer = this.buffer.slice(idx + 1);
|
|
7793
8086
|
if (line.length > 0) lines.push(line);
|
|
7794
8087
|
}
|
|
8088
|
+
if (this.buffer.length > this.maxBytes) {
|
|
8089
|
+
const buffered = this.buffer.length;
|
|
8090
|
+
this.buffer = "";
|
|
8091
|
+
throw new LineOverflowError(buffered, this.maxBytes);
|
|
8092
|
+
}
|
|
7795
8093
|
return lines;
|
|
7796
8094
|
}
|
|
7797
8095
|
/** Flush any remaining partial data (useful on stream close) */
|
|
@@ -7804,7 +8102,57 @@ var LineSplitter = class {
|
|
|
7804
8102
|
};
|
|
7805
8103
|
|
|
7806
8104
|
// src/userResponse/scripts/script-runner.ts
|
|
7807
|
-
var
|
|
8105
|
+
var DEFAULT_SCRIPT_TIMEOUT_MS = 6e4;
|
|
8106
|
+
var SCRIPT_TIMEOUT_MS = (() => {
|
|
8107
|
+
const envVal = Number(process.env.SCRIPT_TIMEOUT_MS);
|
|
8108
|
+
return Number.isFinite(envVal) && envVal > 0 ? envVal : DEFAULT_SCRIPT_TIMEOUT_MS;
|
|
8109
|
+
})();
|
|
8110
|
+
var MAX_CONCURRENT_SCRIPTS = (() => {
|
|
8111
|
+
const envVal = Number(process.env.SCRIPT_MAX_CONCURRENCY);
|
|
8112
|
+
if (Number.isFinite(envVal) && envVal > 0) return Math.floor(envVal);
|
|
8113
|
+
return Math.max(2, Math.min(8, (os.cpus()?.length || 4) - 1));
|
|
8114
|
+
})();
|
|
8115
|
+
var MAX_SCRIPT_QUEUE = (() => {
|
|
8116
|
+
const envVal = Number(process.env.SCRIPT_MAX_QUEUE);
|
|
8117
|
+
if (Number.isFinite(envVal) && envVal >= 0) return Math.floor(envVal);
|
|
8118
|
+
return 100;
|
|
8119
|
+
})();
|
|
8120
|
+
var ScriptCapacityError = class extends Error {
|
|
8121
|
+
constructor(message) {
|
|
8122
|
+
super(message);
|
|
8123
|
+
this.name = "ScriptCapacityError";
|
|
8124
|
+
}
|
|
8125
|
+
};
|
|
8126
|
+
var Semaphore = class {
|
|
8127
|
+
constructor(max, maxQueue) {
|
|
8128
|
+
this.max = max;
|
|
8129
|
+
this.maxQueue = maxQueue;
|
|
8130
|
+
this.active = 0;
|
|
8131
|
+
this.waiters = [];
|
|
8132
|
+
}
|
|
8133
|
+
async acquire() {
|
|
8134
|
+
if (this.active < this.max) {
|
|
8135
|
+
this.active++;
|
|
8136
|
+
return;
|
|
8137
|
+
}
|
|
8138
|
+
if (this.waiters.length >= this.maxQueue) {
|
|
8139
|
+
throw new ScriptCapacityError(
|
|
8140
|
+
`Script execution at capacity (${this.max} running, ${this.waiters.length} queued). Try again shortly.`
|
|
8141
|
+
);
|
|
8142
|
+
}
|
|
8143
|
+
return new Promise((resolve2) => this.waiters.push(resolve2));
|
|
8144
|
+
}
|
|
8145
|
+
release() {
|
|
8146
|
+
const next = this.waiters.shift();
|
|
8147
|
+
if (next) next();
|
|
8148
|
+
else this.active--;
|
|
8149
|
+
}
|
|
8150
|
+
get queued() {
|
|
8151
|
+
return this.waiters.length;
|
|
8152
|
+
}
|
|
8153
|
+
};
|
|
8154
|
+
var scriptSemaphore = new Semaphore(MAX_CONCURRENT_SCRIPTS, MAX_SCRIPT_QUEUE);
|
|
8155
|
+
logger.info(`[ScriptRunner] Concurrency cap: ${MAX_CONCURRENT_SCRIPTS} concurrent, queue ${MAX_SCRIPT_QUEUE}`);
|
|
7808
8156
|
var tsxBinaryPath = null;
|
|
7809
8157
|
function resolveTsxBinary() {
|
|
7810
8158
|
if (tsxBinaryPath) return tsxBinaryPath;
|
|
@@ -7827,6 +8175,22 @@ async function runScript(recipe, scriptPath, params, options) {
|
|
|
7827
8175
|
const resolvedParams = coerceParams(recipe, applyDefaults(recipe, params));
|
|
7828
8176
|
const paramSchema = buildParamSchema(recipe);
|
|
7829
8177
|
logger.info(`[ScriptRunner] Executing "${recipe.name}" (${recipe.id}) with params: ${JSON.stringify(resolvedParams)}`);
|
|
8178
|
+
try {
|
|
8179
|
+
if (scriptSemaphore.queued > 0) {
|
|
8180
|
+
logger.info(`[ScriptRunner] "${recipe.name}" queued (${scriptSemaphore.queued} ahead)`);
|
|
8181
|
+
}
|
|
8182
|
+
await scriptSemaphore.acquire();
|
|
8183
|
+
} catch (capErr) {
|
|
8184
|
+
const msg = capErr instanceof Error ? capErr.message : String(capErr);
|
|
8185
|
+
logger.warn(`[ScriptRunner] "${recipe.name}" shed: ${msg}`);
|
|
8186
|
+
return {
|
|
8187
|
+
success: false,
|
|
8188
|
+
data: [],
|
|
8189
|
+
executedQueries: [],
|
|
8190
|
+
error: msg,
|
|
8191
|
+
executionTimeMs: Date.now() - startedAt
|
|
8192
|
+
};
|
|
8193
|
+
}
|
|
7830
8194
|
try {
|
|
7831
8195
|
const result = await executeInSubprocess(
|
|
7832
8196
|
scriptPath,
|
|
@@ -7868,6 +8232,8 @@ async function runScript(recipe, scriptPath, params, options) {
|
|
|
7868
8232
|
error: msg,
|
|
7869
8233
|
executionTimeMs: totalMs
|
|
7870
8234
|
};
|
|
8235
|
+
} finally {
|
|
8236
|
+
scriptSemaphore.release();
|
|
7871
8237
|
}
|
|
7872
8238
|
}
|
|
7873
8239
|
function executeInSubprocess(scriptPath, params, paramSchema, externalTools, streamBuffer, timeoutMs) {
|
|
@@ -7880,6 +8246,15 @@ function executeInSubprocess(scriptPath, params, paramSchema, externalTools, str
|
|
|
7880
8246
|
[tsxBin, bootstrap, scriptPath],
|
|
7881
8247
|
{
|
|
7882
8248
|
stdio: ["pipe", "pipe", "pipe"],
|
|
8249
|
+
// `detached: true` makes the child a process-group LEADER. tsx 4.x
|
|
8250
|
+
// re-execs node to run the user script, so the script is a GRANDCHILD;
|
|
8251
|
+
// a plain `child.kill()` would hit only the tsx wrapper and orphan the
|
|
8252
|
+
// grandchild (a runaway/looping script that outlives the timeout — #4).
|
|
8253
|
+
// With its own group we can SIGKILL the whole tree via `-pid` in
|
|
8254
|
+
// cleanup(). We deliberately do NOT unref() — the parent still tracks
|
|
8255
|
+
// the child's exit and pipes normally. (POSIX; Windows falls back to
|
|
8256
|
+
// a direct kill in cleanup().)
|
|
8257
|
+
detached: process.platform !== "win32",
|
|
7883
8258
|
env: {
|
|
7884
8259
|
...process.env,
|
|
7885
8260
|
// Keep the child quiet about dotenv etc.
|
|
@@ -7887,22 +8262,23 @@ function executeInSubprocess(scriptPath, params, paramSchema, externalTools, str
|
|
|
7887
8262
|
}
|
|
7888
8263
|
}
|
|
7889
8264
|
);
|
|
7890
|
-
const toolsById = /* @__PURE__ */ new Map();
|
|
7891
|
-
const toolsByName = /* @__PURE__ */ new Map();
|
|
7892
|
-
for (const t of externalTools) {
|
|
7893
|
-
toolsById.set(t.id, t);
|
|
7894
|
-
toolsByName.set(t.name.toLowerCase(), t);
|
|
7895
|
-
}
|
|
7896
8265
|
let resolved = false;
|
|
7897
8266
|
let executedQueries = [];
|
|
7898
8267
|
let stderrBuffer = "";
|
|
7899
8268
|
const cleanup = () => {
|
|
7900
|
-
if (
|
|
8269
|
+
if (child.killed) return;
|
|
8270
|
+
const pid = child.pid;
|
|
8271
|
+
if (pid && process.platform !== "win32") {
|
|
7901
8272
|
try {
|
|
7902
|
-
|
|
8273
|
+
process.kill(-pid, "SIGKILL");
|
|
8274
|
+
return;
|
|
7903
8275
|
} catch {
|
|
7904
8276
|
}
|
|
7905
8277
|
}
|
|
8278
|
+
try {
|
|
8279
|
+
child.kill("SIGKILL");
|
|
8280
|
+
} catch {
|
|
8281
|
+
}
|
|
7906
8282
|
};
|
|
7907
8283
|
const finish = (r) => {
|
|
7908
8284
|
if (resolved) return;
|
|
@@ -7922,7 +8298,19 @@ function executeInSubprocess(scriptPath, params, paramSchema, externalTools, str
|
|
|
7922
8298
|
const stdoutSplitter = new LineSplitter();
|
|
7923
8299
|
child.stdout.setEncoding("utf-8");
|
|
7924
8300
|
child.stdout.on("data", (chunk) => {
|
|
7925
|
-
|
|
8301
|
+
let lines;
|
|
8302
|
+
try {
|
|
8303
|
+
lines = stdoutSplitter.push(chunk);
|
|
8304
|
+
} catch (err) {
|
|
8305
|
+
finish({
|
|
8306
|
+
kind: "err",
|
|
8307
|
+
phase: "ipc",
|
|
8308
|
+
message: err instanceof Error ? err.message : String(err),
|
|
8309
|
+
executedQueries
|
|
8310
|
+
});
|
|
8311
|
+
return;
|
|
8312
|
+
}
|
|
8313
|
+
for (const line of lines) {
|
|
7926
8314
|
let msg;
|
|
7927
8315
|
try {
|
|
7928
8316
|
msg = JSON.parse(line);
|
|
@@ -7966,9 +8354,6 @@ function executeInSubprocess(scriptPath, params, paramSchema, externalTools, str
|
|
|
7966
8354
|
child.stdin.write(init);
|
|
7967
8355
|
function handleChildMessage(msg) {
|
|
7968
8356
|
switch (msg.type) {
|
|
7969
|
-
case "query":
|
|
7970
|
-
void handleQuery8(msg.id, msg.toolId, msg.sql);
|
|
7971
|
-
return;
|
|
7972
8357
|
case "stream":
|
|
7973
8358
|
if (streamBuffer?.hasCallback()) {
|
|
7974
8359
|
streamBuffer.write(msg.chunk);
|
|
@@ -7994,117 +8379,21 @@ ${msg.stack}` : ""),
|
|
|
7994
8379
|
return;
|
|
7995
8380
|
}
|
|
7996
8381
|
}
|
|
7997
|
-
async function handleQuery8(id, toolId, sql) {
|
|
7998
|
-
const tool = resolveAuthorizedTool(toolId, toolsById, toolsByName);
|
|
7999
|
-
if (!tool) {
|
|
8000
|
-
sendToChild({
|
|
8001
|
-
type: "query_error",
|
|
8002
|
-
id,
|
|
8003
|
-
error: `Tool "${toolId}" is not authorized for this request.`
|
|
8004
|
-
});
|
|
8005
|
-
return;
|
|
8006
|
-
}
|
|
8007
|
-
const startedAt = Date.now();
|
|
8008
|
-
streamQueryStart(streamBuffer, tool.name, sql);
|
|
8009
|
-
try {
|
|
8010
|
-
const result = await tool.fn({ sql });
|
|
8011
|
-
const elapsed = Date.now() - startedAt;
|
|
8012
|
-
if (result && result.error) {
|
|
8013
|
-
const errMsg = typeof result.error === "string" ? result.error : JSON.stringify(result.error);
|
|
8014
|
-
streamQueryDone(streamBuffer, elapsed);
|
|
8015
|
-
streamQueryError(streamBuffer, tool.name, errMsg);
|
|
8016
|
-
sendToChild({ type: "query_error", id, error: errMsg });
|
|
8017
|
-
return;
|
|
8018
|
-
}
|
|
8019
|
-
const data = result?.data || [];
|
|
8020
|
-
const count = result?.count ?? data.length;
|
|
8021
|
-
const totalCount = result?.metadata?.totalCount;
|
|
8022
|
-
streamQueryDone(streamBuffer, elapsed);
|
|
8023
|
-
streamQuerySuccess(streamBuffer, tool.name, data.length, totalCount ?? count);
|
|
8024
|
-
streamDataPreview(streamBuffer, data);
|
|
8025
|
-
sendToChild({
|
|
8026
|
-
type: "query_result",
|
|
8027
|
-
id,
|
|
8028
|
-
data,
|
|
8029
|
-
count,
|
|
8030
|
-
metadata: { totalCount, executionTimeMs: elapsed }
|
|
8031
|
-
});
|
|
8032
|
-
} catch (err) {
|
|
8033
|
-
const elapsed = Date.now() - startedAt;
|
|
8034
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
8035
|
-
streamQueryDone(streamBuffer, elapsed);
|
|
8036
|
-
streamQueryError(streamBuffer, tool.name, errMsg);
|
|
8037
|
-
sendToChild({ type: "query_error", id, error: errMsg });
|
|
8038
|
-
}
|
|
8039
|
-
}
|
|
8040
|
-
function sendToChild(msg) {
|
|
8041
|
-
try {
|
|
8042
|
-
child.stdin.write(encodeMessage(msg));
|
|
8043
|
-
} catch (err) {
|
|
8044
|
-
logger.warn(`[ScriptRunner] Failed to write to child stdin: ${err.message}`);
|
|
8045
|
-
}
|
|
8046
|
-
}
|
|
8047
8382
|
});
|
|
8048
8383
|
}
|
|
8049
|
-
function resolveAuthorizedTool(toolId, byId, byName) {
|
|
8050
|
-
const exact = byId.get(toolId);
|
|
8051
|
-
if (exact) return exact;
|
|
8052
|
-
const exactName = byName.get(toolId.toLowerCase());
|
|
8053
|
-
if (exactName) return exactName;
|
|
8054
|
-
for (const [id, tool] of byId) {
|
|
8055
|
-
if (id.includes(toolId) || toolId.includes(id)) return tool;
|
|
8056
|
-
}
|
|
8057
|
-
const lower = toolId.toLowerCase();
|
|
8058
|
-
for (const [name, tool] of byName) {
|
|
8059
|
-
if (name.includes(lower) || lower.includes(name)) return tool;
|
|
8060
|
-
}
|
|
8061
|
-
return null;
|
|
8062
|
-
}
|
|
8063
|
-
function streamQueryStart(sb, sourceName, sql) {
|
|
8064
|
-
if (!sb?.hasCallback()) return;
|
|
8065
|
-
sb.write(`
|
|
8066
|
-
\u{1F4DD} **Querying ${sourceName}:**
|
|
8067
|
-
\`\`\`sql
|
|
8068
|
-
${sql}
|
|
8069
|
-
\`\`\`
|
|
8070
|
-
|
|
8071
|
-
`);
|
|
8072
|
-
sb.write(`__QUERY_TIMER_START_Executing query__`);
|
|
8073
|
-
}
|
|
8074
|
-
function streamQueryDone(sb, ms) {
|
|
8075
|
-
if (!sb?.hasCallback()) return;
|
|
8076
|
-
sb.write(`__QUERY_TIMER_DONE_${(ms / 1e3).toFixed(1)}__
|
|
8077
|
-
|
|
8078
|
-
`);
|
|
8079
|
-
}
|
|
8080
|
-
function streamQuerySuccess(sb, sourceName, rows, total) {
|
|
8081
|
-
if (!sb?.hasCallback()) return;
|
|
8082
|
-
const totalInfo = total > rows ? ` of ${total} total` : "";
|
|
8083
|
-
sb.write(`\u2705 **${rows} rows${totalInfo} from ${sourceName}**
|
|
8084
|
-
|
|
8085
|
-
`);
|
|
8086
|
-
}
|
|
8087
|
-
function streamQueryError(sb, sourceName, msg) {
|
|
8088
|
-
if (!sb?.hasCallback()) return;
|
|
8089
|
-
sb.write(`\u274C **Query failed on ${sourceName}:** ${msg}
|
|
8090
|
-
|
|
8091
|
-
`);
|
|
8092
|
-
}
|
|
8093
|
-
function streamDataPreview(sb, data) {
|
|
8094
|
-
if (!sb?.hasCallback() || data.length === 0) return;
|
|
8095
|
-
try {
|
|
8096
|
-
sb.write(`<DataTable>${JSON.stringify(data.slice(0, 10))}</DataTable>
|
|
8097
|
-
|
|
8098
|
-
`);
|
|
8099
|
-
} catch {
|
|
8100
|
-
sb.write(`_Data preview not available_
|
|
8101
|
-
|
|
8102
|
-
`);
|
|
8103
|
-
}
|
|
8104
|
-
}
|
|
8105
8384
|
function withSynthesizedFinalData(queries, finalData, finalCount) {
|
|
8106
8385
|
if (!finalData || finalData.length === 0) return queries;
|
|
8107
|
-
if (!queries || queries.length === 0)
|
|
8386
|
+
if (!queries || queries.length === 0) {
|
|
8387
|
+
return [{
|
|
8388
|
+
sourceId: "computed:_final",
|
|
8389
|
+
sourceName: "Script final dataset",
|
|
8390
|
+
sql: "-- script final returned data (runTool / pure JS, no ctx.query)",
|
|
8391
|
+
data: finalData,
|
|
8392
|
+
count: finalCount,
|
|
8393
|
+
executionTimeMs: 0,
|
|
8394
|
+
virtual: true
|
|
8395
|
+
}];
|
|
8396
|
+
}
|
|
8108
8397
|
const firstSqlQuery = queries.find((q) => !q.virtual);
|
|
8109
8398
|
const rawSample = firstSqlQuery?.data?.[0];
|
|
8110
8399
|
const finalSample = finalData[0];
|
|
@@ -8244,10 +8533,11 @@ var EXECUTE_SCRIPT_TOOL_DEF = {
|
|
|
8244
8533
|
};
|
|
8245
8534
|
var MAX_SCRIPT_ATTEMPTS = 3;
|
|
8246
8535
|
var MainAgent = class {
|
|
8247
|
-
constructor(externalTools, config, scriptStore, turnId, streamBuffer) {
|
|
8536
|
+
constructor(externalTools, config, scriptStore, turnId, streamBuffer, workflows = []) {
|
|
8248
8537
|
this.createdFromPrompt = "";
|
|
8249
8538
|
this.scriptState = { recipeId: null, attempts: 0, lastSuccessfulResult: null };
|
|
8250
8539
|
this.externalTools = externalTools;
|
|
8540
|
+
this.workflows = workflows;
|
|
8251
8541
|
this.config = config;
|
|
8252
8542
|
this.streamBuffer = streamBuffer || new StreamBuffer();
|
|
8253
8543
|
this.scriptStore = scriptStore ?? null;
|
|
@@ -8273,22 +8563,31 @@ var MainAgent = class {
|
|
|
8273
8563
|
this.createdFromPrompt = userPrompt;
|
|
8274
8564
|
const sourceTools = this.externalTools.filter((t) => t.toolType !== "direct");
|
|
8275
8565
|
const directTools = this.externalTools.filter((t) => t.toolType === "direct");
|
|
8276
|
-
logger.info(`[MainAgent] ${sourceTools.length} source tool(s), ${directTools.length} direct tool(s)`);
|
|
8566
|
+
logger.info(`[MainAgent] ${sourceTools.length} source tool(s), ${directTools.length} direct tool(s), ${this.workflows.length} workflow(s)`);
|
|
8277
8567
|
const summaries = buildSourceSummaries(sourceTools);
|
|
8278
|
-
const systemPrompt = await this.buildSystemPrompt(summaries, directTools, conversationHistory);
|
|
8568
|
+
const systemPrompt = await this.buildSystemPrompt(summaries, directTools, this.workflows, conversationHistory);
|
|
8279
8569
|
logger.logLLMPrompt("mainAgent", "system", extractPromptText(systemPrompt));
|
|
8280
8570
|
logger.logLLMPrompt("mainAgent", "user", userPrompt);
|
|
8281
8571
|
const sourceToolDefs = this.buildSourceToolDefinitions(summaries);
|
|
8282
8572
|
const directToolDefs = this.buildDirectToolDefinitions(directTools);
|
|
8573
|
+
const workflowToolDefs = this.buildWorkflowToolDefinitions(this.workflows);
|
|
8283
8574
|
const tools = [
|
|
8284
8575
|
...sourceToolDefs,
|
|
8285
8576
|
...directToolDefs,
|
|
8577
|
+
...workflowToolDefs,
|
|
8286
8578
|
...this.scriptingEnabled ? [WRITE_SCRIPT_TOOL_DEF, EXECUTE_SCRIPT_TOOL_DEF] : []
|
|
8287
8579
|
];
|
|
8288
8580
|
const sourceResults = [];
|
|
8289
8581
|
const executedTools = [];
|
|
8290
8582
|
let sourceCallCounter = 0;
|
|
8583
|
+
let selectedWorkflow;
|
|
8291
8584
|
const toolHandler = async (toolName, toolInput) => {
|
|
8585
|
+
const workflow = this.workflows.find((w) => w.id === toolName);
|
|
8586
|
+
if (workflow) {
|
|
8587
|
+
return this.handleWorkflow(workflow, toolInput, (w) => {
|
|
8588
|
+
selectedWorkflow = w;
|
|
8589
|
+
});
|
|
8590
|
+
}
|
|
8292
8591
|
if (toolName === "write_script") {
|
|
8293
8592
|
if (!this.scriptingEnabled) return "Scripting is not enabled for this request.";
|
|
8294
8593
|
return this.handleWriteScript(toolInput);
|
|
@@ -8353,24 +8652,34 @@ var MainAgent = class {
|
|
|
8353
8652
|
this.config.maxIterations
|
|
8354
8653
|
);
|
|
8355
8654
|
const totalTime = Date.now() - startTime;
|
|
8356
|
-
logger.info(
|
|
8357
|
-
|
|
8655
|
+
logger.info(
|
|
8656
|
+
`[MainAgent] Complete | ${sourceResults.length} source queries, ${executedTools.length} successful${selectedWorkflow ? ` | workflow="${selectedWorkflow.name}"` : ""} | ${totalTime}ms`
|
|
8657
|
+
);
|
|
8658
|
+
const savedScript = await this.buildSavedScript();
|
|
8358
8659
|
if (savedScript) {
|
|
8359
8660
|
logger.info(`[MainAgent] Script authored: "${savedScript.name}" (${savedScript.parameters.length} params, ${this.scriptState.attempts} attempt${this.scriptState.attempts === 1 ? "" : "s"})`);
|
|
8360
8661
|
} else if (sourceResults.some((r) => r.success)) {
|
|
8361
8662
|
logger.warn(`[MainAgent] Source query succeeded but no script was authored \u2014 LLM skipped write_script/execute_script. Prompt policy may need tightening.`);
|
|
8362
8663
|
}
|
|
8664
|
+
if (!savedScript && this.scriptStore && this.scriptState.recipeId) {
|
|
8665
|
+
try {
|
|
8666
|
+
await this.scriptStore.discardDraft(this.scriptState.recipeId);
|
|
8667
|
+
} catch (err) {
|
|
8668
|
+
logger.warn(`[MainAgent] Failed to discard failed draft ${this.scriptState.recipeId}: ${err}`);
|
|
8669
|
+
}
|
|
8670
|
+
}
|
|
8363
8671
|
return {
|
|
8364
8672
|
text,
|
|
8365
8673
|
executedTools,
|
|
8366
8674
|
sourceResults,
|
|
8367
|
-
savedScript
|
|
8675
|
+
savedScript,
|
|
8676
|
+
workflow: selectedWorkflow
|
|
8368
8677
|
};
|
|
8369
8678
|
}
|
|
8370
8679
|
// ============================================
|
|
8371
8680
|
// Script-authoring tool handlers
|
|
8372
8681
|
// ============================================
|
|
8373
|
-
handleWriteScript(toolInput) {
|
|
8682
|
+
async handleWriteScript(toolInput) {
|
|
8374
8683
|
const scriptBody = typeof toolInput?.scriptBody === "string" ? toolInput.scriptBody.trim() : "";
|
|
8375
8684
|
if (!scriptBody) {
|
|
8376
8685
|
return "write_script requires a non-empty `scriptBody` starting with `export async function getData(ctx, params)`.";
|
|
@@ -8379,7 +8688,7 @@ var MainAgent = class {
|
|
|
8379
8688
|
const intentDescription = toolInput.description || "";
|
|
8380
8689
|
const tags = Array.isArray(toolInput.tags) ? toolInput.tags : [];
|
|
8381
8690
|
const parameters = Array.isArray(toolInput.parameters) ? this.normalizeParameterList(toolInput.parameters) : [];
|
|
8382
|
-
const draft = this.scriptStore.saveDraft({
|
|
8691
|
+
const draft = await this.scriptStore.saveDraft({
|
|
8383
8692
|
recipeId: this.scriptState.recipeId ?? void 0,
|
|
8384
8693
|
turnId: this.turnId,
|
|
8385
8694
|
name,
|
|
@@ -8400,7 +8709,7 @@ var MainAgent = class {
|
|
|
8400
8709
|
if (!this.scriptState.recipeId) {
|
|
8401
8710
|
return "No draft found. Call write_script first with the script you want to verify.";
|
|
8402
8711
|
}
|
|
8403
|
-
const draftRecipe = this.scriptStore.get(this.scriptState.recipeId);
|
|
8712
|
+
const draftRecipe = await this.scriptStore.get(this.scriptState.recipeId);
|
|
8404
8713
|
if (!draftRecipe) {
|
|
8405
8714
|
logger.error(`[MainAgent] execute_script: draft "${this.scriptState.recipeId}" missing from store`);
|
|
8406
8715
|
return "Draft was lost from the store. Call write_script again.";
|
|
@@ -8418,10 +8727,17 @@ var MainAgent = class {
|
|
|
8418
8727
|
);
|
|
8419
8728
|
if (result.success) {
|
|
8420
8729
|
this.scriptState.lastSuccessfulResult = result;
|
|
8730
|
+
const totalRows = result.data.length;
|
|
8421
8731
|
const summary2 = {
|
|
8422
8732
|
ok: true,
|
|
8423
8733
|
executionTimeMs: result.executionTimeMs,
|
|
8734
|
+
totalRows,
|
|
8735
|
+
// dataSummary is computed over ALL rows — use it for any range/count/
|
|
8736
|
+
// min/max/total/"all-or-none" claim. sampleRows is just the first few
|
|
8737
|
+
// rows (head of result, NOT representative) for illustrating shape.
|
|
8738
|
+
dataSummary: summarizeRows(result.data),
|
|
8424
8739
|
sampleRows: result.data.slice(0, 5),
|
|
8740
|
+
analysisGuidance: `Showing ${Math.min(totalRows, 5)} of ${totalRows} rows. The sample is the HEAD of the result and is NOT representative \u2014 do not generalize from it. Base every number, range, count, and "all/none" statement on dataSummary, which is computed over all ${totalRows} rows. The full result is rendered for the user by the on-screen component.`,
|
|
8425
8741
|
executedQueries: result.executedQueries.map((q) => ({
|
|
8426
8742
|
sourceId: q.sourceId,
|
|
8427
8743
|
sourceName: q.sourceName,
|
|
@@ -8429,10 +8745,10 @@ var MainAgent = class {
|
|
|
8429
8745
|
sample: q.data.slice(0, 2)
|
|
8430
8746
|
}))
|
|
8431
8747
|
};
|
|
8432
|
-
logger.info(`[MainAgent] execute_script: ok \u2014 ${
|
|
8748
|
+
logger.info(`[MainAgent] execute_script: ok \u2014 ${totalRows} rows in ${result.executionTimeMs}ms (attempt ${this.scriptState.attempts})`);
|
|
8433
8749
|
return JSON.stringify(summary2, null, 2);
|
|
8434
8750
|
}
|
|
8435
|
-
this.scriptStore.recordDraftError(draftRecipe.id, {
|
|
8751
|
+
await this.scriptStore.recordDraftError(draftRecipe.id, {
|
|
8436
8752
|
phase: result.errorPhase ?? "runtime",
|
|
8437
8753
|
message: result.error ?? "Unknown error",
|
|
8438
8754
|
attempt: this.scriptState.attempts
|
|
@@ -8458,11 +8774,11 @@ var MainAgent = class {
|
|
|
8458
8774
|
* `ScriptStore.promoteToVerified()`. Only returned when a verified
|
|
8459
8775
|
* successful execution is on record.
|
|
8460
8776
|
*/
|
|
8461
|
-
buildSavedScript() {
|
|
8777
|
+
async buildSavedScript() {
|
|
8462
8778
|
const { recipeId, lastSuccessfulResult } = this.scriptState;
|
|
8463
8779
|
if (!recipeId || !lastSuccessfulResult) return void 0;
|
|
8464
8780
|
if (!this.scriptStore) return void 0;
|
|
8465
|
-
const draft = this.scriptStore.get(recipeId);
|
|
8781
|
+
const draft = await this.scriptStore.get(recipeId);
|
|
8466
8782
|
if (!draft) return void 0;
|
|
8467
8783
|
const executedQueries = lastSuccessfulResult.executedQueries.map((q) => ({
|
|
8468
8784
|
sourceId: q.sourceId,
|
|
@@ -8589,6 +8905,7 @@ ${lines.join("\n")}`;
|
|
|
8589
8905
|
maxRows: 5,
|
|
8590
8906
|
maxCharsPerField: 200
|
|
8591
8907
|
});
|
|
8908
|
+
const sampleData = Array.isArray(resultData) ? resultData.slice(0, TOOL_TRACKING_SAMPLE_ROWS) : Array.isArray(formattedResult.data) ? formattedResult.data.slice(0, TOOL_TRACKING_SAMPLE_ROWS) : [];
|
|
8592
8909
|
executedTools.push({
|
|
8593
8910
|
id: tool.id,
|
|
8594
8911
|
name: tool.name,
|
|
@@ -8597,7 +8914,7 @@ ${lines.join("\n")}`;
|
|
|
8597
8914
|
_totalRecords: result.totalItems || result.count || rowCount,
|
|
8598
8915
|
_recordsShown: rowCount,
|
|
8599
8916
|
_metadata: result.metadata,
|
|
8600
|
-
_sampleData:
|
|
8917
|
+
_sampleData: sampleData
|
|
8601
8918
|
},
|
|
8602
8919
|
outputSchema: tool.outputSchema
|
|
8603
8920
|
});
|
|
@@ -8623,9 +8940,10 @@ ${formatted}`;
|
|
|
8623
8940
|
// System Prompt
|
|
8624
8941
|
// ============================================
|
|
8625
8942
|
/**
|
|
8626
|
-
* Build the main agent's system prompt with source summaries
|
|
8943
|
+
* Build the main agent's system prompt with source summaries, direct tool descriptions,
|
|
8944
|
+
* and workflow component descriptions.
|
|
8627
8945
|
*/
|
|
8628
|
-
async buildSystemPrompt(summaries, directTools, conversationHistory) {
|
|
8946
|
+
async buildSystemPrompt(summaries, directTools, workflows, conversationHistory) {
|
|
8629
8947
|
const summariesText = formatSummariesForPrompt(summaries);
|
|
8630
8948
|
const maxSourceCalls = Math.max(2, this.config.maxIterations - 2);
|
|
8631
8949
|
let directToolsText = "";
|
|
@@ -8637,10 +8955,28 @@ ${formatted}`;
|
|
|
8637
8955
|
${t.description || "No description"}${paramList ? "\n Parameters:\n" + paramList : ""}`;
|
|
8638
8956
|
}).join("\n\n");
|
|
8639
8957
|
}
|
|
8958
|
+
let workflowsText = "";
|
|
8959
|
+
if (workflows.length > 0) {
|
|
8960
|
+
workflowsText = workflows.map((w, idx) => {
|
|
8961
|
+
const propLines = Object.entries(w.propsSchema || {}).map(([k, v]) => ` - ${k}: ${v}`).join("\n");
|
|
8962
|
+
return [
|
|
8963
|
+
`${idx + 1}. **${w.name}** (tool: ${w.id})`,
|
|
8964
|
+
` ${w.description}`,
|
|
8965
|
+
` When to use: ${w.whenToUse}`,
|
|
8966
|
+
propLines ? ` Props:
|
|
8967
|
+
${propLines}` : ""
|
|
8968
|
+
].filter(Boolean).join("\n");
|
|
8969
|
+
}).join("\n\n");
|
|
8970
|
+
} else {
|
|
8971
|
+
workflowsText = "No workflow components registered for this project.";
|
|
8972
|
+
}
|
|
8640
8973
|
const prompts = await promptLoader.loadPrompts("agent-main", {
|
|
8641
8974
|
SOURCE_SUMMARIES: summariesText,
|
|
8642
8975
|
DIRECT_TOOLS: directToolsText,
|
|
8643
|
-
|
|
8976
|
+
WORKFLOW_COMPONENTS: workflowsText,
|
|
8977
|
+
MAX_ROWS: String(MAIN_AGENT_COMPLETE_ROWS),
|
|
8978
|
+
MAX_ROWS_RENDERED: String(MAX_ROWS_RENDERED),
|
|
8979
|
+
MAX_ROWS_FETCHED: String(MAX_ROWS_FETCHED),
|
|
8644
8980
|
MAX_SOURCE_CALLS: String(maxSourceCalls),
|
|
8645
8981
|
GLOBAL_KNOWLEDGE_BASE: this.config.globalKnowledgeBase || "No global knowledge base available.",
|
|
8646
8982
|
CURRENT_DATETIME: getCurrentDateTimeForPrompt(),
|
|
@@ -8726,6 +9062,70 @@ ${formatted}`;
|
|
|
8726
9062
|
});
|
|
8727
9063
|
}
|
|
8728
9064
|
// ============================================
|
|
9065
|
+
// Workflow Handling
|
|
9066
|
+
// ============================================
|
|
9067
|
+
/**
|
|
9068
|
+
* Capture a workflow selection. We do NOT execute anything — the LLM has
|
|
9069
|
+
* already extracted the props it wants the workflow rendered with. We
|
|
9070
|
+
* record the selection (via the capture callback) and return a short
|
|
9071
|
+
* acknowledgement so the LLM ends its turn cleanly without writing
|
|
9072
|
+
* analysis text or calling more tools.
|
|
9073
|
+
*/
|
|
9074
|
+
async handleWorkflow(workflow, toolInput, capture) {
|
|
9075
|
+
const props = { ...workflow.defaultProps || {}, ...toolInput || {} };
|
|
9076
|
+
logger.info(
|
|
9077
|
+
`[MainAgent] Workflow selected: "${workflow.name}" | props: ${JSON.stringify(props).substring(0, 200)}`
|
|
9078
|
+
);
|
|
9079
|
+
if (this.streamBuffer.hasCallback()) {
|
|
9080
|
+
this.streamBuffer.write(`
|
|
9081
|
+
|
|
9082
|
+
\u{1F9ED} **Launching ${workflow.name} workflow...**
|
|
9083
|
+
|
|
9084
|
+
`);
|
|
9085
|
+
await streamDelay();
|
|
9086
|
+
}
|
|
9087
|
+
capture({ name: workflow.name, props });
|
|
9088
|
+
return `\u2705 Workflow "${workflow.name}" selected. The UI will render now \u2014 do NOT write analysis text or call any other tools. End your turn.`;
|
|
9089
|
+
}
|
|
9090
|
+
/**
|
|
9091
|
+
* Build LLM tool definitions for workflow components. The workflow's
|
|
9092
|
+
* propsSchema becomes the tool's input_schema so the LLM extracts props
|
|
9093
|
+
* directly from the prompt — same mechanic as direct tools.
|
|
9094
|
+
*/
|
|
9095
|
+
buildWorkflowToolDefinitions(workflows) {
|
|
9096
|
+
return workflows.map((workflow) => {
|
|
9097
|
+
const properties = {};
|
|
9098
|
+
const required = [];
|
|
9099
|
+
Object.entries(workflow.propsSchema || {}).forEach(([key, typeOrValue]) => {
|
|
9100
|
+
const valueStr = String(typeOrValue).toLowerCase();
|
|
9101
|
+
let schemaType = "string";
|
|
9102
|
+
const typeMatch = valueStr.match(/^(string|number|integer|boolean|array|object)\b/);
|
|
9103
|
+
if (typeMatch) {
|
|
9104
|
+
schemaType = typeMatch[1];
|
|
9105
|
+
}
|
|
9106
|
+
const isOptional = valueStr.includes("(optional)") || valueStr.includes("optional");
|
|
9107
|
+
const description = typeof typeOrValue === "string" ? typeOrValue : `Prop: ${key}`;
|
|
9108
|
+
if (schemaType === "array") {
|
|
9109
|
+
properties[key] = { type: "array", items: {}, description };
|
|
9110
|
+
} else if (schemaType === "object") {
|
|
9111
|
+
properties[key] = { type: "object", description };
|
|
9112
|
+
} else {
|
|
9113
|
+
properties[key] = { type: schemaType, description };
|
|
9114
|
+
}
|
|
9115
|
+
if (!isOptional) required.push(key);
|
|
9116
|
+
});
|
|
9117
|
+
return {
|
|
9118
|
+
name: workflow.id,
|
|
9119
|
+
description: `[WORKFLOW] ${workflow.description} \u2014 When to use: ${workflow.whenToUse}`,
|
|
9120
|
+
input_schema: {
|
|
9121
|
+
type: "object",
|
|
9122
|
+
properties,
|
|
9123
|
+
required: required.length > 0 ? required : void 0
|
|
9124
|
+
}
|
|
9125
|
+
};
|
|
9126
|
+
});
|
|
9127
|
+
}
|
|
9128
|
+
// ============================================
|
|
8729
9129
|
// Format Result for Main Agent
|
|
8730
9130
|
// ============================================
|
|
8731
9131
|
/**
|
|
@@ -8735,7 +9135,7 @@ ${formatted}`;
|
|
|
8735
9135
|
if (!result.success) {
|
|
8736
9136
|
return `Data source "${result.sourceName}" could not fulfill the request: ${result.error}. Try rephrasing your intent or querying a different source.`;
|
|
8737
9137
|
}
|
|
8738
|
-
const {
|
|
9138
|
+
const { metadata } = result;
|
|
8739
9139
|
let output = `## Data from "${result.sourceName}"
|
|
8740
9140
|
`;
|
|
8741
9141
|
output += `Rows returned: ${metadata.rowsReturned}`;
|
|
@@ -8746,33 +9146,68 @@ ${formatted}`;
|
|
|
8746
9146
|
Execution time: ${metadata.executionTimeMs}ms
|
|
8747
9147
|
|
|
8748
9148
|
`;
|
|
8749
|
-
|
|
9149
|
+
const successfulTools = result.allExecutedTools && result.allExecutedTools.length > 0 ? result.allExecutedTools : result.executedTool ? [result.executedTool] : [];
|
|
9150
|
+
const queries = successfulTools.map((t) => ({
|
|
9151
|
+
sql: t?.params?.sql || t?.params?.query,
|
|
9152
|
+
rows: t?.result?._mainAgentRows ?? t?.result?._sampleData ?? [],
|
|
9153
|
+
summary: t?.result?._summary,
|
|
9154
|
+
total: t?.result?._totalRecords,
|
|
9155
|
+
shown: t?.result?._recordsShown
|
|
9156
|
+
})).filter((q) => Boolean(q.sql));
|
|
9157
|
+
if (queries.length === 0) {
|
|
8750
9158
|
output += "No data returned.";
|
|
8751
9159
|
return output;
|
|
8752
9160
|
}
|
|
8753
|
-
const
|
|
8754
|
-
|
|
8755
|
-
const truncatedRow = {};
|
|
9161
|
+
const truncRows = (rowsArr) => rowsArr.map((row) => {
|
|
9162
|
+
const out = {};
|
|
8756
9163
|
for (const [key, value] of Object.entries(row)) {
|
|
8757
|
-
|
|
8758
|
-
truncatedRow[key] = value.substring(0, 200) + "...";
|
|
8759
|
-
} else {
|
|
8760
|
-
truncatedRow[key] = value;
|
|
8761
|
-
}
|
|
9164
|
+
out[key] = typeof value === "string" && value.length > 200 ? safeTruncate(value, 200) + "..." : value;
|
|
8762
9165
|
}
|
|
8763
|
-
return
|
|
9166
|
+
return out;
|
|
8764
9167
|
});
|
|
8765
|
-
const
|
|
8766
|
-
|
|
8767
|
-
|
|
9168
|
+
const jsonBlock = (obj) => {
|
|
9169
|
+
try {
|
|
9170
|
+
return "```json\n" + JSON.stringify(obj, null, 2) + "\n```\n";
|
|
9171
|
+
} catch {
|
|
9172
|
+
return "```\n[Data could not be serialized]\n```\n";
|
|
9173
|
+
}
|
|
9174
|
+
};
|
|
9175
|
+
const scopeWarning = `PRESERVE every filter and scope \u2014 brand, product, date range, AND geography/segment (\`State\`/\`Region\`/\`District\`). Do NOT broaden, drop, or "simplify" any predicate.`;
|
|
9176
|
+
output += queries.length === 1 ? `### The SQL below and its result \u2014 copy the SQL VERBATIM into \`write_script\` (inside \`ctx.query(...)\`)
|
|
9177
|
+
${scopeWarning}
|
|
9178
|
+
|
|
9179
|
+
` : `### ${queries.length} successful queries \u2014 each block pairs ONE query's SQL with ITS OWN result. Do NOT mix a query's SQL with another query's data.
|
|
9180
|
+
${scopeWarning} The script re-runs them, so you can include more than one \`ctx.query(...)\`.
|
|
9181
|
+
|
|
8768
9182
|
`;
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
9183
|
+
queries.forEach((q, i) => {
|
|
9184
|
+
const total = q.total ?? q.rows.length;
|
|
9185
|
+
const capped = (q.shown ?? q.rows.length) >= MAX_ROWS_FETCHED;
|
|
9186
|
+
output += queries.length === 1 ? `**SQL:**
|
|
9187
|
+
` : `### Query ${i + 1} \u2014 ${capped ? `${MAX_ROWS_FETCHED}+ (TRUNCATED)` : `${total}`} rows
|
|
9188
|
+
**SQL for Query ${i + 1}:**
|
|
9189
|
+
`;
|
|
9190
|
+
output += "```sql\n" + String(q.sql) + "\n```\n";
|
|
9191
|
+
if (capped) {
|
|
9192
|
+
output += `> \u26A0\uFE0F **TRUNCATED, NON-REPRESENTATIVE SAMPLE.** This query hit the ${MAX_ROWS_FETCHED}-row exploration cap, so it returned only the FIRST ${MAX_ROWS_FETCHED} rows \u2014 and because it is \`ORDER BY\`'d, those rows are likely ALL from one group (e.g. one District/category), not a cross-section. Do NOT conclude any group "has no data" or read any count/total/min/max from this. To reason over all rows, re-query with **aggregation** (\`GROUP BY\` the dimension, e.g. \`SELECT District, COUNT(*) ... GROUP BY District\`) or compute the metric in SQL \u2014 never from this slice.
|
|
9193
|
+
`;
|
|
9194
|
+
}
|
|
9195
|
+
if (total <= MAIN_AGENT_COMPLETE_ROWS) {
|
|
9196
|
+
const rows = truncRows(q.rows);
|
|
9197
|
+
output += `**All ${rows.length} rows (COMPLETE result \u2014 this is the entire result set, nothing hidden):**
|
|
9198
|
+
`;
|
|
9199
|
+
output += jsonBlock(rows);
|
|
9200
|
+
} else {
|
|
9201
|
+
const samples = truncRows(q.rows.slice(0, 3));
|
|
9202
|
+
output += `**Result summary \u2014 computed over ALL ${total} rows. Use THIS for every count, range, total, "all/none", and per-group number; do NOT infer them from the samples:**
|
|
9203
|
+
`;
|
|
9204
|
+
output += jsonBlock(q.summary ?? { totalRows: total });
|
|
9205
|
+
output += `**Sample rows (first ${samples.length} of ${total} \u2014 to show row SHAPE only, NOT representative of the distribution):**
|
|
9206
|
+
`;
|
|
9207
|
+
output += jsonBlock(samples);
|
|
9208
|
+
}
|
|
9209
|
+
output += "\n";
|
|
9210
|
+
});
|
|
8776
9211
|
return output;
|
|
8777
9212
|
}
|
|
8778
9213
|
/**
|
|
@@ -8798,14 +9233,15 @@ function extractTablesFromSQL(sqls) {
|
|
|
8798
9233
|
// src/userResponse/agents/types.ts
|
|
8799
9234
|
var DEFAULT_AGENT_CONFIG = {
|
|
8800
9235
|
maxRowsPerSource: 10,
|
|
9236
|
+
maxRowsFetched: MAX_ROWS_FETCHED,
|
|
8801
9237
|
mainAgentModel: "",
|
|
8802
9238
|
// will use the provider's default model
|
|
8803
9239
|
sourceAgentModel: "",
|
|
8804
9240
|
// will use the provider's default model
|
|
8805
9241
|
maxRetries: 2,
|
|
8806
9242
|
// 2 retries = 3 total query attempts (1 initial + 2 retries for SQL errors)
|
|
8807
|
-
maxIterations:
|
|
8808
|
-
// schema search (2-3) + query attempts (2) + LLM responses + final
|
|
9243
|
+
maxIterations: 12
|
|
9244
|
+
// schema search (2-3) + query attempts (2) + write_script/execute_script (2-3) + LLM responses + final
|
|
8809
9245
|
};
|
|
8810
9246
|
|
|
8811
9247
|
// src/userResponse/utils/component-props-processor.ts
|
|
@@ -9079,6 +9515,7 @@ function formatExecutedTools(executedTools) {
|
|
|
9079
9515
|
Fields:
|
|
9080
9516
|
${fieldsText}`;
|
|
9081
9517
|
}
|
|
9518
|
+
const MAX_SAMPLE_BLOCK_CHARS = 8e3;
|
|
9082
9519
|
let sampleDataText = "";
|
|
9083
9520
|
const sampleData = tool.result?._sampleData;
|
|
9084
9521
|
if (Array.isArray(sampleData) && sampleData.length > 0) {
|
|
@@ -9086,8 +9523,11 @@ ${fieldsText}`;
|
|
|
9086
9523
|
sampleDataText = `
|
|
9087
9524
|
\u{1F511} RESULT FIELDS: ${sampleFields.join(", ")}`;
|
|
9088
9525
|
try {
|
|
9526
|
+
const stringified = JSON.stringify(sampleData, null, 2);
|
|
9527
|
+
const capped = stringified.length > MAX_SAMPLE_BLOCK_CHARS ? `${stringified.substring(0, MAX_SAMPLE_BLOCK_CHARS)}
|
|
9528
|
+
... (truncated; ${stringified.length - MAX_SAMPLE_BLOCK_CHARS} more chars)` : stringified;
|
|
9089
9529
|
sampleDataText += `
|
|
9090
|
-
\u{1F4C4} SAMPLE
|
|
9530
|
+
\u{1F4C4} SAMPLE ROWS (${sampleData.length}): ${capped}`;
|
|
9091
9531
|
} catch {
|
|
9092
9532
|
}
|
|
9093
9533
|
}
|
|
@@ -9375,11 +9815,20 @@ Fixed SQL query:`;
|
|
|
9375
9815
|
return validatedQuery;
|
|
9376
9816
|
}
|
|
9377
9817
|
|
|
9818
|
+
// src/userResponse/scripts/script-metadata-store.ts
|
|
9819
|
+
function resolveScriptRecipeStore(collections) {
|
|
9820
|
+
const s = collections?.["script-recipes"];
|
|
9821
|
+
if (s && typeof s.search === "function" && typeof s.getById === "function") {
|
|
9822
|
+
return s;
|
|
9823
|
+
}
|
|
9824
|
+
return null;
|
|
9825
|
+
}
|
|
9826
|
+
|
|
9378
9827
|
// src/userResponse/scripts/script-store.ts
|
|
9379
9828
|
var fs6 = __toESM(require("fs"));
|
|
9380
9829
|
var path6 = __toESM(require("path"));
|
|
9830
|
+
var import_crypto5 = require("crypto");
|
|
9381
9831
|
var DEFAULT_STORE_DIR = "scripts-store";
|
|
9382
|
-
var METADATA_SUBDIR = "metadata";
|
|
9383
9832
|
function normalizeScriptBody(scriptBody) {
|
|
9384
9833
|
let i = 0;
|
|
9385
9834
|
const n = scriptBody.length;
|
|
@@ -9404,383 +9853,321 @@ function normalizeScriptBody(scriptBody) {
|
|
|
9404
9853
|
} else if (/^\s*function\s+getData\b/.test(cleanBody)) {
|
|
9405
9854
|
cleanBody = cleanBody.replace(/^\s*function\s+getData\b/, "export async function getData");
|
|
9406
9855
|
}
|
|
9856
|
+
cleanBody = cleanBody.replace(/(?<!\\)\\([dwsDWS])/g, "\\\\$1");
|
|
9407
9857
|
return cleanBody;
|
|
9408
9858
|
}
|
|
9409
9859
|
var ScriptStore = class {
|
|
9410
|
-
constructor(
|
|
9411
|
-
this.
|
|
9412
|
-
this.
|
|
9413
|
-
this.
|
|
9414
|
-
|
|
9415
|
-
|
|
9416
|
-
return path6.join(this.storeDir, METADATA_SUBDIR);
|
|
9417
|
-
}
|
|
9418
|
-
/**
|
|
9419
|
-
* Filename base for a recipe. Drafts include the per-turn suffix so two
|
|
9420
|
-
* concurrent turns can never clobber each other's draft files; verified
|
|
9421
|
-
* recipes use the bare slug.
|
|
9422
|
-
*/
|
|
9423
|
-
fileBaseName(recipe) {
|
|
9424
|
-
const slug = this.toFileName(recipe.name);
|
|
9425
|
-
if (recipe.status === "draft" && recipe.turnId) {
|
|
9426
|
-
return `${slug}-${recipe.turnId}`;
|
|
9860
|
+
constructor(opts) {
|
|
9861
|
+
this.store = opts?.store ?? resolveScriptRecipeStore(opts?.collections) ?? null;
|
|
9862
|
+
this.storeDir = opts?.baseDir || path6.join(process.cwd(), DEFAULT_STORE_DIR);
|
|
9863
|
+
this.projectId = opts?.projectId;
|
|
9864
|
+
if (!this.store) {
|
|
9865
|
+
logger.warn("[ScriptStore] No script-recipes metadata store injected \u2014 script reuse disabled this run.");
|
|
9427
9866
|
}
|
|
9428
|
-
return slug;
|
|
9429
|
-
}
|
|
9430
|
-
/**
|
|
9431
|
-
* Absolute path to the .ts file for a recipe. Callers (e.g. ScriptRunner,
|
|
9432
|
-
* MainAgent's execute_script) use this to hand off the path to the tsx child.
|
|
9433
|
-
*/
|
|
9434
|
-
getScriptPath(recipe) {
|
|
9435
|
-
return path6.join(this.storeDir, `${this.fileBaseName(recipe)}.ts`);
|
|
9436
9867
|
}
|
|
9437
|
-
/**
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
getMetadataPath(recipe) {
|
|
9441
|
-
return path6.join(this.metadataDir(), `${this.fileBaseName(recipe)}.json`);
|
|
9868
|
+
/** Whether a metadata store is wired (matcher / authoring are gated on this). */
|
|
9869
|
+
hasStore() {
|
|
9870
|
+
return this.store !== null;
|
|
9442
9871
|
}
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
this.
|
|
9449
|
-
|
|
9872
|
+
// ============================================
|
|
9873
|
+
// Read
|
|
9874
|
+
// ============================================
|
|
9875
|
+
/** Number of healthy verified recipes (gates the script-matching path). */
|
|
9876
|
+
async count() {
|
|
9877
|
+
if (!this.store) return 0;
|
|
9878
|
+
try {
|
|
9879
|
+
return await this.store.count({ projectId: this.projectId });
|
|
9880
|
+
} catch (err) {
|
|
9881
|
+
logger.warn(`[ScriptStore] count failed: ${err}`);
|
|
9882
|
+
return 0;
|
|
9883
|
+
}
|
|
9450
9884
|
}
|
|
9451
9885
|
/**
|
|
9452
|
-
*
|
|
9453
|
-
*
|
|
9886
|
+
* FTS shortlist for the matcher (metadata only — bodies are loaded lazily by
|
|
9887
|
+
* `get()` once the LLM picks one). Returns verified, healthy recipes ranked
|
|
9888
|
+
* by relevance.
|
|
9454
9889
|
*/
|
|
9455
|
-
|
|
9456
|
-
this.
|
|
9457
|
-
|
|
9890
|
+
async search(prompt, limit) {
|
|
9891
|
+
if (!this.store) return [];
|
|
9892
|
+
try {
|
|
9893
|
+
const rows = await this.store.search({ prompt, projectId: this.projectId, limit });
|
|
9894
|
+
return rows.map((r) => this.rowToRecipe(r, ""));
|
|
9895
|
+
} catch (err) {
|
|
9896
|
+
logger.warn(`[ScriptStore] search failed: ${err}`);
|
|
9897
|
+
return [];
|
|
9898
|
+
}
|
|
9458
9899
|
}
|
|
9459
|
-
/**
|
|
9460
|
-
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
|
|
9464
|
-
|
|
9900
|
+
/** Fetch one recipe by id with its body loaded from disk. */
|
|
9901
|
+
async get(id) {
|
|
9902
|
+
if (!this.store) return null;
|
|
9903
|
+
try {
|
|
9904
|
+
const row = await this.store.getById(id);
|
|
9905
|
+
if (!row) return null;
|
|
9906
|
+
const body = this.readBody(row.fileBase);
|
|
9907
|
+
return this.rowToRecipe(row, body);
|
|
9908
|
+
} catch (err) {
|
|
9909
|
+
logger.warn(`[ScriptStore] get(${id}) failed: ${err}`);
|
|
9910
|
+
return null;
|
|
9911
|
+
}
|
|
9465
9912
|
}
|
|
9466
|
-
|
|
9467
|
-
|
|
9468
|
-
|
|
9469
|
-
|
|
9470
|
-
save(recipe) {
|
|
9471
|
-
this.
|
|
9472
|
-
|
|
9473
|
-
|
|
9474
|
-
|
|
9475
|
-
|
|
9913
|
+
// ============================================
|
|
9914
|
+
// Write
|
|
9915
|
+
// ============================================
|
|
9916
|
+
/** Create or update a recipe (metadata upsert + body write when changed). */
|
|
9917
|
+
async save(recipe) {
|
|
9918
|
+
if (!this.store) return;
|
|
9919
|
+
try {
|
|
9920
|
+
if (!recipe.fileBase) {
|
|
9921
|
+
recipe.fileBase = await this.computeFileBase(recipe.name, recipe.id);
|
|
9922
|
+
}
|
|
9923
|
+
if (recipe.scriptBody) {
|
|
9924
|
+
const normalized = normalizeScriptBody(recipe.scriptBody);
|
|
9925
|
+
const hash = this.hash(normalized);
|
|
9926
|
+
if (hash !== recipe.bodyHash) {
|
|
9927
|
+
this.writeBody(recipe.fileBase, normalized);
|
|
9928
|
+
recipe.bodyHash = hash;
|
|
9929
|
+
}
|
|
9930
|
+
}
|
|
9931
|
+
recipe.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9932
|
+
await this.store.upsert(this.recipeToRow(recipe));
|
|
9933
|
+
logger.info(`[ScriptStore] Saved "${recipe.name}" (${recipe.id})`);
|
|
9934
|
+
} catch (err) {
|
|
9935
|
+
logger.warn(`[ScriptStore] save("${recipe.name}") failed: ${err}`);
|
|
9936
|
+
}
|
|
9476
9937
|
}
|
|
9477
9938
|
/**
|
|
9478
|
-
* Persist (or update) a draft
|
|
9479
|
-
* the
|
|
9480
|
-
*
|
|
9481
|
-
* overwrite the same files (the LLM rewriting itself); a fresh `recipeId`
|
|
9482
|
-
* is minted only on the first call of the turn.
|
|
9483
|
-
*
|
|
9484
|
-
* Filename includes the `turnId` suffix so concurrent turns never collide.
|
|
9939
|
+
* Persist (or update) a draft. Within a turn, retries that pass the same
|
|
9940
|
+
* `recipeId` overwrite the same row + file; a fresh `recipeId` mints a new
|
|
9941
|
+
* draft. The body is visible at scripts-store/<fileBase>.ts immediately.
|
|
9485
9942
|
*/
|
|
9486
|
-
saveDraft(input) {
|
|
9487
|
-
this.
|
|
9943
|
+
async saveDraft(input) {
|
|
9944
|
+
if (!this.store) {
|
|
9945
|
+
throw new Error("[ScriptStore] saveDraft called with no metadata store injected");
|
|
9946
|
+
}
|
|
9488
9947
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9489
|
-
const existing = input.recipeId ? this.
|
|
9490
|
-
const
|
|
9491
|
-
const
|
|
9492
|
-
|
|
9493
|
-
|
|
9494
|
-
|
|
9495
|
-
|
|
9496
|
-
|
|
9497
|
-
|
|
9498
|
-
|
|
9499
|
-
} : {
|
|
9500
|
-
id: `script_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
9501
|
-
version: 1,
|
|
9948
|
+
const existing = input.recipeId ? await this.store.getById(input.recipeId) : null;
|
|
9949
|
+
const reuse = existing && existing.status === "draft" && existing.turnId === input.turnId;
|
|
9950
|
+
const id = reuse ? existing.id : `script_${Date.now()}_${(0, import_crypto5.randomBytes)(3).toString("hex")}`;
|
|
9951
|
+
const fileBase = reuse ? existing.fileBase : await this.computeFileBase(input.name, id);
|
|
9952
|
+
const normalized = normalizeScriptBody(input.scriptBody);
|
|
9953
|
+
const bodyHash = this.hash(normalized);
|
|
9954
|
+
this.writeBody(fileBase, normalized);
|
|
9955
|
+
const recipe = {
|
|
9956
|
+
id,
|
|
9957
|
+
version: existing?.version ?? 1,
|
|
9502
9958
|
name: input.name,
|
|
9503
9959
|
intentDescription: input.intentDescription,
|
|
9504
9960
|
tags: input.tags,
|
|
9505
|
-
sourceIds: [],
|
|
9506
|
-
tables: [],
|
|
9961
|
+
sourceIds: existing?.sourceIds ?? [],
|
|
9962
|
+
tables: existing?.tables ?? [],
|
|
9507
9963
|
parameters: input.parameters,
|
|
9508
|
-
scriptBody:
|
|
9509
|
-
|
|
9510
|
-
|
|
9964
|
+
scriptBody: normalized,
|
|
9965
|
+
fileBase,
|
|
9966
|
+
bodyHash,
|
|
9967
|
+
projectId: this.projectId,
|
|
9968
|
+
successCount: existing?.successCount ?? 0,
|
|
9969
|
+
failureCount: existing?.failureCount ?? 0,
|
|
9511
9970
|
lastUsed: now,
|
|
9512
9971
|
createdFrom: input.createdFrom,
|
|
9513
|
-
createdAt: now,
|
|
9972
|
+
createdAt: existing?.createdAt ?? now,
|
|
9514
9973
|
updatedAt: now,
|
|
9515
|
-
forkDepth: 0,
|
|
9974
|
+
forkDepth: existing?.forkDepth ?? 0,
|
|
9516
9975
|
status: "draft",
|
|
9517
9976
|
turnId: input.turnId
|
|
9518
9977
|
};
|
|
9519
|
-
this.
|
|
9520
|
-
this.
|
|
9521
|
-
logger.info(
|
|
9522
|
-
`[ScriptStore] ${reuseExisting ? "Updated" : "Saved"} draft "${recipe.name}" (${recipe.id}) at ${this.getScriptPath(recipe)}`
|
|
9523
|
-
);
|
|
9978
|
+
await this.store.upsert(this.recipeToRow(recipe));
|
|
9979
|
+
logger.info(`[ScriptStore] ${reuse ? "Updated" : "Saved"} draft "${recipe.name}" (${id}) at ${this.getScriptPath(recipe)}`);
|
|
9524
9980
|
return recipe;
|
|
9525
9981
|
}
|
|
9526
|
-
/**
|
|
9527
|
-
|
|
9528
|
-
|
|
9529
|
-
|
|
9530
|
-
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
if (!recipe || (recipe.status ?? "verified") !== "draft") return;
|
|
9535
|
-
recipe.lastError = {
|
|
9536
|
-
phase: err.phase,
|
|
9537
|
-
message: err.message,
|
|
9538
|
-
attempt: err.attempt,
|
|
9539
|
-
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
9540
|
-
};
|
|
9541
|
-
recipe.failureCount++;
|
|
9542
|
-
recipe.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9543
|
-
this.saveRecipeToDisk(recipe);
|
|
9982
|
+
/** Stamp a draft's last execution error (metadata only). */
|
|
9983
|
+
async recordDraftError(recipeId, err) {
|
|
9984
|
+
if (!this.store) return;
|
|
9985
|
+
try {
|
|
9986
|
+
await this.store.recordDraftError(recipeId, { ...err, at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
9987
|
+
} catch (e) {
|
|
9988
|
+
logger.warn(`[ScriptStore] recordDraftError failed: ${e}`);
|
|
9989
|
+
}
|
|
9544
9990
|
}
|
|
9545
9991
|
/**
|
|
9546
9992
|
* Promote a successfully-executed draft into a verified script.
|
|
9547
|
-
*
|
|
9548
|
-
*
|
|
9549
|
-
|
|
9550
|
-
|
|
9551
|
-
|
|
9552
|
-
|
|
9553
|
-
|
|
9554
|
-
|
|
9555
|
-
|
|
9556
|
-
|
|
9557
|
-
|
|
9558
|
-
|
|
9559
|
-
|
|
9560
|
-
|
|
9561
|
-
|
|
9562
|
-
|
|
9563
|
-
|
|
9564
|
-
logger.info(`[ScriptStore] promoteToVerified: recipe "${recipeId}" already verified \u2014 no-op`);
|
|
9565
|
-
return recipe;
|
|
9566
|
-
}
|
|
9567
|
-
const slug = this.toFileName(recipe.name);
|
|
9568
|
-
const canonicalTs = path6.join(this.storeDir, `${slug}.ts`);
|
|
9569
|
-
const canonicalMeta = path6.join(this.metadataDir(), `${slug}.json`);
|
|
9570
|
-
const canonicalFree = !fs6.existsSync(canonicalTs) && !fs6.existsSync(canonicalMeta);
|
|
9571
|
-
const oldTsPath = this.getScriptPath(recipe);
|
|
9572
|
-
const oldMetaPath = this.getMetadataPath(recipe);
|
|
9573
|
-
recipe.status = "verified";
|
|
9574
|
-
recipe.successCount = Math.max(1, recipe.successCount);
|
|
9575
|
-
recipe.failureCount = 0;
|
|
9576
|
-
recipe.lastError = void 0;
|
|
9577
|
-
recipe.lastUsed = (/* @__PURE__ */ new Date()).toISOString();
|
|
9578
|
-
recipe.updatedAt = recipe.lastUsed;
|
|
9579
|
-
recipe.sourceIds = input.sourceIds;
|
|
9580
|
-
recipe.tables = input.tables;
|
|
9581
|
-
if (input.parentId !== void 0) recipe.parentId = input.parentId;
|
|
9582
|
-
if (input.forkDepth !== void 0) recipe.forkDepth = input.forkDepth;
|
|
9583
|
-
if (input.forkReason !== void 0) recipe.forkReason = input.forkReason;
|
|
9584
|
-
if (canonicalFree) {
|
|
9585
|
-
recipe.turnId = void 0;
|
|
9586
|
-
try {
|
|
9587
|
-
if (fs6.existsSync(oldTsPath)) fs6.unlinkSync(oldTsPath);
|
|
9588
|
-
if (fs6.existsSync(oldMetaPath)) fs6.unlinkSync(oldMetaPath);
|
|
9589
|
-
} catch (err) {
|
|
9590
|
-
logger.warn(`[ScriptStore] promoteToVerified cleanup warning: ${err}`);
|
|
9993
|
+
* The on-disk body already exists at <fileBase>.ts (written at write_script
|
|
9994
|
+
* time) and keeps its name — only the DB row flips status + provenance.
|
|
9995
|
+
*/
|
|
9996
|
+
async promoteToVerified(recipeId, input) {
|
|
9997
|
+
if (!this.store) return null;
|
|
9998
|
+
try {
|
|
9999
|
+
const row = await this.store.promote(recipeId, {
|
|
10000
|
+
sourceIds: input.sourceIds,
|
|
10001
|
+
tables: input.tables,
|
|
10002
|
+
parentId: input.parentId,
|
|
10003
|
+
forkDepth: input.forkDepth,
|
|
10004
|
+
forkReason: input.forkReason,
|
|
10005
|
+
components: input.components
|
|
10006
|
+
});
|
|
10007
|
+
if (!row) {
|
|
10008
|
+
logger.warn(`[ScriptStore] promoteToVerified: recipe "${recipeId}" not found`);
|
|
10009
|
+
return null;
|
|
9591
10010
|
}
|
|
9592
|
-
|
|
9593
|
-
|
|
9594
|
-
|
|
9595
|
-
);
|
|
9596
|
-
|
|
9597
|
-
this.saveRecipeToDisk(recipe);
|
|
9598
|
-
logger.info(
|
|
9599
|
-
`[ScriptStore] Promoted draft "${recipe.name}" (${recipe.id}) \u2014 canonical slug "${slug}" was taken, kept turn-suffixed filename ${this.getScriptPath(recipe)}`
|
|
9600
|
-
);
|
|
10011
|
+
logger.info(`[ScriptStore] Promoted "${row.name}" (${recipeId}) \u2192 verified`);
|
|
10012
|
+
return this.rowToRecipe(row, this.readBody(row.fileBase));
|
|
10013
|
+
} catch (err) {
|
|
10014
|
+
logger.warn(`[ScriptStore] promoteToVerified failed: ${err}`);
|
|
10015
|
+
return null;
|
|
9601
10016
|
}
|
|
9602
|
-
return recipe;
|
|
9603
10017
|
}
|
|
9604
10018
|
/**
|
|
9605
|
-
* Drop a draft
|
|
9606
|
-
*
|
|
9607
|
-
*
|
|
10019
|
+
* Drop a draft (row + body file). MainAgent calls this at end-of-turn when a
|
|
10020
|
+
* draft was authored but never verified — failed drafts are never matched, so
|
|
10021
|
+
* deleting them immediately avoids unbounded accumulation (#5). No-op if the
|
|
10022
|
+
* recipe isn't a draft (so a promoted/verified script is never removed here).
|
|
9608
10023
|
*/
|
|
9609
|
-
discardDraft(recipeId) {
|
|
9610
|
-
this.
|
|
9611
|
-
const recipe = this.recipes.get(recipeId);
|
|
9612
|
-
if (!recipe || (recipe.status ?? "verified") !== "draft") return;
|
|
9613
|
-
this.recipes.delete(recipeId);
|
|
9614
|
-
this.deleteRecipeFromDisk(recipe);
|
|
9615
|
-
logger.info(`[ScriptStore] Discarded draft "${recipe.name}" (${recipeId})`);
|
|
10024
|
+
async discardDraft(recipeId) {
|
|
10025
|
+
await this.removeById(recipeId, "draft");
|
|
9616
10026
|
}
|
|
9617
|
-
/**
|
|
9618
|
-
|
|
9619
|
-
|
|
9620
|
-
delete(id) {
|
|
9621
|
-
this.ensureLoaded();
|
|
9622
|
-
const recipe = this.recipes.get(id);
|
|
9623
|
-
if (recipe) {
|
|
9624
|
-
this.recipes.delete(id);
|
|
9625
|
-
this.deleteRecipeFromDisk(recipe);
|
|
9626
|
-
logger.info(`[ScriptStore] Deleted script "${recipe.name}" (${id})`);
|
|
9627
|
-
}
|
|
10027
|
+
/** Delete a recipe (row + body file). */
|
|
10028
|
+
async delete(id) {
|
|
10029
|
+
await this.removeById(id);
|
|
9628
10030
|
}
|
|
9629
|
-
/**
|
|
9630
|
-
|
|
9631
|
-
|
|
9632
|
-
|
|
9633
|
-
|
|
9634
|
-
|
|
9635
|
-
|
|
9636
|
-
recipe.successCount++;
|
|
9637
|
-
recipe.lastUsed = (/* @__PURE__ */ new Date()).toISOString();
|
|
9638
|
-
this.saveRecipeToDisk(recipe);
|
|
10031
|
+
/** Record a successful execution (atomic counter bump). */
|
|
10032
|
+
async recordSuccess(id) {
|
|
10033
|
+
if (!this.store) return;
|
|
10034
|
+
try {
|
|
10035
|
+
await this.store.updateStats(id, { successDelta: 1, lastUsed: (/* @__PURE__ */ new Date()).toISOString() });
|
|
10036
|
+
} catch (err) {
|
|
10037
|
+
logger.warn(`[ScriptStore] recordSuccess failed: ${err}`);
|
|
9639
10038
|
}
|
|
9640
10039
|
}
|
|
9641
|
-
/**
|
|
9642
|
-
|
|
9643
|
-
|
|
9644
|
-
|
|
9645
|
-
|
|
9646
|
-
|
|
9647
|
-
|
|
9648
|
-
recipe.failureCount++;
|
|
9649
|
-
recipe.lastUsed = (/* @__PURE__ */ new Date()).toISOString();
|
|
9650
|
-
this.saveRecipeToDisk(recipe);
|
|
10040
|
+
/** Record a failed execution (atomic counter bump). */
|
|
10041
|
+
async recordFailure(id) {
|
|
10042
|
+
if (!this.store) return;
|
|
10043
|
+
try {
|
|
10044
|
+
await this.store.updateStats(id, { failureDelta: 1, lastUsed: (/* @__PURE__ */ new Date()).toISOString() });
|
|
10045
|
+
} catch (err) {
|
|
10046
|
+
logger.warn(`[ScriptStore] recordFailure failed: ${err}`);
|
|
9651
10047
|
}
|
|
9652
10048
|
}
|
|
9653
|
-
|
|
9654
|
-
|
|
9655
|
-
|
|
9656
|
-
|
|
9657
|
-
|
|
9658
|
-
const
|
|
9659
|
-
|
|
9660
|
-
const total = recipe.successCount + recipe.failureCount;
|
|
9661
|
-
if (total < 5) return 0;
|
|
9662
|
-
return recipe.failureCount / total;
|
|
10049
|
+
// ============================================
|
|
10050
|
+
// Paths (sync — body lives on disk)
|
|
10051
|
+
// ============================================
|
|
10052
|
+
/** Absolute path to the .ts body for a recipe (used by the runner/MainAgent). */
|
|
10053
|
+
getScriptPath(recipe) {
|
|
10054
|
+
const base = recipe.fileBase || this.toSlug(recipe.name);
|
|
10055
|
+
return path6.join(this.storeDir, `${base}.ts`);
|
|
9663
10056
|
}
|
|
9664
10057
|
// ============================================
|
|
9665
|
-
//
|
|
10058
|
+
// Internals
|
|
9666
10059
|
// ============================================
|
|
9667
|
-
|
|
9668
|
-
if (!this.
|
|
9669
|
-
|
|
9670
|
-
|
|
10060
|
+
async removeById(id, requireStatus) {
|
|
10061
|
+
if (!this.store) return;
|
|
10062
|
+
try {
|
|
10063
|
+
const row = await this.store.getById(id);
|
|
10064
|
+
if (!row) return;
|
|
10065
|
+
if (requireStatus && row.status !== requireStatus) return;
|
|
10066
|
+
await this.store.remove(id);
|
|
10067
|
+
this.unlinkBody(row.fileBase);
|
|
10068
|
+
logger.info(`[ScriptStore] Removed "${row.name}" (${id})`);
|
|
10069
|
+
} catch (err) {
|
|
10070
|
+
logger.warn(`[ScriptStore] remove(${id}) failed: ${err}`);
|
|
9671
10071
|
}
|
|
9672
10072
|
}
|
|
9673
|
-
|
|
9674
|
-
|
|
9675
|
-
|
|
9676
|
-
|
|
9677
|
-
|
|
9678
|
-
|
|
10073
|
+
rowToRecipe(row, body) {
|
|
10074
|
+
return {
|
|
10075
|
+
id: row.id,
|
|
10076
|
+
version: row.version ?? 1,
|
|
10077
|
+
name: row.name,
|
|
10078
|
+
intentDescription: row.intentDescription,
|
|
10079
|
+
tags: row.tags ?? [],
|
|
10080
|
+
sourceIds: row.sourceIds ?? [],
|
|
10081
|
+
tables: row.tables ?? [],
|
|
10082
|
+
parameters: row.parameters ?? [],
|
|
10083
|
+
scriptBody: body,
|
|
10084
|
+
fileBase: row.fileBase,
|
|
10085
|
+
bodyHash: row.bodyHash ?? void 0,
|
|
10086
|
+
projectId: row.projectId ?? void 0,
|
|
10087
|
+
successCount: row.successCount ?? 0,
|
|
10088
|
+
failureCount: row.failureCount ?? 0,
|
|
10089
|
+
lastUsed: row.lastUsed ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
10090
|
+
createdFrom: row.createdFrom ?? "",
|
|
10091
|
+
createdAt: row.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
10092
|
+
updatedAt: row.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
10093
|
+
parentId: row.parentId ?? void 0,
|
|
10094
|
+
forkDepth: typeof row.forkDepth === "number" ? row.forkDepth : 0,
|
|
10095
|
+
forkReason: row.forkReason ?? void 0,
|
|
10096
|
+
components: row.components ?? void 0,
|
|
10097
|
+
status: row.status ?? "verified",
|
|
10098
|
+
turnId: row.turnId ?? void 0,
|
|
10099
|
+
lastError: row.lastError ?? void 0
|
|
10100
|
+
};
|
|
9679
10101
|
}
|
|
9680
|
-
|
|
9681
|
-
|
|
9682
|
-
|
|
9683
|
-
|
|
9684
|
-
|
|
9685
|
-
|
|
9686
|
-
|
|
9687
|
-
|
|
9688
|
-
|
|
9689
|
-
|
|
9690
|
-
|
|
9691
|
-
|
|
9692
|
-
|
|
9693
|
-
|
|
9694
|
-
|
|
9695
|
-
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
|
|
9699
|
-
|
|
9700
|
-
|
|
9701
|
-
|
|
9702
|
-
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
|
|
9706
|
-
|
|
9707
|
-
|
|
9708
|
-
|
|
9709
|
-
|
|
9710
|
-
|
|
9711
|
-
|
|
9712
|
-
}
|
|
9713
|
-
}
|
|
9714
|
-
const topLevelJson = fs6.readdirSync(this.storeDir).filter((f) => f.endsWith(".json"));
|
|
9715
|
-
for (const file of topLevelJson) {
|
|
9716
|
-
try {
|
|
9717
|
-
const filePath = path6.join(this.storeDir, file);
|
|
9718
|
-
const recipe = JSON.parse(fs6.readFileSync(filePath, "utf-8"));
|
|
9719
|
-
if (!recipe.id || !recipe.scriptBody) continue;
|
|
9720
|
-
if (this.recipes.has(recipe.id)) continue;
|
|
9721
|
-
if (typeof recipe.forkDepth !== "number") recipe.forkDepth = 0;
|
|
9722
|
-
if (!recipe.status) recipe.status = "verified";
|
|
9723
|
-
this.recipes.set(recipe.id, recipe);
|
|
9724
|
-
} catch (err) {
|
|
9725
|
-
logger.warn(`[ScriptStore] Failed to load legacy script ${file}: ${err}`);
|
|
9726
|
-
}
|
|
9727
|
-
}
|
|
9728
|
-
if (this.recipes.size > 0) {
|
|
9729
|
-
logger.info(`[ScriptStore] Loaded ${this.recipes.size} scripts from ${this.storeDir}`);
|
|
10102
|
+
recipeToRow(recipe) {
|
|
10103
|
+
return {
|
|
10104
|
+
id: recipe.id,
|
|
10105
|
+
projectId: recipe.projectId ?? this.projectId ?? null,
|
|
10106
|
+
version: recipe.version ?? 1,
|
|
10107
|
+
name: recipe.name,
|
|
10108
|
+
intentDescription: recipe.intentDescription,
|
|
10109
|
+
tags: recipe.tags ?? null,
|
|
10110
|
+
createdFrom: recipe.createdFrom ?? null,
|
|
10111
|
+
sourceIds: recipe.sourceIds ?? null,
|
|
10112
|
+
tables: recipe.tables ?? null,
|
|
10113
|
+
parameters: recipe.parameters ?? null,
|
|
10114
|
+
components: recipe.components ?? null,
|
|
10115
|
+
fileBase: recipe.fileBase || this.toSlug(recipe.name),
|
|
10116
|
+
bodyHash: recipe.bodyHash ?? null,
|
|
10117
|
+
successCount: recipe.successCount ?? 0,
|
|
10118
|
+
failureCount: recipe.failureCount ?? 0,
|
|
10119
|
+
lastUsed: recipe.lastUsed ?? null,
|
|
10120
|
+
parentId: recipe.parentId ?? null,
|
|
10121
|
+
forkDepth: typeof recipe.forkDepth === "number" ? recipe.forkDepth : 0,
|
|
10122
|
+
forkReason: recipe.forkReason ?? null,
|
|
10123
|
+
status: recipe.status ?? "verified",
|
|
10124
|
+
turnId: recipe.turnId ?? null,
|
|
10125
|
+
lastError: recipe.lastError ?? null
|
|
10126
|
+
};
|
|
10127
|
+
}
|
|
10128
|
+
/** slug of name, with a short id suffix when the bare slug is already taken. */
|
|
10129
|
+
async computeFileBase(name, id) {
|
|
10130
|
+
const slug = this.toSlug(name);
|
|
10131
|
+
if (this.store) {
|
|
10132
|
+
try {
|
|
10133
|
+
const taken = await this.store.fileBaseTaken(slug, id, this.projectId);
|
|
10134
|
+
if (taken) return `${slug}-${id.slice(-6)}`;
|
|
10135
|
+
} catch {
|
|
9730
10136
|
}
|
|
9731
|
-
} catch (err) {
|
|
9732
|
-
logger.warn(`[ScriptStore] Failed to read store directory ${this.storeDir}: ${err}`);
|
|
9733
10137
|
}
|
|
10138
|
+
return slug;
|
|
9734
10139
|
}
|
|
9735
|
-
|
|
9736
|
-
|
|
9737
|
-
|
|
9738
|
-
|
|
9739
|
-
|
|
9740
|
-
|
|
9741
|
-
|
|
9742
|
-
|
|
9743
|
-
|
|
10140
|
+
toSlug(name) {
|
|
10141
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unnamed-script";
|
|
10142
|
+
}
|
|
10143
|
+
hash(body) {
|
|
10144
|
+
return (0, import_crypto5.createHash)("sha256").update(body).digest("hex");
|
|
10145
|
+
}
|
|
10146
|
+
bodyPath(fileBase) {
|
|
10147
|
+
return path6.join(this.storeDir, `${fileBase}.ts`);
|
|
10148
|
+
}
|
|
10149
|
+
readBody(fileBase) {
|
|
9744
10150
|
try {
|
|
9745
|
-
|
|
9746
|
-
if (!fs6.existsSync(metaDir)) {
|
|
9747
|
-
fs6.mkdirSync(metaDir, { recursive: true });
|
|
9748
|
-
}
|
|
9749
|
-
const baseName = this.fileBaseName(recipe);
|
|
9750
|
-
const metaPath = path6.join(metaDir, `${baseName}.json`);
|
|
9751
|
-
const bodyPath = path6.join(this.storeDir, `${baseName}.ts`);
|
|
9752
|
-
const { scriptBody, ...meta } = recipe;
|
|
9753
|
-
fs6.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
|
|
9754
|
-
fs6.writeFileSync(bodyPath, normalizeScriptBody(scriptBody));
|
|
9755
|
-
const legacyPath = path6.join(this.storeDir, `${this.toFileName(recipe.name)}.json`);
|
|
9756
|
-
if (fs6.existsSync(legacyPath)) {
|
|
9757
|
-
try {
|
|
9758
|
-
fs6.unlinkSync(legacyPath);
|
|
9759
|
-
} catch {
|
|
9760
|
-
}
|
|
9761
|
-
}
|
|
10151
|
+
return fs6.readFileSync(this.bodyPath(fileBase), "utf-8");
|
|
9762
10152
|
} catch (err) {
|
|
9763
|
-
logger.warn(`[ScriptStore]
|
|
10153
|
+
logger.warn(`[ScriptStore] body file ${fileBase}.ts missing/unreadable: ${err}`);
|
|
10154
|
+
return "";
|
|
9764
10155
|
}
|
|
9765
10156
|
}
|
|
9766
|
-
/**
|
|
9767
|
-
|
|
9768
|
-
|
|
9769
|
-
|
|
9770
|
-
const
|
|
9771
|
-
|
|
9772
|
-
|
|
9773
|
-
|
|
9774
|
-
|
|
9775
|
-
|
|
9776
|
-
|
|
9777
|
-
|
|
9778
|
-
|
|
9779
|
-
|
|
9780
|
-
if (fs6.existsSync(p)) fs6.unlinkSync(p);
|
|
9781
|
-
} catch (err) {
|
|
9782
|
-
logger.warn(`[ScriptStore] Failed to delete ${p}: ${err}`);
|
|
9783
|
-
}
|
|
10157
|
+
/** Atomic body write (temp + rename) so concurrent reads never see a partial file. */
|
|
10158
|
+
writeBody(fileBase, normalizedBody) {
|
|
10159
|
+
if (!fs6.existsSync(this.storeDir)) fs6.mkdirSync(this.storeDir, { recursive: true });
|
|
10160
|
+
const finalPath = this.bodyPath(fileBase);
|
|
10161
|
+
const tmpPath = `${finalPath}.tmp-${(0, import_crypto5.randomBytes)(4).toString("hex")}`;
|
|
10162
|
+
fs6.writeFileSync(tmpPath, normalizedBody);
|
|
10163
|
+
fs6.renameSync(tmpPath, finalPath);
|
|
10164
|
+
}
|
|
10165
|
+
unlinkBody(fileBase) {
|
|
10166
|
+
try {
|
|
10167
|
+
const p = this.bodyPath(fileBase);
|
|
10168
|
+
if (fs6.existsSync(p)) fs6.unlinkSync(p);
|
|
10169
|
+
} catch (err) {
|
|
10170
|
+
logger.warn(`[ScriptStore] failed to delete body ${fileBase}.ts: ${err}`);
|
|
9784
10171
|
}
|
|
9785
10172
|
}
|
|
9786
10173
|
};
|
|
@@ -9797,14 +10184,14 @@ var ScriptMatcher = class {
|
|
|
9797
10184
|
* Returns null if no script matches.
|
|
9798
10185
|
*/
|
|
9799
10186
|
async match(userPrompt, apiKey, model) {
|
|
9800
|
-
const recipes = this.store.
|
|
10187
|
+
const recipes = await this.store.search(userPrompt);
|
|
9801
10188
|
if (recipes.length === 0) return null;
|
|
9802
10189
|
const healthyRecipes = recipes.filter((r) => {
|
|
9803
10190
|
const total = r.successCount + r.failureCount;
|
|
9804
10191
|
return total < 5 || r.failureCount / total <= MAX_FAILURE_RATE;
|
|
9805
10192
|
});
|
|
9806
10193
|
if (healthyRecipes.length === 0) {
|
|
9807
|
-
logger.info(`[ScriptMatcher]
|
|
10194
|
+
logger.info(`[ScriptMatcher] No healthy candidates among ${recipes.length} FTS matches \u2014 skipping`);
|
|
9808
10195
|
return null;
|
|
9809
10196
|
}
|
|
9810
10197
|
const scriptCatalog = this.buildScriptCatalog(healthyRecipes);
|
|
@@ -9840,7 +10227,7 @@ var ScriptMatcher = class {
|
|
|
9840
10227
|
logger.info(`[ScriptMatcher] No match. Reason: ${reasoning}`);
|
|
9841
10228
|
return null;
|
|
9842
10229
|
}
|
|
9843
|
-
const recipe = this.store.get(response.scriptId);
|
|
10230
|
+
const recipe = await this.store.get(response.scriptId);
|
|
9844
10231
|
if (!recipe) {
|
|
9845
10232
|
logger.warn(`[ScriptMatcher] LLM returned unknown script ID: "${response.scriptId}"`);
|
|
9846
10233
|
return null;
|
|
@@ -9878,46 +10265,345 @@ var ScriptMatcher = class {
|
|
|
9878
10265
|
return null;
|
|
9879
10266
|
}
|
|
9880
10267
|
}
|
|
9881
|
-
/**
|
|
9882
|
-
* Build the script catalog string for the LLM prompt.
|
|
9883
|
-
* Each script gets: index, ID, name, description, and parameter definitions.
|
|
9884
|
-
*/
|
|
9885
|
-
buildScriptCatalog(recipes) {
|
|
9886
|
-
return recipes.map((r, idx) => {
|
|
9887
|
-
const paramList = r.parameters.length > 0 ? r.parameters.map((p) => {
|
|
9888
|
-
let desc = `${p.name} (${p.type}`;
|
|
9889
|
-
if (!p.required) desc += ", optional";
|
|
9890
|
-
if (p.default !== void 0) desc += `, default: ${JSON.stringify(p.default)}`;
|
|
9891
|
-
if (p.enumValues) desc += `, values: ${Object.keys(p.enumValues).join("/")}`;
|
|
9892
|
-
desc += `) \u2014 ${p.description}`;
|
|
9893
|
-
return ` - ${desc}`;
|
|
9894
|
-
}).join("\n") : " (no parameters)";
|
|
9895
|
-
return `${idx + 1}. [${r.id}] "${r.name}"
|
|
9896
|
-
${r.intentDescription}
|
|
9897
|
-
Parameters:
|
|
9898
|
-
${paramList}`;
|
|
9899
|
-
}).join("\n\n");
|
|
10268
|
+
/**
|
|
10269
|
+
* Build the script catalog string for the LLM prompt.
|
|
10270
|
+
* Each script gets: index, ID, name, description, and parameter definitions.
|
|
10271
|
+
*/
|
|
10272
|
+
buildScriptCatalog(recipes) {
|
|
10273
|
+
return recipes.map((r, idx) => {
|
|
10274
|
+
const paramList = r.parameters.length > 0 ? r.parameters.map((p) => {
|
|
10275
|
+
let desc = `${p.name} (${p.type}`;
|
|
10276
|
+
if (!p.required) desc += ", optional";
|
|
10277
|
+
if (p.default !== void 0) desc += `, default: ${JSON.stringify(p.default)}`;
|
|
10278
|
+
if (p.enumValues) desc += `, values: ${Object.keys(p.enumValues).join("/")}`;
|
|
10279
|
+
desc += `) \u2014 ${p.description}`;
|
|
10280
|
+
return ` - ${desc}`;
|
|
10281
|
+
}).join("\n") : " (no parameters)";
|
|
10282
|
+
return `${idx + 1}. [${r.id}] "${r.name}"
|
|
10283
|
+
${r.intentDescription}
|
|
10284
|
+
Parameters:
|
|
10285
|
+
${paramList}`;
|
|
10286
|
+
}).join("\n\n");
|
|
10287
|
+
}
|
|
10288
|
+
};
|
|
10289
|
+
|
|
10290
|
+
// src/userResponse/scripts/script-component-generator.ts
|
|
10291
|
+
function inferColumns(rows) {
|
|
10292
|
+
const cols = /* @__PURE__ */ new Map();
|
|
10293
|
+
if (!Array.isArray(rows) || rows.length === 0) return cols;
|
|
10294
|
+
const sample = rows.slice(0, 20);
|
|
10295
|
+
const keys = /* @__PURE__ */ new Set();
|
|
10296
|
+
for (const r of sample) {
|
|
10297
|
+
if (r && typeof r === "object") for (const k of Object.keys(r)) keys.add(k);
|
|
10298
|
+
}
|
|
10299
|
+
for (const key of keys) {
|
|
10300
|
+
let type = "TEXT";
|
|
10301
|
+
let typed = false;
|
|
10302
|
+
const distinct = /* @__PURE__ */ new Set();
|
|
10303
|
+
for (const r of sample) {
|
|
10304
|
+
const v = r?.[key];
|
|
10305
|
+
if (v === null || v === void 0) continue;
|
|
10306
|
+
if (!typed) {
|
|
10307
|
+
if (typeof v === "number") type = "NUMBER";
|
|
10308
|
+
else if (typeof v === "boolean") type = "BOOLEAN";
|
|
10309
|
+
else if (v instanceof Date) type = "DATE";
|
|
10310
|
+
else if (typeof v === "string" && v.trim() !== "" && !isNaN(Number(v))) type = "NUMBER";
|
|
10311
|
+
else type = "TEXT";
|
|
10312
|
+
typed = true;
|
|
10313
|
+
}
|
|
10314
|
+
distinct.add(v instanceof Date ? v.toISOString() : String(v));
|
|
10315
|
+
}
|
|
10316
|
+
cols.set(key.toLowerCase(), { name: key, type, invariant: distinct.size <= 1 });
|
|
10317
|
+
}
|
|
10318
|
+
return cols;
|
|
10319
|
+
}
|
|
10320
|
+
function resolveCol(cols, key) {
|
|
10321
|
+
if (key == null) return void 0;
|
|
10322
|
+
return cols.get(String(key).toLowerCase());
|
|
10323
|
+
}
|
|
10324
|
+
function firstOfType(cols, type) {
|
|
10325
|
+
for (const c of cols.values()) if (c.type === type) return c;
|
|
10326
|
+
return void 0;
|
|
10327
|
+
}
|
|
10328
|
+
var ALL_KEY_NAMES = ["xAxisKey", "yAxisKey", "valueKey", "nameKey", "seriesKey", "groupBy", "aggregationField", "categoryKey"];
|
|
10329
|
+
var AVG_KEYWORDS = /\b(average|avg|mean)\b/i;
|
|
10330
|
+
var COUNT_KEYWORDS = /\b(count|number of|how many|# of|no\.? of)\b/i;
|
|
10331
|
+
var SUM_KEYWORDS = /\b(total|sum|overall|combined|aggregate|all)\b/i;
|
|
10332
|
+
function validateAndRepairComponent(comp, componentType, cols, rowCount) {
|
|
10333
|
+
if (cols.size === 0) return comp;
|
|
10334
|
+
const props = comp.props || (comp.props = {});
|
|
10335
|
+
const cfg = props.config || (props.config = {});
|
|
10336
|
+
for (const k of ALL_KEY_NAMES) {
|
|
10337
|
+
if (cfg[k] == null) continue;
|
|
10338
|
+
const r = resolveCol(cols, cfg[k]);
|
|
10339
|
+
if (r) cfg[k] = r.name;
|
|
10340
|
+
else delete cfg[k];
|
|
10341
|
+
}
|
|
10342
|
+
if (Array.isArray(cfg.metricKeys)) {
|
|
10343
|
+
cfg.metricKeys = cfg.metricKeys.map((m) => resolveCol(cols, m)?.name).filter(Boolean);
|
|
10344
|
+
if (cfg.metricKeys.length === 0) delete cfg.metricKeys;
|
|
10345
|
+
}
|
|
10346
|
+
const numeric = () => firstOfType(cols, "NUMBER");
|
|
10347
|
+
const category = () => firstOfType(cols, "TEXT") || firstOfType(cols, "DATE");
|
|
10348
|
+
const hasMetrics = Array.isArray(cfg.metricKeys) && cfg.metricKeys.length > 0;
|
|
10349
|
+
switch (componentType) {
|
|
10350
|
+
case "KPICard":
|
|
10351
|
+
case "GaugeChart": {
|
|
10352
|
+
const vk = resolveCol(cols, cfg.valueKey) || numeric() || category();
|
|
10353
|
+
if (!vk) return null;
|
|
10354
|
+
cfg.valueKey = vk.name;
|
|
10355
|
+
const title = String(props.title || "");
|
|
10356
|
+
if (rowCount > 1 && !cfg.aggregation) {
|
|
10357
|
+
if (COUNT_KEYWORDS.test(title)) cfg.aggregation = "count";
|
|
10358
|
+
else if (!vk.invariant) {
|
|
10359
|
+
if (AVG_KEYWORDS.test(title)) cfg.aggregation = "avg";
|
|
10360
|
+
else if (SUM_KEYWORDS.test(title)) cfg.aggregation = "sum";
|
|
10361
|
+
}
|
|
10362
|
+
}
|
|
10363
|
+
if (cfg.aggregation === "sum" || cfg.aggregation === "avg") {
|
|
10364
|
+
const af = resolveCol(cols, cfg.aggregationField);
|
|
10365
|
+
if (af && af.type === "NUMBER") {
|
|
10366
|
+
cfg.aggregationField = af.name;
|
|
10367
|
+
} else {
|
|
10368
|
+
const n = vk.type === "NUMBER" ? vk : numeric();
|
|
10369
|
+
if (n) cfg.aggregationField = n.name;
|
|
10370
|
+
else {
|
|
10371
|
+
delete cfg.aggregation;
|
|
10372
|
+
delete cfg.aggregationField;
|
|
10373
|
+
}
|
|
10374
|
+
}
|
|
10375
|
+
}
|
|
10376
|
+
return comp;
|
|
10377
|
+
}
|
|
10378
|
+
case "PieChart":
|
|
10379
|
+
case "TreemapChart": {
|
|
10380
|
+
let nameC = resolveCol(cols, cfg.nameKey);
|
|
10381
|
+
let valC = resolveCol(cols, cfg.valueKey);
|
|
10382
|
+
if (nameC?.type === "NUMBER" && valC && valC.type !== "NUMBER") {
|
|
10383
|
+
const t = nameC;
|
|
10384
|
+
nameC = valC;
|
|
10385
|
+
valC = t;
|
|
10386
|
+
}
|
|
10387
|
+
if (!nameC) nameC = category();
|
|
10388
|
+
if (!valC || valC.type !== "NUMBER") valC = numeric() || valC;
|
|
10389
|
+
if (!nameC || !valC) return null;
|
|
10390
|
+
cfg.nameKey = nameC.name;
|
|
10391
|
+
cfg.valueKey = valC.name;
|
|
10392
|
+
return comp;
|
|
10393
|
+
}
|
|
10394
|
+
case "WaterfallChart": {
|
|
10395
|
+
const catC = resolveCol(cols, cfg.categoryKey) || category();
|
|
10396
|
+
let valC = resolveCol(cols, cfg.valueKey);
|
|
10397
|
+
if (!valC || valC.type !== "NUMBER") valC = numeric() || valC;
|
|
10398
|
+
if (!catC || !valC) return null;
|
|
10399
|
+
cfg.categoryKey = catC.name;
|
|
10400
|
+
cfg.valueKey = valC.name;
|
|
10401
|
+
return comp;
|
|
10402
|
+
}
|
|
10403
|
+
case "ScatterChart": {
|
|
10404
|
+
let xC = resolveCol(cols, cfg.xAxisKey);
|
|
10405
|
+
let yC = resolveCol(cols, cfg.yAxisKey);
|
|
10406
|
+
if (!xC || xC.type !== "NUMBER") xC = numeric() || xC;
|
|
10407
|
+
if (!yC || yC.type !== "NUMBER") {
|
|
10408
|
+
yC = [...cols.values()].find((c) => c.type === "NUMBER" && c.name !== xC?.name) || numeric() || yC;
|
|
10409
|
+
}
|
|
10410
|
+
if (!xC || !yC) return null;
|
|
10411
|
+
cfg.xAxisKey = xC.name;
|
|
10412
|
+
cfg.yAxisKey = yC.name;
|
|
10413
|
+
return comp;
|
|
10414
|
+
}
|
|
10415
|
+
case "HeatmapChart": {
|
|
10416
|
+
const xC = resolveCol(cols, cfg.xAxisKey) || category();
|
|
10417
|
+
const yC = resolveCol(cols, cfg.yAxisKey) || [...cols.values()].find((c) => (c.type === "TEXT" || c.type === "DATE") && c.name !== xC?.name);
|
|
10418
|
+
let valC = resolveCol(cols, cfg.valueKey);
|
|
10419
|
+
if (!valC || valC.type !== "NUMBER") valC = numeric() || valC;
|
|
10420
|
+
if (!xC || !yC || !valC) return null;
|
|
10421
|
+
cfg.xAxisKey = xC.name;
|
|
10422
|
+
cfg.yAxisKey = yC.name;
|
|
10423
|
+
cfg.valueKey = valC.name;
|
|
10424
|
+
return comp;
|
|
10425
|
+
}
|
|
10426
|
+
case "BarChart":
|
|
10427
|
+
case "LineChart": {
|
|
10428
|
+
let xC = resolveCol(cols, cfg.xAxisKey);
|
|
10429
|
+
let yC = resolveCol(cols, cfg.yAxisKey);
|
|
10430
|
+
if (!hasMetrics && xC?.type === "NUMBER" && yC && yC.type !== "NUMBER") {
|
|
10431
|
+
const t = xC;
|
|
10432
|
+
xC = yC;
|
|
10433
|
+
yC = t;
|
|
10434
|
+
}
|
|
10435
|
+
if (!xC) xC = (componentType === "LineChart" ? firstOfType(cols, "DATE") || category() : category()) || firstOfType(cols, "NUMBER");
|
|
10436
|
+
if (!xC) return null;
|
|
10437
|
+
cfg.xAxisKey = xC.name;
|
|
10438
|
+
if (!hasMetrics) {
|
|
10439
|
+
if (!yC || yC.type !== "NUMBER") yC = numeric();
|
|
10440
|
+
if (!yC) return null;
|
|
10441
|
+
cfg.yAxisKey = yC.name;
|
|
10442
|
+
}
|
|
10443
|
+
if (cfg.seriesKey) {
|
|
10444
|
+
const sC = resolveCol(cols, cfg.seriesKey);
|
|
10445
|
+
if (!sC || sC.type !== "TEXT" || sC.name === cfg.xAxisKey) delete cfg.seriesKey;
|
|
10446
|
+
else cfg.seriesKey = sC.name;
|
|
10447
|
+
}
|
|
10448
|
+
return comp;
|
|
10449
|
+
}
|
|
10450
|
+
case "RadarChart": {
|
|
10451
|
+
if (!hasMetrics) {
|
|
10452
|
+
const vk = resolveCol(cols, cfg.valueKey) || numeric();
|
|
10453
|
+
if (!vk) return null;
|
|
10454
|
+
cfg.valueKey = vk.name;
|
|
10455
|
+
}
|
|
10456
|
+
return comp;
|
|
10457
|
+
}
|
|
10458
|
+
default:
|
|
10459
|
+
return comp;
|
|
10460
|
+
}
|
|
10461
|
+
}
|
|
10462
|
+
function scriptCompKey(comp) {
|
|
10463
|
+
const id = comp?.componentId || comp?.componentName || "";
|
|
10464
|
+
const src = comp?.sourceIndex ?? "";
|
|
10465
|
+
return `${id}|${src}|${JSON.stringify(comp?.props?.config ?? comp?.config ?? {})}`;
|
|
10466
|
+
}
|
|
10467
|
+
function extractAnswerComponentObject(text) {
|
|
10468
|
+
const hasMatch = text.match(/"hasAnswerComponent"\s*:\s*(true|false)/);
|
|
10469
|
+
if (!hasMatch || hasMatch[1] !== "true") return null;
|
|
10470
|
+
const startMatch = text.match(/"answerComponent"\s*:\s*\{/);
|
|
10471
|
+
if (!startMatch) return null;
|
|
10472
|
+
const startPos = startMatch.index + startMatch[0].length - 1;
|
|
10473
|
+
let depth = 0, inString = false, escapeNext = false, endPos = -1;
|
|
10474
|
+
for (let i = startPos; i < text.length; i++) {
|
|
10475
|
+
const c = text[i];
|
|
10476
|
+
if (escapeNext) {
|
|
10477
|
+
escapeNext = false;
|
|
10478
|
+
continue;
|
|
10479
|
+
}
|
|
10480
|
+
if (c === "\\") {
|
|
10481
|
+
escapeNext = true;
|
|
10482
|
+
continue;
|
|
10483
|
+
}
|
|
10484
|
+
if (c === '"') {
|
|
10485
|
+
inString = !inString;
|
|
10486
|
+
continue;
|
|
10487
|
+
}
|
|
10488
|
+
if (!inString) {
|
|
10489
|
+
if (c === "{") depth++;
|
|
10490
|
+
else if (c === "}") {
|
|
10491
|
+
depth--;
|
|
10492
|
+
if (depth === 0) {
|
|
10493
|
+
endPos = i + 1;
|
|
10494
|
+
break;
|
|
10495
|
+
}
|
|
10496
|
+
}
|
|
10497
|
+
}
|
|
10498
|
+
}
|
|
10499
|
+
if (endPos <= startPos) return null;
|
|
10500
|
+
try {
|
|
10501
|
+
return JSON.parse(text.substring(startPos, endPos));
|
|
10502
|
+
} catch {
|
|
10503
|
+
return null;
|
|
9900
10504
|
}
|
|
9901
|
-
}
|
|
9902
|
-
|
|
9903
|
-
|
|
10505
|
+
}
|
|
10506
|
+
async function buildScriptComponent(comp, ctx) {
|
|
10507
|
+
const { queries, queryIds, columnsByIndex, availableComponents } = ctx;
|
|
10508
|
+
let validationCols = /* @__PURE__ */ new Map();
|
|
10509
|
+
let validationRowCount = 0;
|
|
10510
|
+
let specSourceRef = "";
|
|
10511
|
+
const wantedId = comp.componentId || "";
|
|
10512
|
+
const wantedName = comp.componentName || "";
|
|
10513
|
+
const template = availableComponents.find(
|
|
10514
|
+
(c) => c.id === wantedId || c.name === wantedId || c.id === wantedName || c.name === wantedName
|
|
10515
|
+
);
|
|
10516
|
+
if (!template) {
|
|
10517
|
+
logger.warn(`[ScriptComponentGen] Component "${wantedId || wantedName}" not found \u2014 skipping`);
|
|
10518
|
+
return null;
|
|
10519
|
+
}
|
|
10520
|
+
if (template.type === "MarkdownBlock") {
|
|
10521
|
+
const p = comp.props || {};
|
|
10522
|
+
const content = p.content ?? p.config?.content ?? p.config?.markdown ?? p.config?.text ?? "";
|
|
10523
|
+
return {
|
|
10524
|
+
component: {
|
|
10525
|
+
id: `comp_${Math.random().toString(36).substring(2, 8)}`,
|
|
10526
|
+
name: template.name,
|
|
10527
|
+
displayName: template.displayName,
|
|
10528
|
+
type: template.type,
|
|
10529
|
+
description: template.description,
|
|
10530
|
+
props: { title: p.title, description: p.description, content },
|
|
10531
|
+
category: template.category,
|
|
10532
|
+
keywords: template.keywords
|
|
10533
|
+
},
|
|
10534
|
+
// Persist a content-bearing spec so the markdown survives deterministic
|
|
10535
|
+
// replay (it has no data source, so its text must be stored — otherwise
|
|
10536
|
+
// re-running a matched script rebuilds only the source-bound components
|
|
10537
|
+
// and silently drops the narrative).
|
|
10538
|
+
spec: {
|
|
10539
|
+
componentType: template.name,
|
|
10540
|
+
sourceRef: "markdown",
|
|
10541
|
+
title: p.title,
|
|
10542
|
+
description: p.description,
|
|
10543
|
+
content,
|
|
10544
|
+
config: {}
|
|
10545
|
+
}
|
|
10546
|
+
};
|
|
10547
|
+
}
|
|
10548
|
+
const sourceIdx = typeof comp.sourceIndex === "number" ? comp.sourceIndex : 0;
|
|
10549
|
+
const targetQuery = queries[sourceIdx] || queries[0];
|
|
10550
|
+
const targetQueryId = queryIds[targetQuery.sourceId];
|
|
10551
|
+
if (!targetQueryId) {
|
|
10552
|
+
logger.warn(`[ScriptComponentGen] No queryId for source "${targetQuery.sourceName}" \u2014 skipping`);
|
|
10553
|
+
return null;
|
|
10554
|
+
}
|
|
10555
|
+
validationCols = columnsByIndex[sourceIdx] || inferColumns(targetQuery.data);
|
|
10556
|
+
validationRowCount = targetQuery.count ?? (targetQuery.data?.length || 0);
|
|
10557
|
+
specSourceRef = targetQuery.sourceId;
|
|
10558
|
+
const renderedToolId = targetQuery.virtual ? "script_dataset" : targetQuery.sourceId;
|
|
10559
|
+
const externalToolProp = { toolId: renderedToolId, toolName: targetQuery.sourceName, parameters: { queryId: targetQueryId } };
|
|
10560
|
+
const repaired = validateAndRepairComponent(comp, template.type, validationCols, validationRowCount);
|
|
10561
|
+
if (!repaired) {
|
|
10562
|
+
logger.warn(`[ScriptComponentGen] Component "${template.name}" dropped \u2014 config keys could not be mapped to the data columns`);
|
|
10563
|
+
return null;
|
|
10564
|
+
}
|
|
10565
|
+
if (template.type === "DataTable" && Array.isArray(comp.props?.config?.columns)) {
|
|
10566
|
+
comp.props.config.columns = comp.props.config.columns.map(
|
|
10567
|
+
(c) => typeof c === "string" ? { key: c, label: c.replace(/[_-]+/g, " ").replace(/\b\w/g, (m) => m.toUpperCase()) } : c
|
|
10568
|
+
);
|
|
10569
|
+
}
|
|
10570
|
+
const component = {
|
|
10571
|
+
id: `comp_${Math.random().toString(36).substring(2, 8)}`,
|
|
10572
|
+
name: template.name,
|
|
10573
|
+
displayName: template.displayName,
|
|
10574
|
+
type: template.type,
|
|
10575
|
+
description: template.description,
|
|
10576
|
+
props: { ...comp.props, externalTool: externalToolProp },
|
|
10577
|
+
category: template.category,
|
|
10578
|
+
keywords: template.keywords
|
|
10579
|
+
};
|
|
10580
|
+
const spec = specSourceRef ? {
|
|
10581
|
+
componentType: template.name,
|
|
10582
|
+
sourceRef: specSourceRef,
|
|
10583
|
+
title: comp.props?.title,
|
|
10584
|
+
description: comp.props?.description,
|
|
10585
|
+
config: { ...comp.props?.config || {} }
|
|
10586
|
+
} : void 0;
|
|
10587
|
+
return { component, spec };
|
|
10588
|
+
}
|
|
9904
10589
|
async function generateScriptComponents(params) {
|
|
9905
|
-
const { userPrompt, scriptResult, queryIds, availableComponents, apiKey, model, componentStreamCallback,
|
|
10590
|
+
const { userPrompt, scriptResult, queryIds, availableComponents, apiKey, model, componentStreamCallback, analysisText } = params;
|
|
9906
10591
|
const queries = scriptResult.executedQueries;
|
|
9907
10592
|
if (!queries || queries.length === 0) {
|
|
9908
10593
|
logger.warn(`[ScriptComponentGen] No query data available \u2014 returning empty`);
|
|
9909
|
-
return { components: [], layoutTitle: "", layoutDescription: "", actions: [] };
|
|
10594
|
+
return { components: [], layoutTitle: "", layoutDescription: "", actions: [], componentSpecs: [] };
|
|
9910
10595
|
}
|
|
9911
10596
|
logger.info(`[ScriptComponentGen] Starting | ${queries.length} queries | ${availableComponents.length} available components`);
|
|
9912
|
-
const
|
|
10597
|
+
const columnsByIndex = queries.map((q) => {
|
|
10598
|
+
const data = q.data && q.data.length ? q.data : q.virtual ? scriptResult.data : q.data;
|
|
10599
|
+
return inferColumns(data || []);
|
|
10600
|
+
});
|
|
10601
|
+
const availableCompsText = availableComponents.map((c) => `- ${c.name} (${c.type}): ${c.description || ""}`).join("\n");
|
|
9913
10602
|
const sourceDescriptions = queries.map((q, idx) => {
|
|
9914
10603
|
if (!q.data || q.data.length === 0) return null;
|
|
9915
|
-
const
|
|
9916
|
-
const columns =
|
|
9917
|
-
|
|
9918
|
-
return `${key} (${type})`;
|
|
9919
|
-
}).join(", ");
|
|
9920
|
-
const sampleData = JSON.stringify(q.data.slice(0, 3), null, 2);
|
|
10604
|
+
const cols = columnsByIndex[idx];
|
|
10605
|
+
const columns = cols && cols.size > 0 ? Array.from(cols.values()).map((c) => `${c.name} (${c.type})`).join(", ") : Object.keys(q.data[0]).join(", ");
|
|
10606
|
+
const sampleData = JSON.stringify(q.data.slice(0, TOOL_TRACKING_SAMPLE_ROWS), null, 2);
|
|
9921
10607
|
return `### Source ${idx}: "${q.sourceName}" (sourceIndex: ${idx})
|
|
9922
10608
|
- Rows: ${q.count}${q.totalCount && q.totalCount > q.count ? ` of ${q.totalCount} total` : ""}
|
|
9923
10609
|
- Columns: ${columns}
|
|
@@ -9926,19 +10612,6 @@ async function generateScriptComponents(params) {
|
|
|
9926
10612
|
${sampleData}
|
|
9927
10613
|
\`\`\``;
|
|
9928
10614
|
}).filter(Boolean).join("\n\n");
|
|
9929
|
-
const isMultiSource = queries.length > 1;
|
|
9930
|
-
const federationTool = externalTools?.find((t) => t.id === "federation_query");
|
|
9931
|
-
let federationContext = "";
|
|
9932
|
-
if (isMultiSource && federationTool) {
|
|
9933
|
-
federationContext = `### Cross-Source Federation Available
|
|
9934
|
-
You can create components that JOIN data from multiple sources using DuckDB SQL.
|
|
9935
|
-
Set sourceIndex to "federation" and provide federationSql.
|
|
9936
|
-
${federationTool.fullSchema || federationTool.description || "Use schema-qualified table names."}`;
|
|
9937
|
-
} else if (isMultiSource) {
|
|
9938
|
-
federationContext = `### Multiple Sources Available
|
|
9939
|
-
Each component can use data from one source (specify sourceIndex: 0, 1, etc.).
|
|
9940
|
-
Cross-source JOINs are not available for this project.`;
|
|
9941
|
-
}
|
|
9942
10615
|
try {
|
|
9943
10616
|
const prompts = await promptLoader.loadPrompts("script-components", {
|
|
9944
10617
|
USER_PROMPT: userPrompt,
|
|
@@ -9947,95 +10620,98 @@ Cross-source JOINs are not available for this project.`;
|
|
|
9947
10620
|
ROW_COUNT: queries.map((q) => `${q.sourceName}: ${q.count}`).join(", "),
|
|
9948
10621
|
COLUMN_DESCRIPTIONS: sourceDescriptions,
|
|
9949
10622
|
SAMPLE_DATA: "",
|
|
9950
|
-
|
|
10623
|
+
ANALYSIS_TEXT: analysisText && analysisText.trim() ? analysisText.trim() : "No analysis text provided \u2014 infer intent from the user question and the returned data."
|
|
9951
10624
|
});
|
|
10625
|
+
const ctx = {
|
|
10626
|
+
queries,
|
|
10627
|
+
queryIds,
|
|
10628
|
+
columnsByIndex,
|
|
10629
|
+
availableComponents
|
|
10630
|
+
};
|
|
10631
|
+
let fullText = "";
|
|
10632
|
+
let answerAttempted = false;
|
|
10633
|
+
let streamedAnswerKey = null;
|
|
10634
|
+
const partial = componentStreamCallback ? (chunk) => {
|
|
10635
|
+
fullText += chunk;
|
|
10636
|
+
if (answerAttempted) return;
|
|
10637
|
+
const answerComp = extractAnswerComponentObject(fullText);
|
|
10638
|
+
if (!answerComp) return;
|
|
10639
|
+
answerAttempted = true;
|
|
10640
|
+
streamedAnswerKey = scriptCompKey(answerComp);
|
|
10641
|
+
buildScriptComponent(answerComp, ctx).then((built) => {
|
|
10642
|
+
if (built) {
|
|
10643
|
+
componentStreamCallback(built.component);
|
|
10644
|
+
logger.info("[ScriptComponentGen] Streamed answer component early");
|
|
10645
|
+
} else {
|
|
10646
|
+
streamedAnswerKey = null;
|
|
10647
|
+
}
|
|
10648
|
+
}).catch(() => {
|
|
10649
|
+
streamedAnswerKey = null;
|
|
10650
|
+
});
|
|
10651
|
+
} : void 0;
|
|
9952
10652
|
const response = await LLM.stream(
|
|
9953
10653
|
{ sys: prompts.system, user: prompts.user },
|
|
9954
10654
|
{
|
|
9955
10655
|
model: model || void 0,
|
|
9956
|
-
maxTokens:
|
|
10656
|
+
maxTokens: 5e3,
|
|
9957
10657
|
temperature: 0,
|
|
9958
|
-
apiKey
|
|
10658
|
+
apiKey,
|
|
10659
|
+
partial
|
|
9959
10660
|
},
|
|
9960
10661
|
true
|
|
9961
10662
|
);
|
|
9962
10663
|
if (!response || !response.components || !Array.isArray(response.components)) {
|
|
9963
10664
|
logger.warn(`[ScriptComponentGen] LLM returned invalid response`);
|
|
9964
|
-
return { components: [], layoutTitle: "", layoutDescription: "", actions: [] };
|
|
10665
|
+
return { components: [], layoutTitle: "", layoutDescription: "", actions: [], componentSpecs: [] };
|
|
9965
10666
|
}
|
|
9966
10667
|
const layoutTitle = response.layoutTitle || "Dashboard";
|
|
9967
10668
|
const layoutDescription = response.layoutDescription || "";
|
|
9968
10669
|
const matchedComponents = [];
|
|
10670
|
+
const componentSpecs = [];
|
|
9969
10671
|
for (const comp of response.components) {
|
|
9970
|
-
const
|
|
9971
|
-
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
)
|
|
9975
|
-
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
|
|
9980
|
-
|
|
9981
|
-
|
|
9982
|
-
|
|
9983
|
-
|
|
9984
|
-
);
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
} else {
|
|
9988
|
-
logger.info(`[ScriptComponentGen] Federation component \u2014 executing cross-source SQL`);
|
|
9989
|
-
try {
|
|
9990
|
-
const fedResult = await executeFederationQuery(comp.federationSql, externalTools, collections);
|
|
9991
|
-
if (fedResult) {
|
|
9992
|
-
const fedQueryId = queryCache.storeQuery(comp.federationSql, fedResult);
|
|
9993
|
-
externalToolProp = {
|
|
9994
|
-
toolId: "federation_query",
|
|
9995
|
-
toolName: "Cross-Source Query",
|
|
9996
|
-
parameters: { queryId: fedQueryId }
|
|
9997
|
-
};
|
|
9998
|
-
} else {
|
|
9999
|
-
logger.warn(`[ScriptComponentGen] Federation query failed \u2014 skipping component`);
|
|
10000
|
-
continue;
|
|
10001
|
-
}
|
|
10002
|
-
} catch (fedErr) {
|
|
10003
|
-
logger.warn(`[ScriptComponentGen] Federation query error: ${fedErr} \u2014 skipping component`);
|
|
10004
|
-
continue;
|
|
10005
|
-
}
|
|
10672
|
+
const built = await buildScriptComponent(comp, ctx);
|
|
10673
|
+
if (!built) continue;
|
|
10674
|
+
matchedComponents.push(built.component);
|
|
10675
|
+
if (built.spec) componentSpecs.push(built.spec);
|
|
10676
|
+
if (componentStreamCallback && scriptCompKey(comp) !== streamedAnswerKey) {
|
|
10677
|
+
componentStreamCallback(built.component);
|
|
10678
|
+
}
|
|
10679
|
+
}
|
|
10680
|
+
if (response.hasAnswerComponent && response.answerComponent) {
|
|
10681
|
+
const aKey = scriptCompKey(response.answerComponent);
|
|
10682
|
+
const alreadyIncluded = response.components.some((c) => scriptCompKey(c) === aKey);
|
|
10683
|
+
if (!alreadyIncluded) {
|
|
10684
|
+
const built = await buildScriptComponent(response.answerComponent, ctx);
|
|
10685
|
+
if (built) {
|
|
10686
|
+
matchedComponents.unshift(built.component);
|
|
10687
|
+
if (built.spec) componentSpecs.unshift(built.spec);
|
|
10688
|
+
if (componentStreamCallback && aKey !== streamedAnswerKey) componentStreamCallback(built.component);
|
|
10006
10689
|
}
|
|
10007
10690
|
}
|
|
10008
|
-
|
|
10009
|
-
|
|
10010
|
-
|
|
10011
|
-
|
|
10012
|
-
|
|
10013
|
-
|
|
10014
|
-
|
|
10015
|
-
|
|
10016
|
-
|
|
10017
|
-
|
|
10018
|
-
|
|
10019
|
-
|
|
10020
|
-
|
|
10691
|
+
}
|
|
10692
|
+
const hasMarkdown = matchedComponents.some((c) => c.type === "MarkdownBlock");
|
|
10693
|
+
if (!hasMarkdown && analysisText && analysisText.trim()) {
|
|
10694
|
+
const mdTemplate = availableComponents.find((c) => c.type === "MarkdownBlock" || c.name === "DynamicMarkdownBlock");
|
|
10695
|
+
if (mdTemplate) {
|
|
10696
|
+
const mdComponent = {
|
|
10697
|
+
id: `comp_${Math.random().toString(36).substring(2, 8)}`,
|
|
10698
|
+
name: mdTemplate.name,
|
|
10699
|
+
displayName: mdTemplate.displayName,
|
|
10700
|
+
type: mdTemplate.type,
|
|
10701
|
+
description: mdTemplate.description,
|
|
10702
|
+
props: { content: analysisText.trim() },
|
|
10703
|
+
category: mdTemplate.category,
|
|
10704
|
+
keywords: mdTemplate.keywords
|
|
10021
10705
|
};
|
|
10022
|
-
|
|
10023
|
-
|
|
10024
|
-
|
|
10025
|
-
|
|
10026
|
-
|
|
10027
|
-
|
|
10028
|
-
|
|
10029
|
-
|
|
10030
|
-
|
|
10031
|
-
externalTool: externalToolProp
|
|
10032
|
-
},
|
|
10033
|
-
category: template.category,
|
|
10034
|
-
keywords: template.keywords
|
|
10035
|
-
};
|
|
10036
|
-
matchedComponents.push(component);
|
|
10037
|
-
if (componentStreamCallback) {
|
|
10038
|
-
componentStreamCallback(component);
|
|
10706
|
+
matchedComponents.push(mdComponent);
|
|
10707
|
+
componentSpecs.push({
|
|
10708
|
+
componentType: mdTemplate.name,
|
|
10709
|
+
sourceRef: "markdown",
|
|
10710
|
+
content: analysisText.trim(),
|
|
10711
|
+
config: {}
|
|
10712
|
+
});
|
|
10713
|
+
if (componentStreamCallback) componentStreamCallback(mdComponent);
|
|
10714
|
+
logger.info("[ScriptComponentGen] Injected fallback DynamicMarkdownBlock (LLM omitted one)");
|
|
10039
10715
|
}
|
|
10040
10716
|
}
|
|
10041
10717
|
logger.info(`[ScriptComponentGen] Generated ${matchedComponents.length} components`);
|
|
@@ -10049,37 +10725,97 @@ Cross-source JOINs are not available for this project.`;
|
|
|
10049
10725
|
components: matchedComponents,
|
|
10050
10726
|
layoutTitle,
|
|
10051
10727
|
layoutDescription,
|
|
10052
|
-
actions
|
|
10728
|
+
actions,
|
|
10729
|
+
componentSpecs
|
|
10053
10730
|
};
|
|
10054
10731
|
} catch (error) {
|
|
10055
10732
|
const msg = error instanceof Error ? error.message : String(error);
|
|
10056
10733
|
logger.error(`[ScriptComponentGen] Failed: ${msg}`);
|
|
10057
|
-
return { components: [], layoutTitle: "", layoutDescription: "", actions: [] };
|
|
10734
|
+
return { components: [], layoutTitle: "", layoutDescription: "", actions: [], componentSpecs: [] };
|
|
10058
10735
|
}
|
|
10059
10736
|
}
|
|
10060
|
-
|
|
10061
|
-
const
|
|
10062
|
-
if (
|
|
10063
|
-
|
|
10064
|
-
|
|
10065
|
-
|
|
10066
|
-
|
|
10067
|
-
|
|
10737
|
+
function assembleComponentsFromSpecs(params) {
|
|
10738
|
+
const { specs, scriptResult, queryIds, availableComponents, componentStreamCallback, analysisText } = params;
|
|
10739
|
+
if (!specs || specs.length === 0) return null;
|
|
10740
|
+
const queries = scriptResult.executedQueries || [];
|
|
10741
|
+
const byId = new Map(queries.map((q) => [q.sourceId, q]));
|
|
10742
|
+
const out = [];
|
|
10743
|
+
let answerComponentStreamed = false;
|
|
10744
|
+
const mdTemplate = availableComponents.find((c) => c.type === "MarkdownBlock" || c.name === "DynamicMarkdownBlock");
|
|
10745
|
+
const buildMarkdown = (content, title, description) => {
|
|
10746
|
+
if (!mdTemplate || !content || !content.trim()) return null;
|
|
10747
|
+
return {
|
|
10748
|
+
id: `comp_${Math.random().toString(36).substring(2, 8)}`,
|
|
10749
|
+
name: mdTemplate.name,
|
|
10750
|
+
displayName: mdTemplate.displayName,
|
|
10751
|
+
type: mdTemplate.type,
|
|
10752
|
+
description: mdTemplate.description,
|
|
10753
|
+
props: { title, description, content: content.trim() },
|
|
10754
|
+
category: mdTemplate.category,
|
|
10755
|
+
keywords: mdTemplate.keywords
|
|
10756
|
+
};
|
|
10757
|
+
};
|
|
10758
|
+
for (const spec of specs) {
|
|
10759
|
+
if (spec.sourceRef === "federation") {
|
|
10760
|
+
logger.info("[ScriptComponentGen] Recipe has a federation spec \u2014 not replayable deterministically, falling back to LLM");
|
|
10761
|
+
return null;
|
|
10762
|
+
}
|
|
10763
|
+
if (spec.sourceRef === "markdown") {
|
|
10764
|
+
const md = buildMarkdown(spec.content || analysisText || "", spec.title, spec.description);
|
|
10765
|
+
if (md) {
|
|
10766
|
+
out.push(md);
|
|
10767
|
+
if (componentStreamCallback) componentStreamCallback(md);
|
|
10768
|
+
}
|
|
10769
|
+
continue;
|
|
10770
|
+
}
|
|
10771
|
+
const q = byId.get(spec.sourceRef) || queries.find((x) => x.virtual);
|
|
10772
|
+
if (!q || !q.data || q.data.length === 0) {
|
|
10773
|
+
logger.info(`[ScriptComponentGen] Recipe spec source "${spec.sourceRef}" missing/empty on replay \u2014 falling back to LLM`);
|
|
10774
|
+
return null;
|
|
10775
|
+
}
|
|
10776
|
+
const template = availableComponents.find((c) => c.name === spec.componentType || c.id === spec.componentType);
|
|
10777
|
+
if (!template) {
|
|
10778
|
+
logger.info(`[ScriptComponentGen] Recipe spec component "${spec.componentType}" not in library \u2014 falling back to LLM`);
|
|
10779
|
+
return null;
|
|
10780
|
+
}
|
|
10781
|
+
const cols = inferColumns(q.data);
|
|
10782
|
+
const comp = { props: { title: spec.title, description: spec.description, config: { ...spec.config || {} } } };
|
|
10783
|
+
const repaired = validateAndRepairComponent(comp, template.type, cols, q.count ?? q.data.length);
|
|
10784
|
+
if (!repaired) {
|
|
10785
|
+
logger.info(`[ScriptComponentGen] Recipe spec "${spec.componentType}" no longer maps to columns (shape drift) \u2014 falling back to LLM`);
|
|
10786
|
+
return null;
|
|
10787
|
+
}
|
|
10788
|
+
const queryId = queryIds[q.sourceId] || queryCache.storeQuery(q.sql, { data: q.data, count: q.count });
|
|
10789
|
+
const toolId = q.virtual ? "script_dataset" : q.sourceId;
|
|
10790
|
+
const component = {
|
|
10791
|
+
id: `comp_${Math.random().toString(36).substring(2, 8)}`,
|
|
10792
|
+
name: template.name,
|
|
10793
|
+
displayName: template.displayName,
|
|
10794
|
+
type: template.type,
|
|
10795
|
+
description: template.description,
|
|
10796
|
+
props: {
|
|
10797
|
+
...comp.props,
|
|
10798
|
+
externalTool: { toolId, toolName: q.sourceName, parameters: { queryId } }
|
|
10799
|
+
},
|
|
10800
|
+
category: template.category,
|
|
10801
|
+
keywords: template.keywords
|
|
10802
|
+
};
|
|
10803
|
+
out.push(component);
|
|
10804
|
+
if (componentStreamCallback && !answerComponentStreamed) {
|
|
10805
|
+
componentStreamCallback(component);
|
|
10806
|
+
answerComponentStreamed = true;
|
|
10068
10807
|
}
|
|
10069
10808
|
}
|
|
10070
|
-
if (
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
|
|
10075
|
-
|
|
10076
|
-
});
|
|
10077
|
-
return result;
|
|
10078
|
-
} catch (err) {
|
|
10079
|
-
logger.warn(`[ScriptComponentGen] Federation via collections failed: ${err}`);
|
|
10809
|
+
if (out.length > 0 && !out.some((c) => c.type === "MarkdownBlock")) {
|
|
10810
|
+
const md = buildMarkdown(analysisText || "");
|
|
10811
|
+
if (md) {
|
|
10812
|
+
out.push(md);
|
|
10813
|
+
if (componentStreamCallback) componentStreamCallback(md);
|
|
10814
|
+
logger.info("[ScriptComponentGen] Injected markdown on replay (recipe had no markdown spec)");
|
|
10080
10815
|
}
|
|
10081
10816
|
}
|
|
10082
|
-
|
|
10817
|
+
logger.info(`[ScriptComponentGen] Deterministically assembled ${out.length} component(s) from recipe \u2014 no LLM call`);
|
|
10818
|
+
return out.length > 0 ? out : null;
|
|
10083
10819
|
}
|
|
10084
10820
|
|
|
10085
10821
|
// src/userResponse/anthropic.ts
|
|
@@ -12047,6 +12783,24 @@ var OpenAILLM = class extends BaseLLM {
|
|
|
12047
12783
|
var openaiLLM = new OpenAILLM();
|
|
12048
12784
|
|
|
12049
12785
|
// src/userResponse/agent-user-response.ts
|
|
12786
|
+
function hasExpiredScriptDataset(component) {
|
|
12787
|
+
if (!component || typeof component !== "object") return false;
|
|
12788
|
+
const et = component?.props?.externalTool;
|
|
12789
|
+
if (et?.toolId === "script_dataset") {
|
|
12790
|
+
const qid = et?.parameters?.queryId;
|
|
12791
|
+
if (typeof qid === "string" && qid.length > 0) {
|
|
12792
|
+
const stored = queryCache.getQuery(qid);
|
|
12793
|
+
if (!stored || stored.data == null) return true;
|
|
12794
|
+
}
|
|
12795
|
+
}
|
|
12796
|
+
const children = component?.props?.config?.components;
|
|
12797
|
+
if (Array.isArray(children)) {
|
|
12798
|
+
for (const child of children) {
|
|
12799
|
+
if (hasExpiredScriptDataset(child)) return true;
|
|
12800
|
+
}
|
|
12801
|
+
}
|
|
12802
|
+
return false;
|
|
12803
|
+
}
|
|
12050
12804
|
function rehydrateCachedComponent(component) {
|
|
12051
12805
|
const qMap = component?.props?.config?._queryMap;
|
|
12052
12806
|
if (!qMap || typeof qMap !== "object" || Object.keys(qMap).length === 0) return component;
|
|
@@ -12096,7 +12850,24 @@ function getLLMInstance(provider) {
|
|
|
12096
12850
|
return anthropicLLM;
|
|
12097
12851
|
}
|
|
12098
12852
|
}
|
|
12099
|
-
|
|
12853
|
+
function storeScriptDatasetQuery(q, ctx) {
|
|
12854
|
+
const data = q.data.length > MAX_ROWS_RENDERED ? q.data.slice(0, MAX_ROWS_RENDERED) : q.data;
|
|
12855
|
+
if (q.data.length > MAX_ROWS_RENDERED) {
|
|
12856
|
+
logger.warn(`[AgentFlow] Script dataset "${q.sourceId}" had ${q.data.length} rows \u2014 capped to ${MAX_ROWS_RENDERED} for render/cache. Consider pre-aggregating in the script.`);
|
|
12857
|
+
}
|
|
12858
|
+
const count = data.length;
|
|
12859
|
+
if (q.virtual && ctx.recipeId) {
|
|
12860
|
+
const descriptor = {
|
|
12861
|
+
kind: "script_dataset",
|
|
12862
|
+
recipeId: ctx.recipeId,
|
|
12863
|
+
params: ctx.params || {},
|
|
12864
|
+
datasetRef: q.sourceId
|
|
12865
|
+
};
|
|
12866
|
+
return queryCache.storeQuery(JSON.stringify(descriptor), { data, count });
|
|
12867
|
+
}
|
|
12868
|
+
return queryCache.storeQuery(q.sql, { data, count });
|
|
12869
|
+
}
|
|
12870
|
+
var get_agent_user_response = async (prompt, components, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, conversationHistory, streamCallback, collections, externalTools, userId, mainAgentModel, sourceAgentModel, workflows) => {
|
|
12100
12871
|
const startTime = Date.now();
|
|
12101
12872
|
const providers = llmProviders || ["anthropic"];
|
|
12102
12873
|
const provider = providers[0];
|
|
@@ -12105,19 +12876,19 @@ var get_agent_user_response = async (prompt, components, anthropicApiKey, groqAp
|
|
|
12105
12876
|
logger.logLLMPrompt("agentUserResponse", "user", `User Prompt: ${prompt}`);
|
|
12106
12877
|
logger.info(`[AgentFlow] Starting | provider: ${provider} | prompt: "${prompt.substring(0, 50)}..."`);
|
|
12107
12878
|
try {
|
|
12108
|
-
const conversationMatch = await conversation_search_default.
|
|
12879
|
+
const conversationMatch = await conversation_search_default.findExactMatch({
|
|
12109
12880
|
userPrompt: prompt,
|
|
12110
12881
|
collections,
|
|
12111
|
-
userId
|
|
12112
|
-
similarityThreshold: EXACT_MATCH_SIMILARITY_THRESHOLD
|
|
12882
|
+
userId
|
|
12113
12883
|
});
|
|
12114
12884
|
if (conversationMatch) {
|
|
12115
|
-
logger.info(`[AgentFlow] Found matching conversation (${(conversationMatch.similarity * 100).toFixed(2)}% similarity)`);
|
|
12116
12885
|
const rawComponent = conversationMatch.uiBlock?.component || conversationMatch.uiBlock?.generatedComponentMetadata;
|
|
12117
12886
|
const isValidComponent = rawComponent && typeof rawComponent === "object" && Object.keys(rawComponent).length > 0;
|
|
12118
12887
|
const component = isValidComponent ? rawComponent : null;
|
|
12119
12888
|
const cachedTextResponse = conversationMatch.uiBlock?.analysis || conversationMatch.uiBlock?.textResponse || conversationMatch.uiBlock?.text || "";
|
|
12120
|
-
if (
|
|
12889
|
+
if (component && hasExpiredScriptDataset(component)) {
|
|
12890
|
+
logger.info(`[AgentFlow] Exact match found but cached UIBlock has expired script_dataset \u2014 falling through to script matcher`);
|
|
12891
|
+
} else {
|
|
12121
12892
|
if (streamCallback && cachedTextResponse) {
|
|
12122
12893
|
streamCallback(cachedTextResponse);
|
|
12123
12894
|
}
|
|
@@ -12131,16 +12902,14 @@ var get_agent_user_response = async (prompt, components, anthropicApiKey, groqAp
|
|
|
12131
12902
|
component: rehydratedComponent,
|
|
12132
12903
|
actions: conversationMatch.uiBlock?.actions || [],
|
|
12133
12904
|
reasoning: `Exact match from previous conversation`,
|
|
12134
|
-
method: `${provider}-agent-
|
|
12135
|
-
semanticSimilarity:
|
|
12905
|
+
method: `${provider}-agent-exact-match`,
|
|
12906
|
+
semanticSimilarity: 1
|
|
12136
12907
|
},
|
|
12137
12908
|
errors: []
|
|
12138
12909
|
};
|
|
12139
12910
|
}
|
|
12140
|
-
logger.info(`[AgentFlow] Similar match but below exact threshold \u2014 proceeding with agent`);
|
|
12141
|
-
} else {
|
|
12142
|
-
logger.info(`[AgentFlow] No matching conversations \u2014 proceeding with agent`);
|
|
12143
12911
|
}
|
|
12912
|
+
logger.info(`[AgentFlow] No exact match \u2014 proceeding with agent`);
|
|
12144
12913
|
const apiKey = (() => {
|
|
12145
12914
|
switch (provider) {
|
|
12146
12915
|
case "anthropic":
|
|
@@ -12205,11 +12974,11 @@ var get_agent_user_response = async (prompt, components, anthropicApiKey, groqAp
|
|
|
12205
12974
|
projectId
|
|
12206
12975
|
};
|
|
12207
12976
|
const streamBuffer = new StreamBuffer(streamCallback);
|
|
12208
|
-
const scriptStore = new ScriptStore();
|
|
12977
|
+
const scriptStore = new ScriptStore({ collections, projectId });
|
|
12209
12978
|
const turnId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
12210
12979
|
const FORK_DEPTH_CAP = 3;
|
|
12211
12980
|
let forkContext;
|
|
12212
|
-
if (scriptStore.count() > 0) {
|
|
12981
|
+
if (await scriptStore.count() > 0) {
|
|
12213
12982
|
const scriptMatcher = new ScriptMatcher(scriptStore);
|
|
12214
12983
|
const scriptMatch = await scriptMatcher.match(prompt, apiKey, sourceAgentModel);
|
|
12215
12984
|
if (scriptMatch && scriptMatch.tier === "near") {
|
|
@@ -12243,7 +13012,7 @@ var get_agent_user_response = async (prompt, components, anthropicApiKey, groqAp
|
|
|
12243
13012
|
{ externalTools: agentTools, streamBuffer }
|
|
12244
13013
|
);
|
|
12245
13014
|
if (scriptResult.success && scriptResult.data.length > 0) {
|
|
12246
|
-
scriptStore.recordSuccess(scriptMatch.recipe.id);
|
|
13015
|
+
await scriptStore.recordSuccess(scriptMatch.recipe.id);
|
|
12247
13016
|
const dataContext = scriptResult.executedQueries.map((q) => {
|
|
12248
13017
|
const preview = q.data.slice(0, 15);
|
|
12249
13018
|
return `## Data from "${q.sourceName}"
|
|
@@ -12281,7 +13050,7 @@ ${JSON.stringify(preview, null, 2)}
|
|
|
12281
13050
|
result: {
|
|
12282
13051
|
_totalRecords: q.totalCount || q.count,
|
|
12283
13052
|
_recordsShown: q.data.length,
|
|
12284
|
-
_sampleData: q.data.slice(0,
|
|
13053
|
+
_sampleData: q.data.slice(0, TOOL_TRACKING_SAMPLE_ROWS)
|
|
12285
13054
|
}
|
|
12286
13055
|
}));
|
|
12287
13056
|
let matchedComponents2 = [];
|
|
@@ -12295,19 +13064,31 @@ ${JSON.stringify(preview, null, 2)}
|
|
|
12295
13064
|
streamBuffer.write("__TEXT_COMPLETE__COMPONENT_GENERATION_START__");
|
|
12296
13065
|
streamBuffer.flush();
|
|
12297
13066
|
}
|
|
12298
|
-
const componentStreamCallback =
|
|
12299
|
-
const answerMarker = `__ANSWER_COMPONENT_START__${JSON.stringify(component)}__ANSWER_COMPONENT_END__`;
|
|
12300
|
-
streamBuffer.write(answerMarker);
|
|
12301
|
-
streamBuffer.flush();
|
|
12302
|
-
} : void 0;
|
|
13067
|
+
const componentStreamCallback = void 0;
|
|
12303
13068
|
const scriptQueryIds = {};
|
|
12304
13069
|
for (const q of scriptResult.executedQueries) {
|
|
12305
|
-
scriptQueryIds[q.sourceId] =
|
|
12306
|
-
|
|
12307
|
-
|
|
13070
|
+
scriptQueryIds[q.sourceId] = storeScriptDatasetQuery(q, {
|
|
13071
|
+
recipeId: scriptMatch.recipe.id,
|
|
13072
|
+
params: scriptMatch.extractedParams || {}
|
|
12308
13073
|
});
|
|
12309
13074
|
}
|
|
12310
|
-
|
|
13075
|
+
let usedRecipeComponents = false;
|
|
13076
|
+
if (scriptMatch.recipe.components && scriptMatch.recipe.components.length > 0) {
|
|
13077
|
+
const assembled = assembleComponentsFromSpecs({
|
|
13078
|
+
specs: scriptMatch.recipe.components,
|
|
13079
|
+
scriptResult,
|
|
13080
|
+
queryIds: scriptQueryIds,
|
|
13081
|
+
availableComponents: components,
|
|
13082
|
+
componentStreamCallback,
|
|
13083
|
+
analysisText: scriptTextResponse
|
|
13084
|
+
});
|
|
13085
|
+
if (assembled && assembled.length > 0) {
|
|
13086
|
+
matchedComponents2 = assembled;
|
|
13087
|
+
layoutTitle2 = scriptMatch.recipe.name || layoutTitle2;
|
|
13088
|
+
usedRecipeComponents = true;
|
|
13089
|
+
}
|
|
13090
|
+
}
|
|
13091
|
+
if (!usedRecipeComponents) try {
|
|
12311
13092
|
const compResult = await generateScriptComponents({
|
|
12312
13093
|
userPrompt: prompt,
|
|
12313
13094
|
scriptResult,
|
|
@@ -12317,13 +13098,22 @@ ${JSON.stringify(preview, null, 2)}
|
|
|
12317
13098
|
model: sourceAgentModel,
|
|
12318
13099
|
componentStreamCallback,
|
|
12319
13100
|
externalTools: agentTools,
|
|
12320
|
-
collections
|
|
13101
|
+
collections,
|
|
13102
|
+
analysisText: scriptTextResponse
|
|
12321
13103
|
});
|
|
12322
13104
|
if (compResult.components.length > 0) {
|
|
12323
13105
|
matchedComponents2 = compResult.components;
|
|
12324
13106
|
layoutTitle2 = compResult.layoutTitle;
|
|
12325
13107
|
layoutDescription2 = compResult.layoutDescription;
|
|
12326
13108
|
actions2 = compResult.actions;
|
|
13109
|
+
if (compResult.componentSpecs.length > 0) {
|
|
13110
|
+
try {
|
|
13111
|
+
scriptMatch.recipe.components = compResult.componentSpecs;
|
|
13112
|
+
await scriptStore.save(scriptMatch.recipe);
|
|
13113
|
+
} catch (e) {
|
|
13114
|
+
logger.warn(`[AgentFlow] Failed to backfill component recipe: ${e}`);
|
|
13115
|
+
}
|
|
13116
|
+
}
|
|
12327
13117
|
} else {
|
|
12328
13118
|
logger.info(`[AgentFlow] Lightweight component gen returned no components \u2014 falling back to full generator`);
|
|
12329
13119
|
const matchResult = await generateAgentComponents({
|
|
@@ -12416,12 +13206,12 @@ ${JSON.stringify(preview, null, 2)}
|
|
|
12416
13206
|
};
|
|
12417
13207
|
} else {
|
|
12418
13208
|
logger.warn(`[AgentFlow] Script "${scriptMatch.recipe.name}" failed: ${scriptResult.error || "no data returned"}`);
|
|
12419
|
-
scriptStore.recordFailure(scriptMatch.recipe.id);
|
|
13209
|
+
await scriptStore.recordFailure(scriptMatch.recipe.id);
|
|
12420
13210
|
}
|
|
12421
13211
|
} catch (scriptErr) {
|
|
12422
13212
|
const errMsg = scriptErr instanceof Error ? scriptErr.message : String(scriptErr);
|
|
12423
13213
|
logger.warn(`[AgentFlow] Script execution error (falling back to agent flow): ${errMsg}`);
|
|
12424
|
-
scriptStore.recordFailure(scriptMatch.recipe.id);
|
|
13214
|
+
await scriptStore.recordFailure(scriptMatch.recipe.id);
|
|
12425
13215
|
}
|
|
12426
13216
|
}
|
|
12427
13217
|
} else {
|
|
@@ -12478,13 +13268,35 @@ User question: ${prompt}`;
|
|
|
12478
13268
|
logger.warn(`[AgentFlow] Source routing pre-filter failed (using all tools): ${msg}`);
|
|
12479
13269
|
}
|
|
12480
13270
|
}
|
|
12481
|
-
const mainAgent = new MainAgent(filteredTools, agentConfig, scriptStore, turnId, streamBuffer);
|
|
13271
|
+
const mainAgent = new MainAgent(filteredTools, agentConfig, scriptStore, turnId, streamBuffer, workflows || []);
|
|
12482
13272
|
const agentResponse = await mainAgent.handleQuestion(
|
|
12483
13273
|
effectivePrompt,
|
|
12484
13274
|
apiKey,
|
|
12485
13275
|
conversationHistory,
|
|
12486
13276
|
streamBuffer.hasCallback() ? (chunk) => streamBuffer.write(chunk) : void 0
|
|
12487
13277
|
);
|
|
13278
|
+
if (agentResponse.workflow) {
|
|
13279
|
+
streamBuffer.flush();
|
|
13280
|
+
const workflowComponent = {
|
|
13281
|
+
id: `workflow_${Date.now()}`,
|
|
13282
|
+
name: agentResponse.workflow.name,
|
|
13283
|
+
type: `Workflow_${agentResponse.workflow.name}`,
|
|
13284
|
+
description: `Workflow: ${agentResponse.workflow.name}`,
|
|
13285
|
+
props: agentResponse.workflow.props
|
|
13286
|
+
};
|
|
13287
|
+
const elapsedTime2 = Date.now() - startTime;
|
|
13288
|
+
logger.info(`[AgentFlow] Workflow short-circuit | "${agentResponse.workflow.name}" | ${elapsedTime2}ms`);
|
|
13289
|
+
return {
|
|
13290
|
+
success: true,
|
|
13291
|
+
data: {
|
|
13292
|
+
text: "",
|
|
13293
|
+
component: workflowComponent,
|
|
13294
|
+
actions: [],
|
|
13295
|
+
method: `${provider}-agent-workflow`
|
|
13296
|
+
},
|
|
13297
|
+
errors: []
|
|
13298
|
+
};
|
|
13299
|
+
}
|
|
12488
13300
|
const rawText = streamBuffer.getFullText() || agentResponse.text || "I apologize, but I was unable to generate a response.";
|
|
12489
13301
|
const textResponse = rawText.replace(/_?_?SB_END_?_?/g, "").replace(/__SB_\w+_(?:START|MSG)_?_?/g, "").replace(/__QUERY_TIMER_START_[^_]*__/g, "").replace(/__QUERY_TIMER_DONE_[\d.]+__/g, "").replace(/__TEXT_COMPLETE__COMPONENT_GENERATION_START__/g, "").replace(/\[COMPLEXITY:\s*(?:simple|medium|complex)\]/gi, "");
|
|
12490
13302
|
streamBuffer.flush();
|
|
@@ -12524,6 +13336,7 @@ User question: ${prompt}`;
|
|
|
12524
13336
|
let layoutTitle = "Dashboard";
|
|
12525
13337
|
let layoutDescription = "Multi-component dashboard";
|
|
12526
13338
|
let actions = [];
|
|
13339
|
+
let authoredComponentSpecs = [];
|
|
12527
13340
|
if (!hasExecutedTools) {
|
|
12528
13341
|
logger.info(`[AgentFlow] No tools executed \u2014 general question, wrapping in DynamicMarkdownBlock`);
|
|
12529
13342
|
const mainAgentText = agentResponse.text || textResponse;
|
|
@@ -12571,9 +13384,9 @@ User question: ${prompt}`;
|
|
|
12571
13384
|
logger.info(`[AgentFlow] Using savedScript.executedQueries (${agentResponse.savedScript.executedQueries.length} entries, includes computed columns) for component gen`);
|
|
12572
13385
|
const scriptQueries = agentResponse.savedScript.executedQueries;
|
|
12573
13386
|
for (const q of scriptQueries) {
|
|
12574
|
-
agentQueryIds[q.sourceId] =
|
|
12575
|
-
|
|
12576
|
-
|
|
13387
|
+
agentQueryIds[q.sourceId] = storeScriptDatasetQuery(q, {
|
|
13388
|
+
recipeId: agentResponse.savedScript.recipeId,
|
|
13389
|
+
params: {}
|
|
12577
13390
|
});
|
|
12578
13391
|
}
|
|
12579
13392
|
agentScriptResult = {
|
|
@@ -12611,7 +13424,7 @@ User question: ${prompt}`;
|
|
|
12611
13424
|
executionTimeMs: 0
|
|
12612
13425
|
};
|
|
12613
13426
|
}
|
|
12614
|
-
const
|
|
13427
|
+
const compGenArgs = {
|
|
12615
13428
|
userPrompt: prompt,
|
|
12616
13429
|
scriptResult: agentScriptResult,
|
|
12617
13430
|
queryIds: agentQueryIds,
|
|
@@ -12620,13 +13433,29 @@ User question: ${prompt}`;
|
|
|
12620
13433
|
model: sourceAgentModel,
|
|
12621
13434
|
componentStreamCallback,
|
|
12622
13435
|
externalTools: agentTools,
|
|
12623
|
-
collections
|
|
12624
|
-
|
|
12625
|
-
|
|
13436
|
+
collections,
|
|
13437
|
+
analysisText: agentResponse.text
|
|
13438
|
+
};
|
|
13439
|
+
const MAX_COMP_ATTEMPTS = 3;
|
|
13440
|
+
let compResult;
|
|
13441
|
+
for (let attempt = 1; attempt <= MAX_COMP_ATTEMPTS; attempt++) {
|
|
13442
|
+
try {
|
|
13443
|
+
compResult = await generateScriptComponents(compGenArgs);
|
|
13444
|
+
break;
|
|
13445
|
+
} catch (err) {
|
|
13446
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
13447
|
+
const transient = /connection error|econnreset|etimedout|fetch failed|socket hang up|overloaded|rate.?limit|\b429\b|\b5\d\d\b|network|timeout/i.test(msg);
|
|
13448
|
+
if (!transient || attempt === MAX_COMP_ATTEMPTS) throw err;
|
|
13449
|
+
logger.warn(`[AgentFlow] Lightweight component gen transient error (attempt ${attempt}/${MAX_COMP_ATTEMPTS}) \u2014 retrying: ${msg}`);
|
|
13450
|
+
await new Promise((res) => setTimeout(res, 400 * attempt));
|
|
13451
|
+
}
|
|
13452
|
+
}
|
|
13453
|
+
if (compResult && compResult.components.length > 0) {
|
|
12626
13454
|
matchedComponents = compResult.components;
|
|
12627
13455
|
layoutTitle = compResult.layoutTitle;
|
|
12628
13456
|
layoutDescription = compResult.layoutDescription;
|
|
12629
13457
|
actions = compResult.actions;
|
|
13458
|
+
authoredComponentSpecs = compResult.componentSpecs;
|
|
12630
13459
|
usedLightweight = true;
|
|
12631
13460
|
logger.info(`[AgentFlow] Lightweight component gen succeeded \u2014 ${matchedComponents.length} components`);
|
|
12632
13461
|
}
|
|
@@ -12729,10 +13558,11 @@ User question: ${prompt}`;
|
|
|
12729
13558
|
forkDepth: Math.min(FORK_DEPTH_CAP, forkContext.parentDepth + 1),
|
|
12730
13559
|
forkReason: forkContext.modificationHint
|
|
12731
13560
|
} : void 0;
|
|
12732
|
-
const promoted = scriptStore.promoteToVerified(authored.recipeId, {
|
|
13561
|
+
const promoted = await scriptStore.promoteToVerified(authored.recipeId, {
|
|
12733
13562
|
sourceIds: authored.sourceIds,
|
|
12734
13563
|
tables: authored.tables,
|
|
12735
|
-
...lineage
|
|
13564
|
+
...lineage,
|
|
13565
|
+
components: authoredComponentSpecs
|
|
12736
13566
|
});
|
|
12737
13567
|
if (promoted && lineage?.parentId) {
|
|
12738
13568
|
logger.info(
|
|
@@ -12968,7 +13798,7 @@ var CONTEXT_CONFIG = {
|
|
|
12968
13798
|
};
|
|
12969
13799
|
|
|
12970
13800
|
// src/handlers/user-prompt-request.ts
|
|
12971
|
-
var get_user_request = async (data, components, sendMessage, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, externalTools, mainAgentModel, sourceAgentModel) => {
|
|
13801
|
+
var get_user_request = async (data, components, sendMessage, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, externalTools, mainAgentModel, sourceAgentModel, workflows) => {
|
|
12972
13802
|
const errors = [];
|
|
12973
13803
|
const parseResult = UserPromptRequestMessageSchema.safeParse(data);
|
|
12974
13804
|
if (!parseResult.success) {
|
|
@@ -13054,7 +13884,8 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
|
|
|
13054
13884
|
externalTools,
|
|
13055
13885
|
userId,
|
|
13056
13886
|
mainAgentModel,
|
|
13057
|
-
sourceAgentModel
|
|
13887
|
+
sourceAgentModel,
|
|
13888
|
+
workflows
|
|
13058
13889
|
);
|
|
13059
13890
|
logger.info("User prompt request completed");
|
|
13060
13891
|
const uiBlockId = existingUiBlockId;
|
|
@@ -13207,8 +14038,8 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
|
|
|
13207
14038
|
wsId
|
|
13208
14039
|
};
|
|
13209
14040
|
};
|
|
13210
|
-
async function handleUserPromptRequest(data, components, sendMessage, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, externalTools, mainAgentModel, sourceAgentModel) {
|
|
13211
|
-
const response = await get_user_request(data, components, sendMessage, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, externalTools, mainAgentModel, sourceAgentModel);
|
|
14041
|
+
async function handleUserPromptRequest(data, components, sendMessage, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, externalTools, mainAgentModel, sourceAgentModel, workflows) {
|
|
14042
|
+
const response = await get_user_request(data, components, sendMessage, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, externalTools, mainAgentModel, sourceAgentModel, workflows);
|
|
13212
14043
|
if (response.data?.component?.props?.config?.components) {
|
|
13213
14044
|
response.data.component.props.config.components = response.data.component.props.config.components.map((comp) => ({
|
|
13214
14045
|
...comp,
|
|
@@ -13640,6 +14471,24 @@ async function handleComponentListResponse(data, storeComponents, collections) {
|
|
|
13640
14471
|
}
|
|
13641
14472
|
}
|
|
13642
14473
|
|
|
14474
|
+
// src/handlers/workflow-list-response.ts
|
|
14475
|
+
async function handleWorkflowListResponse(data, storeWorkflows) {
|
|
14476
|
+
try {
|
|
14477
|
+
const parsed = WorkflowListResponseMessageSchema.parse(data);
|
|
14478
|
+
const { payload } = parsed;
|
|
14479
|
+
const workflowsList = payload.workflows;
|
|
14480
|
+
if (!workflowsList) {
|
|
14481
|
+
logger.error("Workflows list not found in WORKFLOW_LIST_RES payload");
|
|
14482
|
+
return;
|
|
14483
|
+
}
|
|
14484
|
+
const workflows = WorkflowsSchema.parse(workflowsList);
|
|
14485
|
+
storeWorkflows(workflows);
|
|
14486
|
+
logger.info(`Stored ${workflows.length} workflow descriptor(s) from frontend`);
|
|
14487
|
+
} catch (error) {
|
|
14488
|
+
logger.error("Failed to handle workflow list response:", error);
|
|
14489
|
+
}
|
|
14490
|
+
}
|
|
14491
|
+
|
|
13643
14492
|
// src/handlers/users.ts
|
|
13644
14493
|
async function handleUsersRequest(data, collections, sendMessage) {
|
|
13645
14494
|
const executeCollection = async (collection, op, params) => {
|
|
@@ -16456,17 +17305,45 @@ function formatComponentsForPrompt(components) {
|
|
|
16456
17305
|
Props Structure: ${propsPreview}`;
|
|
16457
17306
|
}).join("\n\n");
|
|
16458
17307
|
}
|
|
17308
|
+
function classifyTool(tool) {
|
|
17309
|
+
if (tool.toolType === "direct") return "direct";
|
|
17310
|
+
if (tool.id.endsWith("_query")) return "sql";
|
|
17311
|
+
if (tool.id.endsWith("_call")) return "rest";
|
|
17312
|
+
if (tool.id.endsWith("_graphql")) return "graphql";
|
|
17313
|
+
return "direct";
|
|
17314
|
+
}
|
|
16459
17315
|
function formatToolsForPrompt(tools) {
|
|
16460
17316
|
if (!tools || tools.length === 0) {
|
|
16461
17317
|
return "No external tools available.";
|
|
16462
17318
|
}
|
|
16463
|
-
|
|
17319
|
+
const directTools = [];
|
|
17320
|
+
const sourceTools = [];
|
|
17321
|
+
for (const t of tools) {
|
|
17322
|
+
(classifyTool(t) === "direct" ? directTools : sourceTools).push(t);
|
|
17323
|
+
}
|
|
17324
|
+
const renderTool = (tool, idx, label) => {
|
|
16464
17325
|
const paramsStr = Object.entries(tool.params || {}).map(([key, type]) => `${key}: ${type}`).join(", ");
|
|
16465
|
-
return `${idx + 1}. ID: ${tool.id}
|
|
17326
|
+
return `${idx + 1}. [${label}] ID: ${tool.id}
|
|
16466
17327
|
Name: ${tool.name}
|
|
16467
17328
|
Description: ${tool.description}
|
|
16468
17329
|
Parameters: { ${paramsStr} }`;
|
|
16469
|
-
}
|
|
17330
|
+
};
|
|
17331
|
+
const sections = [];
|
|
17332
|
+
if (directTools.length > 0) {
|
|
17333
|
+
const body = directTools.map((t, i) => renderTool(t, i, "DIRECT \u2014 PREFER")).join("\n\n");
|
|
17334
|
+
sections.push(`### Direct Tools (try these first \u2014 they answer most questions without SQL)
|
|
17335
|
+
${body}`);
|
|
17336
|
+
}
|
|
17337
|
+
if (sourceTools.length > 0) {
|
|
17338
|
+
const body = sourceTools.map((t, i) => {
|
|
17339
|
+
const kind = classifyTool(t);
|
|
17340
|
+
const label = kind === "sql" ? "SQL" : kind === "rest" ? "REST" : "GRAPHQL";
|
|
17341
|
+
return renderTool(t, i, label);
|
|
17342
|
+
}).join("\n\n");
|
|
17343
|
+
sections.push(`### Source Tools (fallback \u2014 use only when no direct tool fits)
|
|
17344
|
+
${body}`);
|
|
17345
|
+
}
|
|
17346
|
+
return sections.join("\n\n");
|
|
16470
17347
|
}
|
|
16471
17348
|
function formatExistingComponentsForPrompt(existingComponents) {
|
|
16472
17349
|
if (!existingComponents || existingComponents.length === 0) {
|
|
@@ -16491,7 +17368,7 @@ function sendDashCompResponse(id, res, sendMessage, clientId) {
|
|
|
16491
17368
|
}
|
|
16492
17369
|
|
|
16493
17370
|
// src/dashComp/pick-component.ts
|
|
16494
|
-
async function pickComponentWithLLM(prompt, components, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, tools, dashCompModels, conversationHistory) {
|
|
17371
|
+
async function pickComponentWithLLM(prompt, components, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, tools, dashCompModels, conversationHistory, userId) {
|
|
16495
17372
|
const errors = [];
|
|
16496
17373
|
const availableComponentsText = formatComponentsForPrompt(components);
|
|
16497
17374
|
const availableToolsText = formatToolsForPrompt(tools);
|
|
@@ -16507,6 +17384,25 @@ async function pickComponentWithLLM(prompt, components, anthropicApiKey, groqApi
|
|
|
16507
17384
|
schemaDoc = schema.generateSchemaDocumentation();
|
|
16508
17385
|
}
|
|
16509
17386
|
const databaseRules = await promptLoader.loadDatabaseRules();
|
|
17387
|
+
let globalKnowledgeBase = "No global knowledge base available.";
|
|
17388
|
+
let knowledgeBaseContext = "No additional knowledge base context available.";
|
|
17389
|
+
if (collections) {
|
|
17390
|
+
const kbResult = await knowledge_base_default.getAllKnowledgeBase({
|
|
17391
|
+
prompt,
|
|
17392
|
+
collections,
|
|
17393
|
+
userId,
|
|
17394
|
+
topK: KNOWLEDGE_BASE_TOP_K
|
|
17395
|
+
});
|
|
17396
|
+
globalKnowledgeBase = kbResult.globalContext || globalKnowledgeBase;
|
|
17397
|
+
const dynamicParts = [];
|
|
17398
|
+
if (kbResult.userContext) {
|
|
17399
|
+
dynamicParts.push("## User-Specific Knowledge Base\n" + kbResult.userContext);
|
|
17400
|
+
}
|
|
17401
|
+
if (kbResult.queryContext) {
|
|
17402
|
+
dynamicParts.push("## Relevant Knowledge Base (Query-Matched)\n" + kbResult.queryContext);
|
|
17403
|
+
}
|
|
17404
|
+
knowledgeBaseContext = dynamicParts.join("\n\n") || knowledgeBaseContext;
|
|
17405
|
+
}
|
|
16510
17406
|
const prompts = await promptLoader.loadPrompts("dash-comp-picker", {
|
|
16511
17407
|
USER_PROMPT: prompt,
|
|
16512
17408
|
AVAILABLE_COMPONENTS: availableComponentsText,
|
|
@@ -16514,8 +17410,12 @@ async function pickComponentWithLLM(prompt, components, anthropicApiKey, groqApi
|
|
|
16514
17410
|
DATABASE_RULES: databaseRules,
|
|
16515
17411
|
AVAILABLE_TOOLS: availableToolsText,
|
|
16516
17412
|
CURRENT_DATETIME: getCurrentDateTimeForPrompt(),
|
|
16517
|
-
CONVERSATION_HISTORY: conversationHistory || "No previous conversation"
|
|
17413
|
+
CONVERSATION_HISTORY: conversationHistory || "No previous conversation",
|
|
17414
|
+
GLOBAL_KNOWLEDGE_BASE: globalKnowledgeBase,
|
|
17415
|
+
KNOWLEDGE_BASE_CONTEXT: knowledgeBaseContext
|
|
16518
17416
|
});
|
|
17417
|
+
logger.logLLMPrompt("dashCompPicker", "system", extractPromptText(prompts.system));
|
|
17418
|
+
logger.logLLMPrompt("dashCompPicker", "user", prompts.user);
|
|
16519
17419
|
logger.debug("[DASH_COMP_REQ] Loaded dash-comp-picker prompts with schema and tools");
|
|
16520
17420
|
const { apiKey, model } = getApiKeyAndModel(
|
|
16521
17421
|
anthropicApiKey,
|
|
@@ -16881,6 +17781,18 @@ Fixed SQL query:`;
|
|
|
16881
17781
|
logger.info(`[DASH_COMP_REQ] Replaced direct query with queryId: ${queryId}`);
|
|
16882
17782
|
}
|
|
16883
17783
|
finalComponent = { ...finalComponent, props };
|
|
17784
|
+
const ext = finalComponent.props?.externalTool;
|
|
17785
|
+
if (ext?.toolId && !ext?.parameters?.sql) {
|
|
17786
|
+
const matchedTool = tools?.find((t) => t.id === ext.toolId);
|
|
17787
|
+
if (matchedTool && matchedTool.cache !== false) {
|
|
17788
|
+
const match = executedTools.find((t) => t.id === ext.toolId);
|
|
17789
|
+
if (match) {
|
|
17790
|
+
const cacheKey = buildDirectToolCacheKey(ext.toolId, ext.parameters);
|
|
17791
|
+
queryCache.set(cacheKey, { success: true, data: match.result });
|
|
17792
|
+
logger.info(`[DASH_COMP_REQ] Pre-populated et-direct cache for ${ext.toolId}`);
|
|
17793
|
+
}
|
|
17794
|
+
}
|
|
17795
|
+
}
|
|
16884
17796
|
if (parsedResult.props.query) {
|
|
16885
17797
|
logger.info(`[DASH_COMP_REQ] Data source: Database query`);
|
|
16886
17798
|
}
|
|
@@ -16915,7 +17827,7 @@ Fixed SQL query:`;
|
|
|
16915
17827
|
}
|
|
16916
17828
|
|
|
16917
17829
|
// src/dashComp/create-filter.ts
|
|
16918
|
-
async function createFilterWithLLM(prompt, components, existingComponents, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, tools, dashCompModels, collections) {
|
|
17830
|
+
async function createFilterWithLLM(prompt, components, existingComponents, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, tools, dashCompModels, collections, userId) {
|
|
16919
17831
|
const errors = [];
|
|
16920
17832
|
try {
|
|
16921
17833
|
const filterComponents = components.filter((c) => c.type.startsWith("Filter"));
|
|
@@ -16935,6 +17847,25 @@ async function createFilterWithLLM(prompt, components, existingComponents, anthr
|
|
|
16935
17847
|
schemaDoc = schema.generateSchemaDocumentation();
|
|
16936
17848
|
}
|
|
16937
17849
|
const databaseRules = await promptLoader.loadDatabaseRules();
|
|
17850
|
+
let globalKnowledgeBase = "No global knowledge base available.";
|
|
17851
|
+
let knowledgeBaseContext = "No additional knowledge base context available.";
|
|
17852
|
+
if (collections) {
|
|
17853
|
+
const kbResult = await knowledge_base_default.getAllKnowledgeBase({
|
|
17854
|
+
prompt,
|
|
17855
|
+
collections,
|
|
17856
|
+
userId,
|
|
17857
|
+
topK: KNOWLEDGE_BASE_TOP_K
|
|
17858
|
+
});
|
|
17859
|
+
globalKnowledgeBase = kbResult.globalContext || globalKnowledgeBase;
|
|
17860
|
+
const dynamicParts = [];
|
|
17861
|
+
if (kbResult.userContext) {
|
|
17862
|
+
dynamicParts.push("## User-Specific Knowledge Base\n" + kbResult.userContext);
|
|
17863
|
+
}
|
|
17864
|
+
if (kbResult.queryContext) {
|
|
17865
|
+
dynamicParts.push("## Relevant Knowledge Base (Query-Matched)\n" + kbResult.queryContext);
|
|
17866
|
+
}
|
|
17867
|
+
knowledgeBaseContext = dynamicParts.join("\n\n") || knowledgeBaseContext;
|
|
17868
|
+
}
|
|
16938
17869
|
const prompts = await promptLoader.loadPrompts("dash-filter-picker", {
|
|
16939
17870
|
USER_PROMPT: prompt,
|
|
16940
17871
|
AVAILABLE_COMPONENTS: formatComponentsForPrompt(filterComponents),
|
|
@@ -16942,8 +17873,12 @@ async function createFilterWithLLM(prompt, components, existingComponents, anthr
|
|
|
16942
17873
|
SCHEMA_DOC: schemaDoc || "No database schema available",
|
|
16943
17874
|
DATABASE_RULES: databaseRules,
|
|
16944
17875
|
AVAILABLE_TOOLS: formatToolsForPrompt(tools),
|
|
16945
|
-
CURRENT_DATETIME: getCurrentDateTimeForPrompt()
|
|
17876
|
+
CURRENT_DATETIME: getCurrentDateTimeForPrompt(),
|
|
17877
|
+
GLOBAL_KNOWLEDGE_BASE: globalKnowledgeBase,
|
|
17878
|
+
KNOWLEDGE_BASE_CONTEXT: knowledgeBaseContext
|
|
16946
17879
|
});
|
|
17880
|
+
logger.logLLMPrompt("dashFilterPicker", "system", extractPromptText(prompts.system));
|
|
17881
|
+
logger.logLLMPrompt("dashFilterPicker", "user", prompts.user);
|
|
16947
17882
|
logger.debug("[DASH_COMP_REQ:FILTER] Loaded dash-filter-picker prompts");
|
|
16948
17883
|
const { apiKey, model } = getApiKeyAndModel(
|
|
16949
17884
|
anthropicApiKey,
|
|
@@ -17052,7 +17987,35 @@ async function createFilterWithLLM(prompt, components, existingComponents, anthr
|
|
|
17052
17987
|
const updatedComponents = result.updatedComponents || [];
|
|
17053
17988
|
for (const comp of updatedComponents) {
|
|
17054
17989
|
const extTool = comp.props?.externalTool;
|
|
17055
|
-
if (!extTool?.
|
|
17990
|
+
if (!extTool?.toolId) continue;
|
|
17991
|
+
const isDirectTool = !extTool?.parameters?.sql;
|
|
17992
|
+
if (isDirectTool) {
|
|
17993
|
+
const directTool = tools?.find((t) => t.id === extTool.toolId);
|
|
17994
|
+
if (!directTool) {
|
|
17995
|
+
logger.warn(`[DASH_COMP_REQ:FILTER] direct tool ${extTool.toolId} not found in tool registry`);
|
|
17996
|
+
continue;
|
|
17997
|
+
}
|
|
17998
|
+
const declared = new Set(Object.keys(directTool.params || {}));
|
|
17999
|
+
const sentKeys = Object.keys(extTool.parameters || {});
|
|
18000
|
+
const unknown = sentKeys.filter((k) => !declared.has(k));
|
|
18001
|
+
if (unknown.length > 0) {
|
|
18002
|
+
logger.warn(`[DASH_COMP_REQ:FILTER] dropping unknown params on ${extTool.toolId}: ${unknown.join(", ")}`);
|
|
18003
|
+
unknown.forEach((k) => delete extTool.parameters[k]);
|
|
18004
|
+
}
|
|
18005
|
+
if (directTool.cache !== false) {
|
|
18006
|
+
try {
|
|
18007
|
+
const directResult = await directTool.fn(extTool.parameters || {});
|
|
18008
|
+
const cacheKey2 = buildDirectToolCacheKey(extTool.toolId, extTool.parameters);
|
|
18009
|
+
queryCache.set(cacheKey2, { success: true, data: directResult });
|
|
18010
|
+
logger.info(`[DASH_COMP_REQ:FILTER] direct tool ${extTool.toolId} validated and cached for component: ${comp.id}`);
|
|
18011
|
+
} catch (err) {
|
|
18012
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
18013
|
+
logger.warn(`[DASH_COMP_REQ:FILTER] direct tool validation failed for ${extTool.toolId} on component ${comp.id}: ${errMsg}`);
|
|
18014
|
+
}
|
|
18015
|
+
}
|
|
18016
|
+
continue;
|
|
18017
|
+
}
|
|
18018
|
+
if (!extTool?.parameters?.sql) continue;
|
|
17056
18019
|
let sql = extTool.parameters.sql;
|
|
17057
18020
|
const defaultParams = extTool.parameters.params || {};
|
|
17058
18021
|
const toolId = extTool.toolId;
|
|
@@ -17289,7 +18252,8 @@ var processDashCompRequest = async (data, components, _sendMessage, anthropicApi
|
|
|
17289
18252
|
llmProviders,
|
|
17290
18253
|
tools,
|
|
17291
18254
|
dashCompModels,
|
|
17292
|
-
collections
|
|
18255
|
+
collections,
|
|
18256
|
+
userId
|
|
17293
18257
|
);
|
|
17294
18258
|
} else {
|
|
17295
18259
|
llmResponse = await pickComponentWithLLM(
|
|
@@ -17303,7 +18267,8 @@ var processDashCompRequest = async (data, components, _sendMessage, anthropicApi
|
|
|
17303
18267
|
collections,
|
|
17304
18268
|
tools,
|
|
17305
18269
|
dashCompModels,
|
|
17306
|
-
conversationHistory
|
|
18270
|
+
conversationHistory,
|
|
18271
|
+
userId
|
|
17307
18272
|
);
|
|
17308
18273
|
}
|
|
17309
18274
|
if (llmResponse.success && dashboardId && prompt) {
|
|
@@ -17442,7 +18407,7 @@ function sendReportCompResponse(id, res, sendMessage, clientId) {
|
|
|
17442
18407
|
}
|
|
17443
18408
|
|
|
17444
18409
|
// src/reportComp/generate-report.ts
|
|
17445
|
-
async function generateReportComponents(prompt, components, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, tools, modelConfig, conversationHistory) {
|
|
18410
|
+
async function generateReportComponents(prompt, components, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, tools, modelConfig, conversationHistory, userId) {
|
|
17446
18411
|
const errors = [];
|
|
17447
18412
|
const availableComponentsText = formatComponentsForPrompt2(components);
|
|
17448
18413
|
const availableToolsText = formatToolsForPrompt2(tools);
|
|
@@ -17458,6 +18423,25 @@ async function generateReportComponents(prompt, components, anthropicApiKey, gro
|
|
|
17458
18423
|
schemaDoc = schema.generateSchemaDocumentation();
|
|
17459
18424
|
}
|
|
17460
18425
|
const databaseRules = await promptLoader.loadDatabaseRules();
|
|
18426
|
+
let globalKnowledgeBase = "No global knowledge base available.";
|
|
18427
|
+
let knowledgeBaseContext = "No additional knowledge base context available.";
|
|
18428
|
+
if (collections) {
|
|
18429
|
+
const kbResult = await knowledge_base_default.getAllKnowledgeBase({
|
|
18430
|
+
prompt,
|
|
18431
|
+
collections,
|
|
18432
|
+
userId,
|
|
18433
|
+
topK: KNOWLEDGE_BASE_TOP_K
|
|
18434
|
+
});
|
|
18435
|
+
globalKnowledgeBase = kbResult.globalContext || globalKnowledgeBase;
|
|
18436
|
+
const dynamicParts = [];
|
|
18437
|
+
if (kbResult.userContext) {
|
|
18438
|
+
dynamicParts.push("## User-Specific Knowledge Base\n" + kbResult.userContext);
|
|
18439
|
+
}
|
|
18440
|
+
if (kbResult.queryContext) {
|
|
18441
|
+
dynamicParts.push("## Relevant Knowledge Base (Query-Matched)\n" + kbResult.queryContext);
|
|
18442
|
+
}
|
|
18443
|
+
knowledgeBaseContext = dynamicParts.join("\n\n") || knowledgeBaseContext;
|
|
18444
|
+
}
|
|
17461
18445
|
const prompts = await promptLoader.loadPrompts("report-comp-picker", {
|
|
17462
18446
|
USER_PROMPT: prompt,
|
|
17463
18447
|
AVAILABLE_COMPONENTS: availableComponentsText,
|
|
@@ -17465,8 +18449,12 @@ async function generateReportComponents(prompt, components, anthropicApiKey, gro
|
|
|
17465
18449
|
DATABASE_RULES: databaseRules,
|
|
17466
18450
|
AVAILABLE_TOOLS: availableToolsText,
|
|
17467
18451
|
CURRENT_DATETIME: getCurrentDateTimeForPrompt(),
|
|
17468
|
-
CONVERSATION_HISTORY: conversationHistory || "No previous conversation"
|
|
18452
|
+
CONVERSATION_HISTORY: conversationHistory || "No previous conversation",
|
|
18453
|
+
GLOBAL_KNOWLEDGE_BASE: globalKnowledgeBase,
|
|
18454
|
+
KNOWLEDGE_BASE_CONTEXT: knowledgeBaseContext
|
|
17469
18455
|
});
|
|
18456
|
+
logger.logLLMPrompt("reportCompPicker", "system", extractPromptText(prompts.system));
|
|
18457
|
+
logger.logLLMPrompt("reportCompPicker", "user", prompts.user);
|
|
17470
18458
|
logger.debug("[REPORT_COMP_REQ] Loaded report-comp-picker prompts with schema and tools");
|
|
17471
18459
|
const { apiKey, model } = getApiKeyAndModel2(
|
|
17472
18460
|
anthropicApiKey,
|
|
@@ -17691,13 +18679,21 @@ async function validateAllExternalToolQueries(components, collections, tools, mo
|
|
|
17691
18679
|
data: {}
|
|
17692
18680
|
});
|
|
17693
18681
|
if (result?.success !== false && !result?.error) {
|
|
17694
|
-
const
|
|
17695
|
-
const
|
|
18682
|
+
const toolResult = result?.data ?? result;
|
|
18683
|
+
const valueKey = comp.props?.config?.valueKey;
|
|
18684
|
+
const isKpi = comp.type === "KPICard" || comp.name === "DynamicKPICard";
|
|
18685
|
+
let dataArray;
|
|
18686
|
+
if (isKpi && valueKey && toolResult && typeof toolResult === "object" && !Array.isArray(toolResult) && toolResult[valueKey] !== void 0) {
|
|
18687
|
+
dataArray = [toolResult];
|
|
18688
|
+
} else {
|
|
18689
|
+
const resultData = toolResult?.data ?? toolResult ?? [];
|
|
18690
|
+
dataArray = Array.isArray(resultData) ? resultData : [resultData];
|
|
18691
|
+
}
|
|
17696
18692
|
if (!comp.props.config) {
|
|
17697
18693
|
comp.props.config = {};
|
|
17698
18694
|
}
|
|
17699
18695
|
comp.props.config.data = dataArray;
|
|
17700
|
-
logger.info(`[REPORT_COMP_REQ] \u2713 ${comp.name} prefetched ${dataArray.length} rows (non-SQL tool)`);
|
|
18696
|
+
logger.info(`[REPORT_COMP_REQ] \u2713 ${comp.name} prefetched ${dataArray.length} ${dataArray.length === 1 && isKpi ? "aggregate" : "rows"} (non-SQL tool)`);
|
|
17701
18697
|
}
|
|
17702
18698
|
} catch (err) {
|
|
17703
18699
|
logger.warn(`[REPORT_COMP_REQ] \u26A0 ${comp.name} non-SQL prefetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -17837,7 +18833,8 @@ var processReportCompRequest = async (data, components, _sendMessage, anthropicA
|
|
|
17837
18833
|
collections,
|
|
17838
18834
|
tools,
|
|
17839
18835
|
modelConfig,
|
|
17840
|
-
conversationHistory
|
|
18836
|
+
conversationHistory,
|
|
18837
|
+
userId
|
|
17841
18838
|
);
|
|
17842
18839
|
if (llmResponse.success && reportId && prompt) {
|
|
17843
18840
|
const comps = llmResponse.data?.components;
|
|
@@ -18962,23 +19959,27 @@ var CleanupService = class _CleanupService {
|
|
|
18962
19959
|
// src/index.ts
|
|
18963
19960
|
var DEFAULT_WS_URL = "wss://ws.superatom.ai/websocket";
|
|
18964
19961
|
var SuperatomSDK = class {
|
|
18965
|
-
// 3.5 minutes (PING_INTERVAL + 30s grace)
|
|
18966
19962
|
constructor(config) {
|
|
18967
19963
|
this.ws = null;
|
|
18968
19964
|
this.messageHandlers = /* @__PURE__ */ new Map();
|
|
18969
19965
|
this.messageTypeHandlers = /* @__PURE__ */ new Map();
|
|
18970
19966
|
this.connected = false;
|
|
18971
19967
|
this.reconnectAttempts = 0;
|
|
18972
|
-
|
|
19968
|
+
// Retry forever — the backend must self-heal across NAT timeouts, DO
|
|
19969
|
+
// redeploys, and long network outages. The previous cap (5) gave up after
|
|
19970
|
+
// ~30s and left the singleton SDK dead until process restart.
|
|
19971
|
+
this.maxReconnectAttempts = Infinity;
|
|
18973
19972
|
this.collections = {};
|
|
18974
19973
|
this.components = [];
|
|
18975
19974
|
this.tools = [];
|
|
18976
|
-
|
|
19975
|
+
this.workflows = [];
|
|
19976
|
+
// Heartbeat properties for keeping WebSocket connection alive.
|
|
19977
|
+
// 25s ping + 10s grace stays under common NAT/LB idle thresholds (~60-100s)
|
|
19978
|
+
// so we detect dead sockets within seconds instead of minutes.
|
|
18977
19979
|
this.pingInterval = null;
|
|
18978
19980
|
this.lastPong = Date.now();
|
|
18979
|
-
this.PING_INTERVAL_MS =
|
|
18980
|
-
|
|
18981
|
-
this.PONG_TIMEOUT_MS = 21e4;
|
|
19981
|
+
this.PING_INTERVAL_MS = 25e3;
|
|
19982
|
+
this.PONG_TIMEOUT_MS = 35e3;
|
|
18982
19983
|
if (config.logLevel) {
|
|
18983
19984
|
logger.setLogLevel(config.logLevel);
|
|
18984
19985
|
}
|
|
@@ -19134,7 +20135,7 @@ var SuperatomSDK = class {
|
|
|
19134
20135
|
this.handlePong();
|
|
19135
20136
|
break;
|
|
19136
20137
|
case "DATA_REQ":
|
|
19137
|
-
handleDataRequest(parsed, this.collections, (msg) => this.send(msg)).catch((error) => {
|
|
20138
|
+
handleDataRequest(parsed, this.collections, (msg) => this.send(msg), this.tools).catch((error) => {
|
|
19138
20139
|
logger.error("Failed to handle data request:", error);
|
|
19139
20140
|
});
|
|
19140
20141
|
break;
|
|
@@ -19154,7 +20155,7 @@ var SuperatomSDK = class {
|
|
|
19154
20155
|
});
|
|
19155
20156
|
break;
|
|
19156
20157
|
case "USER_PROMPT_REQ":
|
|
19157
|
-
handleUserPromptRequest(parsed, this.components, (msg) => this.send(msg), this.anthropicApiKey, this.groqApiKey, this.geminiApiKey, this.openaiApiKey, this.llmProviders, this.collections, this.tools, this.mainAgentModel, this.sourceAgentModel).catch((error) => {
|
|
20158
|
+
handleUserPromptRequest(parsed, this.components, (msg) => this.send(msg), this.anthropicApiKey, this.groqApiKey, this.geminiApiKey, this.openaiApiKey, this.llmProviders, this.collections, this.tools, this.mainAgentModel, this.sourceAgentModel, this.workflows).catch((error) => {
|
|
19158
20159
|
logger.error("Failed to handle user prompt request:", error);
|
|
19159
20160
|
});
|
|
19160
20161
|
break;
|
|
@@ -19173,6 +20174,11 @@ var SuperatomSDK = class {
|
|
|
19173
20174
|
logger.error("Failed to handle component list request:", error);
|
|
19174
20175
|
});
|
|
19175
20176
|
break;
|
|
20177
|
+
case "WORKFLOW_LIST_RES":
|
|
20178
|
+
handleWorkflowListResponse(parsed, (wf) => this.setWorkflows(wf)).catch((error) => {
|
|
20179
|
+
logger.error("Failed to handle workflow list request:", error);
|
|
20180
|
+
});
|
|
20181
|
+
break;
|
|
19176
20182
|
case "USERS":
|
|
19177
20183
|
handleUsersRequest(parsed, this.collections, (msg) => this.send(msg)).catch((error) => {
|
|
19178
20184
|
logger.error("Failed to handle users request:", error);
|
|
@@ -19332,9 +20338,9 @@ var SuperatomSDK = class {
|
|
|
19332
20338
|
handleReconnect() {
|
|
19333
20339
|
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
19334
20340
|
this.reconnectAttempts++;
|
|
19335
|
-
const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts),
|
|
20341
|
+
const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
|
|
19336
20342
|
setTimeout(() => {
|
|
19337
|
-
logger.info(`Attempting to reconnect (${this.reconnectAttempts}
|
|
20343
|
+
logger.info(`Attempting to reconnect (attempt ${this.reconnectAttempts})...`);
|
|
19338
20344
|
this.connect().catch((error) => {
|
|
19339
20345
|
logger.error("Reconnection failed:", error);
|
|
19340
20346
|
});
|
|
@@ -19412,6 +20418,24 @@ var SuperatomSDK = class {
|
|
|
19412
20418
|
getTools() {
|
|
19413
20419
|
return this.tools;
|
|
19414
20420
|
}
|
|
20421
|
+
/**
|
|
20422
|
+
* Register workflow components for the SDK instance.
|
|
20423
|
+
*
|
|
20424
|
+
* Workflows are pre-built multi-step UI flows the main agent can pick when
|
|
20425
|
+
* the user's prompt matches a workflow's `whenToUse` trigger. Picking a
|
|
20426
|
+
* workflow short-circuits analysis text + dashboard component generation —
|
|
20427
|
+
* the workflow component is returned directly, with the LLM-extracted props.
|
|
20428
|
+
*/
|
|
20429
|
+
setWorkflows(workflows) {
|
|
20430
|
+
this.workflows = workflows;
|
|
20431
|
+
logger.info(`Workflows stored in SDK: ${workflows.length} workflow(s)`);
|
|
20432
|
+
}
|
|
20433
|
+
/**
|
|
20434
|
+
* Get the registered workflow components.
|
|
20435
|
+
*/
|
|
20436
|
+
getWorkflows() {
|
|
20437
|
+
return this.workflows;
|
|
20438
|
+
}
|
|
19415
20439
|
/**
|
|
19416
20440
|
* Apply model strategy to all LLM provider singletons
|
|
19417
20441
|
* @param strategy - 'best', 'fast', or 'balanced'
|
|
@@ -19472,6 +20496,8 @@ var SuperatomSDK = class {
|
|
|
19472
20496
|
LLM,
|
|
19473
20497
|
MainAgent,
|
|
19474
20498
|
STORAGE_CONFIG,
|
|
20499
|
+
ScriptMatcher,
|
|
20500
|
+
ScriptStore,
|
|
19475
20501
|
SuperatomSDK,
|
|
19476
20502
|
Thread,
|
|
19477
20503
|
ThreadManager,
|
|
@@ -19485,10 +20511,13 @@ var SuperatomSDK = class {
|
|
|
19485
20511
|
hybridRerank,
|
|
19486
20512
|
llmUsageLogger,
|
|
19487
20513
|
logger,
|
|
20514
|
+
normalizeScriptBody,
|
|
19488
20515
|
openaiLLM,
|
|
19489
20516
|
queryCache,
|
|
19490
20517
|
rerankChromaResults,
|
|
19491
20518
|
rerankConversationResults,
|
|
20519
|
+
resolveScriptRecipeStore,
|
|
20520
|
+
runScript,
|
|
19492
20521
|
userPromptErrorLogger
|
|
19493
20522
|
});
|
|
19494
20523
|
//# sourceMappingURL=index.js.map
|