@posthog/ai
Version:
PostHog Node.js AI integrations
1,186 lines (1,116 loc) • 39.1 kB
JavaScript
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