@cap-js/agents 0.9.6 → 0.9.8
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.properties +2 -0
- 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 +15 -10
- package/lib/agents/middleware/content-filter.js +4 -0
- package/lib/agents/middleware/hitl-decision-note-injector.js +18 -6
- package/lib/agents/middleware/hitl.js +17 -6
- package/lib/agents/middleware/index.js +3 -1
- package/lib/agents/middleware/masking.js +127 -0
- package/lib/agents/middleware/remote-mcp.js +10 -5
- package/lib/agents/middleware/status-update.js +209 -76
- package/lib/agents/middleware/tool-wrap.js +1 -2
- package/lib/agents/summarize-on-timeout.js +5 -2
- package/lib/config/local.js +121 -8
- package/lib/eval/eval-run.js +50 -4
- 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 +72 -0
- package/lib/masking/unstructured/index.js +52 -0
- package/lib/models/aicore.js +16 -3
- package/lib/models/openai.js +9 -0
- package/lib/preview/chat.html +623 -119
- package/lib/protocol/persistence/cleanup.js +13 -4
- package/lib/telemetry/chat-tracing.js +9 -2
- package/lib/telemetry/mlflow/evaluation.js +83 -4
- package/lib/telemetry/mlflow/exporter/DatabricksExporter.js +39 -5
- package/lib/telemetry/mlflow/exporter/MlflowExporter.js +43 -4
- package/lib/telemetry/mlflow/index.js +1 -7
- package/lib/telemetry/mlflow/prompts.js +15 -11
- 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 +3 -10
- package/lib/utils/resilience.js +2 -1
- package/lib/utils/toml.js +67 -0
- package/lib/utils/usage.js +34 -0
- package/lib/utils/utils.js +43 -1
- package/package.json +33 -26
- package/srv/handlers/graph-executor/crash-handler.js +47 -0
- package/srv/handlers/graph-executor/hitl.js +27 -0
- package/srv/handlers/graph-executor.js +69 -20
- package/srv/handlers/index.js +6 -1
- package/srv/handlers/mcp-tools.js +11 -2
- package/srv/handlers/subagent-tools.js +13 -4
- package/srv/handlers/system-prompt.js +30 -20
- package/srv/handlers/tools.js +14 -13
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Agent status messages (shown during task execution)
|
|
2
|
+
#XMSG Status info message send to the user client when a tool is called. {0} = tool name
|
|
3
|
+
agent_status_calling_tools=正在呼叫 {0}
|
|
4
|
+
#XMSG Status info message send to the user client when an entity is being queried. {0} = entity name
|
|
5
|
+
agent_status_querying=正在查詢 {0}
|
|
6
|
+
#XMSG Status info message send to the user client after a tool result returned and the agent is processing it
|
|
7
|
+
agent_status_processing_response=正在處理工具回應
|
|
8
|
+
#XMSG Status info message send to the user client after multiple tool results returned and the agent is processing it
|
|
9
|
+
agent_status_processing_responses=正在處理工具回應
|
|
10
|
+
agent_status_summarizing_progress=已中斷代理程式;正在彙總進度。
|
|
11
|
+
AGENT_SUMMARY_QUOTA_FALLBACK=由於工作細項已到達資源耗用限制,因此已停止。可能已完成部份工作 - 請查看結果或使用較簡單的請求重試。
|
|
12
|
+
AGENT_SUMMARY_TIMEOUT_FALLBACK=由於工作細項已到達時間限制,因此已停止。進度可能不完整,要繼續執行或停止?
|
|
13
|
+
|
|
14
|
+
# Authentication
|
|
15
|
+
#XMSG Error message for HTTP 401 status
|
|
16
|
+
UNAUTHORIZED=未授權
|
|
17
|
+
#XMSG Error message for HTTP 403 status
|
|
18
|
+
FORBIDDEN=已禁止
|
|
19
|
+
|
|
20
|
+
# Input validation
|
|
21
|
+
#XMSG Error message send to the users client when their message exceeds the limit. {0} = number of allowed characters
|
|
22
|
+
MESSAGE_TOO_LONG=訊息不可超過 {0} 個字元。
|
|
23
|
+
|
|
24
|
+
# Quota enforcement
|
|
25
|
+
QUOTA_CONCURRENT_TASKS=已達到 {0} 個同時工作細項上限,請稍後再試一次。
|
|
26
|
+
QUOTA_CONCURRENT_TASKS_PER_USER=已達到每位使用者 {0} 個同時工作細項上限,請稍後再試一次。
|
|
27
|
+
QUOTA_TASKS_PER_HOUR=此租用戶已達到每小時 {0} 個工作細項上限,請稍後再試一次。
|
|
28
|
+
QUOTA_TASKS_PER_HOUR_PER_USER=已達到每位使用者每小時 {0} 個工作細項上限,請稍後再試一次。
|
|
29
|
+
QUOTA_TOOL_CALLS_PER_HOUR=已使用每小時的工具呼叫數量上限 ({0}),請稍後再試一次。
|
|
30
|
+
QUOTA_LLM_TOKENS_PER_DAY=已使用今天的 LLM 權杖數量上限 ({0}),請明天再試一次。
|
|
31
|
+
|
|
32
|
+
# Content filter
|
|
33
|
+
CONTENT_FILTER_BLOCKED=內容安全篩選已凍結您的輸入,請重新撰寫。
|
|
34
|
+
CONTENT_FILTER_UNAVAILABLE=內容安全檢查暫時無法使用,請再試一次。
|
|
35
|
+
CONTENT_FILTER_TOOL_BLOCKED=上次呼叫的工具產生惡意結果,請嘗試不同方向以避免相同惡意資料。
|
|
36
|
+
|
|
37
|
+
# Push notifications
|
|
38
|
+
PUSH_URL_NOT_ALLOWED=推送通知回呼 URL ''{0}'' 不在允許的網域中:{1}
|
|
39
|
+
|
|
40
|
+
# HITL
|
|
41
|
+
HITL_APPROVE=核准
|
|
42
|
+
HITL_REJECT=拒絕
|
|
43
|
+
HITL_CONTINUE=繼續
|
|
44
|
+
HITL_STOP=停止
|
|
45
|
+
RESUME_REQUIRES_TEXT=繼續訊息必須包含文字 (例如:''核准'' 或 ''拒絕'') 或資料部份。
|
|
46
|
+
|
|
47
|
+
# LLM
|
|
48
|
+
LLM_UNAVAILABLE=代理程式暫時無法使用,請稍後再試一次。
|
package/cds-plugin.js
CHANGED
|
@@ -69,24 +69,27 @@ cds.on("bootstrap", (app) => {
|
|
|
69
69
|
|
|
70
70
|
!(function cds_requires_llm() {
|
|
71
71
|
cds.on("served", async () => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
cds.requires.llm?.kind === "anthropic" ||
|
|
75
|
-
cds.requires.llm === "auto" ||
|
|
76
|
-
cds.requires.llm?.kind === "auto"
|
|
77
|
-
) {
|
|
72
|
+
const kind = cds.requires.llm?.kind ?? cds.requires.llm
|
|
73
|
+
if (kind === "auto") {
|
|
78
74
|
const { resolve_config } = await import("./lib/config/local.js")
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
75
|
+
cds.env.requires.llm = resolve_config({ kind: "auto" })
|
|
76
|
+
}
|
|
77
|
+
if (kind === "anthropic") {
|
|
78
|
+
const { resolve_anthropic_config } = await import("./lib/config/local.js")
|
|
79
|
+
cds.env.requires.llm = resolve_anthropic_config(cds.requires.llm)
|
|
80
|
+
}
|
|
81
|
+
if (kind === "openai") {
|
|
82
|
+
const { resolve_openai_config } = await import("./lib/config/local.js")
|
|
83
|
+
cds.env.requires.llm = resolve_openai_config(cds.requires.llm)
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
const config = cds.requires.llm,
|
|
85
87
|
credentials = {}
|
|
86
|
-
const { url, destination, anthropicApiUrl, apiKey } = config?.credentials || {}
|
|
88
|
+
const { url, destination, anthropicApiUrl, baseURL, apiKey } = config?.credentials || {}
|
|
87
89
|
if (url) credentials.url = url
|
|
88
90
|
if (destination) credentials.destination = destination
|
|
89
91
|
if (anthropicApiUrl) credentials.anthropicApiUrl = anthropicApiUrl
|
|
92
|
+
if (baseURL) credentials.baseURL = baseURL
|
|
90
93
|
if (apiKey) credentials.apiKey = "***"
|
|
91
94
|
LOG.info(`cds.connect.to 'llm' with:`, { ...config, credentials })
|
|
92
95
|
})
|
|
@@ -103,7 +106,9 @@ if (cds.env.requires?.telemetry)
|
|
|
103
106
|
// Schedule active_users metric computation + MLflow exporter
|
|
104
107
|
cds.on("served", async () => {
|
|
105
108
|
const { setupActiveUsersMetric } = await import("./lib/telemetry/active-users.js")
|
|
109
|
+
const { setupTraceScrubbing } = await import("./lib/telemetry/span-masking.js")
|
|
106
110
|
setupActiveUsersMetric()
|
|
111
|
+
await setupTraceScrubbing()
|
|
107
112
|
// Defer LangChain patching so the CDS model is fully loaded before patches land.
|
|
108
113
|
// opt-out via cds.env.agents.trace_langchain = false
|
|
109
114
|
if (cds.env.agents?.trace_langchain !== false) {
|
|
@@ -47,6 +47,7 @@ export async function contentFilterMiddleware(model) {
|
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
disableStreaming: true,
|
|
50
|
+
maxRetries: 0,
|
|
50
51
|
customRequestConfig: {
|
|
51
52
|
middleware: [timeout(10_000), retry(2)],
|
|
52
53
|
},
|
|
@@ -112,6 +113,9 @@ export async function contentFilterMiddleware(model) {
|
|
|
112
113
|
try {
|
|
113
114
|
await inputFilterModel.invoke(filterMessages)
|
|
114
115
|
} catch (err) {
|
|
116
|
+
if (err.rootCause?.message.match(/Could not find service credentials for AI Core/)) {
|
|
117
|
+
throw err.rootCause
|
|
118
|
+
}
|
|
115
119
|
const { status, data } = extractErrorDetails(err)
|
|
116
120
|
const isFilterBlock =
|
|
117
121
|
/Filtering Module/i.test(data?.error?.location || "") && status === 400
|
|
@@ -2,17 +2,29 @@ import { createMiddleware } from "langchain"
|
|
|
2
2
|
import { HumanMessage } from "@langchain/core/messages"
|
|
3
3
|
import { z } from "zod"
|
|
4
4
|
|
|
5
|
-
// Injects user HITL decisions
|
|
5
|
+
// Injects user HITL decisions (edit/reject) as a HumanMessage before the model
|
|
6
|
+
// continues, so the model treats them as deliberate user decisions rather than
|
|
7
|
+
// tool failures.
|
|
8
|
+
//
|
|
9
|
+
// LangChain's humanInTheLoop middleware routes these decisions differently:
|
|
10
|
+
// - edit → jumpTo undefined → normal flow → `beforeModel` runs. Inject + clear here.
|
|
11
|
+
// - reject → jumpTo "model" → re-enters the model node directly, SKIPPING all
|
|
12
|
+
// `beforeModel` hooks. `wrapModelCall` still runs, so it injects as a
|
|
13
|
+
// fallback when `beforeModel` did not consume the note.
|
|
14
|
+
//
|
|
15
|
+
// So we use `wrapModel` to inject the message and clear the note after the model call.
|
|
6
16
|
export function hitlDecisionNoteInjectorMiddleware() {
|
|
7
17
|
return createMiddleware({
|
|
8
18
|
name: "hitlDecisionNoteInjectorMiddleware",
|
|
9
19
|
stateSchema: z.object({ _hitlDecisionNote: z.string().optional() }),
|
|
10
|
-
|
|
20
|
+
wrapModelCall: async (request, handler) => {
|
|
21
|
+
const note = request.state?._hitlDecisionNote
|
|
22
|
+
if (!note) return handler(request)
|
|
23
|
+
return handler({ ...request, messages: [...(request.messages || []), new HumanMessage(note)] })
|
|
24
|
+
},
|
|
25
|
+
afterModel: async (state) => {
|
|
11
26
|
if (!state._hitlDecisionNote) return
|
|
12
|
-
return {
|
|
13
|
-
messages: [new HumanMessage(state._hitlDecisionNote)],
|
|
14
|
-
_hitlDecisionNote: undefined,
|
|
15
|
-
}
|
|
27
|
+
return { _hitlDecisionNote: undefined }
|
|
16
28
|
},
|
|
17
29
|
})
|
|
18
30
|
}
|
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import { humanInTheLoopMiddleware as hitl } from "langchain"
|
|
2
2
|
import { hitlDecisionNoteInjectorMiddleware } from "./hitl-decision-note-injector.js"
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
const ALLOWED_DECISIONS = ["approve", "reject", "edit"]
|
|
5
|
+
|
|
6
|
+
const isHitlAction = (action) => action?.["@agent.hitl"] ?? action?.["@Common.IsActionCritical"]
|
|
7
|
+
|
|
8
|
+
export function buildHitlInterruptMap(srv, tools = []) {
|
|
5
9
|
return tools.reduce((interruptOn, tool) => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
// Per-action tool: its name IS the action name, so a static entry gates it.
|
|
11
|
+
if (isHitlAction(srv.actions[tool.name])) {
|
|
12
|
+
interruptOn[tool.name] = { allowedDecisions: ALLOWED_DECISIONS }
|
|
13
|
+
}
|
|
14
|
+
// Generic @cap-js/mcp "call" tool: one tool fronts every action, with the
|
|
15
|
+
// target action in args.action. The tool name matches no action, so gate
|
|
16
|
+
// per-call via `when`, which inspects the requested action at invoke time.
|
|
17
|
+
else if (tool.name === "call" && Object.values(srv.actions).some(isHitlAction)) {
|
|
18
|
+
interruptOn[tool.name] = {
|
|
19
|
+
allowedDecisions: ALLOWED_DECISIONS,
|
|
20
|
+
when: (request) => Boolean(isHitlAction(srv.actions[request.toolCall?.args?.action])),
|
|
21
|
+
}
|
|
11
22
|
}
|
|
12
23
|
return interruptOn
|
|
13
24
|
}, {})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export default async function buildMiddleware(srv, options = {}) {
|
|
2
2
|
const { tools, model } = options
|
|
3
|
+
const { masking } = await import("./masking.js")
|
|
3
4
|
const { quotaEnforcerMiddleware } = await import("./quota-enforcer.js")
|
|
4
5
|
const { contentFilterMiddleware } = await import("./content-filter.js")
|
|
5
6
|
const { agentActionsMiddleware } = await import("./agent-actions.js")
|
|
@@ -18,11 +19,12 @@ export default async function buildMiddleware(srv, options = {}) {
|
|
|
18
19
|
|
|
19
20
|
return [
|
|
20
21
|
...dynamicMcpMiddlewares,
|
|
22
|
+
masking(srv), // first — all downstream middleware see pseudonymized data
|
|
21
23
|
...(await quotaEnforcerMiddleware()),
|
|
22
24
|
await contentFilterMiddleware(model),
|
|
23
25
|
await agentActionsMiddleware(),
|
|
24
26
|
patchToolCallsMiddleware(),
|
|
25
|
-
await statusUpdateMiddleware(),
|
|
27
|
+
await statusUpdateMiddleware(tools),
|
|
26
28
|
...(await humanInTheLoopMiddleware(srv, tools)),
|
|
27
29
|
toolSelectionMiddleware(),
|
|
28
30
|
toolWrapMiddleware(srv),
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import cds from "@sap/cds"
|
|
2
|
+
import { createMiddleware, ToolMessage } from "langchain"
|
|
3
|
+
import { randomBytes } from "node:crypto"
|
|
4
|
+
import { decode, encode } from "@toon-format/toon"
|
|
5
|
+
import { ensureSession, pseudonymize, resolveRemoteMcpTool } from "../../masking/index.js"
|
|
6
|
+
import { resolveArgs, needsMasking } from "../../masking/structured/index.js"
|
|
7
|
+
import z from "zod"
|
|
8
|
+
import { Command, StateSchema, ReducedValue, isCommand } from "@langchain/langgraph"
|
|
9
|
+
|
|
10
|
+
// REVISIT: Whether it is possible to make it easier without the ReducedValue class
|
|
11
|
+
const stateSchema = new StateSchema({
|
|
12
|
+
seed: new ReducedValue(z.string().default(randomBytes(16).toString("hex")), {
|
|
13
|
+
reducer: (_current, next) => next,
|
|
14
|
+
}),
|
|
15
|
+
hashToOriginal: new ReducedValue(z.map(z.string(), z.string()).default(new Map()), {
|
|
16
|
+
reducer: (_current, next) => next,
|
|
17
|
+
}),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export function masking(srv) {
|
|
21
|
+
if (!cds.env.agents?.masking) return null
|
|
22
|
+
|
|
23
|
+
return createMiddleware({
|
|
24
|
+
name: "masking",
|
|
25
|
+
stateSchema,
|
|
26
|
+
|
|
27
|
+
beforeAgent: async () => {
|
|
28
|
+
if (!needsMasking(srv.name)) return
|
|
29
|
+
await ensureSession()
|
|
30
|
+
cds.context["agent.masking.tools"] = true
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
wrapToolCall: async (request, handler) => {
|
|
34
|
+
const session = cds.context?.["agent.pseudonyms"]
|
|
35
|
+
if (!session || !cds.context["agent.masking.tools"]) return handler(request)
|
|
36
|
+
|
|
37
|
+
const toolName = request.toolCall?.name
|
|
38
|
+
const toolArgs = request.toolCall?.args ?? {}
|
|
39
|
+
const resolvedArgs = resolveArgs(toolArgs, session)
|
|
40
|
+
const resolvedRequest =
|
|
41
|
+
resolvedArgs !== toolArgs
|
|
42
|
+
? { ...request, toolCall: { ...request.toolCall, args: resolvedArgs } }
|
|
43
|
+
: request
|
|
44
|
+
|
|
45
|
+
const result = await handler(resolvedRequest)
|
|
46
|
+
const isCmd = isCommand(result)
|
|
47
|
+
const toolMessage = isCmd ? result.update?.messages?.[0] : result
|
|
48
|
+
|
|
49
|
+
let outMessage = toolMessage
|
|
50
|
+
if (toolMessage?.content) {
|
|
51
|
+
// Resolve remote MCP tool prefix before srv.send (cds.context differs inside event)
|
|
52
|
+
const remote = resolveRemoteMcpTool(toolName)
|
|
53
|
+
const effectiveToolName = remote?.bareName ?? toolName
|
|
54
|
+
|
|
55
|
+
// Decode TOON or JSON — pseudonymize operates on decoded objects
|
|
56
|
+
const { decoded, format } = decodeContent(toolMessage.content)
|
|
57
|
+
if (decoded && typeof decoded !== "string") {
|
|
58
|
+
const result = await pseudonymize(
|
|
59
|
+
{
|
|
60
|
+
data: decoded,
|
|
61
|
+
type: toolArgs.cql ? "cql" : "action",
|
|
62
|
+
seed: session._seed,
|
|
63
|
+
metadata: {
|
|
64
|
+
cql: toolArgs.cql,
|
|
65
|
+
toolName: effectiveToolName,
|
|
66
|
+
...(remote?.remoteSrv && { serviceName: remote.remoteSrv.name }),
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
srv,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
session.addMappings(result?.mappings)
|
|
73
|
+
|
|
74
|
+
const encoded =
|
|
75
|
+
format === "toon"
|
|
76
|
+
? encode(result.data)
|
|
77
|
+
: format === "json"
|
|
78
|
+
? JSON.stringify(result.data)
|
|
79
|
+
: result.data
|
|
80
|
+
if (encoded !== toolMessage.content) {
|
|
81
|
+
outMessage = new ToolMessage({ ...toolMessage, content: encoded })
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
// Plain string (e.g. read_file output) or unparseable — scrub using session mappings.
|
|
85
|
+
const scrubbed = session.scrubText(toolMessage.content)
|
|
86
|
+
if (scrubbed !== toolMessage.content) {
|
|
87
|
+
outMessage = new ToolMessage({ ...toolMessage, content: scrubbed })
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const cmd = new Command({
|
|
93
|
+
update: {
|
|
94
|
+
messages: [outMessage],
|
|
95
|
+
seed: session._seed,
|
|
96
|
+
hashToOriginal: session._hashToOriginal,
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
if (isCmd) return merge(result, cmd)
|
|
100
|
+
return cmd
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function merge(c1, c2) {
|
|
106
|
+
return new Command({
|
|
107
|
+
...c1,
|
|
108
|
+
...c2,
|
|
109
|
+
update: {
|
|
110
|
+
...c1.update,
|
|
111
|
+
...c2.update,
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function decodeContent(content) {
|
|
117
|
+
if (typeof content !== "string" || !content) return {}
|
|
118
|
+
try {
|
|
119
|
+
return { decoded: decode(content), format: "toon" }
|
|
120
|
+
} catch {
|
|
121
|
+
try {
|
|
122
|
+
return { decoded: JSON.parse(content), format: "json" }
|
|
123
|
+
} catch {
|
|
124
|
+
return {}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -2,7 +2,7 @@ import cds from "@sap/cds"
|
|
|
2
2
|
import { createMiddleware } from "langchain"
|
|
3
3
|
import { ToolMessage } from "@langchain/core/messages"
|
|
4
4
|
import { MultiServerMCPClient } from "@langchain/mcp-adapters"
|
|
5
|
-
import { toolName } from "../../utils/utils.js"
|
|
5
|
+
import { toolName, mcpToolKind } from "../../utils/utils.js"
|
|
6
6
|
|
|
7
7
|
const LOG = cds.log("agents:mcp")
|
|
8
8
|
|
|
@@ -27,20 +27,25 @@ export function remoteMcpMiddleware() {
|
|
|
27
27
|
})
|
|
28
28
|
let raw = await client.getTools()
|
|
29
29
|
if (toolFilter?.length) raw = raw.filter((t) => toolFilter.includes(t.name))
|
|
30
|
-
for (const t of raw)
|
|
31
|
-
|
|
30
|
+
for (const t of raw) {
|
|
31
|
+
t.metadata = { ...t.metadata, serviceName, kind: mcpToolKind(t.name) }
|
|
32
|
+
t.name = toolName(`${serviceName}_${t.name}`)
|
|
33
|
+
}
|
|
34
|
+
cache[mcpUrl] = { serviceName, tools: raw }
|
|
32
35
|
LOG.debug(
|
|
33
36
|
`Got ${raw.length} MCP tools from ${mcpUrl}: ${raw.map((t) => t.name).join(", ")}`,
|
|
34
37
|
)
|
|
35
38
|
}),
|
|
36
39
|
)
|
|
37
|
-
const resolved = placeholders.flatMap(({ mcpUrl }) => cache[mcpUrl] ?? [])
|
|
40
|
+
const resolved = placeholders.flatMap(({ mcpUrl }) => cache[mcpUrl]?.tools ?? [])
|
|
38
41
|
const staticTools = (request.tools ?? []).filter((t) => !t._mcpDynamic)
|
|
39
42
|
return handler({ ...request, tools: [...staticTools, ...resolved] })
|
|
40
43
|
},
|
|
41
44
|
|
|
42
45
|
wrapToolCall: async (request, handler) => {
|
|
43
|
-
const allCached = Object.values(cds.context.__mcpDynamicTools ?? {}).
|
|
46
|
+
const allCached = Object.values(cds.context.__mcpDynamicTools ?? {}).flatMap(
|
|
47
|
+
(e) => e.tools ?? [],
|
|
48
|
+
)
|
|
44
49
|
const tool = allCached.find((t) => t.name === request.toolCall.name)
|
|
45
50
|
if (!tool) return handler(request)
|
|
46
51
|
try {
|