@cap-js/agents 0.9.5 → 0.9.7
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/README.md +2 -131
- package/_i18n/messages_ar.properties +48 -0
- package/_i18n/messages_bg.properties +48 -0
- package/_i18n/messages_cs.properties +48 -0
- package/_i18n/messages_da.properties +48 -0
- package/_i18n/messages_de.properties +48 -0
- package/_i18n/messages_el.properties +48 -0
- package/_i18n/messages_en.properties +48 -0
- package/_i18n/messages_es.properties +48 -0
- package/_i18n/messages_es_MX.properties +48 -0
- package/_i18n/messages_fi.properties +48 -0
- package/_i18n/messages_fr.properties +48 -0
- package/_i18n/messages_he.properties +48 -0
- package/_i18n/messages_hr.properties +48 -0
- package/_i18n/messages_hu.properties +48 -0
- package/_i18n/messages_it.properties +48 -0
- package/_i18n/messages_ja.properties +48 -0
- package/_i18n/messages_kk.properties +48 -0
- package/_i18n/messages_ko.properties +48 -0
- package/_i18n/messages_ms.properties +48 -0
- package/_i18n/messages_nl.properties +48 -0
- package/_i18n/messages_no.properties +48 -0
- package/_i18n/messages_pl.properties +48 -0
- package/_i18n/messages_pt.properties +48 -0
- package/_i18n/messages_ro.properties +48 -0
- package/_i18n/messages_ru.properties +48 -0
- package/_i18n/messages_sh.properties +48 -0
- package/_i18n/messages_sk.properties +48 -0
- package/_i18n/messages_sl.properties +48 -0
- package/_i18n/messages_sv.properties +48 -0
- package/_i18n/messages_th.properties +48 -0
- package/_i18n/messages_tr.properties +48 -0
- package/_i18n/messages_uk.properties +48 -0
- package/_i18n/messages_vi.properties +48 -0
- package/_i18n/messages_zh_CN.properties +48 -0
- package/_i18n/messages_zh_TW.properties +48 -0
- package/cds-plugin.js +2 -0
- package/lib/agents/middleware/index.js +2 -0
- package/lib/agents/middleware/masking.js +124 -0
- package/lib/agents/middleware/remote-mcp.js +5 -3
- package/lib/compile.js +2 -6
- package/lib/masking/index.js +90 -0
- package/lib/masking/store.js +80 -0
- package/lib/masking/structured/findElements.js +459 -0
- package/lib/masking/structured/index.js +170 -0
- package/lib/masking/unstructured/dpi.js +84 -0
- package/lib/masking/unstructured/hana.js +64 -0
- package/lib/masking/unstructured/index.js +52 -0
- package/lib/preview/chat.html +66 -30
- package/lib/protocol/agent-card.js +2 -6
- package/lib/protocol/persistence/cleanup.js +13 -4
- package/lib/telemetry/chat-tracing.js +7 -2
- package/lib/telemetry/mlflow/tracing.js +3 -3
- package/lib/telemetry/span-masking.js +165 -0
- package/lib/telemetry/tool-tracing.js +4 -4
- package/lib/utils/markdown.js +2 -2
- package/lib/utils/utils.js +2 -34
- package/package.json +4 -2
- package/srv/handlers/graph-executor.js +52 -17
- package/srv/handlers/system-prompt.js +7 -0
- package/srv/handlers/tools.js +4 -3
|
@@ -50,9 +50,6 @@ export function _patchToolsProto(proto) {
|
|
|
50
50
|
span.setAttribute("gen_ai.operation.name", "execute_tool")
|
|
51
51
|
span.setAttribute("gen_ai.provider.name", "langchain")
|
|
52
52
|
span.setAttribute("gen_ai.tool.call.id", toolName)
|
|
53
|
-
const inputs = args?.args ?? args
|
|
54
|
-
setSpanAttrs(span, mlflowAttrs("TOOL", { inputs, functionName: toolName }))
|
|
55
|
-
if (LOG._debug) span.setAttribute("gen_ai.tool.call.arguments", JSON.stringify(inputs))
|
|
56
53
|
}
|
|
57
54
|
const taskId = config?.configurable?._taskId || cds.context?.["agent.task.id"]
|
|
58
55
|
let outcome
|
|
@@ -64,9 +61,12 @@ export function _patchToolsProto(proto) {
|
|
|
64
61
|
|
|
65
62
|
const semanticError = result?.artifact?.isError === true
|
|
66
63
|
outcome = semanticError ? "error" : "success"
|
|
67
|
-
|
|
64
|
+
let outputs = result?.content ?? result
|
|
68
65
|
|
|
69
66
|
if (span) {
|
|
67
|
+
const inputs = args?.args ?? args
|
|
68
|
+
setSpanAttrs(span, mlflowAttrs("TOOL", { inputs, functionName: toolName }))
|
|
69
|
+
if (LOG._debug) span.setAttribute("gen_ai.tool.call.arguments", JSON.stringify(inputs))
|
|
70
70
|
span.setAttribute("gen_ai.tool.call.outcome", outcome)
|
|
71
71
|
if (semanticError) {
|
|
72
72
|
const msg = typeof outputs === "string" ? outputs : "tool returned isError"
|
package/lib/utils/markdown.js
CHANGED
|
@@ -7,9 +7,9 @@ const LOG = cds.log("agents")
|
|
|
7
7
|
* Absolute filesystem directory of the `.cds` source file that defines `srv`.
|
|
8
8
|
*/
|
|
9
9
|
export function serviceSourceDir(srv) {
|
|
10
|
-
const file = srv?.definition?.$location?.file
|
|
10
|
+
const file = srv?.definition?.["@source"] || srv?.definition?.$location?.file
|
|
11
11
|
if (!file) return undefined
|
|
12
|
-
// $location.file
|
|
12
|
+
// Both @source and $location.file are relative to cds.root
|
|
13
13
|
return path.dirname(path.join(cds.root, file))
|
|
14
14
|
}
|
|
15
15
|
|
package/lib/utils/utils.js
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
import cds from "@sap/cds"
|
|
2
2
|
import { domainToASCII } from "node:url"
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const match = /{i18n>([^}]+)}/.exec(value)
|
|
7
|
-
if (match) {
|
|
8
|
-
return cds.i18n?.labels?.texts4?.(locale)?.[match[1]] || value
|
|
9
|
-
}
|
|
10
|
-
return value
|
|
11
|
-
}
|
|
4
|
+
import { resolveI18n, getFilteredEntities } from "@cap-js/mcp/lib/utils/tools-shared.js"
|
|
5
|
+
export { resolveI18n, getFilteredEntities }
|
|
12
6
|
|
|
13
7
|
/**
|
|
14
8
|
* Mirrors CAP's internal slug rules used for service path generation.
|
|
@@ -46,32 +40,6 @@ export function getDescription(obj, locale) {
|
|
|
46
40
|
return result || undefined
|
|
47
41
|
}
|
|
48
42
|
|
|
49
|
-
export function getFilteredEntities(srv) {
|
|
50
|
-
return Object.fromEntries(
|
|
51
|
-
Object.entries(srv.entities || {})
|
|
52
|
-
.filter(
|
|
53
|
-
([name, entity]) =>
|
|
54
|
-
!entity["@cds.autoexposed"] &&
|
|
55
|
-
!entity["@cds.api.ignore"] &&
|
|
56
|
-
!name.endsWith("DraftAdministrativeData") &&
|
|
57
|
-
!name.endsWith(".drafts") &&
|
|
58
|
-
!name.endsWith(".texts"),
|
|
59
|
-
)
|
|
60
|
-
.sort(([a], [b]) => a.localeCompare(b)),
|
|
61
|
-
)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function getFilteredActions(srv) {
|
|
65
|
-
const actions = {}
|
|
66
|
-
for (const op of srv.actions || []) {
|
|
67
|
-
if ((op.kind === "action" || op.kind === "function") && !op["@cds.api.ignore"]) {
|
|
68
|
-
const localName = op.name.split(".").pop()
|
|
69
|
-
actions[localName] = op
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return actions
|
|
73
|
-
}
|
|
74
|
-
|
|
75
43
|
/**
|
|
76
44
|
* Sanitize an agent name into a valid LangChain/LLM tool name.
|
|
77
45
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/agents",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.7",
|
|
4
4
|
"description": "CDS plugin for building agents",
|
|
5
5
|
"author": "SAP SE (https://www.sap.com)",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@a2a-js/sdk": "^0.3.12",
|
|
44
44
|
"@cap-js/attachments": ">=3",
|
|
45
|
-
"@cap-js/mcp": "^1.
|
|
45
|
+
"@cap-js/mcp": "^1.5",
|
|
46
46
|
"@langchain/anthropic": "^1.5.1",
|
|
47
47
|
"@langchain/core": "^1",
|
|
48
48
|
"@langchain/langgraph": "^1",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"@sap-ai-sdk/langchain": "^2.13.0",
|
|
51
51
|
"@sap-ai-sdk/orchestration": "^2.11.0",
|
|
52
52
|
"@sap-cloud-sdk/connectivity": "^4.7.0",
|
|
53
|
+
"@sap-cloud-sdk/http-client": "^4.9.1",
|
|
53
54
|
"deepagents": "^1.10.0",
|
|
54
55
|
"langchain": "^1.5.0",
|
|
55
56
|
"marked": "^18",
|
|
@@ -91,6 +92,7 @@
|
|
|
91
92
|
"mlflow": false,
|
|
92
93
|
"connect": "auto",
|
|
93
94
|
"retention": "30d",
|
|
95
|
+
"masking": false,
|
|
94
96
|
"[development]": {
|
|
95
97
|
"pushNotifications": {
|
|
96
98
|
"allowedDomains": [
|
|
@@ -6,6 +6,8 @@ import { mlflowAttrs, mlflowTraceAttrs, setSpanAttrs } from "../../lib/telemetry
|
|
|
6
6
|
import { CdsFileStore } from "../../lib/protocol/persistence/file-store.js"
|
|
7
7
|
import { formatFileSize, sanitizeFilename } from "./tools.js"
|
|
8
8
|
import { convertUsageData } from "../../lib/telemetry/chat-tracing.js"
|
|
9
|
+
import { resolvePseudonyms } from "../../lib/masking/index.js"
|
|
10
|
+
import { pseudonymizeUserMessage } from "../../lib/masking/unstructured/index.js"
|
|
9
11
|
import { triggerCleanup } from "../../lib/protocol/persistence/cleanup.js"
|
|
10
12
|
import { COLLECT_RESULT } from "./chat.js"
|
|
11
13
|
import { linkTraceToPrompt } from "../../lib/telemetry/mlflow/tracing.js"
|
|
@@ -197,6 +199,9 @@ class GraphExecutor {
|
|
|
197
199
|
// final visual (collapse to the last turn's bubble at task completion).
|
|
198
200
|
let currentMsgId = null
|
|
199
201
|
let thinkingCount = 0
|
|
202
|
+
// Holds a trailing fragment of the previous chunk that is a prefix of a known
|
|
203
|
+
// pseudonym hash. Prepended to the next chunk so split hashes are resolved correctly.
|
|
204
|
+
let pendingPrefix = ""
|
|
200
205
|
|
|
201
206
|
try {
|
|
202
207
|
if (typeof graph.stream !== "function" || cds.env.agents?.streaming === false) {
|
|
@@ -238,8 +243,23 @@ class GraphExecutor {
|
|
|
238
243
|
const lastChunk =
|
|
239
244
|
!!msgChunk.additional_kwargs?.intermediate_results?.llm?.choices[0].finish_reason
|
|
240
245
|
|
|
241
|
-
const
|
|
242
|
-
|
|
246
|
+
const raw = pendingPrefix + (messageText(msgChunk?.content) ?? "")
|
|
247
|
+
pendingPrefix = ""
|
|
248
|
+
if (!raw) continue
|
|
249
|
+
|
|
250
|
+
// Hashes look like name-8hexchars and never contain spaces.
|
|
251
|
+
// On non-last chunks, slice last token and append to next chunk
|
|
252
|
+
// to avoid unresolved boundaries
|
|
253
|
+
let toEmit = raw
|
|
254
|
+
if (!lastChunk) {
|
|
255
|
+
const lastSpace = raw.lastIndexOf(" ")
|
|
256
|
+
if (lastSpace !== -1 && lastSpace < raw.length - 1) {
|
|
257
|
+
pendingPrefix = raw.slice(lastSpace + 1)
|
|
258
|
+
toEmit = raw.slice(0, lastSpace + 1)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const text = resolvePseudonyms(toEmit)
|
|
243
263
|
// A2A TaskArtifactUpdateEvent: `append` and `lastChunk` are event-level
|
|
244
264
|
// fields (siblings of `artifact`), NOT properties of `artifact`. The SDK's
|
|
245
265
|
// ResultManager reads event.append; nesting them leaves it undefined and
|
|
@@ -354,16 +374,18 @@ class GraphExecutor {
|
|
|
354
374
|
*/
|
|
355
375
|
async _summarizePartialWork(taskId, contextId, serviceName, reason) {
|
|
356
376
|
const { summarizePartialWork } = await import("../../lib/agents/summarize-on-timeout.js")
|
|
357
|
-
return
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
377
|
+
return resolvePseudonyms(
|
|
378
|
+
await summarizePartialWork({
|
|
379
|
+
taskId,
|
|
380
|
+
contextId,
|
|
381
|
+
serviceName,
|
|
382
|
+
reason,
|
|
383
|
+
checkpointer: this._graph?.checkpointer,
|
|
384
|
+
getModel: () => this._srv.send("buildModel"),
|
|
385
|
+
// Summary runs after graph abort, so no execution-time grace is needed.
|
|
386
|
+
timeout: 10_000,
|
|
387
|
+
}),
|
|
388
|
+
)
|
|
367
389
|
}
|
|
368
390
|
|
|
369
391
|
async execute(requestContext, eventBus) {
|
|
@@ -385,6 +407,17 @@ class GraphExecutor {
|
|
|
385
407
|
cds.context["agent.service"] = serviceName
|
|
386
408
|
cds.context["agent.eventBus"] = eventBus
|
|
387
409
|
|
|
410
|
+
// REVISIT: Resolve graph early for pseudonymizeUserMessage. Mid-term move into beforeAgent together with Audit & Telemetry which rely on it
|
|
411
|
+
const graph = await this._resolveGraph()
|
|
412
|
+
if (cds.env.agents.masking) {
|
|
413
|
+
await pseudonymizeUserMessage(
|
|
414
|
+
this._srv,
|
|
415
|
+
requestContext,
|
|
416
|
+
graph.checkpointer,
|
|
417
|
+
`${serviceName}:${contextId}`,
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
|
|
388
421
|
metrics.concurrentExecutions.add(1, mAttrs)
|
|
389
422
|
|
|
390
423
|
if (!isResume) {
|
|
@@ -533,8 +566,6 @@ class GraphExecutor {
|
|
|
533
566
|
let usageData
|
|
534
567
|
let result
|
|
535
568
|
try {
|
|
536
|
-
const graph = await this._resolveGraph()
|
|
537
|
-
|
|
538
569
|
const extraConfig = this._configMapper ? await this._configMapper(requestContext) : {}
|
|
539
570
|
if (extraConfig !== null && extraConfig !== undefined && typeof extraConfig !== "object") {
|
|
540
571
|
throw new TypeError(`configMapper must return a plain object, got ${typeof extraConfig}`)
|
|
@@ -611,7 +642,7 @@ class GraphExecutor {
|
|
|
611
642
|
}
|
|
612
643
|
|
|
613
644
|
const outputMapper = this._outputMapper || defaultOutputMapper
|
|
614
|
-
const output = outputMapper(result) || "I could not generate a response."
|
|
645
|
+
const output = resolvePseudonyms(outputMapper(result)) || "I could not generate a response."
|
|
615
646
|
|
|
616
647
|
LOG.info(serviceName, "completed", { conversation: short(contextId), duration })
|
|
617
648
|
|
|
@@ -620,7 +651,9 @@ class GraphExecutor {
|
|
|
620
651
|
setSpanAttrs(
|
|
621
652
|
wfSpan,
|
|
622
653
|
mlflowAttrs("AGENT", {
|
|
623
|
-
outputs: {
|
|
654
|
+
outputs: {
|
|
655
|
+
choices: [{ message: { role: "assistant", content: output } }],
|
|
656
|
+
},
|
|
624
657
|
functionName: serviceName,
|
|
625
658
|
}),
|
|
626
659
|
)
|
|
@@ -630,7 +663,9 @@ class GraphExecutor {
|
|
|
630
663
|
setSpanAttrs(
|
|
631
664
|
rootSpan,
|
|
632
665
|
mlflowAttrs("CHAIN", {
|
|
633
|
-
outputs: {
|
|
666
|
+
outputs: {
|
|
667
|
+
choices: [{ message: { role: "assistant", content: output } }],
|
|
668
|
+
},
|
|
634
669
|
}),
|
|
635
670
|
)
|
|
636
671
|
}
|
|
@@ -21,5 +21,12 @@ export function buildSystemPrompt(srv) {
|
|
|
21
21
|
]
|
|
22
22
|
: []),
|
|
23
23
|
"Be concise and helpful.",
|
|
24
|
+
...(cds.env.agents?.masking
|
|
25
|
+
? [
|
|
26
|
+
`## How to handle pseudonymized values`,
|
|
27
|
+
`Never comment on pseudonymization and that you had to work with pseudonymized values but use the pseudonymized values in responses like it would be regular values!`,
|
|
28
|
+
`The user sees the actual values and thus should not be made aware of any pseudonymization that has taken place internally.`,
|
|
29
|
+
]
|
|
30
|
+
: []),
|
|
24
31
|
].join("\n")
|
|
25
32
|
}
|
package/srv/handlers/tools.js
CHANGED
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
executeCallActionTool,
|
|
12
12
|
executePerActionTool,
|
|
13
13
|
} from "@cap-js/mcp/lib/tools.js"
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
import { getFilteredEntities, getFilteredActions } from "@cap-js/mcp/lib/utils/tools-shared.js"
|
|
15
16
|
import { isTextMime } from "../../lib/agents/markdown/backends/mime-utils.js"
|
|
16
17
|
import { checkAuthorization } from "@cap-js/mcp/lib/auth.js"
|
|
17
18
|
|
|
@@ -78,8 +79,8 @@ class DescribeTool extends DynamicStructuredTool {
|
|
|
78
79
|
description: def.description,
|
|
79
80
|
schema: def.inputSchema,
|
|
80
81
|
responseFormat: "content_and_artifact",
|
|
81
|
-
func:
|
|
82
|
-
return unwrap(
|
|
82
|
+
func: (args) => {
|
|
83
|
+
return unwrap(executeDescribe(srv, entities, actions, args))
|
|
83
84
|
},
|
|
84
85
|
})
|
|
85
86
|
this.srv = srv
|