langwatch 0.2.0 → 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 +78 -34
  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 -593
  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-LKD2K67J.mjs +0 -717
  99. package/dist/chunk-LKD2K67J.mjs.map +0 -1
  100. package/dist/index.d.mts +0 -1030
  101. package/dist/index.d.ts +0 -1030
  102. package/dist/index.js +0 -27310
  103. package/dist/index.js.map +0 -1
  104. package/dist/index.mjs +0 -963
  105. package/dist/index.mjs.map +0 -1
  106. package/dist/utils-Cv-rUjJ1.d.mts +0 -313
  107. package/dist/utils-Cv-rUjJ1.d.ts +0 -313
  108. package/dist/utils.d.mts +0 -2
  109. package/dist/utils.d.ts +0 -2
  110. package/dist/utils.js +0 -709
  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 -10917
  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 -96
  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 -82
  234. package/src/utils.ts +0 -205
  235. /package/src/{server/types → internal/generated/openapi}/.gitkeep +0 -0
@@ -0,0 +1,301 @@
1
+ import {
2
+ trace as otelTrace,
3
+ Tracer,
4
+ Span,
5
+ SpanOptions,
6
+ Context,
7
+ SpanStatusCode,
8
+ } from "@opentelemetry/api";
9
+ import { LangWatchSpan, createLangWatchSpan } from "./span";
10
+
11
+ /**
12
+ * LangWatch OpenTelemetry Tracing Extensions
13
+ *
14
+ * This module provides wrappers and helpers for OpenTelemetry Tracer and Span objects,
15
+ * adding ergonomic methods for LLM/GenAI observability and structured tracing.
16
+ *
17
+ * @module trace
18
+ */
19
+ export interface LangWatchTracer extends Tracer {
20
+ /**
21
+ * Starts a new {@link LangWatchSpan}. Start the span without setting it on context.
22
+ *
23
+ * This method does NOT modify the current Context.
24
+ *
25
+ * @param name The name of the span
26
+ * @param [options] SpanOptions used for span creation
27
+ * @param [context] Context to use to extract parent
28
+ * @returns LangWatchSpan The newly created span
29
+ *
30
+ * @example
31
+ * const span = tracer.startSpan('op');
32
+ * span.setAttribute('key', 'value');
33
+ * span.end();
34
+ */
35
+ startSpan(
36
+ name: string,
37
+ options?: SpanOptions,
38
+ context?: Context,
39
+ ): LangWatchSpan;
40
+
41
+ /**
42
+ * Starts a new {@link LangWatchSpan} and calls the given function passing it the
43
+ * created span as first argument.
44
+ * Additionally the new span gets set in context and this context is activated
45
+ * for the duration of the function call.
46
+ *
47
+ * @param name The name of the span
48
+ * @param [options] SpanOptions used for span creation
49
+ * @param [context] Context to use to extract parent
50
+ * @param fn function called in the context of the span and receives the newly created span as an argument
51
+ * @returns return value of fn
52
+ *
53
+ * @example
54
+ * const result = tracer.startActiveSpan('op', span => {
55
+ * try {
56
+ * // do some work
57
+ * span.setStatus({code: SpanStatusCode.OK});
58
+ * return something;
59
+ * } catch (err) {
60
+ * span.setStatus({
61
+ * code: SpanStatusCode.ERROR,
62
+ * message: err.message,
63
+ * });
64
+ * throw err;
65
+ * } finally {
66
+ * span.end();
67
+ * }
68
+ * });
69
+ *
70
+ * @example
71
+ * const span = tracer.startActiveSpan('op', span => {
72
+ * try {
73
+ * do some work
74
+ * return span;
75
+ * } catch (err) {
76
+ * span.setStatus({
77
+ * code: SpanStatusCode.ERROR,
78
+ * message: err.message,
79
+ * });
80
+ * throw err;
81
+ * }
82
+ * });
83
+ * do some more work
84
+ * span.end();
85
+ */
86
+ startActiveSpan<F extends (span: LangWatchSpan) => unknown>(
87
+ name: string,
88
+ fn: F,
89
+ ): ReturnType<F>;
90
+ startActiveSpan<F extends (span: LangWatchSpan) => unknown>(
91
+ name: string,
92
+ options: SpanOptions,
93
+ fn: F,
94
+ ): ReturnType<F>;
95
+ startActiveSpan<F extends (span: LangWatchSpan) => unknown>(
96
+ name: string,
97
+ options: SpanOptions,
98
+ context: Context,
99
+ fn: F,
100
+ ): ReturnType<F>;
101
+
102
+ /**
103
+ * Starts a new {@link LangWatchSpan}, runs the provided async function, and automatically handles
104
+ * error recording, status setting, and span ending. This is a safer and more ergonomic alternative
105
+ * to manually using try/catch/finally blocks with startActiveSpan.
106
+ *
107
+ * Overloads:
108
+ * - withActiveSpan(name, fn)
109
+ * - withActiveSpan(name, options, fn)
110
+ * - withActiveSpan(name, options, context, fn)
111
+ *
112
+ * @param name The name of the span
113
+ * @param options Optional SpanOptions for span creation
114
+ * @param context Optional Context to use to extract parent
115
+ * @param fn The async function to execute within the span context. Receives the span as its first argument.
116
+ * @returns The return value of the provided function
117
+ *
118
+ * @example
119
+ * await tracer.withActiveSpan('my-operation', async (span) => {
120
+ * // ... your code ...
121
+ * });
122
+ *
123
+ * await tracer.withActiveSpan('my-operation', { attributes: { foo: 'bar' } }, async (span) => {
124
+ * // ... your code ...
125
+ * });
126
+ *
127
+ * await tracer.withActiveSpan('my-operation', { attributes: { foo: 'bar' } }, myContext, async (span) => {
128
+ * // ... your code ...
129
+ * });
130
+ */
131
+ withActiveSpan<T>(
132
+ name: string,
133
+ fn: (span: LangWatchSpan) => Promise<T> | T
134
+ ): Promise<T>;
135
+ withActiveSpan<T>(
136
+ name: string,
137
+ options: SpanOptions,
138
+ fn: (span: LangWatchSpan) => Promise<T> | T
139
+ ): Promise<T>;
140
+ withActiveSpan<T>(
141
+ name: string,
142
+ options: SpanOptions,
143
+ context: Context,
144
+ fn: (span: LangWatchSpan) => Promise<T> | T
145
+ ): Promise<T>;
146
+ }
147
+
148
+ /**
149
+ * Extension of OpenTelemetry's Tracer with LangWatch-specific helpers.
150
+ *
151
+ * This interface provides methods for starting spans and active spans that return LangWatchSpan objects,
152
+ * which include ergonomic helpers for LLM/GenAI tracing.
153
+ *
154
+ * @example
155
+ * import { getLangWatchTracer } from 'langwatch';
156
+ * const tracer = getLangWatchTracer('my-service');
157
+ * const span = tracer.startSpan('llm-call');
158
+ * span.setType('llm').setInput('Prompt').setOutput('Completion');
159
+ * span.end();
160
+ *
161
+ * tracer.startActiveSpan('llm-call', (span) => {
162
+ * span.setType('llm');
163
+ * // ...
164
+ * span.end();
165
+ * });
166
+ */
167
+ export function getLangWatchTracer(name: string, version?: string): LangWatchTracer {
168
+ const tracer = otelTrace.getTracer(name, version);
169
+
170
+ // Create a proxy for the tracer that intercepts the calls to startActiveSpan and
171
+ // startSpan, and wraps the span object with our custom LangWatchSpan.
172
+ const handler: ProxyHandler<LangWatchTracer> = {
173
+ get(target, prop, _receiver) {
174
+ switch (prop) {
175
+ case "startActiveSpan": {
176
+ const startActiveSpan: StartActiveSpanOverloads = (
177
+ ...args: [
178
+ string,
179
+ SpanOptions?,
180
+ Context?,
181
+ ((span: Span) => unknown)?,
182
+ ]
183
+ ) => {
184
+ // Find the span callback function (usually the last argument!)
185
+ const fnIndex = args.findIndex((arg) => typeof arg === "function");
186
+ if (fnIndex === -1) {
187
+ throw new Error(
188
+ "startActiveSpan requires a function as the last argument",
189
+ );
190
+ }
191
+
192
+ // A type assertion is safe here due to the check above, but still sad 😥
193
+ const userFn = args[fnIndex] as (
194
+ span: Span,
195
+ ...rest: unknown[]
196
+ ) => unknown;
197
+
198
+ // Replace the function with one that wraps the span first
199
+ const spanWrapFunc = (...fnArgs: unknown[]) => {
200
+ const [span, ...rest] = fnArgs;
201
+ return userFn(createLangWatchSpan(span as Span), ...rest);
202
+ };
203
+
204
+ const newArgs = [...args];
205
+ newArgs[fnIndex] = spanWrapFunc;
206
+
207
+ // TypeScript can't infer the overload, but this is safe
208
+ return (
209
+ target.startActiveSpan as unknown as (
210
+ ...args: unknown[]
211
+ ) => unknown
212
+ )(...newArgs);
213
+ };
214
+ return startActiveSpan;
215
+ }
216
+
217
+ case "startSpan": {
218
+ return function (
219
+ ...args: Parameters<Tracer["startSpan"]>
220
+ ): ReturnType<Tracer["startSpan"]> {
221
+ const span = target.startSpan(...args);
222
+ return createLangWatchSpan(span);
223
+ };
224
+ }
225
+
226
+ case "withActiveSpan": {
227
+ /**
228
+ * Implementation of withActiveSpan: supports all overloads like startActiveSpan.
229
+ * Uses startActiveSpan to ensure context propagation for nested spans.
230
+ */
231
+ return async function withActiveSpan(...args: any[]): Promise<any> {
232
+ // Find the function argument (should be the last argument)
233
+ const fnIndex = args.findIndex((arg) => typeof arg === "function");
234
+ if (fnIndex === -1) {
235
+ throw new Error("withActiveSpan requires a function as the last argument");
236
+ }
237
+ const userFn = args[fnIndex] as (span: LangWatchSpan) => Promise<any> | any;
238
+ // The preceding arguments are: name, options?, context?
239
+ const name = args[0];
240
+ const options = args.length > 2 ? args[1] : undefined;
241
+ const context = args.length > 3 ? args[2] : undefined;
242
+
243
+ return await new Promise((resolve, reject) => {
244
+ // Use startActiveSpan to ensure context propagation
245
+ const cb = async (span: Span) => {
246
+ const wrappedSpan = createLangWatchSpan(span);
247
+ try {
248
+ resolve(await userFn(wrappedSpan));
249
+ } catch (err: any) {
250
+ wrappedSpan.setStatus({
251
+ code: SpanStatusCode.ERROR,
252
+ message: err && err.message ? err.message : String(err),
253
+ });
254
+ wrappedSpan.recordException(err);
255
+ reject(err);
256
+ } finally {
257
+ wrappedSpan.end();
258
+ }
259
+ };
260
+ // Call the correct overload of startActiveSpan
261
+ if (context !== undefined) {
262
+ target.startActiveSpan(name, options, context, cb);
263
+ } else if (options !== undefined) {
264
+ target.startActiveSpan(name, options, cb);
265
+ } else {
266
+ target.startActiveSpan(name, cb);
267
+ }
268
+ });
269
+ };
270
+ }
271
+
272
+ default: {
273
+ const value = target[prop as keyof Tracer];
274
+ return typeof value === "function" ? value.bind(target) : value;
275
+ }
276
+ }
277
+ },
278
+ };
279
+
280
+ return new Proxy(tracer, handler) as LangWatchTracer;
281
+ }
282
+
283
+ /**
284
+ * Helper type for the function overloads of startActiveSpan.
285
+ *
286
+ * This matches OpenTelemetry's Tracer interface and is used internally for type safety.
287
+ */
288
+ type StartActiveSpanOverloads = {
289
+ <F extends (span: Span) => unknown>(name: string, fn: F): ReturnType<F>;
290
+ <F extends (span: Span) => unknown>(
291
+ name: string,
292
+ options: SpanOptions,
293
+ fn: F,
294
+ ): ReturnType<F>;
295
+ <F extends (span: Span) => unknown>(
296
+ name: string,
297
+ options: SpanOptions,
298
+ context: Context,
299
+ fn: F,
300
+ ): ReturnType<F>;
301
+ };
@@ -0,0 +1,139 @@
1
+ import { describe, it, expect, beforeAll, afterEach, vi } from "vitest";
2
+ import { PromptService } from "../service";
3
+ import { Prompt, PromptCompilationError } from "../prompt";
4
+ import type { LangwatchApiClient } from "../../internal/api/client";
5
+
6
+ // Mock the client with proper Vitest mock methods
7
+ const mockClient = {
8
+ GET: vi.fn(),
9
+ POST: vi.fn(),
10
+ PUT: vi.fn(),
11
+ DELETE: vi.fn(),
12
+ } as unknown as LangwatchApiClient & {
13
+ GET: ReturnType<typeof vi.fn>;
14
+ POST: ReturnType<typeof vi.fn>;
15
+ PUT: ReturnType<typeof vi.fn>;
16
+ DELETE: ReturnType<typeof vi.fn>;
17
+ };
18
+
19
+ // Mock the createLangWatchApiClient function
20
+ vi.mock("../../internal/api/client", () => ({
21
+ createLangWatchApiClient: vi.fn(() => mockClient),
22
+ }));
23
+
24
+ describe("Prompt", () => {
25
+ let promptService: PromptService;
26
+
27
+ beforeAll(async () => {
28
+ promptService = new PromptService({ client: mockClient });
29
+ });
30
+
31
+ afterEach(() => {
32
+ vi.clearAllMocks();
33
+ });
34
+
35
+ it("should fetch and compile a prompt", async () => {
36
+ // Mock the API response
37
+ const mockPromptData = {
38
+ id: "prompt_123",
39
+ name: "Test Prompt",
40
+ prompt: "Hello {{user_name}}, how is the {{topic}} today?",
41
+ messages: [
42
+ {
43
+ role: "user",
44
+ content: "Tell me about {{topic}}",
45
+ },
46
+ ],
47
+ model: "gpt-4",
48
+ version: 1,
49
+ updatedAt: "2024-01-01T00:00:00Z",
50
+ };
51
+
52
+ mockClient.GET.mockResolvedValueOnce({
53
+ data: mockPromptData,
54
+ error: null,
55
+ });
56
+
57
+ const prompt = await promptService.get("prompt_123");
58
+
59
+ expect(prompt.id).toBe("prompt_123");
60
+ expect(prompt.name).toBe("Test Prompt");
61
+
62
+ // Test template compilation
63
+ const compiled = prompt.compile({
64
+ user_name: "Alice",
65
+ topic: "weather",
66
+ });
67
+
68
+ expect(compiled.prompt).toContain("Alice");
69
+ expect(JSON.stringify(compiled.messages)).toContain("weather");
70
+ });
71
+
72
+ it("should handle missing template variables gracefully", async () => {
73
+ // Mock the API response
74
+ const mockPromptData = {
75
+ id: "prompt_123",
76
+ name: "Test Prompt",
77
+ prompt: "Hello {{user_name}}, how is the {{topic}} today?",
78
+ messages: [
79
+ {
80
+ role: "user",
81
+ content: "Tell me about {{topic}}",
82
+ },
83
+ ],
84
+ model: "gpt-4",
85
+ version: 1,
86
+ updatedAt: "2024-01-01T00:00:00Z",
87
+ };
88
+
89
+ mockClient.GET.mockResolvedValueOnce({
90
+ data: mockPromptData,
91
+ error: null,
92
+ });
93
+
94
+ const prompt = await promptService.get("prompt_123");
95
+
96
+ // Lenient compilation should not throw and should replace missing variables with empty strings
97
+ const compiled = prompt.compile({ user_name: "Alice", topic: "weather" });
98
+ expect(compiled).toBeInstanceOf(Prompt);
99
+ expect(compiled.prompt).toBe("Hello Alice, how is the weather today?");
100
+ expect(compiled.messages[0]?.content).toBe("Tell me about weather");
101
+ });
102
+
103
+ it("should throw on strict compilation with missing variables", async () => {
104
+ // Mock the API response
105
+ const mockPromptData = {
106
+ id: "prompt_123",
107
+ name: "Test Prompt",
108
+ prompt: "Hello {{user_name}}, how is the {{topic}} today?",
109
+ messages: [
110
+ {
111
+ role: "user",
112
+ content: "Tell me about {{topic}}",
113
+ },
114
+ ],
115
+ model: "gpt-4",
116
+ version: 1,
117
+ updatedAt: "2024-01-01T00:00:00Z",
118
+ };
119
+
120
+ mockClient.GET.mockResolvedValueOnce({
121
+ data: mockPromptData,
122
+ error: null,
123
+ });
124
+
125
+ const prompt = await promptService.get("prompt_123");
126
+
127
+ expect(() => {
128
+ prompt.compileStrict({ });
129
+ }).toThrow(PromptCompilationError);
130
+ });
131
+
132
+ it.todo("should create a prompt");
133
+ it.todo("should update a prompt");
134
+ it.todo("should delete a prompt");
135
+ it.todo("should create a prompt version");
136
+ it.todo("should get a prompt version");
137
+ it.todo("should list prompt versions");
138
+ it.todo("should delete a prompt version");
139
+ });
@@ -0,0 +1,49 @@
1
+ import { PromptService } from "./service";
2
+ import { CompiledPrompt, Prompt, TemplateVariables } from "./prompt";
3
+ import * as intSemconv from "../observability/semconv";
4
+ import { tracer } from "./tracer";
5
+ import { canAutomaticallyCaptureInput, canAutomaticallyCaptureOutput } from "../client";
6
+
7
+ export async function getPromptVersion(id: string, versionId: string, variables: TemplateVariables): Promise<CompiledPrompt>;
8
+ export async function getPromptVersion(id: string, versionId: string): Promise<Prompt>;
9
+
10
+ export async function getPromptVersion(id: string, versionId: string, variables?: TemplateVariables): Promise<Prompt | CompiledPrompt> {
11
+ return tracer.withActiveSpan("retrieve prompt version", async (span) => {
12
+ span.setType("prompt");
13
+ span.setAttribute(intSemconv.ATTR_LANGWATCH_PROMPT_ID, id);
14
+
15
+ const service = PromptService.getInstance();
16
+ const prompt = await service.getVersions(id);
17
+ const promptVersion = prompt[versionId];
18
+
19
+ if (!promptVersion) {
20
+ throw new Error(`Prompt version ${versionId} not found for prompt ${id}`);
21
+ }
22
+
23
+ if (canAutomaticallyCaptureOutput()) {
24
+ span.setOutput(prompt);
25
+ }
26
+
27
+ span.setAttributes({
28
+ [intSemconv.ATTR_LANGWATCH_PROMPT_ID]: id,
29
+ [intSemconv.ATTR_LANGWATCH_PROMPT_VERSION_ID]: promptVersion.id,
30
+ [intSemconv.ATTR_LANGWATCH_PROMPT_VERSION_NUMBER]: promptVersion.version,
31
+ });
32
+
33
+ if (variables) {
34
+ if (canAutomaticallyCaptureInput()) {
35
+ span.setAttribute(
36
+ intSemconv.ATTR_LANGWATCH_PROMPT_VARIABLES,
37
+ JSON.stringify({
38
+ type: "json",
39
+ value: variables,
40
+ }),
41
+ );
42
+ }
43
+
44
+ return promptVersion.compile(variables);
45
+ }
46
+
47
+ return promptVersion;
48
+ });
49
+ }
@@ -0,0 +1,44 @@
1
+ import { PromptService } from "./service";
2
+ import { CompiledPrompt, Prompt, TemplateVariables } from "./prompt";
3
+ import * as intSemconv from "../observability/semconv";
4
+ import { tracer } from "./tracer";
5
+ import { canAutomaticallyCaptureInput, canAutomaticallyCaptureOutput } from "../client";
6
+
7
+ export async function getPrompt(id: string, variables: TemplateVariables): Promise<CompiledPrompt>;
8
+ export async function getPrompt(id: string): Promise<Prompt>;
9
+
10
+ export async function getPrompt(id: string, variables?: TemplateVariables): Promise<Prompt | CompiledPrompt> {
11
+ return tracer.withActiveSpan("retrieve prompt", async (span) => {
12
+ span.setType("prompt");
13
+ span.setAttribute(intSemconv.ATTR_LANGWATCH_PROMPT_ID, id);
14
+
15
+ const service = PromptService.getInstance();
16
+ const prompt = await service.get(id);
17
+
18
+ if (canAutomaticallyCaptureOutput()) {
19
+ span.setOutput(prompt);
20
+ }
21
+
22
+ span.setAttributes({
23
+ [intSemconv.ATTR_LANGWATCH_PROMPT_ID]: id,
24
+ [intSemconv.ATTR_LANGWATCH_PROMPT_VERSION_ID]: prompt.versionId,
25
+ [intSemconv.ATTR_LANGWATCH_PROMPT_VERSION_NUMBER]: prompt.version,
26
+ });
27
+
28
+ if (variables) {
29
+ if (canAutomaticallyCaptureInput()) {
30
+ span.setAttribute(
31
+ intSemconv.ATTR_LANGWATCH_PROMPT_VARIABLES,
32
+ JSON.stringify({
33
+ type: "json",
34
+ value: variables,
35
+ }),
36
+ );
37
+ }
38
+
39
+ return prompt.compile(variables);
40
+ }
41
+
42
+ return prompt;
43
+ });
44
+ }
@@ -0,0 +1,3 @@
1
+ export { getPrompt } from "./get-prompt";
2
+ export { getPromptVersion } from "./get-prompt-version";
3
+ export { CompiledPrompt, Prompt, PromptCompilationError, type TemplateVariables } from "./prompt";
@@ -0,0 +1,133 @@
1
+ import { Liquid } from "liquidjs";
2
+ import type { paths } from "../internal/generated/openapi/api-client";
3
+
4
+ // Extract the prompt response type from OpenAPI schema
5
+ export type PromptResponse = NonNullable<
6
+ paths["/api/prompts/{id}"]["get"]["responses"]["200"]["content"]["application/json"]
7
+ >;
8
+
9
+ // Type for template variables - supporting common data types
10
+ export type TemplateVariables = Record<
11
+ string,
12
+ string | number | boolean | object | null
13
+ >;
14
+
15
+ /**
16
+ * Error class for template compilation issues
17
+ */
18
+ export class PromptCompilationError extends Error {
19
+ constructor(
20
+ message: string,
21
+ public readonly template: string,
22
+ public readonly originalError?: any
23
+ ) {
24
+ super(message);
25
+ this.name = "PromptCompilationError";
26
+ }
27
+ }
28
+
29
+ // Global Liquid instance - shared across all prompts for efficiency
30
+ const liquid = new Liquid({
31
+ strictFilters: true,
32
+ });
33
+
34
+ export class Prompt implements PromptResponse {
35
+ public readonly id!: string;
36
+ public readonly name!: string;
37
+ public readonly updatedAt!: string;
38
+ public readonly version!: number;
39
+ public readonly versionId!: string;
40
+ public readonly versionCreatedAt!: string;
41
+ public readonly model!: string;
42
+ public readonly prompt!: string;
43
+ public readonly messages!: PromptResponse["messages"];
44
+ public readonly response_format!: PromptResponse["response_format"];
45
+
46
+ constructor(promptData: PromptResponse) {
47
+ this.id = promptData.id;
48
+ this.name = promptData.name;
49
+ this.updatedAt = promptData.updatedAt;
50
+ this.version = promptData.version;
51
+ this.versionId = promptData.versionId;
52
+ this.versionCreatedAt = promptData.versionCreatedAt;
53
+ this.model = promptData.model;
54
+ this.prompt = promptData.prompt;
55
+ this.messages = promptData.messages;
56
+ this.response_format = promptData.response_format;
57
+ }
58
+
59
+ /**
60
+ * Get the raw prompt data from the API
61
+ */
62
+ get raw(): PromptResponse {
63
+ return this;
64
+ }
65
+
66
+ /**
67
+ * Compile the prompt template with provided variables (lenient - missing variables become empty)
68
+ * @param variables - Object containing variable values for template compilation
69
+ * @returns CompiledPrompt instance with compiled content
70
+ */
71
+ private _compile(variables: TemplateVariables, strict: boolean): CompiledPrompt {
72
+ try {
73
+ // Compile main prompt
74
+ const compiledPrompt = this.prompt
75
+ ? liquid.parseAndRenderSync(this.prompt, variables, {
76
+ strictVariables: strict,
77
+ })
78
+ : "";
79
+
80
+ // Compile messages
81
+ const compiledMessages = (this.messages || []).map((message) => ({
82
+ ...message,
83
+ content: message.content
84
+ ? liquid.parseAndRenderSync(message.content, variables, {
85
+ strictVariables: strict,
86
+ })
87
+ : message.content,
88
+ }));
89
+
90
+ // Create new prompt data with compiled content
91
+ const compiledData: PromptResponse = {
92
+ ...this,
93
+ prompt: compiledPrompt,
94
+ messages: compiledMessages,
95
+ };
96
+
97
+ return new CompiledPrompt(compiledData, this);
98
+ } catch (error) {
99
+ const templateStr = this.prompt || JSON.stringify(this.messages);
100
+ throw new PromptCompilationError(
101
+ `Failed to compile prompt template: ${error instanceof Error ? error.message : "Unknown error"
102
+ }`,
103
+ templateStr,
104
+ error
105
+ );
106
+ }
107
+ }
108
+
109
+ compile(variables: TemplateVariables = {}): CompiledPrompt {
110
+ return this._compile(variables, false);
111
+ }
112
+
113
+ /**
114
+ * Compile with validation - throws error if required variables are missing
115
+ * @param variables - Template variables
116
+ * @returns CompiledPrompt instance with compiled content
117
+ */
118
+ compileStrict(variables: TemplateVariables): CompiledPrompt {
119
+ return this._compile(variables, true);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Represents a compiled prompt that extends Prompt with reference to the original template
125
+ */
126
+ export class CompiledPrompt extends Prompt {
127
+ constructor(
128
+ compiledData: PromptResponse,
129
+ public readonly original: Prompt
130
+ ) {
131
+ super(compiledData);
132
+ }
133
+ }