UNPKG

@posthog/ai

Version:
1,186 lines (1,116 loc) 39.1 kB
import 'buffer'; import * as uuid from 'uuid'; const getModelParams = params => { if (!params) { return {}; } const modelParams = {}; const paramKeys = ['temperature', 'max_tokens', 'max_completion_tokens', 'top_p', 'frequency_penalty', 'presence_penalty', 'n', 'stop', 'stream', 'streaming']; for (const key of paramKeys) { if (key in params && params[key] !== undefined) { modelParams[key] = params[key]; } } return modelParams; }; const withPrivacyMode = (client, privacyMode, input) => { return client.privacy_mode || privacyMode ? null : input; }; function getDefaultExportFromCjs (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } var decamelize; var hasRequiredDecamelize; function requireDecamelize () { if (hasRequiredDecamelize) return decamelize; hasRequiredDecamelize = 1; decamelize = function (str, sep) { if (typeof str !== 'string') { throw new TypeError('Expected a string'); } sep = typeof sep === 'undefined' ? '_' : sep; return str .replace(/([a-z\d])([A-Z])/g, '$1' + sep + '$2') .replace(/([A-Z]+)([A-Z][a-z\d]+)/g, '$1' + sep + '$2') .toLowerCase(); }; return decamelize; } var decamelizeExports = requireDecamelize(); var snakeCase = /*@__PURE__*/getDefaultExportFromCjs(decamelizeExports); var camelcase = {exports: {}}; var hasRequiredCamelcase; function requireCamelcase () { if (hasRequiredCamelcase) return camelcase.exports; hasRequiredCamelcase = 1; const UPPERCASE = /[\p{Lu}]/u; const LOWERCASE = /[\p{Ll}]/u; const LEADING_CAPITAL = /^[\p{Lu}](?![\p{Lu}])/gu; const IDENTIFIER = /([\p{Alpha}\p{N}_]|$)/u; const SEPARATORS = /[_.\- ]+/; const LEADING_SEPARATORS = new RegExp('^' + SEPARATORS.source); const SEPARATORS_AND_IDENTIFIER = new RegExp(SEPARATORS.source + IDENTIFIER.source, 'gu'); const NUMBERS_AND_IDENTIFIER = new RegExp('\\d+' + IDENTIFIER.source, 'gu'); const preserveCamelCase = (string, toLowerCase, toUpperCase) => { let isLastCharLower = false; let isLastCharUpper = false; let isLastLastCharUpper = false; for (let i = 0; i < string.length; i++) { const character = string[i]; if (isLastCharLower && UPPERCASE.test(character)) { string = string.slice(0, i) + '-' + string.slice(i); isLastCharLower = false; isLastLastCharUpper = isLastCharUpper; isLastCharUpper = true; i++; } else if (isLastCharUpper && isLastLastCharUpper && LOWERCASE.test(character)) { string = string.slice(0, i - 1) + '-' + string.slice(i - 1); isLastLastCharUpper = isLastCharUpper; isLastCharUpper = false; isLastCharLower = true; } else { isLastCharLower = toLowerCase(character) === character && toUpperCase(character) !== character; isLastLastCharUpper = isLastCharUpper; isLastCharUpper = toUpperCase(character) === character && toLowerCase(character) !== character; } } return string; }; const preserveConsecutiveUppercase = (input, toLowerCase) => { LEADING_CAPITAL.lastIndex = 0; return input.replace(LEADING_CAPITAL, m1 => toLowerCase(m1)); }; const postProcess = (input, toUpperCase) => { SEPARATORS_AND_IDENTIFIER.lastIndex = 0; NUMBERS_AND_IDENTIFIER.lastIndex = 0; return input.replace(SEPARATORS_AND_IDENTIFIER, (_, identifier) => toUpperCase(identifier)) .replace(NUMBERS_AND_IDENTIFIER, m => toUpperCase(m)); }; const camelCase = (input, options) => { if (!(typeof input === 'string' || Array.isArray(input))) { throw new TypeError('Expected the input to be `string | string[]`'); } options = { pascalCase: false, preserveConsecutiveUppercase: false, ...options }; if (Array.isArray(input)) { input = input.map(x => x.trim()) .filter(x => x.length) .join('-'); } else { input = input.trim(); } if (input.length === 0) { return ''; } const toLowerCase = options.locale === false ? string => string.toLowerCase() : string => string.toLocaleLowerCase(options.locale); const toUpperCase = options.locale === false ? string => string.toUpperCase() : string => string.toLocaleUpperCase(options.locale); if (input.length === 1) { return options.pascalCase ? toUpperCase(input) : toLowerCase(input); } const hasUpperCase = input !== toLowerCase(input); if (hasUpperCase) { input = preserveCamelCase(input, toLowerCase, toUpperCase); } input = input.replace(LEADING_SEPARATORS, ''); if (options.preserveConsecutiveUppercase) { input = preserveConsecutiveUppercase(input, toLowerCase); } else { input = toLowerCase(input); } if (options.pascalCase) { input = toUpperCase(input.charAt(0)) + input.slice(1); } return postProcess(input, toUpperCase); }; camelcase.exports = camelCase; // TODO: Remove this for the next major release camelcase.exports.default = camelCase; return camelcase.exports; } requireCamelcase(); function keyToJson(key, map) { return map?.[key] || snakeCase(key); } function mapKeys(fields, mapper, map) { const mapped = {}; for (const key in fields) { if (Object.hasOwn(fields, key)) { mapped[mapper(key, map)] = fields[key]; } } return mapped; } function shallowCopy(obj) { return Array.isArray(obj) ? [...obj] : { ...obj }; } function replaceSecrets(root, secretsMap) { const result = shallowCopy(root); for (const [path, secretId] of Object.entries(secretsMap)) { const [last, ...partsReverse] = path.split(".").reverse(); // eslint-disable-next-line @typescript-eslint/no-explicit-any let current = result; for (const part of partsReverse.reverse()) { if (current[part] === undefined) { break; } current[part] = shallowCopy(current[part]); current = current[part]; } if (current[last] !== undefined) { current[last] = { lc: 1, type: "secret", id: [secretId], }; } } return result; } /** * Get a unique name for the module, rather than parent class implementations. * Should not be subclassed, subclass lc_name above instead. */ function get_lc_unique_name( // eslint-disable-next-line @typescript-eslint/no-use-before-define serializableClass) { // "super" here would refer to the parent class of Serializable, // when we want the parent class of the module actually calling this method. const parentClass = Object.getPrototypeOf(serializableClass); const lcNameIsSubclassed = typeof serializableClass.lc_name === "function" && (typeof parentClass.lc_name !== "function" || serializableClass.lc_name() !== parentClass.lc_name()); if (lcNameIsSubclassed) { return serializableClass.lc_name(); } else { return serializableClass.name; } } class Serializable { /** * The name of the serializable. Override to provide an alias or * to preserve the serialized module name in minified environments. * * Implemented as a static method to support loading logic. */ static lc_name() { return this.name; } /** * The final serialized identifier for the module. */ get lc_id() { return [ ...this.lc_namespace, get_lc_unique_name(this.constructor), ]; } /** * A map of secrets, which will be omitted from serialization. * Keys are paths to the secret in constructor args, e.g. "foo.bar.baz". * Values are the secret ids, which will be used when deserializing. */ get lc_secrets() { return undefined; } /** * A map of additional attributes to merge with constructor args. * Keys are the attribute names, e.g. "foo". * Values are the attribute values, which will be serialized. * These attributes need to be accepted by the constructor as arguments. */ get lc_attributes() { return undefined; } /** * A map of aliases for constructor args. * Keys are the attribute names, e.g. "foo". * Values are the alias that will replace the key in serialization. * This is used to eg. make argument names match Python. */ get lc_aliases() { return undefined; } /** * A manual list of keys that should be serialized. * If not overridden, all fields passed into the constructor will be serialized. */ get lc_serializable_keys() { return undefined; } constructor(kwargs, ..._args) { Object.defineProperty(this, "lc_serializable", { enumerable: true, configurable: true, writable: true, value: false }); Object.defineProperty(this, "lc_kwargs", { enumerable: true, configurable: true, writable: true, value: void 0 }); if (this.lc_serializable_keys !== undefined) { this.lc_kwargs = Object.fromEntries(Object.entries(kwargs || {}).filter(([key]) => this.lc_serializable_keys?.includes(key))); } else { this.lc_kwargs = kwargs ?? {}; } } toJSON() { if (!this.lc_serializable) { return this.toJSONNotImplemented(); } if ( // eslint-disable-next-line no-instanceof/no-instanceof this.lc_kwargs instanceof Serializable || typeof this.lc_kwargs !== "object" || Array.isArray(this.lc_kwargs)) { // We do not support serialization of classes with arg not a POJO // I'm aware the check above isn't as strict as it could be return this.toJSONNotImplemented(); } const aliases = {}; const secrets = {}; const kwargs = Object.keys(this.lc_kwargs).reduce((acc, key) => { acc[key] = key in this ? this[key] : this.lc_kwargs[key]; return acc; }, {}); // get secrets, attributes and aliases from all superclasses for ( // eslint-disable-next-line @typescript-eslint/no-this-alias let current = Object.getPrototypeOf(this); current; current = Object.getPrototypeOf(current)) { Object.assign(aliases, Reflect.get(current, "lc_aliases", this)); Object.assign(secrets, Reflect.get(current, "lc_secrets", this)); Object.assign(kwargs, Reflect.get(current, "lc_attributes", this)); } // include all secrets used, even if not in kwargs, // will be replaced with sentinel value in replaceSecrets Object.keys(secrets).forEach((keyPath) => { // eslint-disable-next-line @typescript-eslint/no-this-alias, @typescript-eslint/no-explicit-any let read = this; // eslint-disable-next-line @typescript-eslint/no-explicit-any let write = kwargs; const [last, ...partsReverse] = keyPath.split(".").reverse(); for (const key of partsReverse.reverse()) { if (!(key in read) || read[key] === undefined) return; if (!(key in write) || write[key] === undefined) { if (typeof read[key] === "object" && read[key] != null) { write[key] = {}; } else if (Array.isArray(read[key])) { write[key] = []; } } read = read[key]; write = write[key]; } if (last in read && read[last] !== undefined) { write[last] = write[last] || read[last]; } }); return { lc: 1, type: "constructor", id: this.lc_id, kwargs: mapKeys(Object.keys(secrets).length ? replaceSecrets(kwargs, secrets) : kwargs, keyToJson, aliases), }; } toJSONNotImplemented() { return { lc: 1, type: "not_implemented", id: this.lc_id, }; } } // Supabase Edge Function provides a `Deno` global object // without `version` property const isDeno = () => typeof Deno !== "undefined"; function getEnvironmentVariable(name) { // Certain Deno setups will throw an error if you try to access environment variables // https://github.com/langchain-ai/langchainjs/issues/1412 try { if (typeof process !== "undefined") { // eslint-disable-next-line no-process-env return process.env?.[name]; } else if (isDeno()) { return Deno?.env.get(name); } else { return undefined; } } catch (e) { return undefined; } } /** * Abstract class that provides a set of optional methods that can be * overridden in derived classes to handle various events during the * execution of a LangChain application. */ class BaseCallbackHandlerMethodsClass { } /** * Abstract base class for creating callback handlers in the LangChain * framework. It provides a set of optional methods that can be overridden * in derived classes to handle various events during the execution of a * LangChain application. */ class BaseCallbackHandler extends BaseCallbackHandlerMethodsClass { get lc_namespace() { return ["langchain_core", "callbacks", this.name]; } get lc_secrets() { return undefined; } get lc_attributes() { return undefined; } get lc_aliases() { return undefined; } get lc_serializable_keys() { return undefined; } /** * The name of the serializable. Override to provide an alias or * to preserve the serialized module name in minified environments. * * Implemented as a static method to support loading logic. */ static lc_name() { return this.name; } /** * The final serialized identifier for the module. */ get lc_id() { return [ ...this.lc_namespace, get_lc_unique_name(this.constructor), ]; } constructor(input) { super(); Object.defineProperty(this, "lc_serializable", { enumerable: true, configurable: true, writable: true, value: false }); Object.defineProperty(this, "lc_kwargs", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "ignoreLLM", { enumerable: true, configurable: true, writable: true, value: false }); Object.defineProperty(this, "ignoreChain", { enumerable: true, configurable: true, writable: true, value: false }); Object.defineProperty(this, "ignoreAgent", { enumerable: true, configurable: true, writable: true, value: false }); Object.defineProperty(this, "ignoreRetriever", { enumerable: true, configurable: true, writable: true, value: false }); Object.defineProperty(this, "ignoreCustomEvent", { enumerable: true, configurable: true, writable: true, value: false }); Object.defineProperty(this, "raiseError", { enumerable: true, configurable: true, writable: true, value: false }); Object.defineProperty(this, "awaitHandlers", { enumerable: true, configurable: true, writable: true, value: getEnvironmentVariable("LANGCHAIN_CALLBACKS_BACKGROUND") === "false" }); this.lc_kwargs = input || {}; if (input) { this.ignoreLLM = input.ignoreLLM ?? this.ignoreLLM; this.ignoreChain = input.ignoreChain ?? this.ignoreChain; this.ignoreAgent = input.ignoreAgent ?? this.ignoreAgent; this.ignoreRetriever = input.ignoreRetriever ?? this.ignoreRetriever; this.ignoreCustomEvent = input.ignoreCustomEvent ?? this.ignoreCustomEvent; this.raiseError = input.raiseError ?? this.raiseError; this.awaitHandlers = this.raiseError || (input._awaitHandler ?? this.awaitHandlers); } } copy() { return new this.constructor(this); } toJSON() { return Serializable.prototype.toJSON.call(this); } toJSONNotImplemented() { return Serializable.prototype.toJSONNotImplemented.call(this); } static fromMethods(methods) { class Handler extends BaseCallbackHandler { constructor() { super(); Object.defineProperty(this, "name", { enumerable: true, configurable: true, writable: true, value: uuid.v4() }); Object.assign(this, methods); } } return new Handler(); } } // Type guards for safer type checking const isString = value => { return typeof value === 'string'; }; const isObject = value => { return value !== null && typeof value === 'object' && !Array.isArray(value); }; const REDACTED_IMAGE_PLACEHOLDER = '[base64 image redacted]'; // ============================================ // Base64 Detection Helpers // ============================================ const isBase64DataUrl = str => { return /^data:([^;]+);base64,/.test(str); }; const isValidUrl = str => { try { new URL(str); return true; } catch { // Not an absolute URL, check if it's a relative URL or path return str.startsWith('/') || str.startsWith('./') || str.startsWith('../'); } }; const isRawBase64 = str => { // Skip if it's a valid URL or path if (isValidUrl(str)) { return false; } // Check if it's a valid base64 string // Base64 images are typically at least a few hundred chars, but we'll be conservative return str.length > 20 && /^[A-Za-z0-9+/]+=*$/.test(str); }; function redactBase64DataUrl(str) { if (!isString(str)) return str; // Check for data URL format if (isBase64DataUrl(str)) { return REDACTED_IMAGE_PLACEHOLDER; } // Check for raw base64 (Vercel sends raw base64 for inline images) if (isRawBase64(str)) { return REDACTED_IMAGE_PLACEHOLDER; } return str; } // ============================================ // Common Message Processing // ============================================ const processMessages = (messages, transformContent) => { if (!messages) return messages; const processContent = content => { if (typeof content === 'string') return content; if (!content) return content; if (Array.isArray(content)) { return content.map(transformContent); } // Handle single object content return transformContent(content); }; const processMessage = msg => { if (!isObject(msg) || !('content' in msg)) return msg; return { ...msg, content: processContent(msg.content) }; }; // Handle both arrays and single messages if (Array.isArray(messages)) { return messages.map(processMessage); } return processMessage(messages); }; const sanitizeLangChainImage = item => { if (!isObject(item)) return item; // OpenAI style if (item.type === 'image_url' && 'image_url' in item && isObject(item.image_url) && 'url' in item.image_url) { return { ...item, image_url: { ...item.image_url, url: redactBase64DataUrl(item.image_url.url) } }; } // Direct image with data field if (item.type === 'image' && 'data' in item) { return { ...item, data: redactBase64DataUrl(item.data) }; } // Anthropic style if (item.type === 'image' && 'source' in item && isObject(item.source) && 'data' in item.source) { return { ...item, source: { ...item.source, data: redactBase64DataUrl(item.source.data) } }; } // Google style if (item.type === 'media' && 'data' in item) { return { ...item, data: redactBase64DataUrl(item.data) }; } return item; }; const sanitizeLangChain = data => { return processMessages(data, sanitizeLangChainImage); }; /** A run may either be a Span or a Generation */ /** Storage for run metadata */ class LangChainCallbackHandler extends BaseCallbackHandler { name = 'PosthogCallbackHandler'; runs = {}; parentTree = {}; constructor(options) { if (!options.client) { throw new Error('PostHog client is required'); } super(); this.client = options.client; this.distinctId = options.distinctId; this.traceId = options.traceId; this.properties = options.properties || {}; this.privacyMode = options.privacyMode || false; this.groups = options.groups || {}; this.debug = options.debug || false; } // ===== CALLBACK METHODS ===== handleChainStart(chain, inputs, runId, parentRunId, tags, metadata, runType, runName) { this._logDebugEvent('on_chain_start', runId, parentRunId, { inputs, tags }); this._setParentOfRun(runId, parentRunId); this._setTraceOrSpanMetadata(chain, inputs, runId, parentRunId, metadata, tags, runName); } handleChainEnd(outputs, runId, parentRunId, tags, // eslint-disable-next-line @typescript-eslint/no-unused-vars kwargs) { this._logDebugEvent('on_chain_end', runId, parentRunId, { outputs, tags }); this._popRunAndCaptureTraceOrSpan(runId, parentRunId, outputs); } handleChainError(error, runId, parentRunId, tags, // eslint-disable-next-line @typescript-eslint/no-unused-vars kwargs) { this._logDebugEvent('on_chain_error', runId, parentRunId, { error, tags }); this._popRunAndCaptureTraceOrSpan(runId, parentRunId, error); } handleChatModelStart(serialized, messages, runId, parentRunId, extraParams, tags, metadata, runName) { this._logDebugEvent('on_chat_model_start', runId, parentRunId, { messages, tags }); this._setParentOfRun(runId, parentRunId); // Flatten the two-dimensional messages and convert each message to a plain object const input = messages.flat().map(m => this._convertMessageToDict(m)); this._setLLMMetadata(serialized, runId, input, metadata, extraParams, runName); } handleLLMStart(serialized, prompts, runId, parentRunId, extraParams, tags, metadata, runName) { this._logDebugEvent('on_llm_start', runId, parentRunId, { prompts, tags }); this._setParentOfRun(runId, parentRunId); this._setLLMMetadata(serialized, runId, prompts, metadata, extraParams, runName); } handleLLMEnd(output, runId, parentRunId, tags, // eslint-disable-next-line @typescript-eslint/no-unused-vars extraParams) { this._logDebugEvent('on_llm_end', runId, parentRunId, { output, tags }); this._popRunAndCaptureGeneration(runId, parentRunId, output); } handleLLMError(err, runId, parentRunId, tags, // eslint-disable-next-line @typescript-eslint/no-unused-vars extraParams) { this._logDebugEvent('on_llm_error', runId, parentRunId, { err, tags }); this._popRunAndCaptureGeneration(runId, parentRunId, err); } handleToolStart(tool, input, runId, parentRunId, tags, metadata, runName) { this._logDebugEvent('on_tool_start', runId, parentRunId, { input, tags }); this._setParentOfRun(runId, parentRunId); this._setTraceOrSpanMetadata(tool, input, runId, parentRunId, metadata, tags, runName); } handleToolEnd(output, runId, parentRunId, tags) { this._logDebugEvent('on_tool_end', runId, parentRunId, { output, tags }); this._popRunAndCaptureTraceOrSpan(runId, parentRunId, output); } handleToolError(err, runId, parentRunId, tags) { this._logDebugEvent('on_tool_error', runId, parentRunId, { err, tags }); this._popRunAndCaptureTraceOrSpan(runId, parentRunId, err); } handleRetrieverStart(retriever, query, runId, parentRunId, tags, metadata, name) { this._logDebugEvent('on_retriever_start', runId, parentRunId, { query, tags }); this._setParentOfRun(runId, parentRunId); this._setTraceOrSpanMetadata(retriever, query, runId, parentRunId, metadata, tags, name); } handleRetrieverEnd(documents, runId, parentRunId, tags) { this._logDebugEvent('on_retriever_end', runId, parentRunId, { documents, tags }); this._popRunAndCaptureTraceOrSpan(runId, parentRunId, documents); } handleRetrieverError(err, runId, parentRunId, tags) { this._logDebugEvent('on_retriever_error', runId, parentRunId, { err, tags }); this._popRunAndCaptureTraceOrSpan(runId, parentRunId, err); } handleAgentAction(action, runId, parentRunId, tags) { this._logDebugEvent('on_agent_action', runId, parentRunId, { action, tags }); this._setParentOfRun(runId, parentRunId); this._setTraceOrSpanMetadata(null, action, runId, parentRunId); } handleAgentEnd(action, runId, parentRunId, tags) { this._logDebugEvent('on_agent_finish', runId, parentRunId, { action, tags }); this._popRunAndCaptureTraceOrSpan(runId, parentRunId, action); } // ===== PRIVATE HELPERS ===== _setParentOfRun(runId, parentRunId) { if (parentRunId) { this.parentTree[runId] = parentRunId; } } _popParentOfRun(runId) { delete this.parentTree[runId]; } _findRootRun(runId) { let id = runId; while (this.parentTree[id]) { id = this.parentTree[id]; } return id; } _setTraceOrSpanMetadata(serialized, input, runId, parentRunId, ...args) { // Use default names if not provided: if this is a top-level run, we mark it as a trace, otherwise as a span. const defaultName = parentRunId ? 'span' : 'trace'; const runName = this._getLangchainRunName(serialized, ...args) || defaultName; this.runs[runId] = { name: runName, input, startTime: Date.now() }; } _setLLMMetadata(serialized, runId, messages, metadata, extraParams, runName) { const runNameFound = this._getLangchainRunName(serialized, { extraParams, runName }) || 'generation'; const generation = { name: runNameFound, input: sanitizeLangChain(messages), startTime: Date.now() }; if (extraParams) { generation.modelParams = getModelParams(extraParams.invocation_params); if (extraParams.invocation_params && extraParams.invocation_params.tools) { generation.tools = extraParams.invocation_params.tools; } } if (metadata) { if (metadata.ls_model_name) { generation.model = metadata.ls_model_name; } if (metadata.ls_provider) { generation.provider = metadata.ls_provider; } } if (serialized && 'kwargs' in serialized && serialized.kwargs.openai_api_base) { generation.baseUrl = serialized.kwargs.openai_api_base; } this.runs[runId] = generation; } _popRunMetadata(runId) { const endTime = Date.now(); const run = this.runs[runId]; if (!run) { console.warn(`No run metadata found for run ${runId}`); return undefined; } run.endTime = endTime; delete this.runs[runId]; return run; } _getTraceId(runId) { return this.traceId ? String(this.traceId) : this._findRootRun(runId); } _getParentRunId(traceId, runId, parentRunId) { // Replace the parent-run if not found in our stored parent tree. if (parentRunId && !this.parentTree[parentRunId]) { return traceId; } return parentRunId; } _popRunAndCaptureTraceOrSpan(runId, parentRunId, outputs) { const traceId = this._getTraceId(runId); this._popParentOfRun(runId); const run = this._popRunMetadata(runId); if (!run) { return; } if ('modelParams' in run) { console.warn(`Run ${runId} is a generation, but attempted to be captured as a trace/span.`); return; } const actualParentRunId = this._getParentRunId(traceId, runId, parentRunId); this._captureTraceOrSpan(traceId, runId, run, outputs, actualParentRunId); } _captureTraceOrSpan(traceId, runId, run, outputs, parentRunId) { const eventName = parentRunId ? '$ai_span' : '$ai_trace'; const latency = run.endTime ? (run.endTime - run.startTime) / 1000 : 0; const eventProperties = { $ai_trace_id: traceId, $ai_input_state: withPrivacyMode(this.client, this.privacyMode, run.input), $ai_latency: latency, $ai_span_name: run.name, $ai_span_id: runId }; if (parentRunId) { eventProperties['$ai_parent_id'] = parentRunId; } Object.assign(eventProperties, this.properties); if (!this.distinctId) { eventProperties['$process_person_profile'] = false; } if (outputs instanceof Error) { eventProperties['$ai_error'] = outputs.toString(); eventProperties['$ai_is_error'] = true; } else if (outputs !== undefined) { eventProperties['$ai_output_state'] = withPrivacyMode(this.client, this.privacyMode, outputs); } this.client.capture({ distinctId: this.distinctId ? this.distinctId.toString() : runId, event: eventName, properties: eventProperties, groups: this.groups }); } _popRunAndCaptureGeneration(runId, parentRunId, response) { const traceId = this._getTraceId(runId); this._popParentOfRun(runId); const run = this._popRunMetadata(runId); if (!run || typeof run !== 'object' || !('modelParams' in run)) { console.warn(`Run ${runId} is not a generation, but attempted to be captured as such.`); return; } const actualParentRunId = this._getParentRunId(traceId, runId, parentRunId); this._captureGeneration(traceId, runId, run, response, actualParentRunId); } _captureGeneration(traceId, runId, run, output, parentRunId) { const latency = run.endTime ? (run.endTime - run.startTime) / 1000 : 0; const eventProperties = { $ai_trace_id: traceId, $ai_span_id: runId, $ai_span_name: run.name, $ai_parent_id: parentRunId, $ai_provider: run.provider, $ai_model: run.model, $ai_model_parameters: run.modelParams, $ai_input: withPrivacyMode(this.client, this.privacyMode, run.input), $ai_http_status: 200, $ai_latency: latency, $ai_base_url: run.baseUrl }; if (run.tools) { eventProperties['$ai_tools'] = run.tools; } if (output instanceof Error) { eventProperties['$ai_http_status'] = output.status || 500; eventProperties['$ai_error'] = output.toString(); eventProperties['$ai_is_error'] = true; } else { // Handle token usage const [inputTokens, outputTokens, additionalTokenData] = this.parseUsage(output); eventProperties['$ai_input_tokens'] = inputTokens; eventProperties['$ai_output_tokens'] = outputTokens; // Add additional token data to properties if (additionalTokenData.cacheReadInputTokens) { eventProperties['$ai_cache_read_tokens'] = additionalTokenData.cacheReadInputTokens; } if (additionalTokenData.reasoningTokens) { eventProperties['$ai_reasoning_tokens'] = additionalTokenData.reasoningTokens; } // Handle generations/completions let completions; if (output.generations && Array.isArray(output.generations)) { const lastGeneration = output.generations[output.generations.length - 1]; if (Array.isArray(lastGeneration) && lastGeneration.length > 0) { // Check if this is a ChatGeneration by looking at the first item const isChatGeneration = 'message' in lastGeneration[0] && lastGeneration[0].message; if (isChatGeneration) { // For ChatGeneration, convert messages to dict format completions = lastGeneration.map(gen => { return this._convertMessageToDict(gen.message); }); } else { // For non-ChatGeneration, extract raw response completions = lastGeneration.map(gen => { return this._extractRawResponse(gen); }); } } } if (completions) { eventProperties['$ai_output_choices'] = withPrivacyMode(this.client, this.privacyMode, completions); } } Object.assign(eventProperties, this.properties); if (!this.distinctId) { eventProperties['$process_person_profile'] = false; } this.client.capture({ distinctId: this.distinctId ? this.distinctId.toString() : traceId, event: '$ai_generation', properties: eventProperties, groups: this.groups }); } _logDebugEvent(eventName, runId, parentRunId, extra) { if (this.debug) { console.log(`Event: ${eventName}, runId: ${runId}, parentRunId: ${parentRunId}, extra:`, extra); } } _getLangchainRunName(serialized, ...args) { if (args && args.length > 0) { for (const arg of args) { if (arg && typeof arg === 'object' && 'name' in arg) { return arg.name; } else if (arg && typeof arg === 'object' && 'runName' in arg) { return arg.runName; } } } if (serialized && serialized.name) { return serialized.name; } if (serialized && serialized.id) { return Array.isArray(serialized.id) ? serialized.id[serialized.id.length - 1] : serialized.id; } return undefined; } _convertLcToolCallsToOai(toolCalls) { return toolCalls.map(toolCall => ({ type: 'function', id: toolCall.id, function: { name: toolCall.name, arguments: JSON.stringify(toolCall.args) } })); } _extractRawResponse(generation) { // Extract the response from the last response of the LLM call // We return the text of the response if not empty if (generation.text != null && generation.text.trim() !== '') { return generation.text.trim(); } else if (generation.message) { // Additional kwargs contains the response in case of tool usage return generation.message.additional_kwargs || generation.message.additionalKwargs || {}; } else { // Not tool usage, some LLM responses can be simply empty return ''; } } _convertMessageToDict(message) { let messageDict = {}; const messageType = message.getType(); switch (messageType) { case 'human': messageDict = { role: 'user', content: message.content }; break; case 'ai': messageDict = { role: 'assistant', content: message.content }; if (message.tool_calls) { messageDict.tool_calls = this._convertLcToolCallsToOai(message.tool_calls); } break; case 'system': messageDict = { role: 'system', content: message.content }; break; case 'tool': messageDict = { role: 'tool', content: message.content }; break; case 'function': messageDict = { role: 'function', content: message.content }; break; default: messageDict = { role: messageType, content: String(message.content) }; break; } if (message.additional_kwargs) { messageDict = { ...messageDict, ...message.additional_kwargs }; } // Sanitize the message content to redact base64 images return sanitizeLangChain(messageDict); } _parseUsageModel(usage) { const conversionList = [['promptTokens', 'input'], ['completionTokens', 'output'], ['input_tokens', 'input'], ['output_tokens', 'output'], ['prompt_token_count', 'input'], ['candidates_token_count', 'output'], ['inputTokenCount', 'input'], ['outputTokenCount', 'output'], ['input_token_count', 'input'], ['generated_token_count', 'output']]; const parsedUsage = conversionList.reduce((acc, [modelKey, typeKey]) => { const value = usage[modelKey]; if (value != null) { const finalCount = Array.isArray(value) ? value.reduce((sum, tokenCount) => sum + tokenCount, 0) : value; acc[typeKey] = finalCount; } return acc; }, { input: 0, output: 0 }); // Extract additional token details like cached tokens and reasoning tokens const additionalTokenData = {}; // Check for cached tokens in various formats if (usage.prompt_tokens_details?.cached_tokens != null) { additionalTokenData.cacheReadInputTokens = usage.prompt_tokens_details.cached_tokens; } else if (usage.input_token_details?.cache_read != null) { additionalTokenData.cacheReadInputTokens = usage.input_token_details.cache_read; } else if (usage.cachedPromptTokens != null) { additionalTokenData.cacheReadInputTokens = usage.cachedPromptTokens; } // Check for reasoning tokens in various formats if (usage.completion_tokens_details?.reasoning_tokens != null) { additionalTokenData.reasoningTokens = usage.completion_tokens_details.reasoning_tokens; } else if (usage.output_token_details?.reasoning != null) { additionalTokenData.reasoningTokens = usage.output_token_details.reasoning; } else if (usage.reasoningTokens != null) { additionalTokenData.reasoningTokens = usage.reasoningTokens; } return [parsedUsage.input, parsedUsage.output, additionalTokenData]; } parseUsage(response) { let llmUsage = [0, 0, {}]; const llmUsageKeys = ['token_usage', 'usage', 'tokenUsage']; if (response.llmOutput != null) { const key = llmUsageKeys.find(k => response.llmOutput?.[k] != null); if (key) { llmUsage = this._parseUsageModel(response.llmOutput[key]); } } // If top-level usage info was not found, try checking the generations. if (llmUsage[0] === 0 && llmUsage[1] === 0 && response.generations) { for (const generation of response.generations) { for (const genChunk of generation) { // Check other paths for usage information if (genChunk.generationInfo?.usage_metadata) { llmUsage = this._parseUsageModel(genChunk.generationInfo.usage_metadata); return llmUsage; } const messageChunk = genChunk.generationInfo ?? {}; const responseMetadata = messageChunk.response_metadata ?? {}; const chunkUsage = responseMetadata['usage'] ?? responseMetadata['amazon-bedrock-invocationMetrics'] ?? messageChunk.usage_metadata; if (chunkUsage) { llmUsage = this._parseUsageModel(chunkUsage); return llmUsage; } } } } return llmUsage; } } export { LangChainCallbackHandler }; //# sourceMappingURL=index.mjs.map