@objectstack/service-ai 6.0.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 +621 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +896 -4
- package/dist/index.d.ts +896 -4
- package/dist/index.js +613 -12
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -848,16 +848,20 @@ var init_metadata_tools = __esm({
|
|
|
848
848
|
// src/index.ts
|
|
849
849
|
var index_exports = {};
|
|
850
850
|
__export(index_exports, {
|
|
851
|
+
ACTIONS_EXECUTOR_SKILL: () => ACTIONS_EXECUTOR_SKILL,
|
|
851
852
|
AIService: () => AIService,
|
|
852
853
|
AIServicePlugin: () => AIServicePlugin,
|
|
853
854
|
AgentRuntime: () => AgentRuntime,
|
|
854
855
|
AiConversationObject: () => AiConversationObject,
|
|
855
856
|
AiMessageObject: () => AiMessageObject,
|
|
856
857
|
AiTraceObject: () => AiTraceObject,
|
|
858
|
+
AiTraceView: () => AiTraceView,
|
|
857
859
|
DATA_CHAT_AGENT: () => DATA_CHAT_AGENT,
|
|
860
|
+
DATA_EXPLORER_SKILL: () => DATA_EXPLORER_SKILL,
|
|
858
861
|
DATA_TOOL_DEFINITIONS: () => DATA_TOOL_DEFINITIONS,
|
|
859
862
|
InMemoryConversationService: () => InMemoryConversationService,
|
|
860
863
|
METADATA_ASSISTANT_AGENT: () => METADATA_ASSISTANT_AGENT,
|
|
864
|
+
METADATA_AUTHORING_SKILL: () => METADATA_AUTHORING_SKILL,
|
|
861
865
|
METADATA_TOOL_DEFINITIONS: () => METADATA_TOOL_DEFINITIONS,
|
|
862
866
|
MemoryLLMAdapter: () => MemoryLLMAdapter,
|
|
863
867
|
ModelRegistry: () => ModelRegistry,
|
|
@@ -870,6 +874,9 @@ __export(index_exports, {
|
|
|
870
874
|
SkillRegistry: () => SkillRegistry,
|
|
871
875
|
ToolRegistry: () => ToolRegistry,
|
|
872
876
|
VercelLLMAdapter: () => VercelLLMAdapter,
|
|
877
|
+
actionSkipReason: () => actionSkipReason,
|
|
878
|
+
actionToToolDefinition: () => actionToToolDefinition,
|
|
879
|
+
actionToolName: () => actionToolName,
|
|
873
880
|
addFieldTool: () => addFieldTool,
|
|
874
881
|
buildAIRoutes: () => buildAIRoutes,
|
|
875
882
|
buildAgentRoutes: () => buildAgentRoutes,
|
|
@@ -889,6 +896,7 @@ __export(index_exports, {
|
|
|
889
896
|
listObjectsTool: () => listObjectsTool,
|
|
890
897
|
listPackagesTool: () => listPackagesTool,
|
|
891
898
|
modifyFieldTool: () => modifyFieldTool,
|
|
899
|
+
registerActionsAsTools: () => registerActionsAsTools,
|
|
892
900
|
registerDataTools: () => registerDataTools,
|
|
893
901
|
registerMetadataTools: () => registerMetadataTools,
|
|
894
902
|
registerPackageTools: () => registerPackageTools,
|
|
@@ -908,8 +916,112 @@ var MemoryLLMAdapter = class {
|
|
|
908
916
|
async chat(messages, options) {
|
|
909
917
|
const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
|
|
910
918
|
const userContent = lastUserMessage?.content;
|
|
911
|
-
const
|
|
912
|
-
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)";
|
|
913
1025
|
return {
|
|
914
1026
|
content,
|
|
915
1027
|
model: options?.model ?? "memory",
|
|
@@ -961,23 +1073,42 @@ var MemoryLLMAdapter = class {
|
|
|
961
1073
|
*/
|
|
962
1074
|
async generateObject(messages, schema, options) {
|
|
963
1075
|
const sys = messages.filter((m) => m.role === "system").map((m) => typeof m.content === "string" ? m.content : "").join("\n");
|
|
964
|
-
const headerRe = /^###\s+([a-z0-9_]+)
|
|
1076
|
+
const headerRe = /^###\s+([a-z0-9_]+)(?:\s+—\s+([^\n]+))?/gim;
|
|
965
1077
|
const candidates = [];
|
|
966
1078
|
for (const match of sys.matchAll(headerRe)) {
|
|
967
|
-
|
|
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 });
|
|
968
1093
|
}
|
|
969
1094
|
const lastUser = [...messages].reverse().find((m) => m.role === "user");
|
|
970
1095
|
const userText = typeof lastUser?.content === "string" ? lastUser.content.toLowerCase() : "";
|
|
971
1096
|
const userTokens = new Set(
|
|
972
1097
|
userText.split(/[^a-z0-9_]+/).filter((t) => t.length > 1)
|
|
973
1098
|
);
|
|
974
|
-
|
|
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;
|
|
975
1103
|
let bestScore = -1;
|
|
976
|
-
for (const
|
|
977
|
-
|
|
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
|
+
}
|
|
978
1109
|
if (score > bestScore) {
|
|
979
1110
|
bestScore = score;
|
|
980
|
-
chosen = name;
|
|
1111
|
+
chosen = cand.name;
|
|
981
1112
|
}
|
|
982
1113
|
}
|
|
983
1114
|
const attempts = [];
|
|
@@ -998,6 +1129,98 @@ var MemoryLLMAdapter = class {
|
|
|
998
1129
|
);
|
|
999
1130
|
}
|
|
1000
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
|
+
}
|
|
1001
1224
|
|
|
1002
1225
|
// src/tools/tool-registry.ts
|
|
1003
1226
|
var ToolRegistry = class {
|
|
@@ -1361,6 +1584,13 @@ var _AIService = class _AIService {
|
|
|
1361
1584
|
* maximum number of iterations (`maxIterations`) is reached.
|
|
1362
1585
|
*/
|
|
1363
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) {
|
|
1364
1594
|
const { maxIterations: maxIter, onToolError, ...restOptions } = options ?? {};
|
|
1365
1595
|
const maxIterations = maxIter ?? _AIService.DEFAULT_MAX_ITERATIONS;
|
|
1366
1596
|
const registeredTools = this.toolRegistry.getAll();
|
|
@@ -2872,6 +3102,63 @@ var AiTraceObject = import_data3.ObjectSchema.create({
|
|
|
2872
3102
|
}
|
|
2873
3103
|
});
|
|
2874
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
|
+
|
|
2875
3162
|
// src/plugin.ts
|
|
2876
3163
|
init_data_tools();
|
|
2877
3164
|
init_metadata_tools();
|
|
@@ -2924,8 +3211,11 @@ var SchemaRetriever = class {
|
|
|
2924
3211
|
const lines = ["## Schema context (auto-injected)"];
|
|
2925
3212
|
for (const hit of hits) {
|
|
2926
3213
|
const obj = hit.object;
|
|
2927
|
-
const
|
|
2928
|
-
|
|
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}`);
|
|
2929
3219
|
const fields = Object.entries(obj.fields ?? {}).slice(0, maxFieldsPerObject);
|
|
2930
3220
|
for (const [name, field] of fields) {
|
|
2931
3221
|
lines.push(` - ${name}: ${describeField(field)}`);
|
|
@@ -3127,6 +3417,228 @@ function registerQueryDataTool(registry, context) {
|
|
|
3127
3417
|
registry.register(QUERY_DATA_TOOL, createQueryDataHandler(context));
|
|
3128
3418
|
}
|
|
3129
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
|
+
|
|
3130
3642
|
// src/agent-runtime.ts
|
|
3131
3643
|
var import_ai7 = require("@objectstack/spec/ai");
|
|
3132
3644
|
var AgentRuntime = class {
|
|
@@ -3429,6 +3941,16 @@ var SkillRegistry = class {
|
|
|
3429
3941
|
const resolved = [];
|
|
3430
3942
|
for (const skill of skills) {
|
|
3431
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
|
+
}
|
|
3432
3954
|
if (seen.has(toolName)) continue;
|
|
3433
3955
|
const def = toolMap.get(toolName);
|
|
3434
3956
|
if (def) {
|
|
@@ -3492,7 +4014,8 @@ Always answer in the same language the user is using. Detailed tool-usage guidan
|
|
|
3492
4014
|
maxTokens: 4096
|
|
3493
4015
|
},
|
|
3494
4016
|
// Capability bundle lives on the skill; the agent only references it.
|
|
3495
|
-
|
|
4017
|
+
// `data_explorer` = read side, `actions_executor` = write side.
|
|
4018
|
+
skills: ["data_explorer", "actions_executor"],
|
|
3496
4019
|
active: true,
|
|
3497
4020
|
visibility: "global",
|
|
3498
4021
|
guardrails: {
|
|
@@ -3572,6 +4095,7 @@ Guidelines:
|
|
|
3572
4095
|
7. Never expose internal IDs unless the user explicitly asks for them.
|
|
3573
4096
|
8. Always answer in the same language the user is using.`,
|
|
3574
4097
|
tools: [
|
|
4098
|
+
"query_data",
|
|
3575
4099
|
"list_objects",
|
|
3576
4100
|
"describe_object",
|
|
3577
4101
|
"query_records",
|
|
@@ -3641,6 +4165,44 @@ Guidelines:
|
|
|
3641
4165
|
active: true
|
|
3642
4166
|
};
|
|
3643
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
|
+
|
|
3644
4206
|
// src/adapters/vercel-adapter.ts
|
|
3645
4207
|
var import_ai9 = require("ai");
|
|
3646
4208
|
function buildVercelOptions(options) {
|
|
@@ -3983,7 +4545,8 @@ var AIServicePlugin = class {
|
|
|
3983
4545
|
type: "plugin",
|
|
3984
4546
|
scope: "project",
|
|
3985
4547
|
namespace: "ai",
|
|
3986
|
-
objects: [AiConversationObject, AiMessageObject, AiTraceObject]
|
|
4548
|
+
objects: [AiConversationObject, AiMessageObject, AiTraceObject],
|
|
4549
|
+
views: [AiTraceView]
|
|
3987
4550
|
});
|
|
3988
4551
|
if (this.options.debug) {
|
|
3989
4552
|
ctx.hook("ai:beforeChat", async (messages) => {
|
|
@@ -4022,6 +4585,31 @@ var AIServicePlugin = class {
|
|
|
4022
4585
|
dataEngine
|
|
4023
4586
|
});
|
|
4024
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
|
+
}
|
|
4025
4613
|
}
|
|
4026
4614
|
if (metadataService) {
|
|
4027
4615
|
const { DATA_TOOL_DEFINITIONS: DATA_TOOL_DEFINITIONS2 } = await Promise.resolve().then(() => (init_data_tools(), data_tools_exports));
|
|
@@ -4073,6 +4661,19 @@ var AIServicePlugin = class {
|
|
|
4073
4661
|
} catch (err) {
|
|
4074
4662
|
ctx.logger.warn("[AI] Failed to register data_explorer skill", err instanceof Error ? { error: err.message } : { error: String(err) });
|
|
4075
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
|
+
}
|
|
4076
4677
|
}
|
|
4077
4678
|
}
|
|
4078
4679
|
} catch {
|
|
@@ -4533,16 +5134,20 @@ function registerPackageTools(registry, context) {
|
|
|
4533
5134
|
}
|
|
4534
5135
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4535
5136
|
0 && (module.exports = {
|
|
5137
|
+
ACTIONS_EXECUTOR_SKILL,
|
|
4536
5138
|
AIService,
|
|
4537
5139
|
AIServicePlugin,
|
|
4538
5140
|
AgentRuntime,
|
|
4539
5141
|
AiConversationObject,
|
|
4540
5142
|
AiMessageObject,
|
|
4541
5143
|
AiTraceObject,
|
|
5144
|
+
AiTraceView,
|
|
4542
5145
|
DATA_CHAT_AGENT,
|
|
5146
|
+
DATA_EXPLORER_SKILL,
|
|
4543
5147
|
DATA_TOOL_DEFINITIONS,
|
|
4544
5148
|
InMemoryConversationService,
|
|
4545
5149
|
METADATA_ASSISTANT_AGENT,
|
|
5150
|
+
METADATA_AUTHORING_SKILL,
|
|
4546
5151
|
METADATA_TOOL_DEFINITIONS,
|
|
4547
5152
|
MemoryLLMAdapter,
|
|
4548
5153
|
ModelRegistry,
|
|
@@ -4555,6 +5160,9 @@ function registerPackageTools(registry, context) {
|
|
|
4555
5160
|
SkillRegistry,
|
|
4556
5161
|
ToolRegistry,
|
|
4557
5162
|
VercelLLMAdapter,
|
|
5163
|
+
actionSkipReason,
|
|
5164
|
+
actionToToolDefinition,
|
|
5165
|
+
actionToolName,
|
|
4558
5166
|
addFieldTool,
|
|
4559
5167
|
buildAIRoutes,
|
|
4560
5168
|
buildAgentRoutes,
|
|
@@ -4574,6 +5182,7 @@ function registerPackageTools(registry, context) {
|
|
|
4574
5182
|
listObjectsTool,
|
|
4575
5183
|
listPackagesTool,
|
|
4576
5184
|
modifyFieldTool,
|
|
5185
|
+
registerActionsAsTools,
|
|
4577
5186
|
registerDataTools,
|
|
4578
5187
|
registerMetadataTools,
|
|
4579
5188
|
registerPackageTools,
|