@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.cjs
CHANGED
|
@@ -124,7 +124,9 @@ var init_data_tools = __esm({
|
|
|
124
124
|
properties: {
|
|
125
125
|
field: { type: "string" },
|
|
126
126
|
order: { type: "string", enum: ["asc", "desc"] }
|
|
127
|
-
}
|
|
127
|
+
},
|
|
128
|
+
required: ["field", "order"],
|
|
129
|
+
additionalProperties: false
|
|
128
130
|
},
|
|
129
131
|
description: 'Sort order (e.g. [{ "field": "created_at", "order": "desc" }])'
|
|
130
132
|
},
|
|
@@ -194,7 +196,8 @@ var init_data_tools = __esm({
|
|
|
194
196
|
description: "Result column alias"
|
|
195
197
|
}
|
|
196
198
|
},
|
|
197
|
-
required: ["function", "alias"]
|
|
199
|
+
required: ["function", "alias"],
|
|
200
|
+
additionalProperties: false
|
|
198
201
|
},
|
|
199
202
|
description: "Aggregation definitions"
|
|
200
203
|
},
|
|
@@ -845,29 +848,45 @@ var init_metadata_tools = __esm({
|
|
|
845
848
|
// src/index.ts
|
|
846
849
|
var index_exports = {};
|
|
847
850
|
__export(index_exports, {
|
|
851
|
+
ACTIONS_EXECUTOR_SKILL: () => ACTIONS_EXECUTOR_SKILL,
|
|
848
852
|
AIService: () => AIService,
|
|
849
853
|
AIServicePlugin: () => AIServicePlugin,
|
|
850
854
|
AgentRuntime: () => AgentRuntime,
|
|
851
855
|
AiConversationObject: () => AiConversationObject,
|
|
852
856
|
AiMessageObject: () => AiMessageObject,
|
|
857
|
+
AiTraceObject: () => AiTraceObject,
|
|
858
|
+
AiTraceView: () => AiTraceView,
|
|
853
859
|
DATA_CHAT_AGENT: () => DATA_CHAT_AGENT,
|
|
860
|
+
DATA_EXPLORER_SKILL: () => DATA_EXPLORER_SKILL,
|
|
854
861
|
DATA_TOOL_DEFINITIONS: () => DATA_TOOL_DEFINITIONS,
|
|
855
862
|
InMemoryConversationService: () => InMemoryConversationService,
|
|
856
863
|
METADATA_ASSISTANT_AGENT: () => METADATA_ASSISTANT_AGENT,
|
|
864
|
+
METADATA_AUTHORING_SKILL: () => METADATA_AUTHORING_SKILL,
|
|
857
865
|
METADATA_TOOL_DEFINITIONS: () => METADATA_TOOL_DEFINITIONS,
|
|
858
866
|
MemoryLLMAdapter: () => MemoryLLMAdapter,
|
|
867
|
+
ModelRegistry: () => ModelRegistry,
|
|
868
|
+
NullTraceRecorder: () => NullTraceRecorder,
|
|
859
869
|
ObjectQLConversationService: () => ObjectQLConversationService,
|
|
870
|
+
ObjectQLTraceRecorder: () => ObjectQLTraceRecorder,
|
|
860
871
|
PACKAGE_TOOL_DEFINITIONS: () => PACKAGE_TOOL_DEFINITIONS,
|
|
872
|
+
QUERY_DATA_TOOL: () => QUERY_DATA_TOOL,
|
|
873
|
+
SchemaRetriever: () => SchemaRetriever,
|
|
861
874
|
SkillRegistry: () => SkillRegistry,
|
|
862
875
|
ToolRegistry: () => ToolRegistry,
|
|
863
876
|
VercelLLMAdapter: () => VercelLLMAdapter,
|
|
877
|
+
actionSkipReason: () => actionSkipReason,
|
|
878
|
+
actionToToolDefinition: () => actionToToolDefinition,
|
|
879
|
+
actionToolName: () => actionToolName,
|
|
864
880
|
addFieldTool: () => addFieldTool,
|
|
865
881
|
buildAIRoutes: () => buildAIRoutes,
|
|
866
882
|
buildAgentRoutes: () => buildAgentRoutes,
|
|
867
883
|
buildAssistantRoutes: () => buildAssistantRoutes,
|
|
868
884
|
buildToolRoutes: () => buildToolRoutes,
|
|
885
|
+
buildTraceEvent: () => buildTraceEvent,
|
|
886
|
+
computeCost: () => computeCost,
|
|
869
887
|
createObjectTool: () => createObjectTool,
|
|
870
888
|
createPackageTool: () => createPackageTool,
|
|
889
|
+
createQueryDataHandler: () => createQueryDataHandler,
|
|
871
890
|
deleteFieldTool: () => deleteFieldTool,
|
|
872
891
|
describeObjectTool: () => describeObjectTool,
|
|
873
892
|
encodeStreamPart: () => encodeStreamPart,
|
|
@@ -877,9 +896,11 @@ __export(index_exports, {
|
|
|
877
896
|
listObjectsTool: () => listObjectsTool,
|
|
878
897
|
listPackagesTool: () => listPackagesTool,
|
|
879
898
|
modifyFieldTool: () => modifyFieldTool,
|
|
899
|
+
registerActionsAsTools: () => registerActionsAsTools,
|
|
880
900
|
registerDataTools: () => registerDataTools,
|
|
881
901
|
registerMetadataTools: () => registerMetadataTools,
|
|
882
902
|
registerPackageTools: () => registerPackageTools,
|
|
903
|
+
registerQueryDataTool: () => registerQueryDataTool,
|
|
883
904
|
setActivePackageTool: () => setActivePackageTool
|
|
884
905
|
});
|
|
885
906
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -895,8 +916,112 @@ var MemoryLLMAdapter = class {
|
|
|
895
916
|
async chat(messages, options) {
|
|
896
917
|
const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
|
|
897
918
|
const userContent = lastUserMessage?.content;
|
|
898
|
-
const
|
|
899
|
-
const
|
|
919
|
+
const userText = typeof userContent === "string" ? userContent : "(complex content)";
|
|
920
|
+
const tools = options?.tools;
|
|
921
|
+
const hasQueryDataTool = Array.isArray(tools) && tools.some((t) => t?.name === "query_data");
|
|
922
|
+
const alreadyCalledQueryData = messages.some(
|
|
923
|
+
(m) => m.role === "tool" && Array.isArray(m.content) && m.content.some((c) => c?.toolName === "query_data")
|
|
924
|
+
);
|
|
925
|
+
const alreadyCalledAction = messages.some(
|
|
926
|
+
(m) => m.role === "tool" && Array.isArray(m.content) && m.content.some(
|
|
927
|
+
(c) => typeof c?.toolName === "string" && c.toolName.startsWith("action_")
|
|
928
|
+
)
|
|
929
|
+
);
|
|
930
|
+
if (Array.isArray(tools) && !alreadyCalledAction && lastUserMessage) {
|
|
931
|
+
const actionTools = tools.filter((t) => typeof t?.name === "string" && t.name.startsWith("action_"));
|
|
932
|
+
const chosen = pickActionTool(userText, actionTools);
|
|
933
|
+
if (chosen) {
|
|
934
|
+
const recordId = extractRecordIdFromMessages(messages, userText);
|
|
935
|
+
if (recordId) {
|
|
936
|
+
const toolCallId = `memory_tc_${Date.now().toString(36)}`;
|
|
937
|
+
return {
|
|
938
|
+
content: "",
|
|
939
|
+
model: options?.model ?? "memory",
|
|
940
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
941
|
+
toolCalls: [
|
|
942
|
+
{
|
|
943
|
+
type: "tool-call",
|
|
944
|
+
toolCallId,
|
|
945
|
+
toolName: chosen.name,
|
|
946
|
+
input: { recordId }
|
|
947
|
+
}
|
|
948
|
+
]
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
if (hasQueryDataTool && !alreadyCalledQueryData && lastUserMessage) {
|
|
954
|
+
const toolCallId = `memory_tc_${Date.now().toString(36)}`;
|
|
955
|
+
return {
|
|
956
|
+
content: "",
|
|
957
|
+
model: options?.model ?? "memory",
|
|
958
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
959
|
+
toolCalls: [
|
|
960
|
+
{
|
|
961
|
+
type: "tool-call",
|
|
962
|
+
toolCallId,
|
|
963
|
+
toolName: "query_data",
|
|
964
|
+
input: { request: userText }
|
|
965
|
+
}
|
|
966
|
+
]
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
if (alreadyCalledAction) {
|
|
970
|
+
const lastTool = [...messages].reverse().find((m) => m.role === "tool");
|
|
971
|
+
const part = Array.isArray(lastTool?.content) ? lastTool.content.find((c) => typeof c?.toolName === "string" && c.toolName.startsWith("action_")) : void 0;
|
|
972
|
+
const raw = part?.output && typeof part.output === "object" && "value" in part.output ? part.output.value : part?.result;
|
|
973
|
+
let payload = {};
|
|
974
|
+
if (typeof raw === "string") {
|
|
975
|
+
try {
|
|
976
|
+
payload = JSON.parse(raw);
|
|
977
|
+
} catch {
|
|
978
|
+
}
|
|
979
|
+
} else if (raw && typeof raw === "object") {
|
|
980
|
+
payload = raw;
|
|
981
|
+
}
|
|
982
|
+
if (payload.error) {
|
|
983
|
+
return {
|
|
984
|
+
content: `[memory] action ${payload.action ?? ""} failed: ${payload.error}`,
|
|
985
|
+
model: options?.model ?? "memory",
|
|
986
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
return {
|
|
990
|
+
content: `[memory] ${payload.message ?? "Action executed."} (${payload.action ?? "action"})`,
|
|
991
|
+
model: options?.model ?? "memory",
|
|
992
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
if (alreadyCalledQueryData) {
|
|
996
|
+
const lastTool = [...messages].reverse().find((m) => m.role === "tool");
|
|
997
|
+
const part = Array.isArray(lastTool?.content) ? lastTool.content.find((c) => c?.toolName === "query_data") : void 0;
|
|
998
|
+
let payload = {};
|
|
999
|
+
const raw = part?.output && typeof part.output === "object" && "value" in part.output ? part.output.value : part?.result;
|
|
1000
|
+
if (typeof raw === "string") {
|
|
1001
|
+
try {
|
|
1002
|
+
payload = JSON.parse(raw);
|
|
1003
|
+
} catch {
|
|
1004
|
+
payload = {};
|
|
1005
|
+
}
|
|
1006
|
+
} else if (raw && typeof raw === "object") {
|
|
1007
|
+
payload = raw;
|
|
1008
|
+
}
|
|
1009
|
+
if (payload.error) {
|
|
1010
|
+
return {
|
|
1011
|
+
content: `[memory] query_data failed: ${payload.error}`,
|
|
1012
|
+
model: options?.model ?? "memory",
|
|
1013
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
const records = payload.records ?? [];
|
|
1017
|
+
const count = payload.count ?? records.length;
|
|
1018
|
+
return {
|
|
1019
|
+
content: `[memory] Found ${count} record${count === 1 ? "" : "s"} for "${userText}".`,
|
|
1020
|
+
model: options?.model ?? "memory",
|
|
1021
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
const content = lastUserMessage ? `[memory] ${userText}` : "[memory] (no user message)";
|
|
900
1025
|
return {
|
|
901
1026
|
content,
|
|
902
1027
|
model: options?.model ?? "memory",
|
|
@@ -931,7 +1056,171 @@ var MemoryLLMAdapter = class {
|
|
|
931
1056
|
async listModels() {
|
|
932
1057
|
return ["memory"];
|
|
933
1058
|
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Heuristic structured-output for testing & demos — NOT a real LLM.
|
|
1061
|
+
*
|
|
1062
|
+
* Strategy:
|
|
1063
|
+
* 1. Extract candidate object names from the system messages by matching
|
|
1064
|
+
* schema-context headers (`### name — Label`) emitted by
|
|
1065
|
+
* {@link SchemaRetriever.renderSnippet}.
|
|
1066
|
+
* 2. Pick the candidate whose tokens overlap most with the last user
|
|
1067
|
+
* message (falls back to the first candidate).
|
|
1068
|
+
* 3. Try `schema.safeParse({ objectName, limit: 20 })` — this satisfies the
|
|
1069
|
+
* `QueryPlanSchema` used by the built-in `query_data` tool.
|
|
1070
|
+
* 4. If that fails, fall back to `schema.safeParse({})` for schemas that
|
|
1071
|
+
* accept defaults.
|
|
1072
|
+
* 5. Otherwise throw with a clear message — the demo needs a real provider.
|
|
1073
|
+
*/
|
|
1074
|
+
async generateObject(messages, schema, options) {
|
|
1075
|
+
const sys = messages.filter((m) => m.role === "system").map((m) => typeof m.content === "string" ? m.content : "").join("\n");
|
|
1076
|
+
const headerRe = /^###\s+([a-z0-9_]+)(?:\s+—\s+([^\n]+))?/gim;
|
|
1077
|
+
const candidates = [];
|
|
1078
|
+
for (const match of sys.matchAll(headerRe)) {
|
|
1079
|
+
const machineName = match[1];
|
|
1080
|
+
if (!machineName) continue;
|
|
1081
|
+
const aliasText = match[2] ?? "";
|
|
1082
|
+
const aliasTokens = /* @__PURE__ */ new Set();
|
|
1083
|
+
for (const t of machineName.split(/[^a-z0-9]+/)) {
|
|
1084
|
+
if (t) aliasTokens.add(t);
|
|
1085
|
+
}
|
|
1086
|
+
for (const t of aliasText.toLowerCase().split(/[^a-z0-9]+/)) {
|
|
1087
|
+
if (t) aliasTokens.add(t);
|
|
1088
|
+
}
|
|
1089
|
+
for (const t of [...aliasTokens]) {
|
|
1090
|
+
if (t.length > 3 && t.endsWith("s")) aliasTokens.add(t.slice(0, -1));
|
|
1091
|
+
}
|
|
1092
|
+
candidates.push({ name: machineName, aliasTokens });
|
|
1093
|
+
}
|
|
1094
|
+
const lastUser = [...messages].reverse().find((m) => m.role === "user");
|
|
1095
|
+
const userText = typeof lastUser?.content === "string" ? lastUser.content.toLowerCase() : "";
|
|
1096
|
+
const userTokens = new Set(
|
|
1097
|
+
userText.split(/[^a-z0-9_]+/).filter((t) => t.length > 1)
|
|
1098
|
+
);
|
|
1099
|
+
for (const t of [...userTokens]) {
|
|
1100
|
+
if (t.length > 3 && t.endsWith("s")) userTokens.add(t.slice(0, -1));
|
|
1101
|
+
}
|
|
1102
|
+
let chosen = candidates[0]?.name;
|
|
1103
|
+
let bestScore = -1;
|
|
1104
|
+
for (const cand of candidates) {
|
|
1105
|
+
let score = 0;
|
|
1106
|
+
for (const tok of cand.aliasTokens) {
|
|
1107
|
+
if (userTokens.has(tok)) score += 1;
|
|
1108
|
+
}
|
|
1109
|
+
if (score > bestScore) {
|
|
1110
|
+
bestScore = score;
|
|
1111
|
+
chosen = cand.name;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
const attempts = [];
|
|
1115
|
+
if (chosen) attempts.push({ objectName: chosen, limit: 20 });
|
|
1116
|
+
attempts.push({});
|
|
1117
|
+
for (const attempt of attempts) {
|
|
1118
|
+
const result = schema.safeParse(attempt);
|
|
1119
|
+
if (result.success) {
|
|
1120
|
+
return {
|
|
1121
|
+
object: result.data,
|
|
1122
|
+
model: options?.model ?? "memory",
|
|
1123
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
throw new Error(
|
|
1128
|
+
"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."
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
934
1131
|
};
|
|
1132
|
+
function pickActionTool(userText, actionTools) {
|
|
1133
|
+
if (actionTools.length === 0 || !userText) return null;
|
|
1134
|
+
const userTokens = new Set(
|
|
1135
|
+
userText.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2)
|
|
1136
|
+
);
|
|
1137
|
+
const ACTION_VERBS = /* @__PURE__ */ new Set([
|
|
1138
|
+
"complete",
|
|
1139
|
+
"finish",
|
|
1140
|
+
"done",
|
|
1141
|
+
"close",
|
|
1142
|
+
"start",
|
|
1143
|
+
"begin",
|
|
1144
|
+
"resume",
|
|
1145
|
+
"clone",
|
|
1146
|
+
"copy",
|
|
1147
|
+
"duplicate",
|
|
1148
|
+
"cancel",
|
|
1149
|
+
"abort",
|
|
1150
|
+
"archive",
|
|
1151
|
+
"restore",
|
|
1152
|
+
"approve",
|
|
1153
|
+
"reject",
|
|
1154
|
+
"assign",
|
|
1155
|
+
"unassign",
|
|
1156
|
+
"export",
|
|
1157
|
+
"import",
|
|
1158
|
+
"send",
|
|
1159
|
+
"notify",
|
|
1160
|
+
"publish",
|
|
1161
|
+
"unpublish",
|
|
1162
|
+
"mark"
|
|
1163
|
+
]);
|
|
1164
|
+
const hasActionVerb = [...userTokens].some((t) => ACTION_VERBS.has(t));
|
|
1165
|
+
if (!hasActionVerb) return null;
|
|
1166
|
+
let best = null;
|
|
1167
|
+
let bestScore = 0;
|
|
1168
|
+
for (const tool of actionTools) {
|
|
1169
|
+
const nameTokens = tool.name.replace(/^action_/, "").toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2);
|
|
1170
|
+
let score = 0;
|
|
1171
|
+
for (const tok of nameTokens) {
|
|
1172
|
+
if (!userTokens.has(tok)) continue;
|
|
1173
|
+
score += ACTION_VERBS.has(tok) ? 3 : 1;
|
|
1174
|
+
}
|
|
1175
|
+
if (score > bestScore) {
|
|
1176
|
+
bestScore = score;
|
|
1177
|
+
best = tool;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
return bestScore >= 3 ? best : null;
|
|
1181
|
+
}
|
|
1182
|
+
function extractRecordIdFromMessages(messages, userText) {
|
|
1183
|
+
const userTokens = userText.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2);
|
|
1184
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1185
|
+
const m = messages[i];
|
|
1186
|
+
if (m.role !== "tool" || !Array.isArray(m.content)) continue;
|
|
1187
|
+
const parts = m.content;
|
|
1188
|
+
for (const part of parts) {
|
|
1189
|
+
if (part?.toolName !== "query_data") continue;
|
|
1190
|
+
const raw = part.output && typeof part.output === "object" && "value" in part.output ? part.output.value : part.result;
|
|
1191
|
+
let payload = {};
|
|
1192
|
+
if (typeof raw === "string") {
|
|
1193
|
+
try {
|
|
1194
|
+
payload = JSON.parse(raw);
|
|
1195
|
+
} catch {
|
|
1196
|
+
}
|
|
1197
|
+
} else if (raw && typeof raw === "object") {
|
|
1198
|
+
payload = raw;
|
|
1199
|
+
}
|
|
1200
|
+
const records = payload.records ?? [];
|
|
1201
|
+
if (records.length === 0) continue;
|
|
1202
|
+
let bestId;
|
|
1203
|
+
let bestScore = -1;
|
|
1204
|
+
for (const rec of records) {
|
|
1205
|
+
if (!rec || typeof rec !== "object") continue;
|
|
1206
|
+
const id = rec.id;
|
|
1207
|
+
if (typeof id !== "string" && typeof id !== "number") continue;
|
|
1208
|
+
const hay = Object.values(rec).filter((v) => typeof v === "string").join(" ").toLowerCase();
|
|
1209
|
+
const hayTokens = hay.split(/[^a-z0-9]+/).filter(Boolean);
|
|
1210
|
+
let score = 0;
|
|
1211
|
+
for (const ut of userTokens) {
|
|
1212
|
+
if (hayTokens.includes(ut)) score += 1;
|
|
1213
|
+
}
|
|
1214
|
+
if (score > bestScore) {
|
|
1215
|
+
bestScore = score;
|
|
1216
|
+
bestId = String(id);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
return bestId ?? String(records[0].id);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
return void 0;
|
|
1223
|
+
}
|
|
935
1224
|
|
|
936
1225
|
// src/tools/tool-registry.ts
|
|
937
1226
|
var ToolRegistry = class {
|
|
@@ -1097,6 +1386,70 @@ var InMemoryConversationService = class {
|
|
|
1097
1386
|
}
|
|
1098
1387
|
};
|
|
1099
1388
|
|
|
1389
|
+
// src/trace-recorder.ts
|
|
1390
|
+
var import_node_crypto = require("crypto");
|
|
1391
|
+
var TRACE_OBJECT = "ai_traces";
|
|
1392
|
+
var NullTraceRecorder = class {
|
|
1393
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1394
|
+
record(_event) {
|
|
1395
|
+
}
|
|
1396
|
+
};
|
|
1397
|
+
var ObjectQLTraceRecorder = class {
|
|
1398
|
+
constructor(engine, options = {}) {
|
|
1399
|
+
this.engine = engine;
|
|
1400
|
+
this.logger = options.logger;
|
|
1401
|
+
}
|
|
1402
|
+
async record(event) {
|
|
1403
|
+
const row = {
|
|
1404
|
+
id: `trace_${(0, import_node_crypto.randomUUID)()}`,
|
|
1405
|
+
conversation_id: event.conversationId ?? null,
|
|
1406
|
+
agent_id: event.agentId ?? null,
|
|
1407
|
+
operation: event.operation,
|
|
1408
|
+
model: event.model ?? null,
|
|
1409
|
+
adapter: event.adapter,
|
|
1410
|
+
prompt_tokens: event.promptTokens,
|
|
1411
|
+
completion_tokens: event.completionTokens,
|
|
1412
|
+
total_tokens: event.totalTokens,
|
|
1413
|
+
input_cost: event.cost?.inputCost ?? null,
|
|
1414
|
+
output_cost: event.cost?.outputCost ?? null,
|
|
1415
|
+
total_cost: event.cost?.totalCost ?? null,
|
|
1416
|
+
currency: event.cost?.currency ?? null,
|
|
1417
|
+
latency_ms: event.latencyMs,
|
|
1418
|
+
status: event.status,
|
|
1419
|
+
error: event.error ?? null,
|
|
1420
|
+
metadata: event.metadata ? JSON.stringify(event.metadata) : null,
|
|
1421
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1422
|
+
};
|
|
1423
|
+
try {
|
|
1424
|
+
await this.engine.insert(TRACE_OBJECT, row);
|
|
1425
|
+
} catch (err) {
|
|
1426
|
+
this.logger?.warn(
|
|
1427
|
+
"[AI] Failed to record trace (non-fatal)",
|
|
1428
|
+
err instanceof Error ? { error: err.message } : { error: String(err) }
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
};
|
|
1433
|
+
function buildTraceEvent(input) {
|
|
1434
|
+
const usage = input.usage ?? { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
1435
|
+
const cost = input.model && input.registry ? input.registry.estimateCost(input.model, usage) : void 0;
|
|
1436
|
+
return {
|
|
1437
|
+
operation: input.operation,
|
|
1438
|
+
adapter: input.adapter,
|
|
1439
|
+
model: input.model,
|
|
1440
|
+
agentId: input.agentId,
|
|
1441
|
+
conversationId: input.conversationId,
|
|
1442
|
+
promptTokens: usage.promptTokens,
|
|
1443
|
+
completionTokens: usage.completionTokens,
|
|
1444
|
+
totalTokens: usage.totalTokens,
|
|
1445
|
+
latencyMs: input.latencyMs,
|
|
1446
|
+
status: input.status,
|
|
1447
|
+
error: input.error,
|
|
1448
|
+
cost,
|
|
1449
|
+
metadata: input.metadata
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1100
1453
|
// src/ai-service.ts
|
|
1101
1454
|
function textDeltaPart(id, text) {
|
|
1102
1455
|
return { type: "text-delta", id, text };
|
|
@@ -1115,22 +1468,83 @@ var _AIService = class _AIService {
|
|
|
1115
1468
|
this.logger = config.logger ?? (0, import_core.createLogger)({ level: "info", format: "pretty" });
|
|
1116
1469
|
this.toolRegistry = config.toolRegistry ?? new ToolRegistry();
|
|
1117
1470
|
this.conversationService = config.conversationService ?? new InMemoryConversationService();
|
|
1471
|
+
this.modelRegistry = config.modelRegistry;
|
|
1472
|
+
this.traceRecorder = config.traceRecorder ?? new NullTraceRecorder();
|
|
1118
1473
|
this.logger.info(
|
|
1119
|
-
`[AI] Service initialized with adapter="${this.adapter.name}", tools=${this.toolRegistry.size}`
|
|
1474
|
+
`[AI] Service initialized with adapter="${this.adapter.name}", tools=${this.toolRegistry.size}, models=${this.modelRegistry?.size ?? 0}`
|
|
1120
1475
|
);
|
|
1121
1476
|
}
|
|
1122
1477
|
/** The name of the active LLM adapter. */
|
|
1123
1478
|
get adapterName() {
|
|
1124
1479
|
return this.adapter.name;
|
|
1125
1480
|
}
|
|
1481
|
+
/**
|
|
1482
|
+
* Run an adapter call and emit a trace event.
|
|
1483
|
+
*
|
|
1484
|
+
* Records both success and failure. Tracing failures never escape — the
|
|
1485
|
+
* recorder is expected to be defensive.
|
|
1486
|
+
*/
|
|
1487
|
+
async instrument(operation, options, fn) {
|
|
1488
|
+
const started = Date.now();
|
|
1489
|
+
try {
|
|
1490
|
+
const result = await fn();
|
|
1491
|
+
void this.traceRecorder.record(buildTraceEvent({
|
|
1492
|
+
operation,
|
|
1493
|
+
adapter: this.adapter.name,
|
|
1494
|
+
model: result.model ?? options?.model,
|
|
1495
|
+
usage: result.usage,
|
|
1496
|
+
latencyMs: Date.now() - started,
|
|
1497
|
+
status: "success",
|
|
1498
|
+
registry: this.modelRegistry
|
|
1499
|
+
}));
|
|
1500
|
+
return result;
|
|
1501
|
+
} catch (err) {
|
|
1502
|
+
void this.traceRecorder.record(buildTraceEvent({
|
|
1503
|
+
operation,
|
|
1504
|
+
adapter: this.adapter.name,
|
|
1505
|
+
model: options?.model,
|
|
1506
|
+
latencyMs: Date.now() - started,
|
|
1507
|
+
status: "error",
|
|
1508
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1509
|
+
registry: this.modelRegistry
|
|
1510
|
+
}));
|
|
1511
|
+
throw err;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1126
1514
|
// ── IAIService implementation ──────────────────────────────────
|
|
1127
1515
|
async chat(messages, options) {
|
|
1128
1516
|
this.logger.debug("[AI] chat", { messageCount: messages.length, model: options?.model });
|
|
1129
|
-
return this.adapter.chat(messages, options);
|
|
1517
|
+
return this.instrument("chat", options, () => this.adapter.chat(messages, options));
|
|
1130
1518
|
}
|
|
1131
1519
|
async complete(prompt, options) {
|
|
1132
1520
|
this.logger.debug("[AI] complete", { promptLength: prompt.length, model: options?.model });
|
|
1133
|
-
return this.adapter.complete(prompt, options);
|
|
1521
|
+
return this.instrument("complete", options, () => this.adapter.complete(prompt, options));
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Generate a strongly-typed object validated against a Zod schema.
|
|
1525
|
+
*
|
|
1526
|
+
* Delegates to the adapter's `generateObject` when supported; throws a
|
|
1527
|
+
* descriptive error when the adapter does not implement structured output.
|
|
1528
|
+
*
|
|
1529
|
+
* @example
|
|
1530
|
+
* ```ts
|
|
1531
|
+
* import { z } from 'zod';
|
|
1532
|
+
* const Schema = z.object({ name: z.string(), priority: z.number().int() });
|
|
1533
|
+
* const { object } = await ai.generateObject(messages, Schema);
|
|
1534
|
+
* ```
|
|
1535
|
+
*/
|
|
1536
|
+
async generateObject(messages, schema, options) {
|
|
1537
|
+
this.logger.debug("[AI] generateObject", { messageCount: messages.length, model: options?.model });
|
|
1538
|
+
if (!this.adapter.generateObject) {
|
|
1539
|
+
throw new Error(
|
|
1540
|
+
`[AI] Adapter "${this.adapter.name}" does not support generateObject. Use VercelLLMAdapter with a structured-output-capable model.`
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
return this.instrument(
|
|
1544
|
+
"generate_object",
|
|
1545
|
+
options,
|
|
1546
|
+
() => this.adapter.generateObject(messages, schema, options)
|
|
1547
|
+
);
|
|
1134
1548
|
}
|
|
1135
1549
|
async *streamChat(messages, options) {
|
|
1136
1550
|
this.logger.debug("[AI] streamChat", { messageCount: messages.length, model: options?.model });
|
|
@@ -1170,6 +1584,13 @@ var _AIService = class _AIService {
|
|
|
1170
1584
|
* maximum number of iterations (`maxIterations`) is reached.
|
|
1171
1585
|
*/
|
|
1172
1586
|
async chatWithTools(messages, options) {
|
|
1587
|
+
return this.instrument(
|
|
1588
|
+
"chat_with_tools",
|
|
1589
|
+
options,
|
|
1590
|
+
() => this.chatWithToolsImpl(messages, options)
|
|
1591
|
+
);
|
|
1592
|
+
}
|
|
1593
|
+
async chatWithToolsImpl(messages, options) {
|
|
1173
1594
|
const { maxIterations: maxIter, onToolError, ...restOptions } = options ?? {};
|
|
1174
1595
|
const maxIterations = maxIter ?? _AIService.DEFAULT_MAX_ITERATIONS;
|
|
1175
1596
|
const registeredTools = this.toolRegistry.getAll();
|
|
@@ -2212,7 +2633,7 @@ function buildToolRoutes(aiService, logger) {
|
|
|
2212
2633
|
}
|
|
2213
2634
|
|
|
2214
2635
|
// src/conversation/objectql-conversation-service.ts
|
|
2215
|
-
var
|
|
2636
|
+
var import_node_crypto2 = require("crypto");
|
|
2216
2637
|
var CONVERSATIONS_OBJECT = "ai_conversations";
|
|
2217
2638
|
var MESSAGES_OBJECT = "ai_messages";
|
|
2218
2639
|
var CONVERSATION_ORDER = [
|
|
@@ -2229,7 +2650,7 @@ var ObjectQLConversationService = class {
|
|
|
2229
2650
|
}
|
|
2230
2651
|
async create(options = {}) {
|
|
2231
2652
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2232
|
-
const id = `conv_${(0,
|
|
2653
|
+
const id = `conv_${(0, import_node_crypto2.randomUUID)()}`;
|
|
2233
2654
|
const record = {
|
|
2234
2655
|
id,
|
|
2235
2656
|
title: options.title ?? null,
|
|
@@ -2302,7 +2723,7 @@ var ObjectQLConversationService = class {
|
|
|
2302
2723
|
throw new Error(`Conversation "${conversationId}" not found`);
|
|
2303
2724
|
}
|
|
2304
2725
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2305
|
-
const msgId = `msg_${(0,
|
|
2726
|
+
const msgId = `msg_${(0, import_node_crypto2.randomUUID)()}`;
|
|
2306
2727
|
let contentStr;
|
|
2307
2728
|
let toolCallsJson = null;
|
|
2308
2729
|
let toolCallId = null;
|
|
@@ -2547,10 +2968,677 @@ var AiMessageObject = import_data2.ObjectSchema.create({
|
|
|
2547
2968
|
}
|
|
2548
2969
|
});
|
|
2549
2970
|
|
|
2971
|
+
// src/objects/ai-trace.object.ts
|
|
2972
|
+
var import_data3 = require("@objectstack/spec/data");
|
|
2973
|
+
var AiTraceObject = import_data3.ObjectSchema.create({
|
|
2974
|
+
name: "ai_traces",
|
|
2975
|
+
label: "AI Trace",
|
|
2976
|
+
pluralLabel: "AI Traces",
|
|
2977
|
+
icon: "activity",
|
|
2978
|
+
isSystem: true,
|
|
2979
|
+
description: "Per-call LLM invocation trace with token usage and cost",
|
|
2980
|
+
fields: {
|
|
2981
|
+
id: import_data3.Field.text({
|
|
2982
|
+
label: "Trace ID",
|
|
2983
|
+
required: true,
|
|
2984
|
+
readonly: true
|
|
2985
|
+
}),
|
|
2986
|
+
conversation_id: import_data3.Field.lookup("ai_conversations", {
|
|
2987
|
+
label: "Conversation",
|
|
2988
|
+
required: false,
|
|
2989
|
+
description: "Parent conversation, if any"
|
|
2990
|
+
}),
|
|
2991
|
+
agent_id: import_data3.Field.text({
|
|
2992
|
+
label: "Agent",
|
|
2993
|
+
required: false,
|
|
2994
|
+
maxLength: 128,
|
|
2995
|
+
description: "Agent metadata name that originated the call"
|
|
2996
|
+
}),
|
|
2997
|
+
operation: import_data3.Field.select({
|
|
2998
|
+
label: "Operation",
|
|
2999
|
+
required: true,
|
|
3000
|
+
options: [
|
|
3001
|
+
{ label: "Chat", value: "chat" },
|
|
3002
|
+
{ label: "Complete", value: "complete" },
|
|
3003
|
+
{ label: "Stream Chat", value: "stream_chat" },
|
|
3004
|
+
{ label: "Chat With Tools", value: "chat_with_tools" },
|
|
3005
|
+
{ label: "Generate Object", value: "generate_object" },
|
|
3006
|
+
{ label: "Embed", value: "embed" }
|
|
3007
|
+
]
|
|
3008
|
+
}),
|
|
3009
|
+
model: import_data3.Field.text({
|
|
3010
|
+
label: "Model",
|
|
3011
|
+
required: false,
|
|
3012
|
+
maxLength: 128,
|
|
3013
|
+
description: "Model identifier reported by the adapter"
|
|
3014
|
+
}),
|
|
3015
|
+
adapter: import_data3.Field.text({
|
|
3016
|
+
label: "Adapter",
|
|
3017
|
+
required: false,
|
|
3018
|
+
maxLength: 64,
|
|
3019
|
+
description: 'LLM adapter name (e.g. "vercel", "memory")'
|
|
3020
|
+
}),
|
|
3021
|
+
prompt_tokens: import_data3.Field.number({
|
|
3022
|
+
label: "Prompt Tokens",
|
|
3023
|
+
required: false,
|
|
3024
|
+
defaultValue: 0
|
|
3025
|
+
}),
|
|
3026
|
+
completion_tokens: import_data3.Field.number({
|
|
3027
|
+
label: "Completion Tokens",
|
|
3028
|
+
required: false,
|
|
3029
|
+
defaultValue: 0
|
|
3030
|
+
}),
|
|
3031
|
+
total_tokens: import_data3.Field.number({
|
|
3032
|
+
label: "Total Tokens",
|
|
3033
|
+
required: false,
|
|
3034
|
+
defaultValue: 0
|
|
3035
|
+
}),
|
|
3036
|
+
input_cost: import_data3.Field.number({
|
|
3037
|
+
label: "Input Cost",
|
|
3038
|
+
required: false,
|
|
3039
|
+
description: "Cost attributable to prompt tokens (currency in `currency` field)"
|
|
3040
|
+
}),
|
|
3041
|
+
output_cost: import_data3.Field.number({
|
|
3042
|
+
label: "Output Cost",
|
|
3043
|
+
required: false,
|
|
3044
|
+
description: "Cost attributable to completion tokens"
|
|
3045
|
+
}),
|
|
3046
|
+
total_cost: import_data3.Field.number({
|
|
3047
|
+
label: "Total Cost",
|
|
3048
|
+
required: false,
|
|
3049
|
+
description: "input_cost + output_cost"
|
|
3050
|
+
}),
|
|
3051
|
+
currency: import_data3.Field.text({
|
|
3052
|
+
label: "Currency",
|
|
3053
|
+
required: false,
|
|
3054
|
+
maxLength: 8,
|
|
3055
|
+
defaultValue: "USD"
|
|
3056
|
+
}),
|
|
3057
|
+
latency_ms: import_data3.Field.number({
|
|
3058
|
+
label: "Latency (ms)",
|
|
3059
|
+
required: true,
|
|
3060
|
+
defaultValue: 0,
|
|
3061
|
+
description: "Wall-clock duration of the LLM call"
|
|
3062
|
+
}),
|
|
3063
|
+
status: import_data3.Field.select({
|
|
3064
|
+
label: "Status",
|
|
3065
|
+
required: true,
|
|
3066
|
+
options: [
|
|
3067
|
+
{ label: "Success", value: "success" },
|
|
3068
|
+
{ label: "Error", value: "error" }
|
|
3069
|
+
]
|
|
3070
|
+
}),
|
|
3071
|
+
error: import_data3.Field.textarea({
|
|
3072
|
+
label: "Error",
|
|
3073
|
+
required: false,
|
|
3074
|
+
description: "Error message when status=error"
|
|
3075
|
+
}),
|
|
3076
|
+
metadata: import_data3.Field.textarea({
|
|
3077
|
+
label: "Metadata",
|
|
3078
|
+
required: false,
|
|
3079
|
+
description: "JSON-serialized extra fields (request id, user id, \u2026)"
|
|
3080
|
+
}),
|
|
3081
|
+
created_at: import_data3.Field.datetime({
|
|
3082
|
+
label: "Created At",
|
|
3083
|
+
required: true,
|
|
3084
|
+
defaultValue: "NOW()",
|
|
3085
|
+
readonly: true
|
|
3086
|
+
})
|
|
3087
|
+
},
|
|
3088
|
+
indexes: [
|
|
3089
|
+
{ fields: ["conversation_id"] },
|
|
3090
|
+
{ fields: ["agent_id"] },
|
|
3091
|
+
{ fields: ["model"] },
|
|
3092
|
+
{ fields: ["status"] },
|
|
3093
|
+
{ fields: ["created_at"] }
|
|
3094
|
+
],
|
|
3095
|
+
enable: {
|
|
3096
|
+
trackHistory: false,
|
|
3097
|
+
searchable: false,
|
|
3098
|
+
apiEnabled: true,
|
|
3099
|
+
apiMethods: ["get", "list"],
|
|
3100
|
+
trash: false,
|
|
3101
|
+
mru: false
|
|
3102
|
+
}
|
|
3103
|
+
});
|
|
3104
|
+
|
|
3105
|
+
// src/views/ai-trace.view.ts
|
|
3106
|
+
var import_spec = require("@objectstack/spec");
|
|
3107
|
+
var AiTraceView = (0, import_spec.defineView)({
|
|
3108
|
+
list: {
|
|
3109
|
+
type: "grid",
|
|
3110
|
+
data: { provider: "object", object: "ai_traces" },
|
|
3111
|
+
columns: [
|
|
3112
|
+
{ field: "created_at", label: "Time" },
|
|
3113
|
+
{ field: "operation" },
|
|
3114
|
+
{ field: "model" },
|
|
3115
|
+
{ field: "agent_id", label: "Agent" },
|
|
3116
|
+
{ field: "latency_ms", label: "Latency (ms)" },
|
|
3117
|
+
{ field: "total_tokens", label: "Tokens" },
|
|
3118
|
+
{ field: "cost_total", label: "Cost" },
|
|
3119
|
+
{ field: "status" }
|
|
3120
|
+
],
|
|
3121
|
+
sort: [{ field: "created_at", order: "desc" }],
|
|
3122
|
+
pagination: { pageSize: 50 },
|
|
3123
|
+
searchableFields: ["conversation_id", "agent_id", "model", "error"],
|
|
3124
|
+
filterableFields: ["operation", "model", "status"]
|
|
3125
|
+
},
|
|
3126
|
+
listViews: {
|
|
3127
|
+
errors: {
|
|
3128
|
+
label: "Errors",
|
|
3129
|
+
type: "grid",
|
|
3130
|
+
data: { provider: "object", object: "ai_traces" },
|
|
3131
|
+
columns: [
|
|
3132
|
+
{ field: "created_at", label: "Time" },
|
|
3133
|
+
{ field: "operation" },
|
|
3134
|
+
{ field: "model" },
|
|
3135
|
+
{ field: "latency_ms", label: "Latency (ms)" },
|
|
3136
|
+
{ field: "error" }
|
|
3137
|
+
],
|
|
3138
|
+
filter: [
|
|
3139
|
+
{ field: "status", operator: "=", value: "error" }
|
|
3140
|
+
],
|
|
3141
|
+
sort: [{ field: "created_at", order: "desc" }]
|
|
3142
|
+
},
|
|
3143
|
+
by_model: {
|
|
3144
|
+
label: "By Model",
|
|
3145
|
+
type: "grid",
|
|
3146
|
+
data: { provider: "object", object: "ai_traces" },
|
|
3147
|
+
columns: [
|
|
3148
|
+
{ field: "model" },
|
|
3149
|
+
{ field: "operation" },
|
|
3150
|
+
{ field: "latency_ms", label: "Latency (ms)" },
|
|
3151
|
+
{ field: "total_tokens", label: "Tokens" },
|
|
3152
|
+
{ field: "cost_total", label: "Cost" },
|
|
3153
|
+
{ field: "status" },
|
|
3154
|
+
{ field: "created_at", label: "Time" }
|
|
3155
|
+
],
|
|
3156
|
+
grouping: { fields: [{ field: "model" }] },
|
|
3157
|
+
sort: [{ field: "created_at", order: "desc" }]
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
});
|
|
3161
|
+
|
|
2550
3162
|
// src/plugin.ts
|
|
2551
3163
|
init_data_tools();
|
|
2552
3164
|
init_metadata_tools();
|
|
2553
3165
|
|
|
3166
|
+
// src/tools/query-data.tool.ts
|
|
3167
|
+
var import_zod = require("zod");
|
|
3168
|
+
|
|
3169
|
+
// src/schema-retriever.ts
|
|
3170
|
+
var SchemaRetriever = class {
|
|
3171
|
+
constructor(metadata, options = {}) {
|
|
3172
|
+
this.metadata = metadata;
|
|
3173
|
+
this.options = {
|
|
3174
|
+
limit: options.limit ?? 3,
|
|
3175
|
+
minScore: options.minScore ?? 1,
|
|
3176
|
+
maxFieldsPerObject: options.maxFieldsPerObject ?? 12
|
|
3177
|
+
};
|
|
3178
|
+
}
|
|
3179
|
+
/**
|
|
3180
|
+
* Find object definitions whose name/label/fields match terms in the query.
|
|
3181
|
+
*
|
|
3182
|
+
* Returns matches sorted by score (descending) capped at `limit`. When
|
|
3183
|
+
* the query yields no matches, returns an empty array — callers may
|
|
3184
|
+
* fall back to a generic "describe what data exists" tool call.
|
|
3185
|
+
*/
|
|
3186
|
+
async retrieve(query) {
|
|
3187
|
+
const terms = tokenise(query);
|
|
3188
|
+
if (terms.length === 0) return [];
|
|
3189
|
+
const objects = await this.metadata.listObjects();
|
|
3190
|
+
const hits = [];
|
|
3191
|
+
for (const raw of objects) {
|
|
3192
|
+
const obj = raw;
|
|
3193
|
+
if (!obj?.name) continue;
|
|
3194
|
+
const score = scoreObject(obj, terms);
|
|
3195
|
+
if (score >= this.options.minScore) {
|
|
3196
|
+
hits.push({ object: obj, score });
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
hits.sort((a, b) => b.score - a.score);
|
|
3200
|
+
return hits.slice(0, this.options.limit);
|
|
3201
|
+
}
|
|
3202
|
+
/**
|
|
3203
|
+
* Render hits as a compact Markdown schema snippet.
|
|
3204
|
+
*
|
|
3205
|
+
* Designed to be appended to the system message — every line carries
|
|
3206
|
+
* exactly the information a model needs to choose object/field names
|
|
3207
|
+
* for query construction.
|
|
3208
|
+
*/
|
|
3209
|
+
static renderSnippet(hits, maxFieldsPerObject = 12) {
|
|
3210
|
+
if (hits.length === 0) return "";
|
|
3211
|
+
const lines = ["## Schema context (auto-injected)"];
|
|
3212
|
+
for (const hit of hits) {
|
|
3213
|
+
const obj = hit.object;
|
|
3214
|
+
const parts = [];
|
|
3215
|
+
if (obj.label) parts.push(obj.label);
|
|
3216
|
+
if (obj.pluralLabel && obj.pluralLabel !== obj.label) parts.push(`(${obj.pluralLabel})`);
|
|
3217
|
+
const header = parts.length > 0 ? ` \u2014 ${parts.join(" ")}` : "";
|
|
3218
|
+
lines.push(`### ${obj.name}${header}`);
|
|
3219
|
+
const fields = Object.entries(obj.fields ?? {}).slice(0, maxFieldsPerObject);
|
|
3220
|
+
for (const [name, field] of fields) {
|
|
3221
|
+
lines.push(` - ${name}: ${describeField(field)}`);
|
|
3222
|
+
}
|
|
3223
|
+
const total = Object.keys(obj.fields ?? {}).length;
|
|
3224
|
+
if (total > fields.length) {
|
|
3225
|
+
lines.push(` - \u2026${total - fields.length} more field(s)`);
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
return lines.join("\n");
|
|
3229
|
+
}
|
|
3230
|
+
};
|
|
3231
|
+
function tokenise(query) {
|
|
3232
|
+
const raw = query.toLowerCase().match(/[a-z0-9]+/g) ?? [];
|
|
3233
|
+
return raw.filter((t) => t.length >= 2 && !STOPWORDS.has(t));
|
|
3234
|
+
}
|
|
3235
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
3236
|
+
"the",
|
|
3237
|
+
"and",
|
|
3238
|
+
"for",
|
|
3239
|
+
"with",
|
|
3240
|
+
"from",
|
|
3241
|
+
"are",
|
|
3242
|
+
"has",
|
|
3243
|
+
"have",
|
|
3244
|
+
"had",
|
|
3245
|
+
"was",
|
|
3246
|
+
"were",
|
|
3247
|
+
"this",
|
|
3248
|
+
"that",
|
|
3249
|
+
"these",
|
|
3250
|
+
"those",
|
|
3251
|
+
"all",
|
|
3252
|
+
"any",
|
|
3253
|
+
"how",
|
|
3254
|
+
"what",
|
|
3255
|
+
"when",
|
|
3256
|
+
"where",
|
|
3257
|
+
"who",
|
|
3258
|
+
"why",
|
|
3259
|
+
"which",
|
|
3260
|
+
"show",
|
|
3261
|
+
"list",
|
|
3262
|
+
"find",
|
|
3263
|
+
"get",
|
|
3264
|
+
"count",
|
|
3265
|
+
"of",
|
|
3266
|
+
"in",
|
|
3267
|
+
"on",
|
|
3268
|
+
"at",
|
|
3269
|
+
"to",
|
|
3270
|
+
"as",
|
|
3271
|
+
"by",
|
|
3272
|
+
"is",
|
|
3273
|
+
"it",
|
|
3274
|
+
"an",
|
|
3275
|
+
"or",
|
|
3276
|
+
"be",
|
|
3277
|
+
"me"
|
|
3278
|
+
]);
|
|
3279
|
+
function scoreObject(obj, terms) {
|
|
3280
|
+
let score = 0;
|
|
3281
|
+
const nameTokens = splitSnake(obj.name);
|
|
3282
|
+
const labelTokens = obj.label ? tokenise(obj.label) : [];
|
|
3283
|
+
const pluralTokens = obj.pluralLabel ? tokenise(obj.pluralLabel) : [];
|
|
3284
|
+
const descTokens = obj.description ? tokenise(obj.description) : [];
|
|
3285
|
+
for (const term of terms) {
|
|
3286
|
+
if (nameTokens.includes(term)) score += 3;
|
|
3287
|
+
else if (labelTokens.includes(term) || pluralTokens.includes(term)) score += 2;
|
|
3288
|
+
else if (descTokens.includes(term)) score += 1;
|
|
3289
|
+
}
|
|
3290
|
+
for (const [fieldName, field] of Object.entries(obj.fields ?? {})) {
|
|
3291
|
+
const fnTokens = splitSnake(fieldName);
|
|
3292
|
+
const flTokens = field.label ? tokenise(field.label) : [];
|
|
3293
|
+
for (const term of terms) {
|
|
3294
|
+
if (fnTokens.includes(term)) score += 2;
|
|
3295
|
+
else if (flTokens.includes(term)) score += 1;
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
return score;
|
|
3299
|
+
}
|
|
3300
|
+
function splitSnake(name) {
|
|
3301
|
+
return name.toLowerCase().split("_").filter(Boolean);
|
|
3302
|
+
}
|
|
3303
|
+
function describeField(field) {
|
|
3304
|
+
const t = field.type ?? "unknown";
|
|
3305
|
+
if (t === "lookup" && field.reference) return `lookup \u2192 ${field.reference}`;
|
|
3306
|
+
if (t === "select" && Array.isArray(field.options)) {
|
|
3307
|
+
const values = field.options.map(
|
|
3308
|
+
(o) => typeof o === "string" ? o : o.value
|
|
3309
|
+
).filter(Boolean).slice(0, 6);
|
|
3310
|
+
return `select(${values.join("|")})`;
|
|
3311
|
+
}
|
|
3312
|
+
return t;
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
// src/tools/query-data.tool.ts
|
|
3316
|
+
var QueryPlanSchema = import_zod.z.object({
|
|
3317
|
+
objectName: import_zod.z.string().min(1).describe('The snake_case object name to query (e.g. "task", "account").'),
|
|
3318
|
+
where: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional().describe(
|
|
3319
|
+
'Filter conditions as key-value pairs. Use MongoDB-style operators for ranges, e.g. {"amount": {"$gt": 100}}.'
|
|
3320
|
+
),
|
|
3321
|
+
fields: import_zod.z.array(import_zod.z.string()).optional().describe("Field names to return. Omit to return all fields."),
|
|
3322
|
+
orderBy: import_zod.z.array(
|
|
3323
|
+
import_zod.z.object({
|
|
3324
|
+
field: import_zod.z.string(),
|
|
3325
|
+
order: import_zod.z.enum(["asc", "desc"])
|
|
3326
|
+
})
|
|
3327
|
+
).optional().describe("Sort order. First entry is primary sort key."),
|
|
3328
|
+
limit: import_zod.z.number().int().min(1).max(200).optional().describe("Maximum number of records (default 20, max 200).")
|
|
3329
|
+
});
|
|
3330
|
+
var QUERY_DATA_TOOL = {
|
|
3331
|
+
name: "query_data",
|
|
3332
|
+
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.",
|
|
3333
|
+
parameters: {
|
|
3334
|
+
type: "object",
|
|
3335
|
+
properties: {
|
|
3336
|
+
request: {
|
|
3337
|
+
type: "string",
|
|
3338
|
+
description: "The natural-language question to answer (paraphrase the user's request if needed for clarity)."
|
|
3339
|
+
},
|
|
3340
|
+
model: {
|
|
3341
|
+
type: "string",
|
|
3342
|
+
description: "Optional model id to use for query planning. Defaults to the AI service's default model."
|
|
3343
|
+
}
|
|
3344
|
+
},
|
|
3345
|
+
required: ["request"],
|
|
3346
|
+
additionalProperties: false
|
|
3347
|
+
}
|
|
3348
|
+
};
|
|
3349
|
+
function createQueryDataHandler(ctx) {
|
|
3350
|
+
const retriever = new SchemaRetriever(ctx.metadata);
|
|
3351
|
+
const maxLimit = ctx.maxLimit ?? 100;
|
|
3352
|
+
return async (args) => {
|
|
3353
|
+
const { request, model } = args;
|
|
3354
|
+
if (!request || typeof request !== "string") {
|
|
3355
|
+
return JSON.stringify({ error: "query_data: `request` is required" });
|
|
3356
|
+
}
|
|
3357
|
+
if (!ctx.ai.generateObject) {
|
|
3358
|
+
return JSON.stringify({
|
|
3359
|
+
error: "query_data requires structured-output support. Configure a Vercel-AI-SDK-backed adapter (OpenAI, Anthropic, Google)."
|
|
3360
|
+
});
|
|
3361
|
+
}
|
|
3362
|
+
const hits = await retriever.retrieve(request);
|
|
3363
|
+
if (hits.length === 0) {
|
|
3364
|
+
return JSON.stringify({
|
|
3365
|
+
error: "No matching objects in metadata. Ask the user which object(s) to query, or list available objects via list_objects."
|
|
3366
|
+
});
|
|
3367
|
+
}
|
|
3368
|
+
const snippet = SchemaRetriever.renderSnippet(hits);
|
|
3369
|
+
const planMessages = [
|
|
3370
|
+
{
|
|
3371
|
+
role: "system",
|
|
3372
|
+
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
|
|
3373
|
+
},
|
|
3374
|
+
{ role: "user", content: request }
|
|
3375
|
+
];
|
|
3376
|
+
let plan;
|
|
3377
|
+
try {
|
|
3378
|
+
const generated = await ctx.ai.generateObject(planMessages, QueryPlanSchema, {
|
|
3379
|
+
model,
|
|
3380
|
+
schemaName: "ObjectQLQueryPlan",
|
|
3381
|
+
schemaDescription: "A single ObjectQL find() query to answer the user request."
|
|
3382
|
+
});
|
|
3383
|
+
plan = generated.object;
|
|
3384
|
+
} catch (err) {
|
|
3385
|
+
return JSON.stringify({
|
|
3386
|
+
error: `Failed to plan query: ${err instanceof Error ? err.message : String(err)}`
|
|
3387
|
+
});
|
|
3388
|
+
}
|
|
3389
|
+
const matchedObject = hits.find((h) => h.object.name === plan.objectName)?.object ?? hits[0].object;
|
|
3390
|
+
if (matchedObject.name !== plan.objectName) {
|
|
3391
|
+
return JSON.stringify({
|
|
3392
|
+
error: `Planned object "${plan.objectName}" is not in the retrieved schema. Available: ${hits.map((h) => h.object.name).join(", ")}`
|
|
3393
|
+
});
|
|
3394
|
+
}
|
|
3395
|
+
const limit = Math.min(plan.limit ?? 20, maxLimit);
|
|
3396
|
+
try {
|
|
3397
|
+
const records = await ctx.dataEngine.find(plan.objectName, {
|
|
3398
|
+
where: plan.where,
|
|
3399
|
+
fields: plan.fields,
|
|
3400
|
+
orderBy: plan.orderBy,
|
|
3401
|
+
limit
|
|
3402
|
+
});
|
|
3403
|
+
return JSON.stringify({
|
|
3404
|
+
plan,
|
|
3405
|
+
count: records.length,
|
|
3406
|
+
records
|
|
3407
|
+
});
|
|
3408
|
+
} catch (err) {
|
|
3409
|
+
return JSON.stringify({
|
|
3410
|
+
plan,
|
|
3411
|
+
error: `Query execution failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3412
|
+
});
|
|
3413
|
+
}
|
|
3414
|
+
};
|
|
3415
|
+
}
|
|
3416
|
+
function registerQueryDataTool(registry, context) {
|
|
3417
|
+
registry.register(QUERY_DATA_TOOL, createQueryDataHandler(context));
|
|
3418
|
+
}
|
|
3419
|
+
|
|
3420
|
+
// src/tools/action-tools.ts
|
|
3421
|
+
function actionSkipReason(action) {
|
|
3422
|
+
if (action.aiExposed === false) {
|
|
3423
|
+
return "opted-out via aiExposed:false";
|
|
3424
|
+
}
|
|
3425
|
+
if (action.type !== "script") return `type='${action.type}' not yet supported`;
|
|
3426
|
+
if (!action.target && !action.body) return "no target or body";
|
|
3427
|
+
if (action.confirmText) return "requires confirmation (confirmText set)";
|
|
3428
|
+
if (action.mode === "delete") return "mode='delete' \u2014 destructive";
|
|
3429
|
+
if (action.variant === "danger") return "variant='danger' \u2014 destructive";
|
|
3430
|
+
return null;
|
|
3431
|
+
}
|
|
3432
|
+
function fieldTypeToJsonType(t) {
|
|
3433
|
+
switch (t) {
|
|
3434
|
+
case "number":
|
|
3435
|
+
case "currency":
|
|
3436
|
+
case "percent":
|
|
3437
|
+
case "rating":
|
|
3438
|
+
case "slider":
|
|
3439
|
+
case "autonumber":
|
|
3440
|
+
return "number";
|
|
3441
|
+
case "boolean":
|
|
3442
|
+
case "toggle":
|
|
3443
|
+
return "boolean";
|
|
3444
|
+
case "multiselect":
|
|
3445
|
+
case "checkboxes":
|
|
3446
|
+
case "tags":
|
|
3447
|
+
return "array";
|
|
3448
|
+
default:
|
|
3449
|
+
return "string";
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
function resolveParam(param, ownerObject, allObjects) {
|
|
3453
|
+
const fieldRef = param.field;
|
|
3454
|
+
const owner = param.objectOverride && allObjects.get(param.objectOverride) ? allObjects.get(param.objectOverride) : ownerObject;
|
|
3455
|
+
const field = fieldRef ? owner?.fields?.[fieldRef] : void 0;
|
|
3456
|
+
const name = param.name ?? fieldRef;
|
|
3457
|
+
if (!name) return null;
|
|
3458
|
+
const type = param.type ?? field?.type;
|
|
3459
|
+
const jsonType = fieldTypeToJsonType(type);
|
|
3460
|
+
const schema = { type: jsonType };
|
|
3461
|
+
const label = typeof param.label === "string" ? param.label : field?.label;
|
|
3462
|
+
const help = param.helpText ?? field?.description;
|
|
3463
|
+
const description = [label, help].filter(Boolean).join(" \u2014 ") || void 0;
|
|
3464
|
+
if (description) schema.description = description;
|
|
3465
|
+
const optionSource = param.options ?? field?.options;
|
|
3466
|
+
if (Array.isArray(optionSource) && optionSource.length > 0) {
|
|
3467
|
+
const values = optionSource.map((o) => typeof o === "string" ? o : o.value).filter((v) => typeof v === "string");
|
|
3468
|
+
if (values.length > 0) {
|
|
3469
|
+
schema.enum = jsonType === "array" ? void 0 : values;
|
|
3470
|
+
if (jsonType === "array") {
|
|
3471
|
+
schema.items = { type: "string", enum: values };
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
} else if (jsonType === "array") {
|
|
3475
|
+
schema.items = { type: "string" };
|
|
3476
|
+
}
|
|
3477
|
+
if (param.defaultValue !== void 0) {
|
|
3478
|
+
schema.default = param.defaultValue;
|
|
3479
|
+
}
|
|
3480
|
+
const required = Boolean(param.required ?? field?.required ?? false);
|
|
3481
|
+
return { name, schema, required };
|
|
3482
|
+
}
|
|
3483
|
+
function buildParametersSchema(action, ownerObject, allObjects) {
|
|
3484
|
+
const properties = {};
|
|
3485
|
+
const required = [];
|
|
3486
|
+
const isRowContext = Array.isArray(action.locations) && action.locations.some((l) => l === "list_item" || l === "record_header" || l === "record_more" || l === "record_related");
|
|
3487
|
+
if (action.objectName && isRowContext) {
|
|
3488
|
+
properties.recordId = {
|
|
3489
|
+
type: "string",
|
|
3490
|
+
description: `The ${action.objectName} record id to act on.`
|
|
3491
|
+
};
|
|
3492
|
+
if (action.recordIdParam || action.recordIdField) {
|
|
3493
|
+
required.push("recordId");
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
for (const param of action.params ?? []) {
|
|
3497
|
+
const resolved = resolveParam(param, ownerObject, allObjects);
|
|
3498
|
+
if (!resolved) continue;
|
|
3499
|
+
properties[resolved.name] = resolved.schema;
|
|
3500
|
+
if (resolved.required) required.push(resolved.name);
|
|
3501
|
+
}
|
|
3502
|
+
return {
|
|
3503
|
+
type: "object",
|
|
3504
|
+
properties,
|
|
3505
|
+
...required.length > 0 ? { required } : {},
|
|
3506
|
+
additionalProperties: false
|
|
3507
|
+
};
|
|
3508
|
+
}
|
|
3509
|
+
function actionToolName(action, prefix = "action_") {
|
|
3510
|
+
return `${prefix}${action.name}`;
|
|
3511
|
+
}
|
|
3512
|
+
function describeAction(action, ownerObject) {
|
|
3513
|
+
const label = typeof action.label === "string" ? action.label : action.name.replace(/_/g, " ");
|
|
3514
|
+
const target = action.objectName ?? ownerObject?.name;
|
|
3515
|
+
const targetLabel = ownerObject?.label ?? target;
|
|
3516
|
+
const parts = [];
|
|
3517
|
+
parts.push(`${label}${targetLabel ? ` \u2014 operates on ${targetLabel}` : ""}.`);
|
|
3518
|
+
if (action.successMessage && typeof action.successMessage === "string") {
|
|
3519
|
+
parts.push(`On success: ${action.successMessage}`);
|
|
3520
|
+
}
|
|
3521
|
+
if (action.mode) parts.push(`Mode: ${action.mode}.`);
|
|
3522
|
+
parts.push(
|
|
3523
|
+
"Use this when the user asks to perform this operation in natural language."
|
|
3524
|
+
);
|
|
3525
|
+
return parts.join(" ");
|
|
3526
|
+
}
|
|
3527
|
+
function actionToToolDefinition(action, ownerObject, allObjects, toolPrefix = "action_") {
|
|
3528
|
+
if (actionSkipReason(action) !== null) return null;
|
|
3529
|
+
return {
|
|
3530
|
+
name: actionToolName(action, toolPrefix),
|
|
3531
|
+
description: describeAction(action, ownerObject),
|
|
3532
|
+
parameters: buildParametersSchema(action, ownerObject, allObjects)
|
|
3533
|
+
};
|
|
3534
|
+
}
|
|
3535
|
+
function buildHandlerEngineAdapter(engine) {
|
|
3536
|
+
return {
|
|
3537
|
+
update: (object, id, data) => engine.update(object, { ...data, id }, { where: { id } }),
|
|
3538
|
+
insert: (object, data) => engine.insert(object, data),
|
|
3539
|
+
find: (object, where) => engine.find(object, { where }),
|
|
3540
|
+
delete: (object, ids) => engine.delete(object, { where: { id: ids.length === 1 ? ids[0] : { $in: ids } } })
|
|
3541
|
+
};
|
|
3542
|
+
}
|
|
3543
|
+
function createActionToolHandler(action, ctx) {
|
|
3544
|
+
const principal = ctx.principal ?? { id: "ai_agent", name: "AI Assistant" };
|
|
3545
|
+
const engineAdapter = buildHandlerEngineAdapter(ctx.dataEngine);
|
|
3546
|
+
const requiresRecord = Array.isArray(action.locations) && action.locations.some(
|
|
3547
|
+
(l) => l === "list_item" || l === "record_header" || l === "record_more" || l === "record_related"
|
|
3548
|
+
);
|
|
3549
|
+
return async (args) => {
|
|
3550
|
+
const objectName = action.objectName;
|
|
3551
|
+
const target = action.target;
|
|
3552
|
+
const result = {
|
|
3553
|
+
ok: false,
|
|
3554
|
+
action: action.name,
|
|
3555
|
+
objectName
|
|
3556
|
+
};
|
|
3557
|
+
if (!objectName) {
|
|
3558
|
+
result.error = "Action has no objectName; cannot dispatch.";
|
|
3559
|
+
return JSON.stringify(result);
|
|
3560
|
+
}
|
|
3561
|
+
if (!target) {
|
|
3562
|
+
result.error = "Action has no target handler.";
|
|
3563
|
+
return JSON.stringify(result);
|
|
3564
|
+
}
|
|
3565
|
+
const recordId = typeof args.recordId === "string" && args.recordId.length > 0 ? args.recordId : void 0;
|
|
3566
|
+
let record;
|
|
3567
|
+
if (requiresRecord) {
|
|
3568
|
+
if (!recordId) {
|
|
3569
|
+
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).`;
|
|
3570
|
+
return JSON.stringify(result);
|
|
3571
|
+
}
|
|
3572
|
+
try {
|
|
3573
|
+
const found = await ctx.dataEngine.find(objectName, {
|
|
3574
|
+
where: { id: recordId },
|
|
3575
|
+
limit: 1
|
|
3576
|
+
});
|
|
3577
|
+
record = found[0];
|
|
3578
|
+
if (!record) {
|
|
3579
|
+
result.error = `Record ${recordId} not found in ${objectName}.`;
|
|
3580
|
+
return JSON.stringify(result);
|
|
3581
|
+
}
|
|
3582
|
+
result.recordId = recordId;
|
|
3583
|
+
} catch (err) {
|
|
3584
|
+
result.error = `Failed to load record: ${err instanceof Error ? err.message : String(err)}`;
|
|
3585
|
+
return JSON.stringify(result);
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
const { recordId: _omit, ...userParams } = args;
|
|
3589
|
+
try {
|
|
3590
|
+
const handlerCtx = {
|
|
3591
|
+
record,
|
|
3592
|
+
user: principal,
|
|
3593
|
+
engine: engineAdapter,
|
|
3594
|
+
params: userParams
|
|
3595
|
+
};
|
|
3596
|
+
const out = await ctx.dataEngine.executeAction?.(objectName, target, handlerCtx);
|
|
3597
|
+
result.ok = true;
|
|
3598
|
+
result.result = out ?? null;
|
|
3599
|
+
const successMsg = typeof action.successMessage === "string" ? action.successMessage : void 0;
|
|
3600
|
+
result.message = successMsg ?? `Action '${action.name}' executed successfully.`;
|
|
3601
|
+
return JSON.stringify(result);
|
|
3602
|
+
} catch (err) {
|
|
3603
|
+
result.error = err instanceof Error ? err.message : String(err);
|
|
3604
|
+
return JSON.stringify(result);
|
|
3605
|
+
}
|
|
3606
|
+
};
|
|
3607
|
+
}
|
|
3608
|
+
async function registerActionsAsTools(registry, context) {
|
|
3609
|
+
const objects = await context.metadata.listObjects();
|
|
3610
|
+
const objMap = new Map(
|
|
3611
|
+
objects.filter((o) => Boolean(o?.name)).map((o) => [o.name, o])
|
|
3612
|
+
);
|
|
3613
|
+
const registered = [];
|
|
3614
|
+
const skipped = [];
|
|
3615
|
+
const prefix = context.toolPrefix ?? "action_";
|
|
3616
|
+
for (const obj of objects) {
|
|
3617
|
+
if (!obj?.actions || !Array.isArray(obj.actions)) continue;
|
|
3618
|
+
for (const action of obj.actions) {
|
|
3619
|
+
if (!action || typeof action.name !== "string") continue;
|
|
3620
|
+
const normalized = {
|
|
3621
|
+
...action,
|
|
3622
|
+
objectName: action.objectName ?? obj.name
|
|
3623
|
+
};
|
|
3624
|
+
const reason = actionSkipReason(normalized);
|
|
3625
|
+
if (reason !== null) {
|
|
3626
|
+
skipped.push({ action: normalized.name, reason });
|
|
3627
|
+
continue;
|
|
3628
|
+
}
|
|
3629
|
+
const definition = actionToToolDefinition(normalized, obj, objMap, prefix);
|
|
3630
|
+
if (!definition) continue;
|
|
3631
|
+
if (registry.has(definition.name)) {
|
|
3632
|
+
skipped.push({ action: normalized.name, reason: "tool name already registered" });
|
|
3633
|
+
continue;
|
|
3634
|
+
}
|
|
3635
|
+
registry.register(definition, createActionToolHandler(normalized, context));
|
|
3636
|
+
registered.push(definition.name);
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
return { registered, skipped };
|
|
3640
|
+
}
|
|
3641
|
+
|
|
2554
3642
|
// src/agent-runtime.ts
|
|
2555
3643
|
var import_ai7 = require("@objectstack/spec/ai");
|
|
2556
3644
|
var AgentRuntime = class {
|
|
@@ -2853,6 +3941,16 @@ var SkillRegistry = class {
|
|
|
2853
3941
|
const resolved = [];
|
|
2854
3942
|
for (const skill of skills) {
|
|
2855
3943
|
for (const toolName of skill.tools) {
|
|
3944
|
+
if (toolName.endsWith("*")) {
|
|
3945
|
+
const prefix = toolName.slice(0, -1);
|
|
3946
|
+
for (const def2 of availableTools) {
|
|
3947
|
+
if (!def2.name.startsWith(prefix)) continue;
|
|
3948
|
+
if (seen.has(def2.name)) continue;
|
|
3949
|
+
resolved.push(def2);
|
|
3950
|
+
seen.add(def2.name);
|
|
3951
|
+
}
|
|
3952
|
+
continue;
|
|
3953
|
+
}
|
|
2856
3954
|
if (seen.has(toolName)) continue;
|
|
2857
3955
|
const def = toolMap.get(toolName);
|
|
2858
3956
|
if (def) {
|
|
@@ -2916,7 +4014,8 @@ Always answer in the same language the user is using. Detailed tool-usage guidan
|
|
|
2916
4014
|
maxTokens: 4096
|
|
2917
4015
|
},
|
|
2918
4016
|
// Capability bundle lives on the skill; the agent only references it.
|
|
2919
|
-
|
|
4017
|
+
// `data_explorer` = read side, `actions_executor` = write side.
|
|
4018
|
+
skills: ["data_explorer", "actions_executor"],
|
|
2920
4019
|
active: true,
|
|
2921
4020
|
visibility: "global",
|
|
2922
4021
|
guardrails: {
|
|
@@ -2996,6 +4095,7 @@ Guidelines:
|
|
|
2996
4095
|
7. Never expose internal IDs unless the user explicitly asks for them.
|
|
2997
4096
|
8. Always answer in the same language the user is using.`,
|
|
2998
4097
|
tools: [
|
|
4098
|
+
"query_data",
|
|
2999
4099
|
"list_objects",
|
|
3000
4100
|
"describe_object",
|
|
3001
4101
|
"query_records",
|
|
@@ -3065,6 +4165,44 @@ Guidelines:
|
|
|
3065
4165
|
active: true
|
|
3066
4166
|
};
|
|
3067
4167
|
|
|
4168
|
+
// src/skills/actions-executor-skill.ts
|
|
4169
|
+
var ACTIONS_EXECUTOR_SKILL = {
|
|
4170
|
+
name: "actions_executor",
|
|
4171
|
+
label: "Action Executor",
|
|
4172
|
+
description: "Perform business operations on the user's data \u2014 invoke actions like 'mark as complete', 'start task', 'clone record' through natural language.",
|
|
4173
|
+
instructions: `You can perform business operations by invoking the user's registered actions.
|
|
4174
|
+
|
|
4175
|
+
Capabilities:
|
|
4176
|
+
- Each tool whose name starts with \`action_\` is a business operation declared on an object.
|
|
4177
|
+
- Read the tool description carefully \u2014 it tells you what the action does and what record types it applies to.
|
|
4178
|
+
- 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.
|
|
4179
|
+
|
|
4180
|
+
Guidelines:
|
|
4181
|
+
1. Confirm intent \u2014 when the user says "complete it" / "start that one", make sure you know *which* record they mean. Ask if ambiguous.
|
|
4182
|
+
2. Use \`query_data\` to look up records by natural-language description ("the design review task", "tickets assigned to me").
|
|
4183
|
+
3. After invoking an action, the tool returns \`{ ok, message, result }\`. Summarise success in plain language; surface errors verbatim.
|
|
4184
|
+
4. Never invent recordIds. If \`query_data\` didn't return one, tell the user instead of guessing.
|
|
4185
|
+
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.
|
|
4186
|
+
6. Always answer in the same language the user is using.`,
|
|
4187
|
+
// Dynamically materialised: the runtime registers one tool per Action,
|
|
4188
|
+
// and the skill subscribes to the whole family via the `action_*`
|
|
4189
|
+
// glob (resolved by SkillRegistry.flattenToTools).
|
|
4190
|
+
tools: ["action_*"],
|
|
4191
|
+
triggerPhrases: [
|
|
4192
|
+
"complete",
|
|
4193
|
+
"mark as",
|
|
4194
|
+
"start",
|
|
4195
|
+
"finish",
|
|
4196
|
+
"clone",
|
|
4197
|
+
"duplicate",
|
|
4198
|
+
"do it",
|
|
4199
|
+
"run",
|
|
4200
|
+
"invoke",
|
|
4201
|
+
"execute"
|
|
4202
|
+
],
|
|
4203
|
+
active: true
|
|
4204
|
+
};
|
|
4205
|
+
|
|
3068
4206
|
// src/adapters/vercel-adapter.ts
|
|
3069
4207
|
var import_ai9 = require("ai");
|
|
3070
4208
|
function buildVercelOptions(options) {
|
|
@@ -3148,11 +4286,102 @@ var VercelLLMAdapter = class {
|
|
|
3148
4286
|
"[VercelLLMAdapter] Embeddings require a dedicated EmbeddingModel. Configure an embedding adapter instead."
|
|
3149
4287
|
);
|
|
3150
4288
|
}
|
|
4289
|
+
async generateObject(messages, schema, options) {
|
|
4290
|
+
const { schemaName, schemaDescription, ...rest } = options ?? {};
|
|
4291
|
+
const result = await (0, import_ai9.generateObject)({
|
|
4292
|
+
model: this.model,
|
|
4293
|
+
messages,
|
|
4294
|
+
schema,
|
|
4295
|
+
schemaName,
|
|
4296
|
+
schemaDescription,
|
|
4297
|
+
...buildVercelOptions(rest)
|
|
4298
|
+
});
|
|
4299
|
+
return {
|
|
4300
|
+
object: result.object,
|
|
4301
|
+
model: result.response?.modelId,
|
|
4302
|
+
usage: result.usage ? {
|
|
4303
|
+
promptTokens: result.usage.inputTokens ?? 0,
|
|
4304
|
+
completionTokens: result.usage.outputTokens ?? 0,
|
|
4305
|
+
totalTokens: result.usage.totalTokens ?? 0
|
|
4306
|
+
} : void 0
|
|
4307
|
+
};
|
|
4308
|
+
}
|
|
3151
4309
|
async listModels() {
|
|
3152
4310
|
return [];
|
|
3153
4311
|
}
|
|
3154
4312
|
};
|
|
3155
4313
|
|
|
4314
|
+
// src/model-registry.ts
|
|
4315
|
+
var ModelRegistry = class {
|
|
4316
|
+
constructor(config = {}) {
|
|
4317
|
+
this.models = /* @__PURE__ */ new Map();
|
|
4318
|
+
for (const model of config.models ?? []) {
|
|
4319
|
+
this.models.set(model.id, model);
|
|
4320
|
+
}
|
|
4321
|
+
this.defaultModelId = config.defaultModelId;
|
|
4322
|
+
}
|
|
4323
|
+
/** Register or replace a model. */
|
|
4324
|
+
register(model) {
|
|
4325
|
+
this.models.set(model.id, model);
|
|
4326
|
+
}
|
|
4327
|
+
/** Look up a model by id. */
|
|
4328
|
+
get(id) {
|
|
4329
|
+
return this.models.get(id);
|
|
4330
|
+
}
|
|
4331
|
+
/** Look up a model by id, throwing if missing. */
|
|
4332
|
+
getOrThrow(id) {
|
|
4333
|
+
const model = this.models.get(id);
|
|
4334
|
+
if (!model) {
|
|
4335
|
+
throw new Error(
|
|
4336
|
+
`[ModelRegistry] Unknown model "${id}". Registered: ${[...this.models.keys()].join(", ") || "(none)"}`
|
|
4337
|
+
);
|
|
4338
|
+
}
|
|
4339
|
+
return model;
|
|
4340
|
+
}
|
|
4341
|
+
/** Resolve the default model (explicit > first registered > undefined). */
|
|
4342
|
+
getDefault() {
|
|
4343
|
+
if (this.defaultModelId) {
|
|
4344
|
+
return this.models.get(this.defaultModelId);
|
|
4345
|
+
}
|
|
4346
|
+
return this.models.values().next().value;
|
|
4347
|
+
}
|
|
4348
|
+
/** Set the default model id (must already be registered). */
|
|
4349
|
+
setDefault(id) {
|
|
4350
|
+
this.getOrThrow(id);
|
|
4351
|
+
this.defaultModelId = id;
|
|
4352
|
+
}
|
|
4353
|
+
/** All registered models. */
|
|
4354
|
+
list() {
|
|
4355
|
+
return [...this.models.values()];
|
|
4356
|
+
}
|
|
4357
|
+
/** Number of registered models. */
|
|
4358
|
+
get size() {
|
|
4359
|
+
return this.models.size;
|
|
4360
|
+
}
|
|
4361
|
+
/**
|
|
4362
|
+
* Estimate cost in the model's currency (defaults to USD).
|
|
4363
|
+
*
|
|
4364
|
+
* Returns `undefined` when the model is unknown or has no pricing data.
|
|
4365
|
+
* Costs are computed as `(tokens / 1000) * pricePer1kTokens` for input and
|
|
4366
|
+
* output independently, then summed.
|
|
4367
|
+
*/
|
|
4368
|
+
estimateCost(modelId, usage) {
|
|
4369
|
+
const model = this.models.get(modelId);
|
|
4370
|
+
if (!model?.pricing) return void 0;
|
|
4371
|
+
return computeCost(model.pricing, usage);
|
|
4372
|
+
}
|
|
4373
|
+
};
|
|
4374
|
+
function computeCost(pricing, usage) {
|
|
4375
|
+
const inputCost = pricing.inputCostPer1kTokens != null ? usage.promptTokens / 1e3 * pricing.inputCostPer1kTokens : 0;
|
|
4376
|
+
const outputCost = pricing.outputCostPer1kTokens != null ? usage.completionTokens / 1e3 * pricing.outputCostPer1kTokens : 0;
|
|
4377
|
+
return {
|
|
4378
|
+
inputCost,
|
|
4379
|
+
outputCost,
|
|
4380
|
+
totalCost: inputCost + outputCost,
|
|
4381
|
+
currency: pricing.currency ?? "USD"
|
|
4382
|
+
};
|
|
4383
|
+
}
|
|
4384
|
+
|
|
3156
4385
|
// src/plugin.ts
|
|
3157
4386
|
var AIServicePlugin = class {
|
|
3158
4387
|
constructor(options = {}) {
|
|
@@ -3274,10 +4503,34 @@ var AIServicePlugin = class {
|
|
|
3274
4503
|
adapterDescription = detected.description;
|
|
3275
4504
|
}
|
|
3276
4505
|
ctx.logger.info(`[AI] Using LLM adapter: ${adapterDescription}`);
|
|
4506
|
+
const modelRegistry = new ModelRegistry({
|
|
4507
|
+
models: this.options.models,
|
|
4508
|
+
defaultModelId: this.options.defaultModelId
|
|
4509
|
+
});
|
|
4510
|
+
if (modelRegistry.size > 0) {
|
|
4511
|
+
ctx.logger.info(`[AI] ModelRegistry initialised with ${modelRegistry.size} model(s)`);
|
|
4512
|
+
}
|
|
4513
|
+
let traceRecorder;
|
|
4514
|
+
if (this.options.traceRecorder === null) {
|
|
4515
|
+
ctx.logger.debug("[AI] Tracing disabled (traceRecorder=null)");
|
|
4516
|
+
} else if (this.options.traceRecorder) {
|
|
4517
|
+
traceRecorder = this.options.traceRecorder;
|
|
4518
|
+
} else {
|
|
4519
|
+
try {
|
|
4520
|
+
const engine = ctx.getService("data");
|
|
4521
|
+
if (engine && typeof engine.insert === "function") {
|
|
4522
|
+
traceRecorder = new ObjectQLTraceRecorder(engine, { logger: ctx.logger });
|
|
4523
|
+
ctx.logger.info("[AI] Using ObjectQLTraceRecorder (IDataEngine detected)");
|
|
4524
|
+
}
|
|
4525
|
+
} catch {
|
|
4526
|
+
}
|
|
4527
|
+
}
|
|
3277
4528
|
const config = {
|
|
3278
4529
|
adapter,
|
|
3279
4530
|
logger: ctx.logger,
|
|
3280
|
-
conversationService
|
|
4531
|
+
conversationService,
|
|
4532
|
+
modelRegistry,
|
|
4533
|
+
traceRecorder
|
|
3281
4534
|
};
|
|
3282
4535
|
this.service = new AIService(config);
|
|
3283
4536
|
if (hasExisting) {
|
|
@@ -3292,7 +4545,8 @@ var AIServicePlugin = class {
|
|
|
3292
4545
|
type: "plugin",
|
|
3293
4546
|
scope: "project",
|
|
3294
4547
|
namespace: "ai",
|
|
3295
|
-
objects: [AiConversationObject, AiMessageObject]
|
|
4548
|
+
objects: [AiConversationObject, AiMessageObject, AiTraceObject],
|
|
4549
|
+
views: [AiTraceView]
|
|
3296
4550
|
});
|
|
3297
4551
|
if (this.options.debug) {
|
|
3298
4552
|
ctx.hook("ai:beforeChat", async (messages) => {
|
|
@@ -3324,6 +4578,39 @@ var AIServicePlugin = class {
|
|
|
3324
4578
|
if (dataEngine) {
|
|
3325
4579
|
registerDataTools(this.service.toolRegistry, { dataEngine });
|
|
3326
4580
|
ctx.logger.info("[AI] Built-in data tools registered");
|
|
4581
|
+
if (metadataService) {
|
|
4582
|
+
registerQueryDataTool(this.service.toolRegistry, {
|
|
4583
|
+
ai: this.service,
|
|
4584
|
+
metadata: metadataService,
|
|
4585
|
+
dataEngine
|
|
4586
|
+
});
|
|
4587
|
+
ctx.logger.info("[AI] query_data tool registered");
|
|
4588
|
+
try {
|
|
4589
|
+
const { registered, skipped } = await registerActionsAsTools(
|
|
4590
|
+
this.service.toolRegistry,
|
|
4591
|
+
{
|
|
4592
|
+
metadata: metadataService,
|
|
4593
|
+
dataEngine
|
|
4594
|
+
}
|
|
4595
|
+
);
|
|
4596
|
+
if (registered.length > 0) {
|
|
4597
|
+
ctx.logger.info(
|
|
4598
|
+
`[AI] ${registered.length} action tool(s) registered: ${registered.join(", ")}`
|
|
4599
|
+
);
|
|
4600
|
+
}
|
|
4601
|
+
if (skipped.length > 0) {
|
|
4602
|
+
ctx.logger.debug(
|
|
4603
|
+
`[AI] Skipped ${skipped.length} action(s) for AI exposure`,
|
|
4604
|
+
{ skipped }
|
|
4605
|
+
);
|
|
4606
|
+
}
|
|
4607
|
+
} catch (err) {
|
|
4608
|
+
ctx.logger.warn(
|
|
4609
|
+
"[AI] Failed to register action tools",
|
|
4610
|
+
err instanceof Error ? { error: err.message } : { error: String(err) }
|
|
4611
|
+
);
|
|
4612
|
+
}
|
|
4613
|
+
}
|
|
3327
4614
|
if (metadataService) {
|
|
3328
4615
|
const { DATA_TOOL_DEFINITIONS: DATA_TOOL_DEFINITIONS2 } = await Promise.resolve().then(() => (init_data_tools(), data_tools_exports));
|
|
3329
4616
|
for (const toolDef of DATA_TOOL_DEFINITIONS2) {
|
|
@@ -3374,6 +4661,19 @@ var AIServicePlugin = class {
|
|
|
3374
4661
|
} catch (err) {
|
|
3375
4662
|
ctx.logger.warn("[AI] Failed to register data_explorer skill", err instanceof Error ? { error: err.message } : { error: String(err) });
|
|
3376
4663
|
}
|
|
4664
|
+
try {
|
|
4665
|
+
const skillExists = typeof metadataService.exists === "function" ? await withTimeout(metadataService.exists("skill", ACTIONS_EXECUTOR_SKILL.name)) : false;
|
|
4666
|
+
if (skillExists === null) {
|
|
4667
|
+
ctx.logger.warn("[AI] Metadata service timed out checking actions_executor skill, skipping");
|
|
4668
|
+
} else if (!skillExists) {
|
|
4669
|
+
await withTimeout(metadataService.register("skill", ACTIONS_EXECUTOR_SKILL.name, ACTIONS_EXECUTOR_SKILL));
|
|
4670
|
+
ctx.logger.info("[AI] actions_executor skill registered");
|
|
4671
|
+
} else {
|
|
4672
|
+
ctx.logger.debug("[AI] actions_executor skill already exists, skipping auto-registration");
|
|
4673
|
+
}
|
|
4674
|
+
} catch (err) {
|
|
4675
|
+
ctx.logger.warn("[AI] Failed to register actions_executor skill", err instanceof Error ? { error: err.message } : { error: String(err) });
|
|
4676
|
+
}
|
|
3377
4677
|
}
|
|
3378
4678
|
}
|
|
3379
4679
|
} catch {
|
|
@@ -3834,29 +5134,45 @@ function registerPackageTools(registry, context) {
|
|
|
3834
5134
|
}
|
|
3835
5135
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3836
5136
|
0 && (module.exports = {
|
|
5137
|
+
ACTIONS_EXECUTOR_SKILL,
|
|
3837
5138
|
AIService,
|
|
3838
5139
|
AIServicePlugin,
|
|
3839
5140
|
AgentRuntime,
|
|
3840
5141
|
AiConversationObject,
|
|
3841
5142
|
AiMessageObject,
|
|
5143
|
+
AiTraceObject,
|
|
5144
|
+
AiTraceView,
|
|
3842
5145
|
DATA_CHAT_AGENT,
|
|
5146
|
+
DATA_EXPLORER_SKILL,
|
|
3843
5147
|
DATA_TOOL_DEFINITIONS,
|
|
3844
5148
|
InMemoryConversationService,
|
|
3845
5149
|
METADATA_ASSISTANT_AGENT,
|
|
5150
|
+
METADATA_AUTHORING_SKILL,
|
|
3846
5151
|
METADATA_TOOL_DEFINITIONS,
|
|
3847
5152
|
MemoryLLMAdapter,
|
|
5153
|
+
ModelRegistry,
|
|
5154
|
+
NullTraceRecorder,
|
|
3848
5155
|
ObjectQLConversationService,
|
|
5156
|
+
ObjectQLTraceRecorder,
|
|
3849
5157
|
PACKAGE_TOOL_DEFINITIONS,
|
|
5158
|
+
QUERY_DATA_TOOL,
|
|
5159
|
+
SchemaRetriever,
|
|
3850
5160
|
SkillRegistry,
|
|
3851
5161
|
ToolRegistry,
|
|
3852
5162
|
VercelLLMAdapter,
|
|
5163
|
+
actionSkipReason,
|
|
5164
|
+
actionToToolDefinition,
|
|
5165
|
+
actionToolName,
|
|
3853
5166
|
addFieldTool,
|
|
3854
5167
|
buildAIRoutes,
|
|
3855
5168
|
buildAgentRoutes,
|
|
3856
5169
|
buildAssistantRoutes,
|
|
3857
5170
|
buildToolRoutes,
|
|
5171
|
+
buildTraceEvent,
|
|
5172
|
+
computeCost,
|
|
3858
5173
|
createObjectTool,
|
|
3859
5174
|
createPackageTool,
|
|
5175
|
+
createQueryDataHandler,
|
|
3860
5176
|
deleteFieldTool,
|
|
3861
5177
|
describeObjectTool,
|
|
3862
5178
|
encodeStreamPart,
|
|
@@ -3866,9 +5182,11 @@ function registerPackageTools(registry, context) {
|
|
|
3866
5182
|
listObjectsTool,
|
|
3867
5183
|
listPackagesTool,
|
|
3868
5184
|
modifyFieldTool,
|
|
5185
|
+
registerActionsAsTools,
|
|
3869
5186
|
registerDataTools,
|
|
3870
5187
|
registerMetadataTools,
|
|
3871
5188
|
registerPackageTools,
|
|
5189
|
+
registerQueryDataTool,
|
|
3872
5190
|
setActivePackageTool
|
|
3873
5191
|
});
|
|
3874
5192
|
//# sourceMappingURL=index.cjs.map
|