langwatch 0.1.7 → 0.3.0-prerelease.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/.editorconfig +16 -0
  2. package/LICENSE +7 -0
  3. package/README.md +268 -1
  4. package/copy-types.sh +19 -8
  5. package/examples/langchain/.env.example +2 -0
  6. package/examples/langchain/README.md +42 -0
  7. package/examples/langchain/package-lock.json +2930 -0
  8. package/examples/langchain/package.json +27 -0
  9. package/examples/langchain/src/cli-markdown.d.ts +137 -0
  10. package/examples/langchain/src/index.ts +109 -0
  11. package/examples/langchain/tsconfig.json +25 -0
  12. package/examples/langgraph/.env.example +2 -0
  13. package/examples/langgraph/README.md +42 -0
  14. package/examples/langgraph/package-lock.json +3031 -0
  15. package/examples/langgraph/package.json +28 -0
  16. package/examples/langgraph/src/cli-markdown.d.ts +137 -0
  17. package/examples/langgraph/src/index.ts +196 -0
  18. package/examples/langgraph/tsconfig.json +25 -0
  19. package/examples/mastra/.env.example +2 -0
  20. package/examples/mastra/README.md +57 -0
  21. package/examples/mastra/package-lock.json +5296 -0
  22. package/examples/mastra/package.json +32 -0
  23. package/examples/mastra/src/cli-markdown.d.ts +137 -0
  24. package/examples/mastra/src/index.ts +120 -0
  25. package/examples/mastra/src/mastra/agents/weather-agent.ts +30 -0
  26. package/examples/mastra/src/mastra/index.ts +21 -0
  27. package/examples/mastra/src/mastra/tools/weather-tool.ts +102 -0
  28. package/examples/mastra/tsconfig.json +25 -0
  29. package/examples/vercel-ai/.env.example +2 -0
  30. package/examples/vercel-ai/README.md +38 -0
  31. package/examples/vercel-ai/package-lock.json +2571 -0
  32. package/examples/vercel-ai/package.json +27 -0
  33. package/examples/vercel-ai/src/cli-markdown.d.ts +137 -0
  34. package/examples/vercel-ai/src/index.ts +110 -0
  35. package/examples/vercel-ai/src/instrumentation.ts +9 -0
  36. package/examples/vercel-ai/tsconfig.json +25 -0
  37. package/package.json +80 -33
  38. package/src/__tests__/client-browser.test.ts +92 -0
  39. package/src/__tests__/client-node.test.ts +76 -0
  40. package/src/__tests__/client.test.ts +71 -0
  41. package/src/__tests__/integration/client-browser.test.ts +46 -0
  42. package/src/__tests__/integration/client-node.test.ts +46 -0
  43. package/src/client-browser.ts +70 -0
  44. package/src/client-node.ts +82 -0
  45. package/src/client-shared.ts +72 -0
  46. package/src/client.ts +119 -0
  47. package/src/evaluation/__tests__/record-evaluation.test.ts +112 -0
  48. package/src/evaluation/__tests__/run-evaluation.test.ts +171 -0
  49. package/src/evaluation/index.ts +2 -0
  50. package/src/evaluation/record-evaluation.ts +101 -0
  51. package/src/evaluation/run-evaluation.ts +133 -0
  52. package/src/evaluation/tracer.ts +3 -0
  53. package/src/evaluation/types.ts +23 -0
  54. package/src/index.ts +10 -591
  55. package/src/internal/api/__tests__/errors.test.ts +98 -0
  56. package/src/internal/api/client.ts +30 -0
  57. package/src/internal/api/errors.ts +32 -0
  58. package/src/internal/generated/types/.gitkeep +0 -0
  59. package/src/observability/__tests__/integration/base.test.ts +74 -0
  60. package/src/observability/__tests__/integration/browser-setup-ordering.test.ts +60 -0
  61. package/src/observability/__tests__/integration/complex-nested-spans.test.ts +29 -0
  62. package/src/observability/__tests__/integration/error-handling.test.ts +24 -0
  63. package/src/observability/__tests__/integration/langwatch-disabled-otel.test.ts +24 -0
  64. package/src/observability/__tests__/integration/langwatch-first-then-vercel.test.ts +24 -0
  65. package/src/observability/__tests__/integration/multiple-setup-attempts.test.ts +27 -0
  66. package/src/observability/__tests__/integration/otel-ordering.test.ts +27 -0
  67. package/src/observability/__tests__/integration/vercel-configurations.test.ts +20 -0
  68. package/src/observability/__tests__/integration/vercel-first-then-langwatch.test.ts +27 -0
  69. package/src/observability/__tests__/span.test.ts +214 -0
  70. package/src/observability/__tests__/trace.test.ts +180 -0
  71. package/src/observability/exporters/index.ts +1 -0
  72. package/src/observability/exporters/langwatch-exporter.ts +53 -0
  73. package/src/observability/index.ts +4 -0
  74. package/src/observability/instrumentation/langchain/__tests__/integration/langchain-chatbot.test.ts +112 -0
  75. package/src/observability/instrumentation/langchain/__tests__/langchain.test.ts +284 -0
  76. package/src/observability/instrumentation/langchain/index.ts +624 -0
  77. package/src/observability/processors/__tests__/filterable-batch-span-exporter.test.ts +98 -0
  78. package/src/observability/processors/filterable-batch-span-processor.ts +99 -0
  79. package/src/observability/processors/index.ts +1 -0
  80. package/src/observability/semconv/attributes.ts +185 -0
  81. package/src/observability/semconv/events.ts +42 -0
  82. package/src/observability/semconv/index.ts +16 -0
  83. package/src/observability/semconv/values.ts +159 -0
  84. package/src/observability/span.ts +728 -0
  85. package/src/observability/trace.ts +301 -0
  86. package/src/prompt/__tests__/prompt.test.ts +139 -0
  87. package/src/prompt/get-prompt-version.ts +49 -0
  88. package/src/prompt/get-prompt.ts +44 -0
  89. package/src/prompt/index.ts +3 -0
  90. package/src/prompt/prompt.ts +133 -0
  91. package/src/prompt/service.ts +221 -0
  92. package/src/prompt/tracer.ts +3 -0
  93. package/src/prompt/types.ts +0 -0
  94. package/ts-to-zod.config.js +11 -0
  95. package/tsconfig.json +3 -9
  96. package/tsup.config.ts +11 -1
  97. package/vitest.config.ts +1 -0
  98. package/dist/chunk-FWBCQQYZ.mjs +0 -711
  99. package/dist/chunk-FWBCQQYZ.mjs.map +0 -1
  100. package/dist/index.d.mts +0 -1010
  101. package/dist/index.d.ts +0 -1010
  102. package/dist/index.js +0 -27294
  103. package/dist/index.js.map +0 -1
  104. package/dist/index.mjs +0 -959
  105. package/dist/index.mjs.map +0 -1
  106. package/dist/utils-B0pgWcps.d.mts +0 -303
  107. package/dist/utils-B0pgWcps.d.ts +0 -303
  108. package/dist/utils.d.mts +0 -2
  109. package/dist/utils.d.ts +0 -2
  110. package/dist/utils.js +0 -703
  111. package/dist/utils.js.map +0 -1
  112. package/dist/utils.mjs +0 -11
  113. package/dist/utils.mjs.map +0 -1
  114. package/example/.env.example +0 -12
  115. package/example/.eslintrc.json +0 -26
  116. package/example/LICENSE +0 -13
  117. package/example/README.md +0 -12
  118. package/example/app/(chat)/chat/[id]/page.tsx +0 -60
  119. package/example/app/(chat)/layout.tsx +0 -14
  120. package/example/app/(chat)/page.tsx +0 -27
  121. package/example/app/actions.ts +0 -156
  122. package/example/app/globals.css +0 -76
  123. package/example/app/guardrails/page.tsx +0 -26
  124. package/example/app/langchain/page.tsx +0 -27
  125. package/example/app/langchain-rag/page.tsx +0 -28
  126. package/example/app/late-update/page.tsx +0 -27
  127. package/example/app/layout.tsx +0 -64
  128. package/example/app/login/actions.ts +0 -71
  129. package/example/app/login/page.tsx +0 -18
  130. package/example/app/manual/page.tsx +0 -27
  131. package/example/app/new/page.tsx +0 -5
  132. package/example/app/opengraph-image.png +0 -0
  133. package/example/app/share/[id]/page.tsx +0 -58
  134. package/example/app/signup/actions.ts +0 -111
  135. package/example/app/signup/page.tsx +0 -18
  136. package/example/app/twitter-image.png +0 -0
  137. package/example/auth.config.ts +0 -42
  138. package/example/auth.ts +0 -45
  139. package/example/components/button-scroll-to-bottom.tsx +0 -36
  140. package/example/components/chat-history.tsx +0 -49
  141. package/example/components/chat-list.tsx +0 -52
  142. package/example/components/chat-message-actions.tsx +0 -40
  143. package/example/components/chat-message.tsx +0 -80
  144. package/example/components/chat-panel.tsx +0 -139
  145. package/example/components/chat-share-dialog.tsx +0 -95
  146. package/example/components/chat.tsx +0 -84
  147. package/example/components/clear-history.tsx +0 -75
  148. package/example/components/empty-screen.tsx +0 -38
  149. package/example/components/external-link.tsx +0 -29
  150. package/example/components/footer.tsx +0 -19
  151. package/example/components/header.tsx +0 -114
  152. package/example/components/login-button.tsx +0 -42
  153. package/example/components/login-form.tsx +0 -97
  154. package/example/components/markdown.tsx +0 -9
  155. package/example/components/prompt-form.tsx +0 -115
  156. package/example/components/providers.tsx +0 -17
  157. package/example/components/sidebar-actions.tsx +0 -125
  158. package/example/components/sidebar-desktop.tsx +0 -19
  159. package/example/components/sidebar-footer.tsx +0 -16
  160. package/example/components/sidebar-item.tsx +0 -124
  161. package/example/components/sidebar-items.tsx +0 -42
  162. package/example/components/sidebar-list.tsx +0 -38
  163. package/example/components/sidebar-mobile.tsx +0 -31
  164. package/example/components/sidebar-toggle.tsx +0 -24
  165. package/example/components/sidebar.tsx +0 -21
  166. package/example/components/signup-form.tsx +0 -95
  167. package/example/components/stocks/events-skeleton.tsx +0 -31
  168. package/example/components/stocks/events.tsx +0 -30
  169. package/example/components/stocks/index.tsx +0 -36
  170. package/example/components/stocks/message.tsx +0 -134
  171. package/example/components/stocks/spinner.tsx +0 -16
  172. package/example/components/stocks/stock-purchase.tsx +0 -146
  173. package/example/components/stocks/stock-skeleton.tsx +0 -22
  174. package/example/components/stocks/stock.tsx +0 -210
  175. package/example/components/stocks/stocks-skeleton.tsx +0 -9
  176. package/example/components/stocks/stocks.tsx +0 -67
  177. package/example/components/tailwind-indicator.tsx +0 -14
  178. package/example/components/theme-toggle.tsx +0 -31
  179. package/example/components/ui/alert-dialog.tsx +0 -141
  180. package/example/components/ui/badge.tsx +0 -36
  181. package/example/components/ui/button.tsx +0 -57
  182. package/example/components/ui/codeblock.tsx +0 -148
  183. package/example/components/ui/dialog.tsx +0 -122
  184. package/example/components/ui/dropdown-menu.tsx +0 -205
  185. package/example/components/ui/icons.tsx +0 -507
  186. package/example/components/ui/input.tsx +0 -25
  187. package/example/components/ui/label.tsx +0 -26
  188. package/example/components/ui/select.tsx +0 -164
  189. package/example/components/ui/separator.tsx +0 -31
  190. package/example/components/ui/sheet.tsx +0 -140
  191. package/example/components/ui/sonner.tsx +0 -31
  192. package/example/components/ui/switch.tsx +0 -29
  193. package/example/components/ui/textarea.tsx +0 -24
  194. package/example/components/ui/tooltip.tsx +0 -30
  195. package/example/components/user-menu.tsx +0 -53
  196. package/example/components.json +0 -17
  197. package/example/instrumentation.ts +0 -11
  198. package/example/lib/chat/guardrails.tsx +0 -181
  199. package/example/lib/chat/langchain-rag.tsx +0 -191
  200. package/example/lib/chat/langchain.tsx +0 -112
  201. package/example/lib/chat/late-update.tsx +0 -208
  202. package/example/lib/chat/manual.tsx +0 -605
  203. package/example/lib/chat/vercel-ai.tsx +0 -576
  204. package/example/lib/hooks/use-copy-to-clipboard.tsx +0 -33
  205. package/example/lib/hooks/use-enter-submit.tsx +0 -23
  206. package/example/lib/hooks/use-local-storage.ts +0 -24
  207. package/example/lib/hooks/use-scroll-anchor.tsx +0 -86
  208. package/example/lib/hooks/use-sidebar.tsx +0 -60
  209. package/example/lib/hooks/use-streamable-text.ts +0 -25
  210. package/example/lib/types.ts +0 -41
  211. package/example/lib/utils.ts +0 -89
  212. package/example/middleware.ts +0 -8
  213. package/example/next-env.d.ts +0 -5
  214. package/example/next.config.js +0 -16
  215. package/example/package-lock.json +0 -9990
  216. package/example/package.json +0 -84
  217. package/example/pnpm-lock.yaml +0 -5712
  218. package/example/postcss.config.js +0 -6
  219. package/example/prettier.config.cjs +0 -34
  220. package/example/public/apple-touch-icon.png +0 -0
  221. package/example/public/favicon-16x16.png +0 -0
  222. package/example/public/favicon.ico +0 -0
  223. package/example/public/next.svg +0 -1
  224. package/example/public/thirteen.svg +0 -1
  225. package/example/public/vercel.svg +0 -1
  226. package/example/tailwind.config.ts +0 -81
  227. package/example/tsconfig.json +0 -35
  228. package/src/LangWatchExporter.ts +0 -91
  229. package/src/evaluations.ts +0 -219
  230. package/src/index.test.ts +0 -402
  231. package/src/langchain.ts +0 -557
  232. package/src/typeUtils.ts +0 -89
  233. package/src/types.ts +0 -79
  234. package/src/utils.ts +0 -205
  235. /package/src/{server/types → internal/generated/openapi}/.gitkeep +0 -0
@@ -0,0 +1,624 @@
1
+ import type { AgentAction, AgentFinish } from "@langchain/core/agents";
2
+ import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
3
+ import { type DocumentInterface } from "@langchain/core/documents";
4
+ import type { Serialized } from "@langchain/core/load/serializable";
5
+ import {
6
+ AIMessage,
7
+ AIMessageChunk,
8
+ FunctionMessage,
9
+ FunctionMessageChunk,
10
+ HumanMessage,
11
+ HumanMessageChunk,
12
+ SystemMessage,
13
+ SystemMessageChunk,
14
+ ToolMessage,
15
+ ToolMessageChunk,
16
+ mapChatMessagesToStoredMessages,
17
+ type BaseMessage,
18
+ type StoredMessage,
19
+ } from "@langchain/core/messages";
20
+ import type { ChatGeneration, LLMResult } from "@langchain/core/outputs";
21
+ import type { ChainValues } from "@langchain/core/utils/types";
22
+ import { getLangWatchTracer } from "../../trace";
23
+ import type { LangWatchSpan } from "../../span";
24
+ import { context, trace, SpanStatusCode, Attributes } from "@opentelemetry/api";
25
+ import { chatMessageSchema } from "../../../internal/generated/types/tracer.generated";
26
+ import {
27
+ canAutomaticallyCaptureInput,
28
+ canAutomaticallyCaptureOutput,
29
+ } from "../../../client";
30
+ import * as intSemconv from "../../semconv";
31
+ import { z } from "zod";
32
+
33
+ export class LangWatchCallbackHandler extends BaseCallbackHandler {
34
+ name = "LangWatchCallbackHandler";
35
+ tracer = getLangWatchTracer("langwatch.instrumentation.langchain");
36
+ spans: Record<string, LangWatchSpan | undefined> = {};
37
+
38
+ constructor() {
39
+ super();
40
+ }
41
+
42
+ private getParentContext(parentRunId?: string): any {
43
+ if (parentRunId && this.spans[parentRunId]) {
44
+ return trace.setSpan(context.active(), this.spans[parentRunId]);
45
+ }
46
+
47
+ return context.active();
48
+ }
49
+
50
+ private getSpan(runId: string): LangWatchSpan | undefined {
51
+ return this.spans[runId];
52
+ }
53
+
54
+ async handleLLMStart(
55
+ llm: Serialized,
56
+ prompts: string[],
57
+ runId: string,
58
+ parentRunId?: string | undefined,
59
+ extraParams?: Record<string, unknown> | undefined,
60
+ _tags?: string[] | undefined,
61
+ metadata?: Record<string, unknown> | undefined,
62
+ name?: string,
63
+ ): Promise<void> {
64
+ const parentContext = this.getParentContext(parentRunId);
65
+ const span = this.tracer.startSpan(
66
+ name ?? llm.id?.[llm.id.length - 1]?.toString() ?? "llm",
67
+ {},
68
+ parentContext,
69
+ );
70
+
71
+ span.setType("llm");
72
+
73
+ if (canAutomaticallyCaptureInput()) {
74
+ span.setInput(prompts);
75
+ }
76
+
77
+ if (_tags) {
78
+ span.setAttribute(intSemconv.ATTR_LANGWATCH_LANGCHAIN_RUN_TAGS, _tags);
79
+ }
80
+ if (extraParams) {
81
+ span.setAttributes(
82
+ Object.fromEntries(
83
+ Object.entries(extraParams).map(([key, value]) => [
84
+ [`${intSemconv.ATTR_LANGWATCH_LANGCHAIN_RUN_EXTRA_PARAMS}.${key}`],
85
+ wrapNonScalarValues(value),
86
+ ]),
87
+ ),
88
+ );
89
+ }
90
+ if (metadata) {
91
+ if (metadata.ls_model_name) {
92
+ span.setRequestModel(metadata.ls_model_name as string);
93
+ metadata.ls_model_name = void 0;
94
+ }
95
+
96
+ span.setAttributes(
97
+ Object.fromEntries(
98
+ Object.entries(metadata).map(([key, value]) => [
99
+ [`${intSemconv.ATTR_LANGWATCH_LANGCHAIN_RUN_METADATA}.${key}`],
100
+ wrapNonScalarValues(value),
101
+ ]),
102
+ ),
103
+ );
104
+ }
105
+ this.spans[runId] = span;
106
+ }
107
+
108
+ async handleChatModelStart(
109
+ llm: Serialized,
110
+ messages: BaseMessage[][],
111
+ runId: string,
112
+ parentRunId?: string | undefined,
113
+ extraParams?: Record<string, unknown> | undefined,
114
+ _tags?: string[] | undefined,
115
+ metadata?: Record<string, unknown> | undefined,
116
+ name?: string,
117
+ ): Promise<void> {
118
+ const parentContext = this.getParentContext(parentRunId);
119
+ const span = this.tracer.startSpan(
120
+ name ?? llm.id?.[llm.id.length - 1]?.toString() ?? "chat_model",
121
+ {},
122
+ parentContext,
123
+ );
124
+
125
+ span.setType("llm");
126
+
127
+ if (canAutomaticallyCaptureInput()) {
128
+ span.setInput(messages.flatMap(convertFromLangChainMessages));
129
+ }
130
+
131
+ if (_tags) {
132
+ span.setAttribute(intSemconv.ATTR_LANGWATCH_LANGCHAIN_RUN_TAGS, _tags);
133
+ }
134
+ if (extraParams) {
135
+ span.setAttributes(
136
+ Object.fromEntries(
137
+ Object.entries(extraParams).map(([key, value]) => [
138
+ [`${intSemconv.ATTR_LANGWATCH_LANGCHAIN_RUN_EXTRA_PARAMS}.${key}`],
139
+ wrapNonScalarValues(value),
140
+ ]),
141
+ ),
142
+ );
143
+ }
144
+ if (metadata) {
145
+ if (metadata.ls_model_name) {
146
+ span.setRequestModel(metadata.ls_model_name as string);
147
+ metadata.ls_model_name = void 0;
148
+ }
149
+ span.setAttributes(
150
+ Object.fromEntries(
151
+ Object.entries(metadata).map(([key, value]) => [
152
+ [`${intSemconv.ATTR_LANGWATCH_LANGCHAIN_RUN_METADATA}.${key}`],
153
+ wrapNonScalarValues(value),
154
+ ]),
155
+ ),
156
+ );
157
+ }
158
+
159
+ this.spans[runId] = span;
160
+ }
161
+
162
+ async handleLLMEnd(
163
+ response: LLMResult,
164
+ runId: string,
165
+ _parentRunId?: string | undefined,
166
+ ): Promise<void> {
167
+ const span = this.getSpan(runId);
168
+ if (!span) return;
169
+ const outputs: unknown[] = [];
170
+ for (const generation of response.generations) {
171
+ for (const generation_ of generation) {
172
+ if ("message" in generation_ && generation_.message) {
173
+ outputs.push(
174
+ convertFromLangChainMessages([
175
+ (generation_ as ChatGeneration).message,
176
+ ]),
177
+ );
178
+ } else if ("text" in generation_) {
179
+ outputs.push(generation_.text);
180
+ } else {
181
+ outputs.push(JSON.stringify(generation_));
182
+ }
183
+ }
184
+ }
185
+ const output = outputs.length === 1 ? outputs[0] : outputs;
186
+
187
+ if (canAutomaticallyCaptureOutput()) {
188
+ span.setOutput(output);
189
+ }
190
+
191
+ addLangChainEvent(span, "handleLLMEnd", runId, _parentRunId);
192
+ span.end();
193
+ this.spans[runId] = void 0;
194
+ }
195
+
196
+ async handleLLMError(
197
+ err: Error,
198
+ runId: string,
199
+ _parentRunId?: string | undefined,
200
+ ): Promise<void> {
201
+ const span = this.getSpan(runId);
202
+ if (!span) return;
203
+
204
+ addLangChainEvent(span, "handleLLMError", runId, _parentRunId);
205
+
206
+ span.recordException(err);
207
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
208
+ span.end();
209
+ this.spans[runId] = void 0;
210
+ }
211
+
212
+ async handleChainStart(
213
+ chain: Serialized,
214
+ inputs: ChainValues,
215
+ runId: string,
216
+ parentRunId?: string | undefined,
217
+ _tags?: string[] | undefined,
218
+ _metadata?: Record<string, unknown> | undefined,
219
+ _runType?: string,
220
+ name?: string,
221
+ ): Promise<void> {
222
+ const parentContext = this.getParentContext(parentRunId);
223
+ const span = this.tracer.startSpan(
224
+ name ?? chain.id?.[chain.id.length - 1]?.toString() ?? "chain",
225
+ {},
226
+ parentContext,
227
+ );
228
+ span.setType("chain");
229
+
230
+ if (canAutomaticallyCaptureInput()) {
231
+ span.setInput(inputs);
232
+ }
233
+
234
+ if (_tags) {
235
+ span.setAttribute(intSemconv.ATTR_LANGWATCH_LANGCHAIN_RUN_TAGS, _tags);
236
+ }
237
+ if (_metadata) {
238
+ span.setAttributes(
239
+ Object.fromEntries(
240
+ Object.entries(_metadata).map(([key, value]) => [
241
+ [`${intSemconv.ATTR_LANGWATCH_LANGCHAIN_RUN_METADATA}.${key}`],
242
+ wrapNonScalarValues(value),
243
+ ]),
244
+ ),
245
+ );
246
+ }
247
+ if (_runType) {
248
+ span.setAttribute(intSemconv.ATTR_LANGWATCH_LANGCHAIN_RUN_TYPE, _runType);
249
+ }
250
+
251
+ this.spans[runId] = span;
252
+ }
253
+
254
+ async handleChainEnd(
255
+ output: ChainValues,
256
+ runId: string,
257
+ _parentRunId?: string | undefined,
258
+ ): Promise<void> {
259
+ const span = this.getSpan(runId);
260
+ if (!span) return;
261
+
262
+ addLangChainEvent(span, "handleChainEnd", runId, _parentRunId);
263
+ span.setOutput(output);
264
+ span.end();
265
+ this.spans[runId] = void 0;
266
+ }
267
+
268
+ async handleChainError(
269
+ err: Error,
270
+ runId: string,
271
+ _parentRunId?: string | undefined,
272
+ _tags?: string[] | undefined,
273
+ _kwargs?: { inputs?: Record<string, unknown> | undefined } | undefined,
274
+ ): Promise<void> {
275
+ const span = this.getSpan(runId);
276
+ if (!span) return;
277
+
278
+ addLangChainEvent(span, "handleChainError", runId, _parentRunId);
279
+ span.recordException(err);
280
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
281
+ span.end();
282
+
283
+ this.spans[runId] = void 0;
284
+ }
285
+
286
+ async handleToolStart(
287
+ tool: Serialized,
288
+ input: string,
289
+ runId: string,
290
+ parentRunId?: string | undefined,
291
+ _tags?: string[] | undefined,
292
+ _metadata?: Record<string, unknown> | undefined,
293
+ name?: string,
294
+ ): Promise<void> {
295
+ const parentContext = this.getParentContext(parentRunId);
296
+ const span = this.tracer.startSpan(
297
+ name ?? tool.id?.[tool.id.length - 1]?.toString() ?? "tool",
298
+ {},
299
+ parentContext,
300
+ );
301
+ span.setType("tool");
302
+
303
+ if (canAutomaticallyCaptureInput()) {
304
+ span.setInputString(input);
305
+ }
306
+
307
+ span.setAttributes({
308
+ [intSemconv.ATTR_LANGWATCH_LANGCHAIN_RUN_ID]: runId,
309
+ [intSemconv.ATTR_LANGWATCH_LANGCHAIN_RUN_PARENT_ID]: parentRunId,
310
+ });
311
+
312
+ if (_tags) {
313
+ span.setAttribute(intSemconv.ATTR_LANGWATCH_LANGCHAIN_RUN_TAGS, _tags);
314
+ }
315
+ if (_metadata) {
316
+ span.setAttributes(
317
+ Object.fromEntries(
318
+ Object.entries(_metadata).map(([key, value]) => [
319
+ [`${intSemconv.ATTR_LANGWATCH_LANGCHAIN_RUN_METADATA}.${key}`],
320
+ wrapNonScalarValues(value),
321
+ ]),
322
+ ),
323
+ );
324
+ }
325
+ this.spans[runId] = span;
326
+ }
327
+
328
+ async handleToolEnd(
329
+ output: string,
330
+ runId: string,
331
+ _parentRunId?: string | undefined,
332
+ ): Promise<void> {
333
+ const span = this.getSpan(runId);
334
+ if (!span) return;
335
+ if (canAutomaticallyCaptureOutput()) {
336
+ span.setOutputString(output);
337
+ }
338
+
339
+ addLangChainEvent(span, "handleToolEnd", runId, _parentRunId);
340
+
341
+ span.end();
342
+ this.spans[runId] = void 0;
343
+ }
344
+
345
+ async handleToolError(
346
+ err: Error,
347
+ runId: string,
348
+ _parentRunId?: string | undefined,
349
+ _tags?: string[] | undefined,
350
+ ): Promise<void> {
351
+ const span = this.getSpan(runId);
352
+ if (!span) return;
353
+
354
+ addLangChainEvent(span, "handleToolError", runId, _parentRunId, _tags);
355
+
356
+ span.recordException(err);
357
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
358
+ span.end();
359
+ this.spans[runId] = void 0;
360
+ }
361
+
362
+ async handleRetrieverStart(
363
+ retriever: Serialized,
364
+ query: string,
365
+ runId: string,
366
+ parentRunId?: string | undefined,
367
+ _tags?: string[] | undefined,
368
+ _metadata?: Record<string, unknown> | undefined,
369
+ name?: string | undefined,
370
+ ) {
371
+ const parentContext = this.getParentContext(parentRunId);
372
+ const span = this.tracer.startSpan(
373
+ name ??
374
+ retriever.id?.[retriever.id.length - 1]?.toString() ??
375
+ "retriever",
376
+ {},
377
+ parentContext,
378
+ );
379
+ span.setType("rag");
380
+
381
+ if (canAutomaticallyCaptureInput()) {
382
+ span.setInputString(query);
383
+ }
384
+ if (_tags) {
385
+ span.setAttribute(intSemconv.ATTR_LANGWATCH_LANGCHAIN_RUN_TAGS, _tags);
386
+ }
387
+ if (_metadata) {
388
+ span.setAttributes(
389
+ Object.fromEntries(
390
+ Object.entries(_metadata).map(([key, value]) => [
391
+ [`${intSemconv.ATTR_LANGWATCH_LANGCHAIN_RUN_METADATA}.${key}`],
392
+ wrapNonScalarValues(value),
393
+ ]),
394
+ ),
395
+ );
396
+ }
397
+
398
+ span.setAttributes({
399
+ [intSemconv.ATTR_LANGWATCH_LANGCHAIN_RUN_ID]: runId,
400
+ [intSemconv.ATTR_LANGWATCH_LANGCHAIN_RUN_PARENT_ID]: parentRunId,
401
+ });
402
+
403
+ this.spans[runId] = span;
404
+ }
405
+
406
+ async handleRetrieverEnd(
407
+ documents: DocumentInterface<Record<string, any>>[],
408
+ runId: string,
409
+ _parentRunId?: string | undefined,
410
+ _tags?: string[] | undefined,
411
+ ) {
412
+ const span = this.getSpan(runId);
413
+ if (!span) return;
414
+ if (canAutomaticallyCaptureOutput()) {
415
+ span.setOutput(documents);
416
+ }
417
+
418
+ addLangChainEvent(span, "handleRetrieverEnd", runId, _parentRunId, _tags);
419
+
420
+ span.setRAGContexts(
421
+ documents.map((document) => ({
422
+ document_id: document.metadata.id,
423
+ chunk_id: document.metadata.chunk_id,
424
+ content: canAutomaticallyCaptureInput() ? document.pageContent : "",
425
+ })),
426
+ );
427
+
428
+ span.end();
429
+ this.spans[runId] = void 0;
430
+ }
431
+
432
+ async handleRetrieverError(
433
+ err: Error,
434
+ runId: string,
435
+ _parentRunId?: string | undefined,
436
+ _tags?: string[] | undefined,
437
+ ) {
438
+ const span = this.getSpan(runId);
439
+ if (!span) return;
440
+
441
+ addLangChainEvent(span, "handleRetrieverError", runId, _parentRunId, _tags);
442
+
443
+ span.recordException(err);
444
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
445
+ span.end();
446
+ this.spans[runId] = void 0;
447
+ }
448
+
449
+ async handleAgentAction(
450
+ _action: AgentAction,
451
+ runId: string,
452
+ _parentRunId?: string | undefined,
453
+ _tags?: string[] | undefined,
454
+ ): Promise<void> {
455
+ const span = this.getSpan(runId);
456
+ if (!span) return;
457
+
458
+ addLangChainEvent(span, "handleAgentAction", runId, _parentRunId, _tags);
459
+ span.setType("agent");
460
+ }
461
+
462
+ async handleAgentEnd(
463
+ action: AgentFinish,
464
+ runId: string,
465
+ _parentRunId?: string | undefined,
466
+ _tags?: string[] | undefined,
467
+ ): Promise<void> {
468
+ const span = this.getSpan(runId);
469
+ if (!span) return;
470
+
471
+ addLangChainEvent(span, "handleAgentEnd", runId, _parentRunId, _tags);
472
+
473
+ if (canAutomaticallyCaptureOutput()) {
474
+ span.setOutput(action.returnValues);
475
+ }
476
+
477
+ span.end();
478
+ this.spans[runId] = void 0;
479
+ }
480
+ }
481
+
482
+ export const convertFromLangChainMessages = (
483
+ messages: BaseMessage[],
484
+ ): any[] => {
485
+ const chatMessages: any[] = [];
486
+ for (const message of messages) {
487
+ chatMessages.push(
488
+ convertFromLangChainMessage(message as BaseMessage & { id?: string[] }),
489
+ );
490
+ }
491
+ return chatMessages;
492
+ };
493
+
494
+ const convertFromLangChainMessage = (
495
+ message: BaseMessage & { id?: string[] },
496
+ ): any => {
497
+ let role: string = "user";
498
+ const message_: (BaseMessage | StoredMessage) & {
499
+ id?: string[];
500
+ type?: string;
501
+ } = message.lc_serializable
502
+ ? mapChatMessagesToStoredMessages([message])[0]!
503
+ : message;
504
+ if (
505
+ message_ instanceof HumanMessage ||
506
+ message_ instanceof HumanMessageChunk ||
507
+ message_.id?.[message_.id.length - 1] === "HumanMessage" ||
508
+ message_.id?.[message_.id.length - 1] === "HumanMessageChunk" ||
509
+ message_.type === "human"
510
+ ) {
511
+ role = "user";
512
+ } else if (
513
+ message instanceof AIMessage ||
514
+ message instanceof AIMessageChunk ||
515
+ message.id?.[message.id.length - 1] === "AIMessage" ||
516
+ message.id?.[message.id.length - 1] === "AIMessageChunk" ||
517
+ message_.type === "ai"
518
+ ) {
519
+ role = "assistant";
520
+ } else if (
521
+ message instanceof SystemMessage ||
522
+ message instanceof SystemMessageChunk ||
523
+ message.id?.[message.id.length - 1] === "SystemMessage" ||
524
+ message.id?.[message.id.length - 1] === "SystemMessageChunk" ||
525
+ message_.type === "system"
526
+ ) {
527
+ role = "system";
528
+ } else if (
529
+ message instanceof FunctionMessage ||
530
+ message instanceof FunctionMessageChunk ||
531
+ message.id?.[message.id.length - 1] === "FunctionMessage" ||
532
+ message.id?.[message.id.length - 1] === "FunctionMessageChunk" ||
533
+ message_.type === "function"
534
+ ) {
535
+ role = "function";
536
+ } else if (
537
+ message instanceof ToolMessage ||
538
+ message instanceof ToolMessageChunk ||
539
+ message.id?.[message.id.length - 1] === "ToolMessage" ||
540
+ message.id?.[message.id.length - 1] === "ToolMessageChunk" ||
541
+ message_.type === "tool"
542
+ ) {
543
+ role = "tool";
544
+ }
545
+ const content =
546
+ typeof message.content === "string"
547
+ ? message.content
548
+ : message.content.map((content: any) =>
549
+ content.type === "text"
550
+ ? { type: "text", text: content.text }
551
+ : content.type == "image_url"
552
+ ? { type: "image_url", image_url: content.image_url }
553
+ : { type: "text", text: JSON.stringify(content) },
554
+ );
555
+ const functionCall = message.additional_kwargs as any;
556
+ return {
557
+ role,
558
+ content,
559
+ ...(functionCall &&
560
+ typeof functionCall === "object" &&
561
+ Object.keys(functionCall).length > 0
562
+ ? { function_call: functionCall }
563
+ : {}),
564
+ };
565
+ };
566
+
567
+ function wrapNonScalarValues(value: unknown): string | number | boolean | undefined {
568
+ if (value === void 0) {
569
+ return void 0;
570
+ }
571
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
572
+ return value;
573
+ }
574
+
575
+ const chatMessages = z.array(chatMessageSchema).safeParse(value);
576
+ if (Array.isArray(value) && chatMessages.success) {
577
+ return JSON.stringify({
578
+ type: "chat_messages",
579
+ value: chatMessages.data,
580
+ });
581
+ }
582
+
583
+ try {
584
+ JSON.stringify(value);
585
+
586
+ return JSON.stringify({
587
+ type: "json",
588
+ value: value as object,
589
+ });
590
+ } catch (e) {
591
+ return JSON.stringify({
592
+ type: "raw",
593
+ value: value as any,
594
+ });
595
+ }
596
+ }
597
+
598
+ function addLangChainEvent(
599
+ span: LangWatchSpan,
600
+ eventName: string,
601
+ runId: string,
602
+ parentRunId: string | undefined,
603
+ tags?: string[] | undefined,
604
+ metadata?: Record<string, unknown> | undefined,
605
+ attributes?: Attributes,
606
+ ) {
607
+ const attrs: Attributes = {
608
+ [intSemconv.ATTR_LANGWATCH_LANGCHAIN_RUN_ID]: runId,
609
+ [intSemconv.ATTR_LANGWATCH_LANGCHAIN_RUN_PARENT_ID]: parentRunId,
610
+ [intSemconv.ATTR_LANGWATCH_LANGCHAIN_EVENT_NAME]: eventName,
611
+ ...attributes,
612
+ };
613
+
614
+ if (tags) {
615
+ attrs[intSemconv.ATTR_LANGWATCH_LANGCHAIN_TAGS] = tags;
616
+ }
617
+ if (metadata) {
618
+ Object.entries(metadata).forEach(([key, value]) => {
619
+ attrs[key] = wrapNonScalarValues(value);
620
+ });
621
+ }
622
+
623
+ span.addEvent(intSemconv.EVNT_LANGWATCH_LANGCHAIN_CALLBACK, attrs);
624
+ }
@@ -0,0 +1,98 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { FilterableBatchSpanProcessor, SpanProcessingExcludeRule } from '../filterable-batch-span-processor';
3
+ import type { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base';
4
+
5
+ function makeSpan({ name, instrumentationScopeName }: { name: string; instrumentationScopeName: string }): ReadableSpan {
6
+ return {
7
+ name,
8
+ instrumentationScope: { name: instrumentationScopeName },
9
+ } as any;
10
+ }
11
+
12
+ describe('FilterableBatchSpanProcessor', () => {
13
+ let exporter: SpanExporter;
14
+ let onEndSpy: ReturnType<typeof vi.fn>;
15
+
16
+ beforeEach(() => {
17
+ onEndSpy = vi.fn();
18
+ exporter = { export: vi.fn(), shutdown: vi.fn() } as any;
19
+ // Patch BatchSpanProcessor's onEnd to spy on calls
20
+ (FilterableBatchSpanProcessor.prototype as any).__proto__.onEnd = onEndSpy;
21
+ });
22
+
23
+ afterEach(() => {
24
+ vi.restoreAllMocks();
25
+ });
26
+
27
+ it('should export span if no filters match', () => {
28
+ const filters: SpanProcessingExcludeRule[] = [
29
+ { fieldName: 'span_name', matchValue: 'foo', matchOperation: 'exact_match' },
30
+ ];
31
+ const processor = new FilterableBatchSpanProcessor(exporter, filters);
32
+ const span = makeSpan({ name: 'bar', instrumentationScopeName: 'scope' });
33
+ processor.onEnd(span);
34
+ expect(onEndSpy).toHaveBeenCalledWith(span);
35
+ });
36
+
37
+ it('should not export span if span_name exact_match filter matches', () => {
38
+ const filters: SpanProcessingExcludeRule[] = [
39
+ { fieldName: 'span_name', matchValue: 'heartbeat', matchOperation: 'exact_match' },
40
+ ];
41
+ const processor = new FilterableBatchSpanProcessor(exporter, filters);
42
+ const span = makeSpan({ name: 'heartbeat', instrumentationScopeName: 'scope' });
43
+ processor.onEnd(span);
44
+ expect(onEndSpy).not.toHaveBeenCalled();
45
+ });
46
+
47
+ it('should not export span if instrumentation_scope_name starts_with filter matches', () => {
48
+ const filters: SpanProcessingExcludeRule[] = [
49
+ { fieldName: 'instrumentation_scope_name', matchValue: 'internal', matchOperation: 'starts_with' },
50
+ ];
51
+ const processor = new FilterableBatchSpanProcessor(exporter, filters);
52
+ const span = makeSpan({ name: 'foo', instrumentationScopeName: 'internal-logger' });
53
+ processor.onEnd(span);
54
+ expect(onEndSpy).not.toHaveBeenCalled();
55
+ });
56
+
57
+ it('should not export span if span_name includes filter matches', () => {
58
+ const filters: SpanProcessingExcludeRule[] = [
59
+ { fieldName: 'span_name', matchValue: 'api', matchOperation: 'includes' },
60
+ ];
61
+ const processor = new FilterableBatchSpanProcessor(exporter, filters);
62
+ const span = makeSpan({ name: 'call-api-endpoint', instrumentationScopeName: 'scope' });
63
+ processor.onEnd(span);
64
+ expect(onEndSpy).not.toHaveBeenCalled();
65
+ });
66
+
67
+ it('should not export span if span_name ends_with filter matches', () => {
68
+ const filters: SpanProcessingExcludeRule[] = [
69
+ { fieldName: 'span_name', matchValue: 'end', matchOperation: 'ends_with' },
70
+ ];
71
+ const processor = new FilterableBatchSpanProcessor(exporter, filters);
72
+ const span = makeSpan({ name: 'process-end', instrumentationScopeName: 'scope' });
73
+ processor.onEnd(span);
74
+ expect(onEndSpy).not.toHaveBeenCalled();
75
+ });
76
+
77
+ it('should export span if multiple filters and none match', () => {
78
+ const filters: SpanProcessingExcludeRule[] = [
79
+ { fieldName: 'span_name', matchValue: 'foo', matchOperation: 'exact_match' },
80
+ { fieldName: 'instrumentation_scope_name', matchValue: 'bar', matchOperation: 'includes' },
81
+ ];
82
+ const processor = new FilterableBatchSpanProcessor(exporter, filters);
83
+ const span = makeSpan({ name: 'baz', instrumentationScopeName: 'scope' });
84
+ processor.onEnd(span);
85
+ expect(onEndSpy).toHaveBeenCalledWith(span);
86
+ });
87
+
88
+ it('should not export span if any filter matches (OR logic)', () => {
89
+ const filters: SpanProcessingExcludeRule[] = [
90
+ { fieldName: 'span_name', matchValue: 'baz', matchOperation: 'exact_match' },
91
+ { fieldName: 'instrumentation_scope_name', matchValue: 'scope', matchOperation: 'exact_match' },
92
+ ];
93
+ const processor = new FilterableBatchSpanProcessor(exporter, filters);
94
+ const span = makeSpan({ name: 'baz', instrumentationScopeName: 'scope' });
95
+ processor.onEnd(span);
96
+ expect(onEndSpy).not.toHaveBeenCalled();
97
+ });
98
+ });