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

314 lines 11.2 kB
/** * Utility class for span creation and serialization * Handles conversion between NeuroLink's span format and platform-specific formats */ import { randomBytes } from "node:crypto"; import { SpanStatus, SpanType, } from "../../types/index.js"; import { getActiveTraceContext } from "../../telemetry/traceContext.js"; /** * Utility class for span creation and serialization */ export class SpanSerializer { /** * Create a new span with generated IDs. * * When `traceId` / `parentSpanId` are omitted, the method automatically * attempts to inherit them from the active OTel context so that Pipeline B * spans land inside the same Langfuse trace as Pipeline A spans (fix A5). */ static createSpan(type, name, attributes = {}, parentSpanId, traceId) { // A5 fix: When no explicit traceId is provided, try to inherit from the // active OTel context so Pipeline B spans are linked to Pipeline A traces. let resolvedTraceId = traceId; let resolvedParentSpanId = parentSpanId; if (!resolvedTraceId) { const otelCtx = getActiveTraceContext(); resolvedTraceId = otelCtx.traceId ?? randomBytes(16).toString("hex"); if (!resolvedParentSpanId && otelCtx.parentSpanId) { resolvedParentSpanId = otelCtx.parentSpanId; } } return { spanId: randomBytes(8).toString("hex"), traceId: resolvedTraceId, parentSpanId: resolvedParentSpanId, type, name, startTime: new Date().toISOString(), status: SpanStatus.UNSET, attributes: attributes, events: [], links: [], }; } /** * End a span with status */ static endSpan(span, status = SpanStatus.OK, statusMessage) { const endTime = new Date(); const startTime = new Date(span.startTime); return { ...span, endTime: endTime.toISOString(), durationMs: endTime.getTime() - startTime.getTime(), status, statusMessage, }; } /** * Add event to span */ static addEvent(span, name, attributes) { const event = { name, timestamp: new Date().toISOString(), attributes, }; return { ...span, events: [...span.events, event], }; } /** * Update span attributes */ static updateAttributes(span, attributes) { return { ...span, attributes: { ...span.attributes, ...attributes, }, }; } /** * Serialize span to JSON for export */ static toJSON(span) { return JSON.stringify(span, null, 2); } /** * Instance method to serialize a span object to JSON string * @param span - The span data to serialize (can be partial span data) * @returns JSON string representation of the span */ serialize(span) { return JSON.stringify(span, null, 2); } /** * Instance method to deserialize a JSON string to span data * @param json - The JSON string to parse * @returns Parsed span data */ deserialize(json) { return JSON.parse(json); } /** * Parse span from JSON */ static fromJSON(json) { return JSON.parse(json); } /** * Serialize span for Langfuse format */ static toLangfuseFormat(span) { return { id: span.spanId, traceId: span.traceId, parentObservationId: span.parentSpanId, name: span.name, startTime: span.startTime, endTime: span.endTime, // Only pick safe, non-PII attributes for metadata — intentionally excludes // error.stack and other internal fields. input/output are exported here for // Langfuse tracing; use LangfuseExporterConfig.redactIO=true to suppress them // in compliance-sensitive deployments. metadata: filterSafeMetadata(span.attributes), level: span.status === SpanStatus.ERROR ? "ERROR" : span.status === SpanStatus.WARNING ? "WARNING" : "DEFAULT", statusMessage: span.statusMessage, input: span.attributes["input"], output: span.attributes["output"], usage: span.attributes["ai.tokens.total"] !== undefined ? { promptTokens: span.attributes["ai.tokens.input"], completionTokens: span.attributes["ai.tokens.output"], totalTokens: span.attributes["ai.tokens.total"], } : undefined, }; } /** * Serialize span for LangSmith format */ static toLangSmithFormat(span) { return { id: span.spanId, trace_id: span.traceId, parent_run_id: span.parentSpanId, name: span.name, run_type: SpanSerializer.mapSpanTypeToLangSmithRunType(span.type), start_time: span.startTime, end_time: span.endTime, extra: { ...span.attributes }, error: span.status === SpanStatus.ERROR ? span.statusMessage : undefined, inputs: span.attributes["input"], outputs: span.attributes["output"], tags: SpanSerializer.extractTags(span.attributes), }; } /** * Serialize span for OpenTelemetry format */ static toOtelFormat(span) { return { traceId: span.traceId, spanId: span.spanId, parentSpanId: span.parentSpanId, name: span.name, kind: 1, // SPAN_KIND_INTERNAL startTimeUnixNano: new Date(span.startTime).getTime() * 1_000_000, endTimeUnixNano: span.endTime ? new Date(span.endTime).getTime() * 1_000_000 : undefined, attributes: Object.entries(span.attributes) .filter(([, value]) => value !== undefined) .map(([key, value]) => ({ key, value: SpanSerializer.toOtelAttributeValue(value), })), status: { code: span.status, message: span.statusMessage, }, events: span.events.map((e) => ({ name: e.name, timeUnixNano: new Date(e.timestamp).getTime() * 1_000_000, attributes: e.attributes ? Object.entries(e.attributes).map(([k, v]) => ({ key: k, value: SpanSerializer.toOtelAttributeValue(v), })) : [], })), }; } /** * Convert value to OTel attribute value format */ static toOtelAttributeValue(value) { if (typeof value === "string") { return { stringValue: value }; } if (typeof value === "number") { return Number.isInteger(value) ? { intValue: value } : { stringValue: String(value) }; } if (typeof value === "boolean") { return { boolValue: value }; } return { stringValue: JSON.stringify(value) }; } /** * Map NeuroLink span type to LangSmith run type */ static mapSpanTypeToLangSmithRunType(type) { const mapping = { [SpanType.AGENT_RUN]: "chain", [SpanType.WORKFLOW_STEP]: "chain", [SpanType.TOOL_CALL]: "tool", [SpanType.MODEL_GENERATION]: "llm", [SpanType.EMBEDDING]: "embedding", [SpanType.RETRIEVAL]: "retriever", [SpanType.MEMORY]: "chain", [SpanType.CONTEXT_COMPACTION]: "chain", [SpanType.RAG]: "retriever", [SpanType.EVALUATION]: "chain", [SpanType.MCP_TRANSPORT]: "tool", [SpanType.MEDIA_GENERATION]: "llm", [SpanType.PPT_GENERATION]: "chain", [SpanType.WORKFLOW]: "chain", [SpanType.TTS]: "chain", [SpanType.STT]: "chain", [SpanType.SERVER_REQUEST]: "chain", [SpanType.CUSTOM]: "chain", }; return mapping[type] || "chain"; } /** * Extract tags from span attributes for LangSmith */ static extractTags(attributes) { const tags = []; if (attributes["ai.provider"]) { tags.push(`provider:${attributes["ai.provider"]}`); } if (attributes["ai.model"]) { tags.push(`model:${attributes["ai.model"]}`); } if (attributes["deployment.environment"]) { tags.push(`env:${attributes["deployment.environment"]}`); } if (attributes["tool.name"]) { tags.push(`tool:${attributes["tool.name"]}`); } return tags; } /** * Create a generation span with AI-specific attributes */ static createGenerationSpan(params) { return SpanSerializer.createSpan(SpanType.MODEL_GENERATION, params.name ?? `gen_ai.${params.provider}.chat`, { "ai.provider": params.provider, "ai.model": params.model, "ai.temperature": params.temperature, "ai.max_tokens": params.maxTokens, input: params.input, "user.id": params.userId, "session.id": params.sessionId, }, params.parentSpanId, params.traceId); } /** * Create a tool call span */ static createToolCallSpan(params) { return SpanSerializer.createSpan(SpanType.TOOL_CALL, `tool.${params.toolName}`, { "tool.name": params.toolName, "tool.server": params.server, input: params.input, }, params.parentSpanId, params.traceId); } /** * Enrich span with token usage */ static enrichWithTokenUsage(span, usage) { return SpanSerializer.updateAttributes(span, { "ai.tokens.input": usage.promptTokens ?? 0, "ai.tokens.output": usage.completionTokens ?? 0, "ai.tokens.total": usage.totalTokens ?? (usage.promptTokens ?? 0) + (usage.completionTokens ?? 0), "ai.tokens.cache_creation": usage.cacheCreationTokens, "ai.tokens.cache_read": usage.cacheReadTokens, "ai.tokens.reasoning": usage.reasoningTokens, }); } /** * Enrich span with cost information */ static enrichWithCost(span, cost) { return SpanSerializer.updateAttributes(span, { "ai.cost.input": cost.inputCost, "ai.cost.output": cost.outputCost, "ai.cost.total": cost.totalCost, "ai.cost.currency": cost.currency ?? "USD", }); } } // Safe metadata filtering imported from shared module to avoid duplication import { filterSafeMetadata } from "./safeMetadata.js"; //# sourceMappingURL=spanSerializer.js.map