@arizeai/openinference-genai 0.1.0

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 (34) hide show
  1. package/README.md +137 -0
  2. package/dist/esm/__generated__/opentelemetryInputMessages.d.ts +103 -0
  3. package/dist/esm/__generated__/opentelemetryInputMessages.d.ts.map +1 -0
  4. package/dist/esm/__generated__/opentelemetryInputMessages.js +7 -0
  5. package/dist/esm/__generated__/opentelemetryInputMessages.js.map +1 -0
  6. package/dist/esm/__generated__/opentelemetryOutputMessages.d.ts +116 -0
  7. package/dist/esm/__generated__/opentelemetryOutputMessages.d.ts.map +1 -0
  8. package/dist/esm/__generated__/opentelemetryOutputMessages.js +7 -0
  9. package/dist/esm/__generated__/opentelemetryOutputMessages.js.map +1 -0
  10. package/dist/esm/attributes.d.ts +79 -0
  11. package/dist/esm/attributes.d.ts.map +1 -0
  12. package/dist/esm/attributes.js +306 -0
  13. package/dist/esm/attributes.js.map +1 -0
  14. package/dist/esm/index.d.ts +2 -0
  15. package/dist/esm/index.d.ts.map +1 -0
  16. package/dist/esm/index.js +10 -0
  17. package/dist/esm/index.js.map +1 -0
  18. package/dist/esm/package.json +1 -0
  19. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -0
  20. package/dist/esm/types.d.ts +9 -0
  21. package/dist/esm/types.d.ts.map +1 -0
  22. package/dist/esm/types.js +2 -0
  23. package/dist/esm/types.js.map +1 -0
  24. package/dist/esm/utils.d.ts +42 -0
  25. package/dist/esm/utils.d.ts.map +1 -0
  26. package/dist/esm/utils.js +94 -0
  27. package/dist/esm/utils.js.map +1 -0
  28. package/package.json +68 -0
  29. package/src/__generated__/opentelemetryInputMessages.ts +104 -0
  30. package/src/__generated__/opentelemetryOutputMessages.ts +122 -0
  31. package/src/attributes.ts +467 -0
  32. package/src/index.ts +14 -0
  33. package/src/types.ts +14 -0
  34. package/src/utils.ts +106 -0
@@ -0,0 +1,467 @@
1
+ import type { Attributes } from "@opentelemetry/api";
2
+ import {
3
+ OpenInferenceSpanKind,
4
+ SemanticConventions,
5
+ } from "@arizeai/openinference-semantic-conventions";
6
+ import {
7
+ ATTR_GEN_AI_PROVIDER_NAME,
8
+ ATTR_GEN_AI_REQUEST_MODEL,
9
+ ATTR_GEN_AI_RESPONSE_MODEL,
10
+ ATTR_GEN_AI_REQUEST_MAX_TOKENS,
11
+ ATTR_GEN_AI_REQUEST_TEMPERATURE,
12
+ ATTR_GEN_AI_REQUEST_TOP_P,
13
+ ATTR_GEN_AI_REQUEST_TOP_K,
14
+ ATTR_GEN_AI_REQUEST_PRESENCE_PENALTY,
15
+ ATTR_GEN_AI_REQUEST_FREQUENCY_PENALTY,
16
+ ATTR_GEN_AI_REQUEST_STOP_SEQUENCES,
17
+ ATTR_GEN_AI_REQUEST_SEED,
18
+ ATTR_GEN_AI_INPUT_MESSAGES,
19
+ ATTR_GEN_AI_OUTPUT_MESSAGES,
20
+ ATTR_GEN_AI_USAGE_INPUT_TOKENS,
21
+ ATTR_GEN_AI_USAGE_OUTPUT_TOKENS,
22
+ ATTR_GEN_AI_PROMPT,
23
+ ATTR_GEN_AI_COMPLETION,
24
+ ATTR_GEN_AI_AGENT_ID,
25
+ ATTR_GEN_AI_AGENT_NAME,
26
+ ATTR_GEN_AI_AGENT_DESCRIPTION,
27
+ ATTR_GEN_AI_TOOL_NAME,
28
+ ATTR_GEN_AI_TOOL_DESCRIPTION,
29
+ ATTR_GEN_AI_TOOL_CALL_ID,
30
+ ATTR_GEN_AI_TOOL_TYPE,
31
+ } from "@opentelemetry/semantic-conventions/incubating";
32
+
33
+ import {
34
+ getMimeType,
35
+ getNumber,
36
+ getString,
37
+ getStringArray,
38
+ merge,
39
+ safelyParseJSON,
40
+ safelyJSONStringify,
41
+ set,
42
+ toStringContent,
43
+ } from "./utils.js";
44
+ import type {
45
+ ChatMessage,
46
+ GenericPart,
47
+ } from "./__generated__/opentelemetryInputMessages.js";
48
+ import type { OutputMessage } from "./__generated__/opentelemetryOutputMessages.js";
49
+
50
+ export type GenAIInputMessage = ChatMessage;
51
+ export type GenAIInputMessagePart = ChatMessage["parts"][number];
52
+ export type GenAIOutputMessage = OutputMessage & {
53
+ /** @deprecated use parts instead */
54
+ text?: string;
55
+ };
56
+ export type GenAIOutputMessagePart = OutputMessage["parts"][number];
57
+
58
+ const AGENT_KIND_PREFIXES = [
59
+ ATTR_GEN_AI_AGENT_ID,
60
+ ATTR_GEN_AI_AGENT_NAME,
61
+ ATTR_GEN_AI_AGENT_DESCRIPTION,
62
+ ] as const;
63
+
64
+ const TOOL_EXECUTION_PREFIXES = [
65
+ ATTR_GEN_AI_TOOL_NAME,
66
+ ATTR_GEN_AI_TOOL_DESCRIPTION,
67
+ ATTR_GEN_AI_TOOL_CALL_ID,
68
+ ATTR_GEN_AI_TOOL_TYPE,
69
+ ] as const;
70
+
71
+ // Shared part parsing
72
+ type AnyPart = GenAIInputMessagePart | GenAIOutputMessagePart;
73
+
74
+ /**
75
+ * Type guard for a GenAI chat message
76
+ * @param value - The value to check
77
+ * @returns True if the value is a chat message, false otherwise
78
+ */
79
+ const isGenAIChatMessage = (value: unknown): value is ChatMessage => {
80
+ if (typeof value !== "object" || value === null) return false;
81
+ if (!("role" in value) || !("parts" in value)) return false;
82
+ if (typeof value.role !== "string" || !Array.isArray(value.parts))
83
+ return false;
84
+ if (!value.parts || !Array.isArray(value.parts)) return false;
85
+ return true;
86
+ };
87
+
88
+ /**
89
+ * Process genai message parts into openinference attributes
90
+ *
91
+ * @param params - The parameters to process the message parts
92
+ */
93
+ const processMessageParts = ({
94
+ attrs,
95
+ msgPrefix,
96
+ parts,
97
+ }: {
98
+ /** The attributes to mutate with new message attributes */
99
+ attrs: Attributes;
100
+ /** The prefix to add to the attributes */
101
+ msgPrefix: string;
102
+ /** The parts to process */
103
+ parts: AnyPart[] | undefined;
104
+ }): void => {
105
+ if (!Array.isArray(parts) || parts.length === 0) return;
106
+
107
+ // track the index of the content and tool calls outside the loop
108
+ // this is just in-case we have to skip bad parts
109
+ let contentIndex = 0;
110
+ let toolIndex = 0;
111
+
112
+ for (const part of parts) {
113
+ if (!part || typeof part !== "object") continue;
114
+ if (!part.type) continue;
115
+
116
+ switch (part.type) {
117
+ case "text": {
118
+ const text = toStringContent(part.content);
119
+ if (text !== undefined) {
120
+ // MESSAGE_CONTENTS entries
121
+ const contentPrefix = `${msgPrefix}${SemanticConventions.MESSAGE_CONTENTS}.${contentIndex}.`;
122
+ set(
123
+ attrs,
124
+ `${contentPrefix}${SemanticConventions.MESSAGE_CONTENT_TYPE}`,
125
+ "text",
126
+ );
127
+ set(
128
+ attrs,
129
+ `${contentPrefix}${SemanticConventions.MESSAGE_CONTENT_TEXT}`,
130
+ text,
131
+ );
132
+ contentIndex += 1;
133
+ }
134
+ continue;
135
+ }
136
+ case "tool_call": {
137
+ const id = part.id ?? undefined;
138
+ const name = part.name;
139
+ const args = part.arguments ?? {};
140
+ const toolPrefix = `${msgPrefix}${SemanticConventions.MESSAGE_TOOL_CALLS}.${toolIndex}.`;
141
+ set(attrs, `${toolPrefix}${SemanticConventions.TOOL_CALL_ID}`, id);
142
+ set(
143
+ attrs,
144
+ toolPrefix + SemanticConventions.TOOL_CALL_FUNCTION_NAME,
145
+ name,
146
+ );
147
+ set(
148
+ attrs,
149
+ toolPrefix + SemanticConventions.TOOL_CALL_FUNCTION_ARGUMENTS_JSON,
150
+ safelyJSONStringify(args),
151
+ );
152
+ toolIndex += 1;
153
+ continue;
154
+ }
155
+ case "tool_call_response": {
156
+ const id = part.id ?? undefined;
157
+ const response = toStringContent(part.response);
158
+
159
+ set(
160
+ attrs,
161
+ `${msgPrefix}${SemanticConventions.MESSAGE_TOOL_CALL_ID}`,
162
+ id,
163
+ );
164
+ const contentPrefix = `${msgPrefix}${SemanticConventions.MESSAGE_CONTENTS}.${contentIndex}.`;
165
+ set(
166
+ attrs,
167
+ `${contentPrefix}${SemanticConventions.MESSAGE_CONTENT_TYPE}`,
168
+ "text",
169
+ );
170
+ set(
171
+ attrs,
172
+ `${contentPrefix}${SemanticConventions.MESSAGE_CONTENT_TEXT}`,
173
+ response,
174
+ );
175
+ contentIndex += 1;
176
+ continue;
177
+ }
178
+ default: {
179
+ // Generic / unknown part type: capture as JSON text content
180
+ const genericPart = part as GenericPart;
181
+ const genericText = toStringContent(genericPart);
182
+ const contentPrefix = `${msgPrefix}${SemanticConventions.MESSAGE_CONTENTS}.${contentIndex}.`;
183
+ set(
184
+ attrs,
185
+ `${contentPrefix}${SemanticConventions.MESSAGE_CONTENT_TYPE}`,
186
+ genericPart.type,
187
+ );
188
+ set(
189
+ attrs,
190
+ `${contentPrefix}${SemanticConventions.MESSAGE_CONTENT_TEXT}`,
191
+ genericText,
192
+ );
193
+ set(
194
+ attrs,
195
+ `${msgPrefix}${SemanticConventions.MESSAGE_CONTENT}`,
196
+ genericText,
197
+ );
198
+ contentIndex += 1;
199
+ }
200
+ }
201
+ }
202
+ };
203
+
204
+ /**
205
+ * Convert GenAI span attributes to OpenInference span attributes
206
+ * @param spanAttributes - The span attributes containing GenAI span attributes to convert
207
+ * @returns The converted OpenInference span attributes
208
+ */
209
+ export const convertGenAISpanAttributesToOpenInferenceSpanAttributes = (
210
+ spanAttributes: Attributes,
211
+ ): Attributes => {
212
+ return merge(
213
+ mapProviderAndSystem(spanAttributes),
214
+ mapModels(spanAttributes),
215
+ mapSpanKind(spanAttributes),
216
+ mapInvocationParameters(spanAttributes),
217
+ mapInputMessages(spanAttributes),
218
+ mapOutputMessages(spanAttributes),
219
+ mapTokenCounts(spanAttributes),
220
+ mapToolExecution(spanAttributes),
221
+ mapInputValue(spanAttributes),
222
+ mapOutputValue(spanAttributes),
223
+ );
224
+ };
225
+
226
+ /**
227
+ * Map provider and system to openinference attributes
228
+ * @todo add some heuristics that can map incoming provider names to the correct OpenInference provider name
229
+ * @param spanAttributes - The span attributes containing provider and system to map
230
+ * @returns The mapped provider and system attributes
231
+ */
232
+ export const mapProviderAndSystem = (
233
+ spanAttributes: Attributes,
234
+ ): Attributes => {
235
+ const attrs: Attributes = {};
236
+ const provider = getString(spanAttributes[ATTR_GEN_AI_PROVIDER_NAME]);
237
+ set(attrs, SemanticConventions.LLM_PROVIDER, provider);
238
+ return attrs;
239
+ };
240
+
241
+ /**
242
+ * Map model name to openinference attributes
243
+ * @param spanAttributes - The span attributes containing model name to map
244
+ * @returns The mapped model name attributes
245
+ */
246
+ export const mapModels = (spanAttributes: Attributes): Attributes => {
247
+ const attrs: Attributes = {};
248
+ const requestModel = getString(spanAttributes[ATTR_GEN_AI_REQUEST_MODEL]);
249
+ const responseModel = getString(spanAttributes[ATTR_GEN_AI_RESPONSE_MODEL]);
250
+ const modelName = responseModel ?? requestModel;
251
+ set(attrs, SemanticConventions.LLM_MODEL_NAME, modelName);
252
+ return attrs;
253
+ };
254
+
255
+ /**
256
+ * Map span kind to openinference attributes
257
+ * @param spanAttributes - The span attributes containing span kind to map
258
+ * @returns The mapped span kind attributes
259
+ */
260
+ export const mapSpanKind = (spanAttributes: Attributes): Attributes => {
261
+ const attrs: Attributes = {};
262
+ // default to LLM for now
263
+ let spanKind = OpenInferenceSpanKind.LLM;
264
+ // detect agent kind
265
+ if (AGENT_KIND_PREFIXES.some((prefix) => spanAttributes[prefix])) {
266
+ spanKind = OpenInferenceSpanKind.AGENT;
267
+ }
268
+ // detect tool execution kind
269
+ if (TOOL_EXECUTION_PREFIXES.some((prefix) => spanAttributes[prefix])) {
270
+ spanKind = OpenInferenceSpanKind.TOOL;
271
+ }
272
+
273
+ set(attrs, SemanticConventions.OPENINFERENCE_SPAN_KIND, spanKind);
274
+
275
+ return attrs;
276
+ };
277
+
278
+ /**
279
+ * Map invocation parameters to openinference attributes
280
+ * @param spanAttributes - The span attributes containing invocation parameters to map
281
+ * @returns The mapped invocation parameters attributes
282
+ */
283
+ export const mapInvocationParameters = (
284
+ spanAttributes: Attributes,
285
+ ): Attributes => {
286
+ const attrs: Attributes = {};
287
+ const requestModel = getString(spanAttributes[ATTR_GEN_AI_REQUEST_MODEL]);
288
+ const maxTokens = getNumber(spanAttributes[ATTR_GEN_AI_REQUEST_MAX_TOKENS]);
289
+ const temperature = getNumber(
290
+ spanAttributes[ATTR_GEN_AI_REQUEST_TEMPERATURE],
291
+ );
292
+ const topP = getNumber(spanAttributes[ATTR_GEN_AI_REQUEST_TOP_P]);
293
+ const topK = getNumber(spanAttributes[ATTR_GEN_AI_REQUEST_TOP_K]);
294
+ const presencePenalty = getNumber(
295
+ spanAttributes[ATTR_GEN_AI_REQUEST_PRESENCE_PENALTY],
296
+ );
297
+ const frequencyPenalty = getNumber(
298
+ spanAttributes[ATTR_GEN_AI_REQUEST_FREQUENCY_PENALTY],
299
+ );
300
+ const seed = getNumber(spanAttributes[ATTR_GEN_AI_REQUEST_SEED]);
301
+ const stopSequences = getStringArray(
302
+ spanAttributes[ATTR_GEN_AI_REQUEST_STOP_SEQUENCES],
303
+ );
304
+ const invocationParameters: Record<string, unknown> = {};
305
+ if (requestModel) invocationParameters.model = requestModel;
306
+ if (typeof temperature === "number")
307
+ invocationParameters.temperature = temperature;
308
+ if (typeof topP === "number") invocationParameters.top_p = topP;
309
+ if (typeof topK === "number") invocationParameters.top_k = topK;
310
+ if (typeof presencePenalty === "number")
311
+ invocationParameters.presence_penalty = presencePenalty;
312
+ if (typeof frequencyPenalty === "number")
313
+ invocationParameters.frequency_penalty = frequencyPenalty;
314
+ if (typeof seed === "number") invocationParameters.seed = seed;
315
+ if (stopSequences && stopSequences.length > 0)
316
+ invocationParameters.stop_sequences = stopSequences;
317
+ if (typeof maxTokens === "number")
318
+ invocationParameters.max_completion_tokens = maxTokens;
319
+ if (Object.keys(invocationParameters).length > 0) {
320
+ set(
321
+ attrs,
322
+ SemanticConventions.LLM_INVOCATION_PARAMETERS,
323
+ safelyJSONStringify(invocationParameters),
324
+ );
325
+ }
326
+ return attrs;
327
+ };
328
+
329
+ /**
330
+ * Map input value to openinference attributes
331
+ * @param spanAttributes - The span attributes containing input value to map
332
+ * @returns The mapped input value attributes
333
+ */
334
+ export const mapInputValue = (spanAttributes: Attributes): Attributes => {
335
+ const attrs: Attributes = {};
336
+ let input = getString(spanAttributes["input"]);
337
+ if (!input) {
338
+ // fallback to deprecated prompt attribute if input is not present
339
+ input = getString(spanAttributes[ATTR_GEN_AI_PROMPT]);
340
+ }
341
+ // only set input value and mime type if input is present
342
+ if (input) {
343
+ set(attrs, SemanticConventions.INPUT_VALUE, input);
344
+ set(attrs, SemanticConventions.INPUT_MIME_TYPE, getMimeType(input));
345
+ }
346
+ return attrs;
347
+ };
348
+
349
+ /**
350
+ * Map output value to openinference attributes
351
+ * @param spanAttributes - The span attributes containing output value to map
352
+ * @returns The mapped output value attributes
353
+ */
354
+ export const mapOutputValue = (spanAttributes: Attributes): Attributes => {
355
+ const attrs: Attributes = {};
356
+ let output = getString(spanAttributes["output"]);
357
+ if (!output) {
358
+ // fallback to deprecated completion attribute if output is not present
359
+ output = getString(spanAttributes[ATTR_GEN_AI_COMPLETION]);
360
+ }
361
+ // only set output value and mime type if output is present
362
+ if (output) {
363
+ set(attrs, SemanticConventions.OUTPUT_VALUE, output);
364
+ set(attrs, SemanticConventions.OUTPUT_MIME_TYPE, getMimeType(output));
365
+ }
366
+ return attrs;
367
+ };
368
+
369
+ /**
370
+ * Map input messages to openinference attributes
371
+ * @param spanAttributes - The span attributes containing input messages to map
372
+ * @returns The mapped input messages attributes
373
+ */
374
+ export const mapInputMessages = (spanAttributes: Attributes): Attributes => {
375
+ const attrs: Attributes = {};
376
+ const genAIInputMessages = safelyParseJSON(
377
+ spanAttributes[ATTR_GEN_AI_INPUT_MESSAGES],
378
+ );
379
+
380
+ if (Array.isArray(genAIInputMessages)) {
381
+ (genAIInputMessages as unknown[]).forEach((msg, msgIndex) => {
382
+ if (!isGenAIChatMessage(msg)) return;
383
+ const msgPrefix = `${SemanticConventions.LLM_INPUT_MESSAGES}.${msgIndex}.`;
384
+ // set the message role
385
+ set(attrs, `${msgPrefix}${SemanticConventions.MESSAGE_ROLE}`, msg.role);
386
+ // process and set the rest of the message parts
387
+ processMessageParts({ attrs, msgPrefix, parts: msg.parts });
388
+ });
389
+ }
390
+
391
+ return attrs;
392
+ };
393
+
394
+ /**
395
+ * Map output messages to openinference attributes
396
+ * @param spanAttributes - The span attributes containing output messages to map
397
+ * @returns The mapped output messages attributes
398
+ */
399
+ export const mapOutputMessages = (spanAttributes: Attributes): Attributes => {
400
+ const attrs: Attributes = {};
401
+ const genAIOutputMessages = safelyParseJSON(
402
+ spanAttributes[ATTR_GEN_AI_OUTPUT_MESSAGES],
403
+ );
404
+
405
+ if (Array.isArray(genAIOutputMessages) && genAIOutputMessages.length > 0) {
406
+ // recast as unknown[] for safety, as Array.isArray() retypes to any[]
407
+ (genAIOutputMessages as unknown[]).forEach((msg, msgIndex) => {
408
+ if (!isGenAIChatMessage(msg)) return;
409
+ const msgPrefix = `${SemanticConventions.LLM_OUTPUT_MESSAGES}.${msgIndex}.`;
410
+ // set the message role
411
+ set(attrs, `${msgPrefix}${SemanticConventions.MESSAGE_ROLE}`, msg.role);
412
+ // process and set the rest of the message parts
413
+ processMessageParts({ attrs, msgPrefix, parts: msg.parts });
414
+ });
415
+ }
416
+
417
+ return attrs;
418
+ };
419
+
420
+ /**
421
+ * Map usage token counts to openinference attributes
422
+ * @param spanAttributes - The span attributes containing usage token counts to map
423
+ * @returns The mapped usage token counts attributes
424
+ */
425
+ export const mapTokenCounts = (spanAttributes: Attributes): Attributes => {
426
+ const attrs: Attributes = {};
427
+ const inputTokens = getNumber(spanAttributes[ATTR_GEN_AI_USAGE_INPUT_TOKENS]);
428
+ const outputTokens = getNumber(
429
+ spanAttributes[ATTR_GEN_AI_USAGE_OUTPUT_TOKENS],
430
+ );
431
+ if (typeof inputTokens === "number") {
432
+ set(attrs, SemanticConventions.LLM_TOKEN_COUNT_PROMPT, inputTokens);
433
+ }
434
+ if (typeof outputTokens === "number") {
435
+ set(attrs, SemanticConventions.LLM_TOKEN_COUNT_COMPLETION, outputTokens);
436
+ }
437
+ if (typeof inputTokens === "number" && typeof outputTokens === "number") {
438
+ set(
439
+ attrs,
440
+ SemanticConventions.LLM_TOKEN_COUNT_TOTAL,
441
+ inputTokens + outputTokens,
442
+ );
443
+ }
444
+ return attrs;
445
+ };
446
+
447
+ /**
448
+ * Map tool execution to openinference attributes
449
+ * @see https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/#execute-tool-span
450
+ * @param spanAttributes - The span attributes containing tool execution to map
451
+ * @returns The mapped tool execution attributes
452
+ */
453
+ export const mapToolExecution = (spanAttributes: Attributes): Attributes => {
454
+ const attrs: Attributes = {};
455
+ const toolName = getString(spanAttributes[ATTR_GEN_AI_TOOL_NAME]);
456
+ const toolDescription = getString(
457
+ spanAttributes[ATTR_GEN_AI_TOOL_DESCRIPTION],
458
+ );
459
+ const toolCallId = getString(spanAttributes[ATTR_GEN_AI_TOOL_CALL_ID]);
460
+ // parse supported tool details
461
+ // note: while openinference can track parameters, gen_ai does not provide this information
462
+ set(attrs, SemanticConventions.TOOL_NAME, toolName);
463
+ set(attrs, SemanticConventions.TOOL_DESCRIPTION, toolDescription);
464
+ set(attrs, SemanticConventions.TOOL_CALL_ID, toolCallId);
465
+
466
+ return attrs;
467
+ };
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { convertGenAISpanAttributesToOpenInferenceSpanAttributes as unsafeConvertGenAISpanAttributesToOpenInferenceSpanAttributes } from "./attributes.js";
2
+ import { withSafety } from "./utils.js";
3
+
4
+ export const convertGenAISpanAttributesToOpenInferenceSpanAttributes =
5
+ withSafety({
6
+ fn: unsafeConvertGenAISpanAttributesToOpenInferenceSpanAttributes,
7
+ onError(error) {
8
+ // eslint-disable-next-line no-console
9
+ console.error(
10
+ "Unable to convert GenAI span attributes to OpenInference span attributes",
11
+ error,
12
+ );
13
+ },
14
+ });
package/src/types.ts ADDED
@@ -0,0 +1,14 @@
1
+ export type Mutable<T> = {
2
+ -readonly [P in keyof T]: T[P];
3
+ };
4
+
5
+ export type DeeplyMutable<T> = {
6
+ -readonly [P in keyof T]: T[P] extends object ? DeeplyMutable<T[P]> : T[P];
7
+ };
8
+
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ export type GenericFunction = (...args: any[]) => any;
11
+
12
+ export type SafeFunction<T extends GenericFunction> = (
13
+ ...args: Parameters<T>
14
+ ) => ReturnType<T> | null;
package/src/utils.ts ADDED
@@ -0,0 +1,106 @@
1
+ import { MimeType } from "@arizeai/openinference-semantic-conventions";
2
+ import type { Attributes, AttributeValue } from "@opentelemetry/api";
3
+ import { GenericFunction, SafeFunction } from "./types.js";
4
+
5
+ export const safelyJSONStringify = (value: unknown) => {
6
+ try {
7
+ return JSON.stringify(value);
8
+ } catch {
9
+ return null;
10
+ }
11
+ };
12
+
13
+ export const getNumber = (value: unknown): number | undefined => {
14
+ if (typeof value === "number" && Number.isFinite(value)) return value;
15
+ return undefined;
16
+ };
17
+
18
+ export const getString = (value: unknown): string | undefined => {
19
+ if (typeof value === "string" && value.length > 0) return value;
20
+ return undefined;
21
+ };
22
+
23
+ export const getStringArray = (value: unknown): string[] | undefined => {
24
+ if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
25
+ return value as string[];
26
+ }
27
+ return undefined;
28
+ };
29
+
30
+ export const safelyParseJSON = (value: unknown): unknown => {
31
+ const s = getString(value);
32
+ if (!s) return undefined;
33
+ try {
34
+ return JSON.parse(s);
35
+ } catch {
36
+ return undefined;
37
+ }
38
+ };
39
+
40
+ export const getMimeType = (value: unknown): MimeType => {
41
+ if (safelyParseJSON(value)) return MimeType.JSON;
42
+ return MimeType.TEXT;
43
+ };
44
+
45
+ /**
46
+ * Assign attribute value to attributes object if value is not undefined or null
47
+ * This mutates the attrs object
48
+ * @param attrs - The attributes object to assign the value to
49
+ * @param key - The key to assign the value to
50
+ * @param value - The value to assign to the key
51
+ */
52
+ export const set = (
53
+ attrs: Attributes,
54
+ key: string,
55
+ value?: AttributeValue | null,
56
+ ) => {
57
+ if (value === undefined || value === null) return;
58
+ attrs[key] = value;
59
+ };
60
+
61
+ /**
62
+ * Merge multiple attributes objects into a single attributes object
63
+ * This mutates the first attributes object
64
+ * @param groups - The groups of attributes to merge
65
+ * @returns The merged attributes
66
+ */
67
+ export const merge = (...groups: Attributes[]): Attributes =>
68
+ groups.reduce((acc, g) => Object.assign(acc, g), {} as Attributes);
69
+
70
+ /**
71
+ * Convert a value to a string. If the value is already a string, return it.
72
+ * If the value can be jsonified, jsonify it.
73
+ * Otherwise, return the string representation of the value.
74
+ * @param value - The value to convert to a string
75
+ * @returns The string representation of the value
76
+ */
77
+ export const toStringContent = (value: unknown): string => {
78
+ if (typeof value === "string") return value;
79
+ const json = safelyJSONStringify(value);
80
+ if (typeof json === "string") return json;
81
+ return String(value);
82
+ };
83
+
84
+ /**
85
+ * Wraps a function with a try-catch block to catch and log any errors.
86
+ * @param fn - A function to wrap with a try-catch block.
87
+ * @returns A function that returns null if an error is thrown.
88
+ */
89
+ export function withSafety<T extends GenericFunction>({
90
+ fn,
91
+ onError,
92
+ }: {
93
+ fn: T;
94
+ onError?: (error: unknown) => void;
95
+ }): SafeFunction<T> {
96
+ return (...args) => {
97
+ try {
98
+ return fn(...args);
99
+ } catch (error) {
100
+ if (onError) {
101
+ onError(error);
102
+ }
103
+ return null;
104
+ }
105
+ };
106
+ }