@objectstack/service-ai 5.2.0 → 6.1.1
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.cjs +1331 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5139 -8
- package/dist/index.d.ts +5139 -8
- package/dist/index.js +1314 -14
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -112,7 +112,9 @@ var init_data_tools = __esm({
|
|
|
112
112
|
properties: {
|
|
113
113
|
field: { type: "string" },
|
|
114
114
|
order: { type: "string", enum: ["asc", "desc"] }
|
|
115
|
-
}
|
|
115
|
+
},
|
|
116
|
+
required: ["field", "order"],
|
|
117
|
+
additionalProperties: false
|
|
116
118
|
},
|
|
117
119
|
description: 'Sort order (e.g. [{ "field": "created_at", "order": "desc" }])'
|
|
118
120
|
},
|
|
@@ -182,7 +184,8 @@ var init_data_tools = __esm({
|
|
|
182
184
|
description: "Result column alias"
|
|
183
185
|
}
|
|
184
186
|
},
|
|
185
|
-
required: ["function", "alias"]
|
|
187
|
+
required: ["function", "alias"],
|
|
188
|
+
additionalProperties: false
|
|
186
189
|
},
|
|
187
190
|
description: "Aggregation definitions"
|
|
188
191
|
},
|
|
@@ -841,8 +844,112 @@ var MemoryLLMAdapter = class {
|
|
|
841
844
|
async chat(messages, options) {
|
|
842
845
|
const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
|
|
843
846
|
const userContent = lastUserMessage?.content;
|
|
844
|
-
const
|
|
845
|
-
const
|
|
847
|
+
const userText = typeof userContent === "string" ? userContent : "(complex content)";
|
|
848
|
+
const tools = options?.tools;
|
|
849
|
+
const hasQueryDataTool = Array.isArray(tools) && tools.some((t) => t?.name === "query_data");
|
|
850
|
+
const alreadyCalledQueryData = messages.some(
|
|
851
|
+
(m) => m.role === "tool" && Array.isArray(m.content) && m.content.some((c) => c?.toolName === "query_data")
|
|
852
|
+
);
|
|
853
|
+
const alreadyCalledAction = messages.some(
|
|
854
|
+
(m) => m.role === "tool" && Array.isArray(m.content) && m.content.some(
|
|
855
|
+
(c) => typeof c?.toolName === "string" && c.toolName.startsWith("action_")
|
|
856
|
+
)
|
|
857
|
+
);
|
|
858
|
+
if (Array.isArray(tools) && !alreadyCalledAction && lastUserMessage) {
|
|
859
|
+
const actionTools = tools.filter((t) => typeof t?.name === "string" && t.name.startsWith("action_"));
|
|
860
|
+
const chosen = pickActionTool(userText, actionTools);
|
|
861
|
+
if (chosen) {
|
|
862
|
+
const recordId = extractRecordIdFromMessages(messages, userText);
|
|
863
|
+
if (recordId) {
|
|
864
|
+
const toolCallId = `memory_tc_${Date.now().toString(36)}`;
|
|
865
|
+
return {
|
|
866
|
+
content: "",
|
|
867
|
+
model: options?.model ?? "memory",
|
|
868
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
869
|
+
toolCalls: [
|
|
870
|
+
{
|
|
871
|
+
type: "tool-call",
|
|
872
|
+
toolCallId,
|
|
873
|
+
toolName: chosen.name,
|
|
874
|
+
input: { recordId }
|
|
875
|
+
}
|
|
876
|
+
]
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
if (hasQueryDataTool && !alreadyCalledQueryData && lastUserMessage) {
|
|
882
|
+
const toolCallId = `memory_tc_${Date.now().toString(36)}`;
|
|
883
|
+
return {
|
|
884
|
+
content: "",
|
|
885
|
+
model: options?.model ?? "memory",
|
|
886
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
887
|
+
toolCalls: [
|
|
888
|
+
{
|
|
889
|
+
type: "tool-call",
|
|
890
|
+
toolCallId,
|
|
891
|
+
toolName: "query_data",
|
|
892
|
+
input: { request: userText }
|
|
893
|
+
}
|
|
894
|
+
]
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
if (alreadyCalledAction) {
|
|
898
|
+
const lastTool = [...messages].reverse().find((m) => m.role === "tool");
|
|
899
|
+
const part = Array.isArray(lastTool?.content) ? lastTool.content.find((c) => typeof c?.toolName === "string" && c.toolName.startsWith("action_")) : void 0;
|
|
900
|
+
const raw = part?.output && typeof part.output === "object" && "value" in part.output ? part.output.value : part?.result;
|
|
901
|
+
let payload = {};
|
|
902
|
+
if (typeof raw === "string") {
|
|
903
|
+
try {
|
|
904
|
+
payload = JSON.parse(raw);
|
|
905
|
+
} catch {
|
|
906
|
+
}
|
|
907
|
+
} else if (raw && typeof raw === "object") {
|
|
908
|
+
payload = raw;
|
|
909
|
+
}
|
|
910
|
+
if (payload.error) {
|
|
911
|
+
return {
|
|
912
|
+
content: `[memory] action ${payload.action ?? ""} failed: ${payload.error}`,
|
|
913
|
+
model: options?.model ?? "memory",
|
|
914
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
return {
|
|
918
|
+
content: `[memory] ${payload.message ?? "Action executed."} (${payload.action ?? "action"})`,
|
|
919
|
+
model: options?.model ?? "memory",
|
|
920
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
if (alreadyCalledQueryData) {
|
|
924
|
+
const lastTool = [...messages].reverse().find((m) => m.role === "tool");
|
|
925
|
+
const part = Array.isArray(lastTool?.content) ? lastTool.content.find((c) => c?.toolName === "query_data") : void 0;
|
|
926
|
+
let payload = {};
|
|
927
|
+
const raw = part?.output && typeof part.output === "object" && "value" in part.output ? part.output.value : part?.result;
|
|
928
|
+
if (typeof raw === "string") {
|
|
929
|
+
try {
|
|
930
|
+
payload = JSON.parse(raw);
|
|
931
|
+
} catch {
|
|
932
|
+
payload = {};
|
|
933
|
+
}
|
|
934
|
+
} else if (raw && typeof raw === "object") {
|
|
935
|
+
payload = raw;
|
|
936
|
+
}
|
|
937
|
+
if (payload.error) {
|
|
938
|
+
return {
|
|
939
|
+
content: `[memory] query_data failed: ${payload.error}`,
|
|
940
|
+
model: options?.model ?? "memory",
|
|
941
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
const records = payload.records ?? [];
|
|
945
|
+
const count = payload.count ?? records.length;
|
|
946
|
+
return {
|
|
947
|
+
content: `[memory] Found ${count} record${count === 1 ? "" : "s"} for "${userText}".`,
|
|
948
|
+
model: options?.model ?? "memory",
|
|
949
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
const content = lastUserMessage ? `[memory] ${userText}` : "[memory] (no user message)";
|
|
846
953
|
return {
|
|
847
954
|
content,
|
|
848
955
|
model: options?.model ?? "memory",
|
|
@@ -877,7 +984,171 @@ var MemoryLLMAdapter = class {
|
|
|
877
984
|
async listModels() {
|
|
878
985
|
return ["memory"];
|
|
879
986
|
}
|
|
987
|
+
/**
|
|
988
|
+
* Heuristic structured-output for testing & demos — NOT a real LLM.
|
|
989
|
+
*
|
|
990
|
+
* Strategy:
|
|
991
|
+
* 1. Extract candidate object names from the system messages by matching
|
|
992
|
+
* schema-context headers (`### name — Label`) emitted by
|
|
993
|
+
* {@link SchemaRetriever.renderSnippet}.
|
|
994
|
+
* 2. Pick the candidate whose tokens overlap most with the last user
|
|
995
|
+
* message (falls back to the first candidate).
|
|
996
|
+
* 3. Try `schema.safeParse({ objectName, limit: 20 })` — this satisfies the
|
|
997
|
+
* `QueryPlanSchema` used by the built-in `query_data` tool.
|
|
998
|
+
* 4. If that fails, fall back to `schema.safeParse({})` for schemas that
|
|
999
|
+
* accept defaults.
|
|
1000
|
+
* 5. Otherwise throw with a clear message — the demo needs a real provider.
|
|
1001
|
+
*/
|
|
1002
|
+
async generateObject(messages, schema, options) {
|
|
1003
|
+
const sys = messages.filter((m) => m.role === "system").map((m) => typeof m.content === "string" ? m.content : "").join("\n");
|
|
1004
|
+
const headerRe = /^###\s+([a-z0-9_]+)(?:\s+—\s+([^\n]+))?/gim;
|
|
1005
|
+
const candidates = [];
|
|
1006
|
+
for (const match of sys.matchAll(headerRe)) {
|
|
1007
|
+
const machineName = match[1];
|
|
1008
|
+
if (!machineName) continue;
|
|
1009
|
+
const aliasText = match[2] ?? "";
|
|
1010
|
+
const aliasTokens = /* @__PURE__ */ new Set();
|
|
1011
|
+
for (const t of machineName.split(/[^a-z0-9]+/)) {
|
|
1012
|
+
if (t) aliasTokens.add(t);
|
|
1013
|
+
}
|
|
1014
|
+
for (const t of aliasText.toLowerCase().split(/[^a-z0-9]+/)) {
|
|
1015
|
+
if (t) aliasTokens.add(t);
|
|
1016
|
+
}
|
|
1017
|
+
for (const t of [...aliasTokens]) {
|
|
1018
|
+
if (t.length > 3 && t.endsWith("s")) aliasTokens.add(t.slice(0, -1));
|
|
1019
|
+
}
|
|
1020
|
+
candidates.push({ name: machineName, aliasTokens });
|
|
1021
|
+
}
|
|
1022
|
+
const lastUser = [...messages].reverse().find((m) => m.role === "user");
|
|
1023
|
+
const userText = typeof lastUser?.content === "string" ? lastUser.content.toLowerCase() : "";
|
|
1024
|
+
const userTokens = new Set(
|
|
1025
|
+
userText.split(/[^a-z0-9_]+/).filter((t) => t.length > 1)
|
|
1026
|
+
);
|
|
1027
|
+
for (const t of [...userTokens]) {
|
|
1028
|
+
if (t.length > 3 && t.endsWith("s")) userTokens.add(t.slice(0, -1));
|
|
1029
|
+
}
|
|
1030
|
+
let chosen = candidates[0]?.name;
|
|
1031
|
+
let bestScore = -1;
|
|
1032
|
+
for (const cand of candidates) {
|
|
1033
|
+
let score = 0;
|
|
1034
|
+
for (const tok of cand.aliasTokens) {
|
|
1035
|
+
if (userTokens.has(tok)) score += 1;
|
|
1036
|
+
}
|
|
1037
|
+
if (score > bestScore) {
|
|
1038
|
+
bestScore = score;
|
|
1039
|
+
chosen = cand.name;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
const attempts = [];
|
|
1043
|
+
if (chosen) attempts.push({ objectName: chosen, limit: 20 });
|
|
1044
|
+
attempts.push({});
|
|
1045
|
+
for (const attempt of attempts) {
|
|
1046
|
+
const result = schema.safeParse(attempt);
|
|
1047
|
+
if (result.success) {
|
|
1048
|
+
return {
|
|
1049
|
+
object: result.data,
|
|
1050
|
+
model: options?.model ?? "memory",
|
|
1051
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
throw new Error(
|
|
1056
|
+
"MemoryLLMAdapter.generateObject: unable to synthesise a value for the requested schema. The memory adapter only handles QueryPlan-shaped schemas \u2014 wire a real LLM adapter (OpenAI / Anthropic / Google) for arbitrary structured output."
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
880
1059
|
};
|
|
1060
|
+
function pickActionTool(userText, actionTools) {
|
|
1061
|
+
if (actionTools.length === 0 || !userText) return null;
|
|
1062
|
+
const userTokens = new Set(
|
|
1063
|
+
userText.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2)
|
|
1064
|
+
);
|
|
1065
|
+
const ACTION_VERBS = /* @__PURE__ */ new Set([
|
|
1066
|
+
"complete",
|
|
1067
|
+
"finish",
|
|
1068
|
+
"done",
|
|
1069
|
+
"close",
|
|
1070
|
+
"start",
|
|
1071
|
+
"begin",
|
|
1072
|
+
"resume",
|
|
1073
|
+
"clone",
|
|
1074
|
+
"copy",
|
|
1075
|
+
"duplicate",
|
|
1076
|
+
"cancel",
|
|
1077
|
+
"abort",
|
|
1078
|
+
"archive",
|
|
1079
|
+
"restore",
|
|
1080
|
+
"approve",
|
|
1081
|
+
"reject",
|
|
1082
|
+
"assign",
|
|
1083
|
+
"unassign",
|
|
1084
|
+
"export",
|
|
1085
|
+
"import",
|
|
1086
|
+
"send",
|
|
1087
|
+
"notify",
|
|
1088
|
+
"publish",
|
|
1089
|
+
"unpublish",
|
|
1090
|
+
"mark"
|
|
1091
|
+
]);
|
|
1092
|
+
const hasActionVerb = [...userTokens].some((t) => ACTION_VERBS.has(t));
|
|
1093
|
+
if (!hasActionVerb) return null;
|
|
1094
|
+
let best = null;
|
|
1095
|
+
let bestScore = 0;
|
|
1096
|
+
for (const tool of actionTools) {
|
|
1097
|
+
const nameTokens = tool.name.replace(/^action_/, "").toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2);
|
|
1098
|
+
let score = 0;
|
|
1099
|
+
for (const tok of nameTokens) {
|
|
1100
|
+
if (!userTokens.has(tok)) continue;
|
|
1101
|
+
score += ACTION_VERBS.has(tok) ? 3 : 1;
|
|
1102
|
+
}
|
|
1103
|
+
if (score > bestScore) {
|
|
1104
|
+
bestScore = score;
|
|
1105
|
+
best = tool;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
return bestScore >= 3 ? best : null;
|
|
1109
|
+
}
|
|
1110
|
+
function extractRecordIdFromMessages(messages, userText) {
|
|
1111
|
+
const userTokens = userText.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2);
|
|
1112
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1113
|
+
const m = messages[i];
|
|
1114
|
+
if (m.role !== "tool" || !Array.isArray(m.content)) continue;
|
|
1115
|
+
const parts = m.content;
|
|
1116
|
+
for (const part of parts) {
|
|
1117
|
+
if (part?.toolName !== "query_data") continue;
|
|
1118
|
+
const raw = part.output && typeof part.output === "object" && "value" in part.output ? part.output.value : part.result;
|
|
1119
|
+
let payload = {};
|
|
1120
|
+
if (typeof raw === "string") {
|
|
1121
|
+
try {
|
|
1122
|
+
payload = JSON.parse(raw);
|
|
1123
|
+
} catch {
|
|
1124
|
+
}
|
|
1125
|
+
} else if (raw && typeof raw === "object") {
|
|
1126
|
+
payload = raw;
|
|
1127
|
+
}
|
|
1128
|
+
const records = payload.records ?? [];
|
|
1129
|
+
if (records.length === 0) continue;
|
|
1130
|
+
let bestId;
|
|
1131
|
+
let bestScore = -1;
|
|
1132
|
+
for (const rec of records) {
|
|
1133
|
+
if (!rec || typeof rec !== "object") continue;
|
|
1134
|
+
const id = rec.id;
|
|
1135
|
+
if (typeof id !== "string" && typeof id !== "number") continue;
|
|
1136
|
+
const hay = Object.values(rec).filter((v) => typeof v === "string").join(" ").toLowerCase();
|
|
1137
|
+
const hayTokens = hay.split(/[^a-z0-9]+/).filter(Boolean);
|
|
1138
|
+
let score = 0;
|
|
1139
|
+
for (const ut of userTokens) {
|
|
1140
|
+
if (hayTokens.includes(ut)) score += 1;
|
|
1141
|
+
}
|
|
1142
|
+
if (score > bestScore) {
|
|
1143
|
+
bestScore = score;
|
|
1144
|
+
bestId = String(id);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return bestId ?? String(records[0].id);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
return void 0;
|
|
1151
|
+
}
|
|
881
1152
|
|
|
882
1153
|
// src/tools/tool-registry.ts
|
|
883
1154
|
var ToolRegistry = class {
|
|
@@ -1043,6 +1314,70 @@ var InMemoryConversationService = class {
|
|
|
1043
1314
|
}
|
|
1044
1315
|
};
|
|
1045
1316
|
|
|
1317
|
+
// src/trace-recorder.ts
|
|
1318
|
+
import { randomUUID } from "crypto";
|
|
1319
|
+
var TRACE_OBJECT = "ai_traces";
|
|
1320
|
+
var NullTraceRecorder = class {
|
|
1321
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1322
|
+
record(_event) {
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
var ObjectQLTraceRecorder = class {
|
|
1326
|
+
constructor(engine, options = {}) {
|
|
1327
|
+
this.engine = engine;
|
|
1328
|
+
this.logger = options.logger;
|
|
1329
|
+
}
|
|
1330
|
+
async record(event) {
|
|
1331
|
+
const row = {
|
|
1332
|
+
id: `trace_${randomUUID()}`,
|
|
1333
|
+
conversation_id: event.conversationId ?? null,
|
|
1334
|
+
agent_id: event.agentId ?? null,
|
|
1335
|
+
operation: event.operation,
|
|
1336
|
+
model: event.model ?? null,
|
|
1337
|
+
adapter: event.adapter,
|
|
1338
|
+
prompt_tokens: event.promptTokens,
|
|
1339
|
+
completion_tokens: event.completionTokens,
|
|
1340
|
+
total_tokens: event.totalTokens,
|
|
1341
|
+
input_cost: event.cost?.inputCost ?? null,
|
|
1342
|
+
output_cost: event.cost?.outputCost ?? null,
|
|
1343
|
+
total_cost: event.cost?.totalCost ?? null,
|
|
1344
|
+
currency: event.cost?.currency ?? null,
|
|
1345
|
+
latency_ms: event.latencyMs,
|
|
1346
|
+
status: event.status,
|
|
1347
|
+
error: event.error ?? null,
|
|
1348
|
+
metadata: event.metadata ? JSON.stringify(event.metadata) : null,
|
|
1349
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1350
|
+
};
|
|
1351
|
+
try {
|
|
1352
|
+
await this.engine.insert(TRACE_OBJECT, row);
|
|
1353
|
+
} catch (err) {
|
|
1354
|
+
this.logger?.warn(
|
|
1355
|
+
"[AI] Failed to record trace (non-fatal)",
|
|
1356
|
+
err instanceof Error ? { error: err.message } : { error: String(err) }
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
function buildTraceEvent(input) {
|
|
1362
|
+
const usage = input.usage ?? { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
1363
|
+
const cost = input.model && input.registry ? input.registry.estimateCost(input.model, usage) : void 0;
|
|
1364
|
+
return {
|
|
1365
|
+
operation: input.operation,
|
|
1366
|
+
adapter: input.adapter,
|
|
1367
|
+
model: input.model,
|
|
1368
|
+
agentId: input.agentId,
|
|
1369
|
+
conversationId: input.conversationId,
|
|
1370
|
+
promptTokens: usage.promptTokens,
|
|
1371
|
+
completionTokens: usage.completionTokens,
|
|
1372
|
+
totalTokens: usage.totalTokens,
|
|
1373
|
+
latencyMs: input.latencyMs,
|
|
1374
|
+
status: input.status,
|
|
1375
|
+
error: input.error,
|
|
1376
|
+
cost,
|
|
1377
|
+
metadata: input.metadata
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1046
1381
|
// src/ai-service.ts
|
|
1047
1382
|
function textDeltaPart(id, text) {
|
|
1048
1383
|
return { type: "text-delta", id, text };
|
|
@@ -1061,22 +1396,83 @@ var _AIService = class _AIService {
|
|
|
1061
1396
|
this.logger = config.logger ?? createLogger({ level: "info", format: "pretty" });
|
|
1062
1397
|
this.toolRegistry = config.toolRegistry ?? new ToolRegistry();
|
|
1063
1398
|
this.conversationService = config.conversationService ?? new InMemoryConversationService();
|
|
1399
|
+
this.modelRegistry = config.modelRegistry;
|
|
1400
|
+
this.traceRecorder = config.traceRecorder ?? new NullTraceRecorder();
|
|
1064
1401
|
this.logger.info(
|
|
1065
|
-
`[AI] Service initialized with adapter="${this.adapter.name}", tools=${this.toolRegistry.size}`
|
|
1402
|
+
`[AI] Service initialized with adapter="${this.adapter.name}", tools=${this.toolRegistry.size}, models=${this.modelRegistry?.size ?? 0}`
|
|
1066
1403
|
);
|
|
1067
1404
|
}
|
|
1068
1405
|
/** The name of the active LLM adapter. */
|
|
1069
1406
|
get adapterName() {
|
|
1070
1407
|
return this.adapter.name;
|
|
1071
1408
|
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Run an adapter call and emit a trace event.
|
|
1411
|
+
*
|
|
1412
|
+
* Records both success and failure. Tracing failures never escape — the
|
|
1413
|
+
* recorder is expected to be defensive.
|
|
1414
|
+
*/
|
|
1415
|
+
async instrument(operation, options, fn) {
|
|
1416
|
+
const started = Date.now();
|
|
1417
|
+
try {
|
|
1418
|
+
const result = await fn();
|
|
1419
|
+
void this.traceRecorder.record(buildTraceEvent({
|
|
1420
|
+
operation,
|
|
1421
|
+
adapter: this.adapter.name,
|
|
1422
|
+
model: result.model ?? options?.model,
|
|
1423
|
+
usage: result.usage,
|
|
1424
|
+
latencyMs: Date.now() - started,
|
|
1425
|
+
status: "success",
|
|
1426
|
+
registry: this.modelRegistry
|
|
1427
|
+
}));
|
|
1428
|
+
return result;
|
|
1429
|
+
} catch (err) {
|
|
1430
|
+
void this.traceRecorder.record(buildTraceEvent({
|
|
1431
|
+
operation,
|
|
1432
|
+
adapter: this.adapter.name,
|
|
1433
|
+
model: options?.model,
|
|
1434
|
+
latencyMs: Date.now() - started,
|
|
1435
|
+
status: "error",
|
|
1436
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1437
|
+
registry: this.modelRegistry
|
|
1438
|
+
}));
|
|
1439
|
+
throw err;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1072
1442
|
// ── IAIService implementation ──────────────────────────────────
|
|
1073
1443
|
async chat(messages, options) {
|
|
1074
1444
|
this.logger.debug("[AI] chat", { messageCount: messages.length, model: options?.model });
|
|
1075
|
-
return this.adapter.chat(messages, options);
|
|
1445
|
+
return this.instrument("chat", options, () => this.adapter.chat(messages, options));
|
|
1076
1446
|
}
|
|
1077
1447
|
async complete(prompt, options) {
|
|
1078
1448
|
this.logger.debug("[AI] complete", { promptLength: prompt.length, model: options?.model });
|
|
1079
|
-
return this.adapter.complete(prompt, options);
|
|
1449
|
+
return this.instrument("complete", options, () => this.adapter.complete(prompt, options));
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Generate a strongly-typed object validated against a Zod schema.
|
|
1453
|
+
*
|
|
1454
|
+
* Delegates to the adapter's `generateObject` when supported; throws a
|
|
1455
|
+
* descriptive error when the adapter does not implement structured output.
|
|
1456
|
+
*
|
|
1457
|
+
* @example
|
|
1458
|
+
* ```ts
|
|
1459
|
+
* import { z } from 'zod';
|
|
1460
|
+
* const Schema = z.object({ name: z.string(), priority: z.number().int() });
|
|
1461
|
+
* const { object } = await ai.generateObject(messages, Schema);
|
|
1462
|
+
* ```
|
|
1463
|
+
*/
|
|
1464
|
+
async generateObject(messages, schema, options) {
|
|
1465
|
+
this.logger.debug("[AI] generateObject", { messageCount: messages.length, model: options?.model });
|
|
1466
|
+
if (!this.adapter.generateObject) {
|
|
1467
|
+
throw new Error(
|
|
1468
|
+
`[AI] Adapter "${this.adapter.name}" does not support generateObject. Use VercelLLMAdapter with a structured-output-capable model.`
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
return this.instrument(
|
|
1472
|
+
"generate_object",
|
|
1473
|
+
options,
|
|
1474
|
+
() => this.adapter.generateObject(messages, schema, options)
|
|
1475
|
+
);
|
|
1080
1476
|
}
|
|
1081
1477
|
async *streamChat(messages, options) {
|
|
1082
1478
|
this.logger.debug("[AI] streamChat", { messageCount: messages.length, model: options?.model });
|
|
@@ -1116,6 +1512,13 @@ var _AIService = class _AIService {
|
|
|
1116
1512
|
* maximum number of iterations (`maxIterations`) is reached.
|
|
1117
1513
|
*/
|
|
1118
1514
|
async chatWithTools(messages, options) {
|
|
1515
|
+
return this.instrument(
|
|
1516
|
+
"chat_with_tools",
|
|
1517
|
+
options,
|
|
1518
|
+
() => this.chatWithToolsImpl(messages, options)
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
async chatWithToolsImpl(messages, options) {
|
|
1119
1522
|
const { maxIterations: maxIter, onToolError, ...restOptions } = options ?? {};
|
|
1120
1523
|
const maxIterations = maxIter ?? _AIService.DEFAULT_MAX_ITERATIONS;
|
|
1121
1524
|
const registeredTools = this.toolRegistry.getAll();
|
|
@@ -2158,7 +2561,7 @@ function buildToolRoutes(aiService, logger) {
|
|
|
2158
2561
|
}
|
|
2159
2562
|
|
|
2160
2563
|
// src/conversation/objectql-conversation-service.ts
|
|
2161
|
-
import { randomUUID } from "crypto";
|
|
2564
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2162
2565
|
var CONVERSATIONS_OBJECT = "ai_conversations";
|
|
2163
2566
|
var MESSAGES_OBJECT = "ai_messages";
|
|
2164
2567
|
var CONVERSATION_ORDER = [
|
|
@@ -2175,7 +2578,7 @@ var ObjectQLConversationService = class {
|
|
|
2175
2578
|
}
|
|
2176
2579
|
async create(options = {}) {
|
|
2177
2580
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2178
|
-
const id = `conv_${
|
|
2581
|
+
const id = `conv_${randomUUID2()}`;
|
|
2179
2582
|
const record = {
|
|
2180
2583
|
id,
|
|
2181
2584
|
title: options.title ?? null,
|
|
@@ -2248,7 +2651,7 @@ var ObjectQLConversationService = class {
|
|
|
2248
2651
|
throw new Error(`Conversation "${conversationId}" not found`);
|
|
2249
2652
|
}
|
|
2250
2653
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2251
|
-
const msgId = `msg_${
|
|
2654
|
+
const msgId = `msg_${randomUUID2()}`;
|
|
2252
2655
|
let contentStr;
|
|
2253
2656
|
let toolCallsJson = null;
|
|
2254
2657
|
let toolCallId = null;
|
|
@@ -2493,10 +2896,677 @@ var AiMessageObject = ObjectSchema2.create({
|
|
|
2493
2896
|
}
|
|
2494
2897
|
});
|
|
2495
2898
|
|
|
2899
|
+
// src/objects/ai-trace.object.ts
|
|
2900
|
+
import { ObjectSchema as ObjectSchema3, Field as Field3 } from "@objectstack/spec/data";
|
|
2901
|
+
var AiTraceObject = ObjectSchema3.create({
|
|
2902
|
+
name: "ai_traces",
|
|
2903
|
+
label: "AI Trace",
|
|
2904
|
+
pluralLabel: "AI Traces",
|
|
2905
|
+
icon: "activity",
|
|
2906
|
+
isSystem: true,
|
|
2907
|
+
description: "Per-call LLM invocation trace with token usage and cost",
|
|
2908
|
+
fields: {
|
|
2909
|
+
id: Field3.text({
|
|
2910
|
+
label: "Trace ID",
|
|
2911
|
+
required: true,
|
|
2912
|
+
readonly: true
|
|
2913
|
+
}),
|
|
2914
|
+
conversation_id: Field3.lookup("ai_conversations", {
|
|
2915
|
+
label: "Conversation",
|
|
2916
|
+
required: false,
|
|
2917
|
+
description: "Parent conversation, if any"
|
|
2918
|
+
}),
|
|
2919
|
+
agent_id: Field3.text({
|
|
2920
|
+
label: "Agent",
|
|
2921
|
+
required: false,
|
|
2922
|
+
maxLength: 128,
|
|
2923
|
+
description: "Agent metadata name that originated the call"
|
|
2924
|
+
}),
|
|
2925
|
+
operation: Field3.select({
|
|
2926
|
+
label: "Operation",
|
|
2927
|
+
required: true,
|
|
2928
|
+
options: [
|
|
2929
|
+
{ label: "Chat", value: "chat" },
|
|
2930
|
+
{ label: "Complete", value: "complete" },
|
|
2931
|
+
{ label: "Stream Chat", value: "stream_chat" },
|
|
2932
|
+
{ label: "Chat With Tools", value: "chat_with_tools" },
|
|
2933
|
+
{ label: "Generate Object", value: "generate_object" },
|
|
2934
|
+
{ label: "Embed", value: "embed" }
|
|
2935
|
+
]
|
|
2936
|
+
}),
|
|
2937
|
+
model: Field3.text({
|
|
2938
|
+
label: "Model",
|
|
2939
|
+
required: false,
|
|
2940
|
+
maxLength: 128,
|
|
2941
|
+
description: "Model identifier reported by the adapter"
|
|
2942
|
+
}),
|
|
2943
|
+
adapter: Field3.text({
|
|
2944
|
+
label: "Adapter",
|
|
2945
|
+
required: false,
|
|
2946
|
+
maxLength: 64,
|
|
2947
|
+
description: 'LLM adapter name (e.g. "vercel", "memory")'
|
|
2948
|
+
}),
|
|
2949
|
+
prompt_tokens: Field3.number({
|
|
2950
|
+
label: "Prompt Tokens",
|
|
2951
|
+
required: false,
|
|
2952
|
+
defaultValue: 0
|
|
2953
|
+
}),
|
|
2954
|
+
completion_tokens: Field3.number({
|
|
2955
|
+
label: "Completion Tokens",
|
|
2956
|
+
required: false,
|
|
2957
|
+
defaultValue: 0
|
|
2958
|
+
}),
|
|
2959
|
+
total_tokens: Field3.number({
|
|
2960
|
+
label: "Total Tokens",
|
|
2961
|
+
required: false,
|
|
2962
|
+
defaultValue: 0
|
|
2963
|
+
}),
|
|
2964
|
+
input_cost: Field3.number({
|
|
2965
|
+
label: "Input Cost",
|
|
2966
|
+
required: false,
|
|
2967
|
+
description: "Cost attributable to prompt tokens (currency in `currency` field)"
|
|
2968
|
+
}),
|
|
2969
|
+
output_cost: Field3.number({
|
|
2970
|
+
label: "Output Cost",
|
|
2971
|
+
required: false,
|
|
2972
|
+
description: "Cost attributable to completion tokens"
|
|
2973
|
+
}),
|
|
2974
|
+
total_cost: Field3.number({
|
|
2975
|
+
label: "Total Cost",
|
|
2976
|
+
required: false,
|
|
2977
|
+
description: "input_cost + output_cost"
|
|
2978
|
+
}),
|
|
2979
|
+
currency: Field3.text({
|
|
2980
|
+
label: "Currency",
|
|
2981
|
+
required: false,
|
|
2982
|
+
maxLength: 8,
|
|
2983
|
+
defaultValue: "USD"
|
|
2984
|
+
}),
|
|
2985
|
+
latency_ms: Field3.number({
|
|
2986
|
+
label: "Latency (ms)",
|
|
2987
|
+
required: true,
|
|
2988
|
+
defaultValue: 0,
|
|
2989
|
+
description: "Wall-clock duration of the LLM call"
|
|
2990
|
+
}),
|
|
2991
|
+
status: Field3.select({
|
|
2992
|
+
label: "Status",
|
|
2993
|
+
required: true,
|
|
2994
|
+
options: [
|
|
2995
|
+
{ label: "Success", value: "success" },
|
|
2996
|
+
{ label: "Error", value: "error" }
|
|
2997
|
+
]
|
|
2998
|
+
}),
|
|
2999
|
+
error: Field3.textarea({
|
|
3000
|
+
label: "Error",
|
|
3001
|
+
required: false,
|
|
3002
|
+
description: "Error message when status=error"
|
|
3003
|
+
}),
|
|
3004
|
+
metadata: Field3.textarea({
|
|
3005
|
+
label: "Metadata",
|
|
3006
|
+
required: false,
|
|
3007
|
+
description: "JSON-serialized extra fields (request id, user id, \u2026)"
|
|
3008
|
+
}),
|
|
3009
|
+
created_at: Field3.datetime({
|
|
3010
|
+
label: "Created At",
|
|
3011
|
+
required: true,
|
|
3012
|
+
defaultValue: "NOW()",
|
|
3013
|
+
readonly: true
|
|
3014
|
+
})
|
|
3015
|
+
},
|
|
3016
|
+
indexes: [
|
|
3017
|
+
{ fields: ["conversation_id"] },
|
|
3018
|
+
{ fields: ["agent_id"] },
|
|
3019
|
+
{ fields: ["model"] },
|
|
3020
|
+
{ fields: ["status"] },
|
|
3021
|
+
{ fields: ["created_at"] }
|
|
3022
|
+
],
|
|
3023
|
+
enable: {
|
|
3024
|
+
trackHistory: false,
|
|
3025
|
+
searchable: false,
|
|
3026
|
+
apiEnabled: true,
|
|
3027
|
+
apiMethods: ["get", "list"],
|
|
3028
|
+
trash: false,
|
|
3029
|
+
mru: false
|
|
3030
|
+
}
|
|
3031
|
+
});
|
|
3032
|
+
|
|
3033
|
+
// src/views/ai-trace.view.ts
|
|
3034
|
+
import { defineView } from "@objectstack/spec";
|
|
3035
|
+
var AiTraceView = defineView({
|
|
3036
|
+
list: {
|
|
3037
|
+
type: "grid",
|
|
3038
|
+
data: { provider: "object", object: "ai_traces" },
|
|
3039
|
+
columns: [
|
|
3040
|
+
{ field: "created_at", label: "Time" },
|
|
3041
|
+
{ field: "operation" },
|
|
3042
|
+
{ field: "model" },
|
|
3043
|
+
{ field: "agent_id", label: "Agent" },
|
|
3044
|
+
{ field: "latency_ms", label: "Latency (ms)" },
|
|
3045
|
+
{ field: "total_tokens", label: "Tokens" },
|
|
3046
|
+
{ field: "cost_total", label: "Cost" },
|
|
3047
|
+
{ field: "status" }
|
|
3048
|
+
],
|
|
3049
|
+
sort: [{ field: "created_at", order: "desc" }],
|
|
3050
|
+
pagination: { pageSize: 50 },
|
|
3051
|
+
searchableFields: ["conversation_id", "agent_id", "model", "error"],
|
|
3052
|
+
filterableFields: ["operation", "model", "status"]
|
|
3053
|
+
},
|
|
3054
|
+
listViews: {
|
|
3055
|
+
errors: {
|
|
3056
|
+
label: "Errors",
|
|
3057
|
+
type: "grid",
|
|
3058
|
+
data: { provider: "object", object: "ai_traces" },
|
|
3059
|
+
columns: [
|
|
3060
|
+
{ field: "created_at", label: "Time" },
|
|
3061
|
+
{ field: "operation" },
|
|
3062
|
+
{ field: "model" },
|
|
3063
|
+
{ field: "latency_ms", label: "Latency (ms)" },
|
|
3064
|
+
{ field: "error" }
|
|
3065
|
+
],
|
|
3066
|
+
filter: [
|
|
3067
|
+
{ field: "status", operator: "=", value: "error" }
|
|
3068
|
+
],
|
|
3069
|
+
sort: [{ field: "created_at", order: "desc" }]
|
|
3070
|
+
},
|
|
3071
|
+
by_model: {
|
|
3072
|
+
label: "By Model",
|
|
3073
|
+
type: "grid",
|
|
3074
|
+
data: { provider: "object", object: "ai_traces" },
|
|
3075
|
+
columns: [
|
|
3076
|
+
{ field: "model" },
|
|
3077
|
+
{ field: "operation" },
|
|
3078
|
+
{ field: "latency_ms", label: "Latency (ms)" },
|
|
3079
|
+
{ field: "total_tokens", label: "Tokens" },
|
|
3080
|
+
{ field: "cost_total", label: "Cost" },
|
|
3081
|
+
{ field: "status" },
|
|
3082
|
+
{ field: "created_at", label: "Time" }
|
|
3083
|
+
],
|
|
3084
|
+
grouping: { fields: [{ field: "model" }] },
|
|
3085
|
+
sort: [{ field: "created_at", order: "desc" }]
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
});
|
|
3089
|
+
|
|
2496
3090
|
// src/plugin.ts
|
|
2497
3091
|
init_data_tools();
|
|
2498
3092
|
init_metadata_tools();
|
|
2499
3093
|
|
|
3094
|
+
// src/tools/query-data.tool.ts
|
|
3095
|
+
import { z } from "zod";
|
|
3096
|
+
|
|
3097
|
+
// src/schema-retriever.ts
|
|
3098
|
+
var SchemaRetriever = class {
|
|
3099
|
+
constructor(metadata, options = {}) {
|
|
3100
|
+
this.metadata = metadata;
|
|
3101
|
+
this.options = {
|
|
3102
|
+
limit: options.limit ?? 3,
|
|
3103
|
+
minScore: options.minScore ?? 1,
|
|
3104
|
+
maxFieldsPerObject: options.maxFieldsPerObject ?? 12
|
|
3105
|
+
};
|
|
3106
|
+
}
|
|
3107
|
+
/**
|
|
3108
|
+
* Find object definitions whose name/label/fields match terms in the query.
|
|
3109
|
+
*
|
|
3110
|
+
* Returns matches sorted by score (descending) capped at `limit`. When
|
|
3111
|
+
* the query yields no matches, returns an empty array — callers may
|
|
3112
|
+
* fall back to a generic "describe what data exists" tool call.
|
|
3113
|
+
*/
|
|
3114
|
+
async retrieve(query) {
|
|
3115
|
+
const terms = tokenise(query);
|
|
3116
|
+
if (terms.length === 0) return [];
|
|
3117
|
+
const objects = await this.metadata.listObjects();
|
|
3118
|
+
const hits = [];
|
|
3119
|
+
for (const raw of objects) {
|
|
3120
|
+
const obj = raw;
|
|
3121
|
+
if (!obj?.name) continue;
|
|
3122
|
+
const score = scoreObject(obj, terms);
|
|
3123
|
+
if (score >= this.options.minScore) {
|
|
3124
|
+
hits.push({ object: obj, score });
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
hits.sort((a, b) => b.score - a.score);
|
|
3128
|
+
return hits.slice(0, this.options.limit);
|
|
3129
|
+
}
|
|
3130
|
+
/**
|
|
3131
|
+
* Render hits as a compact Markdown schema snippet.
|
|
3132
|
+
*
|
|
3133
|
+
* Designed to be appended to the system message — every line carries
|
|
3134
|
+
* exactly the information a model needs to choose object/field names
|
|
3135
|
+
* for query construction.
|
|
3136
|
+
*/
|
|
3137
|
+
static renderSnippet(hits, maxFieldsPerObject = 12) {
|
|
3138
|
+
if (hits.length === 0) return "";
|
|
3139
|
+
const lines = ["## Schema context (auto-injected)"];
|
|
3140
|
+
for (const hit of hits) {
|
|
3141
|
+
const obj = hit.object;
|
|
3142
|
+
const parts = [];
|
|
3143
|
+
if (obj.label) parts.push(obj.label);
|
|
3144
|
+
if (obj.pluralLabel && obj.pluralLabel !== obj.label) parts.push(`(${obj.pluralLabel})`);
|
|
3145
|
+
const header = parts.length > 0 ? ` \u2014 ${parts.join(" ")}` : "";
|
|
3146
|
+
lines.push(`### ${obj.name}${header}`);
|
|
3147
|
+
const fields = Object.entries(obj.fields ?? {}).slice(0, maxFieldsPerObject);
|
|
3148
|
+
for (const [name, field] of fields) {
|
|
3149
|
+
lines.push(` - ${name}: ${describeField(field)}`);
|
|
3150
|
+
}
|
|
3151
|
+
const total = Object.keys(obj.fields ?? {}).length;
|
|
3152
|
+
if (total > fields.length) {
|
|
3153
|
+
lines.push(` - \u2026${total - fields.length} more field(s)`);
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
return lines.join("\n");
|
|
3157
|
+
}
|
|
3158
|
+
};
|
|
3159
|
+
function tokenise(query) {
|
|
3160
|
+
const raw = query.toLowerCase().match(/[a-z0-9]+/g) ?? [];
|
|
3161
|
+
return raw.filter((t) => t.length >= 2 && !STOPWORDS.has(t));
|
|
3162
|
+
}
|
|
3163
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
3164
|
+
"the",
|
|
3165
|
+
"and",
|
|
3166
|
+
"for",
|
|
3167
|
+
"with",
|
|
3168
|
+
"from",
|
|
3169
|
+
"are",
|
|
3170
|
+
"has",
|
|
3171
|
+
"have",
|
|
3172
|
+
"had",
|
|
3173
|
+
"was",
|
|
3174
|
+
"were",
|
|
3175
|
+
"this",
|
|
3176
|
+
"that",
|
|
3177
|
+
"these",
|
|
3178
|
+
"those",
|
|
3179
|
+
"all",
|
|
3180
|
+
"any",
|
|
3181
|
+
"how",
|
|
3182
|
+
"what",
|
|
3183
|
+
"when",
|
|
3184
|
+
"where",
|
|
3185
|
+
"who",
|
|
3186
|
+
"why",
|
|
3187
|
+
"which",
|
|
3188
|
+
"show",
|
|
3189
|
+
"list",
|
|
3190
|
+
"find",
|
|
3191
|
+
"get",
|
|
3192
|
+
"count",
|
|
3193
|
+
"of",
|
|
3194
|
+
"in",
|
|
3195
|
+
"on",
|
|
3196
|
+
"at",
|
|
3197
|
+
"to",
|
|
3198
|
+
"as",
|
|
3199
|
+
"by",
|
|
3200
|
+
"is",
|
|
3201
|
+
"it",
|
|
3202
|
+
"an",
|
|
3203
|
+
"or",
|
|
3204
|
+
"be",
|
|
3205
|
+
"me"
|
|
3206
|
+
]);
|
|
3207
|
+
function scoreObject(obj, terms) {
|
|
3208
|
+
let score = 0;
|
|
3209
|
+
const nameTokens = splitSnake(obj.name);
|
|
3210
|
+
const labelTokens = obj.label ? tokenise(obj.label) : [];
|
|
3211
|
+
const pluralTokens = obj.pluralLabel ? tokenise(obj.pluralLabel) : [];
|
|
3212
|
+
const descTokens = obj.description ? tokenise(obj.description) : [];
|
|
3213
|
+
for (const term of terms) {
|
|
3214
|
+
if (nameTokens.includes(term)) score += 3;
|
|
3215
|
+
else if (labelTokens.includes(term) || pluralTokens.includes(term)) score += 2;
|
|
3216
|
+
else if (descTokens.includes(term)) score += 1;
|
|
3217
|
+
}
|
|
3218
|
+
for (const [fieldName, field] of Object.entries(obj.fields ?? {})) {
|
|
3219
|
+
const fnTokens = splitSnake(fieldName);
|
|
3220
|
+
const flTokens = field.label ? tokenise(field.label) : [];
|
|
3221
|
+
for (const term of terms) {
|
|
3222
|
+
if (fnTokens.includes(term)) score += 2;
|
|
3223
|
+
else if (flTokens.includes(term)) score += 1;
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
return score;
|
|
3227
|
+
}
|
|
3228
|
+
function splitSnake(name) {
|
|
3229
|
+
return name.toLowerCase().split("_").filter(Boolean);
|
|
3230
|
+
}
|
|
3231
|
+
function describeField(field) {
|
|
3232
|
+
const t = field.type ?? "unknown";
|
|
3233
|
+
if (t === "lookup" && field.reference) return `lookup \u2192 ${field.reference}`;
|
|
3234
|
+
if (t === "select" && Array.isArray(field.options)) {
|
|
3235
|
+
const values = field.options.map(
|
|
3236
|
+
(o) => typeof o === "string" ? o : o.value
|
|
3237
|
+
).filter(Boolean).slice(0, 6);
|
|
3238
|
+
return `select(${values.join("|")})`;
|
|
3239
|
+
}
|
|
3240
|
+
return t;
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
// src/tools/query-data.tool.ts
|
|
3244
|
+
var QueryPlanSchema = z.object({
|
|
3245
|
+
objectName: z.string().min(1).describe('The snake_case object name to query (e.g. "task", "account").'),
|
|
3246
|
+
where: z.record(z.string(), z.unknown()).optional().describe(
|
|
3247
|
+
'Filter conditions as key-value pairs. Use MongoDB-style operators for ranges, e.g. {"amount": {"$gt": 100}}.'
|
|
3248
|
+
),
|
|
3249
|
+
fields: z.array(z.string()).optional().describe("Field names to return. Omit to return all fields."),
|
|
3250
|
+
orderBy: z.array(
|
|
3251
|
+
z.object({
|
|
3252
|
+
field: z.string(),
|
|
3253
|
+
order: z.enum(["asc", "desc"])
|
|
3254
|
+
})
|
|
3255
|
+
).optional().describe("Sort order. First entry is primary sort key."),
|
|
3256
|
+
limit: z.number().int().min(1).max(200).optional().describe("Maximum number of records (default 20, max 200).")
|
|
3257
|
+
});
|
|
3258
|
+
var QUERY_DATA_TOOL = {
|
|
3259
|
+
name: "query_data",
|
|
3260
|
+
description: "Answer a natural-language question about the user's data. Internally retrieves the relevant object schema, generates an ObjectQL query, executes it, and returns the matching records. Prefer this tool over `query_records` / `aggregate_data` when the user's intent is expressed in plain language.",
|
|
3261
|
+
parameters: {
|
|
3262
|
+
type: "object",
|
|
3263
|
+
properties: {
|
|
3264
|
+
request: {
|
|
3265
|
+
type: "string",
|
|
3266
|
+
description: "The natural-language question to answer (paraphrase the user's request if needed for clarity)."
|
|
3267
|
+
},
|
|
3268
|
+
model: {
|
|
3269
|
+
type: "string",
|
|
3270
|
+
description: "Optional model id to use for query planning. Defaults to the AI service's default model."
|
|
3271
|
+
}
|
|
3272
|
+
},
|
|
3273
|
+
required: ["request"],
|
|
3274
|
+
additionalProperties: false
|
|
3275
|
+
}
|
|
3276
|
+
};
|
|
3277
|
+
function createQueryDataHandler(ctx) {
|
|
3278
|
+
const retriever = new SchemaRetriever(ctx.metadata);
|
|
3279
|
+
const maxLimit = ctx.maxLimit ?? 100;
|
|
3280
|
+
return async (args) => {
|
|
3281
|
+
const { request, model } = args;
|
|
3282
|
+
if (!request || typeof request !== "string") {
|
|
3283
|
+
return JSON.stringify({ error: "query_data: `request` is required" });
|
|
3284
|
+
}
|
|
3285
|
+
if (!ctx.ai.generateObject) {
|
|
3286
|
+
return JSON.stringify({
|
|
3287
|
+
error: "query_data requires structured-output support. Configure a Vercel-AI-SDK-backed adapter (OpenAI, Anthropic, Google)."
|
|
3288
|
+
});
|
|
3289
|
+
}
|
|
3290
|
+
const hits = await retriever.retrieve(request);
|
|
3291
|
+
if (hits.length === 0) {
|
|
3292
|
+
return JSON.stringify({
|
|
3293
|
+
error: "No matching objects in metadata. Ask the user which object(s) to query, or list available objects via list_objects."
|
|
3294
|
+
});
|
|
3295
|
+
}
|
|
3296
|
+
const snippet = SchemaRetriever.renderSnippet(hits);
|
|
3297
|
+
const planMessages = [
|
|
3298
|
+
{
|
|
3299
|
+
role: "system",
|
|
3300
|
+
content: "You translate user data questions into a single ObjectQL query plan. Use ONLY the objects and fields listed in the schema context below. Never invent field names. If the question is ambiguous, pick the most likely interpretation and use a reasonable `limit`.\n\n" + snippet
|
|
3301
|
+
},
|
|
3302
|
+
{ role: "user", content: request }
|
|
3303
|
+
];
|
|
3304
|
+
let plan;
|
|
3305
|
+
try {
|
|
3306
|
+
const generated = await ctx.ai.generateObject(planMessages, QueryPlanSchema, {
|
|
3307
|
+
model,
|
|
3308
|
+
schemaName: "ObjectQLQueryPlan",
|
|
3309
|
+
schemaDescription: "A single ObjectQL find() query to answer the user request."
|
|
3310
|
+
});
|
|
3311
|
+
plan = generated.object;
|
|
3312
|
+
} catch (err) {
|
|
3313
|
+
return JSON.stringify({
|
|
3314
|
+
error: `Failed to plan query: ${err instanceof Error ? err.message : String(err)}`
|
|
3315
|
+
});
|
|
3316
|
+
}
|
|
3317
|
+
const matchedObject = hits.find((h) => h.object.name === plan.objectName)?.object ?? hits[0].object;
|
|
3318
|
+
if (matchedObject.name !== plan.objectName) {
|
|
3319
|
+
return JSON.stringify({
|
|
3320
|
+
error: `Planned object "${plan.objectName}" is not in the retrieved schema. Available: ${hits.map((h) => h.object.name).join(", ")}`
|
|
3321
|
+
});
|
|
3322
|
+
}
|
|
3323
|
+
const limit = Math.min(plan.limit ?? 20, maxLimit);
|
|
3324
|
+
try {
|
|
3325
|
+
const records = await ctx.dataEngine.find(plan.objectName, {
|
|
3326
|
+
where: plan.where,
|
|
3327
|
+
fields: plan.fields,
|
|
3328
|
+
orderBy: plan.orderBy,
|
|
3329
|
+
limit
|
|
3330
|
+
});
|
|
3331
|
+
return JSON.stringify({
|
|
3332
|
+
plan,
|
|
3333
|
+
count: records.length,
|
|
3334
|
+
records
|
|
3335
|
+
});
|
|
3336
|
+
} catch (err) {
|
|
3337
|
+
return JSON.stringify({
|
|
3338
|
+
plan,
|
|
3339
|
+
error: `Query execution failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3340
|
+
});
|
|
3341
|
+
}
|
|
3342
|
+
};
|
|
3343
|
+
}
|
|
3344
|
+
function registerQueryDataTool(registry, context) {
|
|
3345
|
+
registry.register(QUERY_DATA_TOOL, createQueryDataHandler(context));
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
// src/tools/action-tools.ts
|
|
3349
|
+
function actionSkipReason(action) {
|
|
3350
|
+
if (action.aiExposed === false) {
|
|
3351
|
+
return "opted-out via aiExposed:false";
|
|
3352
|
+
}
|
|
3353
|
+
if (action.type !== "script") return `type='${action.type}' not yet supported`;
|
|
3354
|
+
if (!action.target && !action.body) return "no target or body";
|
|
3355
|
+
if (action.confirmText) return "requires confirmation (confirmText set)";
|
|
3356
|
+
if (action.mode === "delete") return "mode='delete' \u2014 destructive";
|
|
3357
|
+
if (action.variant === "danger") return "variant='danger' \u2014 destructive";
|
|
3358
|
+
return null;
|
|
3359
|
+
}
|
|
3360
|
+
function fieldTypeToJsonType(t) {
|
|
3361
|
+
switch (t) {
|
|
3362
|
+
case "number":
|
|
3363
|
+
case "currency":
|
|
3364
|
+
case "percent":
|
|
3365
|
+
case "rating":
|
|
3366
|
+
case "slider":
|
|
3367
|
+
case "autonumber":
|
|
3368
|
+
return "number";
|
|
3369
|
+
case "boolean":
|
|
3370
|
+
case "toggle":
|
|
3371
|
+
return "boolean";
|
|
3372
|
+
case "multiselect":
|
|
3373
|
+
case "checkboxes":
|
|
3374
|
+
case "tags":
|
|
3375
|
+
return "array";
|
|
3376
|
+
default:
|
|
3377
|
+
return "string";
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
function resolveParam(param, ownerObject, allObjects) {
|
|
3381
|
+
const fieldRef = param.field;
|
|
3382
|
+
const owner = param.objectOverride && allObjects.get(param.objectOverride) ? allObjects.get(param.objectOverride) : ownerObject;
|
|
3383
|
+
const field = fieldRef ? owner?.fields?.[fieldRef] : void 0;
|
|
3384
|
+
const name = param.name ?? fieldRef;
|
|
3385
|
+
if (!name) return null;
|
|
3386
|
+
const type = param.type ?? field?.type;
|
|
3387
|
+
const jsonType = fieldTypeToJsonType(type);
|
|
3388
|
+
const schema = { type: jsonType };
|
|
3389
|
+
const label = typeof param.label === "string" ? param.label : field?.label;
|
|
3390
|
+
const help = param.helpText ?? field?.description;
|
|
3391
|
+
const description = [label, help].filter(Boolean).join(" \u2014 ") || void 0;
|
|
3392
|
+
if (description) schema.description = description;
|
|
3393
|
+
const optionSource = param.options ?? field?.options;
|
|
3394
|
+
if (Array.isArray(optionSource) && optionSource.length > 0) {
|
|
3395
|
+
const values = optionSource.map((o) => typeof o === "string" ? o : o.value).filter((v) => typeof v === "string");
|
|
3396
|
+
if (values.length > 0) {
|
|
3397
|
+
schema.enum = jsonType === "array" ? void 0 : values;
|
|
3398
|
+
if (jsonType === "array") {
|
|
3399
|
+
schema.items = { type: "string", enum: values };
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
} else if (jsonType === "array") {
|
|
3403
|
+
schema.items = { type: "string" };
|
|
3404
|
+
}
|
|
3405
|
+
if (param.defaultValue !== void 0) {
|
|
3406
|
+
schema.default = param.defaultValue;
|
|
3407
|
+
}
|
|
3408
|
+
const required = Boolean(param.required ?? field?.required ?? false);
|
|
3409
|
+
return { name, schema, required };
|
|
3410
|
+
}
|
|
3411
|
+
function buildParametersSchema(action, ownerObject, allObjects) {
|
|
3412
|
+
const properties = {};
|
|
3413
|
+
const required = [];
|
|
3414
|
+
const isRowContext = Array.isArray(action.locations) && action.locations.some((l) => l === "list_item" || l === "record_header" || l === "record_more" || l === "record_related");
|
|
3415
|
+
if (action.objectName && isRowContext) {
|
|
3416
|
+
properties.recordId = {
|
|
3417
|
+
type: "string",
|
|
3418
|
+
description: `The ${action.objectName} record id to act on.`
|
|
3419
|
+
};
|
|
3420
|
+
if (action.recordIdParam || action.recordIdField) {
|
|
3421
|
+
required.push("recordId");
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
for (const param of action.params ?? []) {
|
|
3425
|
+
const resolved = resolveParam(param, ownerObject, allObjects);
|
|
3426
|
+
if (!resolved) continue;
|
|
3427
|
+
properties[resolved.name] = resolved.schema;
|
|
3428
|
+
if (resolved.required) required.push(resolved.name);
|
|
3429
|
+
}
|
|
3430
|
+
return {
|
|
3431
|
+
type: "object",
|
|
3432
|
+
properties,
|
|
3433
|
+
...required.length > 0 ? { required } : {},
|
|
3434
|
+
additionalProperties: false
|
|
3435
|
+
};
|
|
3436
|
+
}
|
|
3437
|
+
function actionToolName(action, prefix = "action_") {
|
|
3438
|
+
return `${prefix}${action.name}`;
|
|
3439
|
+
}
|
|
3440
|
+
function describeAction(action, ownerObject) {
|
|
3441
|
+
const label = typeof action.label === "string" ? action.label : action.name.replace(/_/g, " ");
|
|
3442
|
+
const target = action.objectName ?? ownerObject?.name;
|
|
3443
|
+
const targetLabel = ownerObject?.label ?? target;
|
|
3444
|
+
const parts = [];
|
|
3445
|
+
parts.push(`${label}${targetLabel ? ` \u2014 operates on ${targetLabel}` : ""}.`);
|
|
3446
|
+
if (action.successMessage && typeof action.successMessage === "string") {
|
|
3447
|
+
parts.push(`On success: ${action.successMessage}`);
|
|
3448
|
+
}
|
|
3449
|
+
if (action.mode) parts.push(`Mode: ${action.mode}.`);
|
|
3450
|
+
parts.push(
|
|
3451
|
+
"Use this when the user asks to perform this operation in natural language."
|
|
3452
|
+
);
|
|
3453
|
+
return parts.join(" ");
|
|
3454
|
+
}
|
|
3455
|
+
function actionToToolDefinition(action, ownerObject, allObjects, toolPrefix = "action_") {
|
|
3456
|
+
if (actionSkipReason(action) !== null) return null;
|
|
3457
|
+
return {
|
|
3458
|
+
name: actionToolName(action, toolPrefix),
|
|
3459
|
+
description: describeAction(action, ownerObject),
|
|
3460
|
+
parameters: buildParametersSchema(action, ownerObject, allObjects)
|
|
3461
|
+
};
|
|
3462
|
+
}
|
|
3463
|
+
function buildHandlerEngineAdapter(engine) {
|
|
3464
|
+
return {
|
|
3465
|
+
update: (object, id, data) => engine.update(object, { ...data, id }, { where: { id } }),
|
|
3466
|
+
insert: (object, data) => engine.insert(object, data),
|
|
3467
|
+
find: (object, where) => engine.find(object, { where }),
|
|
3468
|
+
delete: (object, ids) => engine.delete(object, { where: { id: ids.length === 1 ? ids[0] : { $in: ids } } })
|
|
3469
|
+
};
|
|
3470
|
+
}
|
|
3471
|
+
function createActionToolHandler(action, ctx) {
|
|
3472
|
+
const principal = ctx.principal ?? { id: "ai_agent", name: "AI Assistant" };
|
|
3473
|
+
const engineAdapter = buildHandlerEngineAdapter(ctx.dataEngine);
|
|
3474
|
+
const requiresRecord = Array.isArray(action.locations) && action.locations.some(
|
|
3475
|
+
(l) => l === "list_item" || l === "record_header" || l === "record_more" || l === "record_related"
|
|
3476
|
+
);
|
|
3477
|
+
return async (args) => {
|
|
3478
|
+
const objectName = action.objectName;
|
|
3479
|
+
const target = action.target;
|
|
3480
|
+
const result = {
|
|
3481
|
+
ok: false,
|
|
3482
|
+
action: action.name,
|
|
3483
|
+
objectName
|
|
3484
|
+
};
|
|
3485
|
+
if (!objectName) {
|
|
3486
|
+
result.error = "Action has no objectName; cannot dispatch.";
|
|
3487
|
+
return JSON.stringify(result);
|
|
3488
|
+
}
|
|
3489
|
+
if (!target) {
|
|
3490
|
+
result.error = "Action has no target handler.";
|
|
3491
|
+
return JSON.stringify(result);
|
|
3492
|
+
}
|
|
3493
|
+
const recordId = typeof args.recordId === "string" && args.recordId.length > 0 ? args.recordId : void 0;
|
|
3494
|
+
let record;
|
|
3495
|
+
if (requiresRecord) {
|
|
3496
|
+
if (!recordId) {
|
|
3497
|
+
result.error = `recordId is required for this action \u2014 supply the id of the ${objectName} record to act on (use query_data first if you don't have it).`;
|
|
3498
|
+
return JSON.stringify(result);
|
|
3499
|
+
}
|
|
3500
|
+
try {
|
|
3501
|
+
const found = await ctx.dataEngine.find(objectName, {
|
|
3502
|
+
where: { id: recordId },
|
|
3503
|
+
limit: 1
|
|
3504
|
+
});
|
|
3505
|
+
record = found[0];
|
|
3506
|
+
if (!record) {
|
|
3507
|
+
result.error = `Record ${recordId} not found in ${objectName}.`;
|
|
3508
|
+
return JSON.stringify(result);
|
|
3509
|
+
}
|
|
3510
|
+
result.recordId = recordId;
|
|
3511
|
+
} catch (err) {
|
|
3512
|
+
result.error = `Failed to load record: ${err instanceof Error ? err.message : String(err)}`;
|
|
3513
|
+
return JSON.stringify(result);
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
const { recordId: _omit, ...userParams } = args;
|
|
3517
|
+
try {
|
|
3518
|
+
const handlerCtx = {
|
|
3519
|
+
record,
|
|
3520
|
+
user: principal,
|
|
3521
|
+
engine: engineAdapter,
|
|
3522
|
+
params: userParams
|
|
3523
|
+
};
|
|
3524
|
+
const out = await ctx.dataEngine.executeAction?.(objectName, target, handlerCtx);
|
|
3525
|
+
result.ok = true;
|
|
3526
|
+
result.result = out ?? null;
|
|
3527
|
+
const successMsg = typeof action.successMessage === "string" ? action.successMessage : void 0;
|
|
3528
|
+
result.message = successMsg ?? `Action '${action.name}' executed successfully.`;
|
|
3529
|
+
return JSON.stringify(result);
|
|
3530
|
+
} catch (err) {
|
|
3531
|
+
result.error = err instanceof Error ? err.message : String(err);
|
|
3532
|
+
return JSON.stringify(result);
|
|
3533
|
+
}
|
|
3534
|
+
};
|
|
3535
|
+
}
|
|
3536
|
+
async function registerActionsAsTools(registry, context) {
|
|
3537
|
+
const objects = await context.metadata.listObjects();
|
|
3538
|
+
const objMap = new Map(
|
|
3539
|
+
objects.filter((o) => Boolean(o?.name)).map((o) => [o.name, o])
|
|
3540
|
+
);
|
|
3541
|
+
const registered = [];
|
|
3542
|
+
const skipped = [];
|
|
3543
|
+
const prefix = context.toolPrefix ?? "action_";
|
|
3544
|
+
for (const obj of objects) {
|
|
3545
|
+
if (!obj?.actions || !Array.isArray(obj.actions)) continue;
|
|
3546
|
+
for (const action of obj.actions) {
|
|
3547
|
+
if (!action || typeof action.name !== "string") continue;
|
|
3548
|
+
const normalized = {
|
|
3549
|
+
...action,
|
|
3550
|
+
objectName: action.objectName ?? obj.name
|
|
3551
|
+
};
|
|
3552
|
+
const reason = actionSkipReason(normalized);
|
|
3553
|
+
if (reason !== null) {
|
|
3554
|
+
skipped.push({ action: normalized.name, reason });
|
|
3555
|
+
continue;
|
|
3556
|
+
}
|
|
3557
|
+
const definition = actionToToolDefinition(normalized, obj, objMap, prefix);
|
|
3558
|
+
if (!definition) continue;
|
|
3559
|
+
if (registry.has(definition.name)) {
|
|
3560
|
+
skipped.push({ action: normalized.name, reason: "tool name already registered" });
|
|
3561
|
+
continue;
|
|
3562
|
+
}
|
|
3563
|
+
registry.register(definition, createActionToolHandler(normalized, context));
|
|
3564
|
+
registered.push(definition.name);
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
return { registered, skipped };
|
|
3568
|
+
}
|
|
3569
|
+
|
|
2500
3570
|
// src/agent-runtime.ts
|
|
2501
3571
|
import { AgentSchema } from "@objectstack/spec/ai";
|
|
2502
3572
|
var AgentRuntime = class {
|
|
@@ -2799,6 +3869,16 @@ var SkillRegistry = class {
|
|
|
2799
3869
|
const resolved = [];
|
|
2800
3870
|
for (const skill of skills) {
|
|
2801
3871
|
for (const toolName of skill.tools) {
|
|
3872
|
+
if (toolName.endsWith("*")) {
|
|
3873
|
+
const prefix = toolName.slice(0, -1);
|
|
3874
|
+
for (const def2 of availableTools) {
|
|
3875
|
+
if (!def2.name.startsWith(prefix)) continue;
|
|
3876
|
+
if (seen.has(def2.name)) continue;
|
|
3877
|
+
resolved.push(def2);
|
|
3878
|
+
seen.add(def2.name);
|
|
3879
|
+
}
|
|
3880
|
+
continue;
|
|
3881
|
+
}
|
|
2802
3882
|
if (seen.has(toolName)) continue;
|
|
2803
3883
|
const def = toolMap.get(toolName);
|
|
2804
3884
|
if (def) {
|
|
@@ -2862,7 +3942,8 @@ Always answer in the same language the user is using. Detailed tool-usage guidan
|
|
|
2862
3942
|
maxTokens: 4096
|
|
2863
3943
|
},
|
|
2864
3944
|
// Capability bundle lives on the skill; the agent only references it.
|
|
2865
|
-
|
|
3945
|
+
// `data_explorer` = read side, `actions_executor` = write side.
|
|
3946
|
+
skills: ["data_explorer", "actions_executor"],
|
|
2866
3947
|
active: true,
|
|
2867
3948
|
visibility: "global",
|
|
2868
3949
|
guardrails: {
|
|
@@ -2942,6 +4023,7 @@ Guidelines:
|
|
|
2942
4023
|
7. Never expose internal IDs unless the user explicitly asks for them.
|
|
2943
4024
|
8. Always answer in the same language the user is using.`,
|
|
2944
4025
|
tools: [
|
|
4026
|
+
"query_data",
|
|
2945
4027
|
"list_objects",
|
|
2946
4028
|
"describe_object",
|
|
2947
4029
|
"query_records",
|
|
@@ -3011,8 +4093,46 @@ Guidelines:
|
|
|
3011
4093
|
active: true
|
|
3012
4094
|
};
|
|
3013
4095
|
|
|
4096
|
+
// src/skills/actions-executor-skill.ts
|
|
4097
|
+
var ACTIONS_EXECUTOR_SKILL = {
|
|
4098
|
+
name: "actions_executor",
|
|
4099
|
+
label: "Action Executor",
|
|
4100
|
+
description: "Perform business operations on the user's data \u2014 invoke actions like 'mark as complete', 'start task', 'clone record' through natural language.",
|
|
4101
|
+
instructions: `You can perform business operations by invoking the user's registered actions.
|
|
4102
|
+
|
|
4103
|
+
Capabilities:
|
|
4104
|
+
- Each tool whose name starts with \`action_\` is a business operation declared on an object.
|
|
4105
|
+
- Read the tool description carefully \u2014 it tells you what the action does and what record types it applies to.
|
|
4106
|
+
- Most actions need a \`recordId\` argument. If you don't already have one from a prior \`query_data\` call, run \`query_data\` first to find the right record, then invoke the action with its id.
|
|
4107
|
+
|
|
4108
|
+
Guidelines:
|
|
4109
|
+
1. Confirm intent \u2014 when the user says "complete it" / "start that one", make sure you know *which* record they mean. Ask if ambiguous.
|
|
4110
|
+
2. Use \`query_data\` to look up records by natural-language description ("the design review task", "tickets assigned to me").
|
|
4111
|
+
3. After invoking an action, the tool returns \`{ ok, message, result }\`. Summarise success in plain language; surface errors verbatim.
|
|
4112
|
+
4. Never invent recordIds. If \`query_data\` didn't return one, tell the user instead of guessing.
|
|
4113
|
+
5. Action tools are pre-filtered for safety \u2014 destructive operations (\`mode: 'delete'\`, \`variant: 'danger'\`, anything with \`confirmText\`) are *not* exposed here and require explicit user confirmation in the UI.
|
|
4114
|
+
6. Always answer in the same language the user is using.`,
|
|
4115
|
+
// Dynamically materialised: the runtime registers one tool per Action,
|
|
4116
|
+
// and the skill subscribes to the whole family via the `action_*`
|
|
4117
|
+
// glob (resolved by SkillRegistry.flattenToTools).
|
|
4118
|
+
tools: ["action_*"],
|
|
4119
|
+
triggerPhrases: [
|
|
4120
|
+
"complete",
|
|
4121
|
+
"mark as",
|
|
4122
|
+
"start",
|
|
4123
|
+
"finish",
|
|
4124
|
+
"clone",
|
|
4125
|
+
"duplicate",
|
|
4126
|
+
"do it",
|
|
4127
|
+
"run",
|
|
4128
|
+
"invoke",
|
|
4129
|
+
"execute"
|
|
4130
|
+
],
|
|
4131
|
+
active: true
|
|
4132
|
+
};
|
|
4133
|
+
|
|
3014
4134
|
// src/adapters/vercel-adapter.ts
|
|
3015
|
-
import { generateText, streamText, tool as vercelTool, jsonSchema } from "ai";
|
|
4135
|
+
import { generateText, streamText, generateObject, tool as vercelTool, jsonSchema } from "ai";
|
|
3016
4136
|
function buildVercelOptions(options) {
|
|
3017
4137
|
if (!options) return {};
|
|
3018
4138
|
const opts = {};
|
|
@@ -3094,11 +4214,102 @@ var VercelLLMAdapter = class {
|
|
|
3094
4214
|
"[VercelLLMAdapter] Embeddings require a dedicated EmbeddingModel. Configure an embedding adapter instead."
|
|
3095
4215
|
);
|
|
3096
4216
|
}
|
|
4217
|
+
async generateObject(messages, schema, options) {
|
|
4218
|
+
const { schemaName, schemaDescription, ...rest } = options ?? {};
|
|
4219
|
+
const result = await generateObject({
|
|
4220
|
+
model: this.model,
|
|
4221
|
+
messages,
|
|
4222
|
+
schema,
|
|
4223
|
+
schemaName,
|
|
4224
|
+
schemaDescription,
|
|
4225
|
+
...buildVercelOptions(rest)
|
|
4226
|
+
});
|
|
4227
|
+
return {
|
|
4228
|
+
object: result.object,
|
|
4229
|
+
model: result.response?.modelId,
|
|
4230
|
+
usage: result.usage ? {
|
|
4231
|
+
promptTokens: result.usage.inputTokens ?? 0,
|
|
4232
|
+
completionTokens: result.usage.outputTokens ?? 0,
|
|
4233
|
+
totalTokens: result.usage.totalTokens ?? 0
|
|
4234
|
+
} : void 0
|
|
4235
|
+
};
|
|
4236
|
+
}
|
|
3097
4237
|
async listModels() {
|
|
3098
4238
|
return [];
|
|
3099
4239
|
}
|
|
3100
4240
|
};
|
|
3101
4241
|
|
|
4242
|
+
// src/model-registry.ts
|
|
4243
|
+
var ModelRegistry = class {
|
|
4244
|
+
constructor(config = {}) {
|
|
4245
|
+
this.models = /* @__PURE__ */ new Map();
|
|
4246
|
+
for (const model of config.models ?? []) {
|
|
4247
|
+
this.models.set(model.id, model);
|
|
4248
|
+
}
|
|
4249
|
+
this.defaultModelId = config.defaultModelId;
|
|
4250
|
+
}
|
|
4251
|
+
/** Register or replace a model. */
|
|
4252
|
+
register(model) {
|
|
4253
|
+
this.models.set(model.id, model);
|
|
4254
|
+
}
|
|
4255
|
+
/** Look up a model by id. */
|
|
4256
|
+
get(id) {
|
|
4257
|
+
return this.models.get(id);
|
|
4258
|
+
}
|
|
4259
|
+
/** Look up a model by id, throwing if missing. */
|
|
4260
|
+
getOrThrow(id) {
|
|
4261
|
+
const model = this.models.get(id);
|
|
4262
|
+
if (!model) {
|
|
4263
|
+
throw new Error(
|
|
4264
|
+
`[ModelRegistry] Unknown model "${id}". Registered: ${[...this.models.keys()].join(", ") || "(none)"}`
|
|
4265
|
+
);
|
|
4266
|
+
}
|
|
4267
|
+
return model;
|
|
4268
|
+
}
|
|
4269
|
+
/** Resolve the default model (explicit > first registered > undefined). */
|
|
4270
|
+
getDefault() {
|
|
4271
|
+
if (this.defaultModelId) {
|
|
4272
|
+
return this.models.get(this.defaultModelId);
|
|
4273
|
+
}
|
|
4274
|
+
return this.models.values().next().value;
|
|
4275
|
+
}
|
|
4276
|
+
/** Set the default model id (must already be registered). */
|
|
4277
|
+
setDefault(id) {
|
|
4278
|
+
this.getOrThrow(id);
|
|
4279
|
+
this.defaultModelId = id;
|
|
4280
|
+
}
|
|
4281
|
+
/** All registered models. */
|
|
4282
|
+
list() {
|
|
4283
|
+
return [...this.models.values()];
|
|
4284
|
+
}
|
|
4285
|
+
/** Number of registered models. */
|
|
4286
|
+
get size() {
|
|
4287
|
+
return this.models.size;
|
|
4288
|
+
}
|
|
4289
|
+
/**
|
|
4290
|
+
* Estimate cost in the model's currency (defaults to USD).
|
|
4291
|
+
*
|
|
4292
|
+
* Returns `undefined` when the model is unknown or has no pricing data.
|
|
4293
|
+
* Costs are computed as `(tokens / 1000) * pricePer1kTokens` for input and
|
|
4294
|
+
* output independently, then summed.
|
|
4295
|
+
*/
|
|
4296
|
+
estimateCost(modelId, usage) {
|
|
4297
|
+
const model = this.models.get(modelId);
|
|
4298
|
+
if (!model?.pricing) return void 0;
|
|
4299
|
+
return computeCost(model.pricing, usage);
|
|
4300
|
+
}
|
|
4301
|
+
};
|
|
4302
|
+
function computeCost(pricing, usage) {
|
|
4303
|
+
const inputCost = pricing.inputCostPer1kTokens != null ? usage.promptTokens / 1e3 * pricing.inputCostPer1kTokens : 0;
|
|
4304
|
+
const outputCost = pricing.outputCostPer1kTokens != null ? usage.completionTokens / 1e3 * pricing.outputCostPer1kTokens : 0;
|
|
4305
|
+
return {
|
|
4306
|
+
inputCost,
|
|
4307
|
+
outputCost,
|
|
4308
|
+
totalCost: inputCost + outputCost,
|
|
4309
|
+
currency: pricing.currency ?? "USD"
|
|
4310
|
+
};
|
|
4311
|
+
}
|
|
4312
|
+
|
|
3102
4313
|
// src/plugin.ts
|
|
3103
4314
|
var AIServicePlugin = class {
|
|
3104
4315
|
constructor(options = {}) {
|
|
@@ -3220,10 +4431,34 @@ var AIServicePlugin = class {
|
|
|
3220
4431
|
adapterDescription = detected.description;
|
|
3221
4432
|
}
|
|
3222
4433
|
ctx.logger.info(`[AI] Using LLM adapter: ${adapterDescription}`);
|
|
4434
|
+
const modelRegistry = new ModelRegistry({
|
|
4435
|
+
models: this.options.models,
|
|
4436
|
+
defaultModelId: this.options.defaultModelId
|
|
4437
|
+
});
|
|
4438
|
+
if (modelRegistry.size > 0) {
|
|
4439
|
+
ctx.logger.info(`[AI] ModelRegistry initialised with ${modelRegistry.size} model(s)`);
|
|
4440
|
+
}
|
|
4441
|
+
let traceRecorder;
|
|
4442
|
+
if (this.options.traceRecorder === null) {
|
|
4443
|
+
ctx.logger.debug("[AI] Tracing disabled (traceRecorder=null)");
|
|
4444
|
+
} else if (this.options.traceRecorder) {
|
|
4445
|
+
traceRecorder = this.options.traceRecorder;
|
|
4446
|
+
} else {
|
|
4447
|
+
try {
|
|
4448
|
+
const engine = ctx.getService("data");
|
|
4449
|
+
if (engine && typeof engine.insert === "function") {
|
|
4450
|
+
traceRecorder = new ObjectQLTraceRecorder(engine, { logger: ctx.logger });
|
|
4451
|
+
ctx.logger.info("[AI] Using ObjectQLTraceRecorder (IDataEngine detected)");
|
|
4452
|
+
}
|
|
4453
|
+
} catch {
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
3223
4456
|
const config = {
|
|
3224
4457
|
adapter,
|
|
3225
4458
|
logger: ctx.logger,
|
|
3226
|
-
conversationService
|
|
4459
|
+
conversationService,
|
|
4460
|
+
modelRegistry,
|
|
4461
|
+
traceRecorder
|
|
3227
4462
|
};
|
|
3228
4463
|
this.service = new AIService(config);
|
|
3229
4464
|
if (hasExisting) {
|
|
@@ -3238,7 +4473,8 @@ var AIServicePlugin = class {
|
|
|
3238
4473
|
type: "plugin",
|
|
3239
4474
|
scope: "project",
|
|
3240
4475
|
namespace: "ai",
|
|
3241
|
-
objects: [AiConversationObject, AiMessageObject]
|
|
4476
|
+
objects: [AiConversationObject, AiMessageObject, AiTraceObject],
|
|
4477
|
+
views: [AiTraceView]
|
|
3242
4478
|
});
|
|
3243
4479
|
if (this.options.debug) {
|
|
3244
4480
|
ctx.hook("ai:beforeChat", async (messages) => {
|
|
@@ -3270,6 +4506,39 @@ var AIServicePlugin = class {
|
|
|
3270
4506
|
if (dataEngine) {
|
|
3271
4507
|
registerDataTools(this.service.toolRegistry, { dataEngine });
|
|
3272
4508
|
ctx.logger.info("[AI] Built-in data tools registered");
|
|
4509
|
+
if (metadataService) {
|
|
4510
|
+
registerQueryDataTool(this.service.toolRegistry, {
|
|
4511
|
+
ai: this.service,
|
|
4512
|
+
metadata: metadataService,
|
|
4513
|
+
dataEngine
|
|
4514
|
+
});
|
|
4515
|
+
ctx.logger.info("[AI] query_data tool registered");
|
|
4516
|
+
try {
|
|
4517
|
+
const { registered, skipped } = await registerActionsAsTools(
|
|
4518
|
+
this.service.toolRegistry,
|
|
4519
|
+
{
|
|
4520
|
+
metadata: metadataService,
|
|
4521
|
+
dataEngine
|
|
4522
|
+
}
|
|
4523
|
+
);
|
|
4524
|
+
if (registered.length > 0) {
|
|
4525
|
+
ctx.logger.info(
|
|
4526
|
+
`[AI] ${registered.length} action tool(s) registered: ${registered.join(", ")}`
|
|
4527
|
+
);
|
|
4528
|
+
}
|
|
4529
|
+
if (skipped.length > 0) {
|
|
4530
|
+
ctx.logger.debug(
|
|
4531
|
+
`[AI] Skipped ${skipped.length} action(s) for AI exposure`,
|
|
4532
|
+
{ skipped }
|
|
4533
|
+
);
|
|
4534
|
+
}
|
|
4535
|
+
} catch (err) {
|
|
4536
|
+
ctx.logger.warn(
|
|
4537
|
+
"[AI] Failed to register action tools",
|
|
4538
|
+
err instanceof Error ? { error: err.message } : { error: String(err) }
|
|
4539
|
+
);
|
|
4540
|
+
}
|
|
4541
|
+
}
|
|
3273
4542
|
if (metadataService) {
|
|
3274
4543
|
const { DATA_TOOL_DEFINITIONS: DATA_TOOL_DEFINITIONS2 } = await Promise.resolve().then(() => (init_data_tools(), data_tools_exports));
|
|
3275
4544
|
for (const toolDef of DATA_TOOL_DEFINITIONS2) {
|
|
@@ -3320,6 +4589,19 @@ var AIServicePlugin = class {
|
|
|
3320
4589
|
} catch (err) {
|
|
3321
4590
|
ctx.logger.warn("[AI] Failed to register data_explorer skill", err instanceof Error ? { error: err.message } : { error: String(err) });
|
|
3322
4591
|
}
|
|
4592
|
+
try {
|
|
4593
|
+
const skillExists = typeof metadataService.exists === "function" ? await withTimeout(metadataService.exists("skill", ACTIONS_EXECUTOR_SKILL.name)) : false;
|
|
4594
|
+
if (skillExists === null) {
|
|
4595
|
+
ctx.logger.warn("[AI] Metadata service timed out checking actions_executor skill, skipping");
|
|
4596
|
+
} else if (!skillExists) {
|
|
4597
|
+
await withTimeout(metadataService.register("skill", ACTIONS_EXECUTOR_SKILL.name, ACTIONS_EXECUTOR_SKILL));
|
|
4598
|
+
ctx.logger.info("[AI] actions_executor skill registered");
|
|
4599
|
+
} else {
|
|
4600
|
+
ctx.logger.debug("[AI] actions_executor skill already exists, skipping auto-registration");
|
|
4601
|
+
}
|
|
4602
|
+
} catch (err) {
|
|
4603
|
+
ctx.logger.warn("[AI] Failed to register actions_executor skill", err instanceof Error ? { error: err.message } : { error: String(err) });
|
|
4604
|
+
}
|
|
3323
4605
|
}
|
|
3324
4606
|
}
|
|
3325
4607
|
} catch {
|
|
@@ -3779,29 +5061,45 @@ function registerPackageTools(registry, context) {
|
|
|
3779
5061
|
registry.register(setActivePackageTool, createSetActivePackageHandler(context));
|
|
3780
5062
|
}
|
|
3781
5063
|
export {
|
|
5064
|
+
ACTIONS_EXECUTOR_SKILL,
|
|
3782
5065
|
AIService,
|
|
3783
5066
|
AIServicePlugin,
|
|
3784
5067
|
AgentRuntime,
|
|
3785
5068
|
AiConversationObject,
|
|
3786
5069
|
AiMessageObject,
|
|
5070
|
+
AiTraceObject,
|
|
5071
|
+
AiTraceView,
|
|
3787
5072
|
DATA_CHAT_AGENT,
|
|
5073
|
+
DATA_EXPLORER_SKILL,
|
|
3788
5074
|
DATA_TOOL_DEFINITIONS,
|
|
3789
5075
|
InMemoryConversationService,
|
|
3790
5076
|
METADATA_ASSISTANT_AGENT,
|
|
5077
|
+
METADATA_AUTHORING_SKILL,
|
|
3791
5078
|
METADATA_TOOL_DEFINITIONS,
|
|
3792
5079
|
MemoryLLMAdapter,
|
|
5080
|
+
ModelRegistry,
|
|
5081
|
+
NullTraceRecorder,
|
|
3793
5082
|
ObjectQLConversationService,
|
|
5083
|
+
ObjectQLTraceRecorder,
|
|
3794
5084
|
PACKAGE_TOOL_DEFINITIONS,
|
|
5085
|
+
QUERY_DATA_TOOL,
|
|
5086
|
+
SchemaRetriever,
|
|
3795
5087
|
SkillRegistry,
|
|
3796
5088
|
ToolRegistry,
|
|
3797
5089
|
VercelLLMAdapter,
|
|
5090
|
+
actionSkipReason,
|
|
5091
|
+
actionToToolDefinition,
|
|
5092
|
+
actionToolName,
|
|
3798
5093
|
addFieldTool,
|
|
3799
5094
|
buildAIRoutes,
|
|
3800
5095
|
buildAgentRoutes,
|
|
3801
5096
|
buildAssistantRoutes,
|
|
3802
5097
|
buildToolRoutes,
|
|
5098
|
+
buildTraceEvent,
|
|
5099
|
+
computeCost,
|
|
3803
5100
|
createObjectTool,
|
|
3804
5101
|
createPackageTool,
|
|
5102
|
+
createQueryDataHandler,
|
|
3805
5103
|
deleteFieldTool,
|
|
3806
5104
|
describeObjectTool,
|
|
3807
5105
|
encodeStreamPart,
|
|
@@ -3811,9 +5109,11 @@ export {
|
|
|
3811
5109
|
listObjectsTool,
|
|
3812
5110
|
listPackagesTool,
|
|
3813
5111
|
modifyFieldTool,
|
|
5112
|
+
registerActionsAsTools,
|
|
3814
5113
|
registerDataTools,
|
|
3815
5114
|
registerMetadataTools,
|
|
3816
5115
|
registerPackageTools,
|
|
5116
|
+
registerQueryDataTool,
|
|
3817
5117
|
setActivePackageTool
|
|
3818
5118
|
};
|
|
3819
5119
|
//# sourceMappingURL=index.js.map
|