@lmnr-ai/lmnr 0.4.5 → 0.4.7-alpha

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.
@@ -0,0 +1,357 @@
1
+ /* eslint-disable @typescript-eslint/no-var-requires */
2
+ import { NodeSDK } from "@opentelemetry/sdk-node";
3
+ import {
4
+ SimpleSpanProcessor,
5
+ BatchSpanProcessor,
6
+ SpanProcessor,
7
+ } from "@opentelemetry/sdk-trace-node";
8
+ import { baggageUtils } from "@opentelemetry/core";
9
+ import { Span, context, diag } from "@opentelemetry/api";
10
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
11
+ import { Resource } from "@opentelemetry/resources";
12
+ import { SEMRESATTRS_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
13
+ import { Instrumentation } from "@opentelemetry/instrumentation";
14
+ import { InitializeOptions } from "../interfaces";
15
+ import {
16
+ Detector,
17
+ DetectorSync,
18
+ IResource,
19
+ ResourceDetectionConfig,
20
+ envDetectorSync,
21
+ hostDetectorSync,
22
+ processDetectorSync,
23
+ } from "@opentelemetry/resources"
24
+ import {
25
+ ASSOCIATION_PROPERTIES_KEY,
26
+ } from "./tracing";
27
+ import { Telemetry } from "../telemetry/telemetry";
28
+ import { _configuration } from "../configuration";
29
+ import {
30
+ SpanAttributes,
31
+ } from "@traceloop/ai-semantic-conventions";
32
+ import { AnthropicInstrumentation } from "@traceloop/instrumentation-anthropic";
33
+ import { OpenAIInstrumentation } from "@traceloop/instrumentation-openai";
34
+ import { AzureOpenAIInstrumentation } from "@traceloop/instrumentation-azure";
35
+ import { LlamaIndexInstrumentation } from "@traceloop/instrumentation-llamaindex";
36
+ import {
37
+ AIPlatformInstrumentation,
38
+ VertexAIInstrumentation,
39
+ } from "@traceloop/instrumentation-vertexai";
40
+ import { BedrockInstrumentation } from "@traceloop/instrumentation-bedrock";
41
+ import { CohereInstrumentation } from "@traceloop/instrumentation-cohere";
42
+ import { PineconeInstrumentation } from "@traceloop/instrumentation-pinecone";
43
+ import { LangChainInstrumentation } from "@traceloop/instrumentation-langchain";
44
+ import { ChromaDBInstrumentation } from "@traceloop/instrumentation-chromadb";
45
+ import { QdrantInstrumentation } from "@traceloop/instrumentation-qdrant";
46
+
47
+ let _sdk: NodeSDK;
48
+ let _spanProcessor: SimpleSpanProcessor | BatchSpanProcessor;
49
+ let openAIInstrumentation: OpenAIInstrumentation | undefined;
50
+ let anthropicInstrumentation: AnthropicInstrumentation | undefined;
51
+ let azureOpenAIInstrumentation: AzureOpenAIInstrumentation | undefined;
52
+ let cohereInstrumentation: CohereInstrumentation | undefined;
53
+ let vertexaiInstrumentation: VertexAIInstrumentation | undefined;
54
+ let aiplatformInstrumentation: AIPlatformInstrumentation | undefined;
55
+ let bedrockInstrumentation: BedrockInstrumentation | undefined;
56
+ let langchainInstrumentation: LangChainInstrumentation | undefined;
57
+ let llamaIndexInstrumentation: LlamaIndexInstrumentation | undefined;
58
+ let pineconeInstrumentation: PineconeInstrumentation | undefined;
59
+ let chromadbInstrumentation: ChromaDBInstrumentation | undefined;
60
+ let qdrantInstrumentation: QdrantInstrumentation | undefined;
61
+
62
+
63
+ const instrumentations: Instrumentation[] = [];
64
+
65
+ const initInstrumentations = () => {
66
+ const exceptionLogger = (e: Error) => Telemetry.getInstance().logException(e);
67
+ const enrichTokens =
68
+ (process.env.TRACELOOP_ENRICH_TOKENS || "true").toLowerCase() === "true";
69
+
70
+ openAIInstrumentation = new OpenAIInstrumentation({
71
+ enrichTokens,
72
+ exceptionLogger,
73
+ });
74
+ instrumentations.push(openAIInstrumentation);
75
+
76
+ anthropicInstrumentation = new AnthropicInstrumentation({ exceptionLogger });
77
+ instrumentations.push(anthropicInstrumentation);
78
+
79
+ azureOpenAIInstrumentation = new AzureOpenAIInstrumentation({
80
+ exceptionLogger,
81
+ });
82
+ instrumentations.push(azureOpenAIInstrumentation);
83
+
84
+ cohereInstrumentation = new CohereInstrumentation({ exceptionLogger });
85
+ instrumentations.push(cohereInstrumentation);
86
+
87
+ vertexaiInstrumentation = new VertexAIInstrumentation({
88
+ exceptionLogger,
89
+ });
90
+ instrumentations.push(vertexaiInstrumentation);
91
+
92
+ aiplatformInstrumentation = new AIPlatformInstrumentation({
93
+ exceptionLogger,
94
+ });
95
+ instrumentations.push(aiplatformInstrumentation);
96
+
97
+ bedrockInstrumentation = new BedrockInstrumentation({ exceptionLogger });
98
+ instrumentations.push(bedrockInstrumentation);
99
+
100
+ pineconeInstrumentation = new PineconeInstrumentation({ exceptionLogger });
101
+ instrumentations.push(pineconeInstrumentation);
102
+
103
+ langchainInstrumentation = new LangChainInstrumentation({ exceptionLogger });
104
+ instrumentations.push(langchainInstrumentation);
105
+
106
+ llamaIndexInstrumentation = new LlamaIndexInstrumentation({
107
+ exceptionLogger,
108
+ });
109
+ instrumentations.push(llamaIndexInstrumentation);
110
+
111
+ chromadbInstrumentation = new ChromaDBInstrumentation({ exceptionLogger });
112
+ instrumentations.push(chromadbInstrumentation);
113
+
114
+ qdrantInstrumentation = new QdrantInstrumentation({ exceptionLogger });
115
+ instrumentations.push(qdrantInstrumentation);
116
+ };
117
+
118
+ const manuallyInitInstrumentations = (
119
+ instrumentModules: InitializeOptions["instrumentModules"],
120
+ ) => {
121
+ const exceptionLogger = (e: Error) => Telemetry.getInstance().logException(e);
122
+ const enrichTokens =
123
+ (process.env.TRACELOOP_ENRICH_TOKENS || "true").toLowerCase() === "true";
124
+
125
+ instrumentations.length = 0;
126
+
127
+ if (instrumentModules?.openAI) {
128
+ openAIInstrumentation = new OpenAIInstrumentation({
129
+ enrichTokens,
130
+ exceptionLogger,
131
+ });
132
+ instrumentations.push(openAIInstrumentation);
133
+ openAIInstrumentation.manuallyInstrument(instrumentModules.openAI);
134
+ }
135
+
136
+ if (instrumentModules?.anthropic) {
137
+ anthropicInstrumentation = new AnthropicInstrumentation({
138
+ exceptionLogger,
139
+ });
140
+ instrumentations.push(anthropicInstrumentation);
141
+ anthropicInstrumentation.manuallyInstrument(instrumentModules.anthropic);
142
+ }
143
+
144
+ if (instrumentModules?.azureOpenAI) {
145
+ const instrumentation = new AzureOpenAIInstrumentation({ exceptionLogger });
146
+ instrumentations.push(instrumentation as Instrumentation);
147
+ azureOpenAIInstrumentation = instrumentation;
148
+ instrumentation.manuallyInstrument(instrumentModules.azureOpenAI);
149
+ }
150
+
151
+ if (instrumentModules?.cohere) {
152
+ cohereInstrumentation = new CohereInstrumentation({ exceptionLogger });
153
+ instrumentations.push(cohereInstrumentation);
154
+ cohereInstrumentation.manuallyInstrument(instrumentModules.cohere);
155
+ }
156
+
157
+ if (instrumentModules?.google_vertexai) {
158
+ vertexaiInstrumentation = new VertexAIInstrumentation({
159
+ exceptionLogger,
160
+ });
161
+ instrumentations.push(vertexaiInstrumentation);
162
+ vertexaiInstrumentation.manuallyInstrument(
163
+ instrumentModules.google_vertexai,
164
+ );
165
+ }
166
+
167
+ if (instrumentModules?.google_aiplatform) {
168
+ aiplatformInstrumentation = new AIPlatformInstrumentation({
169
+ exceptionLogger,
170
+ });
171
+ instrumentations.push(aiplatformInstrumentation);
172
+ aiplatformInstrumentation.manuallyInstrument(
173
+ instrumentModules.google_aiplatform,
174
+ );
175
+ }
176
+
177
+ if (instrumentModules?.bedrock) {
178
+ bedrockInstrumentation = new BedrockInstrumentation({ exceptionLogger });
179
+ instrumentations.push(bedrockInstrumentation);
180
+ bedrockInstrumentation.manuallyInstrument(instrumentModules.bedrock);
181
+ }
182
+
183
+ if (instrumentModules?.pinecone) {
184
+ const instrumentation = new PineconeInstrumentation({ exceptionLogger });
185
+ instrumentations.push(instrumentation as Instrumentation);
186
+ instrumentation.manuallyInstrument(instrumentModules.pinecone);
187
+ }
188
+
189
+ if (instrumentModules?.langchain) {
190
+ langchainInstrumentation = new LangChainInstrumentation({
191
+ exceptionLogger,
192
+ });
193
+ instrumentations.push(langchainInstrumentation);
194
+ langchainInstrumentation.manuallyInstrument(instrumentModules.langchain);
195
+ }
196
+
197
+ if (instrumentModules?.llamaIndex) {
198
+ llamaIndexInstrumentation = new LlamaIndexInstrumentation({
199
+ exceptionLogger,
200
+ });
201
+ instrumentations.push(llamaIndexInstrumentation);
202
+ llamaIndexInstrumentation.manuallyInstrument(instrumentModules.llamaIndex);
203
+ }
204
+
205
+ if (instrumentModules?.chromadb) {
206
+ chromadbInstrumentation = new ChromaDBInstrumentation({ exceptionLogger });
207
+ instrumentations.push(chromadbInstrumentation);
208
+ chromadbInstrumentation.manuallyInstrument(instrumentModules.chromadb);
209
+ }
210
+
211
+ if (instrumentModules?.qdrant) {
212
+ qdrantInstrumentation = new QdrantInstrumentation({ exceptionLogger });
213
+ instrumentations.push(qdrantInstrumentation);
214
+ qdrantInstrumentation.manuallyInstrument(instrumentModules.qdrant);
215
+ }
216
+ };
217
+
218
+ function awaitAttributes(detector: DetectorSync): Detector {
219
+ return {
220
+ async detect(config?: ResourceDetectionConfig): Promise<IResource> {
221
+ const resource = detector.detect(config)
222
+ await resource.waitForAsyncAttributes?.()
223
+
224
+ return resource
225
+ },
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Initializes the Traceloop SDK.
231
+ * Must be called once before any other SDK methods.
232
+ *
233
+ * @param options - The options to initialize the SDK. See the {@link InitializeOptions} for details.
234
+ */
235
+ export const startTracing = (options: InitializeOptions) => {
236
+ if (options.instrumentModules !== undefined) {
237
+ // If options.instrumentModules is empty, it will not initialize anything,
238
+ // so empty dict can essentially be passed to disable any kind of automatic instrumentation.
239
+ manuallyInitInstrumentations(options.instrumentModules);
240
+ } else {
241
+ initInstrumentations();
242
+ }
243
+
244
+ if (!shouldSendTraces()) {
245
+ openAIInstrumentation?.setConfig({
246
+ traceContent: false,
247
+ });
248
+ azureOpenAIInstrumentation?.setConfig({
249
+ traceContent: false,
250
+ });
251
+ llamaIndexInstrumentation?.setConfig({
252
+ traceContent: false,
253
+ });
254
+ vertexaiInstrumentation?.setConfig({
255
+ traceContent: false,
256
+ });
257
+ aiplatformInstrumentation?.setConfig({
258
+ traceContent: false,
259
+ });
260
+ bedrockInstrumentation?.setConfig({
261
+ traceContent: false,
262
+ });
263
+ cohereInstrumentation?.setConfig({
264
+ traceContent: false,
265
+ });
266
+ chromadbInstrumentation?.setConfig({
267
+ traceContent: false,
268
+ });
269
+ }
270
+
271
+ const headers = process.env.TRACELOOP_HEADERS
272
+ ? baggageUtils.parseKeyPairsIntoRecord(process.env.TRACELOOP_HEADERS)
273
+ : { Authorization: `Bearer ${options.apiKey}` };
274
+
275
+ const traceExporter =
276
+ options.exporter ??
277
+ new OTLPTraceExporter({
278
+ url: `${options.baseUrl}/v1/traces`,
279
+ headers,
280
+ });
281
+ _spanProcessor = options.disableBatch
282
+ ? new SimpleSpanProcessor(traceExporter)
283
+ : new BatchSpanProcessor(traceExporter);
284
+
285
+ _spanProcessor.onStart = (span: Span) => {
286
+ // This sets the properties only if the context has them
287
+ const associationProperties = context
288
+ .active()
289
+ .getValue(ASSOCIATION_PROPERTIES_KEY);
290
+ if (associationProperties) {
291
+ for (const [key, value] of Object.entries(associationProperties)) {
292
+ span.setAttribute(
293
+ `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.${key}`,
294
+ value,
295
+ );
296
+ }
297
+ }
298
+ };
299
+
300
+ if (options.exporter) {
301
+ Telemetry.getInstance().capture("tracer:init", {
302
+ exporter: "custom",
303
+ processor: options.disableBatch ? "simple" : "batch",
304
+ });
305
+ } else {
306
+ Telemetry.getInstance().capture("tracer:init", {
307
+ exporter: options.baseUrl ?? "",
308
+ processor: options.disableBatch ? "simple" : "batch",
309
+ });
310
+ }
311
+
312
+ const spanProcessors: SpanProcessor[] = [_spanProcessor];
313
+ if (options.processor) {
314
+ spanProcessors.push(options.processor);
315
+ }
316
+
317
+ _sdk = new NodeSDK({
318
+ resource: new Resource({
319
+ [SEMRESATTRS_SERVICE_NAME]:
320
+ options.appName || process.env.npm_package_name,
321
+ }),
322
+ spanProcessors,
323
+ contextManager: options.contextManager,
324
+ textMapPropagator: options.propagator,
325
+ traceExporter,
326
+ instrumentations,
327
+ // We should re-consider removing irrelevant spans here in the future
328
+ // sampler: new TraceloopSampler(),
329
+ resourceDetectors: [
330
+ awaitAttributes(envDetectorSync),
331
+ awaitAttributes(processDetectorSync),
332
+ awaitAttributes(hostDetectorSync),
333
+ ],
334
+ });
335
+
336
+ _sdk.start();
337
+ };
338
+
339
+ export const shouldSendTraces = () => {
340
+ if (!_configuration) {
341
+ diag.warn("Traceloop not initialized");
342
+ return false;
343
+ }
344
+
345
+ if (
346
+ _configuration.traceContent === false ||
347
+ (process.env.TRACELOOP_TRACE_CONTENT || "true").toLowerCase() === "false"
348
+ ) {
349
+ return false;
350
+ }
351
+
352
+ return true;
353
+ };
354
+
355
+ export const forceFlush = async () => {
356
+ await _spanProcessor.forceFlush();
357
+ };
@@ -0,0 +1,179 @@
1
+ import { Span, context, trace } from "@opentelemetry/api";
2
+ import { getTracer } from "./tracing";
3
+ import {
4
+ Events,
5
+ EventAttributes,
6
+ SpanAttributes,
7
+ } from "@traceloop/ai-semantic-conventions";
8
+ import { shouldSendTraces } from ".";
9
+
10
+ type VectorDBCallConfig = {
11
+ vendor: string;
12
+ type: "query" | "upsert" | "delete";
13
+ };
14
+
15
+ type LLMCallConfig = {
16
+ vendor: string;
17
+ type: "chat" | "completion";
18
+ };
19
+
20
+ export class VectorSpan {
21
+ private span: Span;
22
+
23
+ constructor(span: Span) {
24
+ this.span = span;
25
+ }
26
+
27
+ reportQuery({ queryVector }: { queryVector: number[] }) {
28
+ if (!shouldSendTraces()) {
29
+ this.span.addEvent(Events.DB_QUERY_EMBEDDINGS);
30
+ }
31
+
32
+ this.span.addEvent(Events.DB_QUERY_EMBEDDINGS, {
33
+ [EventAttributes.DB_QUERY_EMBEDDINGS_VECTOR]: JSON.stringify(queryVector),
34
+ });
35
+ }
36
+
37
+ reportResults({
38
+ results,
39
+ }: {
40
+ results: {
41
+ ids?: string;
42
+ scores?: number;
43
+ distances?: number;
44
+ metadata?: Record<string, unknown>;
45
+ vectors?: number[];
46
+ documents?: string;
47
+ }[];
48
+ }) {
49
+ for (let i = 0; i < results.length; i++) {
50
+ this.span.addEvent(Events.DB_QUERY_RESULT, {
51
+ [EventAttributes.DB_QUERY_RESULT_ID]: results[i].ids,
52
+ [EventAttributes.DB_QUERY_RESULT_SCORE]: results[i].scores,
53
+ [EventAttributes.DB_QUERY_RESULT_DISTANCE]: results[i].distances,
54
+ [EventAttributes.DB_QUERY_RESULT_METADATA]: JSON.stringify(
55
+ results[i].metadata,
56
+ ),
57
+ [EventAttributes.DB_QUERY_RESULT_VECTOR]: results[i].vectors,
58
+ [EventAttributes.DB_QUERY_RESULT_DOCUMENT]: results[i].documents,
59
+ });
60
+ }
61
+ }
62
+ }
63
+
64
+ export class LLMSpan {
65
+ private span: Span;
66
+
67
+ constructor(span: Span) {
68
+ this.span = span;
69
+ }
70
+
71
+ reportRequest({
72
+ model,
73
+ messages,
74
+ }: {
75
+ model: string;
76
+ messages: {
77
+ role: string;
78
+ content?: string | unknown;
79
+ }[];
80
+ }) {
81
+ this.span.setAttributes({
82
+ [SpanAttributes.LLM_REQUEST_MODEL]: model,
83
+ });
84
+
85
+ messages.forEach((message, index) => {
86
+ this.span.setAttributes({
87
+ [`${SpanAttributes.LLM_PROMPTS}.${index}.role`]: message.role,
88
+ [`${SpanAttributes.LLM_PROMPTS}.${index}.content`]:
89
+ typeof message.content === "string"
90
+ ? message.content
91
+ : JSON.stringify(message.content),
92
+ });
93
+ });
94
+ }
95
+
96
+ reportResponse({
97
+ model,
98
+ usage,
99
+ completions,
100
+ }: {
101
+ model: string;
102
+ usage?: {
103
+ prompt_tokens: number;
104
+ completion_tokens: number;
105
+ total_tokens: number;
106
+ };
107
+ completions?: {
108
+ finish_reason: string;
109
+ message: {
110
+ role: "system" | "user" | "assistant";
111
+ content: string | null;
112
+ };
113
+ }[];
114
+ }) {
115
+ this.span.setAttribute(SpanAttributes.LLM_RESPONSE_MODEL, model);
116
+
117
+ if (usage) {
118
+ this.span.setAttributes({
119
+ [SpanAttributes.LLM_USAGE_PROMPT_TOKENS]: usage.prompt_tokens,
120
+ [SpanAttributes.LLM_USAGE_COMPLETION_TOKENS]: usage.completion_tokens,
121
+ [SpanAttributes.LLM_USAGE_TOTAL_TOKENS]: usage.total_tokens,
122
+ });
123
+ }
124
+
125
+ completions?.forEach((completion, index) => {
126
+ this.span.setAttributes({
127
+ [`${SpanAttributes.LLM_COMPLETIONS}.${index}.finish_reason`]:
128
+ completion.finish_reason,
129
+ [`${SpanAttributes.LLM_COMPLETIONS}.${index}.role`]:
130
+ completion.message.role,
131
+ [`${SpanAttributes.LLM_COMPLETIONS}.${index}.content`]:
132
+ completion.message.content || "",
133
+ });
134
+ });
135
+ }
136
+ }
137
+
138
+ export function withVectorDBCall<
139
+ F extends ({ span }: { span: VectorSpan }) => ReturnType<F>,
140
+ >({ vendor, type }: VectorDBCallConfig, fn: F, thisArg?: ThisParameterType<F>) {
141
+ const entityContext = context.active();
142
+
143
+ return getTracer().startActiveSpan(
144
+ `${vendor}.${type}`,
145
+ { [SpanAttributes.LLM_REQUEST_TYPE]: type },
146
+ entityContext,
147
+ (span: Span) => {
148
+ const res = fn.apply(thisArg, [{ span: new VectorSpan(span) }]);
149
+ if (res instanceof Promise) {
150
+ return res.then((resolvedRes) => {
151
+ span.end();
152
+ return resolvedRes;
153
+ });
154
+ }
155
+
156
+ span.end();
157
+ return res;
158
+ },
159
+ );
160
+ }
161
+
162
+ export function withLLMCall<
163
+ F extends ({ span }: { span: LLMSpan }) => ReturnType<F>,
164
+ >({ vendor, type }: LLMCallConfig, fn: F, thisArg?: ThisParameterType<F>) {
165
+ const span = getTracer().startSpan(`${vendor}.${type}`, {}, context.active());
166
+ span.setAttribute(SpanAttributes.LLM_REQUEST_TYPE, type);
167
+ trace.setSpan(context.active(), span);
168
+
169
+ const res = fn.apply(thisArg, [{ span: new LLMSpan(span) }]);
170
+ if (res instanceof Promise) {
171
+ return res.then((resolvedRes) => {
172
+ span.end();
173
+ return resolvedRes;
174
+ });
175
+ }
176
+
177
+ span.end();
178
+ return res;
179
+ }
@@ -0,0 +1,35 @@
1
+ import {
2
+ Sampler,
3
+ SamplingResult,
4
+ SamplingDecision,
5
+ } from "@opentelemetry/sdk-trace-base";
6
+ import { Context, SpanKind, Attributes } from "@opentelemetry/api";
7
+
8
+ const FILTERED_ATTRIBUTE_KEYS = ["next.span_name"];
9
+
10
+ export class TraceloopSampler implements Sampler {
11
+ shouldSample(
12
+ _context: Context,
13
+ _traceId: string,
14
+ _spanName: string,
15
+ _spanKind: SpanKind,
16
+ attributes: Attributes,
17
+ ): SamplingResult {
18
+ let filter = false;
19
+ FILTERED_ATTRIBUTE_KEYS.forEach((key) => {
20
+ if (attributes?.[key]) {
21
+ filter = true;
22
+ }
23
+ });
24
+
25
+ return {
26
+ decision: filter
27
+ ? SamplingDecision.NOT_RECORD
28
+ : SamplingDecision.RECORD_AND_SAMPLED,
29
+ };
30
+ }
31
+
32
+ toString(): string {
33
+ return "TraceloopSampler";
34
+ }
35
+ }
@@ -0,0 +1,19 @@
1
+ import { trace, createContextKey } from "@opentelemetry/api";
2
+ import { Context } from "@opentelemetry/api/build/src/context/types";
3
+
4
+ const TRACER_NAME = "traceloop.tracer";
5
+ export const WORKFLOW_NAME_KEY = createContextKey("workflow_name");
6
+ export const ENTITY_NAME_KEY = createContextKey("entity_name");
7
+ export const ASSOCIATION_PROPERTIES_KEY = createContextKey(
8
+ "association_properties",
9
+ );
10
+
11
+ export const getTracer = () => {
12
+ return trace.getTracer(TRACER_NAME);
13
+ };
14
+
15
+ export const getEntityPath = (entityContext: Context): string | undefined => {
16
+ const path = entityContext.getValue(ENTITY_NAME_KEY);
17
+
18
+ return path ? `${path}` : undefined;
19
+ };