@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.
- package/README.md +137 -0
- package/dist/esm/__generated__/opentelemetryInputMessages.d.ts +103 -0
- package/dist/esm/__generated__/opentelemetryInputMessages.d.ts.map +1 -0
- package/dist/esm/__generated__/opentelemetryInputMessages.js +7 -0
- package/dist/esm/__generated__/opentelemetryInputMessages.js.map +1 -0
- package/dist/esm/__generated__/opentelemetryOutputMessages.d.ts +116 -0
- package/dist/esm/__generated__/opentelemetryOutputMessages.d.ts.map +1 -0
- package/dist/esm/__generated__/opentelemetryOutputMessages.js +7 -0
- package/dist/esm/__generated__/opentelemetryOutputMessages.js.map +1 -0
- package/dist/esm/attributes.d.ts +79 -0
- package/dist/esm/attributes.d.ts.map +1 -0
- package/dist/esm/attributes.js +306 -0
- package/dist/esm/attributes.js.map +1 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +10 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -0
- package/dist/esm/types.d.ts +9 -0
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +2 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/utils.d.ts +42 -0
- package/dist/esm/utils.d.ts.map +1 -0
- package/dist/esm/utils.js +94 -0
- package/dist/esm/utils.js.map +1 -0
- package/package.json +68 -0
- package/src/__generated__/opentelemetryInputMessages.ts +104 -0
- package/src/__generated__/opentelemetryOutputMessages.ts +122 -0
- package/src/attributes.ts +467 -0
- package/src/index.ts +14 -0
- package/src/types.ts +14 -0
- 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
|
+
}
|