run_trees.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  1. import * as uuid from "uuid";
  2. import { getEnvironmentVariable, getLangSmithEnvironmentVariable, getRuntimeEnvironment, } from "./utils/env.js";
  3. import { Client } from "./client.js";
  4. import { isTracingEnabled } from "./env.js";
  5. import { warnOnce } from "./utils/warn.js";
  6. import { _LC_CONTEXT_VARIABLES_KEY } from "./singletons/constants.js";
  7. function stripNonAlphanumeric(input) {
  8. return input.replace(/[-:.]/g, "");
  9. }
  10. export function convertToDottedOrderFormat(epoch, runId, executionOrder = 1) {
  11. // Date only has millisecond precision, so we use the microseconds to break
  12. // possible ties, avoiding incorrect run order
  13. const paddedOrder = executionOrder.toFixed(0).slice(0, 3).padStart(3, "0");
  14. return (stripNonAlphanumeric(`${new Date(epoch).toISOString().slice(0, -1)}${paddedOrder}Z`) + runId);
  15. }
  16. /**
  17. * Baggage header information
  18. */
  19. class Baggage {
  20. constructor(metadata, tags, project_name) {
  21. Object.defineProperty(this, "metadata", {
  22. enumerable: true,
  23. configurable: true,
  24. writable: true,
  25. value: void 0
  26. });
  27. Object.defineProperty(this, "tags", {
  28. enumerable: true,
  29. configurable: true,
  30. writable: true,
  31. value: void 0
  32. });
  33. Object.defineProperty(this, "project_name", {
  34. enumerable: true,
  35. configurable: true,
  36. writable: true,
  37. value: void 0
  38. });
  39. this.metadata = metadata;
  40. this.tags = tags;
  41. this.project_name = project_name;
  42. }
  43. static fromHeader(value) {
  44. const items = value.split(",");
  45. let metadata = {};
  46. let tags = [];
  47. let project_name;
  48. for (const item of items) {
  49. const [key, uriValue] = item.split("=");
  50. const value = decodeURIComponent(uriValue);
  51. if (key === "langsmith-metadata") {
  52. metadata = JSON.parse(value);
  53. }
  54. else if (key === "langsmith-tags") {
  55. tags = value.split(",");
  56. }
  57. else if (key === "langsmith-project") {
  58. project_name = value;
  59. }
  60. }
  61. return new Baggage(metadata, tags, project_name);
  62. }
  63. toHeader() {
  64. const items = [];
  65. if (this.metadata && Object.keys(this.metadata).length > 0) {
  66. items.push(`langsmith-metadata=${encodeURIComponent(JSON.stringify(this.metadata))}`);
  67. }
  68. if (this.tags && this.tags.length > 0) {
  69. items.push(`langsmith-tags=${encodeURIComponent(this.tags.join(","))}`);
  70. }
  71. if (this.project_name) {
  72. items.push(`langsmith-project=${encodeURIComponent(this.project_name)}`);
  73. }
  74. return items.join(",");
  75. }
  76. }
  77. export class RunTree {
  78. constructor(originalConfig) {
  79. Object.defineProperty(this, "id", {
  80. enumerable: true,
  81. configurable: true,
  82. writable: true,
  83. value: void 0
  84. });
  85. Object.defineProperty(this, "name", {
  86. enumerable: true,
  87. configurable: true,
  88. writable: true,
  89. value: void 0
  90. });
  91. Object.defineProperty(this, "run_type", {
  92. enumerable: true,
  93. configurable: true,
  94. writable: true,
  95. value: void 0
  96. });
  97. Object.defineProperty(this, "project_name", {
  98. enumerable: true,
  99. configurable: true,
  100. writable: true,
  101. value: void 0
  102. });
  103. Object.defineProperty(this, "parent_run", {
  104. enumerable: true,
  105. configurable: true,
  106. writable: true,
  107. value: void 0
  108. });
  109. Object.defineProperty(this, "child_runs", {
  110. enumerable: true,
  111. configurable: true,
  112. writable: true,
  113. value: void 0
  114. });
  115. Object.defineProperty(this, "start_time", {
  116. enumerable: true,
  117. configurable: true,
  118. writable: true,
  119. value: void 0
  120. });
  121. Object.defineProperty(this, "end_time", {
  122. enumerable: true,
  123. configurable: true,
  124. writable: true,
  125. value: void 0
  126. });
  127. Object.defineProperty(this, "extra", {
  128. enumerable: true,
  129. configurable: true,
  130. writable: true,
  131. value: void 0
  132. });
  133. Object.defineProperty(this, "tags", {
  134. enumerable: true,
  135. configurable: true,
  136. writable: true,
  137. value: void 0
  138. });
  139. Object.defineProperty(this, "error", {
  140. enumerable: true,
  141. configurable: true,
  142. writable: true,
  143. value: void 0
  144. });
  145. Object.defineProperty(this, "serialized", {
  146. enumerable: true,
  147. configurable: true,
  148. writable: true,
  149. value: void 0
  150. });
  151. Object.defineProperty(this, "inputs", {
  152. enumerable: true,
  153. configurable: true,
  154. writable: true,
  155. value: void 0
  156. });
  157. Object.defineProperty(this, "outputs", {
  158. enumerable: true,
  159. configurable: true,
  160. writable: true,
  161. value: void 0
  162. });
  163. Object.defineProperty(this, "reference_example_id", {
  164. enumerable: true,
  165. configurable: true,
  166. writable: true,
  167. value: void 0
  168. });
  169. Object.defineProperty(this, "client", {
  170. enumerable: true,
  171. configurable: true,
  172. writable: true,
  173. value: void 0
  174. });
  175. Object.defineProperty(this, "events", {
  176. enumerable: true,
  177. configurable: true,
  178. writable: true,
  179. value: void 0
  180. });
  181. Object.defineProperty(this, "trace_id", {
  182. enumerable: true,
  183. configurable: true,
  184. writable: true,
  185. value: void 0
  186. });
  187. Object.defineProperty(this, "dotted_order", {
  188. enumerable: true,
  189. configurable: true,
  190. writable: true,
  191. value: void 0
  192. });
  193. Object.defineProperty(this, "tracingEnabled", {
  194. enumerable: true,
  195. configurable: true,
  196. writable: true,
  197. value: void 0
  198. });
  199. Object.defineProperty(this, "execution_order", {
  200. enumerable: true,
  201. configurable: true,
  202. writable: true,
  203. value: void 0
  204. });
  205. Object.defineProperty(this, "child_execution_order", {
  206. enumerable: true,
  207. configurable: true,
  208. writable: true,
  209. value: void 0
  210. });
  211. /**
  212. * Attachments associated with the run.
  213. * Each entry is a tuple of [mime_type, bytes]
  214. */
  215. Object.defineProperty(this, "attachments", {
  216. enumerable: true,
  217. configurable: true,
  218. writable: true,
  219. value: void 0
  220. });
  221. // If you pass in a run tree directly, return a shallow clone
  222. if (isRunTree(originalConfig)) {
  223. Object.assign(this, { ...originalConfig });
  224. return;
  225. }
  226. const defaultConfig = RunTree.getDefaultConfig();
  227. const { metadata, ...config } = originalConfig;
  228. const client = config.client ?? RunTree.getSharedClient();
  229. const dedupedMetadata = {
  230. ...metadata,
  231. ...config?.extra?.metadata,
  232. };
  233. config.extra = { ...config.extra, metadata: dedupedMetadata };
  234. Object.assign(this, { ...defaultConfig, ...config, client });
  235. if (!this.trace_id) {
  236. if (this.parent_run) {
  237. this.trace_id = this.parent_run.trace_id ?? this.id;
  238. }
  239. else {
  240. this.trace_id = this.id;
  241. }
  242. }
  243. this.execution_order ??= 1;
  244. this.child_execution_order ??= 1;
  245. if (!this.dotted_order) {
  246. const currentDottedOrder = convertToDottedOrderFormat(this.start_time, this.id, this.execution_order);
  247. if (this.parent_run) {
  248. this.dotted_order =
  249. this.parent_run.dotted_order + "." + currentDottedOrder;
  250. }
  251. else {
  252. this.dotted_order = currentDottedOrder;
  253. }
  254. }
  255. }
  256. static getDefaultConfig() {
  257. return {
  258. id: uuid.v4(),
  259. run_type: "chain",
  260. project_name: getLangSmithEnvironmentVariable("PROJECT") ??
  261. getEnvironmentVariable("LANGCHAIN_SESSION") ?? // TODO: Deprecate
  262. "default",
  263. child_runs: [],
  264. api_url: getEnvironmentVariable("LANGCHAIN_ENDPOINT") ?? "http://localhost:1984",
  265. api_key: getEnvironmentVariable("LANGCHAIN_API_KEY"),
  266. caller_options: {},
  267. start_time: Date.now(),
  268. serialized: {},
  269. inputs: {},
  270. extra: {},
  271. };
  272. }
  273. static getSharedClient() {
  274. if (!RunTree.sharedClient) {
  275. RunTree.sharedClient = new Client();
  276. }
  277. return RunTree.sharedClient;
  278. }
  279. createChild(config) {
  280. const child_execution_order = this.child_execution_order + 1;
  281. const child = new RunTree({
  282. ...config,
  283. parent_run: this,
  284. project_name: this.project_name,
  285. client: this.client,
  286. tracingEnabled: this.tracingEnabled,
  287. execution_order: child_execution_order,
  288. child_execution_order: child_execution_order,
  289. });
  290. // Copy context vars over into the new run tree.
  291. if (_LC_CONTEXT_VARIABLES_KEY in this) {
  292. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  293. child[_LC_CONTEXT_VARIABLES_KEY] =
  294. this[_LC_CONTEXT_VARIABLES_KEY];
  295. }
  296. const LC_CHILD = Symbol.for("lc:child_config");
  297. const presentConfig = config.extra?.[LC_CHILD] ??
  298. this.extra[LC_CHILD];
  299. // tracing for LangChain is defined by the _parentRunId and runMap of the tracer
  300. if (isRunnableConfigLike(presentConfig)) {
  301. const newConfig = { ...presentConfig };
  302. const callbacks = isCallbackManagerLike(newConfig.callbacks)
  303. ? newConfig.callbacks.copy?.()
  304. : undefined;
  305. if (callbacks) {
  306. // update the parent run id
  307. Object.assign(callbacks, { _parentRunId: child.id });
  308. // only populate if we're in a newer LC.JS version
  309. callbacks.handlers
  310. ?.find(isLangChainTracerLike)
  311. ?.updateFromRunTree?.(child);
  312. newConfig.callbacks = callbacks;
  313. }
  314. child.extra[LC_CHILD] = newConfig;
  315. }
  316. // propagate child_execution_order upwards
  317. const visited = new Set();
  318. let current = this;
  319. while (current != null && !visited.has(current.id)) {
  320. visited.add(current.id);
  321. current.child_execution_order = Math.max(current.child_execution_order, child_execution_order);
  322. current = current.parent_run;
  323. }
  324. this.child_runs.push(child);
  325. return child;
  326. }
  327. async end(outputs, error, endTime = Date.now(), metadata) {
  328. this.outputs = this.outputs ?? outputs;
  329. this.error = this.error ?? error;
  330. this.end_time = this.end_time ?? endTime;
  331. if (metadata && Object.keys(metadata).length > 0) {
  332. this.extra = this.extra
  333. ? { ...this.extra, metadata: { ...this.extra.metadata, ...metadata } }
  334. : { metadata };
  335. }
  336. }
  337. _convertToCreate(run, runtimeEnv, excludeChildRuns = true) {
  338. const runExtra = run.extra ?? {};
  339. if (!runExtra.runtime) {
  340. runExtra.runtime = {};
  341. }
  342. if (runtimeEnv) {
  343. for (const [k, v] of Object.entries(runtimeEnv)) {
  344. if (!runExtra.runtime[k]) {
  345. runExtra.runtime[k] = v;
  346. }
  347. }
  348. }
  349. let child_runs;
  350. let parent_run_id;
  351. if (!excludeChildRuns) {
  352. child_runs = run.child_runs.map((child_run) => this._convertToCreate(child_run, runtimeEnv, excludeChildRuns));
  353. parent_run_id = undefined;
  354. }
  355. else {
  356. parent_run_id = run.parent_run?.id;
  357. child_runs = [];
  358. }
  359. const persistedRun = {
  360. id: run.id,
  361. name: run.name,
  362. start_time: run.start_time,
  363. end_time: run.end_time,
  364. run_type: run.run_type,
  365. reference_example_id: run.reference_example_id,
  366. extra: runExtra,
  367. serialized: run.serialized,
  368. error: run.error,
  369. inputs: run.inputs,
  370. outputs: run.outputs,
  371. session_name: run.project_name,
  372. child_runs: child_runs,
  373. parent_run_id: parent_run_id,
  374. trace_id: run.trace_id,
  375. dotted_order: run.dotted_order,
  376. tags: run.tags,
  377. attachments: run.attachments,
  378. };
  379. return persistedRun;
  380. }
  381. async postRun(excludeChildRuns = true) {
  382. try {
  383. const runtimeEnv = getRuntimeEnvironment();
  384. const runCreate = await this._convertToCreate(this, runtimeEnv, true);
  385. await this.client.createRun(runCreate);
  386. if (!excludeChildRuns) {
  387. warnOnce("Posting with excludeChildRuns=false is deprecated and will be removed in a future version.");
  388. for (const childRun of this.child_runs) {
  389. await childRun.postRun(false);
  390. }
  391. }
  392. }
  393. catch (error) {
  394. console.error(`Error in postRun for run ${this.id}:`, error);
  395. }
  396. }
  397. async patchRun() {
  398. try {
  399. const runUpdate = {
  400. end_time: this.end_time,
  401. error: this.error,
  402. inputs: this.inputs,
  403. outputs: this.outputs,
  404. parent_run_id: this.parent_run?.id,
  405. reference_example_id: this.reference_example_id,
  406. extra: this.extra,
  407. events: this.events,
  408. dotted_order: this.dotted_order,
  409. trace_id: this.trace_id,
  410. tags: this.tags,
  411. attachments: this.attachments,
  412. session_name: this.project_name,
  413. };
  414. await this.client.updateRun(this.id, runUpdate);
  415. }
  416. catch (error) {
  417. console.error(`Error in patchRun for run ${this.id}`, error);
  418. }
  419. }
  420. toJSON() {
  421. return this._convertToCreate(this, undefined, false);
  422. }
  423. /**
  424. * Add an event to the run tree.
  425. * @param event - A single event or string to add
  426. */
  427. addEvent(event) {
  428. if (!this.events) {
  429. this.events = [];
  430. }
  431. if (typeof event === "string") {
  432. this.events.push({
  433. name: "event",
  434. time: new Date().toISOString(),
  435. message: event,
  436. });
  437. }
  438. else {
  439. this.events.push({
  440. ...event,
  441. time: event.time ?? new Date().toISOString(),
  442. });
  443. }
  444. }
  445. static fromRunnableConfig(parentConfig, props) {
  446. // We only handle the callback manager case for now
  447. const callbackManager = parentConfig?.callbacks;
  448. let parentRun;
  449. let projectName;
  450. let client;
  451. let tracingEnabled = isTracingEnabled();
  452. if (callbackManager) {
  453. const parentRunId = callbackManager?.getParentRunId?.() ?? "";
  454. const langChainTracer = callbackManager?.handlers?.find((handler) => handler?.name == "langchain_tracer");
  455. parentRun = langChainTracer?.getRun?.(parentRunId);
  456. projectName = langChainTracer?.projectName;
  457. client = langChainTracer?.client;
  458. tracingEnabled = tracingEnabled || !!langChainTracer;
  459. }
  460. if (!parentRun) {
  461. return new RunTree({
  462. ...props,
  463. client,
  464. tracingEnabled,
  465. project_name: projectName,
  466. });
  467. }
  468. const parentRunTree = new RunTree({
  469. name: parentRun.name,
  470. id: parentRun.id,
  471. trace_id: parentRun.trace_id,
  472. dotted_order: parentRun.dotted_order,
  473. client,
  474. tracingEnabled,
  475. project_name: projectName,
  476. tags: [
  477. ...new Set((parentRun?.tags ?? []).concat(parentConfig?.tags ?? [])),
  478. ],
  479. extra: {
  480. metadata: {
  481. ...parentRun?.extra?.metadata,
  482. ...parentConfig?.metadata,
  483. },
  484. },
  485. });
  486. return parentRunTree.createChild(props);
  487. }
  488. static fromDottedOrder(dottedOrder) {
  489. return this.fromHeaders({ "langsmith-trace": dottedOrder });
  490. }
  491. static fromHeaders(headers, inheritArgs) {
  492. const rawHeaders = "get" in headers && typeof headers.get === "function"
  493. ? {
  494. "langsmith-trace": headers.get("langsmith-trace"),
  495. baggage: headers.get("baggage"),
  496. }
  497. : headers;
  498. const headerTrace = rawHeaders["langsmith-trace"];
  499. if (!headerTrace || typeof headerTrace !== "string")
  500. return undefined;
  501. const parentDottedOrder = headerTrace.trim();
  502. const parsedDottedOrder = parentDottedOrder.split(".").map((part) => {
  503. const [strTime, uuid] = part.split("Z");
  504. return { strTime, time: Date.parse(strTime + "Z"), uuid };
  505. });
  506. const traceId = parsedDottedOrder[0].uuid;
  507. const config = {
  508. ...inheritArgs,
  509. name: inheritArgs?.["name"] ?? "parent",
  510. run_type: inheritArgs?.["run_type"] ?? "chain",
  511. start_time: inheritArgs?.["start_time"] ?? Date.now(),
  512. id: parsedDottedOrder.at(-1)?.uuid,
  513. trace_id: traceId,
  514. dotted_order: parentDottedOrder,
  515. };
  516. if (rawHeaders["baggage"] && typeof rawHeaders["baggage"] === "string") {
  517. const baggage = Baggage.fromHeader(rawHeaders["baggage"]);
  518. config.metadata = baggage.metadata;
  519. config.tags = baggage.tags;
  520. config.project_name = baggage.project_name;
  521. }
  522. return new RunTree(config);
  523. }
  524. toHeaders(headers) {
  525. const result = {
  526. "langsmith-trace": this.dotted_order,
  527. baggage: new Baggage(this.extra?.metadata, this.tags, this.project_name).toHeader(),
  528. };
  529. if (headers) {
  530. for (const [key, value] of Object.entries(result)) {
  531. headers.set(key, value);
  532. }
  533. }
  534. return result;
  535. }
  536. }
  537. Object.defineProperty(RunTree, "sharedClient", {
  538. enumerable: true,
  539. configurable: true,
  540. writable: true,
  541. value: null
  542. });
  543. export function isRunTree(x) {
  544. return (x !== undefined &&
  545. typeof x.createChild === "function" &&
  546. typeof x.postRun === "function");
  547. }
  548. function isLangChainTracerLike(x) {
  549. return (typeof x === "object" &&
  550. x != null &&
  551. typeof x.name === "string" &&
  552. x.name === "langchain_tracer");
  553. }
  554. function containsLangChainTracerLike(x) {
  555. return (Array.isArray(x) && x.some((callback) => isLangChainTracerLike(callback)));
  556. }
  557. function isCallbackManagerLike(x) {
  558. return (typeof x === "object" &&
  559. x != null &&
  560. Array.isArray(x.handlers));
  561. }
  562. export function isRunnableConfigLike(x) {
  563. // Check that it's an object with a callbacks arg
  564. // that has either a CallbackManagerLike object with a langchain tracer within it
  565. // or an array with a LangChainTracerLike object within it
  566. return (x !== undefined &&
  567. typeof x.callbacks === "object" &&
  568. // Callback manager with a langchain tracer
  569. (containsLangChainTracerLike(x.callbacks?.handlers) ||
  570. // Or it's an array with a LangChainTracerLike object within it
  571. containsLangChainTracerLike(x.callbacks)));
  572. }