UNPKG

@maximai/maxim-js

Version:

Maxim AI JS SDK. Visit https://getmaxim.ai for more info.

535 lines 24.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MaximLangchainTracer = void 0; const base_1 = require("@langchain/core/callbacks/base"); const messages_1 = require("@langchain/core/messages"); const util_1 = require("util"); const uuid_1 = require("uuid"); const containers_1 = require("../../models/containers"); const utils_1 = require("./utils"); /** * LangChain callback handler for automatic observability with the Maxim platform. * * Extends LangChain's BaseCallbackHandler to automatically trace and log * LangChain application executions including LLM calls, chains, tools, and * retrievers. Seamlessly integrates with existing LangChain applications * to provide comprehensive observability without code changes. * * @class MaximLangchainTracer * @extends {BaseCallbackHandler} * @example * import { MaximLangchainTracer } from '@maximai/maxim-js'; * import { ChatOpenAI } from '@langchain/openai'; * * // Basic setup * const maxim = new Maxim({ apiKey: 'your-api-key' }); * const logger = await maxim.logger({ id: 'langchain-app' }); * const tracer = new MaximLangchainTracer(logger); * * // Use with LangChain models * const model = new ChatOpenAI({ * openAIApiKey: process.env.OPENAI_API_KEY, * modelName: "gpt-4o-mini", * callbacks: [tracer], * metadata: { * maxim: { * generationName: "basic-openai-chat", * generationTags: { testType: "basic", model: "openai" } * }, * }, * }); * * const response = await model.invoke("Hello world"); * // Automatically logged to Maxim */ class MaximLangchainTracer extends base_1.BaseCallbackHandler { /** * Creates a new MaximLangchainTracer instance. * * @param logger - The Maxim logger instance to use for tracing * @param input - Optional LangChain callback handler configuration * @example * const maxim = new Maxim({ apiKey: 'your-api-key' }); * const logger = await maxim.logger({ id: 'my-app' }); * const tracer = new MaximLangchainTracer(logger); * * @example * // With custom callback configuration * const tracer = new MaximLangchainTracer(logger, { * ignoreLLM: false, * ignoreChain: false, * ignoreAgent: false * }); */ constructor(logger, input) { super(input); this.logger = logger; this.name = "MaximLangchainTracer"; this.containerManager = new containers_1.ContainerManager(); } /** * Safely serializes any value to string, handling circular references and unsupported types like BigInt */ safeStringify(value) { try { return JSON.stringify(value); } catch (error) { // Fallback to util.inspect for circular references, BigInt, and other unsupported types return (0, util_1.inspect)(value, { depth: 3, maxArrayLength: 100, maxStringLength: 1000, breakLength: Infinity, compact: true, }); } } getMetadataClassFromRecord(metadata) { if (metadata && "maxim" in metadata) { return new containers_1.Metadata(metadata["maxim"]); } return null; } getContainer(runId, parentRunId, metadata, tags) { var _a, _b; let container; if (parentRunId) { container = this.containerManager.getContainer(parentRunId); } else { // This is the first activity in this run - either get existing or create new trace container = this.containerManager.getContainer(runId); if (!container) { const traceId = (0, uuid_1.v4)(); // Process metadata for trace-specific information const maximMetadata = this.getMetadataClassFromRecord(metadata); const traceName = (_a = maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.traceName) !== null && _a !== void 0 ? _a : "Trace"; const sessionId = (_b = maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.sessionId) !== null && _b !== void 0 ? _b : undefined; const traceTags = (0, utils_1.parseLangchainTags)(maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.traceTags, tags); // Add metadata as tags if provided if (metadata) { Object.entries(metadata).forEach(([key, value]) => { if (key !== "maxim") { // Don't add the maxim object itself as a tag traceTags[key.trim()] = typeof value === "string" ? value.trim() : this.safeStringify(value); } }); } container = new containers_1.TraceContainer(this.containerManager, this.logger, traceId, traceName, undefined, false); if (container instanceof containers_1.TraceContainer) { container.create(traceTags, sessionId); } else { container.create(traceTags); } } } return container; } handleChainStart(_chain, inputs, runId, parentRunId, tags, metadata, _runType, runName) { var _a, _b; try { // Process metadata const maximMetadata = this.getMetadataClassFromRecord(metadata); // Get/create container const container = this.getContainer(runId, parentRunId, metadata, tags); if (!container) { console.error("[MaximSDK] Couldn't find a container for chain"); return; } // Check container creation status if (!container.isCreated()) { container.create(); } // Process entity-specific logic const name = (_b = (_a = maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.chainName) !== null && _a !== void 0 ? _a : runName) !== null && _b !== void 0 ? _b : "default-chain"; const chainTags = (0, utils_1.parseLangchainTags)(maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.chainTags, tags); // Add run_id and parent_run_id to tags chainTags["run_id"] = runId; if (parentRunId) { chainTags["parent_run_id"] = parentRunId; } // Add metadata as tags if provided if (metadata) { Object.entries(metadata).forEach(([key, value]) => { if (key !== "maxim") { chainTags[key.trim()] = typeof value === "string" ? value.trim() : this.safeStringify(value); } }); } const spanConfig = { id: runId, name: name, tags: chainTags, }; const span = container.addSpan(spanConfig); span.addMetadata({ inputs }); // Manage container lifecycle new containers_1.SpanContainer(this.containerManager, this.logger, runId, name, parentRunId, container.type === "trace" ? container.id : undefined, true); } catch (e) { console.error("[MaximSDK] Error while processing chain_start", e); } } handleChainEnd(outputs, runId, _parentRunId, tags, _kwargs) { try { // Get/create container (no parentRunId here because we want the runId container to be closed itself) const container = this.getContainer(runId, undefined, undefined, tags); if (!container) { console.error("[MaximSDK] Couldn't find a container for chain"); return; } // Handle tags and outputs container.addTags((0, utils_1.parseLangchainTags)(undefined, tags)); container.addMetadata({ outputs }); // Manage container lifecycle container.end(); } catch (e) { console.error("[MaximSDK] Failed to parse chain-end:", e); } } handleChainError(err, runId, _parentRunId, tags) { try { // Get/create container (no parentRunId here because we want the runId container to be closed itself) const container = this.getContainer(runId, undefined, undefined, tags); if (!container) { console.error("[MaximSDK] Couldn't find a container for chain"); return; } // Handle tags container.addTags((0, utils_1.parseLangchainTags)(undefined, tags)); container.addMetadata({ error: err.message }); // Manage container lifecycle container.end(); } catch (e) { console.error("[MaximSDK] Failed to parse chain-error:", e); } } async handleLLMStart(llm, prompts, runId, parentRunId, extraParams, tags, metadata, name) { var _a, _b; try { // Process metadata const maximMetadata = this.getMetadataClassFromRecord(metadata); // Get/create container const container = this.getContainer(runId, parentRunId, metadata, tags); if (!container) { console.error("[MaximSDK] Couldn't find a container for LLM"); return; } // Check container creation status if (!container.isCreated()) { container.create(); } // Process entity-specific logic const parsedMessages = (0, utils_1.parseLangchainMessages)(prompts); const traceContainer = container.getTraceContainer(); if (traceContainer && traceContainer instanceof containers_1.TraceContainer) { let lastUserInput = ""; const lastUserMessage = parsedMessages.findLast((m) => m.role === "user"); if (lastUserMessage) { if (typeof lastUserMessage.content === "string") { lastUserInput = lastUserMessage.content; } else if (Array.isArray(lastUserMessage.content)) { const textParts = lastUserMessage.content .filter((item) => item.type === "text") .map((item) => item.text || "") .join(" "); if (textParts) { lastUserInput = textParts; } } } // Set trace input if we found user input if (lastUserInput) { traceContainer.setInput(lastUserInput); } } const generationName = (_b = (_a = maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.generationName) !== null && _a !== void 0 ? _a : name) !== null && _b !== void 0 ? _b : "default-generation"; const [model, modelParameters] = (0, utils_1.parseLangchainModelAndParameters)(metadata, extraParams); const generationConfig = { id: runId, name: generationName, provider: (0, utils_1.determineProvider)(llm.id, metadata), model, messages: parsedMessages, modelParameters, tags: (0, utils_1.parseLangchainTags)(maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.generationTags, tags), }; container.addGeneration(generationConfig); // Manage container lifecycle if (!container.parentId) { this.containerManager.setContainer(runId, container); } } catch (e) { console.error("[MaximSDK] Error while processing LLM start", e); } } handleLLMEnd(output, runId, parentRunId, tags) { try { // Get/create container const container = this.getContainer(runId, parentRunId, undefined, tags); if (!container) { console.error("[MaximSDK] Couldn't find a container for generation"); return; } // Process entity-specific logic const result = (0, utils_1.convertLLMResultToCompletionResult)(output); // Handle tags (0, utils_1.addParsedTagsToLogger)(tags, (key, value) => this.logger.generationAddTag(runId, key, value)); this.logger.generationResult(runId, result); if (!container.parentId) { this.containerManager.removeRunIdMapping(runId); } } catch (e) { console.error("[MaximSDK] Error while processing LLM end", e); } } handleLLMError(err, runId, parentRunId, tags) { try { // Get/create container const container = this.getContainer(runId, parentRunId, undefined, tags); if (!container) { console.error("[MaximSDK] Couldn't find a container for generation"); return; } // Handle tags (0, utils_1.addParsedTagsToLogger)(tags, (key, value) => this.logger.generationAddTag(runId, key, value)); // Process entity-specific logic const generationError = (0, utils_1.parseLangchainErrorToMaximError)(err); this.logger.generationError(runId, generationError); if (!container.parentId) { this.containerManager.removeRunIdMapping(runId); } } catch (e) { console.error("[MaximSDK] Error while processing LLM error", e); } } async handleChatModelStart(llm, messages, runId, parentRunId, extraParams, tags, metadata, runName) { var _a, _b; try { // Process metadata const maximMetadata = this.getMetadataClassFromRecord(metadata); // Get/create container const container = this.getContainer(runId, parentRunId, metadata, tags); if (!container) { console.error("[MaximSDK] Couldn't find a container for chat model"); return; } // Check container creation status if (!container.isCreated()) { container.create(); } // Process entity-specific logic const parsedMessages = (0, utils_1.parseLangchainMessages)(messages); const traceContainer = container.getTraceContainer(); if (traceContainer && traceContainer instanceof containers_1.TraceContainer) { let lastUserInput = ""; const lastUserMessage = parsedMessages.findLast((m) => m.role === "user"); if (lastUserMessage) { if (typeof lastUserMessage.content === "string") { lastUserInput = lastUserMessage.content; } else if (Array.isArray(lastUserMessage.content)) { const textParts = lastUserMessage.content .filter((item) => item.type === "text") .map((item) => item.text || "") .join(" "); if (textParts) { lastUserInput = textParts; } } } // Set trace input if we found user input if (lastUserInput) { traceContainer.setInput(lastUserInput); } } const generationName = (_b = (_a = maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.generationName) !== null && _a !== void 0 ? _a : runName) !== null && _b !== void 0 ? _b : "default-generation"; const [model, modelParameters] = (0, utils_1.parseLangchainModelAndParameters)(metadata, extraParams); const generationConfig = { id: runId, name: generationName, provider: (0, utils_1.determineProvider)(llm.id, metadata), model, messages: parsedMessages, modelParameters, tags: (0, utils_1.parseLangchainTags)(maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.generationTags, tags), }; container.addGeneration(generationConfig); // Manage container lifecycle if (!container.parentId) { this.containerManager.setContainer(runId, container); } } catch (e) { console.error("[MaximSDK] Error while processing chat model start", e); } } handleRetrieverStart(_retriever, query, runId, parentRunId, tags, metadata, name) { var _a, _b; try { // Process metadata const maximMetadata = this.getMetadataClassFromRecord(metadata); // Get/create container const container = this.getContainer(runId, parentRunId, metadata, tags); if (!container) { console.error("[MaximSDK] Couldn't find a container for retrieval"); return; } // Check container creation status if (!container.isCreated()) { container.create(); } // Process entity-specific logic const retrievalId = runId; const retrievalName = (_b = (_a = maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.retrievalName) !== null && _a !== void 0 ? _a : name) !== null && _b !== void 0 ? _b : "default-retrieval"; const retrievalConfig = { id: retrievalId, name: retrievalName, tags: (0, utils_1.parseLangchainTags)(maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.retrievalTags, tags), }; const retrieval = container.addRetrieval(retrievalConfig); retrieval.input(query); // Manage container lifecycle if (!container.parentId) { this.containerManager.setContainer(runId, container); } } catch (e) { console.error("[MaximSDK] Error while processing retriever start", e); } } handleRetrieverEnd(documents, runId, parentRunId, tags) { try { // Get/create container const container = this.getContainer(runId, parentRunId, undefined, tags); if (!container) { console.error("[MaximSDK] Couldn't find a container for retrieval"); return; } // Handle tags (0, utils_1.addParsedTagsToLogger)(tags, (key, value) => this.logger.retrievalAddTag(runId, key, value)); // Process entity-specific logic this.logger.retrievalOutput(runId, this.safeStringify(documents)); if (!container.parentId) { this.containerManager.removeRunIdMapping(runId); } } catch (e) { console.error("[MaximSDK] Error while processing retriever end", e); } } handleToolStart(tool, input, runId, parentRunId, tags, metadata, runName) { var _a, _b, _c; try { // Process metadata const maximMetadata = this.getMetadataClassFromRecord(metadata); // Get/create container const container = this.getContainer(runId, parentRunId, metadata, tags); if (!container) { console.error("[MaximSDK] Couldn't find a container for tool"); return; } // Check container creation status if (!container.isCreated()) { container.create(); } // Process entity-specific logic const name = (_c = (_b = (_a = maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.toolCallName) !== null && _a !== void 0 ? _a : runName) !== null && _b !== void 0 ? _b : tool.name) !== null && _c !== void 0 ? _c : "default-tool"; const description = "description" in tool ? tool.description : ""; const toolCallConfig = { id: runId, name, description, args: input, tags: (0, utils_1.parseLangchainTags)(maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.toolCallTags, tags), }; container.addToolCall(toolCallConfig); // Manage container lifecycle if (!container.parentId) { this.containerManager.setContainer(runId, container); } } catch (e) { console.error("[MaximSDK] Failed to parse tool-start:", e); } } handleToolEnd(output, runId, parentRunId, tags) { try { // Get/create container const container = this.getContainer(runId, parentRunId, undefined, tags); if (!container) { console.error("[MaximSDK] Couldn't find a container for tool"); return; } // Handle tags (0, utils_1.addParsedTagsToLogger)(tags, (key, value) => this.logger.toolCallAddTag(runId, key, value)); // Process entity-specific logic - handle different output formats try { (0, messages_1.isToolMessage)(output); // Handle different possible output formats from LangChain if (output.status) { // Status-based output format if (output.status === "success") { this.logger.toolCallResult(runId, typeof output.content === "string" ? output.content : this.safeStringify(output.content)); } else if (output.status === "error") { this.logger.toolCallError(runId, { message: typeof output.content === "string" ? output.content : this.safeStringify(output.content), }); } } else { // Fallback: stringify the entire output this.logger.toolCallResult(runId, typeof output.content === "string" ? output.content : this.safeStringify(output.content)); } } catch (err) { // Fallback for any other type this.logger.toolCallResult(runId, typeof output === "function" || typeof output === "object" || typeof output === "symbol" || typeof output === "undefined" || output === null ? this.safeStringify(output) : String(output)); } if (!container.parentId) { this.containerManager.removeRunIdMapping(runId); } } catch (e) { console.error("[MaximSDK] Failed to parse tool-end:", e); } } handleToolError(error, runId, parentRunId, tags) { try { // Get/create container const container = this.getContainer(runId, parentRunId, undefined, tags); if (!container) { console.error("[MaximSDK] Couldn't find a container for tool"); return; } // Handle tags (0, utils_1.addParsedTagsToLogger)(tags, (key, value) => this.logger.toolCallAddTag(runId, key, value)); // Process entity-specific logic const toolCallError = (0, utils_1.parseLangchainErrorToMaximError)(error); this.logger.toolCallError(runId, toolCallError); if (!container.parentId) { this.containerManager.removeRunIdMapping(runId); } } catch (e) { console.error("[MaximSDK] Failed to parse tool-end:", e); } } } exports.MaximLangchainTracer = MaximLangchainTracer; //# sourceMappingURL=tracer.js.map