@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.mjs
CHANGED
|
@@ -326,6 +326,24 @@ var ComponentListResponseMessageSchema = z3.object({
|
|
|
326
326
|
type: z3.literal("COMPONENT_LIST_RES"),
|
|
327
327
|
payload: ComponentListResponsePayloadSchema
|
|
328
328
|
});
|
|
329
|
+
var WorkflowDescriptorSchema = z3.object({
|
|
330
|
+
id: z3.string(),
|
|
331
|
+
name: z3.string(),
|
|
332
|
+
description: z3.string(),
|
|
333
|
+
whenToUse: z3.string(),
|
|
334
|
+
propsSchema: z3.record(z3.string()),
|
|
335
|
+
defaultProps: z3.record(z3.unknown()).optional()
|
|
336
|
+
});
|
|
337
|
+
var WorkflowsSchema = z3.array(WorkflowDescriptorSchema);
|
|
338
|
+
var WorkflowListResponsePayloadSchema = z3.object({
|
|
339
|
+
workflows: z3.array(WorkflowDescriptorSchema)
|
|
340
|
+
});
|
|
341
|
+
var WorkflowListResponseMessageSchema = z3.object({
|
|
342
|
+
id: z3.string(),
|
|
343
|
+
from: MessageParticipantSchema,
|
|
344
|
+
type: z3.literal("WORKFLOW_LIST_RES"),
|
|
345
|
+
payload: WorkflowListResponsePayloadSchema
|
|
346
|
+
});
|
|
329
347
|
var OutputFieldSchema = z3.object({
|
|
330
348
|
name: z3.string(),
|
|
331
349
|
// Field name (column name in the result)
|
|
@@ -349,8 +367,10 @@ var ToolSchema = z3.object({
|
|
|
349
367
|
fullSchema: z3.string().optional(),
|
|
350
368
|
params: z3.record(z3.string()),
|
|
351
369
|
fn: z3.function().args(z3.any()).returns(z3.any()),
|
|
352
|
-
outputSchema: OutputSchema.optional()
|
|
370
|
+
outputSchema: OutputSchema.optional(),
|
|
353
371
|
// Optional: describes the data structure returned by this tool
|
|
372
|
+
/** Cache policy. `false` = never cache (live data, write ops). Mirrors HTTP `Cache-Control: no-store`. */
|
|
373
|
+
cache: z3.union([z3.literal(false), z3.object({ ttlMs: z3.number().optional() })]).optional()
|
|
354
374
|
});
|
|
355
375
|
var UserQueryFiltersSchema = z3.object({
|
|
356
376
|
username: z3.string().optional(),
|
|
@@ -1465,7 +1485,7 @@ var QueryCache = class {
|
|
|
1465
1485
|
this.cache = /* @__PURE__ */ new Map();
|
|
1466
1486
|
this.ttlMs = 10 * 60 * 1e3;
|
|
1467
1487
|
// Default: 10 minutes
|
|
1468
|
-
this.maxCacheSize =
|
|
1488
|
+
this.maxCacheSize = 5e3;
|
|
1469
1489
|
// Max data cache entries
|
|
1470
1490
|
this.cleanupInterval = null;
|
|
1471
1491
|
// Encryption for queryId tokens
|
|
@@ -1492,9 +1512,13 @@ var QueryCache = class {
|
|
|
1492
1512
|
return this.ttlMs / 60 / 1e3;
|
|
1493
1513
|
}
|
|
1494
1514
|
/**
|
|
1495
|
-
* Store query result in data cache
|
|
1515
|
+
* Store query result in data cache.
|
|
1516
|
+
* If the key already exists, it's removed first so the re-insert places it
|
|
1517
|
+
* at the back of the iteration order (LRU). Eviction only fires when adding
|
|
1518
|
+
* a genuinely new key past the size limit.
|
|
1496
1519
|
*/
|
|
1497
1520
|
set(query, data) {
|
|
1521
|
+
this.cache.delete(query);
|
|
1498
1522
|
if (this.cache.size >= this.maxCacheSize) {
|
|
1499
1523
|
const oldestKey = this.cache.keys().next().value;
|
|
1500
1524
|
if (oldestKey) this.cache.delete(oldestKey);
|
|
@@ -1507,7 +1531,9 @@ var QueryCache = class {
|
|
|
1507
1531
|
logger.debug(`[QueryCache] Stored result for query (${query.substring(0, 50)}...)`);
|
|
1508
1532
|
}
|
|
1509
1533
|
/**
|
|
1510
|
-
* Get cached result if exists and not expired
|
|
1534
|
+
* Get cached result if exists and not expired.
|
|
1535
|
+
* On hit, re-inserts the entry so it moves to the back of the Map's
|
|
1536
|
+
* iteration order — turning FIFO eviction into true LRU.
|
|
1511
1537
|
*/
|
|
1512
1538
|
get(query) {
|
|
1513
1539
|
const entry = this.cache.get(query);
|
|
@@ -1516,6 +1542,8 @@ var QueryCache = class {
|
|
|
1516
1542
|
this.cache.delete(query);
|
|
1517
1543
|
return null;
|
|
1518
1544
|
}
|
|
1545
|
+
this.cache.delete(query);
|
|
1546
|
+
this.cache.set(query, entry);
|
|
1519
1547
|
logger.info(`[QueryCache] Cache HIT for query (${query.substring(0, 50)}...)`);
|
|
1520
1548
|
return entry.data;
|
|
1521
1549
|
}
|
|
@@ -1657,6 +1685,21 @@ var QueryCache = class {
|
|
|
1657
1685
|
};
|
|
1658
1686
|
var queryCache = new QueryCache();
|
|
1659
1687
|
|
|
1688
|
+
// src/utils/surrogate.ts
|
|
1689
|
+
var LONE_SURROGATE_RE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
|
|
1690
|
+
function stripLoneSurrogates(value) {
|
|
1691
|
+
if (typeof value !== "string") return value;
|
|
1692
|
+
if (!/[\uD800-\uDFFF]/.test(value)) return value;
|
|
1693
|
+
return value.replace(LONE_SURROGATE_RE, "\uFFFD");
|
|
1694
|
+
}
|
|
1695
|
+
function safeTruncate(text, maxUnits) {
|
|
1696
|
+
if (typeof text !== "string" || text.length <= maxUnits || maxUnits < 0) return text;
|
|
1697
|
+
let end = maxUnits;
|
|
1698
|
+
const lastCode = text.charCodeAt(end - 1);
|
|
1699
|
+
if (lastCode >= 55296 && lastCode <= 56319) end -= 1;
|
|
1700
|
+
return text.slice(0, end);
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1660
1703
|
// src/userResponse/llm-result-truncator.ts
|
|
1661
1704
|
var DEFAULT_MAX_ROWS = 10;
|
|
1662
1705
|
var DEFAULT_MAX_CHARS_PER_FIELD = 500;
|
|
@@ -1699,12 +1742,12 @@ function isDateString(value) {
|
|
|
1699
1742
|
}
|
|
1700
1743
|
function truncateTextField(value, maxLength) {
|
|
1701
1744
|
if (value.length <= maxLength) {
|
|
1702
|
-
return { text: value, wasTruncated: false };
|
|
1745
|
+
return { text: stripLoneSurrogates(value), wasTruncated: false };
|
|
1703
1746
|
}
|
|
1704
|
-
const truncated = value
|
|
1705
|
-
const remaining = value.length -
|
|
1747
|
+
const truncated = safeTruncate(value, maxLength);
|
|
1748
|
+
const remaining = value.length - truncated.length;
|
|
1706
1749
|
return {
|
|
1707
|
-
text: `${truncated}... (${remaining} more chars)`,
|
|
1750
|
+
text: `${stripLoneSurrogates(truncated)}... (${remaining} more chars)`,
|
|
1708
1751
|
wasTruncated: true
|
|
1709
1752
|
};
|
|
1710
1753
|
}
|
|
@@ -2002,6 +2045,21 @@ function formatResultAsString(formattedResult) {
|
|
|
2002
2045
|
return JSON.stringify(formattedResult, null, 2);
|
|
2003
2046
|
}
|
|
2004
2047
|
|
|
2048
|
+
// src/utils/cache-key.ts
|
|
2049
|
+
function stableStringify(value) {
|
|
2050
|
+
if (value === null || typeof value !== "object") {
|
|
2051
|
+
return JSON.stringify(value);
|
|
2052
|
+
}
|
|
2053
|
+
if (Array.isArray(value)) {
|
|
2054
|
+
return "[" + value.map(stableStringify).join(",") + "]";
|
|
2055
|
+
}
|
|
2056
|
+
const keys = Object.keys(value).sort();
|
|
2057
|
+
return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(value[k])).join(",") + "}";
|
|
2058
|
+
}
|
|
2059
|
+
function buildDirectToolCacheKey(toolId, parameters) {
|
|
2060
|
+
return `et-direct:${toolId}:${stableStringify(parameters || {})}`;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2005
2063
|
// src/handlers/data-request.ts
|
|
2006
2064
|
function getQueryCacheKey(query) {
|
|
2007
2065
|
if (typeof query === "string") {
|
|
@@ -2015,7 +2073,7 @@ function getQueryCacheKey(query) {
|
|
|
2015
2073
|
}
|
|
2016
2074
|
return "";
|
|
2017
2075
|
}
|
|
2018
|
-
function getCacheKey(collection, op, params) {
|
|
2076
|
+
function getCacheKey(collection, op, params, tools) {
|
|
2019
2077
|
if (collection === "database" && op === "execute" && params?.sql) {
|
|
2020
2078
|
return getQueryCacheKey(params.sql);
|
|
2021
2079
|
}
|
|
@@ -2025,6 +2083,13 @@ function getCacheKey(collection, op, params) {
|
|
|
2025
2083
|
const paramsKey = params.params ? JSON.stringify(params.params) : "";
|
|
2026
2084
|
return sqlKey ? `et:${toolId}:${sqlKey}:${paramsKey}` : "";
|
|
2027
2085
|
}
|
|
2086
|
+
if (collection === "external-tools" && op === "execute" && params?.toolId && !params?.sql) {
|
|
2087
|
+
const tool = tools?.find((t) => t.id === params.toolId);
|
|
2088
|
+
if (tool?.cache === false) return "";
|
|
2089
|
+
const { toolId, toolName, ...rest } = params;
|
|
2090
|
+
const paramsKey = stableStringify(rest);
|
|
2091
|
+
return `et-direct:${toolId}:${paramsKey}`;
|
|
2092
|
+
}
|
|
2028
2093
|
if (collection === "external-tools" && op === "executeByQueryId" && params?.queryId) {
|
|
2029
2094
|
const toolId = params.toolId || "";
|
|
2030
2095
|
const filterKey = params.filterParams ? JSON.stringify(params.filterParams) : "";
|
|
@@ -2033,7 +2098,7 @@ function getCacheKey(collection, op, params) {
|
|
|
2033
2098
|
}
|
|
2034
2099
|
return "";
|
|
2035
2100
|
}
|
|
2036
|
-
async function handleDataRequest(data, collections, sendMessage) {
|
|
2101
|
+
async function handleDataRequest(data, collections, sendMessage, tools) {
|
|
2037
2102
|
let requestId;
|
|
2038
2103
|
let collection;
|
|
2039
2104
|
let op;
|
|
@@ -2059,7 +2124,7 @@ async function handleDataRequest(data, collections, sendMessage) {
|
|
|
2059
2124
|
const startTime = performance.now();
|
|
2060
2125
|
let result;
|
|
2061
2126
|
let fromCache = false;
|
|
2062
|
-
const cacheKey = getCacheKey(collection, op, params);
|
|
2127
|
+
const cacheKey = getCacheKey(collection, op, params, tools);
|
|
2063
2128
|
if (cacheKey) {
|
|
2064
2129
|
const cachedResult = queryCache.get(cacheKey);
|
|
2065
2130
|
if (cachedResult !== null) {
|
|
@@ -3724,6 +3789,12 @@ You MUST respond with ONLY a valid JSON object (no markdown, no code blocks):
|
|
|
3724
3789
|
---
|
|
3725
3790
|
|
|
3726
3791
|
## CONTEXT
|
|
3792
|
+
|
|
3793
|
+
### Global Knowledge Base
|
|
3794
|
+
{{GLOBAL_KNOWLEDGE_BASE}}
|
|
3795
|
+
|
|
3796
|
+
### Knowledge Base Context
|
|
3797
|
+
{{KNOWLEDGE_BASE_CONTEXT}}
|
|
3727
3798
|
`,
|
|
3728
3799
|
user: `{{USER_PROMPT}}`
|
|
3729
3800
|
},
|
|
@@ -4276,13 +4347,11 @@ var PromptLoader = class {
|
|
|
4276
4347
|
* @returns Processed string
|
|
4277
4348
|
*/
|
|
4278
4349
|
replaceVariables(template, variables) {
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
const
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
}
|
|
4285
|
-
return content;
|
|
4350
|
+
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
4351
|
+
if (!(key in variables)) return match;
|
|
4352
|
+
const value = variables[key];
|
|
4353
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
4354
|
+
});
|
|
4286
4355
|
}
|
|
4287
4356
|
/**
|
|
4288
4357
|
* Load both system and user prompts from cache and replace variables
|
|
@@ -4305,8 +4374,6 @@ var PromptLoader = class {
|
|
|
4305
4374
|
const [staticPart, contextPart] = template.system.split(contextMarker);
|
|
4306
4375
|
const processedStatic = this.replaceVariables(staticPart, variables);
|
|
4307
4376
|
const processedContext = this.replaceVariables(contextMarker + contextPart, variables);
|
|
4308
|
-
const staticLength = processedStatic.length;
|
|
4309
|
-
const contextLength = processedContext.length;
|
|
4310
4377
|
return {
|
|
4311
4378
|
system: [
|
|
4312
4379
|
{
|
|
@@ -5236,9 +5303,36 @@ var searchConversationsWithReranking = async (options) => {
|
|
|
5236
5303
|
return null;
|
|
5237
5304
|
}
|
|
5238
5305
|
};
|
|
5306
|
+
var findExactMatch = async ({
|
|
5307
|
+
userPrompt,
|
|
5308
|
+
collections,
|
|
5309
|
+
userId
|
|
5310
|
+
}) => {
|
|
5311
|
+
try {
|
|
5312
|
+
if (!collections || !collections["conversation-history"] || !collections["conversation-history"]["exactMatch"]) {
|
|
5313
|
+
logger.info("[ConversationSearch] conversation-history.exactMatch collection not registered, skipping");
|
|
5314
|
+
return null;
|
|
5315
|
+
}
|
|
5316
|
+
const result = await collections["conversation-history"]["exactMatch"]({
|
|
5317
|
+
userPrompt,
|
|
5318
|
+
userId
|
|
5319
|
+
});
|
|
5320
|
+
if (!result || !result.uiBlock) {
|
|
5321
|
+
logger.info("[ConversationSearch] No exact match found");
|
|
5322
|
+
return null;
|
|
5323
|
+
}
|
|
5324
|
+
logger.info(`[ConversationSearch] \u2713 Exact prompt match found for "${userPrompt.substring(0, 50)}..."`);
|
|
5325
|
+
return result;
|
|
5326
|
+
} catch (error) {
|
|
5327
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
5328
|
+
logger.warn(`[ConversationSearch] Error in exact match lookup: ${errorMsg}`);
|
|
5329
|
+
return null;
|
|
5330
|
+
}
|
|
5331
|
+
};
|
|
5239
5332
|
var ConversationSearch = {
|
|
5240
5333
|
searchConversations,
|
|
5241
|
-
searchConversationsWithReranking
|
|
5334
|
+
searchConversationsWithReranking,
|
|
5335
|
+
findExactMatch
|
|
5242
5336
|
};
|
|
5243
5337
|
var conversation_search_default = ConversationSearch;
|
|
5244
5338
|
|
|
@@ -5256,16 +5350,20 @@ var MAX_TOKENS_CLASSIFICATION = 1500;
|
|
|
5256
5350
|
var MAX_TOKENS_ADAPTATION = 8192;
|
|
5257
5351
|
var MAX_TOKENS_TEXT_RESPONSE = 4e3;
|
|
5258
5352
|
var MAX_TOKENS_NEXT_QUESTIONS = 1200;
|
|
5259
|
-
var
|
|
5353
|
+
var MAX_ROWS_FETCHED = 50;
|
|
5354
|
+
var MAX_ROWS_TO_LLM = 10;
|
|
5355
|
+
var LLM_SAMPLE_ROWS = 10;
|
|
5356
|
+
var MAX_ROWS_RENDERED = 250;
|
|
5260
5357
|
var DEFAULT_MAX_CHARS_PER_FIELD2 = 500;
|
|
5261
|
-
var STREAM_PREVIEW_MAX_ROWS = 10;
|
|
5262
5358
|
var STREAM_PREVIEW_MAX_CHARS = 200;
|
|
5263
|
-
var TOOL_TRACKING_MAX_ROWS = 5;
|
|
5264
5359
|
var TOOL_TRACKING_MAX_CHARS = 200;
|
|
5265
|
-
var
|
|
5360
|
+
var DEFAULT_MAX_ROWS_FOR_LLM = LLM_SAMPLE_ROWS;
|
|
5361
|
+
var STREAM_PREVIEW_MAX_ROWS = LLM_SAMPLE_ROWS;
|
|
5362
|
+
var TOOL_TRACKING_SAMPLE_ROWS = LLM_SAMPLE_ROWS;
|
|
5363
|
+
var TOOL_TRACKING_MAX_ROWS = 5;
|
|
5266
5364
|
var DEFAULT_QUERY_LIMIT = 24;
|
|
5267
|
-
var MAX_AGENT_QUERY_LIMIT =
|
|
5268
|
-
var MAX_COMPONENT_QUERY_LIMIT =
|
|
5365
|
+
var MAX_AGENT_QUERY_LIMIT = LLM_SAMPLE_ROWS;
|
|
5366
|
+
var MAX_COMPONENT_QUERY_LIMIT = MAX_ROWS_RENDERED;
|
|
5269
5367
|
var EXACT_MATCH_SIMILARITY_THRESHOLD = 0.99;
|
|
5270
5368
|
var DEFAULT_CONVERSATION_SIMILARITY_THRESHOLD = 0.8;
|
|
5271
5369
|
var MAX_TOOL_CALLING_ITERATIONS = 20;
|
|
@@ -5762,6 +5860,7 @@ var LLM = class {
|
|
|
5762
5860
|
/* Get a complete text response from an LLM (Anthropic or Groq) */
|
|
5763
5861
|
static async text(messages, options = {}) {
|
|
5764
5862
|
const [provider, modelName] = this._parseModel(options.model);
|
|
5863
|
+
messages = this._sanitizeMessages(messages);
|
|
5765
5864
|
if (provider === "anthropic") {
|
|
5766
5865
|
return this._anthropicText(messages, modelName, options);
|
|
5767
5866
|
} else if (provider === "groq") {
|
|
@@ -5777,6 +5876,7 @@ var LLM = class {
|
|
|
5777
5876
|
/* Stream response from an LLM (Anthropic or Groq) */
|
|
5778
5877
|
static async stream(messages, options = {}, json) {
|
|
5779
5878
|
const [provider, modelName] = this._parseModel(options.model);
|
|
5879
|
+
messages = this._sanitizeMessages(messages);
|
|
5780
5880
|
if (provider === "anthropic") {
|
|
5781
5881
|
return this._anthropicStream(messages, modelName, options, json);
|
|
5782
5882
|
} else if (provider === "groq") {
|
|
@@ -5792,6 +5892,7 @@ var LLM = class {
|
|
|
5792
5892
|
/* Stream response with tool calling support (Anthropic and Gemini) */
|
|
5793
5893
|
static async streamWithTools(messages, tools, toolHandler, options = {}, maxIterations = 3) {
|
|
5794
5894
|
const [provider, modelName] = this._parseModel(options.model);
|
|
5895
|
+
messages = this._sanitizeMessages(messages);
|
|
5795
5896
|
if (provider === "anthropic") {
|
|
5796
5897
|
return this._anthropicStreamWithTools(messages, tools, toolHandler, modelName, options, maxIterations);
|
|
5797
5898
|
} else if (provider === "gemini") {
|
|
@@ -5817,6 +5918,26 @@ var LLM = class {
|
|
|
5817
5918
|
}
|
|
5818
5919
|
return sys;
|
|
5819
5920
|
}
|
|
5921
|
+
/**
|
|
5922
|
+
* Strip unpaired UTF-16 surrogates from every text field of a message set.
|
|
5923
|
+
*
|
|
5924
|
+
* A lone surrogate (from mid-pair string slicing or corrupt source data)
|
|
5925
|
+
* serializes to a bare `\udXXX` escape that strict JSON parsers — including
|
|
5926
|
+
* the one on Anthropic's API — reject with "no low surrogate in string",
|
|
5927
|
+
* failing the whole request. Sanitizing here, at the single boundary every
|
|
5928
|
+
* provider call flows through, guarantees no request can carry one.
|
|
5929
|
+
*/
|
|
5930
|
+
static _sanitizeMessages(messages) {
|
|
5931
|
+
const sys = typeof messages.sys === "string" ? stripLoneSurrogates(messages.sys) : messages.sys.map(
|
|
5932
|
+
(block) => block?.type === "text" && typeof block.text === "string" ? { ...block, text: stripLoneSurrogates(block.text) } : block
|
|
5933
|
+
);
|
|
5934
|
+
return {
|
|
5935
|
+
...messages,
|
|
5936
|
+
sys,
|
|
5937
|
+
user: stripLoneSurrogates(messages.user),
|
|
5938
|
+
...messages.prefill !== void 0 && { prefill: stripLoneSurrogates(messages.prefill) }
|
|
5939
|
+
};
|
|
5940
|
+
}
|
|
5820
5941
|
/**
|
|
5821
5942
|
* Log cache usage metrics from Anthropic API response
|
|
5822
5943
|
* Shows cache hits, costs, and savings
|
|
@@ -6196,12 +6317,14 @@ var LLM = class {
|
|
|
6196
6317
|
let resultContent = typeof result === "string" ? result : JSON.stringify(result);
|
|
6197
6318
|
const MAX_RESULT_LENGTH = 5e4;
|
|
6198
6319
|
if (resultContent.length > MAX_RESULT_LENGTH) {
|
|
6199
|
-
resultContent = resultContent
|
|
6320
|
+
resultContent = safeTruncate(resultContent, MAX_RESULT_LENGTH) + "\n\n... [Result truncated - showing first 50000 characters of " + resultContent.length + " total]";
|
|
6200
6321
|
}
|
|
6201
6322
|
return {
|
|
6202
6323
|
type: "tool_result",
|
|
6203
6324
|
tool_use_id: toolUse.id,
|
|
6204
|
-
|
|
6325
|
+
// Final safety net: tool results carry source data and are built
|
|
6326
|
+
// mid-loop (after entry-point sanitize), so strip lone surrogates here.
|
|
6327
|
+
content: stripLoneSurrogates(resultContent)
|
|
6205
6328
|
};
|
|
6206
6329
|
} catch (error) {
|
|
6207
6330
|
return {
|
|
@@ -6716,11 +6839,12 @@ var LLM = class {
|
|
|
6716
6839
|
let resultContent = typeof result2 === "string" ? result2 : JSON.stringify(result2);
|
|
6717
6840
|
const MAX_RESULT_LENGTH = 5e4;
|
|
6718
6841
|
if (resultContent.length > MAX_RESULT_LENGTH) {
|
|
6719
|
-
resultContent = resultContent
|
|
6842
|
+
resultContent = safeTruncate(resultContent, MAX_RESULT_LENGTH) + "\n\n... [Result truncated - showing first 50000 characters of " + resultContent.length + " total]";
|
|
6720
6843
|
}
|
|
6721
6844
|
return {
|
|
6722
6845
|
name: fc.name,
|
|
6723
|
-
|
|
6846
|
+
// Final safety net: strip lone surrogates from source-data results.
|
|
6847
|
+
response: { result: stripLoneSurrogates(resultContent) }
|
|
6724
6848
|
};
|
|
6725
6849
|
} catch (error) {
|
|
6726
6850
|
return {
|
|
@@ -6993,12 +7117,12 @@ var LLM = class {
|
|
|
6993
7117
|
result = typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult);
|
|
6994
7118
|
const MAX_RESULT_LENGTH = 5e4;
|
|
6995
7119
|
if (result.length > MAX_RESULT_LENGTH) {
|
|
6996
|
-
result = result
|
|
7120
|
+
result = safeTruncate(result, MAX_RESULT_LENGTH) + "\n\n... [Result truncated - showing first 50000 characters of " + result.length + " total]";
|
|
6997
7121
|
}
|
|
6998
7122
|
} catch (error) {
|
|
6999
7123
|
result = JSON.stringify({ error: error instanceof Error ? error.message : String(error) });
|
|
7000
7124
|
}
|
|
7001
|
-
return { role: "tool", tool_call_id: tc.id, content: result };
|
|
7125
|
+
return { role: "tool", tool_call_id: tc.id, content: stripLoneSurrogates(result) };
|
|
7002
7126
|
}));
|
|
7003
7127
|
toolCallResults.forEach((r) => conversationMessages.push(r));
|
|
7004
7128
|
}
|
|
@@ -7137,6 +7261,130 @@ function extractObjectText(obj) {
|
|
|
7137
7261
|
return JSON.stringify(obj, null, 2);
|
|
7138
7262
|
}
|
|
7139
7263
|
|
|
7264
|
+
// src/userResponse/agents/data-summary.ts
|
|
7265
|
+
var MAIN_AGENT_COMPLETE_ROWS = MAX_ROWS_TO_LLM;
|
|
7266
|
+
function summarizeRows(rows) {
|
|
7267
|
+
if (!rows.length) return { totalRows: 0, columns: {} };
|
|
7268
|
+
const MAX_CATEGORIES = 20;
|
|
7269
|
+
const keys = Object.keys(rows[0]);
|
|
7270
|
+
const columns = {};
|
|
7271
|
+
const fmtDate = (ms) => {
|
|
7272
|
+
const d = new Date(ms);
|
|
7273
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
7274
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
|
7275
|
+
};
|
|
7276
|
+
for (const key of keys) {
|
|
7277
|
+
let nonNull = 0, zeros = 0, isNumeric = true, isDateLike = true, sum = 0;
|
|
7278
|
+
let min, max;
|
|
7279
|
+
let dMin, dMax;
|
|
7280
|
+
const counts = /* @__PURE__ */ new Map();
|
|
7281
|
+
let overflow = false;
|
|
7282
|
+
for (const row of rows) {
|
|
7283
|
+
const v = row[key];
|
|
7284
|
+
if (v === null || v === void 0 || v === "") continue;
|
|
7285
|
+
nonNull++;
|
|
7286
|
+
const num = typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" && !isNaN(Number(v)) ? Number(v) : NaN;
|
|
7287
|
+
if (!Number.isNaN(num) && typeof v !== "boolean") {
|
|
7288
|
+
if (num === 0) zeros++;
|
|
7289
|
+
min = min === void 0 ? num : Math.min(min, num);
|
|
7290
|
+
max = max === void 0 ? num : Math.max(max, num);
|
|
7291
|
+
sum += num;
|
|
7292
|
+
} else {
|
|
7293
|
+
isNumeric = false;
|
|
7294
|
+
}
|
|
7295
|
+
const t = v instanceof Date ? v.getTime() : typeof v === "string" ? Date.parse(v) : NaN;
|
|
7296
|
+
if (!Number.isNaN(t) && typeof v !== "number") {
|
|
7297
|
+
dMin = dMin === void 0 ? t : Math.min(dMin, t);
|
|
7298
|
+
dMax = dMax === void 0 ? t : Math.max(dMax, t);
|
|
7299
|
+
} else {
|
|
7300
|
+
isDateLike = false;
|
|
7301
|
+
}
|
|
7302
|
+
if (!overflow) {
|
|
7303
|
+
const sv = String(v);
|
|
7304
|
+
counts.set(sv, (counts.get(sv) || 0) + 1);
|
|
7305
|
+
if (counts.size > MAX_CATEGORIES * 5) overflow = true;
|
|
7306
|
+
}
|
|
7307
|
+
}
|
|
7308
|
+
const nulls = rows.length - nonNull;
|
|
7309
|
+
if (nonNull === 0) {
|
|
7310
|
+
columns[key] = { type: "empty", nulls };
|
|
7311
|
+
continue;
|
|
7312
|
+
}
|
|
7313
|
+
if (isNumeric) {
|
|
7314
|
+
columns[key] = {
|
|
7315
|
+
type: "number",
|
|
7316
|
+
nonNull,
|
|
7317
|
+
nulls,
|
|
7318
|
+
zeros,
|
|
7319
|
+
min,
|
|
7320
|
+
max,
|
|
7321
|
+
avg: Math.round(sum / nonNull * 100) / 100
|
|
7322
|
+
};
|
|
7323
|
+
} else if (isDateLike) {
|
|
7324
|
+
columns[key] = {
|
|
7325
|
+
type: "date",
|
|
7326
|
+
nonNull,
|
|
7327
|
+
min: fmtDate(dMin),
|
|
7328
|
+
max: fmtDate(dMax),
|
|
7329
|
+
distinct: overflow ? `${MAX_CATEGORIES * 5}+` : counts.size
|
|
7330
|
+
};
|
|
7331
|
+
} else if (!overflow && counts.size <= MAX_CATEGORIES) {
|
|
7332
|
+
const dist = {};
|
|
7333
|
+
for (const [k, c] of [...counts.entries()].sort((a, b) => b[1] - a[1])) dist[k] = c;
|
|
7334
|
+
columns[key] = { type: "category", distinct: counts.size, counts: dist };
|
|
7335
|
+
} else {
|
|
7336
|
+
columns[key] = {
|
|
7337
|
+
type: "text",
|
|
7338
|
+
distinct: overflow ? `${MAX_CATEGORIES * 5}+` : counts.size,
|
|
7339
|
+
examples: [...counts.keys()].slice(0, 3)
|
|
7340
|
+
};
|
|
7341
|
+
}
|
|
7342
|
+
}
|
|
7343
|
+
const dims = keys.filter((k) => columns[k]?.type === "category" && columns[k].distinct <= 6).sort((a, b) => columns[a].distinct - columns[b].distinct);
|
|
7344
|
+
const measures = keys.filter((k) => columns[k]?.type === "number");
|
|
7345
|
+
let groups;
|
|
7346
|
+
if (dims.length && measures.length) {
|
|
7347
|
+
const MAX_GROUPS = 30;
|
|
7348
|
+
let useDims = dims.slice(0, 2);
|
|
7349
|
+
const keyOf = (row) => useDims.map((d) => `${d}=${row[d]}`).join(" | ");
|
|
7350
|
+
if (new Set(rows.map(keyOf)).size > MAX_GROUPS) useDims = dims.slice(0, 1);
|
|
7351
|
+
const agg = /* @__PURE__ */ new Map();
|
|
7352
|
+
for (const row of rows) {
|
|
7353
|
+
const key = useDims.map((d) => `${d}=${row[d]}`).join(" | ");
|
|
7354
|
+
let g = agg.get(key);
|
|
7355
|
+
if (!g) {
|
|
7356
|
+
g = { count: 0, sums: {}, ns: {} };
|
|
7357
|
+
agg.set(key, g);
|
|
7358
|
+
}
|
|
7359
|
+
g.count++;
|
|
7360
|
+
for (const m of measures) {
|
|
7361
|
+
const v = row[m];
|
|
7362
|
+
if (typeof v === "number" && !Number.isNaN(v)) {
|
|
7363
|
+
g.sums[m] = (g.sums[m] || 0) + v;
|
|
7364
|
+
g.ns[m] = (g.ns[m] || 0) + 1;
|
|
7365
|
+
}
|
|
7366
|
+
}
|
|
7367
|
+
}
|
|
7368
|
+
if (agg.size <= MAX_GROUPS) {
|
|
7369
|
+
const isCountLike = (name) => /count|\bobs\b|observation|qty|quantity|\bnum|total|evidential|volume|units/i.test(name);
|
|
7370
|
+
groups = {
|
|
7371
|
+
by: useDims,
|
|
7372
|
+
rows: [...agg.entries()].map(([key, g]) => {
|
|
7373
|
+
const avg = {};
|
|
7374
|
+
const sum = {};
|
|
7375
|
+
for (const m of measures) {
|
|
7376
|
+
if (!g.ns[m]) continue;
|
|
7377
|
+
avg[m] = Math.round(g.sums[m] / g.ns[m] * 100) / 100;
|
|
7378
|
+
if (isCountLike(m)) sum[m] = Math.round(g.sums[m] * 100) / 100;
|
|
7379
|
+
}
|
|
7380
|
+
return Object.keys(sum).length ? { key, count: g.count, avg, sum } : { key, count: g.count, avg };
|
|
7381
|
+
})
|
|
7382
|
+
};
|
|
7383
|
+
}
|
|
7384
|
+
}
|
|
7385
|
+
return groups ? { totalRows: rows.length, columns, groups } : { totalRows: rows.length, columns };
|
|
7386
|
+
}
|
|
7387
|
+
|
|
7140
7388
|
// src/userResponse/agents/agent-prompt-builder.ts
|
|
7141
7389
|
function buildSourceSummaries(externalTools) {
|
|
7142
7390
|
return externalTools.map((tool) => {
|
|
@@ -7356,8 +7604,15 @@ ${result}`;
|
|
|
7356
7604
|
await streamDelay();
|
|
7357
7605
|
}
|
|
7358
7606
|
const cappedInput = { ...toolInput };
|
|
7359
|
-
if (cappedInput.limit === void 0 || cappedInput.limit > this.config.
|
|
7360
|
-
cappedInput.limit = this.config.
|
|
7607
|
+
if (cappedInput.limit === void 0 || cappedInput.limit > this.config.maxRowsFetched) {
|
|
7608
|
+
cappedInput.limit = this.config.maxRowsFetched;
|
|
7609
|
+
}
|
|
7610
|
+
const _st = this.extractSourceType();
|
|
7611
|
+
if (typeof cappedInput.sql === "string") {
|
|
7612
|
+
cappedInput.sql = ensureQueryLimit(cappedInput.sql, this.config.maxRowsFetched, this.config.maxRowsFetched, _st);
|
|
7613
|
+
}
|
|
7614
|
+
if (typeof cappedInput.query === "string") {
|
|
7615
|
+
cappedInput.query = ensureQueryLimit(cappedInput.query, this.config.maxRowsFetched, this.config.maxRowsFetched, _st);
|
|
7361
7616
|
}
|
|
7362
7617
|
queryExecuted = cappedInput.sql || cappedInput.query || JSON.stringify(cappedInput);
|
|
7363
7618
|
if (this.streamBuffer.hasCallback() && queryExecuted) {
|
|
@@ -7408,8 +7663,11 @@ Analyze the error and try again with a corrected query.`;
|
|
|
7408
7663
|
`);
|
|
7409
7664
|
if (resultData.length > 0) {
|
|
7410
7665
|
try {
|
|
7411
|
-
const
|
|
7412
|
-
|
|
7666
|
+
const preview = formatQueryResultForLLM(resultData, {
|
|
7667
|
+
maxRows: STREAM_PREVIEW_MAX_ROWS,
|
|
7668
|
+
maxCharsPerField: STREAM_PREVIEW_MAX_CHARS
|
|
7669
|
+
});
|
|
7670
|
+
this.streamBuffer.write(`<DataTable>${JSON.stringify(preview.data)}</DataTable>
|
|
7413
7671
|
|
|
7414
7672
|
`);
|
|
7415
7673
|
} catch {
|
|
@@ -7432,7 +7690,12 @@ Analyze the error and try again with a corrected query.`;
|
|
|
7432
7690
|
_totalRecords: totalRowsMatched,
|
|
7433
7691
|
_recordsShown: resultData.length,
|
|
7434
7692
|
_metadata: result.metadata,
|
|
7435
|
-
_sampleData: resultData.slice(0,
|
|
7693
|
+
_sampleData: resultData.slice(0, TOOL_TRACKING_SAMPLE_ROWS),
|
|
7694
|
+
// For the main agent: a bounded summary over the FULL fetched
|
|
7695
|
+
// result (complete structure regardless of size) + a complete
|
|
7696
|
+
// slice for small results (so lookups arrive whole, not biased).
|
|
7697
|
+
_summary: summarizeRows(resultData),
|
|
7698
|
+
_mainAgentRows: resultData.slice(0, MAIN_AGENT_COMPLETE_ROWS)
|
|
7436
7699
|
},
|
|
7437
7700
|
outputSchema: this.tool.outputSchema,
|
|
7438
7701
|
sourceSchema: this.tool.description,
|
|
@@ -7440,7 +7703,7 @@ Analyze the error and try again with a corrected query.`;
|
|
|
7440
7703
|
};
|
|
7441
7704
|
allExecutedTools.push(executedTool);
|
|
7442
7705
|
const formatted = typeof formattedResult === "string" ? formattedResult : JSON.stringify(formattedResult);
|
|
7443
|
-
const followUpNote = successfulQueries < 2 ? "
|
|
7706
|
+
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.";
|
|
7444
7707
|
return `\u2705 Query executed successfully. ${resultData.length} rows returned (${totalRowsMatched} total matched). ${followUpNote}
|
|
7445
7708
|
|
|
7446
7709
|
${formatted}`;
|
|
@@ -7502,7 +7765,13 @@ Analyze the error and try again with a corrected query.`;
|
|
|
7502
7765
|
sourceId: this.tool.id,
|
|
7503
7766
|
sourceName: this.tool.name,
|
|
7504
7767
|
success: true,
|
|
7505
|
-
|
|
7768
|
+
// Don't retain the full fetched result — only the bounded slice the
|
|
7769
|
+
// main agent can actually use. The complete-structure summary and the
|
|
7770
|
+
// complete-small rows already live on each executedTool (`_summary` /
|
|
7771
|
+
// `_mainAgentRows`); keeping the full resultData here would hold up to
|
|
7772
|
+
// MAX_ROWS_FETCHED rows × every query in memory for the whole request
|
|
7773
|
+
// for no functional gain.
|
|
7774
|
+
data: resultData.slice(0, MAIN_AGENT_COMPLETE_ROWS),
|
|
7506
7775
|
metadata: {
|
|
7507
7776
|
totalRowsMatched,
|
|
7508
7777
|
rowsReturned: resultData.length,
|
|
@@ -7553,13 +7822,13 @@ Analyze the error and try again with a corrected query.`;
|
|
|
7553
7822
|
const fullSchema = this.options.preResolvedSchema || this.tool.fullSchema || this.tool.description || "No schema available";
|
|
7554
7823
|
const databaseRules = await promptLoader.loadDatabaseRulesForType(sourceType);
|
|
7555
7824
|
const rowLimitSyntax = {
|
|
7556
|
-
"mssql": `Use SELECT TOP ${this.config.
|
|
7557
|
-
"postgres": `Add LIMIT ${this.config.
|
|
7558
|
-
"mysql": `Add LIMIT ${this.config.
|
|
7559
|
-
"excel": `Add LIMIT ${this.config.
|
|
7560
|
-
"csv": `Add LIMIT ${this.config.
|
|
7825
|
+
"mssql": `Use SELECT TOP ${this.config.maxRowsFetched} in every SELECT statement`,
|
|
7826
|
+
"postgres": `Add LIMIT ${this.config.maxRowsFetched} at the end of every query`,
|
|
7827
|
+
"mysql": `Add LIMIT ${this.config.maxRowsFetched} at the end of every query`,
|
|
7828
|
+
"excel": `Add LIMIT ${this.config.maxRowsFetched} at the end of every query`,
|
|
7829
|
+
"csv": `Add LIMIT ${this.config.maxRowsFetched} at the end of every query`
|
|
7561
7830
|
};
|
|
7562
|
-
const rowLimitInstruction = rowLimitSyntax[sourceType] || `Limit results to ${this.config.
|
|
7831
|
+
const rowLimitInstruction = rowLimitSyntax[sourceType] || `Limit results to ${this.config.maxRowsFetched} rows`;
|
|
7563
7832
|
const hasSchemaSearch = !this.options.skipSchemaSearch && !!this.tool.schemaSearchFn;
|
|
7564
7833
|
const schemaTier = this.tool.schemaTier || "";
|
|
7565
7834
|
let schemaSearchInstructions = "";
|
|
@@ -7598,8 +7867,13 @@ Even if a table appears in the detailed schema above, search_schema returns samp
|
|
|
7598
7867
|
FULL_SCHEMA: fullSchema,
|
|
7599
7868
|
DATABASE_RULES: databaseRules,
|
|
7600
7869
|
ROW_LIMIT_SYNTAX: rowLimitInstruction,
|
|
7870
|
+
// NOTE: MAX_ROWS here governs the SQL row cap (the {{MAX_ROWS}} in the
|
|
7871
|
+
// "limit results to N rows" line), so it MUST match the fetch cap that
|
|
7872
|
+
// ROW_LIMIT_SYNTAX states — not the smaller LLM-show cap — or the prompt
|
|
7873
|
+
// contradicts itself ("max 10 rows" + "LIMIT 50") and the LLM picks its
|
|
7874
|
+
// own number. See maxRowsFetched override below.
|
|
7601
7875
|
SCHEMA_SEARCH_INSTRUCTIONS: schemaSearchInstructions,
|
|
7602
|
-
MAX_ROWS: String(this.config.
|
|
7876
|
+
MAX_ROWS: String(this.config.maxRowsFetched),
|
|
7603
7877
|
AGGREGATION_MODE: aggregation,
|
|
7604
7878
|
GLOBAL_KNOWLEDGE_BASE: this.config.globalKnowledgeBase || "No global knowledge base available.",
|
|
7605
7879
|
CURRENT_DATETIME: getCurrentDateTimeForPrompt(),
|
|
@@ -7721,13 +7995,27 @@ Even if a table appears in the detailed schema above, search_schema returns samp
|
|
|
7721
7995
|
// src/userResponse/scripts/script-runner.ts
|
|
7722
7996
|
import { spawn } from "child_process";
|
|
7723
7997
|
import * as path5 from "path";
|
|
7998
|
+
import * as os from "os";
|
|
7724
7999
|
|
|
7725
8000
|
// src/userResponse/scripts/script-ipc.ts
|
|
7726
8001
|
function encodeMessage(msg) {
|
|
7727
8002
|
return JSON.stringify(msg) + "\n";
|
|
7728
8003
|
}
|
|
8004
|
+
var LineOverflowError = class extends Error {
|
|
8005
|
+
constructor(buffered, limit) {
|
|
8006
|
+
super(`IPC message exceeded ${limit} bytes without a newline (${buffered} buffered) \u2014 aborting to avoid unbounded memory.`);
|
|
8007
|
+
this.buffered = buffered;
|
|
8008
|
+
this.limit = limit;
|
|
8009
|
+
this.name = "LineOverflowError";
|
|
8010
|
+
}
|
|
8011
|
+
};
|
|
8012
|
+
var DEFAULT_MAX_IPC_BYTES = (() => {
|
|
8013
|
+
const envVal = Number(process.env.SCRIPT_MAX_IPC_BYTES);
|
|
8014
|
+
return Number.isFinite(envVal) && envVal > 0 ? Math.floor(envVal) : 64 * 1024 * 1024;
|
|
8015
|
+
})();
|
|
7729
8016
|
var LineSplitter = class {
|
|
7730
|
-
constructor() {
|
|
8017
|
+
constructor(maxBytes = DEFAULT_MAX_IPC_BYTES) {
|
|
8018
|
+
this.maxBytes = maxBytes;
|
|
7731
8019
|
this.buffer = "";
|
|
7732
8020
|
}
|
|
7733
8021
|
push(chunk) {
|
|
@@ -7739,6 +8027,11 @@ var LineSplitter = class {
|
|
|
7739
8027
|
this.buffer = this.buffer.slice(idx + 1);
|
|
7740
8028
|
if (line.length > 0) lines.push(line);
|
|
7741
8029
|
}
|
|
8030
|
+
if (this.buffer.length > this.maxBytes) {
|
|
8031
|
+
const buffered = this.buffer.length;
|
|
8032
|
+
this.buffer = "";
|
|
8033
|
+
throw new LineOverflowError(buffered, this.maxBytes);
|
|
8034
|
+
}
|
|
7742
8035
|
return lines;
|
|
7743
8036
|
}
|
|
7744
8037
|
/** Flush any remaining partial data (useful on stream close) */
|
|
@@ -7751,7 +8044,57 @@ var LineSplitter = class {
|
|
|
7751
8044
|
};
|
|
7752
8045
|
|
|
7753
8046
|
// src/userResponse/scripts/script-runner.ts
|
|
7754
|
-
var
|
|
8047
|
+
var DEFAULT_SCRIPT_TIMEOUT_MS = 6e4;
|
|
8048
|
+
var SCRIPT_TIMEOUT_MS = (() => {
|
|
8049
|
+
const envVal = Number(process.env.SCRIPT_TIMEOUT_MS);
|
|
8050
|
+
return Number.isFinite(envVal) && envVal > 0 ? envVal : DEFAULT_SCRIPT_TIMEOUT_MS;
|
|
8051
|
+
})();
|
|
8052
|
+
var MAX_CONCURRENT_SCRIPTS = (() => {
|
|
8053
|
+
const envVal = Number(process.env.SCRIPT_MAX_CONCURRENCY);
|
|
8054
|
+
if (Number.isFinite(envVal) && envVal > 0) return Math.floor(envVal);
|
|
8055
|
+
return Math.max(2, Math.min(8, (os.cpus()?.length || 4) - 1));
|
|
8056
|
+
})();
|
|
8057
|
+
var MAX_SCRIPT_QUEUE = (() => {
|
|
8058
|
+
const envVal = Number(process.env.SCRIPT_MAX_QUEUE);
|
|
8059
|
+
if (Number.isFinite(envVal) && envVal >= 0) return Math.floor(envVal);
|
|
8060
|
+
return 100;
|
|
8061
|
+
})();
|
|
8062
|
+
var ScriptCapacityError = class extends Error {
|
|
8063
|
+
constructor(message) {
|
|
8064
|
+
super(message);
|
|
8065
|
+
this.name = "ScriptCapacityError";
|
|
8066
|
+
}
|
|
8067
|
+
};
|
|
8068
|
+
var Semaphore = class {
|
|
8069
|
+
constructor(max, maxQueue) {
|
|
8070
|
+
this.max = max;
|
|
8071
|
+
this.maxQueue = maxQueue;
|
|
8072
|
+
this.active = 0;
|
|
8073
|
+
this.waiters = [];
|
|
8074
|
+
}
|
|
8075
|
+
async acquire() {
|
|
8076
|
+
if (this.active < this.max) {
|
|
8077
|
+
this.active++;
|
|
8078
|
+
return;
|
|
8079
|
+
}
|
|
8080
|
+
if (this.waiters.length >= this.maxQueue) {
|
|
8081
|
+
throw new ScriptCapacityError(
|
|
8082
|
+
`Script execution at capacity (${this.max} running, ${this.waiters.length} queued). Try again shortly.`
|
|
8083
|
+
);
|
|
8084
|
+
}
|
|
8085
|
+
return new Promise((resolve2) => this.waiters.push(resolve2));
|
|
8086
|
+
}
|
|
8087
|
+
release() {
|
|
8088
|
+
const next = this.waiters.shift();
|
|
8089
|
+
if (next) next();
|
|
8090
|
+
else this.active--;
|
|
8091
|
+
}
|
|
8092
|
+
get queued() {
|
|
8093
|
+
return this.waiters.length;
|
|
8094
|
+
}
|
|
8095
|
+
};
|
|
8096
|
+
var scriptSemaphore = new Semaphore(MAX_CONCURRENT_SCRIPTS, MAX_SCRIPT_QUEUE);
|
|
8097
|
+
logger.info(`[ScriptRunner] Concurrency cap: ${MAX_CONCURRENT_SCRIPTS} concurrent, queue ${MAX_SCRIPT_QUEUE}`);
|
|
7755
8098
|
var tsxBinaryPath = null;
|
|
7756
8099
|
function resolveTsxBinary() {
|
|
7757
8100
|
if (tsxBinaryPath) return tsxBinaryPath;
|
|
@@ -7774,6 +8117,22 @@ async function runScript(recipe, scriptPath, params, options) {
|
|
|
7774
8117
|
const resolvedParams = coerceParams(recipe, applyDefaults(recipe, params));
|
|
7775
8118
|
const paramSchema = buildParamSchema(recipe);
|
|
7776
8119
|
logger.info(`[ScriptRunner] Executing "${recipe.name}" (${recipe.id}) with params: ${JSON.stringify(resolvedParams)}`);
|
|
8120
|
+
try {
|
|
8121
|
+
if (scriptSemaphore.queued > 0) {
|
|
8122
|
+
logger.info(`[ScriptRunner] "${recipe.name}" queued (${scriptSemaphore.queued} ahead)`);
|
|
8123
|
+
}
|
|
8124
|
+
await scriptSemaphore.acquire();
|
|
8125
|
+
} catch (capErr) {
|
|
8126
|
+
const msg = capErr instanceof Error ? capErr.message : String(capErr);
|
|
8127
|
+
logger.warn(`[ScriptRunner] "${recipe.name}" shed: ${msg}`);
|
|
8128
|
+
return {
|
|
8129
|
+
success: false,
|
|
8130
|
+
data: [],
|
|
8131
|
+
executedQueries: [],
|
|
8132
|
+
error: msg,
|
|
8133
|
+
executionTimeMs: Date.now() - startedAt
|
|
8134
|
+
};
|
|
8135
|
+
}
|
|
7777
8136
|
try {
|
|
7778
8137
|
const result = await executeInSubprocess(
|
|
7779
8138
|
scriptPath,
|
|
@@ -7815,6 +8174,8 @@ async function runScript(recipe, scriptPath, params, options) {
|
|
|
7815
8174
|
error: msg,
|
|
7816
8175
|
executionTimeMs: totalMs
|
|
7817
8176
|
};
|
|
8177
|
+
} finally {
|
|
8178
|
+
scriptSemaphore.release();
|
|
7818
8179
|
}
|
|
7819
8180
|
}
|
|
7820
8181
|
function executeInSubprocess(scriptPath, params, paramSchema, externalTools, streamBuffer, timeoutMs) {
|
|
@@ -7827,6 +8188,15 @@ function executeInSubprocess(scriptPath, params, paramSchema, externalTools, str
|
|
|
7827
8188
|
[tsxBin, bootstrap, scriptPath],
|
|
7828
8189
|
{
|
|
7829
8190
|
stdio: ["pipe", "pipe", "pipe"],
|
|
8191
|
+
// `detached: true` makes the child a process-group LEADER. tsx 4.x
|
|
8192
|
+
// re-execs node to run the user script, so the script is a GRANDCHILD;
|
|
8193
|
+
// a plain `child.kill()` would hit only the tsx wrapper and orphan the
|
|
8194
|
+
// grandchild (a runaway/looping script that outlives the timeout — #4).
|
|
8195
|
+
// With its own group we can SIGKILL the whole tree via `-pid` in
|
|
8196
|
+
// cleanup(). We deliberately do NOT unref() — the parent still tracks
|
|
8197
|
+
// the child's exit and pipes normally. (POSIX; Windows falls back to
|
|
8198
|
+
// a direct kill in cleanup().)
|
|
8199
|
+
detached: process.platform !== "win32",
|
|
7830
8200
|
env: {
|
|
7831
8201
|
...process.env,
|
|
7832
8202
|
// Keep the child quiet about dotenv etc.
|
|
@@ -7834,22 +8204,23 @@ function executeInSubprocess(scriptPath, params, paramSchema, externalTools, str
|
|
|
7834
8204
|
}
|
|
7835
8205
|
}
|
|
7836
8206
|
);
|
|
7837
|
-
const toolsById = /* @__PURE__ */ new Map();
|
|
7838
|
-
const toolsByName = /* @__PURE__ */ new Map();
|
|
7839
|
-
for (const t of externalTools) {
|
|
7840
|
-
toolsById.set(t.id, t);
|
|
7841
|
-
toolsByName.set(t.name.toLowerCase(), t);
|
|
7842
|
-
}
|
|
7843
8207
|
let resolved = false;
|
|
7844
8208
|
let executedQueries = [];
|
|
7845
8209
|
let stderrBuffer = "";
|
|
7846
8210
|
const cleanup = () => {
|
|
7847
|
-
if (
|
|
8211
|
+
if (child.killed) return;
|
|
8212
|
+
const pid = child.pid;
|
|
8213
|
+
if (pid && process.platform !== "win32") {
|
|
7848
8214
|
try {
|
|
7849
|
-
|
|
8215
|
+
process.kill(-pid, "SIGKILL");
|
|
8216
|
+
return;
|
|
7850
8217
|
} catch {
|
|
7851
8218
|
}
|
|
7852
8219
|
}
|
|
8220
|
+
try {
|
|
8221
|
+
child.kill("SIGKILL");
|
|
8222
|
+
} catch {
|
|
8223
|
+
}
|
|
7853
8224
|
};
|
|
7854
8225
|
const finish = (r) => {
|
|
7855
8226
|
if (resolved) return;
|
|
@@ -7869,7 +8240,19 @@ function executeInSubprocess(scriptPath, params, paramSchema, externalTools, str
|
|
|
7869
8240
|
const stdoutSplitter = new LineSplitter();
|
|
7870
8241
|
child.stdout.setEncoding("utf-8");
|
|
7871
8242
|
child.stdout.on("data", (chunk) => {
|
|
7872
|
-
|
|
8243
|
+
let lines;
|
|
8244
|
+
try {
|
|
8245
|
+
lines = stdoutSplitter.push(chunk);
|
|
8246
|
+
} catch (err) {
|
|
8247
|
+
finish({
|
|
8248
|
+
kind: "err",
|
|
8249
|
+
phase: "ipc",
|
|
8250
|
+
message: err instanceof Error ? err.message : String(err),
|
|
8251
|
+
executedQueries
|
|
8252
|
+
});
|
|
8253
|
+
return;
|
|
8254
|
+
}
|
|
8255
|
+
for (const line of lines) {
|
|
7873
8256
|
let msg;
|
|
7874
8257
|
try {
|
|
7875
8258
|
msg = JSON.parse(line);
|
|
@@ -7913,9 +8296,6 @@ function executeInSubprocess(scriptPath, params, paramSchema, externalTools, str
|
|
|
7913
8296
|
child.stdin.write(init);
|
|
7914
8297
|
function handleChildMessage(msg) {
|
|
7915
8298
|
switch (msg.type) {
|
|
7916
|
-
case "query":
|
|
7917
|
-
void handleQuery8(msg.id, msg.toolId, msg.sql);
|
|
7918
|
-
return;
|
|
7919
8299
|
case "stream":
|
|
7920
8300
|
if (streamBuffer?.hasCallback()) {
|
|
7921
8301
|
streamBuffer.write(msg.chunk);
|
|
@@ -7941,117 +8321,21 @@ ${msg.stack}` : ""),
|
|
|
7941
8321
|
return;
|
|
7942
8322
|
}
|
|
7943
8323
|
}
|
|
7944
|
-
async function handleQuery8(id, toolId, sql) {
|
|
7945
|
-
const tool = resolveAuthorizedTool(toolId, toolsById, toolsByName);
|
|
7946
|
-
if (!tool) {
|
|
7947
|
-
sendToChild({
|
|
7948
|
-
type: "query_error",
|
|
7949
|
-
id,
|
|
7950
|
-
error: `Tool "${toolId}" is not authorized for this request.`
|
|
7951
|
-
});
|
|
7952
|
-
return;
|
|
7953
|
-
}
|
|
7954
|
-
const startedAt = Date.now();
|
|
7955
|
-
streamQueryStart(streamBuffer, tool.name, sql);
|
|
7956
|
-
try {
|
|
7957
|
-
const result = await tool.fn({ sql });
|
|
7958
|
-
const elapsed = Date.now() - startedAt;
|
|
7959
|
-
if (result && result.error) {
|
|
7960
|
-
const errMsg = typeof result.error === "string" ? result.error : JSON.stringify(result.error);
|
|
7961
|
-
streamQueryDone(streamBuffer, elapsed);
|
|
7962
|
-
streamQueryError(streamBuffer, tool.name, errMsg);
|
|
7963
|
-
sendToChild({ type: "query_error", id, error: errMsg });
|
|
7964
|
-
return;
|
|
7965
|
-
}
|
|
7966
|
-
const data = result?.data || [];
|
|
7967
|
-
const count = result?.count ?? data.length;
|
|
7968
|
-
const totalCount = result?.metadata?.totalCount;
|
|
7969
|
-
streamQueryDone(streamBuffer, elapsed);
|
|
7970
|
-
streamQuerySuccess(streamBuffer, tool.name, data.length, totalCount ?? count);
|
|
7971
|
-
streamDataPreview(streamBuffer, data);
|
|
7972
|
-
sendToChild({
|
|
7973
|
-
type: "query_result",
|
|
7974
|
-
id,
|
|
7975
|
-
data,
|
|
7976
|
-
count,
|
|
7977
|
-
metadata: { totalCount, executionTimeMs: elapsed }
|
|
7978
|
-
});
|
|
7979
|
-
} catch (err) {
|
|
7980
|
-
const elapsed = Date.now() - startedAt;
|
|
7981
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
7982
|
-
streamQueryDone(streamBuffer, elapsed);
|
|
7983
|
-
streamQueryError(streamBuffer, tool.name, errMsg);
|
|
7984
|
-
sendToChild({ type: "query_error", id, error: errMsg });
|
|
7985
|
-
}
|
|
7986
|
-
}
|
|
7987
|
-
function sendToChild(msg) {
|
|
7988
|
-
try {
|
|
7989
|
-
child.stdin.write(encodeMessage(msg));
|
|
7990
|
-
} catch (err) {
|
|
7991
|
-
logger.warn(`[ScriptRunner] Failed to write to child stdin: ${err.message}`);
|
|
7992
|
-
}
|
|
7993
|
-
}
|
|
7994
8324
|
});
|
|
7995
8325
|
}
|
|
7996
|
-
function resolveAuthorizedTool(toolId, byId, byName) {
|
|
7997
|
-
const exact = byId.get(toolId);
|
|
7998
|
-
if (exact) return exact;
|
|
7999
|
-
const exactName = byName.get(toolId.toLowerCase());
|
|
8000
|
-
if (exactName) return exactName;
|
|
8001
|
-
for (const [id, tool] of byId) {
|
|
8002
|
-
if (id.includes(toolId) || toolId.includes(id)) return tool;
|
|
8003
|
-
}
|
|
8004
|
-
const lower = toolId.toLowerCase();
|
|
8005
|
-
for (const [name, tool] of byName) {
|
|
8006
|
-
if (name.includes(lower) || lower.includes(name)) return tool;
|
|
8007
|
-
}
|
|
8008
|
-
return null;
|
|
8009
|
-
}
|
|
8010
|
-
function streamQueryStart(sb, sourceName, sql) {
|
|
8011
|
-
if (!sb?.hasCallback()) return;
|
|
8012
|
-
sb.write(`
|
|
8013
|
-
\u{1F4DD} **Querying ${sourceName}:**
|
|
8014
|
-
\`\`\`sql
|
|
8015
|
-
${sql}
|
|
8016
|
-
\`\`\`
|
|
8017
|
-
|
|
8018
|
-
`);
|
|
8019
|
-
sb.write(`__QUERY_TIMER_START_Executing query__`);
|
|
8020
|
-
}
|
|
8021
|
-
function streamQueryDone(sb, ms) {
|
|
8022
|
-
if (!sb?.hasCallback()) return;
|
|
8023
|
-
sb.write(`__QUERY_TIMER_DONE_${(ms / 1e3).toFixed(1)}__
|
|
8024
|
-
|
|
8025
|
-
`);
|
|
8026
|
-
}
|
|
8027
|
-
function streamQuerySuccess(sb, sourceName, rows, total) {
|
|
8028
|
-
if (!sb?.hasCallback()) return;
|
|
8029
|
-
const totalInfo = total > rows ? ` of ${total} total` : "";
|
|
8030
|
-
sb.write(`\u2705 **${rows} rows${totalInfo} from ${sourceName}**
|
|
8031
|
-
|
|
8032
|
-
`);
|
|
8033
|
-
}
|
|
8034
|
-
function streamQueryError(sb, sourceName, msg) {
|
|
8035
|
-
if (!sb?.hasCallback()) return;
|
|
8036
|
-
sb.write(`\u274C **Query failed on ${sourceName}:** ${msg}
|
|
8037
|
-
|
|
8038
|
-
`);
|
|
8039
|
-
}
|
|
8040
|
-
function streamDataPreview(sb, data) {
|
|
8041
|
-
if (!sb?.hasCallback() || data.length === 0) return;
|
|
8042
|
-
try {
|
|
8043
|
-
sb.write(`<DataTable>${JSON.stringify(data.slice(0, 10))}</DataTable>
|
|
8044
|
-
|
|
8045
|
-
`);
|
|
8046
|
-
} catch {
|
|
8047
|
-
sb.write(`_Data preview not available_
|
|
8048
|
-
|
|
8049
|
-
`);
|
|
8050
|
-
}
|
|
8051
|
-
}
|
|
8052
8326
|
function withSynthesizedFinalData(queries, finalData, finalCount) {
|
|
8053
8327
|
if (!finalData || finalData.length === 0) return queries;
|
|
8054
|
-
if (!queries || queries.length === 0)
|
|
8328
|
+
if (!queries || queries.length === 0) {
|
|
8329
|
+
return [{
|
|
8330
|
+
sourceId: "computed:_final",
|
|
8331
|
+
sourceName: "Script final dataset",
|
|
8332
|
+
sql: "-- script final returned data (runTool / pure JS, no ctx.query)",
|
|
8333
|
+
data: finalData,
|
|
8334
|
+
count: finalCount,
|
|
8335
|
+
executionTimeMs: 0,
|
|
8336
|
+
virtual: true
|
|
8337
|
+
}];
|
|
8338
|
+
}
|
|
8055
8339
|
const firstSqlQuery = queries.find((q) => !q.virtual);
|
|
8056
8340
|
const rawSample = firstSqlQuery?.data?.[0];
|
|
8057
8341
|
const finalSample = finalData[0];
|
|
@@ -8191,10 +8475,11 @@ var EXECUTE_SCRIPT_TOOL_DEF = {
|
|
|
8191
8475
|
};
|
|
8192
8476
|
var MAX_SCRIPT_ATTEMPTS = 3;
|
|
8193
8477
|
var MainAgent = class {
|
|
8194
|
-
constructor(externalTools, config, scriptStore, turnId, streamBuffer) {
|
|
8478
|
+
constructor(externalTools, config, scriptStore, turnId, streamBuffer, workflows = []) {
|
|
8195
8479
|
this.createdFromPrompt = "";
|
|
8196
8480
|
this.scriptState = { recipeId: null, attempts: 0, lastSuccessfulResult: null };
|
|
8197
8481
|
this.externalTools = externalTools;
|
|
8482
|
+
this.workflows = workflows;
|
|
8198
8483
|
this.config = config;
|
|
8199
8484
|
this.streamBuffer = streamBuffer || new StreamBuffer();
|
|
8200
8485
|
this.scriptStore = scriptStore ?? null;
|
|
@@ -8220,22 +8505,31 @@ var MainAgent = class {
|
|
|
8220
8505
|
this.createdFromPrompt = userPrompt;
|
|
8221
8506
|
const sourceTools = this.externalTools.filter((t) => t.toolType !== "direct");
|
|
8222
8507
|
const directTools = this.externalTools.filter((t) => t.toolType === "direct");
|
|
8223
|
-
logger.info(`[MainAgent] ${sourceTools.length} source tool(s), ${directTools.length} direct tool(s)`);
|
|
8508
|
+
logger.info(`[MainAgent] ${sourceTools.length} source tool(s), ${directTools.length} direct tool(s), ${this.workflows.length} workflow(s)`);
|
|
8224
8509
|
const summaries = buildSourceSummaries(sourceTools);
|
|
8225
|
-
const systemPrompt = await this.buildSystemPrompt(summaries, directTools, conversationHistory);
|
|
8510
|
+
const systemPrompt = await this.buildSystemPrompt(summaries, directTools, this.workflows, conversationHistory);
|
|
8226
8511
|
logger.logLLMPrompt("mainAgent", "system", extractPromptText(systemPrompt));
|
|
8227
8512
|
logger.logLLMPrompt("mainAgent", "user", userPrompt);
|
|
8228
8513
|
const sourceToolDefs = this.buildSourceToolDefinitions(summaries);
|
|
8229
8514
|
const directToolDefs = this.buildDirectToolDefinitions(directTools);
|
|
8515
|
+
const workflowToolDefs = this.buildWorkflowToolDefinitions(this.workflows);
|
|
8230
8516
|
const tools = [
|
|
8231
8517
|
...sourceToolDefs,
|
|
8232
8518
|
...directToolDefs,
|
|
8519
|
+
...workflowToolDefs,
|
|
8233
8520
|
...this.scriptingEnabled ? [WRITE_SCRIPT_TOOL_DEF, EXECUTE_SCRIPT_TOOL_DEF] : []
|
|
8234
8521
|
];
|
|
8235
8522
|
const sourceResults = [];
|
|
8236
8523
|
const executedTools = [];
|
|
8237
8524
|
let sourceCallCounter = 0;
|
|
8525
|
+
let selectedWorkflow;
|
|
8238
8526
|
const toolHandler = async (toolName, toolInput) => {
|
|
8527
|
+
const workflow = this.workflows.find((w) => w.id === toolName);
|
|
8528
|
+
if (workflow) {
|
|
8529
|
+
return this.handleWorkflow(workflow, toolInput, (w) => {
|
|
8530
|
+
selectedWorkflow = w;
|
|
8531
|
+
});
|
|
8532
|
+
}
|
|
8239
8533
|
if (toolName === "write_script") {
|
|
8240
8534
|
if (!this.scriptingEnabled) return "Scripting is not enabled for this request.";
|
|
8241
8535
|
return this.handleWriteScript(toolInput);
|
|
@@ -8300,24 +8594,34 @@ var MainAgent = class {
|
|
|
8300
8594
|
this.config.maxIterations
|
|
8301
8595
|
);
|
|
8302
8596
|
const totalTime = Date.now() - startTime;
|
|
8303
|
-
logger.info(
|
|
8304
|
-
|
|
8597
|
+
logger.info(
|
|
8598
|
+
`[MainAgent] Complete | ${sourceResults.length} source queries, ${executedTools.length} successful${selectedWorkflow ? ` | workflow="${selectedWorkflow.name}"` : ""} | ${totalTime}ms`
|
|
8599
|
+
);
|
|
8600
|
+
const savedScript = await this.buildSavedScript();
|
|
8305
8601
|
if (savedScript) {
|
|
8306
8602
|
logger.info(`[MainAgent] Script authored: "${savedScript.name}" (${savedScript.parameters.length} params, ${this.scriptState.attempts} attempt${this.scriptState.attempts === 1 ? "" : "s"})`);
|
|
8307
8603
|
} else if (sourceResults.some((r) => r.success)) {
|
|
8308
8604
|
logger.warn(`[MainAgent] Source query succeeded but no script was authored \u2014 LLM skipped write_script/execute_script. Prompt policy may need tightening.`);
|
|
8309
8605
|
}
|
|
8606
|
+
if (!savedScript && this.scriptStore && this.scriptState.recipeId) {
|
|
8607
|
+
try {
|
|
8608
|
+
await this.scriptStore.discardDraft(this.scriptState.recipeId);
|
|
8609
|
+
} catch (err) {
|
|
8610
|
+
logger.warn(`[MainAgent] Failed to discard failed draft ${this.scriptState.recipeId}: ${err}`);
|
|
8611
|
+
}
|
|
8612
|
+
}
|
|
8310
8613
|
return {
|
|
8311
8614
|
text,
|
|
8312
8615
|
executedTools,
|
|
8313
8616
|
sourceResults,
|
|
8314
|
-
savedScript
|
|
8617
|
+
savedScript,
|
|
8618
|
+
workflow: selectedWorkflow
|
|
8315
8619
|
};
|
|
8316
8620
|
}
|
|
8317
8621
|
// ============================================
|
|
8318
8622
|
// Script-authoring tool handlers
|
|
8319
8623
|
// ============================================
|
|
8320
|
-
handleWriteScript(toolInput) {
|
|
8624
|
+
async handleWriteScript(toolInput) {
|
|
8321
8625
|
const scriptBody = typeof toolInput?.scriptBody === "string" ? toolInput.scriptBody.trim() : "";
|
|
8322
8626
|
if (!scriptBody) {
|
|
8323
8627
|
return "write_script requires a non-empty `scriptBody` starting with `export async function getData(ctx, params)`.";
|
|
@@ -8326,7 +8630,7 @@ var MainAgent = class {
|
|
|
8326
8630
|
const intentDescription = toolInput.description || "";
|
|
8327
8631
|
const tags = Array.isArray(toolInput.tags) ? toolInput.tags : [];
|
|
8328
8632
|
const parameters = Array.isArray(toolInput.parameters) ? this.normalizeParameterList(toolInput.parameters) : [];
|
|
8329
|
-
const draft = this.scriptStore.saveDraft({
|
|
8633
|
+
const draft = await this.scriptStore.saveDraft({
|
|
8330
8634
|
recipeId: this.scriptState.recipeId ?? void 0,
|
|
8331
8635
|
turnId: this.turnId,
|
|
8332
8636
|
name,
|
|
@@ -8347,7 +8651,7 @@ var MainAgent = class {
|
|
|
8347
8651
|
if (!this.scriptState.recipeId) {
|
|
8348
8652
|
return "No draft found. Call write_script first with the script you want to verify.";
|
|
8349
8653
|
}
|
|
8350
|
-
const draftRecipe = this.scriptStore.get(this.scriptState.recipeId);
|
|
8654
|
+
const draftRecipe = await this.scriptStore.get(this.scriptState.recipeId);
|
|
8351
8655
|
if (!draftRecipe) {
|
|
8352
8656
|
logger.error(`[MainAgent] execute_script: draft "${this.scriptState.recipeId}" missing from store`);
|
|
8353
8657
|
return "Draft was lost from the store. Call write_script again.";
|
|
@@ -8365,10 +8669,17 @@ var MainAgent = class {
|
|
|
8365
8669
|
);
|
|
8366
8670
|
if (result.success) {
|
|
8367
8671
|
this.scriptState.lastSuccessfulResult = result;
|
|
8672
|
+
const totalRows = result.data.length;
|
|
8368
8673
|
const summary2 = {
|
|
8369
8674
|
ok: true,
|
|
8370
8675
|
executionTimeMs: result.executionTimeMs,
|
|
8676
|
+
totalRows,
|
|
8677
|
+
// dataSummary is computed over ALL rows — use it for any range/count/
|
|
8678
|
+
// min/max/total/"all-or-none" claim. sampleRows is just the first few
|
|
8679
|
+
// rows (head of result, NOT representative) for illustrating shape.
|
|
8680
|
+
dataSummary: summarizeRows(result.data),
|
|
8371
8681
|
sampleRows: result.data.slice(0, 5),
|
|
8682
|
+
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.`,
|
|
8372
8683
|
executedQueries: result.executedQueries.map((q) => ({
|
|
8373
8684
|
sourceId: q.sourceId,
|
|
8374
8685
|
sourceName: q.sourceName,
|
|
@@ -8376,10 +8687,10 @@ var MainAgent = class {
|
|
|
8376
8687
|
sample: q.data.slice(0, 2)
|
|
8377
8688
|
}))
|
|
8378
8689
|
};
|
|
8379
|
-
logger.info(`[MainAgent] execute_script: ok \u2014 ${
|
|
8690
|
+
logger.info(`[MainAgent] execute_script: ok \u2014 ${totalRows} rows in ${result.executionTimeMs}ms (attempt ${this.scriptState.attempts})`);
|
|
8380
8691
|
return JSON.stringify(summary2, null, 2);
|
|
8381
8692
|
}
|
|
8382
|
-
this.scriptStore.recordDraftError(draftRecipe.id, {
|
|
8693
|
+
await this.scriptStore.recordDraftError(draftRecipe.id, {
|
|
8383
8694
|
phase: result.errorPhase ?? "runtime",
|
|
8384
8695
|
message: result.error ?? "Unknown error",
|
|
8385
8696
|
attempt: this.scriptState.attempts
|
|
@@ -8405,11 +8716,11 @@ var MainAgent = class {
|
|
|
8405
8716
|
* `ScriptStore.promoteToVerified()`. Only returned when a verified
|
|
8406
8717
|
* successful execution is on record.
|
|
8407
8718
|
*/
|
|
8408
|
-
buildSavedScript() {
|
|
8719
|
+
async buildSavedScript() {
|
|
8409
8720
|
const { recipeId, lastSuccessfulResult } = this.scriptState;
|
|
8410
8721
|
if (!recipeId || !lastSuccessfulResult) return void 0;
|
|
8411
8722
|
if (!this.scriptStore) return void 0;
|
|
8412
|
-
const draft = this.scriptStore.get(recipeId);
|
|
8723
|
+
const draft = await this.scriptStore.get(recipeId);
|
|
8413
8724
|
if (!draft) return void 0;
|
|
8414
8725
|
const executedQueries = lastSuccessfulResult.executedQueries.map((q) => ({
|
|
8415
8726
|
sourceId: q.sourceId,
|
|
@@ -8536,6 +8847,7 @@ ${lines.join("\n")}`;
|
|
|
8536
8847
|
maxRows: 5,
|
|
8537
8848
|
maxCharsPerField: 200
|
|
8538
8849
|
});
|
|
8850
|
+
const sampleData = Array.isArray(resultData) ? resultData.slice(0, TOOL_TRACKING_SAMPLE_ROWS) : Array.isArray(formattedResult.data) ? formattedResult.data.slice(0, TOOL_TRACKING_SAMPLE_ROWS) : [];
|
|
8539
8851
|
executedTools.push({
|
|
8540
8852
|
id: tool.id,
|
|
8541
8853
|
name: tool.name,
|
|
@@ -8544,7 +8856,7 @@ ${lines.join("\n")}`;
|
|
|
8544
8856
|
_totalRecords: result.totalItems || result.count || rowCount,
|
|
8545
8857
|
_recordsShown: rowCount,
|
|
8546
8858
|
_metadata: result.metadata,
|
|
8547
|
-
_sampleData:
|
|
8859
|
+
_sampleData: sampleData
|
|
8548
8860
|
},
|
|
8549
8861
|
outputSchema: tool.outputSchema
|
|
8550
8862
|
});
|
|
@@ -8570,9 +8882,10 @@ ${formatted}`;
|
|
|
8570
8882
|
// System Prompt
|
|
8571
8883
|
// ============================================
|
|
8572
8884
|
/**
|
|
8573
|
-
* Build the main agent's system prompt with source summaries
|
|
8885
|
+
* Build the main agent's system prompt with source summaries, direct tool descriptions,
|
|
8886
|
+
* and workflow component descriptions.
|
|
8574
8887
|
*/
|
|
8575
|
-
async buildSystemPrompt(summaries, directTools, conversationHistory) {
|
|
8888
|
+
async buildSystemPrompt(summaries, directTools, workflows, conversationHistory) {
|
|
8576
8889
|
const summariesText = formatSummariesForPrompt(summaries);
|
|
8577
8890
|
const maxSourceCalls = Math.max(2, this.config.maxIterations - 2);
|
|
8578
8891
|
let directToolsText = "";
|
|
@@ -8584,10 +8897,28 @@ ${formatted}`;
|
|
|
8584
8897
|
${t.description || "No description"}${paramList ? "\n Parameters:\n" + paramList : ""}`;
|
|
8585
8898
|
}).join("\n\n");
|
|
8586
8899
|
}
|
|
8900
|
+
let workflowsText = "";
|
|
8901
|
+
if (workflows.length > 0) {
|
|
8902
|
+
workflowsText = workflows.map((w, idx) => {
|
|
8903
|
+
const propLines = Object.entries(w.propsSchema || {}).map(([k, v]) => ` - ${k}: ${v}`).join("\n");
|
|
8904
|
+
return [
|
|
8905
|
+
`${idx + 1}. **${w.name}** (tool: ${w.id})`,
|
|
8906
|
+
` ${w.description}`,
|
|
8907
|
+
` When to use: ${w.whenToUse}`,
|
|
8908
|
+
propLines ? ` Props:
|
|
8909
|
+
${propLines}` : ""
|
|
8910
|
+
].filter(Boolean).join("\n");
|
|
8911
|
+
}).join("\n\n");
|
|
8912
|
+
} else {
|
|
8913
|
+
workflowsText = "No workflow components registered for this project.";
|
|
8914
|
+
}
|
|
8587
8915
|
const prompts = await promptLoader.loadPrompts("agent-main", {
|
|
8588
8916
|
SOURCE_SUMMARIES: summariesText,
|
|
8589
8917
|
DIRECT_TOOLS: directToolsText,
|
|
8590
|
-
|
|
8918
|
+
WORKFLOW_COMPONENTS: workflowsText,
|
|
8919
|
+
MAX_ROWS: String(MAIN_AGENT_COMPLETE_ROWS),
|
|
8920
|
+
MAX_ROWS_RENDERED: String(MAX_ROWS_RENDERED),
|
|
8921
|
+
MAX_ROWS_FETCHED: String(MAX_ROWS_FETCHED),
|
|
8591
8922
|
MAX_SOURCE_CALLS: String(maxSourceCalls),
|
|
8592
8923
|
GLOBAL_KNOWLEDGE_BASE: this.config.globalKnowledgeBase || "No global knowledge base available.",
|
|
8593
8924
|
CURRENT_DATETIME: getCurrentDateTimeForPrompt(),
|
|
@@ -8673,6 +9004,70 @@ ${formatted}`;
|
|
|
8673
9004
|
});
|
|
8674
9005
|
}
|
|
8675
9006
|
// ============================================
|
|
9007
|
+
// Workflow Handling
|
|
9008
|
+
// ============================================
|
|
9009
|
+
/**
|
|
9010
|
+
* Capture a workflow selection. We do NOT execute anything — the LLM has
|
|
9011
|
+
* already extracted the props it wants the workflow rendered with. We
|
|
9012
|
+
* record the selection (via the capture callback) and return a short
|
|
9013
|
+
* acknowledgement so the LLM ends its turn cleanly without writing
|
|
9014
|
+
* analysis text or calling more tools.
|
|
9015
|
+
*/
|
|
9016
|
+
async handleWorkflow(workflow, toolInput, capture) {
|
|
9017
|
+
const props = { ...workflow.defaultProps || {}, ...toolInput || {} };
|
|
9018
|
+
logger.info(
|
|
9019
|
+
`[MainAgent] Workflow selected: "${workflow.name}" | props: ${JSON.stringify(props).substring(0, 200)}`
|
|
9020
|
+
);
|
|
9021
|
+
if (this.streamBuffer.hasCallback()) {
|
|
9022
|
+
this.streamBuffer.write(`
|
|
9023
|
+
|
|
9024
|
+
\u{1F9ED} **Launching ${workflow.name} workflow...**
|
|
9025
|
+
|
|
9026
|
+
`);
|
|
9027
|
+
await streamDelay();
|
|
9028
|
+
}
|
|
9029
|
+
capture({ name: workflow.name, props });
|
|
9030
|
+
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.`;
|
|
9031
|
+
}
|
|
9032
|
+
/**
|
|
9033
|
+
* Build LLM tool definitions for workflow components. The workflow's
|
|
9034
|
+
* propsSchema becomes the tool's input_schema so the LLM extracts props
|
|
9035
|
+
* directly from the prompt — same mechanic as direct tools.
|
|
9036
|
+
*/
|
|
9037
|
+
buildWorkflowToolDefinitions(workflows) {
|
|
9038
|
+
return workflows.map((workflow) => {
|
|
9039
|
+
const properties = {};
|
|
9040
|
+
const required = [];
|
|
9041
|
+
Object.entries(workflow.propsSchema || {}).forEach(([key, typeOrValue]) => {
|
|
9042
|
+
const valueStr = String(typeOrValue).toLowerCase();
|
|
9043
|
+
let schemaType = "string";
|
|
9044
|
+
const typeMatch = valueStr.match(/^(string|number|integer|boolean|array|object)\b/);
|
|
9045
|
+
if (typeMatch) {
|
|
9046
|
+
schemaType = typeMatch[1];
|
|
9047
|
+
}
|
|
9048
|
+
const isOptional = valueStr.includes("(optional)") || valueStr.includes("optional");
|
|
9049
|
+
const description = typeof typeOrValue === "string" ? typeOrValue : `Prop: ${key}`;
|
|
9050
|
+
if (schemaType === "array") {
|
|
9051
|
+
properties[key] = { type: "array", items: {}, description };
|
|
9052
|
+
} else if (schemaType === "object") {
|
|
9053
|
+
properties[key] = { type: "object", description };
|
|
9054
|
+
} else {
|
|
9055
|
+
properties[key] = { type: schemaType, description };
|
|
9056
|
+
}
|
|
9057
|
+
if (!isOptional) required.push(key);
|
|
9058
|
+
});
|
|
9059
|
+
return {
|
|
9060
|
+
name: workflow.id,
|
|
9061
|
+
description: `[WORKFLOW] ${workflow.description} \u2014 When to use: ${workflow.whenToUse}`,
|
|
9062
|
+
input_schema: {
|
|
9063
|
+
type: "object",
|
|
9064
|
+
properties,
|
|
9065
|
+
required: required.length > 0 ? required : void 0
|
|
9066
|
+
}
|
|
9067
|
+
};
|
|
9068
|
+
});
|
|
9069
|
+
}
|
|
9070
|
+
// ============================================
|
|
8676
9071
|
// Format Result for Main Agent
|
|
8677
9072
|
// ============================================
|
|
8678
9073
|
/**
|
|
@@ -8682,7 +9077,7 @@ ${formatted}`;
|
|
|
8682
9077
|
if (!result.success) {
|
|
8683
9078
|
return `Data source "${result.sourceName}" could not fulfill the request: ${result.error}. Try rephrasing your intent or querying a different source.`;
|
|
8684
9079
|
}
|
|
8685
|
-
const {
|
|
9080
|
+
const { metadata } = result;
|
|
8686
9081
|
let output = `## Data from "${result.sourceName}"
|
|
8687
9082
|
`;
|
|
8688
9083
|
output += `Rows returned: ${metadata.rowsReturned}`;
|
|
@@ -8693,33 +9088,68 @@ ${formatted}`;
|
|
|
8693
9088
|
Execution time: ${metadata.executionTimeMs}ms
|
|
8694
9089
|
|
|
8695
9090
|
`;
|
|
8696
|
-
|
|
9091
|
+
const successfulTools = result.allExecutedTools && result.allExecutedTools.length > 0 ? result.allExecutedTools : result.executedTool ? [result.executedTool] : [];
|
|
9092
|
+
const queries = successfulTools.map((t) => ({
|
|
9093
|
+
sql: t?.params?.sql || t?.params?.query,
|
|
9094
|
+
rows: t?.result?._mainAgentRows ?? t?.result?._sampleData ?? [],
|
|
9095
|
+
summary: t?.result?._summary,
|
|
9096
|
+
total: t?.result?._totalRecords,
|
|
9097
|
+
shown: t?.result?._recordsShown
|
|
9098
|
+
})).filter((q) => Boolean(q.sql));
|
|
9099
|
+
if (queries.length === 0) {
|
|
8697
9100
|
output += "No data returned.";
|
|
8698
9101
|
return output;
|
|
8699
9102
|
}
|
|
8700
|
-
const
|
|
8701
|
-
|
|
8702
|
-
const truncatedRow = {};
|
|
9103
|
+
const truncRows = (rowsArr) => rowsArr.map((row) => {
|
|
9104
|
+
const out = {};
|
|
8703
9105
|
for (const [key, value] of Object.entries(row)) {
|
|
8704
|
-
|
|
8705
|
-
truncatedRow[key] = value.substring(0, 200) + "...";
|
|
8706
|
-
} else {
|
|
8707
|
-
truncatedRow[key] = value;
|
|
8708
|
-
}
|
|
9106
|
+
out[key] = typeof value === "string" && value.length > 200 ? safeTruncate(value, 200) + "..." : value;
|
|
8709
9107
|
}
|
|
8710
|
-
return
|
|
9108
|
+
return out;
|
|
8711
9109
|
});
|
|
8712
|
-
const
|
|
8713
|
-
|
|
8714
|
-
|
|
9110
|
+
const jsonBlock = (obj) => {
|
|
9111
|
+
try {
|
|
9112
|
+
return "```json\n" + JSON.stringify(obj, null, 2) + "\n```\n";
|
|
9113
|
+
} catch {
|
|
9114
|
+
return "```\n[Data could not be serialized]\n```\n";
|
|
9115
|
+
}
|
|
9116
|
+
};
|
|
9117
|
+
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.`;
|
|
9118
|
+
output += queries.length === 1 ? `### The SQL below and its result \u2014 copy the SQL VERBATIM into \`write_script\` (inside \`ctx.query(...)\`)
|
|
9119
|
+
${scopeWarning}
|
|
9120
|
+
|
|
9121
|
+
` : `### ${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.
|
|
9122
|
+
${scopeWarning} The script re-runs them, so you can include more than one \`ctx.query(...)\`.
|
|
9123
|
+
|
|
8715
9124
|
`;
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
9125
|
+
queries.forEach((q, i) => {
|
|
9126
|
+
const total = q.total ?? q.rows.length;
|
|
9127
|
+
const capped = (q.shown ?? q.rows.length) >= MAX_ROWS_FETCHED;
|
|
9128
|
+
output += queries.length === 1 ? `**SQL:**
|
|
9129
|
+
` : `### Query ${i + 1} \u2014 ${capped ? `${MAX_ROWS_FETCHED}+ (TRUNCATED)` : `${total}`} rows
|
|
9130
|
+
**SQL for Query ${i + 1}:**
|
|
9131
|
+
`;
|
|
9132
|
+
output += "```sql\n" + String(q.sql) + "\n```\n";
|
|
9133
|
+
if (capped) {
|
|
9134
|
+
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.
|
|
9135
|
+
`;
|
|
9136
|
+
}
|
|
9137
|
+
if (total <= MAIN_AGENT_COMPLETE_ROWS) {
|
|
9138
|
+
const rows = truncRows(q.rows);
|
|
9139
|
+
output += `**All ${rows.length} rows (COMPLETE result \u2014 this is the entire result set, nothing hidden):**
|
|
9140
|
+
`;
|
|
9141
|
+
output += jsonBlock(rows);
|
|
9142
|
+
} else {
|
|
9143
|
+
const samples = truncRows(q.rows.slice(0, 3));
|
|
9144
|
+
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:**
|
|
9145
|
+
`;
|
|
9146
|
+
output += jsonBlock(q.summary ?? { totalRows: total });
|
|
9147
|
+
output += `**Sample rows (first ${samples.length} of ${total} \u2014 to show row SHAPE only, NOT representative of the distribution):**
|
|
9148
|
+
`;
|
|
9149
|
+
output += jsonBlock(samples);
|
|
9150
|
+
}
|
|
9151
|
+
output += "\n";
|
|
9152
|
+
});
|
|
8723
9153
|
return output;
|
|
8724
9154
|
}
|
|
8725
9155
|
/**
|
|
@@ -8745,14 +9175,15 @@ function extractTablesFromSQL(sqls) {
|
|
|
8745
9175
|
// src/userResponse/agents/types.ts
|
|
8746
9176
|
var DEFAULT_AGENT_CONFIG = {
|
|
8747
9177
|
maxRowsPerSource: 10,
|
|
9178
|
+
maxRowsFetched: MAX_ROWS_FETCHED,
|
|
8748
9179
|
mainAgentModel: "",
|
|
8749
9180
|
// will use the provider's default model
|
|
8750
9181
|
sourceAgentModel: "",
|
|
8751
9182
|
// will use the provider's default model
|
|
8752
9183
|
maxRetries: 2,
|
|
8753
9184
|
// 2 retries = 3 total query attempts (1 initial + 2 retries for SQL errors)
|
|
8754
|
-
maxIterations:
|
|
8755
|
-
// schema search (2-3) + query attempts (2) + LLM responses + final
|
|
9185
|
+
maxIterations: 12
|
|
9186
|
+
// schema search (2-3) + query attempts (2) + write_script/execute_script (2-3) + LLM responses + final
|
|
8756
9187
|
};
|
|
8757
9188
|
|
|
8758
9189
|
// src/userResponse/utils/component-props-processor.ts
|
|
@@ -9026,6 +9457,7 @@ function formatExecutedTools(executedTools) {
|
|
|
9026
9457
|
Fields:
|
|
9027
9458
|
${fieldsText}`;
|
|
9028
9459
|
}
|
|
9460
|
+
const MAX_SAMPLE_BLOCK_CHARS = 8e3;
|
|
9029
9461
|
let sampleDataText = "";
|
|
9030
9462
|
const sampleData = tool.result?._sampleData;
|
|
9031
9463
|
if (Array.isArray(sampleData) && sampleData.length > 0) {
|
|
@@ -9033,8 +9465,11 @@ ${fieldsText}`;
|
|
|
9033
9465
|
sampleDataText = `
|
|
9034
9466
|
\u{1F511} RESULT FIELDS: ${sampleFields.join(", ")}`;
|
|
9035
9467
|
try {
|
|
9468
|
+
const stringified = JSON.stringify(sampleData, null, 2);
|
|
9469
|
+
const capped = stringified.length > MAX_SAMPLE_BLOCK_CHARS ? `${stringified.substring(0, MAX_SAMPLE_BLOCK_CHARS)}
|
|
9470
|
+
... (truncated; ${stringified.length - MAX_SAMPLE_BLOCK_CHARS} more chars)` : stringified;
|
|
9036
9471
|
sampleDataText += `
|
|
9037
|
-
\u{1F4C4} SAMPLE
|
|
9472
|
+
\u{1F4C4} SAMPLE ROWS (${sampleData.length}): ${capped}`;
|
|
9038
9473
|
} catch {
|
|
9039
9474
|
}
|
|
9040
9475
|
}
|
|
@@ -9322,11 +9757,20 @@ Fixed SQL query:`;
|
|
|
9322
9757
|
return validatedQuery;
|
|
9323
9758
|
}
|
|
9324
9759
|
|
|
9760
|
+
// src/userResponse/scripts/script-metadata-store.ts
|
|
9761
|
+
function resolveScriptRecipeStore(collections) {
|
|
9762
|
+
const s = collections?.["script-recipes"];
|
|
9763
|
+
if (s && typeof s.search === "function" && typeof s.getById === "function") {
|
|
9764
|
+
return s;
|
|
9765
|
+
}
|
|
9766
|
+
return null;
|
|
9767
|
+
}
|
|
9768
|
+
|
|
9325
9769
|
// src/userResponse/scripts/script-store.ts
|
|
9326
9770
|
import * as fs6 from "fs";
|
|
9327
9771
|
import * as path6 from "path";
|
|
9772
|
+
import { createHash, randomBytes } from "crypto";
|
|
9328
9773
|
var DEFAULT_STORE_DIR = "scripts-store";
|
|
9329
|
-
var METADATA_SUBDIR = "metadata";
|
|
9330
9774
|
function normalizeScriptBody(scriptBody) {
|
|
9331
9775
|
let i = 0;
|
|
9332
9776
|
const n = scriptBody.length;
|
|
@@ -9351,383 +9795,321 @@ function normalizeScriptBody(scriptBody) {
|
|
|
9351
9795
|
} else if (/^\s*function\s+getData\b/.test(cleanBody)) {
|
|
9352
9796
|
cleanBody = cleanBody.replace(/^\s*function\s+getData\b/, "export async function getData");
|
|
9353
9797
|
}
|
|
9798
|
+
cleanBody = cleanBody.replace(/(?<!\\)\\([dwsDWS])/g, "\\\\$1");
|
|
9354
9799
|
return cleanBody;
|
|
9355
9800
|
}
|
|
9356
9801
|
var ScriptStore = class {
|
|
9357
|
-
constructor(
|
|
9358
|
-
this.
|
|
9359
|
-
this.
|
|
9360
|
-
this.
|
|
9361
|
-
|
|
9362
|
-
|
|
9363
|
-
return path6.join(this.storeDir, METADATA_SUBDIR);
|
|
9364
|
-
}
|
|
9365
|
-
/**
|
|
9366
|
-
* Filename base for a recipe. Drafts include the per-turn suffix so two
|
|
9367
|
-
* concurrent turns can never clobber each other's draft files; verified
|
|
9368
|
-
* recipes use the bare slug.
|
|
9369
|
-
*/
|
|
9370
|
-
fileBaseName(recipe) {
|
|
9371
|
-
const slug = this.toFileName(recipe.name);
|
|
9372
|
-
if (recipe.status === "draft" && recipe.turnId) {
|
|
9373
|
-
return `${slug}-${recipe.turnId}`;
|
|
9802
|
+
constructor(opts) {
|
|
9803
|
+
this.store = opts?.store ?? resolveScriptRecipeStore(opts?.collections) ?? null;
|
|
9804
|
+
this.storeDir = opts?.baseDir || path6.join(process.cwd(), DEFAULT_STORE_DIR);
|
|
9805
|
+
this.projectId = opts?.projectId;
|
|
9806
|
+
if (!this.store) {
|
|
9807
|
+
logger.warn("[ScriptStore] No script-recipes metadata store injected \u2014 script reuse disabled this run.");
|
|
9374
9808
|
}
|
|
9375
|
-
return slug;
|
|
9376
9809
|
}
|
|
9377
|
-
/**
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
*/
|
|
9381
|
-
getScriptPath(recipe) {
|
|
9382
|
-
return path6.join(this.storeDir, `${this.fileBaseName(recipe)}.ts`);
|
|
9383
|
-
}
|
|
9384
|
-
/**
|
|
9385
|
-
* Absolute path to the metadata JSON for a recipe.
|
|
9386
|
-
*/
|
|
9387
|
-
getMetadataPath(recipe) {
|
|
9388
|
-
return path6.join(this.metadataDir(), `${this.fileBaseName(recipe)}.json`);
|
|
9810
|
+
/** Whether a metadata store is wired (matcher / authoring are gated on this). */
|
|
9811
|
+
hasStore() {
|
|
9812
|
+
return this.store !== null;
|
|
9389
9813
|
}
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
|
|
9393
|
-
|
|
9394
|
-
|
|
9395
|
-
this.
|
|
9396
|
-
|
|
9814
|
+
// ============================================
|
|
9815
|
+
// Read
|
|
9816
|
+
// ============================================
|
|
9817
|
+
/** Number of healthy verified recipes (gates the script-matching path). */
|
|
9818
|
+
async count() {
|
|
9819
|
+
if (!this.store) return 0;
|
|
9820
|
+
try {
|
|
9821
|
+
return await this.store.count({ projectId: this.projectId });
|
|
9822
|
+
} catch (err) {
|
|
9823
|
+
logger.warn(`[ScriptStore] count failed: ${err}`);
|
|
9824
|
+
return 0;
|
|
9825
|
+
}
|
|
9397
9826
|
}
|
|
9398
9827
|
/**
|
|
9399
|
-
*
|
|
9400
|
-
*
|
|
9828
|
+
* FTS shortlist for the matcher (metadata only — bodies are loaded lazily by
|
|
9829
|
+
* `get()` once the LLM picks one). Returns verified, healthy recipes ranked
|
|
9830
|
+
* by relevance.
|
|
9401
9831
|
*/
|
|
9402
|
-
|
|
9403
|
-
this.
|
|
9404
|
-
|
|
9832
|
+
async search(prompt, limit) {
|
|
9833
|
+
if (!this.store) return [];
|
|
9834
|
+
try {
|
|
9835
|
+
const rows = await this.store.search({ prompt, projectId: this.projectId, limit });
|
|
9836
|
+
return rows.map((r) => this.rowToRecipe(r, ""));
|
|
9837
|
+
} catch (err) {
|
|
9838
|
+
logger.warn(`[ScriptStore] search failed: ${err}`);
|
|
9839
|
+
return [];
|
|
9840
|
+
}
|
|
9405
9841
|
}
|
|
9406
|
-
/**
|
|
9407
|
-
|
|
9408
|
-
|
|
9409
|
-
|
|
9410
|
-
|
|
9411
|
-
|
|
9842
|
+
/** Fetch one recipe by id with its body loaded from disk. */
|
|
9843
|
+
async get(id) {
|
|
9844
|
+
if (!this.store) return null;
|
|
9845
|
+
try {
|
|
9846
|
+
const row = await this.store.getById(id);
|
|
9847
|
+
if (!row) return null;
|
|
9848
|
+
const body = this.readBody(row.fileBase);
|
|
9849
|
+
return this.rowToRecipe(row, body);
|
|
9850
|
+
} catch (err) {
|
|
9851
|
+
logger.warn(`[ScriptStore] get(${id}) failed: ${err}`);
|
|
9852
|
+
return null;
|
|
9853
|
+
}
|
|
9412
9854
|
}
|
|
9413
|
-
|
|
9414
|
-
|
|
9415
|
-
|
|
9416
|
-
|
|
9417
|
-
save(recipe) {
|
|
9418
|
-
this.
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
|
|
9422
|
-
|
|
9855
|
+
// ============================================
|
|
9856
|
+
// Write
|
|
9857
|
+
// ============================================
|
|
9858
|
+
/** Create or update a recipe (metadata upsert + body write when changed). */
|
|
9859
|
+
async save(recipe) {
|
|
9860
|
+
if (!this.store) return;
|
|
9861
|
+
try {
|
|
9862
|
+
if (!recipe.fileBase) {
|
|
9863
|
+
recipe.fileBase = await this.computeFileBase(recipe.name, recipe.id);
|
|
9864
|
+
}
|
|
9865
|
+
if (recipe.scriptBody) {
|
|
9866
|
+
const normalized = normalizeScriptBody(recipe.scriptBody);
|
|
9867
|
+
const hash = this.hash(normalized);
|
|
9868
|
+
if (hash !== recipe.bodyHash) {
|
|
9869
|
+
this.writeBody(recipe.fileBase, normalized);
|
|
9870
|
+
recipe.bodyHash = hash;
|
|
9871
|
+
}
|
|
9872
|
+
}
|
|
9873
|
+
recipe.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9874
|
+
await this.store.upsert(this.recipeToRow(recipe));
|
|
9875
|
+
logger.info(`[ScriptStore] Saved "${recipe.name}" (${recipe.id})`);
|
|
9876
|
+
} catch (err) {
|
|
9877
|
+
logger.warn(`[ScriptStore] save("${recipe.name}") failed: ${err}`);
|
|
9878
|
+
}
|
|
9423
9879
|
}
|
|
9424
9880
|
/**
|
|
9425
|
-
* Persist (or update) a draft
|
|
9426
|
-
* the
|
|
9427
|
-
*
|
|
9428
|
-
* overwrite the same files (the LLM rewriting itself); a fresh `recipeId`
|
|
9429
|
-
* is minted only on the first call of the turn.
|
|
9430
|
-
*
|
|
9431
|
-
* Filename includes the `turnId` suffix so concurrent turns never collide.
|
|
9881
|
+
* Persist (or update) a draft. Within a turn, retries that pass the same
|
|
9882
|
+
* `recipeId` overwrite the same row + file; a fresh `recipeId` mints a new
|
|
9883
|
+
* draft. The body is visible at scripts-store/<fileBase>.ts immediately.
|
|
9432
9884
|
*/
|
|
9433
|
-
saveDraft(input) {
|
|
9434
|
-
this.
|
|
9885
|
+
async saveDraft(input) {
|
|
9886
|
+
if (!this.store) {
|
|
9887
|
+
throw new Error("[ScriptStore] saveDraft called with no metadata store injected");
|
|
9888
|
+
}
|
|
9435
9889
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9436
|
-
const existing = input.recipeId ? this.
|
|
9437
|
-
const
|
|
9438
|
-
const
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
} : {
|
|
9447
|
-
id: `script_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
9448
|
-
version: 1,
|
|
9890
|
+
const existing = input.recipeId ? await this.store.getById(input.recipeId) : null;
|
|
9891
|
+
const reuse = existing && existing.status === "draft" && existing.turnId === input.turnId;
|
|
9892
|
+
const id = reuse ? existing.id : `script_${Date.now()}_${randomBytes(3).toString("hex")}`;
|
|
9893
|
+
const fileBase = reuse ? existing.fileBase : await this.computeFileBase(input.name, id);
|
|
9894
|
+
const normalized = normalizeScriptBody(input.scriptBody);
|
|
9895
|
+
const bodyHash = this.hash(normalized);
|
|
9896
|
+
this.writeBody(fileBase, normalized);
|
|
9897
|
+
const recipe = {
|
|
9898
|
+
id,
|
|
9899
|
+
version: existing?.version ?? 1,
|
|
9449
9900
|
name: input.name,
|
|
9450
9901
|
intentDescription: input.intentDescription,
|
|
9451
9902
|
tags: input.tags,
|
|
9452
|
-
sourceIds: [],
|
|
9453
|
-
tables: [],
|
|
9903
|
+
sourceIds: existing?.sourceIds ?? [],
|
|
9904
|
+
tables: existing?.tables ?? [],
|
|
9454
9905
|
parameters: input.parameters,
|
|
9455
|
-
scriptBody:
|
|
9456
|
-
|
|
9457
|
-
|
|
9906
|
+
scriptBody: normalized,
|
|
9907
|
+
fileBase,
|
|
9908
|
+
bodyHash,
|
|
9909
|
+
projectId: this.projectId,
|
|
9910
|
+
successCount: existing?.successCount ?? 0,
|
|
9911
|
+
failureCount: existing?.failureCount ?? 0,
|
|
9458
9912
|
lastUsed: now,
|
|
9459
9913
|
createdFrom: input.createdFrom,
|
|
9460
|
-
createdAt: now,
|
|
9914
|
+
createdAt: existing?.createdAt ?? now,
|
|
9461
9915
|
updatedAt: now,
|
|
9462
|
-
forkDepth: 0,
|
|
9916
|
+
forkDepth: existing?.forkDepth ?? 0,
|
|
9463
9917
|
status: "draft",
|
|
9464
9918
|
turnId: input.turnId
|
|
9465
9919
|
};
|
|
9466
|
-
this.
|
|
9467
|
-
this.
|
|
9468
|
-
logger.info(
|
|
9469
|
-
`[ScriptStore] ${reuseExisting ? "Updated" : "Saved"} draft "${recipe.name}" (${recipe.id}) at ${this.getScriptPath(recipe)}`
|
|
9470
|
-
);
|
|
9920
|
+
await this.store.upsert(this.recipeToRow(recipe));
|
|
9921
|
+
logger.info(`[ScriptStore] ${reuse ? "Updated" : "Saved"} draft "${recipe.name}" (${id}) at ${this.getScriptPath(recipe)}`);
|
|
9471
9922
|
return recipe;
|
|
9472
9923
|
}
|
|
9473
|
-
/**
|
|
9474
|
-
|
|
9475
|
-
|
|
9476
|
-
|
|
9477
|
-
|
|
9478
|
-
|
|
9479
|
-
|
|
9480
|
-
|
|
9481
|
-
if (!recipe || (recipe.status ?? "verified") !== "draft") return;
|
|
9482
|
-
recipe.lastError = {
|
|
9483
|
-
phase: err.phase,
|
|
9484
|
-
message: err.message,
|
|
9485
|
-
attempt: err.attempt,
|
|
9486
|
-
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
9487
|
-
};
|
|
9488
|
-
recipe.failureCount++;
|
|
9489
|
-
recipe.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9490
|
-
this.saveRecipeToDisk(recipe);
|
|
9924
|
+
/** Stamp a draft's last execution error (metadata only). */
|
|
9925
|
+
async recordDraftError(recipeId, err) {
|
|
9926
|
+
if (!this.store) return;
|
|
9927
|
+
try {
|
|
9928
|
+
await this.store.recordDraftError(recipeId, { ...err, at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
9929
|
+
} catch (e) {
|
|
9930
|
+
logger.warn(`[ScriptStore] recordDraftError failed: ${e}`);
|
|
9931
|
+
}
|
|
9491
9932
|
}
|
|
9492
9933
|
/**
|
|
9493
9934
|
* Promote a successfully-executed draft into a verified script.
|
|
9494
|
-
*
|
|
9495
|
-
*
|
|
9496
|
-
|
|
9497
|
-
|
|
9498
|
-
|
|
9499
|
-
|
|
9500
|
-
|
|
9501
|
-
|
|
9502
|
-
|
|
9503
|
-
|
|
9504
|
-
|
|
9505
|
-
|
|
9506
|
-
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
9510
|
-
|
|
9511
|
-
logger.info(`[ScriptStore] promoteToVerified: recipe "${recipeId}" already verified \u2014 no-op`);
|
|
9512
|
-
return recipe;
|
|
9513
|
-
}
|
|
9514
|
-
const slug = this.toFileName(recipe.name);
|
|
9515
|
-
const canonicalTs = path6.join(this.storeDir, `${slug}.ts`);
|
|
9516
|
-
const canonicalMeta = path6.join(this.metadataDir(), `${slug}.json`);
|
|
9517
|
-
const canonicalFree = !fs6.existsSync(canonicalTs) && !fs6.existsSync(canonicalMeta);
|
|
9518
|
-
const oldTsPath = this.getScriptPath(recipe);
|
|
9519
|
-
const oldMetaPath = this.getMetadataPath(recipe);
|
|
9520
|
-
recipe.status = "verified";
|
|
9521
|
-
recipe.successCount = Math.max(1, recipe.successCount);
|
|
9522
|
-
recipe.failureCount = 0;
|
|
9523
|
-
recipe.lastError = void 0;
|
|
9524
|
-
recipe.lastUsed = (/* @__PURE__ */ new Date()).toISOString();
|
|
9525
|
-
recipe.updatedAt = recipe.lastUsed;
|
|
9526
|
-
recipe.sourceIds = input.sourceIds;
|
|
9527
|
-
recipe.tables = input.tables;
|
|
9528
|
-
if (input.parentId !== void 0) recipe.parentId = input.parentId;
|
|
9529
|
-
if (input.forkDepth !== void 0) recipe.forkDepth = input.forkDepth;
|
|
9530
|
-
if (input.forkReason !== void 0) recipe.forkReason = input.forkReason;
|
|
9531
|
-
if (canonicalFree) {
|
|
9532
|
-
recipe.turnId = void 0;
|
|
9533
|
-
try {
|
|
9534
|
-
if (fs6.existsSync(oldTsPath)) fs6.unlinkSync(oldTsPath);
|
|
9535
|
-
if (fs6.existsSync(oldMetaPath)) fs6.unlinkSync(oldMetaPath);
|
|
9536
|
-
} catch (err) {
|
|
9537
|
-
logger.warn(`[ScriptStore] promoteToVerified cleanup warning: ${err}`);
|
|
9935
|
+
* The on-disk body already exists at <fileBase>.ts (written at write_script
|
|
9936
|
+
* time) and keeps its name — only the DB row flips status + provenance.
|
|
9937
|
+
*/
|
|
9938
|
+
async promoteToVerified(recipeId, input) {
|
|
9939
|
+
if (!this.store) return null;
|
|
9940
|
+
try {
|
|
9941
|
+
const row = await this.store.promote(recipeId, {
|
|
9942
|
+
sourceIds: input.sourceIds,
|
|
9943
|
+
tables: input.tables,
|
|
9944
|
+
parentId: input.parentId,
|
|
9945
|
+
forkDepth: input.forkDepth,
|
|
9946
|
+
forkReason: input.forkReason,
|
|
9947
|
+
components: input.components
|
|
9948
|
+
});
|
|
9949
|
+
if (!row) {
|
|
9950
|
+
logger.warn(`[ScriptStore] promoteToVerified: recipe "${recipeId}" not found`);
|
|
9951
|
+
return null;
|
|
9538
9952
|
}
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
|
|
9542
|
-
);
|
|
9543
|
-
|
|
9544
|
-
this.saveRecipeToDisk(recipe);
|
|
9545
|
-
logger.info(
|
|
9546
|
-
`[ScriptStore] Promoted draft "${recipe.name}" (${recipe.id}) \u2014 canonical slug "${slug}" was taken, kept turn-suffixed filename ${this.getScriptPath(recipe)}`
|
|
9547
|
-
);
|
|
9953
|
+
logger.info(`[ScriptStore] Promoted "${row.name}" (${recipeId}) \u2192 verified`);
|
|
9954
|
+
return this.rowToRecipe(row, this.readBody(row.fileBase));
|
|
9955
|
+
} catch (err) {
|
|
9956
|
+
logger.warn(`[ScriptStore] promoteToVerified failed: ${err}`);
|
|
9957
|
+
return null;
|
|
9548
9958
|
}
|
|
9549
|
-
return recipe;
|
|
9550
9959
|
}
|
|
9551
9960
|
/**
|
|
9552
|
-
* Drop a draft
|
|
9553
|
-
*
|
|
9554
|
-
*
|
|
9961
|
+
* Drop a draft (row + body file). MainAgent calls this at end-of-turn when a
|
|
9962
|
+
* draft was authored but never verified — failed drafts are never matched, so
|
|
9963
|
+
* deleting them immediately avoids unbounded accumulation (#5). No-op if the
|
|
9964
|
+
* recipe isn't a draft (so a promoted/verified script is never removed here).
|
|
9555
9965
|
*/
|
|
9556
|
-
discardDraft(recipeId) {
|
|
9557
|
-
this.
|
|
9558
|
-
const recipe = this.recipes.get(recipeId);
|
|
9559
|
-
if (!recipe || (recipe.status ?? "verified") !== "draft") return;
|
|
9560
|
-
this.recipes.delete(recipeId);
|
|
9561
|
-
this.deleteRecipeFromDisk(recipe);
|
|
9562
|
-
logger.info(`[ScriptStore] Discarded draft "${recipe.name}" (${recipeId})`);
|
|
9966
|
+
async discardDraft(recipeId) {
|
|
9967
|
+
await this.removeById(recipeId, "draft");
|
|
9563
9968
|
}
|
|
9564
|
-
/**
|
|
9565
|
-
|
|
9566
|
-
|
|
9567
|
-
delete(id) {
|
|
9568
|
-
this.ensureLoaded();
|
|
9569
|
-
const recipe = this.recipes.get(id);
|
|
9570
|
-
if (recipe) {
|
|
9571
|
-
this.recipes.delete(id);
|
|
9572
|
-
this.deleteRecipeFromDisk(recipe);
|
|
9573
|
-
logger.info(`[ScriptStore] Deleted script "${recipe.name}" (${id})`);
|
|
9574
|
-
}
|
|
9969
|
+
/** Delete a recipe (row + body file). */
|
|
9970
|
+
async delete(id) {
|
|
9971
|
+
await this.removeById(id);
|
|
9575
9972
|
}
|
|
9576
|
-
/**
|
|
9577
|
-
|
|
9578
|
-
|
|
9579
|
-
|
|
9580
|
-
|
|
9581
|
-
|
|
9582
|
-
|
|
9583
|
-
recipe.successCount++;
|
|
9584
|
-
recipe.lastUsed = (/* @__PURE__ */ new Date()).toISOString();
|
|
9585
|
-
this.saveRecipeToDisk(recipe);
|
|
9973
|
+
/** Record a successful execution (atomic counter bump). */
|
|
9974
|
+
async recordSuccess(id) {
|
|
9975
|
+
if (!this.store) return;
|
|
9976
|
+
try {
|
|
9977
|
+
await this.store.updateStats(id, { successDelta: 1, lastUsed: (/* @__PURE__ */ new Date()).toISOString() });
|
|
9978
|
+
} catch (err) {
|
|
9979
|
+
logger.warn(`[ScriptStore] recordSuccess failed: ${err}`);
|
|
9586
9980
|
}
|
|
9587
9981
|
}
|
|
9588
|
-
/**
|
|
9589
|
-
|
|
9590
|
-
|
|
9591
|
-
|
|
9592
|
-
|
|
9593
|
-
|
|
9594
|
-
|
|
9595
|
-
recipe.failureCount++;
|
|
9596
|
-
recipe.lastUsed = (/* @__PURE__ */ new Date()).toISOString();
|
|
9597
|
-
this.saveRecipeToDisk(recipe);
|
|
9982
|
+
/** Record a failed execution (atomic counter bump). */
|
|
9983
|
+
async recordFailure(id) {
|
|
9984
|
+
if (!this.store) return;
|
|
9985
|
+
try {
|
|
9986
|
+
await this.store.updateStats(id, { failureDelta: 1, lastUsed: (/* @__PURE__ */ new Date()).toISOString() });
|
|
9987
|
+
} catch (err) {
|
|
9988
|
+
logger.warn(`[ScriptStore] recordFailure failed: ${err}`);
|
|
9598
9989
|
}
|
|
9599
9990
|
}
|
|
9600
|
-
|
|
9601
|
-
|
|
9602
|
-
|
|
9603
|
-
|
|
9604
|
-
|
|
9605
|
-
const
|
|
9606
|
-
|
|
9607
|
-
const total = recipe.successCount + recipe.failureCount;
|
|
9608
|
-
if (total < 5) return 0;
|
|
9609
|
-
return recipe.failureCount / total;
|
|
9991
|
+
// ============================================
|
|
9992
|
+
// Paths (sync — body lives on disk)
|
|
9993
|
+
// ============================================
|
|
9994
|
+
/** Absolute path to the .ts body for a recipe (used by the runner/MainAgent). */
|
|
9995
|
+
getScriptPath(recipe) {
|
|
9996
|
+
const base = recipe.fileBase || this.toSlug(recipe.name);
|
|
9997
|
+
return path6.join(this.storeDir, `${base}.ts`);
|
|
9610
9998
|
}
|
|
9611
9999
|
// ============================================
|
|
9612
|
-
//
|
|
10000
|
+
// Internals
|
|
9613
10001
|
// ============================================
|
|
9614
|
-
|
|
9615
|
-
if (!this.
|
|
9616
|
-
|
|
9617
|
-
|
|
10002
|
+
async removeById(id, requireStatus) {
|
|
10003
|
+
if (!this.store) return;
|
|
10004
|
+
try {
|
|
10005
|
+
const row = await this.store.getById(id);
|
|
10006
|
+
if (!row) return;
|
|
10007
|
+
if (requireStatus && row.status !== requireStatus) return;
|
|
10008
|
+
await this.store.remove(id);
|
|
10009
|
+
this.unlinkBody(row.fileBase);
|
|
10010
|
+
logger.info(`[ScriptStore] Removed "${row.name}" (${id})`);
|
|
10011
|
+
} catch (err) {
|
|
10012
|
+
logger.warn(`[ScriptStore] remove(${id}) failed: ${err}`);
|
|
9618
10013
|
}
|
|
9619
10014
|
}
|
|
9620
|
-
|
|
9621
|
-
|
|
9622
|
-
|
|
9623
|
-
|
|
9624
|
-
|
|
9625
|
-
|
|
10015
|
+
rowToRecipe(row, body) {
|
|
10016
|
+
return {
|
|
10017
|
+
id: row.id,
|
|
10018
|
+
version: row.version ?? 1,
|
|
10019
|
+
name: row.name,
|
|
10020
|
+
intentDescription: row.intentDescription,
|
|
10021
|
+
tags: row.tags ?? [],
|
|
10022
|
+
sourceIds: row.sourceIds ?? [],
|
|
10023
|
+
tables: row.tables ?? [],
|
|
10024
|
+
parameters: row.parameters ?? [],
|
|
10025
|
+
scriptBody: body,
|
|
10026
|
+
fileBase: row.fileBase,
|
|
10027
|
+
bodyHash: row.bodyHash ?? void 0,
|
|
10028
|
+
projectId: row.projectId ?? void 0,
|
|
10029
|
+
successCount: row.successCount ?? 0,
|
|
10030
|
+
failureCount: row.failureCount ?? 0,
|
|
10031
|
+
lastUsed: row.lastUsed ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
10032
|
+
createdFrom: row.createdFrom ?? "",
|
|
10033
|
+
createdAt: row.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
10034
|
+
updatedAt: row.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
10035
|
+
parentId: row.parentId ?? void 0,
|
|
10036
|
+
forkDepth: typeof row.forkDepth === "number" ? row.forkDepth : 0,
|
|
10037
|
+
forkReason: row.forkReason ?? void 0,
|
|
10038
|
+
components: row.components ?? void 0,
|
|
10039
|
+
status: row.status ?? "verified",
|
|
10040
|
+
turnId: row.turnId ?? void 0,
|
|
10041
|
+
lastError: row.lastError ?? void 0
|
|
10042
|
+
};
|
|
9626
10043
|
}
|
|
9627
|
-
|
|
9628
|
-
|
|
9629
|
-
|
|
9630
|
-
|
|
9631
|
-
|
|
9632
|
-
|
|
9633
|
-
|
|
9634
|
-
|
|
9635
|
-
|
|
9636
|
-
|
|
9637
|
-
|
|
9638
|
-
|
|
9639
|
-
|
|
9640
|
-
|
|
9641
|
-
|
|
9642
|
-
|
|
9643
|
-
|
|
9644
|
-
|
|
9645
|
-
|
|
9646
|
-
|
|
9647
|
-
|
|
9648
|
-
|
|
9649
|
-
|
|
9650
|
-
|
|
9651
|
-
|
|
9652
|
-
|
|
9653
|
-
|
|
9654
|
-
|
|
9655
|
-
|
|
9656
|
-
|
|
9657
|
-
|
|
9658
|
-
|
|
9659
|
-
}
|
|
9660
|
-
}
|
|
9661
|
-
const topLevelJson = fs6.readdirSync(this.storeDir).filter((f) => f.endsWith(".json"));
|
|
9662
|
-
for (const file of topLevelJson) {
|
|
9663
|
-
try {
|
|
9664
|
-
const filePath = path6.join(this.storeDir, file);
|
|
9665
|
-
const recipe = JSON.parse(fs6.readFileSync(filePath, "utf-8"));
|
|
9666
|
-
if (!recipe.id || !recipe.scriptBody) continue;
|
|
9667
|
-
if (this.recipes.has(recipe.id)) continue;
|
|
9668
|
-
if (typeof recipe.forkDepth !== "number") recipe.forkDepth = 0;
|
|
9669
|
-
if (!recipe.status) recipe.status = "verified";
|
|
9670
|
-
this.recipes.set(recipe.id, recipe);
|
|
9671
|
-
} catch (err) {
|
|
9672
|
-
logger.warn(`[ScriptStore] Failed to load legacy script ${file}: ${err}`);
|
|
9673
|
-
}
|
|
9674
|
-
}
|
|
9675
|
-
if (this.recipes.size > 0) {
|
|
9676
|
-
logger.info(`[ScriptStore] Loaded ${this.recipes.size} scripts from ${this.storeDir}`);
|
|
10044
|
+
recipeToRow(recipe) {
|
|
10045
|
+
return {
|
|
10046
|
+
id: recipe.id,
|
|
10047
|
+
projectId: recipe.projectId ?? this.projectId ?? null,
|
|
10048
|
+
version: recipe.version ?? 1,
|
|
10049
|
+
name: recipe.name,
|
|
10050
|
+
intentDescription: recipe.intentDescription,
|
|
10051
|
+
tags: recipe.tags ?? null,
|
|
10052
|
+
createdFrom: recipe.createdFrom ?? null,
|
|
10053
|
+
sourceIds: recipe.sourceIds ?? null,
|
|
10054
|
+
tables: recipe.tables ?? null,
|
|
10055
|
+
parameters: recipe.parameters ?? null,
|
|
10056
|
+
components: recipe.components ?? null,
|
|
10057
|
+
fileBase: recipe.fileBase || this.toSlug(recipe.name),
|
|
10058
|
+
bodyHash: recipe.bodyHash ?? null,
|
|
10059
|
+
successCount: recipe.successCount ?? 0,
|
|
10060
|
+
failureCount: recipe.failureCount ?? 0,
|
|
10061
|
+
lastUsed: recipe.lastUsed ?? null,
|
|
10062
|
+
parentId: recipe.parentId ?? null,
|
|
10063
|
+
forkDepth: typeof recipe.forkDepth === "number" ? recipe.forkDepth : 0,
|
|
10064
|
+
forkReason: recipe.forkReason ?? null,
|
|
10065
|
+
status: recipe.status ?? "verified",
|
|
10066
|
+
turnId: recipe.turnId ?? null,
|
|
10067
|
+
lastError: recipe.lastError ?? null
|
|
10068
|
+
};
|
|
10069
|
+
}
|
|
10070
|
+
/** slug of name, with a short id suffix when the bare slug is already taken. */
|
|
10071
|
+
async computeFileBase(name, id) {
|
|
10072
|
+
const slug = this.toSlug(name);
|
|
10073
|
+
if (this.store) {
|
|
10074
|
+
try {
|
|
10075
|
+
const taken = await this.store.fileBaseTaken(slug, id, this.projectId);
|
|
10076
|
+
if (taken) return `${slug}-${id.slice(-6)}`;
|
|
10077
|
+
} catch {
|
|
9677
10078
|
}
|
|
9678
|
-
} catch (err) {
|
|
9679
|
-
logger.warn(`[ScriptStore] Failed to read store directory ${this.storeDir}: ${err}`);
|
|
9680
10079
|
}
|
|
10080
|
+
return slug;
|
|
9681
10081
|
}
|
|
9682
|
-
|
|
9683
|
-
|
|
9684
|
-
|
|
9685
|
-
|
|
9686
|
-
|
|
9687
|
-
|
|
9688
|
-
|
|
9689
|
-
|
|
9690
|
-
|
|
10082
|
+
toSlug(name) {
|
|
10083
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unnamed-script";
|
|
10084
|
+
}
|
|
10085
|
+
hash(body) {
|
|
10086
|
+
return createHash("sha256").update(body).digest("hex");
|
|
10087
|
+
}
|
|
10088
|
+
bodyPath(fileBase) {
|
|
10089
|
+
return path6.join(this.storeDir, `${fileBase}.ts`);
|
|
10090
|
+
}
|
|
10091
|
+
readBody(fileBase) {
|
|
9691
10092
|
try {
|
|
9692
|
-
|
|
9693
|
-
if (!fs6.existsSync(metaDir)) {
|
|
9694
|
-
fs6.mkdirSync(metaDir, { recursive: true });
|
|
9695
|
-
}
|
|
9696
|
-
const baseName = this.fileBaseName(recipe);
|
|
9697
|
-
const metaPath = path6.join(metaDir, `${baseName}.json`);
|
|
9698
|
-
const bodyPath = path6.join(this.storeDir, `${baseName}.ts`);
|
|
9699
|
-
const { scriptBody, ...meta } = recipe;
|
|
9700
|
-
fs6.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
|
|
9701
|
-
fs6.writeFileSync(bodyPath, normalizeScriptBody(scriptBody));
|
|
9702
|
-
const legacyPath = path6.join(this.storeDir, `${this.toFileName(recipe.name)}.json`);
|
|
9703
|
-
if (fs6.existsSync(legacyPath)) {
|
|
9704
|
-
try {
|
|
9705
|
-
fs6.unlinkSync(legacyPath);
|
|
9706
|
-
} catch {
|
|
9707
|
-
}
|
|
9708
|
-
}
|
|
10093
|
+
return fs6.readFileSync(this.bodyPath(fileBase), "utf-8");
|
|
9709
10094
|
} catch (err) {
|
|
9710
|
-
logger.warn(`[ScriptStore]
|
|
10095
|
+
logger.warn(`[ScriptStore] body file ${fileBase}.ts missing/unreadable: ${err}`);
|
|
10096
|
+
return "";
|
|
9711
10097
|
}
|
|
9712
10098
|
}
|
|
9713
|
-
/**
|
|
9714
|
-
|
|
9715
|
-
|
|
9716
|
-
|
|
9717
|
-
const
|
|
9718
|
-
|
|
9719
|
-
|
|
9720
|
-
|
|
9721
|
-
|
|
9722
|
-
|
|
9723
|
-
|
|
9724
|
-
|
|
9725
|
-
|
|
9726
|
-
|
|
9727
|
-
if (fs6.existsSync(p)) fs6.unlinkSync(p);
|
|
9728
|
-
} catch (err) {
|
|
9729
|
-
logger.warn(`[ScriptStore] Failed to delete ${p}: ${err}`);
|
|
9730
|
-
}
|
|
10099
|
+
/** Atomic body write (temp + rename) so concurrent reads never see a partial file. */
|
|
10100
|
+
writeBody(fileBase, normalizedBody) {
|
|
10101
|
+
if (!fs6.existsSync(this.storeDir)) fs6.mkdirSync(this.storeDir, { recursive: true });
|
|
10102
|
+
const finalPath = this.bodyPath(fileBase);
|
|
10103
|
+
const tmpPath = `${finalPath}.tmp-${randomBytes(4).toString("hex")}`;
|
|
10104
|
+
fs6.writeFileSync(tmpPath, normalizedBody);
|
|
10105
|
+
fs6.renameSync(tmpPath, finalPath);
|
|
10106
|
+
}
|
|
10107
|
+
unlinkBody(fileBase) {
|
|
10108
|
+
try {
|
|
10109
|
+
const p = this.bodyPath(fileBase);
|
|
10110
|
+
if (fs6.existsSync(p)) fs6.unlinkSync(p);
|
|
10111
|
+
} catch (err) {
|
|
10112
|
+
logger.warn(`[ScriptStore] failed to delete body ${fileBase}.ts: ${err}`);
|
|
9731
10113
|
}
|
|
9732
10114
|
}
|
|
9733
10115
|
};
|
|
@@ -9744,14 +10126,14 @@ var ScriptMatcher = class {
|
|
|
9744
10126
|
* Returns null if no script matches.
|
|
9745
10127
|
*/
|
|
9746
10128
|
async match(userPrompt, apiKey, model) {
|
|
9747
|
-
const recipes = this.store.
|
|
10129
|
+
const recipes = await this.store.search(userPrompt);
|
|
9748
10130
|
if (recipes.length === 0) return null;
|
|
9749
10131
|
const healthyRecipes = recipes.filter((r) => {
|
|
9750
10132
|
const total = r.successCount + r.failureCount;
|
|
9751
10133
|
return total < 5 || r.failureCount / total <= MAX_FAILURE_RATE;
|
|
9752
10134
|
});
|
|
9753
10135
|
if (healthyRecipes.length === 0) {
|
|
9754
|
-
logger.info(`[ScriptMatcher]
|
|
10136
|
+
logger.info(`[ScriptMatcher] No healthy candidates among ${recipes.length} FTS matches \u2014 skipping`);
|
|
9755
10137
|
return null;
|
|
9756
10138
|
}
|
|
9757
10139
|
const scriptCatalog = this.buildScriptCatalog(healthyRecipes);
|
|
@@ -9787,7 +10169,7 @@ var ScriptMatcher = class {
|
|
|
9787
10169
|
logger.info(`[ScriptMatcher] No match. Reason: ${reasoning}`);
|
|
9788
10170
|
return null;
|
|
9789
10171
|
}
|
|
9790
|
-
const recipe = this.store.get(response.scriptId);
|
|
10172
|
+
const recipe = await this.store.get(response.scriptId);
|
|
9791
10173
|
if (!recipe) {
|
|
9792
10174
|
logger.warn(`[ScriptMatcher] LLM returned unknown script ID: "${response.scriptId}"`);
|
|
9793
10175
|
return null;
|
|
@@ -9810,61 +10192,360 @@ var ScriptMatcher = class {
|
|
|
9810
10192
|
reasoning
|
|
9811
10193
|
};
|
|
9812
10194
|
}
|
|
9813
|
-
logger.info(`[ScriptMatcher] Matched "${recipe.name}" (tier: high). Reason: ${reasoning}`);
|
|
9814
|
-
return {
|
|
9815
|
-
recipe,
|
|
9816
|
-
tier: "high",
|
|
9817
|
-
similarity: 0.95,
|
|
9818
|
-
confidence: "high",
|
|
9819
|
-
extractedParams: response.params || {},
|
|
9820
|
-
reasoning
|
|
9821
|
-
};
|
|
9822
|
-
} catch (error) {
|
|
9823
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
9824
|
-
logger.error(`[ScriptMatcher] LLM matching failed: ${msg}`);
|
|
9825
|
-
return null;
|
|
9826
|
-
}
|
|
10195
|
+
logger.info(`[ScriptMatcher] Matched "${recipe.name}" (tier: high). Reason: ${reasoning}`);
|
|
10196
|
+
return {
|
|
10197
|
+
recipe,
|
|
10198
|
+
tier: "high",
|
|
10199
|
+
similarity: 0.95,
|
|
10200
|
+
confidence: "high",
|
|
10201
|
+
extractedParams: response.params || {},
|
|
10202
|
+
reasoning
|
|
10203
|
+
};
|
|
10204
|
+
} catch (error) {
|
|
10205
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
10206
|
+
logger.error(`[ScriptMatcher] LLM matching failed: ${msg}`);
|
|
10207
|
+
return null;
|
|
10208
|
+
}
|
|
10209
|
+
}
|
|
10210
|
+
/**
|
|
10211
|
+
* Build the script catalog string for the LLM prompt.
|
|
10212
|
+
* Each script gets: index, ID, name, description, and parameter definitions.
|
|
10213
|
+
*/
|
|
10214
|
+
buildScriptCatalog(recipes) {
|
|
10215
|
+
return recipes.map((r, idx) => {
|
|
10216
|
+
const paramList = r.parameters.length > 0 ? r.parameters.map((p) => {
|
|
10217
|
+
let desc = `${p.name} (${p.type}`;
|
|
10218
|
+
if (!p.required) desc += ", optional";
|
|
10219
|
+
if (p.default !== void 0) desc += `, default: ${JSON.stringify(p.default)}`;
|
|
10220
|
+
if (p.enumValues) desc += `, values: ${Object.keys(p.enumValues).join("/")}`;
|
|
10221
|
+
desc += `) \u2014 ${p.description}`;
|
|
10222
|
+
return ` - ${desc}`;
|
|
10223
|
+
}).join("\n") : " (no parameters)";
|
|
10224
|
+
return `${idx + 1}. [${r.id}] "${r.name}"
|
|
10225
|
+
${r.intentDescription}
|
|
10226
|
+
Parameters:
|
|
10227
|
+
${paramList}`;
|
|
10228
|
+
}).join("\n\n");
|
|
10229
|
+
}
|
|
10230
|
+
};
|
|
10231
|
+
|
|
10232
|
+
// src/userResponse/scripts/script-component-generator.ts
|
|
10233
|
+
function inferColumns(rows) {
|
|
10234
|
+
const cols = /* @__PURE__ */ new Map();
|
|
10235
|
+
if (!Array.isArray(rows) || rows.length === 0) return cols;
|
|
10236
|
+
const sample = rows.slice(0, 20);
|
|
10237
|
+
const keys = /* @__PURE__ */ new Set();
|
|
10238
|
+
for (const r of sample) {
|
|
10239
|
+
if (r && typeof r === "object") for (const k of Object.keys(r)) keys.add(k);
|
|
10240
|
+
}
|
|
10241
|
+
for (const key of keys) {
|
|
10242
|
+
let type = "TEXT";
|
|
10243
|
+
let typed = false;
|
|
10244
|
+
const distinct = /* @__PURE__ */ new Set();
|
|
10245
|
+
for (const r of sample) {
|
|
10246
|
+
const v = r?.[key];
|
|
10247
|
+
if (v === null || v === void 0) continue;
|
|
10248
|
+
if (!typed) {
|
|
10249
|
+
if (typeof v === "number") type = "NUMBER";
|
|
10250
|
+
else if (typeof v === "boolean") type = "BOOLEAN";
|
|
10251
|
+
else if (v instanceof Date) type = "DATE";
|
|
10252
|
+
else if (typeof v === "string" && v.trim() !== "" && !isNaN(Number(v))) type = "NUMBER";
|
|
10253
|
+
else type = "TEXT";
|
|
10254
|
+
typed = true;
|
|
10255
|
+
}
|
|
10256
|
+
distinct.add(v instanceof Date ? v.toISOString() : String(v));
|
|
10257
|
+
}
|
|
10258
|
+
cols.set(key.toLowerCase(), { name: key, type, invariant: distinct.size <= 1 });
|
|
10259
|
+
}
|
|
10260
|
+
return cols;
|
|
10261
|
+
}
|
|
10262
|
+
function resolveCol(cols, key) {
|
|
10263
|
+
if (key == null) return void 0;
|
|
10264
|
+
return cols.get(String(key).toLowerCase());
|
|
10265
|
+
}
|
|
10266
|
+
function firstOfType(cols, type) {
|
|
10267
|
+
for (const c of cols.values()) if (c.type === type) return c;
|
|
10268
|
+
return void 0;
|
|
10269
|
+
}
|
|
10270
|
+
var ALL_KEY_NAMES = ["xAxisKey", "yAxisKey", "valueKey", "nameKey", "seriesKey", "groupBy", "aggregationField", "categoryKey"];
|
|
10271
|
+
var AVG_KEYWORDS = /\b(average|avg|mean)\b/i;
|
|
10272
|
+
var COUNT_KEYWORDS = /\b(count|number of|how many|# of|no\.? of)\b/i;
|
|
10273
|
+
var SUM_KEYWORDS = /\b(total|sum|overall|combined|aggregate|all)\b/i;
|
|
10274
|
+
function validateAndRepairComponent(comp, componentType, cols, rowCount) {
|
|
10275
|
+
if (cols.size === 0) return comp;
|
|
10276
|
+
const props = comp.props || (comp.props = {});
|
|
10277
|
+
const cfg = props.config || (props.config = {});
|
|
10278
|
+
for (const k of ALL_KEY_NAMES) {
|
|
10279
|
+
if (cfg[k] == null) continue;
|
|
10280
|
+
const r = resolveCol(cols, cfg[k]);
|
|
10281
|
+
if (r) cfg[k] = r.name;
|
|
10282
|
+
else delete cfg[k];
|
|
10283
|
+
}
|
|
10284
|
+
if (Array.isArray(cfg.metricKeys)) {
|
|
10285
|
+
cfg.metricKeys = cfg.metricKeys.map((m) => resolveCol(cols, m)?.name).filter(Boolean);
|
|
10286
|
+
if (cfg.metricKeys.length === 0) delete cfg.metricKeys;
|
|
10287
|
+
}
|
|
10288
|
+
const numeric = () => firstOfType(cols, "NUMBER");
|
|
10289
|
+
const category = () => firstOfType(cols, "TEXT") || firstOfType(cols, "DATE");
|
|
10290
|
+
const hasMetrics = Array.isArray(cfg.metricKeys) && cfg.metricKeys.length > 0;
|
|
10291
|
+
switch (componentType) {
|
|
10292
|
+
case "KPICard":
|
|
10293
|
+
case "GaugeChart": {
|
|
10294
|
+
const vk = resolveCol(cols, cfg.valueKey) || numeric() || category();
|
|
10295
|
+
if (!vk) return null;
|
|
10296
|
+
cfg.valueKey = vk.name;
|
|
10297
|
+
const title = String(props.title || "");
|
|
10298
|
+
if (rowCount > 1 && !cfg.aggregation) {
|
|
10299
|
+
if (COUNT_KEYWORDS.test(title)) cfg.aggregation = "count";
|
|
10300
|
+
else if (!vk.invariant) {
|
|
10301
|
+
if (AVG_KEYWORDS.test(title)) cfg.aggregation = "avg";
|
|
10302
|
+
else if (SUM_KEYWORDS.test(title)) cfg.aggregation = "sum";
|
|
10303
|
+
}
|
|
10304
|
+
}
|
|
10305
|
+
if (cfg.aggregation === "sum" || cfg.aggregation === "avg") {
|
|
10306
|
+
const af = resolveCol(cols, cfg.aggregationField);
|
|
10307
|
+
if (af && af.type === "NUMBER") {
|
|
10308
|
+
cfg.aggregationField = af.name;
|
|
10309
|
+
} else {
|
|
10310
|
+
const n = vk.type === "NUMBER" ? vk : numeric();
|
|
10311
|
+
if (n) cfg.aggregationField = n.name;
|
|
10312
|
+
else {
|
|
10313
|
+
delete cfg.aggregation;
|
|
10314
|
+
delete cfg.aggregationField;
|
|
10315
|
+
}
|
|
10316
|
+
}
|
|
10317
|
+
}
|
|
10318
|
+
return comp;
|
|
10319
|
+
}
|
|
10320
|
+
case "PieChart":
|
|
10321
|
+
case "TreemapChart": {
|
|
10322
|
+
let nameC = resolveCol(cols, cfg.nameKey);
|
|
10323
|
+
let valC = resolveCol(cols, cfg.valueKey);
|
|
10324
|
+
if (nameC?.type === "NUMBER" && valC && valC.type !== "NUMBER") {
|
|
10325
|
+
const t = nameC;
|
|
10326
|
+
nameC = valC;
|
|
10327
|
+
valC = t;
|
|
10328
|
+
}
|
|
10329
|
+
if (!nameC) nameC = category();
|
|
10330
|
+
if (!valC || valC.type !== "NUMBER") valC = numeric() || valC;
|
|
10331
|
+
if (!nameC || !valC) return null;
|
|
10332
|
+
cfg.nameKey = nameC.name;
|
|
10333
|
+
cfg.valueKey = valC.name;
|
|
10334
|
+
return comp;
|
|
10335
|
+
}
|
|
10336
|
+
case "WaterfallChart": {
|
|
10337
|
+
const catC = resolveCol(cols, cfg.categoryKey) || category();
|
|
10338
|
+
let valC = resolveCol(cols, cfg.valueKey);
|
|
10339
|
+
if (!valC || valC.type !== "NUMBER") valC = numeric() || valC;
|
|
10340
|
+
if (!catC || !valC) return null;
|
|
10341
|
+
cfg.categoryKey = catC.name;
|
|
10342
|
+
cfg.valueKey = valC.name;
|
|
10343
|
+
return comp;
|
|
10344
|
+
}
|
|
10345
|
+
case "ScatterChart": {
|
|
10346
|
+
let xC = resolveCol(cols, cfg.xAxisKey);
|
|
10347
|
+
let yC = resolveCol(cols, cfg.yAxisKey);
|
|
10348
|
+
if (!xC || xC.type !== "NUMBER") xC = numeric() || xC;
|
|
10349
|
+
if (!yC || yC.type !== "NUMBER") {
|
|
10350
|
+
yC = [...cols.values()].find((c) => c.type === "NUMBER" && c.name !== xC?.name) || numeric() || yC;
|
|
10351
|
+
}
|
|
10352
|
+
if (!xC || !yC) return null;
|
|
10353
|
+
cfg.xAxisKey = xC.name;
|
|
10354
|
+
cfg.yAxisKey = yC.name;
|
|
10355
|
+
return comp;
|
|
10356
|
+
}
|
|
10357
|
+
case "HeatmapChart": {
|
|
10358
|
+
const xC = resolveCol(cols, cfg.xAxisKey) || category();
|
|
10359
|
+
const yC = resolveCol(cols, cfg.yAxisKey) || [...cols.values()].find((c) => (c.type === "TEXT" || c.type === "DATE") && c.name !== xC?.name);
|
|
10360
|
+
let valC = resolveCol(cols, cfg.valueKey);
|
|
10361
|
+
if (!valC || valC.type !== "NUMBER") valC = numeric() || valC;
|
|
10362
|
+
if (!xC || !yC || !valC) return null;
|
|
10363
|
+
cfg.xAxisKey = xC.name;
|
|
10364
|
+
cfg.yAxisKey = yC.name;
|
|
10365
|
+
cfg.valueKey = valC.name;
|
|
10366
|
+
return comp;
|
|
10367
|
+
}
|
|
10368
|
+
case "BarChart":
|
|
10369
|
+
case "LineChart": {
|
|
10370
|
+
let xC = resolveCol(cols, cfg.xAxisKey);
|
|
10371
|
+
let yC = resolveCol(cols, cfg.yAxisKey);
|
|
10372
|
+
if (!hasMetrics && xC?.type === "NUMBER" && yC && yC.type !== "NUMBER") {
|
|
10373
|
+
const t = xC;
|
|
10374
|
+
xC = yC;
|
|
10375
|
+
yC = t;
|
|
10376
|
+
}
|
|
10377
|
+
if (!xC) xC = (componentType === "LineChart" ? firstOfType(cols, "DATE") || category() : category()) || firstOfType(cols, "NUMBER");
|
|
10378
|
+
if (!xC) return null;
|
|
10379
|
+
cfg.xAxisKey = xC.name;
|
|
10380
|
+
if (!hasMetrics) {
|
|
10381
|
+
if (!yC || yC.type !== "NUMBER") yC = numeric();
|
|
10382
|
+
if (!yC) return null;
|
|
10383
|
+
cfg.yAxisKey = yC.name;
|
|
10384
|
+
}
|
|
10385
|
+
if (cfg.seriesKey) {
|
|
10386
|
+
const sC = resolveCol(cols, cfg.seriesKey);
|
|
10387
|
+
if (!sC || sC.type !== "TEXT" || sC.name === cfg.xAxisKey) delete cfg.seriesKey;
|
|
10388
|
+
else cfg.seriesKey = sC.name;
|
|
10389
|
+
}
|
|
10390
|
+
return comp;
|
|
10391
|
+
}
|
|
10392
|
+
case "RadarChart": {
|
|
10393
|
+
if (!hasMetrics) {
|
|
10394
|
+
const vk = resolveCol(cols, cfg.valueKey) || numeric();
|
|
10395
|
+
if (!vk) return null;
|
|
10396
|
+
cfg.valueKey = vk.name;
|
|
10397
|
+
}
|
|
10398
|
+
return comp;
|
|
10399
|
+
}
|
|
10400
|
+
default:
|
|
10401
|
+
return comp;
|
|
10402
|
+
}
|
|
10403
|
+
}
|
|
10404
|
+
function scriptCompKey(comp) {
|
|
10405
|
+
const id = comp?.componentId || comp?.componentName || "";
|
|
10406
|
+
const src = comp?.sourceIndex ?? "";
|
|
10407
|
+
return `${id}|${src}|${JSON.stringify(comp?.props?.config ?? comp?.config ?? {})}`;
|
|
10408
|
+
}
|
|
10409
|
+
function extractAnswerComponentObject(text) {
|
|
10410
|
+
const hasMatch = text.match(/"hasAnswerComponent"\s*:\s*(true|false)/);
|
|
10411
|
+
if (!hasMatch || hasMatch[1] !== "true") return null;
|
|
10412
|
+
const startMatch = text.match(/"answerComponent"\s*:\s*\{/);
|
|
10413
|
+
if (!startMatch) return null;
|
|
10414
|
+
const startPos = startMatch.index + startMatch[0].length - 1;
|
|
10415
|
+
let depth = 0, inString = false, escapeNext = false, endPos = -1;
|
|
10416
|
+
for (let i = startPos; i < text.length; i++) {
|
|
10417
|
+
const c = text[i];
|
|
10418
|
+
if (escapeNext) {
|
|
10419
|
+
escapeNext = false;
|
|
10420
|
+
continue;
|
|
10421
|
+
}
|
|
10422
|
+
if (c === "\\") {
|
|
10423
|
+
escapeNext = true;
|
|
10424
|
+
continue;
|
|
10425
|
+
}
|
|
10426
|
+
if (c === '"') {
|
|
10427
|
+
inString = !inString;
|
|
10428
|
+
continue;
|
|
10429
|
+
}
|
|
10430
|
+
if (!inString) {
|
|
10431
|
+
if (c === "{") depth++;
|
|
10432
|
+
else if (c === "}") {
|
|
10433
|
+
depth--;
|
|
10434
|
+
if (depth === 0) {
|
|
10435
|
+
endPos = i + 1;
|
|
10436
|
+
break;
|
|
10437
|
+
}
|
|
10438
|
+
}
|
|
10439
|
+
}
|
|
10440
|
+
}
|
|
10441
|
+
if (endPos <= startPos) return null;
|
|
10442
|
+
try {
|
|
10443
|
+
return JSON.parse(text.substring(startPos, endPos));
|
|
10444
|
+
} catch {
|
|
10445
|
+
return null;
|
|
10446
|
+
}
|
|
10447
|
+
}
|
|
10448
|
+
async function buildScriptComponent(comp, ctx) {
|
|
10449
|
+
const { queries, queryIds, columnsByIndex, availableComponents } = ctx;
|
|
10450
|
+
let validationCols = /* @__PURE__ */ new Map();
|
|
10451
|
+
let validationRowCount = 0;
|
|
10452
|
+
let specSourceRef = "";
|
|
10453
|
+
const wantedId = comp.componentId || "";
|
|
10454
|
+
const wantedName = comp.componentName || "";
|
|
10455
|
+
const template = availableComponents.find(
|
|
10456
|
+
(c) => c.id === wantedId || c.name === wantedId || c.id === wantedName || c.name === wantedName
|
|
10457
|
+
);
|
|
10458
|
+
if (!template) {
|
|
10459
|
+
logger.warn(`[ScriptComponentGen] Component "${wantedId || wantedName}" not found \u2014 skipping`);
|
|
10460
|
+
return null;
|
|
10461
|
+
}
|
|
10462
|
+
if (template.type === "MarkdownBlock") {
|
|
10463
|
+
const p = comp.props || {};
|
|
10464
|
+
const content = p.content ?? p.config?.content ?? p.config?.markdown ?? p.config?.text ?? "";
|
|
10465
|
+
return {
|
|
10466
|
+
component: {
|
|
10467
|
+
id: `comp_${Math.random().toString(36).substring(2, 8)}`,
|
|
10468
|
+
name: template.name,
|
|
10469
|
+
displayName: template.displayName,
|
|
10470
|
+
type: template.type,
|
|
10471
|
+
description: template.description,
|
|
10472
|
+
props: { title: p.title, description: p.description, content },
|
|
10473
|
+
category: template.category,
|
|
10474
|
+
keywords: template.keywords
|
|
10475
|
+
},
|
|
10476
|
+
// Persist a content-bearing spec so the markdown survives deterministic
|
|
10477
|
+
// replay (it has no data source, so its text must be stored — otherwise
|
|
10478
|
+
// re-running a matched script rebuilds only the source-bound components
|
|
10479
|
+
// and silently drops the narrative).
|
|
10480
|
+
spec: {
|
|
10481
|
+
componentType: template.name,
|
|
10482
|
+
sourceRef: "markdown",
|
|
10483
|
+
title: p.title,
|
|
10484
|
+
description: p.description,
|
|
10485
|
+
content,
|
|
10486
|
+
config: {}
|
|
10487
|
+
}
|
|
10488
|
+
};
|
|
9827
10489
|
}
|
|
9828
|
-
|
|
9829
|
-
|
|
9830
|
-
|
|
9831
|
-
|
|
9832
|
-
|
|
9833
|
-
return
|
|
9834
|
-
const paramList = r.parameters.length > 0 ? r.parameters.map((p) => {
|
|
9835
|
-
let desc = `${p.name} (${p.type}`;
|
|
9836
|
-
if (!p.required) desc += ", optional";
|
|
9837
|
-
if (p.default !== void 0) desc += `, default: ${JSON.stringify(p.default)}`;
|
|
9838
|
-
if (p.enumValues) desc += `, values: ${Object.keys(p.enumValues).join("/")}`;
|
|
9839
|
-
desc += `) \u2014 ${p.description}`;
|
|
9840
|
-
return ` - ${desc}`;
|
|
9841
|
-
}).join("\n") : " (no parameters)";
|
|
9842
|
-
return `${idx + 1}. [${r.id}] "${r.name}"
|
|
9843
|
-
${r.intentDescription}
|
|
9844
|
-
Parameters:
|
|
9845
|
-
${paramList}`;
|
|
9846
|
-
}).join("\n\n");
|
|
10490
|
+
const sourceIdx = typeof comp.sourceIndex === "number" ? comp.sourceIndex : 0;
|
|
10491
|
+
const targetQuery = queries[sourceIdx] || queries[0];
|
|
10492
|
+
const targetQueryId = queryIds[targetQuery.sourceId];
|
|
10493
|
+
if (!targetQueryId) {
|
|
10494
|
+
logger.warn(`[ScriptComponentGen] No queryId for source "${targetQuery.sourceName}" \u2014 skipping`);
|
|
10495
|
+
return null;
|
|
9847
10496
|
}
|
|
9848
|
-
|
|
9849
|
-
|
|
9850
|
-
|
|
10497
|
+
validationCols = columnsByIndex[sourceIdx] || inferColumns(targetQuery.data);
|
|
10498
|
+
validationRowCount = targetQuery.count ?? (targetQuery.data?.length || 0);
|
|
10499
|
+
specSourceRef = targetQuery.sourceId;
|
|
10500
|
+
const renderedToolId = targetQuery.virtual ? "script_dataset" : targetQuery.sourceId;
|
|
10501
|
+
const externalToolProp = { toolId: renderedToolId, toolName: targetQuery.sourceName, parameters: { queryId: targetQueryId } };
|
|
10502
|
+
const repaired = validateAndRepairComponent(comp, template.type, validationCols, validationRowCount);
|
|
10503
|
+
if (!repaired) {
|
|
10504
|
+
logger.warn(`[ScriptComponentGen] Component "${template.name}" dropped \u2014 config keys could not be mapped to the data columns`);
|
|
10505
|
+
return null;
|
|
10506
|
+
}
|
|
10507
|
+
if (template.type === "DataTable" && Array.isArray(comp.props?.config?.columns)) {
|
|
10508
|
+
comp.props.config.columns = comp.props.config.columns.map(
|
|
10509
|
+
(c) => typeof c === "string" ? { key: c, label: c.replace(/[_-]+/g, " ").replace(/\b\w/g, (m) => m.toUpperCase()) } : c
|
|
10510
|
+
);
|
|
10511
|
+
}
|
|
10512
|
+
const component = {
|
|
10513
|
+
id: `comp_${Math.random().toString(36).substring(2, 8)}`,
|
|
10514
|
+
name: template.name,
|
|
10515
|
+
displayName: template.displayName,
|
|
10516
|
+
type: template.type,
|
|
10517
|
+
description: template.description,
|
|
10518
|
+
props: { ...comp.props, externalTool: externalToolProp },
|
|
10519
|
+
category: template.category,
|
|
10520
|
+
keywords: template.keywords
|
|
10521
|
+
};
|
|
10522
|
+
const spec = specSourceRef ? {
|
|
10523
|
+
componentType: template.name,
|
|
10524
|
+
sourceRef: specSourceRef,
|
|
10525
|
+
title: comp.props?.title,
|
|
10526
|
+
description: comp.props?.description,
|
|
10527
|
+
config: { ...comp.props?.config || {} }
|
|
10528
|
+
} : void 0;
|
|
10529
|
+
return { component, spec };
|
|
10530
|
+
}
|
|
9851
10531
|
async function generateScriptComponents(params) {
|
|
9852
|
-
const { userPrompt, scriptResult, queryIds, availableComponents, apiKey, model, componentStreamCallback,
|
|
10532
|
+
const { userPrompt, scriptResult, queryIds, availableComponents, apiKey, model, componentStreamCallback, analysisText } = params;
|
|
9853
10533
|
const queries = scriptResult.executedQueries;
|
|
9854
10534
|
if (!queries || queries.length === 0) {
|
|
9855
10535
|
logger.warn(`[ScriptComponentGen] No query data available \u2014 returning empty`);
|
|
9856
|
-
return { components: [], layoutTitle: "", layoutDescription: "", actions: [] };
|
|
10536
|
+
return { components: [], layoutTitle: "", layoutDescription: "", actions: [], componentSpecs: [] };
|
|
9857
10537
|
}
|
|
9858
10538
|
logger.info(`[ScriptComponentGen] Starting | ${queries.length} queries | ${availableComponents.length} available components`);
|
|
9859
|
-
const
|
|
10539
|
+
const columnsByIndex = queries.map((q) => {
|
|
10540
|
+
const data = q.data && q.data.length ? q.data : q.virtual ? scriptResult.data : q.data;
|
|
10541
|
+
return inferColumns(data || []);
|
|
10542
|
+
});
|
|
10543
|
+
const availableCompsText = availableComponents.map((c) => `- ${c.name} (${c.type}): ${c.description || ""}`).join("\n");
|
|
9860
10544
|
const sourceDescriptions = queries.map((q, idx) => {
|
|
9861
10545
|
if (!q.data || q.data.length === 0) return null;
|
|
9862
|
-
const
|
|
9863
|
-
const columns =
|
|
9864
|
-
|
|
9865
|
-
return `${key} (${type})`;
|
|
9866
|
-
}).join(", ");
|
|
9867
|
-
const sampleData = JSON.stringify(q.data.slice(0, 3), null, 2);
|
|
10546
|
+
const cols = columnsByIndex[idx];
|
|
10547
|
+
const columns = cols && cols.size > 0 ? Array.from(cols.values()).map((c) => `${c.name} (${c.type})`).join(", ") : Object.keys(q.data[0]).join(", ");
|
|
10548
|
+
const sampleData = JSON.stringify(q.data.slice(0, TOOL_TRACKING_SAMPLE_ROWS), null, 2);
|
|
9868
10549
|
return `### Source ${idx}: "${q.sourceName}" (sourceIndex: ${idx})
|
|
9869
10550
|
- Rows: ${q.count}${q.totalCount && q.totalCount > q.count ? ` of ${q.totalCount} total` : ""}
|
|
9870
10551
|
- Columns: ${columns}
|
|
@@ -9873,19 +10554,6 @@ async function generateScriptComponents(params) {
|
|
|
9873
10554
|
${sampleData}
|
|
9874
10555
|
\`\`\``;
|
|
9875
10556
|
}).filter(Boolean).join("\n\n");
|
|
9876
|
-
const isMultiSource = queries.length > 1;
|
|
9877
|
-
const federationTool = externalTools?.find((t) => t.id === "federation_query");
|
|
9878
|
-
let federationContext = "";
|
|
9879
|
-
if (isMultiSource && federationTool) {
|
|
9880
|
-
federationContext = `### Cross-Source Federation Available
|
|
9881
|
-
You can create components that JOIN data from multiple sources using DuckDB SQL.
|
|
9882
|
-
Set sourceIndex to "federation" and provide federationSql.
|
|
9883
|
-
${federationTool.fullSchema || federationTool.description || "Use schema-qualified table names."}`;
|
|
9884
|
-
} else if (isMultiSource) {
|
|
9885
|
-
federationContext = `### Multiple Sources Available
|
|
9886
|
-
Each component can use data from one source (specify sourceIndex: 0, 1, etc.).
|
|
9887
|
-
Cross-source JOINs are not available for this project.`;
|
|
9888
|
-
}
|
|
9889
10557
|
try {
|
|
9890
10558
|
const prompts = await promptLoader.loadPrompts("script-components", {
|
|
9891
10559
|
USER_PROMPT: userPrompt,
|
|
@@ -9894,95 +10562,98 @@ Cross-source JOINs are not available for this project.`;
|
|
|
9894
10562
|
ROW_COUNT: queries.map((q) => `${q.sourceName}: ${q.count}`).join(", "),
|
|
9895
10563
|
COLUMN_DESCRIPTIONS: sourceDescriptions,
|
|
9896
10564
|
SAMPLE_DATA: "",
|
|
9897
|
-
|
|
10565
|
+
ANALYSIS_TEXT: analysisText && analysisText.trim() ? analysisText.trim() : "No analysis text provided \u2014 infer intent from the user question and the returned data."
|
|
9898
10566
|
});
|
|
10567
|
+
const ctx = {
|
|
10568
|
+
queries,
|
|
10569
|
+
queryIds,
|
|
10570
|
+
columnsByIndex,
|
|
10571
|
+
availableComponents
|
|
10572
|
+
};
|
|
10573
|
+
let fullText = "";
|
|
10574
|
+
let answerAttempted = false;
|
|
10575
|
+
let streamedAnswerKey = null;
|
|
10576
|
+
const partial = componentStreamCallback ? (chunk) => {
|
|
10577
|
+
fullText += chunk;
|
|
10578
|
+
if (answerAttempted) return;
|
|
10579
|
+
const answerComp = extractAnswerComponentObject(fullText);
|
|
10580
|
+
if (!answerComp) return;
|
|
10581
|
+
answerAttempted = true;
|
|
10582
|
+
streamedAnswerKey = scriptCompKey(answerComp);
|
|
10583
|
+
buildScriptComponent(answerComp, ctx).then((built) => {
|
|
10584
|
+
if (built) {
|
|
10585
|
+
componentStreamCallback(built.component);
|
|
10586
|
+
logger.info("[ScriptComponentGen] Streamed answer component early");
|
|
10587
|
+
} else {
|
|
10588
|
+
streamedAnswerKey = null;
|
|
10589
|
+
}
|
|
10590
|
+
}).catch(() => {
|
|
10591
|
+
streamedAnswerKey = null;
|
|
10592
|
+
});
|
|
10593
|
+
} : void 0;
|
|
9899
10594
|
const response = await LLM.stream(
|
|
9900
10595
|
{ sys: prompts.system, user: prompts.user },
|
|
9901
10596
|
{
|
|
9902
10597
|
model: model || void 0,
|
|
9903
|
-
maxTokens:
|
|
10598
|
+
maxTokens: 5e3,
|
|
9904
10599
|
temperature: 0,
|
|
9905
|
-
apiKey
|
|
10600
|
+
apiKey,
|
|
10601
|
+
partial
|
|
9906
10602
|
},
|
|
9907
10603
|
true
|
|
9908
10604
|
);
|
|
9909
10605
|
if (!response || !response.components || !Array.isArray(response.components)) {
|
|
9910
10606
|
logger.warn(`[ScriptComponentGen] LLM returned invalid response`);
|
|
9911
|
-
return { components: [], layoutTitle: "", layoutDescription: "", actions: [] };
|
|
10607
|
+
return { components: [], layoutTitle: "", layoutDescription: "", actions: [], componentSpecs: [] };
|
|
9912
10608
|
}
|
|
9913
10609
|
const layoutTitle = response.layoutTitle || "Dashboard";
|
|
9914
10610
|
const layoutDescription = response.layoutDescription || "";
|
|
9915
10611
|
const matchedComponents = [];
|
|
10612
|
+
const componentSpecs = [];
|
|
9916
10613
|
for (const comp of response.components) {
|
|
9917
|
-
const
|
|
9918
|
-
|
|
9919
|
-
|
|
9920
|
-
|
|
9921
|
-
)
|
|
9922
|
-
|
|
9923
|
-
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
|
|
9927
|
-
|
|
9928
|
-
|
|
9929
|
-
|
|
9930
|
-
|
|
9931
|
-
);
|
|
9932
|
-
|
|
9933
|
-
|
|
9934
|
-
} else {
|
|
9935
|
-
logger.info(`[ScriptComponentGen] Federation component \u2014 executing cross-source SQL`);
|
|
9936
|
-
try {
|
|
9937
|
-
const fedResult = await executeFederationQuery(comp.federationSql, externalTools, collections);
|
|
9938
|
-
if (fedResult) {
|
|
9939
|
-
const fedQueryId = queryCache.storeQuery(comp.federationSql, fedResult);
|
|
9940
|
-
externalToolProp = {
|
|
9941
|
-
toolId: "federation_query",
|
|
9942
|
-
toolName: "Cross-Source Query",
|
|
9943
|
-
parameters: { queryId: fedQueryId }
|
|
9944
|
-
};
|
|
9945
|
-
} else {
|
|
9946
|
-
logger.warn(`[ScriptComponentGen] Federation query failed \u2014 skipping component`);
|
|
9947
|
-
continue;
|
|
9948
|
-
}
|
|
9949
|
-
} catch (fedErr) {
|
|
9950
|
-
logger.warn(`[ScriptComponentGen] Federation query error: ${fedErr} \u2014 skipping component`);
|
|
9951
|
-
continue;
|
|
9952
|
-
}
|
|
10614
|
+
const built = await buildScriptComponent(comp, ctx);
|
|
10615
|
+
if (!built) continue;
|
|
10616
|
+
matchedComponents.push(built.component);
|
|
10617
|
+
if (built.spec) componentSpecs.push(built.spec);
|
|
10618
|
+
if (componentStreamCallback && scriptCompKey(comp) !== streamedAnswerKey) {
|
|
10619
|
+
componentStreamCallback(built.component);
|
|
10620
|
+
}
|
|
10621
|
+
}
|
|
10622
|
+
if (response.hasAnswerComponent && response.answerComponent) {
|
|
10623
|
+
const aKey = scriptCompKey(response.answerComponent);
|
|
10624
|
+
const alreadyIncluded = response.components.some((c) => scriptCompKey(c) === aKey);
|
|
10625
|
+
if (!alreadyIncluded) {
|
|
10626
|
+
const built = await buildScriptComponent(response.answerComponent, ctx);
|
|
10627
|
+
if (built) {
|
|
10628
|
+
matchedComponents.unshift(built.component);
|
|
10629
|
+
if (built.spec) componentSpecs.unshift(built.spec);
|
|
10630
|
+
if (componentStreamCallback && aKey !== streamedAnswerKey) componentStreamCallback(built.component);
|
|
9953
10631
|
}
|
|
9954
10632
|
}
|
|
9955
|
-
|
|
9956
|
-
|
|
9957
|
-
|
|
9958
|
-
|
|
9959
|
-
|
|
9960
|
-
|
|
9961
|
-
|
|
9962
|
-
|
|
9963
|
-
|
|
9964
|
-
|
|
9965
|
-
|
|
9966
|
-
|
|
9967
|
-
|
|
10633
|
+
}
|
|
10634
|
+
const hasMarkdown = matchedComponents.some((c) => c.type === "MarkdownBlock");
|
|
10635
|
+
if (!hasMarkdown && analysisText && analysisText.trim()) {
|
|
10636
|
+
const mdTemplate = availableComponents.find((c) => c.type === "MarkdownBlock" || c.name === "DynamicMarkdownBlock");
|
|
10637
|
+
if (mdTemplate) {
|
|
10638
|
+
const mdComponent = {
|
|
10639
|
+
id: `comp_${Math.random().toString(36).substring(2, 8)}`,
|
|
10640
|
+
name: mdTemplate.name,
|
|
10641
|
+
displayName: mdTemplate.displayName,
|
|
10642
|
+
type: mdTemplate.type,
|
|
10643
|
+
description: mdTemplate.description,
|
|
10644
|
+
props: { content: analysisText.trim() },
|
|
10645
|
+
category: mdTemplate.category,
|
|
10646
|
+
keywords: mdTemplate.keywords
|
|
9968
10647
|
};
|
|
9969
|
-
|
|
9970
|
-
|
|
9971
|
-
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
externalTool: externalToolProp
|
|
9979
|
-
},
|
|
9980
|
-
category: template.category,
|
|
9981
|
-
keywords: template.keywords
|
|
9982
|
-
};
|
|
9983
|
-
matchedComponents.push(component);
|
|
9984
|
-
if (componentStreamCallback) {
|
|
9985
|
-
componentStreamCallback(component);
|
|
10648
|
+
matchedComponents.push(mdComponent);
|
|
10649
|
+
componentSpecs.push({
|
|
10650
|
+
componentType: mdTemplate.name,
|
|
10651
|
+
sourceRef: "markdown",
|
|
10652
|
+
content: analysisText.trim(),
|
|
10653
|
+
config: {}
|
|
10654
|
+
});
|
|
10655
|
+
if (componentStreamCallback) componentStreamCallback(mdComponent);
|
|
10656
|
+
logger.info("[ScriptComponentGen] Injected fallback DynamicMarkdownBlock (LLM omitted one)");
|
|
9986
10657
|
}
|
|
9987
10658
|
}
|
|
9988
10659
|
logger.info(`[ScriptComponentGen] Generated ${matchedComponents.length} components`);
|
|
@@ -9996,37 +10667,97 @@ Cross-source JOINs are not available for this project.`;
|
|
|
9996
10667
|
components: matchedComponents,
|
|
9997
10668
|
layoutTitle,
|
|
9998
10669
|
layoutDescription,
|
|
9999
|
-
actions
|
|
10670
|
+
actions,
|
|
10671
|
+
componentSpecs
|
|
10000
10672
|
};
|
|
10001
10673
|
} catch (error) {
|
|
10002
10674
|
const msg = error instanceof Error ? error.message : String(error);
|
|
10003
10675
|
logger.error(`[ScriptComponentGen] Failed: ${msg}`);
|
|
10004
|
-
return { components: [], layoutTitle: "", layoutDescription: "", actions: [] };
|
|
10676
|
+
return { components: [], layoutTitle: "", layoutDescription: "", actions: [], componentSpecs: [] };
|
|
10005
10677
|
}
|
|
10006
10678
|
}
|
|
10007
|
-
|
|
10008
|
-
const
|
|
10009
|
-
if (
|
|
10010
|
-
|
|
10011
|
-
|
|
10012
|
-
|
|
10013
|
-
|
|
10014
|
-
|
|
10679
|
+
function assembleComponentsFromSpecs(params) {
|
|
10680
|
+
const { specs, scriptResult, queryIds, availableComponents, componentStreamCallback, analysisText } = params;
|
|
10681
|
+
if (!specs || specs.length === 0) return null;
|
|
10682
|
+
const queries = scriptResult.executedQueries || [];
|
|
10683
|
+
const byId = new Map(queries.map((q) => [q.sourceId, q]));
|
|
10684
|
+
const out = [];
|
|
10685
|
+
let answerComponentStreamed = false;
|
|
10686
|
+
const mdTemplate = availableComponents.find((c) => c.type === "MarkdownBlock" || c.name === "DynamicMarkdownBlock");
|
|
10687
|
+
const buildMarkdown = (content, title, description) => {
|
|
10688
|
+
if (!mdTemplate || !content || !content.trim()) return null;
|
|
10689
|
+
return {
|
|
10690
|
+
id: `comp_${Math.random().toString(36).substring(2, 8)}`,
|
|
10691
|
+
name: mdTemplate.name,
|
|
10692
|
+
displayName: mdTemplate.displayName,
|
|
10693
|
+
type: mdTemplate.type,
|
|
10694
|
+
description: mdTemplate.description,
|
|
10695
|
+
props: { title, description, content: content.trim() },
|
|
10696
|
+
category: mdTemplate.category,
|
|
10697
|
+
keywords: mdTemplate.keywords
|
|
10698
|
+
};
|
|
10699
|
+
};
|
|
10700
|
+
for (const spec of specs) {
|
|
10701
|
+
if (spec.sourceRef === "federation") {
|
|
10702
|
+
logger.info("[ScriptComponentGen] Recipe has a federation spec \u2014 not replayable deterministically, falling back to LLM");
|
|
10703
|
+
return null;
|
|
10704
|
+
}
|
|
10705
|
+
if (spec.sourceRef === "markdown") {
|
|
10706
|
+
const md = buildMarkdown(spec.content || analysisText || "", spec.title, spec.description);
|
|
10707
|
+
if (md) {
|
|
10708
|
+
out.push(md);
|
|
10709
|
+
if (componentStreamCallback) componentStreamCallback(md);
|
|
10710
|
+
}
|
|
10711
|
+
continue;
|
|
10712
|
+
}
|
|
10713
|
+
const q = byId.get(spec.sourceRef) || queries.find((x) => x.virtual);
|
|
10714
|
+
if (!q || !q.data || q.data.length === 0) {
|
|
10715
|
+
logger.info(`[ScriptComponentGen] Recipe spec source "${spec.sourceRef}" missing/empty on replay \u2014 falling back to LLM`);
|
|
10716
|
+
return null;
|
|
10717
|
+
}
|
|
10718
|
+
const template = availableComponents.find((c) => c.name === spec.componentType || c.id === spec.componentType);
|
|
10719
|
+
if (!template) {
|
|
10720
|
+
logger.info(`[ScriptComponentGen] Recipe spec component "${spec.componentType}" not in library \u2014 falling back to LLM`);
|
|
10721
|
+
return null;
|
|
10722
|
+
}
|
|
10723
|
+
const cols = inferColumns(q.data);
|
|
10724
|
+
const comp = { props: { title: spec.title, description: spec.description, config: { ...spec.config || {} } } };
|
|
10725
|
+
const repaired = validateAndRepairComponent(comp, template.type, cols, q.count ?? q.data.length);
|
|
10726
|
+
if (!repaired) {
|
|
10727
|
+
logger.info(`[ScriptComponentGen] Recipe spec "${spec.componentType}" no longer maps to columns (shape drift) \u2014 falling back to LLM`);
|
|
10728
|
+
return null;
|
|
10729
|
+
}
|
|
10730
|
+
const queryId = queryIds[q.sourceId] || queryCache.storeQuery(q.sql, { data: q.data, count: q.count });
|
|
10731
|
+
const toolId = q.virtual ? "script_dataset" : q.sourceId;
|
|
10732
|
+
const component = {
|
|
10733
|
+
id: `comp_${Math.random().toString(36).substring(2, 8)}`,
|
|
10734
|
+
name: template.name,
|
|
10735
|
+
displayName: template.displayName,
|
|
10736
|
+
type: template.type,
|
|
10737
|
+
description: template.description,
|
|
10738
|
+
props: {
|
|
10739
|
+
...comp.props,
|
|
10740
|
+
externalTool: { toolId, toolName: q.sourceName, parameters: { queryId } }
|
|
10741
|
+
},
|
|
10742
|
+
category: template.category,
|
|
10743
|
+
keywords: template.keywords
|
|
10744
|
+
};
|
|
10745
|
+
out.push(component);
|
|
10746
|
+
if (componentStreamCallback && !answerComponentStreamed) {
|
|
10747
|
+
componentStreamCallback(component);
|
|
10748
|
+
answerComponentStreamed = true;
|
|
10015
10749
|
}
|
|
10016
10750
|
}
|
|
10017
|
-
if (
|
|
10018
|
-
|
|
10019
|
-
|
|
10020
|
-
|
|
10021
|
-
|
|
10022
|
-
|
|
10023
|
-
});
|
|
10024
|
-
return result;
|
|
10025
|
-
} catch (err) {
|
|
10026
|
-
logger.warn(`[ScriptComponentGen] Federation via collections failed: ${err}`);
|
|
10751
|
+
if (out.length > 0 && !out.some((c) => c.type === "MarkdownBlock")) {
|
|
10752
|
+
const md = buildMarkdown(analysisText || "");
|
|
10753
|
+
if (md) {
|
|
10754
|
+
out.push(md);
|
|
10755
|
+
if (componentStreamCallback) componentStreamCallback(md);
|
|
10756
|
+
logger.info("[ScriptComponentGen] Injected markdown on replay (recipe had no markdown spec)");
|
|
10027
10757
|
}
|
|
10028
10758
|
}
|
|
10029
|
-
|
|
10759
|
+
logger.info(`[ScriptComponentGen] Deterministically assembled ${out.length} component(s) from recipe \u2014 no LLM call`);
|
|
10760
|
+
return out.length > 0 ? out : null;
|
|
10030
10761
|
}
|
|
10031
10762
|
|
|
10032
10763
|
// src/userResponse/anthropic.ts
|
|
@@ -11994,6 +12725,24 @@ var OpenAILLM = class extends BaseLLM {
|
|
|
11994
12725
|
var openaiLLM = new OpenAILLM();
|
|
11995
12726
|
|
|
11996
12727
|
// src/userResponse/agent-user-response.ts
|
|
12728
|
+
function hasExpiredScriptDataset(component) {
|
|
12729
|
+
if (!component || typeof component !== "object") return false;
|
|
12730
|
+
const et = component?.props?.externalTool;
|
|
12731
|
+
if (et?.toolId === "script_dataset") {
|
|
12732
|
+
const qid = et?.parameters?.queryId;
|
|
12733
|
+
if (typeof qid === "string" && qid.length > 0) {
|
|
12734
|
+
const stored = queryCache.getQuery(qid);
|
|
12735
|
+
if (!stored || stored.data == null) return true;
|
|
12736
|
+
}
|
|
12737
|
+
}
|
|
12738
|
+
const children = component?.props?.config?.components;
|
|
12739
|
+
if (Array.isArray(children)) {
|
|
12740
|
+
for (const child of children) {
|
|
12741
|
+
if (hasExpiredScriptDataset(child)) return true;
|
|
12742
|
+
}
|
|
12743
|
+
}
|
|
12744
|
+
return false;
|
|
12745
|
+
}
|
|
11997
12746
|
function rehydrateCachedComponent(component) {
|
|
11998
12747
|
const qMap = component?.props?.config?._queryMap;
|
|
11999
12748
|
if (!qMap || typeof qMap !== "object" || Object.keys(qMap).length === 0) return component;
|
|
@@ -12043,7 +12792,24 @@ function getLLMInstance(provider) {
|
|
|
12043
12792
|
return anthropicLLM;
|
|
12044
12793
|
}
|
|
12045
12794
|
}
|
|
12046
|
-
|
|
12795
|
+
function storeScriptDatasetQuery(q, ctx) {
|
|
12796
|
+
const data = q.data.length > MAX_ROWS_RENDERED ? q.data.slice(0, MAX_ROWS_RENDERED) : q.data;
|
|
12797
|
+
if (q.data.length > MAX_ROWS_RENDERED) {
|
|
12798
|
+
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.`);
|
|
12799
|
+
}
|
|
12800
|
+
const count = data.length;
|
|
12801
|
+
if (q.virtual && ctx.recipeId) {
|
|
12802
|
+
const descriptor = {
|
|
12803
|
+
kind: "script_dataset",
|
|
12804
|
+
recipeId: ctx.recipeId,
|
|
12805
|
+
params: ctx.params || {},
|
|
12806
|
+
datasetRef: q.sourceId
|
|
12807
|
+
};
|
|
12808
|
+
return queryCache.storeQuery(JSON.stringify(descriptor), { data, count });
|
|
12809
|
+
}
|
|
12810
|
+
return queryCache.storeQuery(q.sql, { data, count });
|
|
12811
|
+
}
|
|
12812
|
+
var get_agent_user_response = async (prompt, components, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, conversationHistory, streamCallback, collections, externalTools, userId, mainAgentModel, sourceAgentModel, workflows) => {
|
|
12047
12813
|
const startTime = Date.now();
|
|
12048
12814
|
const providers = llmProviders || ["anthropic"];
|
|
12049
12815
|
const provider = providers[0];
|
|
@@ -12052,19 +12818,19 @@ var get_agent_user_response = async (prompt, components, anthropicApiKey, groqAp
|
|
|
12052
12818
|
logger.logLLMPrompt("agentUserResponse", "user", `User Prompt: ${prompt}`);
|
|
12053
12819
|
logger.info(`[AgentFlow] Starting | provider: ${provider} | prompt: "${prompt.substring(0, 50)}..."`);
|
|
12054
12820
|
try {
|
|
12055
|
-
const conversationMatch = await conversation_search_default.
|
|
12821
|
+
const conversationMatch = await conversation_search_default.findExactMatch({
|
|
12056
12822
|
userPrompt: prompt,
|
|
12057
12823
|
collections,
|
|
12058
|
-
userId
|
|
12059
|
-
similarityThreshold: EXACT_MATCH_SIMILARITY_THRESHOLD
|
|
12824
|
+
userId
|
|
12060
12825
|
});
|
|
12061
12826
|
if (conversationMatch) {
|
|
12062
|
-
logger.info(`[AgentFlow] Found matching conversation (${(conversationMatch.similarity * 100).toFixed(2)}% similarity)`);
|
|
12063
12827
|
const rawComponent = conversationMatch.uiBlock?.component || conversationMatch.uiBlock?.generatedComponentMetadata;
|
|
12064
12828
|
const isValidComponent = rawComponent && typeof rawComponent === "object" && Object.keys(rawComponent).length > 0;
|
|
12065
12829
|
const component = isValidComponent ? rawComponent : null;
|
|
12066
12830
|
const cachedTextResponse = conversationMatch.uiBlock?.analysis || conversationMatch.uiBlock?.textResponse || conversationMatch.uiBlock?.text || "";
|
|
12067
|
-
if (
|
|
12831
|
+
if (component && hasExpiredScriptDataset(component)) {
|
|
12832
|
+
logger.info(`[AgentFlow] Exact match found but cached UIBlock has expired script_dataset \u2014 falling through to script matcher`);
|
|
12833
|
+
} else {
|
|
12068
12834
|
if (streamCallback && cachedTextResponse) {
|
|
12069
12835
|
streamCallback(cachedTextResponse);
|
|
12070
12836
|
}
|
|
@@ -12078,16 +12844,14 @@ var get_agent_user_response = async (prompt, components, anthropicApiKey, groqAp
|
|
|
12078
12844
|
component: rehydratedComponent,
|
|
12079
12845
|
actions: conversationMatch.uiBlock?.actions || [],
|
|
12080
12846
|
reasoning: `Exact match from previous conversation`,
|
|
12081
|
-
method: `${provider}-agent-
|
|
12082
|
-
semanticSimilarity:
|
|
12847
|
+
method: `${provider}-agent-exact-match`,
|
|
12848
|
+
semanticSimilarity: 1
|
|
12083
12849
|
},
|
|
12084
12850
|
errors: []
|
|
12085
12851
|
};
|
|
12086
12852
|
}
|
|
12087
|
-
logger.info(`[AgentFlow] Similar match but below exact threshold \u2014 proceeding with agent`);
|
|
12088
|
-
} else {
|
|
12089
|
-
logger.info(`[AgentFlow] No matching conversations \u2014 proceeding with agent`);
|
|
12090
12853
|
}
|
|
12854
|
+
logger.info(`[AgentFlow] No exact match \u2014 proceeding with agent`);
|
|
12091
12855
|
const apiKey = (() => {
|
|
12092
12856
|
switch (provider) {
|
|
12093
12857
|
case "anthropic":
|
|
@@ -12152,11 +12916,11 @@ var get_agent_user_response = async (prompt, components, anthropicApiKey, groqAp
|
|
|
12152
12916
|
projectId
|
|
12153
12917
|
};
|
|
12154
12918
|
const streamBuffer = new StreamBuffer(streamCallback);
|
|
12155
|
-
const scriptStore = new ScriptStore();
|
|
12919
|
+
const scriptStore = new ScriptStore({ collections, projectId });
|
|
12156
12920
|
const turnId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
12157
12921
|
const FORK_DEPTH_CAP = 3;
|
|
12158
12922
|
let forkContext;
|
|
12159
|
-
if (scriptStore.count() > 0) {
|
|
12923
|
+
if (await scriptStore.count() > 0) {
|
|
12160
12924
|
const scriptMatcher = new ScriptMatcher(scriptStore);
|
|
12161
12925
|
const scriptMatch = await scriptMatcher.match(prompt, apiKey, sourceAgentModel);
|
|
12162
12926
|
if (scriptMatch && scriptMatch.tier === "near") {
|
|
@@ -12190,7 +12954,7 @@ var get_agent_user_response = async (prompt, components, anthropicApiKey, groqAp
|
|
|
12190
12954
|
{ externalTools: agentTools, streamBuffer }
|
|
12191
12955
|
);
|
|
12192
12956
|
if (scriptResult.success && scriptResult.data.length > 0) {
|
|
12193
|
-
scriptStore.recordSuccess(scriptMatch.recipe.id);
|
|
12957
|
+
await scriptStore.recordSuccess(scriptMatch.recipe.id);
|
|
12194
12958
|
const dataContext = scriptResult.executedQueries.map((q) => {
|
|
12195
12959
|
const preview = q.data.slice(0, 15);
|
|
12196
12960
|
return `## Data from "${q.sourceName}"
|
|
@@ -12228,7 +12992,7 @@ ${JSON.stringify(preview, null, 2)}
|
|
|
12228
12992
|
result: {
|
|
12229
12993
|
_totalRecords: q.totalCount || q.count,
|
|
12230
12994
|
_recordsShown: q.data.length,
|
|
12231
|
-
_sampleData: q.data.slice(0,
|
|
12995
|
+
_sampleData: q.data.slice(0, TOOL_TRACKING_SAMPLE_ROWS)
|
|
12232
12996
|
}
|
|
12233
12997
|
}));
|
|
12234
12998
|
let matchedComponents2 = [];
|
|
@@ -12242,19 +13006,31 @@ ${JSON.stringify(preview, null, 2)}
|
|
|
12242
13006
|
streamBuffer.write("__TEXT_COMPLETE__COMPONENT_GENERATION_START__");
|
|
12243
13007
|
streamBuffer.flush();
|
|
12244
13008
|
}
|
|
12245
|
-
const componentStreamCallback =
|
|
12246
|
-
const answerMarker = `__ANSWER_COMPONENT_START__${JSON.stringify(component)}__ANSWER_COMPONENT_END__`;
|
|
12247
|
-
streamBuffer.write(answerMarker);
|
|
12248
|
-
streamBuffer.flush();
|
|
12249
|
-
} : void 0;
|
|
13009
|
+
const componentStreamCallback = void 0;
|
|
12250
13010
|
const scriptQueryIds = {};
|
|
12251
13011
|
for (const q of scriptResult.executedQueries) {
|
|
12252
|
-
scriptQueryIds[q.sourceId] =
|
|
12253
|
-
|
|
12254
|
-
|
|
13012
|
+
scriptQueryIds[q.sourceId] = storeScriptDatasetQuery(q, {
|
|
13013
|
+
recipeId: scriptMatch.recipe.id,
|
|
13014
|
+
params: scriptMatch.extractedParams || {}
|
|
12255
13015
|
});
|
|
12256
13016
|
}
|
|
12257
|
-
|
|
13017
|
+
let usedRecipeComponents = false;
|
|
13018
|
+
if (scriptMatch.recipe.components && scriptMatch.recipe.components.length > 0) {
|
|
13019
|
+
const assembled = assembleComponentsFromSpecs({
|
|
13020
|
+
specs: scriptMatch.recipe.components,
|
|
13021
|
+
scriptResult,
|
|
13022
|
+
queryIds: scriptQueryIds,
|
|
13023
|
+
availableComponents: components,
|
|
13024
|
+
componentStreamCallback,
|
|
13025
|
+
analysisText: scriptTextResponse
|
|
13026
|
+
});
|
|
13027
|
+
if (assembled && assembled.length > 0) {
|
|
13028
|
+
matchedComponents2 = assembled;
|
|
13029
|
+
layoutTitle2 = scriptMatch.recipe.name || layoutTitle2;
|
|
13030
|
+
usedRecipeComponents = true;
|
|
13031
|
+
}
|
|
13032
|
+
}
|
|
13033
|
+
if (!usedRecipeComponents) try {
|
|
12258
13034
|
const compResult = await generateScriptComponents({
|
|
12259
13035
|
userPrompt: prompt,
|
|
12260
13036
|
scriptResult,
|
|
@@ -12264,13 +13040,22 @@ ${JSON.stringify(preview, null, 2)}
|
|
|
12264
13040
|
model: sourceAgentModel,
|
|
12265
13041
|
componentStreamCallback,
|
|
12266
13042
|
externalTools: agentTools,
|
|
12267
|
-
collections
|
|
13043
|
+
collections,
|
|
13044
|
+
analysisText: scriptTextResponse
|
|
12268
13045
|
});
|
|
12269
13046
|
if (compResult.components.length > 0) {
|
|
12270
13047
|
matchedComponents2 = compResult.components;
|
|
12271
13048
|
layoutTitle2 = compResult.layoutTitle;
|
|
12272
13049
|
layoutDescription2 = compResult.layoutDescription;
|
|
12273
13050
|
actions2 = compResult.actions;
|
|
13051
|
+
if (compResult.componentSpecs.length > 0) {
|
|
13052
|
+
try {
|
|
13053
|
+
scriptMatch.recipe.components = compResult.componentSpecs;
|
|
13054
|
+
await scriptStore.save(scriptMatch.recipe);
|
|
13055
|
+
} catch (e) {
|
|
13056
|
+
logger.warn(`[AgentFlow] Failed to backfill component recipe: ${e}`);
|
|
13057
|
+
}
|
|
13058
|
+
}
|
|
12274
13059
|
} else {
|
|
12275
13060
|
logger.info(`[AgentFlow] Lightweight component gen returned no components \u2014 falling back to full generator`);
|
|
12276
13061
|
const matchResult = await generateAgentComponents({
|
|
@@ -12363,12 +13148,12 @@ ${JSON.stringify(preview, null, 2)}
|
|
|
12363
13148
|
};
|
|
12364
13149
|
} else {
|
|
12365
13150
|
logger.warn(`[AgentFlow] Script "${scriptMatch.recipe.name}" failed: ${scriptResult.error || "no data returned"}`);
|
|
12366
|
-
scriptStore.recordFailure(scriptMatch.recipe.id);
|
|
13151
|
+
await scriptStore.recordFailure(scriptMatch.recipe.id);
|
|
12367
13152
|
}
|
|
12368
13153
|
} catch (scriptErr) {
|
|
12369
13154
|
const errMsg = scriptErr instanceof Error ? scriptErr.message : String(scriptErr);
|
|
12370
13155
|
logger.warn(`[AgentFlow] Script execution error (falling back to agent flow): ${errMsg}`);
|
|
12371
|
-
scriptStore.recordFailure(scriptMatch.recipe.id);
|
|
13156
|
+
await scriptStore.recordFailure(scriptMatch.recipe.id);
|
|
12372
13157
|
}
|
|
12373
13158
|
}
|
|
12374
13159
|
} else {
|
|
@@ -12425,13 +13210,35 @@ User question: ${prompt}`;
|
|
|
12425
13210
|
logger.warn(`[AgentFlow] Source routing pre-filter failed (using all tools): ${msg}`);
|
|
12426
13211
|
}
|
|
12427
13212
|
}
|
|
12428
|
-
const mainAgent = new MainAgent(filteredTools, agentConfig, scriptStore, turnId, streamBuffer);
|
|
13213
|
+
const mainAgent = new MainAgent(filteredTools, agentConfig, scriptStore, turnId, streamBuffer, workflows || []);
|
|
12429
13214
|
const agentResponse = await mainAgent.handleQuestion(
|
|
12430
13215
|
effectivePrompt,
|
|
12431
13216
|
apiKey,
|
|
12432
13217
|
conversationHistory,
|
|
12433
13218
|
streamBuffer.hasCallback() ? (chunk) => streamBuffer.write(chunk) : void 0
|
|
12434
13219
|
);
|
|
13220
|
+
if (agentResponse.workflow) {
|
|
13221
|
+
streamBuffer.flush();
|
|
13222
|
+
const workflowComponent = {
|
|
13223
|
+
id: `workflow_${Date.now()}`,
|
|
13224
|
+
name: agentResponse.workflow.name,
|
|
13225
|
+
type: `Workflow_${agentResponse.workflow.name}`,
|
|
13226
|
+
description: `Workflow: ${agentResponse.workflow.name}`,
|
|
13227
|
+
props: agentResponse.workflow.props
|
|
13228
|
+
};
|
|
13229
|
+
const elapsedTime2 = Date.now() - startTime;
|
|
13230
|
+
logger.info(`[AgentFlow] Workflow short-circuit | "${agentResponse.workflow.name}" | ${elapsedTime2}ms`);
|
|
13231
|
+
return {
|
|
13232
|
+
success: true,
|
|
13233
|
+
data: {
|
|
13234
|
+
text: "",
|
|
13235
|
+
component: workflowComponent,
|
|
13236
|
+
actions: [],
|
|
13237
|
+
method: `${provider}-agent-workflow`
|
|
13238
|
+
},
|
|
13239
|
+
errors: []
|
|
13240
|
+
};
|
|
13241
|
+
}
|
|
12435
13242
|
const rawText = streamBuffer.getFullText() || agentResponse.text || "I apologize, but I was unable to generate a response.";
|
|
12436
13243
|
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, "");
|
|
12437
13244
|
streamBuffer.flush();
|
|
@@ -12471,6 +13278,7 @@ User question: ${prompt}`;
|
|
|
12471
13278
|
let layoutTitle = "Dashboard";
|
|
12472
13279
|
let layoutDescription = "Multi-component dashboard";
|
|
12473
13280
|
let actions = [];
|
|
13281
|
+
let authoredComponentSpecs = [];
|
|
12474
13282
|
if (!hasExecutedTools) {
|
|
12475
13283
|
logger.info(`[AgentFlow] No tools executed \u2014 general question, wrapping in DynamicMarkdownBlock`);
|
|
12476
13284
|
const mainAgentText = agentResponse.text || textResponse;
|
|
@@ -12518,9 +13326,9 @@ User question: ${prompt}`;
|
|
|
12518
13326
|
logger.info(`[AgentFlow] Using savedScript.executedQueries (${agentResponse.savedScript.executedQueries.length} entries, includes computed columns) for component gen`);
|
|
12519
13327
|
const scriptQueries = agentResponse.savedScript.executedQueries;
|
|
12520
13328
|
for (const q of scriptQueries) {
|
|
12521
|
-
agentQueryIds[q.sourceId] =
|
|
12522
|
-
|
|
12523
|
-
|
|
13329
|
+
agentQueryIds[q.sourceId] = storeScriptDatasetQuery(q, {
|
|
13330
|
+
recipeId: agentResponse.savedScript.recipeId,
|
|
13331
|
+
params: {}
|
|
12524
13332
|
});
|
|
12525
13333
|
}
|
|
12526
13334
|
agentScriptResult = {
|
|
@@ -12558,7 +13366,7 @@ User question: ${prompt}`;
|
|
|
12558
13366
|
executionTimeMs: 0
|
|
12559
13367
|
};
|
|
12560
13368
|
}
|
|
12561
|
-
const
|
|
13369
|
+
const compGenArgs = {
|
|
12562
13370
|
userPrompt: prompt,
|
|
12563
13371
|
scriptResult: agentScriptResult,
|
|
12564
13372
|
queryIds: agentQueryIds,
|
|
@@ -12567,13 +13375,29 @@ User question: ${prompt}`;
|
|
|
12567
13375
|
model: sourceAgentModel,
|
|
12568
13376
|
componentStreamCallback,
|
|
12569
13377
|
externalTools: agentTools,
|
|
12570
|
-
collections
|
|
12571
|
-
|
|
12572
|
-
|
|
13378
|
+
collections,
|
|
13379
|
+
analysisText: agentResponse.text
|
|
13380
|
+
};
|
|
13381
|
+
const MAX_COMP_ATTEMPTS = 3;
|
|
13382
|
+
let compResult;
|
|
13383
|
+
for (let attempt = 1; attempt <= MAX_COMP_ATTEMPTS; attempt++) {
|
|
13384
|
+
try {
|
|
13385
|
+
compResult = await generateScriptComponents(compGenArgs);
|
|
13386
|
+
break;
|
|
13387
|
+
} catch (err) {
|
|
13388
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
13389
|
+
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);
|
|
13390
|
+
if (!transient || attempt === MAX_COMP_ATTEMPTS) throw err;
|
|
13391
|
+
logger.warn(`[AgentFlow] Lightweight component gen transient error (attempt ${attempt}/${MAX_COMP_ATTEMPTS}) \u2014 retrying: ${msg}`);
|
|
13392
|
+
await new Promise((res) => setTimeout(res, 400 * attempt));
|
|
13393
|
+
}
|
|
13394
|
+
}
|
|
13395
|
+
if (compResult && compResult.components.length > 0) {
|
|
12573
13396
|
matchedComponents = compResult.components;
|
|
12574
13397
|
layoutTitle = compResult.layoutTitle;
|
|
12575
13398
|
layoutDescription = compResult.layoutDescription;
|
|
12576
13399
|
actions = compResult.actions;
|
|
13400
|
+
authoredComponentSpecs = compResult.componentSpecs;
|
|
12577
13401
|
usedLightweight = true;
|
|
12578
13402
|
logger.info(`[AgentFlow] Lightweight component gen succeeded \u2014 ${matchedComponents.length} components`);
|
|
12579
13403
|
}
|
|
@@ -12676,10 +13500,11 @@ User question: ${prompt}`;
|
|
|
12676
13500
|
forkDepth: Math.min(FORK_DEPTH_CAP, forkContext.parentDepth + 1),
|
|
12677
13501
|
forkReason: forkContext.modificationHint
|
|
12678
13502
|
} : void 0;
|
|
12679
|
-
const promoted = scriptStore.promoteToVerified(authored.recipeId, {
|
|
13503
|
+
const promoted = await scriptStore.promoteToVerified(authored.recipeId, {
|
|
12680
13504
|
sourceIds: authored.sourceIds,
|
|
12681
13505
|
tables: authored.tables,
|
|
12682
|
-
...lineage
|
|
13506
|
+
...lineage,
|
|
13507
|
+
components: authoredComponentSpecs
|
|
12683
13508
|
});
|
|
12684
13509
|
if (promoted && lineage?.parentId) {
|
|
12685
13510
|
logger.info(
|
|
@@ -12915,7 +13740,7 @@ var CONTEXT_CONFIG = {
|
|
|
12915
13740
|
};
|
|
12916
13741
|
|
|
12917
13742
|
// src/handlers/user-prompt-request.ts
|
|
12918
|
-
var get_user_request = async (data, components, sendMessage, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, externalTools, mainAgentModel, sourceAgentModel) => {
|
|
13743
|
+
var get_user_request = async (data, components, sendMessage, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, externalTools, mainAgentModel, sourceAgentModel, workflows) => {
|
|
12919
13744
|
const errors = [];
|
|
12920
13745
|
const parseResult = UserPromptRequestMessageSchema.safeParse(data);
|
|
12921
13746
|
if (!parseResult.success) {
|
|
@@ -13001,7 +13826,8 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
|
|
|
13001
13826
|
externalTools,
|
|
13002
13827
|
userId,
|
|
13003
13828
|
mainAgentModel,
|
|
13004
|
-
sourceAgentModel
|
|
13829
|
+
sourceAgentModel,
|
|
13830
|
+
workflows
|
|
13005
13831
|
);
|
|
13006
13832
|
logger.info("User prompt request completed");
|
|
13007
13833
|
const uiBlockId = existingUiBlockId;
|
|
@@ -13154,8 +13980,8 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
|
|
|
13154
13980
|
wsId
|
|
13155
13981
|
};
|
|
13156
13982
|
};
|
|
13157
|
-
async function handleUserPromptRequest(data, components, sendMessage, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, externalTools, mainAgentModel, sourceAgentModel) {
|
|
13158
|
-
const response = await get_user_request(data, components, sendMessage, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, externalTools, mainAgentModel, sourceAgentModel);
|
|
13983
|
+
async function handleUserPromptRequest(data, components, sendMessage, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, externalTools, mainAgentModel, sourceAgentModel, workflows) {
|
|
13984
|
+
const response = await get_user_request(data, components, sendMessage, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, externalTools, mainAgentModel, sourceAgentModel, workflows);
|
|
13159
13985
|
if (response.data?.component?.props?.config?.components) {
|
|
13160
13986
|
response.data.component.props.config.components = response.data.component.props.config.components.map((comp) => ({
|
|
13161
13987
|
...comp,
|
|
@@ -13587,6 +14413,24 @@ async function handleComponentListResponse(data, storeComponents, collections) {
|
|
|
13587
14413
|
}
|
|
13588
14414
|
}
|
|
13589
14415
|
|
|
14416
|
+
// src/handlers/workflow-list-response.ts
|
|
14417
|
+
async function handleWorkflowListResponse(data, storeWorkflows) {
|
|
14418
|
+
try {
|
|
14419
|
+
const parsed = WorkflowListResponseMessageSchema.parse(data);
|
|
14420
|
+
const { payload } = parsed;
|
|
14421
|
+
const workflowsList = payload.workflows;
|
|
14422
|
+
if (!workflowsList) {
|
|
14423
|
+
logger.error("Workflows list not found in WORKFLOW_LIST_RES payload");
|
|
14424
|
+
return;
|
|
14425
|
+
}
|
|
14426
|
+
const workflows = WorkflowsSchema.parse(workflowsList);
|
|
14427
|
+
storeWorkflows(workflows);
|
|
14428
|
+
logger.info(`Stored ${workflows.length} workflow descriptor(s) from frontend`);
|
|
14429
|
+
} catch (error) {
|
|
14430
|
+
logger.error("Failed to handle workflow list response:", error);
|
|
14431
|
+
}
|
|
14432
|
+
}
|
|
14433
|
+
|
|
13590
14434
|
// src/handlers/users.ts
|
|
13591
14435
|
async function handleUsersRequest(data, collections, sendMessage) {
|
|
13592
14436
|
const executeCollection = async (collection, op, params) => {
|
|
@@ -16403,17 +17247,45 @@ function formatComponentsForPrompt(components) {
|
|
|
16403
17247
|
Props Structure: ${propsPreview}`;
|
|
16404
17248
|
}).join("\n\n");
|
|
16405
17249
|
}
|
|
17250
|
+
function classifyTool(tool) {
|
|
17251
|
+
if (tool.toolType === "direct") return "direct";
|
|
17252
|
+
if (tool.id.endsWith("_query")) return "sql";
|
|
17253
|
+
if (tool.id.endsWith("_call")) return "rest";
|
|
17254
|
+
if (tool.id.endsWith("_graphql")) return "graphql";
|
|
17255
|
+
return "direct";
|
|
17256
|
+
}
|
|
16406
17257
|
function formatToolsForPrompt(tools) {
|
|
16407
17258
|
if (!tools || tools.length === 0) {
|
|
16408
17259
|
return "No external tools available.";
|
|
16409
17260
|
}
|
|
16410
|
-
|
|
17261
|
+
const directTools = [];
|
|
17262
|
+
const sourceTools = [];
|
|
17263
|
+
for (const t of tools) {
|
|
17264
|
+
(classifyTool(t) === "direct" ? directTools : sourceTools).push(t);
|
|
17265
|
+
}
|
|
17266
|
+
const renderTool = (tool, idx, label) => {
|
|
16411
17267
|
const paramsStr = Object.entries(tool.params || {}).map(([key, type]) => `${key}: ${type}`).join(", ");
|
|
16412
|
-
return `${idx + 1}. ID: ${tool.id}
|
|
17268
|
+
return `${idx + 1}. [${label}] ID: ${tool.id}
|
|
16413
17269
|
Name: ${tool.name}
|
|
16414
17270
|
Description: ${tool.description}
|
|
16415
17271
|
Parameters: { ${paramsStr} }`;
|
|
16416
|
-
}
|
|
17272
|
+
};
|
|
17273
|
+
const sections = [];
|
|
17274
|
+
if (directTools.length > 0) {
|
|
17275
|
+
const body = directTools.map((t, i) => renderTool(t, i, "DIRECT \u2014 PREFER")).join("\n\n");
|
|
17276
|
+
sections.push(`### Direct Tools (try these first \u2014 they answer most questions without SQL)
|
|
17277
|
+
${body}`);
|
|
17278
|
+
}
|
|
17279
|
+
if (sourceTools.length > 0) {
|
|
17280
|
+
const body = sourceTools.map((t, i) => {
|
|
17281
|
+
const kind = classifyTool(t);
|
|
17282
|
+
const label = kind === "sql" ? "SQL" : kind === "rest" ? "REST" : "GRAPHQL";
|
|
17283
|
+
return renderTool(t, i, label);
|
|
17284
|
+
}).join("\n\n");
|
|
17285
|
+
sections.push(`### Source Tools (fallback \u2014 use only when no direct tool fits)
|
|
17286
|
+
${body}`);
|
|
17287
|
+
}
|
|
17288
|
+
return sections.join("\n\n");
|
|
16417
17289
|
}
|
|
16418
17290
|
function formatExistingComponentsForPrompt(existingComponents) {
|
|
16419
17291
|
if (!existingComponents || existingComponents.length === 0) {
|
|
@@ -16438,7 +17310,7 @@ function sendDashCompResponse(id, res, sendMessage, clientId) {
|
|
|
16438
17310
|
}
|
|
16439
17311
|
|
|
16440
17312
|
// src/dashComp/pick-component.ts
|
|
16441
|
-
async function pickComponentWithLLM(prompt, components, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, tools, dashCompModels, conversationHistory) {
|
|
17313
|
+
async function pickComponentWithLLM(prompt, components, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, tools, dashCompModels, conversationHistory, userId) {
|
|
16442
17314
|
const errors = [];
|
|
16443
17315
|
const availableComponentsText = formatComponentsForPrompt(components);
|
|
16444
17316
|
const availableToolsText = formatToolsForPrompt(tools);
|
|
@@ -16454,6 +17326,25 @@ async function pickComponentWithLLM(prompt, components, anthropicApiKey, groqApi
|
|
|
16454
17326
|
schemaDoc = schema.generateSchemaDocumentation();
|
|
16455
17327
|
}
|
|
16456
17328
|
const databaseRules = await promptLoader.loadDatabaseRules();
|
|
17329
|
+
let globalKnowledgeBase = "No global knowledge base available.";
|
|
17330
|
+
let knowledgeBaseContext = "No additional knowledge base context available.";
|
|
17331
|
+
if (collections) {
|
|
17332
|
+
const kbResult = await knowledge_base_default.getAllKnowledgeBase({
|
|
17333
|
+
prompt,
|
|
17334
|
+
collections,
|
|
17335
|
+
userId,
|
|
17336
|
+
topK: KNOWLEDGE_BASE_TOP_K
|
|
17337
|
+
});
|
|
17338
|
+
globalKnowledgeBase = kbResult.globalContext || globalKnowledgeBase;
|
|
17339
|
+
const dynamicParts = [];
|
|
17340
|
+
if (kbResult.userContext) {
|
|
17341
|
+
dynamicParts.push("## User-Specific Knowledge Base\n" + kbResult.userContext);
|
|
17342
|
+
}
|
|
17343
|
+
if (kbResult.queryContext) {
|
|
17344
|
+
dynamicParts.push("## Relevant Knowledge Base (Query-Matched)\n" + kbResult.queryContext);
|
|
17345
|
+
}
|
|
17346
|
+
knowledgeBaseContext = dynamicParts.join("\n\n") || knowledgeBaseContext;
|
|
17347
|
+
}
|
|
16457
17348
|
const prompts = await promptLoader.loadPrompts("dash-comp-picker", {
|
|
16458
17349
|
USER_PROMPT: prompt,
|
|
16459
17350
|
AVAILABLE_COMPONENTS: availableComponentsText,
|
|
@@ -16461,8 +17352,12 @@ async function pickComponentWithLLM(prompt, components, anthropicApiKey, groqApi
|
|
|
16461
17352
|
DATABASE_RULES: databaseRules,
|
|
16462
17353
|
AVAILABLE_TOOLS: availableToolsText,
|
|
16463
17354
|
CURRENT_DATETIME: getCurrentDateTimeForPrompt(),
|
|
16464
|
-
CONVERSATION_HISTORY: conversationHistory || "No previous conversation"
|
|
17355
|
+
CONVERSATION_HISTORY: conversationHistory || "No previous conversation",
|
|
17356
|
+
GLOBAL_KNOWLEDGE_BASE: globalKnowledgeBase,
|
|
17357
|
+
KNOWLEDGE_BASE_CONTEXT: knowledgeBaseContext
|
|
16465
17358
|
});
|
|
17359
|
+
logger.logLLMPrompt("dashCompPicker", "system", extractPromptText(prompts.system));
|
|
17360
|
+
logger.logLLMPrompt("dashCompPicker", "user", prompts.user);
|
|
16466
17361
|
logger.debug("[DASH_COMP_REQ] Loaded dash-comp-picker prompts with schema and tools");
|
|
16467
17362
|
const { apiKey, model } = getApiKeyAndModel(
|
|
16468
17363
|
anthropicApiKey,
|
|
@@ -16828,6 +17723,18 @@ Fixed SQL query:`;
|
|
|
16828
17723
|
logger.info(`[DASH_COMP_REQ] Replaced direct query with queryId: ${queryId}`);
|
|
16829
17724
|
}
|
|
16830
17725
|
finalComponent = { ...finalComponent, props };
|
|
17726
|
+
const ext = finalComponent.props?.externalTool;
|
|
17727
|
+
if (ext?.toolId && !ext?.parameters?.sql) {
|
|
17728
|
+
const matchedTool = tools?.find((t) => t.id === ext.toolId);
|
|
17729
|
+
if (matchedTool && matchedTool.cache !== false) {
|
|
17730
|
+
const match = executedTools.find((t) => t.id === ext.toolId);
|
|
17731
|
+
if (match) {
|
|
17732
|
+
const cacheKey = buildDirectToolCacheKey(ext.toolId, ext.parameters);
|
|
17733
|
+
queryCache.set(cacheKey, { success: true, data: match.result });
|
|
17734
|
+
logger.info(`[DASH_COMP_REQ] Pre-populated et-direct cache for ${ext.toolId}`);
|
|
17735
|
+
}
|
|
17736
|
+
}
|
|
17737
|
+
}
|
|
16831
17738
|
if (parsedResult.props.query) {
|
|
16832
17739
|
logger.info(`[DASH_COMP_REQ] Data source: Database query`);
|
|
16833
17740
|
}
|
|
@@ -16862,7 +17769,7 @@ Fixed SQL query:`;
|
|
|
16862
17769
|
}
|
|
16863
17770
|
|
|
16864
17771
|
// src/dashComp/create-filter.ts
|
|
16865
|
-
async function createFilterWithLLM(prompt, components, existingComponents, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, tools, dashCompModels, collections) {
|
|
17772
|
+
async function createFilterWithLLM(prompt, components, existingComponents, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, tools, dashCompModels, collections, userId) {
|
|
16866
17773
|
const errors = [];
|
|
16867
17774
|
try {
|
|
16868
17775
|
const filterComponents = components.filter((c) => c.type.startsWith("Filter"));
|
|
@@ -16882,6 +17789,25 @@ async function createFilterWithLLM(prompt, components, existingComponents, anthr
|
|
|
16882
17789
|
schemaDoc = schema.generateSchemaDocumentation();
|
|
16883
17790
|
}
|
|
16884
17791
|
const databaseRules = await promptLoader.loadDatabaseRules();
|
|
17792
|
+
let globalKnowledgeBase = "No global knowledge base available.";
|
|
17793
|
+
let knowledgeBaseContext = "No additional knowledge base context available.";
|
|
17794
|
+
if (collections) {
|
|
17795
|
+
const kbResult = await knowledge_base_default.getAllKnowledgeBase({
|
|
17796
|
+
prompt,
|
|
17797
|
+
collections,
|
|
17798
|
+
userId,
|
|
17799
|
+
topK: KNOWLEDGE_BASE_TOP_K
|
|
17800
|
+
});
|
|
17801
|
+
globalKnowledgeBase = kbResult.globalContext || globalKnowledgeBase;
|
|
17802
|
+
const dynamicParts = [];
|
|
17803
|
+
if (kbResult.userContext) {
|
|
17804
|
+
dynamicParts.push("## User-Specific Knowledge Base\n" + kbResult.userContext);
|
|
17805
|
+
}
|
|
17806
|
+
if (kbResult.queryContext) {
|
|
17807
|
+
dynamicParts.push("## Relevant Knowledge Base (Query-Matched)\n" + kbResult.queryContext);
|
|
17808
|
+
}
|
|
17809
|
+
knowledgeBaseContext = dynamicParts.join("\n\n") || knowledgeBaseContext;
|
|
17810
|
+
}
|
|
16885
17811
|
const prompts = await promptLoader.loadPrompts("dash-filter-picker", {
|
|
16886
17812
|
USER_PROMPT: prompt,
|
|
16887
17813
|
AVAILABLE_COMPONENTS: formatComponentsForPrompt(filterComponents),
|
|
@@ -16889,8 +17815,12 @@ async function createFilterWithLLM(prompt, components, existingComponents, anthr
|
|
|
16889
17815
|
SCHEMA_DOC: schemaDoc || "No database schema available",
|
|
16890
17816
|
DATABASE_RULES: databaseRules,
|
|
16891
17817
|
AVAILABLE_TOOLS: formatToolsForPrompt(tools),
|
|
16892
|
-
CURRENT_DATETIME: getCurrentDateTimeForPrompt()
|
|
17818
|
+
CURRENT_DATETIME: getCurrentDateTimeForPrompt(),
|
|
17819
|
+
GLOBAL_KNOWLEDGE_BASE: globalKnowledgeBase,
|
|
17820
|
+
KNOWLEDGE_BASE_CONTEXT: knowledgeBaseContext
|
|
16893
17821
|
});
|
|
17822
|
+
logger.logLLMPrompt("dashFilterPicker", "system", extractPromptText(prompts.system));
|
|
17823
|
+
logger.logLLMPrompt("dashFilterPicker", "user", prompts.user);
|
|
16894
17824
|
logger.debug("[DASH_COMP_REQ:FILTER] Loaded dash-filter-picker prompts");
|
|
16895
17825
|
const { apiKey, model } = getApiKeyAndModel(
|
|
16896
17826
|
anthropicApiKey,
|
|
@@ -16999,7 +17929,35 @@ async function createFilterWithLLM(prompt, components, existingComponents, anthr
|
|
|
16999
17929
|
const updatedComponents = result.updatedComponents || [];
|
|
17000
17930
|
for (const comp of updatedComponents) {
|
|
17001
17931
|
const extTool = comp.props?.externalTool;
|
|
17002
|
-
if (!extTool?.
|
|
17932
|
+
if (!extTool?.toolId) continue;
|
|
17933
|
+
const isDirectTool = !extTool?.parameters?.sql;
|
|
17934
|
+
if (isDirectTool) {
|
|
17935
|
+
const directTool = tools?.find((t) => t.id === extTool.toolId);
|
|
17936
|
+
if (!directTool) {
|
|
17937
|
+
logger.warn(`[DASH_COMP_REQ:FILTER] direct tool ${extTool.toolId} not found in tool registry`);
|
|
17938
|
+
continue;
|
|
17939
|
+
}
|
|
17940
|
+
const declared = new Set(Object.keys(directTool.params || {}));
|
|
17941
|
+
const sentKeys = Object.keys(extTool.parameters || {});
|
|
17942
|
+
const unknown = sentKeys.filter((k) => !declared.has(k));
|
|
17943
|
+
if (unknown.length > 0) {
|
|
17944
|
+
logger.warn(`[DASH_COMP_REQ:FILTER] dropping unknown params on ${extTool.toolId}: ${unknown.join(", ")}`);
|
|
17945
|
+
unknown.forEach((k) => delete extTool.parameters[k]);
|
|
17946
|
+
}
|
|
17947
|
+
if (directTool.cache !== false) {
|
|
17948
|
+
try {
|
|
17949
|
+
const directResult = await directTool.fn(extTool.parameters || {});
|
|
17950
|
+
const cacheKey2 = buildDirectToolCacheKey(extTool.toolId, extTool.parameters);
|
|
17951
|
+
queryCache.set(cacheKey2, { success: true, data: directResult });
|
|
17952
|
+
logger.info(`[DASH_COMP_REQ:FILTER] direct tool ${extTool.toolId} validated and cached for component: ${comp.id}`);
|
|
17953
|
+
} catch (err) {
|
|
17954
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
17955
|
+
logger.warn(`[DASH_COMP_REQ:FILTER] direct tool validation failed for ${extTool.toolId} on component ${comp.id}: ${errMsg}`);
|
|
17956
|
+
}
|
|
17957
|
+
}
|
|
17958
|
+
continue;
|
|
17959
|
+
}
|
|
17960
|
+
if (!extTool?.parameters?.sql) continue;
|
|
17003
17961
|
let sql = extTool.parameters.sql;
|
|
17004
17962
|
const defaultParams = extTool.parameters.params || {};
|
|
17005
17963
|
const toolId = extTool.toolId;
|
|
@@ -17236,7 +18194,8 @@ var processDashCompRequest = async (data, components, _sendMessage, anthropicApi
|
|
|
17236
18194
|
llmProviders,
|
|
17237
18195
|
tools,
|
|
17238
18196
|
dashCompModels,
|
|
17239
|
-
collections
|
|
18197
|
+
collections,
|
|
18198
|
+
userId
|
|
17240
18199
|
);
|
|
17241
18200
|
} else {
|
|
17242
18201
|
llmResponse = await pickComponentWithLLM(
|
|
@@ -17250,7 +18209,8 @@ var processDashCompRequest = async (data, components, _sendMessage, anthropicApi
|
|
|
17250
18209
|
collections,
|
|
17251
18210
|
tools,
|
|
17252
18211
|
dashCompModels,
|
|
17253
|
-
conversationHistory
|
|
18212
|
+
conversationHistory,
|
|
18213
|
+
userId
|
|
17254
18214
|
);
|
|
17255
18215
|
}
|
|
17256
18216
|
if (llmResponse.success && dashboardId && prompt) {
|
|
@@ -17389,7 +18349,7 @@ function sendReportCompResponse(id, res, sendMessage, clientId) {
|
|
|
17389
18349
|
}
|
|
17390
18350
|
|
|
17391
18351
|
// src/reportComp/generate-report.ts
|
|
17392
|
-
async function generateReportComponents(prompt, components, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, tools, modelConfig, conversationHistory) {
|
|
18352
|
+
async function generateReportComponents(prompt, components, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, tools, modelConfig, conversationHistory, userId) {
|
|
17393
18353
|
const errors = [];
|
|
17394
18354
|
const availableComponentsText = formatComponentsForPrompt2(components);
|
|
17395
18355
|
const availableToolsText = formatToolsForPrompt2(tools);
|
|
@@ -17405,6 +18365,25 @@ async function generateReportComponents(prompt, components, anthropicApiKey, gro
|
|
|
17405
18365
|
schemaDoc = schema.generateSchemaDocumentation();
|
|
17406
18366
|
}
|
|
17407
18367
|
const databaseRules = await promptLoader.loadDatabaseRules();
|
|
18368
|
+
let globalKnowledgeBase = "No global knowledge base available.";
|
|
18369
|
+
let knowledgeBaseContext = "No additional knowledge base context available.";
|
|
18370
|
+
if (collections) {
|
|
18371
|
+
const kbResult = await knowledge_base_default.getAllKnowledgeBase({
|
|
18372
|
+
prompt,
|
|
18373
|
+
collections,
|
|
18374
|
+
userId,
|
|
18375
|
+
topK: KNOWLEDGE_BASE_TOP_K
|
|
18376
|
+
});
|
|
18377
|
+
globalKnowledgeBase = kbResult.globalContext || globalKnowledgeBase;
|
|
18378
|
+
const dynamicParts = [];
|
|
18379
|
+
if (kbResult.userContext) {
|
|
18380
|
+
dynamicParts.push("## User-Specific Knowledge Base\n" + kbResult.userContext);
|
|
18381
|
+
}
|
|
18382
|
+
if (kbResult.queryContext) {
|
|
18383
|
+
dynamicParts.push("## Relevant Knowledge Base (Query-Matched)\n" + kbResult.queryContext);
|
|
18384
|
+
}
|
|
18385
|
+
knowledgeBaseContext = dynamicParts.join("\n\n") || knowledgeBaseContext;
|
|
18386
|
+
}
|
|
17408
18387
|
const prompts = await promptLoader.loadPrompts("report-comp-picker", {
|
|
17409
18388
|
USER_PROMPT: prompt,
|
|
17410
18389
|
AVAILABLE_COMPONENTS: availableComponentsText,
|
|
@@ -17412,8 +18391,12 @@ async function generateReportComponents(prompt, components, anthropicApiKey, gro
|
|
|
17412
18391
|
DATABASE_RULES: databaseRules,
|
|
17413
18392
|
AVAILABLE_TOOLS: availableToolsText,
|
|
17414
18393
|
CURRENT_DATETIME: getCurrentDateTimeForPrompt(),
|
|
17415
|
-
CONVERSATION_HISTORY: conversationHistory || "No previous conversation"
|
|
18394
|
+
CONVERSATION_HISTORY: conversationHistory || "No previous conversation",
|
|
18395
|
+
GLOBAL_KNOWLEDGE_BASE: globalKnowledgeBase,
|
|
18396
|
+
KNOWLEDGE_BASE_CONTEXT: knowledgeBaseContext
|
|
17416
18397
|
});
|
|
18398
|
+
logger.logLLMPrompt("reportCompPicker", "system", extractPromptText(prompts.system));
|
|
18399
|
+
logger.logLLMPrompt("reportCompPicker", "user", prompts.user);
|
|
17417
18400
|
logger.debug("[REPORT_COMP_REQ] Loaded report-comp-picker prompts with schema and tools");
|
|
17418
18401
|
const { apiKey, model } = getApiKeyAndModel2(
|
|
17419
18402
|
anthropicApiKey,
|
|
@@ -17638,13 +18621,21 @@ async function validateAllExternalToolQueries(components, collections, tools, mo
|
|
|
17638
18621
|
data: {}
|
|
17639
18622
|
});
|
|
17640
18623
|
if (result?.success !== false && !result?.error) {
|
|
17641
|
-
const
|
|
17642
|
-
const
|
|
18624
|
+
const toolResult = result?.data ?? result;
|
|
18625
|
+
const valueKey = comp.props?.config?.valueKey;
|
|
18626
|
+
const isKpi = comp.type === "KPICard" || comp.name === "DynamicKPICard";
|
|
18627
|
+
let dataArray;
|
|
18628
|
+
if (isKpi && valueKey && toolResult && typeof toolResult === "object" && !Array.isArray(toolResult) && toolResult[valueKey] !== void 0) {
|
|
18629
|
+
dataArray = [toolResult];
|
|
18630
|
+
} else {
|
|
18631
|
+
const resultData = toolResult?.data ?? toolResult ?? [];
|
|
18632
|
+
dataArray = Array.isArray(resultData) ? resultData : [resultData];
|
|
18633
|
+
}
|
|
17643
18634
|
if (!comp.props.config) {
|
|
17644
18635
|
comp.props.config = {};
|
|
17645
18636
|
}
|
|
17646
18637
|
comp.props.config.data = dataArray;
|
|
17647
|
-
logger.info(`[REPORT_COMP_REQ] \u2713 ${comp.name} prefetched ${dataArray.length} rows (non-SQL tool)`);
|
|
18638
|
+
logger.info(`[REPORT_COMP_REQ] \u2713 ${comp.name} prefetched ${dataArray.length} ${dataArray.length === 1 && isKpi ? "aggregate" : "rows"} (non-SQL tool)`);
|
|
17648
18639
|
}
|
|
17649
18640
|
} catch (err) {
|
|
17650
18641
|
logger.warn(`[REPORT_COMP_REQ] \u26A0 ${comp.name} non-SQL prefetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -17784,7 +18775,8 @@ var processReportCompRequest = async (data, components, _sendMessage, anthropicA
|
|
|
17784
18775
|
collections,
|
|
17785
18776
|
tools,
|
|
17786
18777
|
modelConfig,
|
|
17787
|
-
conversationHistory
|
|
18778
|
+
conversationHistory,
|
|
18779
|
+
userId
|
|
17788
18780
|
);
|
|
17789
18781
|
if (llmResponse.success && reportId && prompt) {
|
|
17790
18782
|
const comps = llmResponse.data?.components;
|
|
@@ -17914,7 +18906,7 @@ function getLLMProviders() {
|
|
|
17914
18906
|
// src/auth/user-manager.ts
|
|
17915
18907
|
import fs8 from "fs";
|
|
17916
18908
|
import path8 from "path";
|
|
17917
|
-
import
|
|
18909
|
+
import os2 from "os";
|
|
17918
18910
|
var UserManager = class {
|
|
17919
18911
|
/**
|
|
17920
18912
|
* Initialize UserManager with file path and sync interval
|
|
@@ -17926,7 +18918,7 @@ var UserManager = class {
|
|
|
17926
18918
|
this.hasChanged = false;
|
|
17927
18919
|
this.syncInterval = null;
|
|
17928
18920
|
this.isInitialized = false;
|
|
17929
|
-
this.filePath = path8.join(
|
|
18921
|
+
this.filePath = path8.join(os2.homedir(), ".superatom", "projects", projectId, "users.json");
|
|
17930
18922
|
this.syncIntervalMs = syncIntervalMs;
|
|
17931
18923
|
}
|
|
17932
18924
|
/**
|
|
@@ -18212,7 +19204,7 @@ var UserManager = class {
|
|
|
18212
19204
|
// src/dashboards/dashboard-manager.ts
|
|
18213
19205
|
import fs9 from "fs";
|
|
18214
19206
|
import path9 from "path";
|
|
18215
|
-
import
|
|
19207
|
+
import os3 from "os";
|
|
18216
19208
|
var DashboardManager = class {
|
|
18217
19209
|
/**
|
|
18218
19210
|
* Initialize DashboardManager with project ID
|
|
@@ -18221,7 +19213,7 @@ var DashboardManager = class {
|
|
|
18221
19213
|
constructor(projectId = "snowflake-dataset") {
|
|
18222
19214
|
this.projectId = projectId;
|
|
18223
19215
|
this.dashboardsBasePath = path9.join(
|
|
18224
|
-
|
|
19216
|
+
os3.homedir(),
|
|
18225
19217
|
".superatom",
|
|
18226
19218
|
"projects",
|
|
18227
19219
|
projectId,
|
|
@@ -18379,7 +19371,7 @@ var DashboardManager = class {
|
|
|
18379
19371
|
// src/reports/report-manager.ts
|
|
18380
19372
|
import fs10 from "fs";
|
|
18381
19373
|
import path10 from "path";
|
|
18382
|
-
import
|
|
19374
|
+
import os4 from "os";
|
|
18383
19375
|
var ReportManager = class {
|
|
18384
19376
|
/**
|
|
18385
19377
|
* Initialize ReportManager with project ID
|
|
@@ -18388,7 +19380,7 @@ var ReportManager = class {
|
|
|
18388
19380
|
constructor(projectId = "snowflake-dataset") {
|
|
18389
19381
|
this.projectId = projectId;
|
|
18390
19382
|
this.reportsBasePath = path10.join(
|
|
18391
|
-
|
|
19383
|
+
os4.homedir(),
|
|
18392
19384
|
".superatom",
|
|
18393
19385
|
"projects",
|
|
18394
19386
|
projectId,
|
|
@@ -18909,23 +19901,27 @@ var CleanupService = class _CleanupService {
|
|
|
18909
19901
|
// src/index.ts
|
|
18910
19902
|
var DEFAULT_WS_URL = "wss://ws.superatom.ai/websocket";
|
|
18911
19903
|
var SuperatomSDK = class {
|
|
18912
|
-
// 3.5 minutes (PING_INTERVAL + 30s grace)
|
|
18913
19904
|
constructor(config) {
|
|
18914
19905
|
this.ws = null;
|
|
18915
19906
|
this.messageHandlers = /* @__PURE__ */ new Map();
|
|
18916
19907
|
this.messageTypeHandlers = /* @__PURE__ */ new Map();
|
|
18917
19908
|
this.connected = false;
|
|
18918
19909
|
this.reconnectAttempts = 0;
|
|
18919
|
-
|
|
19910
|
+
// Retry forever — the backend must self-heal across NAT timeouts, DO
|
|
19911
|
+
// redeploys, and long network outages. The previous cap (5) gave up after
|
|
19912
|
+
// ~30s and left the singleton SDK dead until process restart.
|
|
19913
|
+
this.maxReconnectAttempts = Infinity;
|
|
18920
19914
|
this.collections = {};
|
|
18921
19915
|
this.components = [];
|
|
18922
19916
|
this.tools = [];
|
|
18923
|
-
|
|
19917
|
+
this.workflows = [];
|
|
19918
|
+
// Heartbeat properties for keeping WebSocket connection alive.
|
|
19919
|
+
// 25s ping + 10s grace stays under common NAT/LB idle thresholds (~60-100s)
|
|
19920
|
+
// so we detect dead sockets within seconds instead of minutes.
|
|
18924
19921
|
this.pingInterval = null;
|
|
18925
19922
|
this.lastPong = Date.now();
|
|
18926
|
-
this.PING_INTERVAL_MS =
|
|
18927
|
-
|
|
18928
|
-
this.PONG_TIMEOUT_MS = 21e4;
|
|
19923
|
+
this.PING_INTERVAL_MS = 25e3;
|
|
19924
|
+
this.PONG_TIMEOUT_MS = 35e3;
|
|
18929
19925
|
if (config.logLevel) {
|
|
18930
19926
|
logger.setLogLevel(config.logLevel);
|
|
18931
19927
|
}
|
|
@@ -19081,7 +20077,7 @@ var SuperatomSDK = class {
|
|
|
19081
20077
|
this.handlePong();
|
|
19082
20078
|
break;
|
|
19083
20079
|
case "DATA_REQ":
|
|
19084
|
-
handleDataRequest(parsed, this.collections, (msg) => this.send(msg)).catch((error) => {
|
|
20080
|
+
handleDataRequest(parsed, this.collections, (msg) => this.send(msg), this.tools).catch((error) => {
|
|
19085
20081
|
logger.error("Failed to handle data request:", error);
|
|
19086
20082
|
});
|
|
19087
20083
|
break;
|
|
@@ -19101,7 +20097,7 @@ var SuperatomSDK = class {
|
|
|
19101
20097
|
});
|
|
19102
20098
|
break;
|
|
19103
20099
|
case "USER_PROMPT_REQ":
|
|
19104
|
-
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) => {
|
|
20100
|
+
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) => {
|
|
19105
20101
|
logger.error("Failed to handle user prompt request:", error);
|
|
19106
20102
|
});
|
|
19107
20103
|
break;
|
|
@@ -19120,6 +20116,11 @@ var SuperatomSDK = class {
|
|
|
19120
20116
|
logger.error("Failed to handle component list request:", error);
|
|
19121
20117
|
});
|
|
19122
20118
|
break;
|
|
20119
|
+
case "WORKFLOW_LIST_RES":
|
|
20120
|
+
handleWorkflowListResponse(parsed, (wf) => this.setWorkflows(wf)).catch((error) => {
|
|
20121
|
+
logger.error("Failed to handle workflow list request:", error);
|
|
20122
|
+
});
|
|
20123
|
+
break;
|
|
19123
20124
|
case "USERS":
|
|
19124
20125
|
handleUsersRequest(parsed, this.collections, (msg) => this.send(msg)).catch((error) => {
|
|
19125
20126
|
logger.error("Failed to handle users request:", error);
|
|
@@ -19279,9 +20280,9 @@ var SuperatomSDK = class {
|
|
|
19279
20280
|
handleReconnect() {
|
|
19280
20281
|
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
19281
20282
|
this.reconnectAttempts++;
|
|
19282
|
-
const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts),
|
|
20283
|
+
const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
|
|
19283
20284
|
setTimeout(() => {
|
|
19284
|
-
logger.info(`Attempting to reconnect (${this.reconnectAttempts}
|
|
20285
|
+
logger.info(`Attempting to reconnect (attempt ${this.reconnectAttempts})...`);
|
|
19285
20286
|
this.connect().catch((error) => {
|
|
19286
20287
|
logger.error("Reconnection failed:", error);
|
|
19287
20288
|
});
|
|
@@ -19359,6 +20360,24 @@ var SuperatomSDK = class {
|
|
|
19359
20360
|
getTools() {
|
|
19360
20361
|
return this.tools;
|
|
19361
20362
|
}
|
|
20363
|
+
/**
|
|
20364
|
+
* Register workflow components for the SDK instance.
|
|
20365
|
+
*
|
|
20366
|
+
* Workflows are pre-built multi-step UI flows the main agent can pick when
|
|
20367
|
+
* the user's prompt matches a workflow's `whenToUse` trigger. Picking a
|
|
20368
|
+
* workflow short-circuits analysis text + dashboard component generation —
|
|
20369
|
+
* the workflow component is returned directly, with the LLM-extracted props.
|
|
20370
|
+
*/
|
|
20371
|
+
setWorkflows(workflows) {
|
|
20372
|
+
this.workflows = workflows;
|
|
20373
|
+
logger.info(`Workflows stored in SDK: ${workflows.length} workflow(s)`);
|
|
20374
|
+
}
|
|
20375
|
+
/**
|
|
20376
|
+
* Get the registered workflow components.
|
|
20377
|
+
*/
|
|
20378
|
+
getWorkflows() {
|
|
20379
|
+
return this.workflows;
|
|
20380
|
+
}
|
|
19362
20381
|
/**
|
|
19363
20382
|
* Apply model strategy to all LLM provider singletons
|
|
19364
20383
|
* @param strategy - 'best', 'fast', or 'balanced'
|
|
@@ -19418,6 +20437,8 @@ export {
|
|
|
19418
20437
|
LLM,
|
|
19419
20438
|
MainAgent,
|
|
19420
20439
|
STORAGE_CONFIG,
|
|
20440
|
+
ScriptMatcher,
|
|
20441
|
+
ScriptStore,
|
|
19421
20442
|
SuperatomSDK,
|
|
19422
20443
|
Thread,
|
|
19423
20444
|
ThreadManager,
|
|
@@ -19431,10 +20452,13 @@ export {
|
|
|
19431
20452
|
hybridRerank,
|
|
19432
20453
|
llmUsageLogger,
|
|
19433
20454
|
logger,
|
|
20455
|
+
normalizeScriptBody,
|
|
19434
20456
|
openaiLLM,
|
|
19435
20457
|
queryCache,
|
|
19436
20458
|
rerankChromaResults,
|
|
19437
20459
|
rerankConversationResults,
|
|
20460
|
+
resolveScriptRecipeStore,
|
|
20461
|
+
runScript,
|
|
19438
20462
|
userPromptErrorLogger
|
|
19439
20463
|
};
|
|
19440
20464
|
//# sourceMappingURL=index.mjs.map
|