@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.js
CHANGED
|
@@ -844,8 +844,112 @@ var MemoryLLMAdapter = class {
|
|
|
844
844
|
async chat(messages, options) {
|
|
845
845
|
const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
|
|
846
846
|
const userContent = lastUserMessage?.content;
|
|
847
|
-
const
|
|
848
|
-
const
|
|
847
|
+
const userText = typeof userContent === "string" ? userContent : "(complex content)";
|
|
848
|
+
const tools = options?.tools;
|
|
849
|
+
const hasQueryDataTool = Array.isArray(tools) && tools.some((t) => t?.name === "query_data");
|
|
850
|
+
const alreadyCalledQueryData = messages.some(
|
|
851
|
+
(m) => m.role === "tool" && Array.isArray(m.content) && m.content.some((c) => c?.toolName === "query_data")
|
|
852
|
+
);
|
|
853
|
+
const alreadyCalledAction = messages.some(
|
|
854
|
+
(m) => m.role === "tool" && Array.isArray(m.content) && m.content.some(
|
|
855
|
+
(c) => typeof c?.toolName === "string" && c.toolName.startsWith("action_")
|
|
856
|
+
)
|
|
857
|
+
);
|
|
858
|
+
if (Array.isArray(tools) && !alreadyCalledAction && lastUserMessage) {
|
|
859
|
+
const actionTools = tools.filter((t) => typeof t?.name === "string" && t.name.startsWith("action_"));
|
|
860
|
+
const chosen = pickActionTool(userText, actionTools);
|
|
861
|
+
if (chosen) {
|
|
862
|
+
const recordId = extractRecordIdFromMessages(messages, userText);
|
|
863
|
+
if (recordId) {
|
|
864
|
+
const toolCallId = `memory_tc_${Date.now().toString(36)}`;
|
|
865
|
+
return {
|
|
866
|
+
content: "",
|
|
867
|
+
model: options?.model ?? "memory",
|
|
868
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
869
|
+
toolCalls: [
|
|
870
|
+
{
|
|
871
|
+
type: "tool-call",
|
|
872
|
+
toolCallId,
|
|
873
|
+
toolName: chosen.name,
|
|
874
|
+
input: { recordId }
|
|
875
|
+
}
|
|
876
|
+
]
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
if (hasQueryDataTool && !alreadyCalledQueryData && lastUserMessage) {
|
|
882
|
+
const toolCallId = `memory_tc_${Date.now().toString(36)}`;
|
|
883
|
+
return {
|
|
884
|
+
content: "",
|
|
885
|
+
model: options?.model ?? "memory",
|
|
886
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
887
|
+
toolCalls: [
|
|
888
|
+
{
|
|
889
|
+
type: "tool-call",
|
|
890
|
+
toolCallId,
|
|
891
|
+
toolName: "query_data",
|
|
892
|
+
input: { request: userText }
|
|
893
|
+
}
|
|
894
|
+
]
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
if (alreadyCalledAction) {
|
|
898
|
+
const lastTool = [...messages].reverse().find((m) => m.role === "tool");
|
|
899
|
+
const part = Array.isArray(lastTool?.content) ? lastTool.content.find((c) => typeof c?.toolName === "string" && c.toolName.startsWith("action_")) : void 0;
|
|
900
|
+
const raw = part?.output && typeof part.output === "object" && "value" in part.output ? part.output.value : part?.result;
|
|
901
|
+
let payload = {};
|
|
902
|
+
if (typeof raw === "string") {
|
|
903
|
+
try {
|
|
904
|
+
payload = JSON.parse(raw);
|
|
905
|
+
} catch {
|
|
906
|
+
}
|
|
907
|
+
} else if (raw && typeof raw === "object") {
|
|
908
|
+
payload = raw;
|
|
909
|
+
}
|
|
910
|
+
if (payload.error) {
|
|
911
|
+
return {
|
|
912
|
+
content: `[memory] action ${payload.action ?? ""} failed: ${payload.error}`,
|
|
913
|
+
model: options?.model ?? "memory",
|
|
914
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
return {
|
|
918
|
+
content: `[memory] ${payload.message ?? "Action executed."} (${payload.action ?? "action"})`,
|
|
919
|
+
model: options?.model ?? "memory",
|
|
920
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
if (alreadyCalledQueryData) {
|
|
924
|
+
const lastTool = [...messages].reverse().find((m) => m.role === "tool");
|
|
925
|
+
const part = Array.isArray(lastTool?.content) ? lastTool.content.find((c) => c?.toolName === "query_data") : void 0;
|
|
926
|
+
let payload = {};
|
|
927
|
+
const raw = part?.output && typeof part.output === "object" && "value" in part.output ? part.output.value : part?.result;
|
|
928
|
+
if (typeof raw === "string") {
|
|
929
|
+
try {
|
|
930
|
+
payload = JSON.parse(raw);
|
|
931
|
+
} catch {
|
|
932
|
+
payload = {};
|
|
933
|
+
}
|
|
934
|
+
} else if (raw && typeof raw === "object") {
|
|
935
|
+
payload = raw;
|
|
936
|
+
}
|
|
937
|
+
if (payload.error) {
|
|
938
|
+
return {
|
|
939
|
+
content: `[memory] query_data failed: ${payload.error}`,
|
|
940
|
+
model: options?.model ?? "memory",
|
|
941
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
const records = payload.records ?? [];
|
|
945
|
+
const count = payload.count ?? records.length;
|
|
946
|
+
return {
|
|
947
|
+
content: `[memory] Found ${count} record${count === 1 ? "" : "s"} for "${userText}".`,
|
|
948
|
+
model: options?.model ?? "memory",
|
|
949
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
const content = lastUserMessage ? `[memory] ${userText}` : "[memory] (no user message)";
|
|
849
953
|
return {
|
|
850
954
|
content,
|
|
851
955
|
model: options?.model ?? "memory",
|
|
@@ -897,23 +1001,42 @@ var MemoryLLMAdapter = class {
|
|
|
897
1001
|
*/
|
|
898
1002
|
async generateObject(messages, schema, options) {
|
|
899
1003
|
const sys = messages.filter((m) => m.role === "system").map((m) => typeof m.content === "string" ? m.content : "").join("\n");
|
|
900
|
-
const headerRe = /^###\s+([a-z0-9_]+)
|
|
1004
|
+
const headerRe = /^###\s+([a-z0-9_]+)(?:\s+—\s+([^\n]+))?/gim;
|
|
901
1005
|
const candidates = [];
|
|
902
1006
|
for (const match of sys.matchAll(headerRe)) {
|
|
903
|
-
|
|
1007
|
+
const machineName = match[1];
|
|
1008
|
+
if (!machineName) continue;
|
|
1009
|
+
const aliasText = match[2] ?? "";
|
|
1010
|
+
const aliasTokens = /* @__PURE__ */ new Set();
|
|
1011
|
+
for (const t of machineName.split(/[^a-z0-9]+/)) {
|
|
1012
|
+
if (t) aliasTokens.add(t);
|
|
1013
|
+
}
|
|
1014
|
+
for (const t of aliasText.toLowerCase().split(/[^a-z0-9]+/)) {
|
|
1015
|
+
if (t) aliasTokens.add(t);
|
|
1016
|
+
}
|
|
1017
|
+
for (const t of [...aliasTokens]) {
|
|
1018
|
+
if (t.length > 3 && t.endsWith("s")) aliasTokens.add(t.slice(0, -1));
|
|
1019
|
+
}
|
|
1020
|
+
candidates.push({ name: machineName, aliasTokens });
|
|
904
1021
|
}
|
|
905
1022
|
const lastUser = [...messages].reverse().find((m) => m.role === "user");
|
|
906
1023
|
const userText = typeof lastUser?.content === "string" ? lastUser.content.toLowerCase() : "";
|
|
907
1024
|
const userTokens = new Set(
|
|
908
1025
|
userText.split(/[^a-z0-9_]+/).filter((t) => t.length > 1)
|
|
909
1026
|
);
|
|
910
|
-
|
|
1027
|
+
for (const t of [...userTokens]) {
|
|
1028
|
+
if (t.length > 3 && t.endsWith("s")) userTokens.add(t.slice(0, -1));
|
|
1029
|
+
}
|
|
1030
|
+
let chosen = candidates[0]?.name;
|
|
911
1031
|
let bestScore = -1;
|
|
912
|
-
for (const
|
|
913
|
-
|
|
1032
|
+
for (const cand of candidates) {
|
|
1033
|
+
let score = 0;
|
|
1034
|
+
for (const tok of cand.aliasTokens) {
|
|
1035
|
+
if (userTokens.has(tok)) score += 1;
|
|
1036
|
+
}
|
|
914
1037
|
if (score > bestScore) {
|
|
915
1038
|
bestScore = score;
|
|
916
|
-
chosen = name;
|
|
1039
|
+
chosen = cand.name;
|
|
917
1040
|
}
|
|
918
1041
|
}
|
|
919
1042
|
const attempts = [];
|
|
@@ -934,6 +1057,98 @@ var MemoryLLMAdapter = class {
|
|
|
934
1057
|
);
|
|
935
1058
|
}
|
|
936
1059
|
};
|
|
1060
|
+
function pickActionTool(userText, actionTools) {
|
|
1061
|
+
if (actionTools.length === 0 || !userText) return null;
|
|
1062
|
+
const userTokens = new Set(
|
|
1063
|
+
userText.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2)
|
|
1064
|
+
);
|
|
1065
|
+
const ACTION_VERBS = /* @__PURE__ */ new Set([
|
|
1066
|
+
"complete",
|
|
1067
|
+
"finish",
|
|
1068
|
+
"done",
|
|
1069
|
+
"close",
|
|
1070
|
+
"start",
|
|
1071
|
+
"begin",
|
|
1072
|
+
"resume",
|
|
1073
|
+
"clone",
|
|
1074
|
+
"copy",
|
|
1075
|
+
"duplicate",
|
|
1076
|
+
"cancel",
|
|
1077
|
+
"abort",
|
|
1078
|
+
"archive",
|
|
1079
|
+
"restore",
|
|
1080
|
+
"approve",
|
|
1081
|
+
"reject",
|
|
1082
|
+
"assign",
|
|
1083
|
+
"unassign",
|
|
1084
|
+
"export",
|
|
1085
|
+
"import",
|
|
1086
|
+
"send",
|
|
1087
|
+
"notify",
|
|
1088
|
+
"publish",
|
|
1089
|
+
"unpublish",
|
|
1090
|
+
"mark"
|
|
1091
|
+
]);
|
|
1092
|
+
const hasActionVerb = [...userTokens].some((t) => ACTION_VERBS.has(t));
|
|
1093
|
+
if (!hasActionVerb) return null;
|
|
1094
|
+
let best = null;
|
|
1095
|
+
let bestScore = 0;
|
|
1096
|
+
for (const tool of actionTools) {
|
|
1097
|
+
const nameTokens = tool.name.replace(/^action_/, "").toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2);
|
|
1098
|
+
let score = 0;
|
|
1099
|
+
for (const tok of nameTokens) {
|
|
1100
|
+
if (!userTokens.has(tok)) continue;
|
|
1101
|
+
score += ACTION_VERBS.has(tok) ? 3 : 1;
|
|
1102
|
+
}
|
|
1103
|
+
if (score > bestScore) {
|
|
1104
|
+
bestScore = score;
|
|
1105
|
+
best = tool;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
return bestScore >= 3 ? best : null;
|
|
1109
|
+
}
|
|
1110
|
+
function extractRecordIdFromMessages(messages, userText) {
|
|
1111
|
+
const userTokens = userText.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2);
|
|
1112
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1113
|
+
const m = messages[i];
|
|
1114
|
+
if (m.role !== "tool" || !Array.isArray(m.content)) continue;
|
|
1115
|
+
const parts = m.content;
|
|
1116
|
+
for (const part of parts) {
|
|
1117
|
+
if (part?.toolName !== "query_data") continue;
|
|
1118
|
+
const raw = part.output && typeof part.output === "object" && "value" in part.output ? part.output.value : part.result;
|
|
1119
|
+
let payload = {};
|
|
1120
|
+
if (typeof raw === "string") {
|
|
1121
|
+
try {
|
|
1122
|
+
payload = JSON.parse(raw);
|
|
1123
|
+
} catch {
|
|
1124
|
+
}
|
|
1125
|
+
} else if (raw && typeof raw === "object") {
|
|
1126
|
+
payload = raw;
|
|
1127
|
+
}
|
|
1128
|
+
const records = payload.records ?? [];
|
|
1129
|
+
if (records.length === 0) continue;
|
|
1130
|
+
let bestId;
|
|
1131
|
+
let bestScore = -1;
|
|
1132
|
+
for (const rec of records) {
|
|
1133
|
+
if (!rec || typeof rec !== "object") continue;
|
|
1134
|
+
const id = rec.id;
|
|
1135
|
+
if (typeof id !== "string" && typeof id !== "number") continue;
|
|
1136
|
+
const hay = Object.values(rec).filter((v) => typeof v === "string").join(" ").toLowerCase();
|
|
1137
|
+
const hayTokens = hay.split(/[^a-z0-9]+/).filter(Boolean);
|
|
1138
|
+
let score = 0;
|
|
1139
|
+
for (const ut of userTokens) {
|
|
1140
|
+
if (hayTokens.includes(ut)) score += 1;
|
|
1141
|
+
}
|
|
1142
|
+
if (score > bestScore) {
|
|
1143
|
+
bestScore = score;
|
|
1144
|
+
bestId = String(id);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return bestId ?? String(records[0].id);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
return void 0;
|
|
1151
|
+
}
|
|
937
1152
|
|
|
938
1153
|
// src/tools/tool-registry.ts
|
|
939
1154
|
var ToolRegistry = class {
|
|
@@ -1297,6 +1512,13 @@ var _AIService = class _AIService {
|
|
|
1297
1512
|
* maximum number of iterations (`maxIterations`) is reached.
|
|
1298
1513
|
*/
|
|
1299
1514
|
async chatWithTools(messages, options) {
|
|
1515
|
+
return this.instrument(
|
|
1516
|
+
"chat_with_tools",
|
|
1517
|
+
options,
|
|
1518
|
+
() => this.chatWithToolsImpl(messages, options)
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
async chatWithToolsImpl(messages, options) {
|
|
1300
1522
|
const { maxIterations: maxIter, onToolError, ...restOptions } = options ?? {};
|
|
1301
1523
|
const maxIterations = maxIter ?? _AIService.DEFAULT_MAX_ITERATIONS;
|
|
1302
1524
|
const registeredTools = this.toolRegistry.getAll();
|
|
@@ -2808,6 +3030,63 @@ var AiTraceObject = ObjectSchema3.create({
|
|
|
2808
3030
|
}
|
|
2809
3031
|
});
|
|
2810
3032
|
|
|
3033
|
+
// src/views/ai-trace.view.ts
|
|
3034
|
+
import { defineView } from "@objectstack/spec";
|
|
3035
|
+
var AiTraceView = defineView({
|
|
3036
|
+
list: {
|
|
3037
|
+
type: "grid",
|
|
3038
|
+
data: { provider: "object", object: "ai_traces" },
|
|
3039
|
+
columns: [
|
|
3040
|
+
{ field: "created_at", label: "Time" },
|
|
3041
|
+
{ field: "operation" },
|
|
3042
|
+
{ field: "model" },
|
|
3043
|
+
{ field: "agent_id", label: "Agent" },
|
|
3044
|
+
{ field: "latency_ms", label: "Latency (ms)" },
|
|
3045
|
+
{ field: "total_tokens", label: "Tokens" },
|
|
3046
|
+
{ field: "cost_total", label: "Cost" },
|
|
3047
|
+
{ field: "status" }
|
|
3048
|
+
],
|
|
3049
|
+
sort: [{ field: "created_at", order: "desc" }],
|
|
3050
|
+
pagination: { pageSize: 50 },
|
|
3051
|
+
searchableFields: ["conversation_id", "agent_id", "model", "error"],
|
|
3052
|
+
filterableFields: ["operation", "model", "status"]
|
|
3053
|
+
},
|
|
3054
|
+
listViews: {
|
|
3055
|
+
errors: {
|
|
3056
|
+
label: "Errors",
|
|
3057
|
+
type: "grid",
|
|
3058
|
+
data: { provider: "object", object: "ai_traces" },
|
|
3059
|
+
columns: [
|
|
3060
|
+
{ field: "created_at", label: "Time" },
|
|
3061
|
+
{ field: "operation" },
|
|
3062
|
+
{ field: "model" },
|
|
3063
|
+
{ field: "latency_ms", label: "Latency (ms)" },
|
|
3064
|
+
{ field: "error" }
|
|
3065
|
+
],
|
|
3066
|
+
filter: [
|
|
3067
|
+
{ field: "status", operator: "=", value: "error" }
|
|
3068
|
+
],
|
|
3069
|
+
sort: [{ field: "created_at", order: "desc" }]
|
|
3070
|
+
},
|
|
3071
|
+
by_model: {
|
|
3072
|
+
label: "By Model",
|
|
3073
|
+
type: "grid",
|
|
3074
|
+
data: { provider: "object", object: "ai_traces" },
|
|
3075
|
+
columns: [
|
|
3076
|
+
{ field: "model" },
|
|
3077
|
+
{ field: "operation" },
|
|
3078
|
+
{ field: "latency_ms", label: "Latency (ms)" },
|
|
3079
|
+
{ field: "total_tokens", label: "Tokens" },
|
|
3080
|
+
{ field: "cost_total", label: "Cost" },
|
|
3081
|
+
{ field: "status" },
|
|
3082
|
+
{ field: "created_at", label: "Time" }
|
|
3083
|
+
],
|
|
3084
|
+
grouping: { fields: [{ field: "model" }] },
|
|
3085
|
+
sort: [{ field: "created_at", order: "desc" }]
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
});
|
|
3089
|
+
|
|
2811
3090
|
// src/plugin.ts
|
|
2812
3091
|
init_data_tools();
|
|
2813
3092
|
init_metadata_tools();
|
|
@@ -2860,8 +3139,11 @@ var SchemaRetriever = class {
|
|
|
2860
3139
|
const lines = ["## Schema context (auto-injected)"];
|
|
2861
3140
|
for (const hit of hits) {
|
|
2862
3141
|
const obj = hit.object;
|
|
2863
|
-
const
|
|
2864
|
-
|
|
3142
|
+
const parts = [];
|
|
3143
|
+
if (obj.label) parts.push(obj.label);
|
|
3144
|
+
if (obj.pluralLabel && obj.pluralLabel !== obj.label) parts.push(`(${obj.pluralLabel})`);
|
|
3145
|
+
const header = parts.length > 0 ? ` \u2014 ${parts.join(" ")}` : "";
|
|
3146
|
+
lines.push(`### ${obj.name}${header}`);
|
|
2865
3147
|
const fields = Object.entries(obj.fields ?? {}).slice(0, maxFieldsPerObject);
|
|
2866
3148
|
for (const [name, field] of fields) {
|
|
2867
3149
|
lines.push(` - ${name}: ${describeField(field)}`);
|
|
@@ -3063,6 +3345,228 @@ function registerQueryDataTool(registry, context) {
|
|
|
3063
3345
|
registry.register(QUERY_DATA_TOOL, createQueryDataHandler(context));
|
|
3064
3346
|
}
|
|
3065
3347
|
|
|
3348
|
+
// src/tools/action-tools.ts
|
|
3349
|
+
function actionSkipReason(action) {
|
|
3350
|
+
if (action.aiExposed === false) {
|
|
3351
|
+
return "opted-out via aiExposed:false";
|
|
3352
|
+
}
|
|
3353
|
+
if (action.type !== "script") return `type='${action.type}' not yet supported`;
|
|
3354
|
+
if (!action.target && !action.body) return "no target or body";
|
|
3355
|
+
if (action.confirmText) return "requires confirmation (confirmText set)";
|
|
3356
|
+
if (action.mode === "delete") return "mode='delete' \u2014 destructive";
|
|
3357
|
+
if (action.variant === "danger") return "variant='danger' \u2014 destructive";
|
|
3358
|
+
return null;
|
|
3359
|
+
}
|
|
3360
|
+
function fieldTypeToJsonType(t) {
|
|
3361
|
+
switch (t) {
|
|
3362
|
+
case "number":
|
|
3363
|
+
case "currency":
|
|
3364
|
+
case "percent":
|
|
3365
|
+
case "rating":
|
|
3366
|
+
case "slider":
|
|
3367
|
+
case "autonumber":
|
|
3368
|
+
return "number";
|
|
3369
|
+
case "boolean":
|
|
3370
|
+
case "toggle":
|
|
3371
|
+
return "boolean";
|
|
3372
|
+
case "multiselect":
|
|
3373
|
+
case "checkboxes":
|
|
3374
|
+
case "tags":
|
|
3375
|
+
return "array";
|
|
3376
|
+
default:
|
|
3377
|
+
return "string";
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
function resolveParam(param, ownerObject, allObjects) {
|
|
3381
|
+
const fieldRef = param.field;
|
|
3382
|
+
const owner = param.objectOverride && allObjects.get(param.objectOverride) ? allObjects.get(param.objectOverride) : ownerObject;
|
|
3383
|
+
const field = fieldRef ? owner?.fields?.[fieldRef] : void 0;
|
|
3384
|
+
const name = param.name ?? fieldRef;
|
|
3385
|
+
if (!name) return null;
|
|
3386
|
+
const type = param.type ?? field?.type;
|
|
3387
|
+
const jsonType = fieldTypeToJsonType(type);
|
|
3388
|
+
const schema = { type: jsonType };
|
|
3389
|
+
const label = typeof param.label === "string" ? param.label : field?.label;
|
|
3390
|
+
const help = param.helpText ?? field?.description;
|
|
3391
|
+
const description = [label, help].filter(Boolean).join(" \u2014 ") || void 0;
|
|
3392
|
+
if (description) schema.description = description;
|
|
3393
|
+
const optionSource = param.options ?? field?.options;
|
|
3394
|
+
if (Array.isArray(optionSource) && optionSource.length > 0) {
|
|
3395
|
+
const values = optionSource.map((o) => typeof o === "string" ? o : o.value).filter((v) => typeof v === "string");
|
|
3396
|
+
if (values.length > 0) {
|
|
3397
|
+
schema.enum = jsonType === "array" ? void 0 : values;
|
|
3398
|
+
if (jsonType === "array") {
|
|
3399
|
+
schema.items = { type: "string", enum: values };
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
} else if (jsonType === "array") {
|
|
3403
|
+
schema.items = { type: "string" };
|
|
3404
|
+
}
|
|
3405
|
+
if (param.defaultValue !== void 0) {
|
|
3406
|
+
schema.default = param.defaultValue;
|
|
3407
|
+
}
|
|
3408
|
+
const required = Boolean(param.required ?? field?.required ?? false);
|
|
3409
|
+
return { name, schema, required };
|
|
3410
|
+
}
|
|
3411
|
+
function buildParametersSchema(action, ownerObject, allObjects) {
|
|
3412
|
+
const properties = {};
|
|
3413
|
+
const required = [];
|
|
3414
|
+
const isRowContext = Array.isArray(action.locations) && action.locations.some((l) => l === "list_item" || l === "record_header" || l === "record_more" || l === "record_related");
|
|
3415
|
+
if (action.objectName && isRowContext) {
|
|
3416
|
+
properties.recordId = {
|
|
3417
|
+
type: "string",
|
|
3418
|
+
description: `The ${action.objectName} record id to act on.`
|
|
3419
|
+
};
|
|
3420
|
+
if (action.recordIdParam || action.recordIdField) {
|
|
3421
|
+
required.push("recordId");
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
for (const param of action.params ?? []) {
|
|
3425
|
+
const resolved = resolveParam(param, ownerObject, allObjects);
|
|
3426
|
+
if (!resolved) continue;
|
|
3427
|
+
properties[resolved.name] = resolved.schema;
|
|
3428
|
+
if (resolved.required) required.push(resolved.name);
|
|
3429
|
+
}
|
|
3430
|
+
return {
|
|
3431
|
+
type: "object",
|
|
3432
|
+
properties,
|
|
3433
|
+
...required.length > 0 ? { required } : {},
|
|
3434
|
+
additionalProperties: false
|
|
3435
|
+
};
|
|
3436
|
+
}
|
|
3437
|
+
function actionToolName(action, prefix = "action_") {
|
|
3438
|
+
return `${prefix}${action.name}`;
|
|
3439
|
+
}
|
|
3440
|
+
function describeAction(action, ownerObject) {
|
|
3441
|
+
const label = typeof action.label === "string" ? action.label : action.name.replace(/_/g, " ");
|
|
3442
|
+
const target = action.objectName ?? ownerObject?.name;
|
|
3443
|
+
const targetLabel = ownerObject?.label ?? target;
|
|
3444
|
+
const parts = [];
|
|
3445
|
+
parts.push(`${label}${targetLabel ? ` \u2014 operates on ${targetLabel}` : ""}.`);
|
|
3446
|
+
if (action.successMessage && typeof action.successMessage === "string") {
|
|
3447
|
+
parts.push(`On success: ${action.successMessage}`);
|
|
3448
|
+
}
|
|
3449
|
+
if (action.mode) parts.push(`Mode: ${action.mode}.`);
|
|
3450
|
+
parts.push(
|
|
3451
|
+
"Use this when the user asks to perform this operation in natural language."
|
|
3452
|
+
);
|
|
3453
|
+
return parts.join(" ");
|
|
3454
|
+
}
|
|
3455
|
+
function actionToToolDefinition(action, ownerObject, allObjects, toolPrefix = "action_") {
|
|
3456
|
+
if (actionSkipReason(action) !== null) return null;
|
|
3457
|
+
return {
|
|
3458
|
+
name: actionToolName(action, toolPrefix),
|
|
3459
|
+
description: describeAction(action, ownerObject),
|
|
3460
|
+
parameters: buildParametersSchema(action, ownerObject, allObjects)
|
|
3461
|
+
};
|
|
3462
|
+
}
|
|
3463
|
+
function buildHandlerEngineAdapter(engine) {
|
|
3464
|
+
return {
|
|
3465
|
+
update: (object, id, data) => engine.update(object, { ...data, id }, { where: { id } }),
|
|
3466
|
+
insert: (object, data) => engine.insert(object, data),
|
|
3467
|
+
find: (object, where) => engine.find(object, { where }),
|
|
3468
|
+
delete: (object, ids) => engine.delete(object, { where: { id: ids.length === 1 ? ids[0] : { $in: ids } } })
|
|
3469
|
+
};
|
|
3470
|
+
}
|
|
3471
|
+
function createActionToolHandler(action, ctx) {
|
|
3472
|
+
const principal = ctx.principal ?? { id: "ai_agent", name: "AI Assistant" };
|
|
3473
|
+
const engineAdapter = buildHandlerEngineAdapter(ctx.dataEngine);
|
|
3474
|
+
const requiresRecord = Array.isArray(action.locations) && action.locations.some(
|
|
3475
|
+
(l) => l === "list_item" || l === "record_header" || l === "record_more" || l === "record_related"
|
|
3476
|
+
);
|
|
3477
|
+
return async (args) => {
|
|
3478
|
+
const objectName = action.objectName;
|
|
3479
|
+
const target = action.target;
|
|
3480
|
+
const result = {
|
|
3481
|
+
ok: false,
|
|
3482
|
+
action: action.name,
|
|
3483
|
+
objectName
|
|
3484
|
+
};
|
|
3485
|
+
if (!objectName) {
|
|
3486
|
+
result.error = "Action has no objectName; cannot dispatch.";
|
|
3487
|
+
return JSON.stringify(result);
|
|
3488
|
+
}
|
|
3489
|
+
if (!target) {
|
|
3490
|
+
result.error = "Action has no target handler.";
|
|
3491
|
+
return JSON.stringify(result);
|
|
3492
|
+
}
|
|
3493
|
+
const recordId = typeof args.recordId === "string" && args.recordId.length > 0 ? args.recordId : void 0;
|
|
3494
|
+
let record;
|
|
3495
|
+
if (requiresRecord) {
|
|
3496
|
+
if (!recordId) {
|
|
3497
|
+
result.error = `recordId is required for this action \u2014 supply the id of the ${objectName} record to act on (use query_data first if you don't have it).`;
|
|
3498
|
+
return JSON.stringify(result);
|
|
3499
|
+
}
|
|
3500
|
+
try {
|
|
3501
|
+
const found = await ctx.dataEngine.find(objectName, {
|
|
3502
|
+
where: { id: recordId },
|
|
3503
|
+
limit: 1
|
|
3504
|
+
});
|
|
3505
|
+
record = found[0];
|
|
3506
|
+
if (!record) {
|
|
3507
|
+
result.error = `Record ${recordId} not found in ${objectName}.`;
|
|
3508
|
+
return JSON.stringify(result);
|
|
3509
|
+
}
|
|
3510
|
+
result.recordId = recordId;
|
|
3511
|
+
} catch (err) {
|
|
3512
|
+
result.error = `Failed to load record: ${err instanceof Error ? err.message : String(err)}`;
|
|
3513
|
+
return JSON.stringify(result);
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
const { recordId: _omit, ...userParams } = args;
|
|
3517
|
+
try {
|
|
3518
|
+
const handlerCtx = {
|
|
3519
|
+
record,
|
|
3520
|
+
user: principal,
|
|
3521
|
+
engine: engineAdapter,
|
|
3522
|
+
params: userParams
|
|
3523
|
+
};
|
|
3524
|
+
const out = await ctx.dataEngine.executeAction?.(objectName, target, handlerCtx);
|
|
3525
|
+
result.ok = true;
|
|
3526
|
+
result.result = out ?? null;
|
|
3527
|
+
const successMsg = typeof action.successMessage === "string" ? action.successMessage : void 0;
|
|
3528
|
+
result.message = successMsg ?? `Action '${action.name}' executed successfully.`;
|
|
3529
|
+
return JSON.stringify(result);
|
|
3530
|
+
} catch (err) {
|
|
3531
|
+
result.error = err instanceof Error ? err.message : String(err);
|
|
3532
|
+
return JSON.stringify(result);
|
|
3533
|
+
}
|
|
3534
|
+
};
|
|
3535
|
+
}
|
|
3536
|
+
async function registerActionsAsTools(registry, context) {
|
|
3537
|
+
const objects = await context.metadata.listObjects();
|
|
3538
|
+
const objMap = new Map(
|
|
3539
|
+
objects.filter((o) => Boolean(o?.name)).map((o) => [o.name, o])
|
|
3540
|
+
);
|
|
3541
|
+
const registered = [];
|
|
3542
|
+
const skipped = [];
|
|
3543
|
+
const prefix = context.toolPrefix ?? "action_";
|
|
3544
|
+
for (const obj of objects) {
|
|
3545
|
+
if (!obj?.actions || !Array.isArray(obj.actions)) continue;
|
|
3546
|
+
for (const action of obj.actions) {
|
|
3547
|
+
if (!action || typeof action.name !== "string") continue;
|
|
3548
|
+
const normalized = {
|
|
3549
|
+
...action,
|
|
3550
|
+
objectName: action.objectName ?? obj.name
|
|
3551
|
+
};
|
|
3552
|
+
const reason = actionSkipReason(normalized);
|
|
3553
|
+
if (reason !== null) {
|
|
3554
|
+
skipped.push({ action: normalized.name, reason });
|
|
3555
|
+
continue;
|
|
3556
|
+
}
|
|
3557
|
+
const definition = actionToToolDefinition(normalized, obj, objMap, prefix);
|
|
3558
|
+
if (!definition) continue;
|
|
3559
|
+
if (registry.has(definition.name)) {
|
|
3560
|
+
skipped.push({ action: normalized.name, reason: "tool name already registered" });
|
|
3561
|
+
continue;
|
|
3562
|
+
}
|
|
3563
|
+
registry.register(definition, createActionToolHandler(normalized, context));
|
|
3564
|
+
registered.push(definition.name);
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
return { registered, skipped };
|
|
3568
|
+
}
|
|
3569
|
+
|
|
3066
3570
|
// src/agent-runtime.ts
|
|
3067
3571
|
import { AgentSchema } from "@objectstack/spec/ai";
|
|
3068
3572
|
var AgentRuntime = class {
|
|
@@ -3365,6 +3869,16 @@ var SkillRegistry = class {
|
|
|
3365
3869
|
const resolved = [];
|
|
3366
3870
|
for (const skill of skills) {
|
|
3367
3871
|
for (const toolName of skill.tools) {
|
|
3872
|
+
if (toolName.endsWith("*")) {
|
|
3873
|
+
const prefix = toolName.slice(0, -1);
|
|
3874
|
+
for (const def2 of availableTools) {
|
|
3875
|
+
if (!def2.name.startsWith(prefix)) continue;
|
|
3876
|
+
if (seen.has(def2.name)) continue;
|
|
3877
|
+
resolved.push(def2);
|
|
3878
|
+
seen.add(def2.name);
|
|
3879
|
+
}
|
|
3880
|
+
continue;
|
|
3881
|
+
}
|
|
3368
3882
|
if (seen.has(toolName)) continue;
|
|
3369
3883
|
const def = toolMap.get(toolName);
|
|
3370
3884
|
if (def) {
|
|
@@ -3428,7 +3942,8 @@ Always answer in the same language the user is using. Detailed tool-usage guidan
|
|
|
3428
3942
|
maxTokens: 4096
|
|
3429
3943
|
},
|
|
3430
3944
|
// Capability bundle lives on the skill; the agent only references it.
|
|
3431
|
-
|
|
3945
|
+
// `data_explorer` = read side, `actions_executor` = write side.
|
|
3946
|
+
skills: ["data_explorer", "actions_executor"],
|
|
3432
3947
|
active: true,
|
|
3433
3948
|
visibility: "global",
|
|
3434
3949
|
guardrails: {
|
|
@@ -3508,6 +4023,7 @@ Guidelines:
|
|
|
3508
4023
|
7. Never expose internal IDs unless the user explicitly asks for them.
|
|
3509
4024
|
8. Always answer in the same language the user is using.`,
|
|
3510
4025
|
tools: [
|
|
4026
|
+
"query_data",
|
|
3511
4027
|
"list_objects",
|
|
3512
4028
|
"describe_object",
|
|
3513
4029
|
"query_records",
|
|
@@ -3577,6 +4093,44 @@ Guidelines:
|
|
|
3577
4093
|
active: true
|
|
3578
4094
|
};
|
|
3579
4095
|
|
|
4096
|
+
// src/skills/actions-executor-skill.ts
|
|
4097
|
+
var ACTIONS_EXECUTOR_SKILL = {
|
|
4098
|
+
name: "actions_executor",
|
|
4099
|
+
label: "Action Executor",
|
|
4100
|
+
description: "Perform business operations on the user's data \u2014 invoke actions like 'mark as complete', 'start task', 'clone record' through natural language.",
|
|
4101
|
+
instructions: `You can perform business operations by invoking the user's registered actions.
|
|
4102
|
+
|
|
4103
|
+
Capabilities:
|
|
4104
|
+
- Each tool whose name starts with \`action_\` is a business operation declared on an object.
|
|
4105
|
+
- Read the tool description carefully \u2014 it tells you what the action does and what record types it applies to.
|
|
4106
|
+
- Most actions need a \`recordId\` argument. If you don't already have one from a prior \`query_data\` call, run \`query_data\` first to find the right record, then invoke the action with its id.
|
|
4107
|
+
|
|
4108
|
+
Guidelines:
|
|
4109
|
+
1. Confirm intent \u2014 when the user says "complete it" / "start that one", make sure you know *which* record they mean. Ask if ambiguous.
|
|
4110
|
+
2. Use \`query_data\` to look up records by natural-language description ("the design review task", "tickets assigned to me").
|
|
4111
|
+
3. After invoking an action, the tool returns \`{ ok, message, result }\`. Summarise success in plain language; surface errors verbatim.
|
|
4112
|
+
4. Never invent recordIds. If \`query_data\` didn't return one, tell the user instead of guessing.
|
|
4113
|
+
5. Action tools are pre-filtered for safety \u2014 destructive operations (\`mode: 'delete'\`, \`variant: 'danger'\`, anything with \`confirmText\`) are *not* exposed here and require explicit user confirmation in the UI.
|
|
4114
|
+
6. Always answer in the same language the user is using.`,
|
|
4115
|
+
// Dynamically materialised: the runtime registers one tool per Action,
|
|
4116
|
+
// and the skill subscribes to the whole family via the `action_*`
|
|
4117
|
+
// glob (resolved by SkillRegistry.flattenToTools).
|
|
4118
|
+
tools: ["action_*"],
|
|
4119
|
+
triggerPhrases: [
|
|
4120
|
+
"complete",
|
|
4121
|
+
"mark as",
|
|
4122
|
+
"start",
|
|
4123
|
+
"finish",
|
|
4124
|
+
"clone",
|
|
4125
|
+
"duplicate",
|
|
4126
|
+
"do it",
|
|
4127
|
+
"run",
|
|
4128
|
+
"invoke",
|
|
4129
|
+
"execute"
|
|
4130
|
+
],
|
|
4131
|
+
active: true
|
|
4132
|
+
};
|
|
4133
|
+
|
|
3580
4134
|
// src/adapters/vercel-adapter.ts
|
|
3581
4135
|
import { generateText, streamText, generateObject, tool as vercelTool, jsonSchema } from "ai";
|
|
3582
4136
|
function buildVercelOptions(options) {
|
|
@@ -3919,7 +4473,8 @@ var AIServicePlugin = class {
|
|
|
3919
4473
|
type: "plugin",
|
|
3920
4474
|
scope: "project",
|
|
3921
4475
|
namespace: "ai",
|
|
3922
|
-
objects: [AiConversationObject, AiMessageObject, AiTraceObject]
|
|
4476
|
+
objects: [AiConversationObject, AiMessageObject, AiTraceObject],
|
|
4477
|
+
views: [AiTraceView]
|
|
3923
4478
|
});
|
|
3924
4479
|
if (this.options.debug) {
|
|
3925
4480
|
ctx.hook("ai:beforeChat", async (messages) => {
|
|
@@ -3958,6 +4513,31 @@ var AIServicePlugin = class {
|
|
|
3958
4513
|
dataEngine
|
|
3959
4514
|
});
|
|
3960
4515
|
ctx.logger.info("[AI] query_data tool registered");
|
|
4516
|
+
try {
|
|
4517
|
+
const { registered, skipped } = await registerActionsAsTools(
|
|
4518
|
+
this.service.toolRegistry,
|
|
4519
|
+
{
|
|
4520
|
+
metadata: metadataService,
|
|
4521
|
+
dataEngine
|
|
4522
|
+
}
|
|
4523
|
+
);
|
|
4524
|
+
if (registered.length > 0) {
|
|
4525
|
+
ctx.logger.info(
|
|
4526
|
+
`[AI] ${registered.length} action tool(s) registered: ${registered.join(", ")}`
|
|
4527
|
+
);
|
|
4528
|
+
}
|
|
4529
|
+
if (skipped.length > 0) {
|
|
4530
|
+
ctx.logger.debug(
|
|
4531
|
+
`[AI] Skipped ${skipped.length} action(s) for AI exposure`,
|
|
4532
|
+
{ skipped }
|
|
4533
|
+
);
|
|
4534
|
+
}
|
|
4535
|
+
} catch (err) {
|
|
4536
|
+
ctx.logger.warn(
|
|
4537
|
+
"[AI] Failed to register action tools",
|
|
4538
|
+
err instanceof Error ? { error: err.message } : { error: String(err) }
|
|
4539
|
+
);
|
|
4540
|
+
}
|
|
3961
4541
|
}
|
|
3962
4542
|
if (metadataService) {
|
|
3963
4543
|
const { DATA_TOOL_DEFINITIONS: DATA_TOOL_DEFINITIONS2 } = await Promise.resolve().then(() => (init_data_tools(), data_tools_exports));
|
|
@@ -4009,6 +4589,19 @@ var AIServicePlugin = class {
|
|
|
4009
4589
|
} catch (err) {
|
|
4010
4590
|
ctx.logger.warn("[AI] Failed to register data_explorer skill", err instanceof Error ? { error: err.message } : { error: String(err) });
|
|
4011
4591
|
}
|
|
4592
|
+
try {
|
|
4593
|
+
const skillExists = typeof metadataService.exists === "function" ? await withTimeout(metadataService.exists("skill", ACTIONS_EXECUTOR_SKILL.name)) : false;
|
|
4594
|
+
if (skillExists === null) {
|
|
4595
|
+
ctx.logger.warn("[AI] Metadata service timed out checking actions_executor skill, skipping");
|
|
4596
|
+
} else if (!skillExists) {
|
|
4597
|
+
await withTimeout(metadataService.register("skill", ACTIONS_EXECUTOR_SKILL.name, ACTIONS_EXECUTOR_SKILL));
|
|
4598
|
+
ctx.logger.info("[AI] actions_executor skill registered");
|
|
4599
|
+
} else {
|
|
4600
|
+
ctx.logger.debug("[AI] actions_executor skill already exists, skipping auto-registration");
|
|
4601
|
+
}
|
|
4602
|
+
} catch (err) {
|
|
4603
|
+
ctx.logger.warn("[AI] Failed to register actions_executor skill", err instanceof Error ? { error: err.message } : { error: String(err) });
|
|
4604
|
+
}
|
|
4012
4605
|
}
|
|
4013
4606
|
}
|
|
4014
4607
|
} catch {
|
|
@@ -4468,16 +5061,20 @@ function registerPackageTools(registry, context) {
|
|
|
4468
5061
|
registry.register(setActivePackageTool, createSetActivePackageHandler(context));
|
|
4469
5062
|
}
|
|
4470
5063
|
export {
|
|
5064
|
+
ACTIONS_EXECUTOR_SKILL,
|
|
4471
5065
|
AIService,
|
|
4472
5066
|
AIServicePlugin,
|
|
4473
5067
|
AgentRuntime,
|
|
4474
5068
|
AiConversationObject,
|
|
4475
5069
|
AiMessageObject,
|
|
4476
5070
|
AiTraceObject,
|
|
5071
|
+
AiTraceView,
|
|
4477
5072
|
DATA_CHAT_AGENT,
|
|
5073
|
+
DATA_EXPLORER_SKILL,
|
|
4478
5074
|
DATA_TOOL_DEFINITIONS,
|
|
4479
5075
|
InMemoryConversationService,
|
|
4480
5076
|
METADATA_ASSISTANT_AGENT,
|
|
5077
|
+
METADATA_AUTHORING_SKILL,
|
|
4481
5078
|
METADATA_TOOL_DEFINITIONS,
|
|
4482
5079
|
MemoryLLMAdapter,
|
|
4483
5080
|
ModelRegistry,
|
|
@@ -4490,6 +5087,9 @@ export {
|
|
|
4490
5087
|
SkillRegistry,
|
|
4491
5088
|
ToolRegistry,
|
|
4492
5089
|
VercelLLMAdapter,
|
|
5090
|
+
actionSkipReason,
|
|
5091
|
+
actionToToolDefinition,
|
|
5092
|
+
actionToolName,
|
|
4493
5093
|
addFieldTool,
|
|
4494
5094
|
buildAIRoutes,
|
|
4495
5095
|
buildAgentRoutes,
|
|
@@ -4509,6 +5109,7 @@ export {
|
|
|
4509
5109
|
listObjectsTool,
|
|
4510
5110
|
listPackagesTool,
|
|
4511
5111
|
modifyFieldTool,
|
|
5112
|
+
registerActionsAsTools,
|
|
4512
5113
|
registerDataTools,
|
|
4513
5114
|
registerMetadataTools,
|
|
4514
5115
|
registerPackageTools,
|