UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

210 lines 7.49 kB
/** * Langfuse Exporter * Exports spans to Langfuse observability platform */ import { SpanType } from "../../types/index.js"; import { SpanSerializer } from "../utils/spanSerializer.js"; import { BaseExporter } from "./baseExporter.js"; /** * Langfuse exporter for LLM observability * Supports traces, generations, spans, and scores */ export class LangfuseExporter extends BaseExporter { publicKey; secretKey; baseUrl; release; redactIO; constructor(config) { super("langfuse", config); this.publicKey = config.publicKey; this.secretKey = config.secretKey; this.baseUrl = config.baseUrl ?? "https://cloud.langfuse.com"; this.release = config.release; this.redactIO = config.redactIO ?? false; } async initialize() { if (this.initialized) { return; } // Verify credentials with a simple check if (!this.publicKey || !this.secretKey) { throw new Error("Langfuse publicKey and secretKey are required"); } this.initialized = true; this.startFlushInterval(this.config.flushIntervalMs ?? 5000); } async exportSpan(span) { const startTime = Date.now(); try { // Create trace if this is a root span if (!span.parentSpanId) { await this.createTrace(span); } // Create span/generation based on type if (span.type === SpanType.MODEL_GENERATION) { await this.createGeneration(span); } else { await this.createSpan(span); } return this.createSuccessResult(1, Date.now() - startTime); } catch (error) { return this.createFailureResult([span.spanId], error instanceof Error ? error.message : String(error), Date.now() - startTime); } } async exportBatch(spans) { const startTime = Date.now(); const results = await Promise.allSettled(spans.map((s) => this.exportSpan(s))); const successful = results.filter((r) => r.status === "fulfilled" && r.value.success).length; const failed = spans.length - successful; return { success: failed === 0, exportedCount: successful, failedCount: failed, errors: results .filter((r) => r.status === "rejected" || (r.status === "fulfilled" && !r.value.success)) .map((r, i) => ({ spanId: spans[i].spanId, error: r.status === "rejected" ? String(r.reason) : "Export failed", retryable: true, })), durationMs: Date.now() - startTime, }; } async flush() { if (this.buffer.length > 0) { const spans = [...this.buffer]; this.buffer = []; await this.exportBatch(spans); } } async shutdown() { await this.flush(); this.stopFlushInterval(); this.initialized = false; } async healthCheck() { try { await this.withRetry(() => this.ping(), "health check"); return this.createHealthStatus(true); } catch { return this.createHealthStatus(false, ["Health check failed"]); } } /** * Verify connectivity to Langfuse API */ async ping() { const credentials = Buffer.from(`${this.publicKey}:${this.secretKey}`).toString("base64"); const response = await fetch(`${this.baseUrl}/api/public/health`, { method: "GET", headers: { Authorization: `Basic ${credentials}`, }, }); if (!response.ok && response.status !== 404) { // 404 is acceptable as health endpoint may not exist throw new Error(`Langfuse API unreachable: ${response.status}`); } } /** * Create a Langfuse trace */ async createTrace(span) { const body = { id: span.traceId, name: span.name, userId: span.attributes["user.id"], sessionId: span.attributes["session.id"], // Only pick safe, non-PII attributes for metadata — intentionally excludes // input, output, error.stack, and other user content to match Braintrust exporter metadata: filterSafeMetadata(span.attributes), release: this.release, tags: this.extractTags(span), }; await this.apiCall("/api/public/traces", body); } /** * Create a Langfuse span */ async createSpan(span) { const langfuseSpan = SpanSerializer.toLangfuseFormat(span); const body = { ...langfuseSpan, traceId: span.traceId, }; if (this.redactIO) { delete body["input"]; delete body["output"]; } await this.apiCall("/api/public/spans", body); } /** * Create a Langfuse generation (for LLM calls) */ async createGeneration(span) { const langfuseSpan = SpanSerializer.toLangfuseFormat(span); const body = { traceId: span.traceId, id: langfuseSpan.id, parentObservationId: langfuseSpan.parentObservationId, name: langfuseSpan.name, startTime: langfuseSpan.startTime, endTime: langfuseSpan.endTime, model: span.attributes["ai.model"], modelParameters: { temperature: span.attributes["ai.temperature"], maxTokens: span.attributes["ai.max_tokens"], topP: span.attributes["ai.top_p"], }, input: this.redactIO ? undefined : langfuseSpan.input, output: this.redactIO ? undefined : langfuseSpan.output, usage: langfuseSpan.usage, metadata: langfuseSpan.metadata, level: langfuseSpan.level, statusMessage: langfuseSpan.statusMessage, }; await this.apiCall("/api/public/generations", body); } /** * Make API call to Langfuse */ async apiCall(path, body) { const credentials = Buffer.from(`${this.publicKey}:${this.secretKey}`).toString("base64"); const response = await fetch(`${this.baseUrl}${path}`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Basic ${credentials}`, }, body: JSON.stringify(body), }); if (!response.ok) { const text = await response.text(); throw new Error(`Langfuse API error: ${response.status} - ${text}`); } } /** * Extract tags from span attributes */ extractTags(span) { const tags = []; if (span.attributes["ai.provider"]) { tags.push(`provider:${span.attributes["ai.provider"]}`); } if (span.attributes["ai.model"]) { tags.push(`model:${span.attributes["ai.model"]}`); } if (span.attributes["deployment.environment"]) { tags.push(`env:${span.attributes["deployment.environment"]}`); } return tags; } } // Safe metadata filtering imported from shared module to avoid duplication import { filterSafeMetadata } from "../utils/safeMetadata.js"; //# sourceMappingURL=langfuseExporter.js.map