UNPKG

@dooor-ai/toolkit

Version:

Guards, Evals & Observability for AI applications - works seamlessly with LangChain/LangGraph

331 lines (330 loc) • 13.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ObservabilityCollector = exports.CortexDBBackend = exports.ConsoleBackend = void 0; exports.logTrace = logTrace; const cortexdb_client_1 = require("./cortexdb-client"); /** * Console-based observability backend for development */ class ConsoleBackend { constructor(verbose = true) { this.verbose = verbose; } logTrace(trace) { if (!this.verbose) return; console.log("\n" + "=".repeat(80)); console.log(`šŸ” DOOOR Trace [${trace.traceId}]`); console.log("=".repeat(80)); console.log(`\nšŸ“ Input:\n${trace.input.substring(0, 200)}${trace.input.length > 200 ? "..." : ""}`); if (trace.output) { console.log(`\nāœ… Output:\n${trace.output.substring(0, 200)}${trace.output.length > 200 ? "..." : ""}`); } console.log(`\nāš™ļø Model: ${trace.model}`); console.log(`ā±ļø Latency: ${trace.latency}ms`); if (trace.tokens) { console.log(`šŸ”¢ Tokens: ${trace.tokens.total} (prompt: ${trace.tokens.prompt}, completion: ${trace.tokens.completion})`); } if (trace.cost) { console.log(`šŸ’° Cost: $${trace.cost.toFixed(4)}`); } if (trace.guards && trace.guards.length > 0) { console.log("\nšŸ›”ļø Guards:"); trace.guards.forEach((guard) => { const icon = guard.result.passed ? "āœ…" : "🚫"; console.log(` ${icon} ${guard.name} (${guard.latency}ms)`); if (!guard.result.passed) { console.log(` Reason: ${guard.result.reason}`); console.log(` Severity: ${guard.result.severity}`); } }); } if (trace.evals && trace.evals.length > 0) { console.log("\nšŸ“Š Evals:"); trace.evals.forEach((evalResult) => { const icon = evalResult.result.passed ? "āœ…" : "āš ļø"; console.log(` ${icon} ${evalResult.name}: ${evalResult.result.score.toFixed(2)} (${evalResult.latency}ms)`); if (evalResult.result.details) { console.log(` ${evalResult.result.details}`); } }); } console.log("\n" + "=".repeat(80) + "\n"); } logError(error, context) { console.error("\n🚨 DOOOR Error:"); console.error(` ${error.name}: ${error.message}`); if (context) { console.error(" Context:", JSON.stringify(context, null, 2)); } console.error(error.stack); } async updateTrace(traceId, updates) { // ConsoleBackend doesn't need to update traces (already logged) if (this.verbose && updates.evals) { console.log(`\nšŸ“Š Updated Evals for Trace [${traceId}]:`); updates.evals.forEach((evalResult) => { const icon = evalResult.result.passed ? "āœ…" : "āš ļø"; console.log(` ${icon} ${evalResult.name}: ${evalResult.result.score.toFixed(2)} (${evalResult.latency}ms)`); }); } } logMetric(name, value, tags) { if (!this.verbose) return; const tagsStr = tags ? ` [${Object.entries(tags).map(([k, v]) => `${k}:${v}`).join(", ")}]` : ""; console.log(`šŸ“ˆ ${name}: ${value}${tagsStr}`); } } exports.ConsoleBackend = ConsoleBackend; /** * CortexDB-based observability backend for production * Saves traces, evals, and guard blocks to CortexDB */ class CortexDBBackend { constructor(project) { this.project = project; } async logTrace(trace) { try { console.log("[CortexDBBackend] logTrace called with:", { traceId: trace.traceId, project: this.project, model: trace.model, hasInput: !!trace.input, hasOutput: !!trace.output, }); const client = (0, cortexdb_client_1.getCortexDBClient)(); console.log("[CortexDBBackend] Got CortexDB client, config:", { baseUrl: client.getConfig().baseUrl, database: client.getConfig().database, hasApiKey: !!client.getConfig().apiKey, }); // Save trace const promptTokens = trace.tokens?.prompt || 0; const completionTokens = trace.tokens?.completion || 0; const totalTokens = trace.tokens?.total || promptTokens + completionTokens; const metadataPayload = { ...(trace.metadata || {}), timestamp: trace.timestamp?.toISOString() || new Date().toISOString(), }; if (trace.guards && trace.guards.length > 0) { metadataPayload.guards = trace.guards.map((guard) => ({ name: guard.name, passed: guard.result.passed, reason: guard.result.reason, severity: guard.result.severity, latency_ms: guard.latency, metadata: guard.result.metadata || {}, })); } if (trace.toolCalls && trace.toolCalls.length > 0) { metadataPayload.toolCalls = trace.toolCalls; } const traceData = { trace_id: trace.traceId, session_id: trace.sessionId || null, project: this.project || "default", model: trace.model, input: trace.input, output: trace.output, latency_ms: trace.latency, input_tokens: promptTokens, output_tokens: completionTokens, total_tokens: totalTokens, tokens_input: promptTokens, tokens_output: completionTokens, tokens_total: totalTokens, timestamp: Date.now(), metadata: { ...metadataPayload, trace_sequence: trace.traceSequence, trace_type: trace.traceType, }, }; // Only send cost_usd if explicitly provided (non-zero) // Otherwise, let the gateway calculate it based on tokens and pricing table console.log("[CortexDBBackend] Cost decision:", { traceCost: trace.cost, hasCost: trace.cost !== undefined, isPositive: trace.cost && trace.cost > 0, willSendCostUsd: !!(trace.cost && trace.cost > 0), }); if (trace.cost && trace.cost > 0) { traceData.cost_usd = trace.cost; console.log("[CortexDBBackend] Including cost_usd in payload:", trace.cost); } else { console.log("[CortexDBBackend] NOT sending cost_usd - letting gateway calculate from tokens"); } console.log("[CortexDBBackend] Final trace payload:", traceData); await client.saveTrace(traceData); console.log("[CortexDBBackend] āœ… Trace saved successfully!"); // Save guards results as guard_blocks if (trace.guards) { const config = client.getConfig(); for (const guard of trace.guards) { if (!guard.result.passed) { const guardPayload = { trace_id: trace.traceId, project: this.project || "default", session_id: trace.sessionId, guard_name: guard.name, guard_type: guard.result.metadata?.type, timestamp: Date.now(), input: trace.input, reason: guard.result.reason || "Unknown", score: guard.result.confidence, threshold: guard.result?.threshold, metadata: { ...(guard.result.metadata || {}), severity: guard.result.severity || "medium", blocked: true, trace_id: trace.traceId, }, }; await fetch(`${config.baseUrl}/databases/${config.database}/guard_blocks`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${config.apiKey}`, }, body: JSON.stringify(guardPayload), }); } } } } catch (error) { console.error("Failed to save trace to CortexDB:", error); // Don't throw - observability should never break the main flow } } async logError(error, context) { // For now, just log to console // TODO: Add error tracking endpoint to CortexDB console.error("🚨 DOOOR Error:", error.message, context); } async updateTrace(traceId, updates) { try { console.log("[CortexDBBackend] updateTrace called with:", { traceId, hasEvals: !!updates.evals }); // Save evals if provided if (updates.evals) { const client = (0, cortexdb_client_1.getCortexDBClient)(); for (const evalResult of updates.evals) { await client.saveEval({ trace_id: traceId, eval_name: evalResult.name, score: evalResult.result.score, passed: evalResult.result.passed, latency_ms: evalResult.latency, details: evalResult.result.details || null, metadata: evalResult.result.metadata || {}, }); } console.log("[CortexDBBackend] āœ… Evals saved successfully for trace:", traceId); } if (updates.toolCalls) { const client = (0, cortexdb_client_1.getCortexDBClient)(); await client.updateToolCalls(traceId, updates.toolCalls); console.log("[CortexDBBackend] āœ… Tool calls updated for trace:", traceId); } } catch (error) { console.error("Failed to update trace in CortexDB:", error); } } async logMetric(name, value, tags) { // For now, just log to console // TODO: Add metrics endpoint to CortexDB console.log(`šŸ“ˆ ${name}: ${value}`, tags); } } exports.CortexDBBackend = CortexDBBackend; /** * Observability collector that manages trace data */ class ObservabilityCollector { constructor(backend, enabled = true) { this.backend = backend ?? new ConsoleBackend(); this.enabled = enabled; } async logTrace(trace) { if (!this.enabled) return; await this.backend.logTrace(trace); } async updateTrace(traceId, updates) { if (!this.enabled) return; await this.backend.updateTrace(traceId, updates); } logError(error, context) { if (!this.enabled) return; this.backend.logError(error, context); } logMetric(name, value, tags) { if (!this.enabled) return; this.backend.logMetric(name, value, tags); } setEnabled(enabled) { this.enabled = enabled; } isEnabled() { return this.enabled; } } exports.ObservabilityCollector = ObservabilityCollector; /** * Convenience helper for manual integrations (e.g., HTTP fetch calls). * * Example: * ```ts * configureCortexDBFromConnectionString(process.env.CORTEXDB!); * await logTrace( * { * traceId: crypto.randomUUID(), * input: prompt, * output: response, * model: "claude-3-sonnet", * latency, * tokens: { prompt: 100, completion: 200, total: 300 }, * timestamp: new Date(), * }, * { project: "vaultly-api" } * ); * ``` */ async function logTrace(trace, options) { // Generate traceId and sessionId if not provided const uuid = await import("uuid"); const traceWithId = { ...trace, traceId: trace.traceId || uuid.v4(), sessionId: trace.sessionId || uuid.v4(), }; const collector = options?.collector || new ObservabilityCollector(resolveBackend(options), options?.enabled ?? true); try { await collector.logTrace(traceWithId); } catch (error) { console.warn("[logTrace] Failed to send trace to configured backend. Falling back to console.", error); if (!options?.collector) { const fallback = new ObservabilityCollector(new ConsoleBackend(true)); await fallback.logTrace(traceWithId); } } } function resolveBackend(options) { if (options?.backend) { return options.backend; } // Prefer CortexDB backend if a project is provided if (options?.project) { return new CortexDBBackend(options.project); } return new ConsoleBackend(true); }