UNPKG

@botpress/adk-cli

Version:

Command-line interface for the Botpress Agent Development Kit (ADK)

632 lines (629 loc) 21.6 kB
// @bun import { parseDuration } from "./chunk-wk6ynbab.js"; import { SqliteTraceStore } from "./chunk-2re833ev.js"; import { ensureJsonOnlyFormat } from "./chunk-7sfagm12.js"; import { findAgentRootOrFail } from "./chunk-kk3h6qaj.js"; import { createCliLogger, source_default } from "./chunk-gzwt1qdr.js"; import"./chunk-nxy2ya5r.js"; import"./chunk-wzj4dc7n.js"; import { AdkError } from "./chunk-p0hjqn4r.js"; import"./chunk-np5wcwfv.js"; import"./chunk-dq2xpa24.js"; import"./chunk-6w0knnta.js"; import"./chunk-40x04ckt.js"; import"./chunk-t76d8fxx.js"; import"./chunk-nh2akp42.js"; import"./chunk-0fdvzjbh.js"; import"./chunk-2a5b6azq.js"; import"./chunk-vay209b5.js"; import"./chunk-3xrpxgq4.js"; import"./chunk-rfm3jr1m.js"; import"./chunk-w346ejn9.js"; import"./chunk-knvm2anf.js"; import"./chunk-65h5trb5.js"; import"./chunk-s2akeqpw.js"; import"./chunk-6771vrjp.js"; import"./chunk-g8mm42v1.js"; import"./chunk-50hzjdck.js"; import"./chunk-nn2jb0x0.js"; import"./chunk-v8xvth6j.js"; import"./chunk-kkk13rcb.js"; import"./chunk-ytpp1kam.js"; import"./chunk-na956zz3.js"; import"./chunk-f4bw8q7c.js"; import"./chunk-0v8vgrns.js"; import"./chunk-54qt5g7m.js"; import"./chunk-dhs2bg35.js"; // src/commands/adk-traces.ts import { createInterface } from "readline"; // src/traces/trace-filter-tokens.ts var VALID_BARE_KEYWORDS = "error"; var VALID_KV_KEYS = "workflow, conversation, action, trigger, trace, since, until, limit"; var DURATION_HINT = "Use number + unit: 30s, 5m, 1h, 2d, 1w"; var POSITIVE_INT_HINT = "Must be a positive integer"; var ARRAY_KEYS = new Set(["workflow", "conversation", "action", "trigger", "trace"]); function parseTraceFilterTokens(tokens) { const opts = {}; const errors = []; for (const token of tokens) { const lower = token.toLowerCase(); if (lower === "error" || lower === "errors") { opts.status = "error"; continue; } const eqIdx = token.indexOf("="); if (eqIdx === -1) { errors.push({ token, message: `Unknown filter \`${token}\``, suggestion: `Valid filters: ${VALID_BARE_KEYWORDS}, ${VALID_KV_KEYS}` }); continue; } const key = token.slice(0, eqIdx).toLowerCase(); const val = token.slice(eqIdx + 1); if (ARRAY_KEYS.has(key)) { const values = val.split(",").map((v) => v.trim()).filter(Boolean); if (values.length === 0) { errors.push({ token, message: `Empty value for \`${key}\``, suggestion: `Provide one or more values separated by commas: ${key}=val1,val2` }); continue; } switch (key) { case "workflow": opts.workflows = (opts.workflows ?? []).concat(values); break; case "conversation": opts.conversations = (opts.conversations ?? []).concat(values); break; case "action": opts.actions = (opts.actions ?? []).concat(values); break; case "trigger": opts.triggers = (opts.triggers ?? []).concat(values); break; case "trace": opts.traceIds = (opts.traceIds ?? []).concat(values); break; } continue; } if (key === "since") { const ms = parseDuration(val); if (ms !== null) { opts.since = Date.now() - ms; } else { errors.push({ token, message: `Invalid duration \`${val}\` for \`since\``, suggestion: DURATION_HINT }); } continue; } if (key === "until") { const ms = parseDuration(val); if (ms !== null) { opts.until = Date.now() - ms; } else { errors.push({ token, message: `Invalid duration \`${val}\` for \`until\``, suggestion: DURATION_HINT }); } continue; } if (key === "limit") { const n = parseInt(val, 10); if (!isNaN(n) && n > 0) { opts.limit = n; } else { errors.push({ token, message: `Invalid value \`${val}\` for \`limit\``, suggestion: POSITIVE_INT_HINT }); } continue; } errors.push({ token, message: `Unknown filter key \`${key}\``, suggestion: `Valid keys: ${VALID_KV_KEYS}` }); } return { options: opts, errors }; } // src/traces/trace-summary.ts var HIGH_IMPORTANCE_SPANS = new Set([ "request.incoming", "cognitive.request", "handler.conversation", "handler.trigger", "handler.workflow", "handler.workflow.step", "handler.action", "handler.event", "autonomous.execution", "autonomous.iteration", "autonomous.tool", "chat.sendMessage" ]); var MAX_PATH_HOPS = 5; function buildTraceSummary(spans) { if (spans.length === 0) { throw new AdkError({ code: "EMPTY_SPANS", message: "Cannot build summary from empty spans array" }); } const traceId = spans[0].id.trace; const roots = spans.filter((s) => s.id.parent === null); const root = roots.sort((a, b) => a.timing.startedAt - b.timing.startedAt)[0] ?? spans[0]; const childrenMap = new Map; for (const span of spans) { if (span.id.parent) { const children = childrenMap.get(span.id.parent) ?? []; children.push(span); childrenMap.set(span.id.parent, children); } } const pathNames = []; let current = root; while (current && pathNames.length < MAX_PATH_HOPS) { const displayName = current.label && current.label !== current.name ? current.label : current.name; pathNames.push(displayName); const children = childrenMap.get(current.id.span); if (!children || children.length === 0) break; current = pickCriticalChild(children); } const hasError = spans.some((s) => s.status === "error"); const hasRunning = spans.some((s) => s.status === "running"); const status = hasError ? "error" : hasRunning ? "running" : "ok"; const startTime = Math.min(...spans.map((s) => s.timing.startedAt)); const duration = root.timing.duration ?? null; const conversationId = spans.find((s) => s.context.conversationId)?.context.conversationId; const summary = `${pathNames.join(" \u2192 ")} [${status}]`; return { traceId, startTime: new Date(startTime).toISOString(), duration, status, spanCount: spans.length, conversationId, summary }; } function pickCriticalChild(children) { return children.sort((a, b) => { const aHigh = HIGH_IMPORTANCE_SPANS.has(a.name) ? 1 : 0; const bHigh = HIGH_IMPORTANCE_SPANS.has(b.name) ? 1 : 0; if (aHigh !== bHigh) return bHigh - aHigh; return (b.timing.duration ?? 0) - (a.timing.duration ?? 0); })[0]; } // src/traces/trace-formatter.ts function formatDuration(ms) { if (ms == null) return "---"; if (ms < 1) return "<1ms"; if (ms < 1000) return `${Math.round(ms)}ms`; if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; return `${(ms / 60000).toFixed(1)}m`; } function padDuration(ms, width = 6) { return formatDuration(ms).padEnd(width); } function statusIcon(status) { switch (status) { case "ok": return source_default.green("\u2713"); case "error": return source_default.red("\u2717"); case "running": return source_default.yellow("\u25CC"); } } function statusWord(status) { switch (status) { case "ok": return source_default.green("ok"); case "error": return source_default.red("error"); case "running": return source_default.yellow("running"); } } function formatTraceSummary(summary) { const time = new Date(summary.startTime).toLocaleTimeString("en-US", { hour12: false }); const dur = padDuration(summary.duration); const icon = statusIcon(summary.status); const id = source_default.dim(summary.traceId.slice(0, 8)); return `[${time}] ${dur} ${icon} ${id} ${summary.summary}`; } function formatTraceDetail(traceId, spans, opts = {}) { if (spans.length === 0) return source_default.dim(`Trace ${traceId}: no spans found`); const roots = spans.filter((s) => s.id.parent === null); const root = roots.sort((a, b) => a.timing.startedAt - b.timing.startedAt)[0] ?? spans[0]; const hasError = spans.some((s) => s.status === "error"); const hasRunning = spans.some((s) => s.status === "running"); const traceStatus = hasError ? "error" : hasRunning ? "running" : "ok"; const lines = []; const totalDuration = root.timing.duration; lines.push(`Trace ${source_default.bold(traceId)} ${statusIcon(traceStatus)} ${statusWord(traceStatus)} ${formatDuration(totalDuration)}`); const childrenMap = new Map; for (const span of spans) { if (span.id.parent) { const children = childrenMap.get(span.id.parent) ?? []; children.push(span); childrenMap.set(span.id.parent, children); } } for (const [, children] of childrenMap) { children.sort((a, b) => a.timing.startedAt - b.timing.startedAt); } renderSpanTree(root, "", true, childrenMap, lines, opts); return lines.join(` `); } function renderSpanTree(span, prefix, isLast, childrenMap, lines, opts) { const connector = prefix === "" ? "" : isLast ? "\u2514\u2500 " : "\u251C\u2500 "; const displayName = span.label && span.label !== span.name ? `${span.name} (${span.label})` : span.name; const dur = formatDuration(span.timing.duration); const errorMark = span.status === "error" ? ` ${source_default.red("\u2717")}` : ""; const cachedMark = isCachedStep(span) ? ` ${source_default.dim("(cached)")}` : ""; const tokenInfo = formatTokenInfo(span); lines.push(`${prefix}${connector}${displayName}${errorMark}${cachedMark} ${source_default.dim(dur)}${tokenInfo}`); if (span.error) { const errPrefix = prefix === "" ? " " : prefix + (isLast ? " " : "\u2502 "); lines.push(`${errPrefix}${source_default.red(span.error)}`); } if (opts.includeLlm) { const detailPrefix = prefix === "" ? " " : prefix + (isLast ? " " : "\u2502 "); const llmLines = formatLlmDetails(span); for (const line of llmLines) { lines.push(`${detailPrefix}${line}`); } } const children = childrenMap.get(span.id.span); if (!children) return; const childPrefix = prefix === "" ? "" : prefix + (isLast ? " " : "\u2502 "); for (let i = 0;i < children.length; i++) { renderSpanTree(children[i], childPrefix, i === children.length - 1, childrenMap, lines, opts); } } function formatTokenInfo(span) { if (span.name !== "cognitive.request") return ""; const data = span.data; const inputTokens = data?.["ai.input_tokens"]; const outputTokens = data?.["ai.output_tokens"]; if (inputTokens != null && outputTokens != null) { return source_default.dim(` ${inputTokens}\u2192${outputTokens} tokens`); } return ""; } function isCachedStep(span) { if (span.name !== "handler.workflow.step") return false; const data = span.data; return data?.["workflow.step.cached"] === true; } function formatLlmDetails(span) { const data = span.data; const lines = []; if (span.name === "cognitive.request") { const instructions = data?.["ai.instructions"]; if (typeof instructions === "string") { const truncated = instructions.length > 200 ? instructions.slice(0, 200) + "..." : instructions; lines.push(source_default.dim(`instructions: ${truncated}`)); } const tools = data?.["ai.tools"]; if (tools) { lines.push(source_default.dim(`tools: ${JSON.stringify(tools)}`)); } } if (span.name === "autonomous.iteration") { const instructions = data?.["autonomous.instructions"]; if (typeof instructions === "string") { const truncated = instructions.length > 200 ? instructions.slice(0, 200) + "..." : instructions; lines.push(source_default.dim(`instructions: ${truncated}`)); } const code = data?.["autonomous.code"]; if (typeof code === "string") { const truncated = code.length > 300 ? code.slice(0, 300) + "..." : code; lines.push(source_default.dim(`code: ${truncated}`)); } const thoughts = data?.["autonomous.thoughts"]; if (typeof thoughts === "string") { const truncated = thoughts.length > 200 ? thoughts.slice(0, 200) + "..." : thoughts; lines.push(source_default.dim(`thoughts: ${truncated}`)); } } if (span.name === "botpress.client" || span.name === "http.client") { const bp = span.name === "botpress.client"; const method = data?.[bp ? "botpress.method" : "http.method"]; const url = data?.[bp ? "botpress.url" : "http.url"]; if (method || url) { lines.push(source_default.dim(`${method ?? ""} ${url ?? ""}`.trim())); } const requestBody = data?.[bp ? "botpress.request.body" : "http.request.body"]; if (typeof requestBody === "string" && requestBody) { lines.push(source_default.dim(`request: ${requestBody.length > 200 ? requestBody.slice(0, 200) + "..." : requestBody}`)); } const responseBody = data?.[bp ? "botpress.response.body" : "http.response.body"]; if (typeof responseBody === "string" && responseBody) { lines.push(source_default.dim(`response: ${responseBody.length > 200 ? responseBody.slice(0, 200) + "..." : responseBody}`)); } } return lines; } // src/traces/trace-query.ts function queryTraceOverviews(store, filters) { const traceIds = store.queryTraceIds({ since: filters.since, until: filters.until, status: filters.status, conversations: filters.conversations, workflows: filters.workflows, actions: filters.actions, triggers: filters.triggers, limit: filters.limit ?? 20 }); const summaries = []; for (const traceId of traceIds) { const spans = store.getTraceById(traceId); if (spans.length > 0) { summaries.push(buildTraceSummary(spans)); } } return summaries.sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime()); } function queryTraceDetails(store, traceIds) { const result = new Map; for (const traceId of traceIds) { const spans = store.getTraceById(traceId); if (spans.length > 0) { result.set(traceId, spans); } } return result; } function createTraceFollower(store, filters) { let lastPollTs = Date.now(); let stopped = false; const emittedTraceIds = new Set; let resolveNext = null; const buffer = []; let intervalId = null; function poll() { if (stopped) return; const prevTs = lastPollTs; lastPollTs = Date.now(); const completedRoots = store.getCompletedRootSpansSince(prevTs); const traceIds = new Set; for (const span of completedRoots) { if (!emittedTraceIds.has(span.id.trace)) { traceIds.add(span.id.trace); } } for (const traceId of traceIds) { const spans = store.getTraceById(traceId); if (spans.length === 0) continue; const summary = buildTraceSummary(spans); if (filters.status === "error" && summary.status !== "error") continue; if (filters.workflows && filters.workflows.length > 0) { const hasWorkflow = spans.some((s) => s.name === "handler.workflow" && filters.workflows.includes(s.data?.["workflow.name"])); if (!hasWorkflow) continue; } if (filters.conversations && filters.conversations.length > 0) { const hasConv = spans.some((s) => s.context.conversationId && filters.conversations.includes(s.context.conversationId)); if (!hasConv) continue; } if (filters.actions && filters.actions.length > 0) { const hasAction = spans.some((s) => { const data = s.data; return s.name === "handler.action" && filters.actions.includes(data?.["action.name"]) || s.name === "autonomous.tool" && filters.actions.includes(data?.["autonomous.tool.name"]); }); if (!hasAction) continue; } if (filters.triggers && filters.triggers.length > 0) { const hasTrigger = spans.some((s) => s.name === "handler.trigger" && filters.triggers.includes(s.data?.["trigger.name"])); if (!hasTrigger) continue; } emittedTraceIds.add(traceId); if (resolveNext) { const resolve = resolveNext; resolveNext = null; resolve({ value: summary, done: false }); } else { buffer.push(summary); } } } function stop() { stopped = true; if (intervalId) { clearInterval(intervalId); intervalId = null; } if (resolveNext) { const resolve = resolveNext; resolveNext = null; resolve({ value: undefined, done: true }); } } const summaries = { [Symbol.asyncIterator]() { if (!intervalId && !stopped) { intervalId = setInterval(poll, 1000); } return { next() { if (buffer.length > 0) { return Promise.resolve({ value: buffer.shift(), done: false }); } if (stopped) { return Promise.resolve({ value: undefined, done: true }); } return new Promise((resolve) => { resolveNext = resolve; }); } }; } }; return { summaries, stop }; } // src/commands/adk-traces.ts async function resolveTraceId(store, partial, logger) { const exact = store.getTraceById(partial); if (exact.length > 0) return partial; const candidates = store.findTraceIdsByPrefix(partial); if (candidates.length === 0) return null; if (candidates.length === 1) return candidates[0]; logger.warn(`"${partial}" matches ${candidates.length} traces: `); for (let i = 0;i < candidates.length; i++) { const spans = store.getTraceById(candidates[i]); if (spans.length > 0) { const summary = buildTraceSummary(spans); const time = new Date(summary.startTime).toLocaleTimeString("en-US", { hour12: false }); const statusMark = summary.status === "error" ? "\u2717" : summary.status === "running" ? "\u25CC" : "\u2713"; logger.info(` ${i + 1}. ${candidates[i]} [${time}] ${statusMark} ${summary.summary}`, "gray"); } else { logger.info(` ${i + 1}. ${candidates[i]} (no spans)`, "gray"); } } logger.newline(); const rl = createInterface({ input: process.stdin, output: process.stderr }); const answer = await new Promise((resolve) => { rl.question(`Pick a trace (1-${candidates.length}): `, resolve); }); rl.close(); const choice = parseInt(answer.trim(), 10); if (isNaN(choice) || choice < 1 || choice > candidates.length) { return null; } return candidates[choice - 1]; } async function adkTraces(tokens, options) { const logger = createCliLogger({ format: options?.format }); ensureJsonOnlyFormat(options.format); const agentRoot = await findAgentRootOrFail(process.cwd()); const { options: filters, errors } = parseTraceFilterTokens(tokens); if (errors.length > 0) { throw new AdkError({ code: "INVALID_FLAGS", message: errors.map((e) => e.suggestion ? `${e.message} (${e.suggestion})` : e.message).join("; "), expected: true }); } const store = new SqliteTraceStore(agentRoot); try { if (filters.traceIds && filters.traceIds.length > 0) { if (options.follow) { logger.warn("Warning: --follow is not supported in drill-in mode, ignoring."); } const resolvedIds = []; for (const partial of filters.traceIds) { const resolved = await resolveTraceId(store, partial, logger); if (resolved) { resolvedIds.push(resolved); } else { logger.info(`No trace found matching "${partial}".`); } } if (resolvedIds.length === 0) { return; } const details = queryTraceDetails(store, resolvedIds); if (details.size === 0) { logger.info("No traces found for the given IDs."); return; } for (const [traceId, spans] of details) { const jsonSpans = spans.map((s) => { const cached = isCachedStep(s); const base = options.includeLlm ? s : (({ data: _, ...rest }) => rest)(s); return cached ? { ...base, cached: true } : base; }); logger.info(formatTraceDetail(traceId, spans, { includeLlm: options.includeLlm })).result({ traceId, spans: jsonSpans }); logger.newline(); } } else { if (options.follow) { const existing = queryTraceOverviews(store, { ...filters, limit: filters.limit }); const cap = filters.limit; let printed = 0; for (const summary of existing) { if (cap && printed >= cap) break; logger.info(formatTraceSummary(summary)).result(summary); printed++; } if (cap && printed >= cap) return; const { summaries, stop } = createTraceFollower(store, filters); process.on("SIGINT", stop); process.on("SIGTERM", stop); for await (const summary of summaries) { logger.info(formatTraceSummary(summary)).result(summary); printed++; if (cap && printed >= cap) { stop(); break; } } } else { if (!filters.limit) { filters.limit = 20; } const summaries = queryTraceOverviews(store, filters); if (summaries.length === 0) { logger.info("No traces match the given filters."); return; } for (const summary of summaries) { logger.info(formatTraceSummary(summary)).result(summary); } } } } finally { store.close(); } } export { adkTraces };