mingx 0.1.0__py3-none-any.whl
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.
- mingx/__init__.py +50 -0
- mingx/_default_attributes.py +76 -0
- mingx/_trace.py +147 -0
- mingx/adapters/__init__.py +21 -0
- mingx/adapters/base.py +77 -0
- mingx/adapters/langchain.py +646 -0
- mingx/decorator.py +185 -0
- mingx/genai/__init__.py +99 -0
- mingx/genai/attributes.py +176 -0
- mingx/genai/io.py +439 -0
- mingx/genai/span_attributes.py +172 -0
- mingx/genai/spans.py +175 -0
- mingx-0.1.0.dist-info/METADATA +373 -0
- mingx-0.1.0.dist-info/RECORD +15 -0
- mingx-0.1.0.dist-info/WHEEL +4 -0
mingx/genai/io.py
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GenAI 输入/输出记录:优先使用 Span Event,符合 OTEL GenAI 规范。
|
|
3
|
+
|
|
4
|
+
与 Traceloop 等 SDK 一致:模型的输入/输出优先通过 Event 记录,避免大 payload
|
|
5
|
+
撑大 Span、便于后端对 Event 单独采样或脱敏。也可选记录为 Span 属性。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from typing import Any, Dict, List, Literal, Optional
|
|
12
|
+
|
|
13
|
+
from mingx.genai.attributes import (
|
|
14
|
+
GEN_AI_INPUT_MESSAGES,
|
|
15
|
+
GEN_AI_OUTPUT_MESSAGES,
|
|
16
|
+
GEN_AI_SYSTEM_INSTRUCTIONS,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
RecordInputOutputAs = Literal["events", "attributes", "none"]
|
|
20
|
+
|
|
21
|
+
# Event 名称(与 OTEL GenAI 语义一致,便于后端识别)
|
|
22
|
+
GEN_AI_EVENT_INPUT = "gen_ai.input"
|
|
23
|
+
GEN_AI_EVENT_OUTPUT = "gen_ai.output"
|
|
24
|
+
GEN_AI_EVENT_SYSTEM_INSTRUCTIONS = "gen_ai.system_instructions"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _coerce_attribute_value(value: Any) -> Any:
|
|
28
|
+
"""OTEL 属性值:str / int / float / bool / Sequence 或转 str;None 转为空串。"""
|
|
29
|
+
if value is None:
|
|
30
|
+
return ""
|
|
31
|
+
if isinstance(value, (str, int, float, bool)):
|
|
32
|
+
return value
|
|
33
|
+
if isinstance(value, (list, tuple)):
|
|
34
|
+
return [_coerce_attribute_value(x) for x in value]
|
|
35
|
+
if isinstance(value, dict):
|
|
36
|
+
return json.dumps(value, ensure_ascii=False)
|
|
37
|
+
return str(value)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def record_llm_input_output(
|
|
41
|
+
span: Any,
|
|
42
|
+
input_messages: List[Dict[str, Any]],
|
|
43
|
+
output_messages: List[Dict[str, Any]],
|
|
44
|
+
*,
|
|
45
|
+
record_as: RecordInputOutputAs = "events",
|
|
46
|
+
system_instructions: Optional[List[Dict[str, Any]]] = None,
|
|
47
|
+
max_length: Optional[int] = None,
|
|
48
|
+
) -> None:
|
|
49
|
+
"""
|
|
50
|
+
在 Span 上记录 LLM 输入/输出,优先使用 Event(推荐)。
|
|
51
|
+
|
|
52
|
+
与 OpenTelemetry GenAI 语义一致:gen_ai.input.messages、gen_ai.output.messages。
|
|
53
|
+
使用 Event 时:Span 本体保持精简,后端可对 Event 单独采样、脱敏或存储。
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
span: OpenTelemetry Span(需支持 add_event / set_attribute)。
|
|
57
|
+
input_messages: 输入消息列表,每项建议 { "role": "user"|"assistant"|"system", "content": "..." }。
|
|
58
|
+
output_messages: 输出消息列表,结构同上。
|
|
59
|
+
record_as: "events"(默认,推荐)| "attributes" | "none"。
|
|
60
|
+
system_instructions: 可选,系统指令(单独一条 Event 或属性)。
|
|
61
|
+
max_length: 可选,对单条 content 截断长度(字符),避免过大。
|
|
62
|
+
"""
|
|
63
|
+
if record_as == "none":
|
|
64
|
+
return
|
|
65
|
+
if not span.is_recording():
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
def _truncate(obj: Any) -> Any:
|
|
69
|
+
if max_length is None:
|
|
70
|
+
return obj
|
|
71
|
+
if isinstance(obj, dict):
|
|
72
|
+
if "content" in obj and isinstance(obj["content"], str) and len(obj["content"]) > max_length:
|
|
73
|
+
return {**obj, "content": obj["content"][:max_length] + "..."}
|
|
74
|
+
return {k: _truncate(v) for k, v in obj.items()}
|
|
75
|
+
if isinstance(obj, list):
|
|
76
|
+
return [_truncate(x) for x in obj]
|
|
77
|
+
return obj
|
|
78
|
+
|
|
79
|
+
input_messages = _truncate(input_messages)
|
|
80
|
+
output_messages = _truncate(output_messages)
|
|
81
|
+
payload_input = json.dumps(input_messages, ensure_ascii=False)
|
|
82
|
+
payload_output = json.dumps(output_messages, ensure_ascii=False)
|
|
83
|
+
|
|
84
|
+
if record_as == "events":
|
|
85
|
+
span.add_event(GEN_AI_EVENT_INPUT, attributes={GEN_AI_INPUT_MESSAGES: payload_input})
|
|
86
|
+
span.add_event(GEN_AI_EVENT_OUTPUT, attributes={GEN_AI_OUTPUT_MESSAGES: payload_output})
|
|
87
|
+
if system_instructions is not None:
|
|
88
|
+
payload_sys = json.dumps(system_instructions, ensure_ascii=False)
|
|
89
|
+
span.add_event(GEN_AI_EVENT_SYSTEM_INSTRUCTIONS, attributes={GEN_AI_SYSTEM_INSTRUCTIONS: payload_sys})
|
|
90
|
+
elif record_as == "attributes":
|
|
91
|
+
span.set_attribute(GEN_AI_INPUT_MESSAGES, payload_input)
|
|
92
|
+
span.set_attribute(GEN_AI_OUTPUT_MESSAGES, payload_output)
|
|
93
|
+
if system_instructions is not None:
|
|
94
|
+
span.set_attribute(GEN_AI_SYSTEM_INSTRUCTIONS, json.dumps(system_instructions, ensure_ascii=False))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _is_langchain_message(obj: Any) -> bool:
|
|
98
|
+
"""Duck-typing: LangChain BaseMessage 有 type 与 content。"""
|
|
99
|
+
return (
|
|
100
|
+
obj is not None
|
|
101
|
+
and hasattr(obj, "type")
|
|
102
|
+
and hasattr(obj, "content")
|
|
103
|
+
and isinstance(getattr(obj, "type", None), str)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _message_type_to_role(msg_type: str) -> str:
|
|
108
|
+
"""LangChain message type -> GenAI role."""
|
|
109
|
+
t = (msg_type or "").strip().lower()
|
|
110
|
+
if t == "system":
|
|
111
|
+
return "system"
|
|
112
|
+
if t == "human":
|
|
113
|
+
return "user"
|
|
114
|
+
if t == "ai":
|
|
115
|
+
return "assistant"
|
|
116
|
+
return msg_type or "user"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _message_content_to_serializable(content: Any, truncate_fn: Any) -> Any:
|
|
120
|
+
"""将 Message.content(str 或 content blocks 列表)转为可 JSON 的格式。"""
|
|
121
|
+
if content is None:
|
|
122
|
+
return ""
|
|
123
|
+
if isinstance(content, str):
|
|
124
|
+
return truncate_fn(content)
|
|
125
|
+
if isinstance(content, (list, tuple)):
|
|
126
|
+
out: List[Any] = []
|
|
127
|
+
for block in content:
|
|
128
|
+
if isinstance(block, dict):
|
|
129
|
+
out.append(block)
|
|
130
|
+
elif hasattr(block, "model_dump"):
|
|
131
|
+
out.append(block.model_dump())
|
|
132
|
+
elif isinstance(block, str):
|
|
133
|
+
out.append({"type": "text", "text": truncate_fn(block)})
|
|
134
|
+
else:
|
|
135
|
+
out.append({"type": "unknown", "value": str(block)})
|
|
136
|
+
return out
|
|
137
|
+
return str(content)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _langchain_messages_to_messages_json(
|
|
141
|
+
messages: List[Any],
|
|
142
|
+
truncate_fn: Any,
|
|
143
|
+
) -> List[Dict[str, Any]]:
|
|
144
|
+
"""将 LangChain Message 列表转为 [{"role": "...", "content": ...}, ...] 的 JSON 友好结构。"""
|
|
145
|
+
result: List[Dict[str, Any]] = []
|
|
146
|
+
for m in messages:
|
|
147
|
+
if not _is_langchain_message(m):
|
|
148
|
+
return [] # 非全为 message 时交给默认序列化
|
|
149
|
+
msg_type = getattr(m, "type", "user")
|
|
150
|
+
content = getattr(m, "content", None)
|
|
151
|
+
role = _message_type_to_role(msg_type)
|
|
152
|
+
result.append({
|
|
153
|
+
"role": role,
|
|
154
|
+
"content": _message_content_to_serializable(content, truncate_fn),
|
|
155
|
+
})
|
|
156
|
+
return result
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _serialize_span_io(
|
|
160
|
+
data: Any,
|
|
161
|
+
max_length: Optional[int],
|
|
162
|
+
) -> str:
|
|
163
|
+
def _truncate_str(s: str) -> str:
|
|
164
|
+
if max_length is not None and len(s) > max_length:
|
|
165
|
+
return s[:max_length] + "..."
|
|
166
|
+
return s
|
|
167
|
+
|
|
168
|
+
def _to_serializable(obj: Any) -> Any:
|
|
169
|
+
if obj is None:
|
|
170
|
+
return None
|
|
171
|
+
if isinstance(obj, str):
|
|
172
|
+
return _truncate_str(obj)
|
|
173
|
+
if isinstance(obj, dict):
|
|
174
|
+
return {k: _to_serializable(v) for k, v in obj.items()}
|
|
175
|
+
if isinstance(obj, (list, tuple)):
|
|
176
|
+
seq = list(obj)
|
|
177
|
+
if seq and _is_langchain_message(seq[0]) and all(_is_langchain_message(x) for x in seq):
|
|
178
|
+
return _langchain_messages_to_messages_json(seq, _truncate_str)
|
|
179
|
+
return [_to_serializable(x) for x in obj]
|
|
180
|
+
if hasattr(obj, "page_content") and hasattr(obj, "metadata"):
|
|
181
|
+
return {"page_content": _truncate_str(getattr(obj, "page_content", "") or ""), "metadata": getattr(obj, "metadata", {}) or {}}
|
|
182
|
+
# 如 PromptValue 等带 .messages 的对象,统一转为 [{"role","content"}] 的 JSON
|
|
183
|
+
if hasattr(obj, "messages"):
|
|
184
|
+
msgs = getattr(obj, "messages", None)
|
|
185
|
+
if isinstance(msgs, (list, tuple)) and msgs and all(_is_langchain_message(m) for m in msgs):
|
|
186
|
+
return _langchain_messages_to_messages_json(list(msgs), _truncate_str)
|
|
187
|
+
if _is_langchain_message(obj):
|
|
188
|
+
role = _message_type_to_role(getattr(obj, "type", "user"))
|
|
189
|
+
content = _message_content_to_serializable(getattr(obj, "content", None), _truncate_str)
|
|
190
|
+
return [{"role": role, "content": content}]
|
|
191
|
+
try:
|
|
192
|
+
json.dumps(obj)
|
|
193
|
+
return obj
|
|
194
|
+
except (TypeError, ValueError):
|
|
195
|
+
return str(obj)
|
|
196
|
+
|
|
197
|
+
return json.dumps(_to_serializable(data), ensure_ascii=False)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def record_span_input(
|
|
201
|
+
span: Any,
|
|
202
|
+
input_data: Any,
|
|
203
|
+
*,
|
|
204
|
+
record_as: RecordInputOutputAs = "events",
|
|
205
|
+
max_length: Optional[int] = None,
|
|
206
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
207
|
+
event_attributes: Optional[Dict[str, Any]] = None,
|
|
208
|
+
) -> None:
|
|
209
|
+
"""
|
|
210
|
+
在 Span 上仅记录输入。可与 record_span_output 分开调用,按需只添加输入或只添加输出。
|
|
211
|
+
attributes: 自定义 Span 属性(会写入当前 Span)。
|
|
212
|
+
event_attributes: 自定义 Event 属性(record_as=events 时合并到 gen_ai.input 的 attributes)。
|
|
213
|
+
"""
|
|
214
|
+
if record_as == "none" and not (attributes or event_attributes):
|
|
215
|
+
return
|
|
216
|
+
if not span.is_recording():
|
|
217
|
+
return
|
|
218
|
+
if attributes:
|
|
219
|
+
for k, v in attributes.items():
|
|
220
|
+
span.set_attribute(k, _coerce_attribute_value(v))
|
|
221
|
+
if record_as != "none":
|
|
222
|
+
payload = _serialize_span_io(input_data, max_length)
|
|
223
|
+
ev_attrs = {GEN_AI_INPUT_MESSAGES: payload}
|
|
224
|
+
if event_attributes:
|
|
225
|
+
for k, v in event_attributes.items():
|
|
226
|
+
ev_attrs[k] = _coerce_attribute_value(v)
|
|
227
|
+
if record_as == "events":
|
|
228
|
+
span.add_event(GEN_AI_EVENT_INPUT, attributes=ev_attrs)
|
|
229
|
+
elif record_as == "attributes":
|
|
230
|
+
span.set_attribute(GEN_AI_INPUT_MESSAGES, payload)
|
|
231
|
+
for k, v in (event_attributes or {}).items():
|
|
232
|
+
span.set_attribute(k, _coerce_attribute_value(v))
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def record_span_output(
|
|
236
|
+
span: Any,
|
|
237
|
+
output_data: Any,
|
|
238
|
+
*,
|
|
239
|
+
record_as: RecordInputOutputAs = "events",
|
|
240
|
+
max_length: Optional[int] = None,
|
|
241
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
242
|
+
event_attributes: Optional[Dict[str, Any]] = None,
|
|
243
|
+
) -> None:
|
|
244
|
+
"""
|
|
245
|
+
在 Span 上仅记录输出。可与 record_span_input 分开调用,按需只添加输入或只添加输出。
|
|
246
|
+
attributes: 自定义 Span 属性(会写入当前 Span)。
|
|
247
|
+
event_attributes: 自定义 Event 属性(record_as=events 时合并到 gen_ai.output 的 attributes)。
|
|
248
|
+
"""
|
|
249
|
+
if record_as == "none" and not (attributes or event_attributes):
|
|
250
|
+
return
|
|
251
|
+
if not span.is_recording():
|
|
252
|
+
return
|
|
253
|
+
if attributes:
|
|
254
|
+
for k, v in attributes.items():
|
|
255
|
+
span.set_attribute(k, _coerce_attribute_value(v))
|
|
256
|
+
if record_as != "none":
|
|
257
|
+
payload = _serialize_span_io(output_data, max_length)
|
|
258
|
+
ev_attrs = {GEN_AI_OUTPUT_MESSAGES: payload}
|
|
259
|
+
if event_attributes:
|
|
260
|
+
for k, v in event_attributes.items():
|
|
261
|
+
ev_attrs[k] = _coerce_attribute_value(v)
|
|
262
|
+
if record_as == "events":
|
|
263
|
+
span.add_event(GEN_AI_EVENT_OUTPUT, attributes=ev_attrs)
|
|
264
|
+
elif record_as == "attributes":
|
|
265
|
+
span.set_attribute(GEN_AI_OUTPUT_MESSAGES, payload)
|
|
266
|
+
for k, v in (event_attributes or {}).items():
|
|
267
|
+
span.set_attribute(k, _coerce_attribute_value(v))
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def record_span_input_output(
|
|
271
|
+
span: Any,
|
|
272
|
+
input_data: Any,
|
|
273
|
+
output_data: Any,
|
|
274
|
+
*,
|
|
275
|
+
record_as: RecordInputOutputAs = "events",
|
|
276
|
+
max_length: Optional[int] = None,
|
|
277
|
+
) -> None:
|
|
278
|
+
"""
|
|
279
|
+
在 Span 上记录任意类型的输入/输出(chain、tool、retriever 等),优先使用 Event。
|
|
280
|
+
|
|
281
|
+
与 record_llm_input_output 一致:同一套 Event 名与属性键,payload 为 JSON 序列化后的
|
|
282
|
+
input_data / output_data(dict、list、str 等可序列化结构)。
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
span: OpenTelemetry Span。
|
|
286
|
+
input_data: 可 JSON 序列化的输入(如 chain 的 inputs、tool 的 input_str、retriever 的 query)。
|
|
287
|
+
output_data: 可 JSON 序列化的输出(如 chain 的 outputs、tool 的 output、retriever 的 documents)。
|
|
288
|
+
record_as: "events"(默认)| "attributes" | "none"。
|
|
289
|
+
max_length: 可选,对字符串类 content 截断长度。
|
|
290
|
+
"""
|
|
291
|
+
if record_as == "none":
|
|
292
|
+
return
|
|
293
|
+
if not span.is_recording():
|
|
294
|
+
return
|
|
295
|
+
payload_input = _serialize_span_io(input_data, max_length)
|
|
296
|
+
payload_output = _serialize_span_io(output_data, max_length)
|
|
297
|
+
if record_as == "events":
|
|
298
|
+
span.add_event(GEN_AI_EVENT_INPUT, attributes={GEN_AI_INPUT_MESSAGES: payload_input})
|
|
299
|
+
span.add_event(GEN_AI_EVENT_OUTPUT, attributes={GEN_AI_OUTPUT_MESSAGES: payload_output})
|
|
300
|
+
elif record_as == "attributes":
|
|
301
|
+
span.set_attribute(GEN_AI_INPUT_MESSAGES, payload_input)
|
|
302
|
+
span.set_attribute(GEN_AI_OUTPUT_MESSAGES, payload_output)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def span_input(
|
|
306
|
+
input_data: Any,
|
|
307
|
+
*,
|
|
308
|
+
span: Optional[Any] = None,
|
|
309
|
+
record_as: RecordInputOutputAs = "events",
|
|
310
|
+
max_length: Optional[int] = None,
|
|
311
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
312
|
+
event_attributes: Optional[Dict[str, Any]] = None,
|
|
313
|
+
) -> None:
|
|
314
|
+
"""
|
|
315
|
+
向 Span 添加输入(入参)。可与 span_output 分开调用,按需只添加输入或只添加输出。
|
|
316
|
+
|
|
317
|
+
用法(原生 OTEL 风格):
|
|
318
|
+
with get_tracer().start_as_current_span("call_model") as span:
|
|
319
|
+
span_input({"prompt": "..."})
|
|
320
|
+
result = call_model()
|
|
321
|
+
span_output(result)
|
|
322
|
+
|
|
323
|
+
若未传 span,则对当前 Span 生效;无活跃 Span 或未 recording 时静默忽略。
|
|
324
|
+
attributes: 自定义 Span 属性。event_attributes: 自定义 Event 属性(合并到 gen_ai.input)。
|
|
325
|
+
"""
|
|
326
|
+
from opentelemetry import trace
|
|
327
|
+
s = span if span is not None else trace.get_current_span()
|
|
328
|
+
record_span_input(
|
|
329
|
+
s,
|
|
330
|
+
input_data,
|
|
331
|
+
record_as=record_as,
|
|
332
|
+
max_length=max_length,
|
|
333
|
+
attributes=attributes,
|
|
334
|
+
event_attributes=event_attributes,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def span_output(
|
|
339
|
+
output_data: Any,
|
|
340
|
+
*,
|
|
341
|
+
span: Optional[Any] = None,
|
|
342
|
+
record_as: RecordInputOutputAs = "events",
|
|
343
|
+
max_length: Optional[int] = None,
|
|
344
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
345
|
+
event_attributes: Optional[Dict[str, Any]] = None,
|
|
346
|
+
) -> None:
|
|
347
|
+
"""
|
|
348
|
+
向 Span 添加输出(返回值)。可与 span_input 分开调用,按需只添加输入或只添加输出。
|
|
349
|
+
|
|
350
|
+
用法(原生 OTEL 风格):
|
|
351
|
+
with get_tracer().start_as_current_span("call_model") as span:
|
|
352
|
+
span_input({"prompt": "..."})
|
|
353
|
+
result = call_model()
|
|
354
|
+
span_output(result)
|
|
355
|
+
|
|
356
|
+
若未传 span,则对当前 Span 生效;无活跃 Span 或未 recording 时静默忽略。
|
|
357
|
+
attributes: 自定义 Span 属性。event_attributes: 自定义 Event 属性(合并到 gen_ai.output)。
|
|
358
|
+
"""
|
|
359
|
+
from opentelemetry import trace
|
|
360
|
+
s = span if span is not None else trace.get_current_span()
|
|
361
|
+
record_span_output(
|
|
362
|
+
s,
|
|
363
|
+
output_data,
|
|
364
|
+
record_as=record_as,
|
|
365
|
+
max_length=max_length,
|
|
366
|
+
attributes=attributes,
|
|
367
|
+
event_attributes=event_attributes,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def record_current_span_input_output(
|
|
372
|
+
input_data: Any,
|
|
373
|
+
output_data: Any,
|
|
374
|
+
*,
|
|
375
|
+
record_as: RecordInputOutputAs = "events",
|
|
376
|
+
max_length: Optional[int] = None,
|
|
377
|
+
) -> None:
|
|
378
|
+
"""
|
|
379
|
+
手动向当前 Span 上报输入/输出(入参作为输入,返回值作为输出,无返回值传 None 或空即可)。
|
|
380
|
+
|
|
381
|
+
适用于:在 @traced 装饰的函数内、或已有 Span 的上下文中,手动补充或覆盖输入/输出。
|
|
382
|
+
若当前无活跃 Span 或 Span 未在 recording 则静默忽略。
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
input_data: 可序列化的输入(如函数入参、dict、list、str)。
|
|
386
|
+
output_data: 可序列化的输出(如函数返回值);无返回值可传 None 或 {}。
|
|
387
|
+
record_as: "events"(默认)| "attributes" | "none"。
|
|
388
|
+
max_length: 可选,单条字符串截断长度。
|
|
389
|
+
"""
|
|
390
|
+
from opentelemetry import trace
|
|
391
|
+
span = trace.get_current_span()
|
|
392
|
+
if not span.is_recording():
|
|
393
|
+
return
|
|
394
|
+
record_span_input_output(
|
|
395
|
+
span,
|
|
396
|
+
input_data,
|
|
397
|
+
output_data,
|
|
398
|
+
record_as=record_as,
|
|
399
|
+
max_length=max_length,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def build_input_messages_from_prompts(prompts: List[str]) -> List[Dict[str, Any]]:
|
|
404
|
+
"""将 LangChain 的 prompts(str 列表)转为 GenAI 规范的 input messages 结构。"""
|
|
405
|
+
return [{"role": "user", "content": p} for p in (prompts or [])]
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def build_input_messages_from_langchain_messages(
|
|
409
|
+
messages: List[Any],
|
|
410
|
+
max_length: Optional[int] = None,
|
|
411
|
+
) -> List[Dict[str, Any]]:
|
|
412
|
+
"""
|
|
413
|
+
将 LangChain BaseMessage 列表转为 GenAI 规范的 input messages 结构 [{"role", "content"}, ...]。
|
|
414
|
+
Adapter 在 on_chat_model_start 收到 messages 后,在 on_llm_end 用此函数生成 input_messages。
|
|
415
|
+
"""
|
|
416
|
+
if not messages:
|
|
417
|
+
return []
|
|
418
|
+
if not all(_is_langchain_message(m) for m in messages):
|
|
419
|
+
return []
|
|
420
|
+
truncate_fn = (lambda s: s[:max_length] + "..." if max_length is not None and len(s) > max_length else s) if max_length else (lambda s: s)
|
|
421
|
+
return _langchain_messages_to_messages_json(messages, truncate_fn)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def build_output_messages_from_llm_result(response: Any) -> List[Dict[str, Any]]:
|
|
425
|
+
"""从 LangChain LLMResult 提取 output messages 结构。"""
|
|
426
|
+
out: List[Dict[str, Any]] = []
|
|
427
|
+
try:
|
|
428
|
+
for gen_list in (response.generations or []):
|
|
429
|
+
for gen in gen_list or []:
|
|
430
|
+
if hasattr(gen, "message") and gen.message:
|
|
431
|
+
msg = gen.message
|
|
432
|
+
content = getattr(msg, "content", None) or ""
|
|
433
|
+
if isinstance(content, str):
|
|
434
|
+
out.append({"role": "assistant", "content": content})
|
|
435
|
+
else:
|
|
436
|
+
out.append({"role": "assistant", "content": str(content)})
|
|
437
|
+
except Exception:
|
|
438
|
+
pass
|
|
439
|
+
return out
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""
|
|
2
|
+
各类型 Span 上送属性的数据模型。
|
|
3
|
+
|
|
4
|
+
将 LLM/Chain/Tool/Retriever 等不同语义的 Span 属性拆成独立模型,
|
|
5
|
+
通过 to_attributes() 统一输出为 OpenTelemetry 所需的 Dict[str, Any],保证设计清晰、易扩展。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from mingx._default_attributes import (
|
|
14
|
+
MINGX_SPAN_TYPE,
|
|
15
|
+
SPAN_TYPE_CHAIN,
|
|
16
|
+
SPAN_TYPE_MODEL,
|
|
17
|
+
SPAN_TYPE_RETRIEVER,
|
|
18
|
+
SPAN_TYPE_TOOL,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from . import attributes as genai_attrs
|
|
22
|
+
|
|
23
|
+
# 适配器层常用键(如 LangChain run_id)
|
|
24
|
+
LC_RUN_ID = "lc.run_id"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class InferenceSpanAttributes:
|
|
29
|
+
"""推理/LLM Span 上送属性(gen_ai.request.* 等)。"""
|
|
30
|
+
|
|
31
|
+
operation_name: str
|
|
32
|
+
provider_name: str
|
|
33
|
+
run_id: str
|
|
34
|
+
span_type: str = SPAN_TYPE_MODEL
|
|
35
|
+
model: Optional[str] = None
|
|
36
|
+
temperature: Optional[float] = None
|
|
37
|
+
max_tokens: Optional[int] = None
|
|
38
|
+
top_p: Optional[float] = None
|
|
39
|
+
top_k: Optional[int] = None
|
|
40
|
+
frequency_penalty: Optional[float] = None
|
|
41
|
+
presence_penalty: Optional[float] = None
|
|
42
|
+
stop_sequences: Optional[List[str]] = None
|
|
43
|
+
|
|
44
|
+
def to_attributes(self) -> Dict[str, Any]:
|
|
45
|
+
d = genai_attrs.inference_attributes(
|
|
46
|
+
self.operation_name,
|
|
47
|
+
self.provider_name,
|
|
48
|
+
model=self.model,
|
|
49
|
+
temperature=self.temperature,
|
|
50
|
+
max_tokens=self.max_tokens,
|
|
51
|
+
top_p=self.top_p,
|
|
52
|
+
top_k=self.top_k,
|
|
53
|
+
frequency_penalty=self.frequency_penalty,
|
|
54
|
+
presence_penalty=self.presence_penalty,
|
|
55
|
+
stop_sequences=self.stop_sequences,
|
|
56
|
+
)
|
|
57
|
+
d[LC_RUN_ID] = self.run_id
|
|
58
|
+
d[MINGX_SPAN_TYPE] = self.span_type
|
|
59
|
+
return d
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class ChainSpanAttributes:
|
|
64
|
+
"""Chain Span 上送属性。"""
|
|
65
|
+
|
|
66
|
+
run_id: str
|
|
67
|
+
operation_name: str = genai_attrs.OPERATION_INVOKE_AGENT
|
|
68
|
+
span_type: str = SPAN_TYPE_CHAIN
|
|
69
|
+
extra: Dict[str, Any] = field(default_factory=dict)
|
|
70
|
+
|
|
71
|
+
def to_attributes(self) -> Dict[str, Any]:
|
|
72
|
+
d: Dict[str, Any] = {
|
|
73
|
+
LC_RUN_ID: self.run_id,
|
|
74
|
+
genai_attrs.GEN_AI_OPERATION_NAME: self.operation_name,
|
|
75
|
+
MINGX_SPAN_TYPE: self.span_type,
|
|
76
|
+
}
|
|
77
|
+
d.update(self.extra)
|
|
78
|
+
return d
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True)
|
|
82
|
+
class ToolSpanAttributes:
|
|
83
|
+
"""Execute Tool Span 上送属性。"""
|
|
84
|
+
|
|
85
|
+
tool_name: str
|
|
86
|
+
run_id: str
|
|
87
|
+
span_type: str = SPAN_TYPE_TOOL
|
|
88
|
+
tool_description: Optional[str] = None
|
|
89
|
+
tool_call_id: Optional[str] = None
|
|
90
|
+
tool_type: Optional[str] = None
|
|
91
|
+
extra: Dict[str, Any] = field(default_factory=dict)
|
|
92
|
+
|
|
93
|
+
def to_attributes(self) -> Dict[str, Any]:
|
|
94
|
+
d = genai_attrs.execute_tool_attributes(
|
|
95
|
+
self.tool_name,
|
|
96
|
+
tool_description=self.tool_description,
|
|
97
|
+
tool_call_id=self.tool_call_id,
|
|
98
|
+
tool_type=self.tool_type,
|
|
99
|
+
)
|
|
100
|
+
d[LC_RUN_ID] = self.run_id
|
|
101
|
+
d[MINGX_SPAN_TYPE] = self.span_type
|
|
102
|
+
d.update(self.extra)
|
|
103
|
+
return d
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass(frozen=True)
|
|
107
|
+
class RetrieverSpanAttributes:
|
|
108
|
+
"""Retriever Span 上送属性。"""
|
|
109
|
+
|
|
110
|
+
run_id: str
|
|
111
|
+
operation_name: str = "retriever"
|
|
112
|
+
span_type: str = SPAN_TYPE_RETRIEVER
|
|
113
|
+
extra: Dict[str, Any] = field(default_factory=dict)
|
|
114
|
+
|
|
115
|
+
def to_attributes(self) -> Dict[str, Any]:
|
|
116
|
+
d: Dict[str, Any] = {
|
|
117
|
+
LC_RUN_ID: self.run_id,
|
|
118
|
+
genai_attrs.GEN_AI_OPERATION_NAME: self.operation_name,
|
|
119
|
+
MINGX_SPAN_TYPE: self.span_type,
|
|
120
|
+
}
|
|
121
|
+
d.update(self.extra)
|
|
122
|
+
return d
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
# 大模型调用结束时的上送数据:Token 使用、响应元数据、输入/输出消息
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(frozen=True)
|
|
131
|
+
class TokenUsage:
|
|
132
|
+
"""Token 使用情况(gen_ai.usage.*),用于推理/Embeddings 等 Span 结束时的属性。"""
|
|
133
|
+
|
|
134
|
+
input_tokens: Optional[int] = None
|
|
135
|
+
output_tokens: Optional[int] = None
|
|
136
|
+
|
|
137
|
+
def apply_to_span(self, span: Any) -> None:
|
|
138
|
+
"""将 usage 写入当前 Span 的 gen_ai.usage.* 属性。"""
|
|
139
|
+
if not getattr(span, "is_recording", lambda: False)():
|
|
140
|
+
return
|
|
141
|
+
if self.input_tokens is not None:
|
|
142
|
+
span.set_attribute(genai_attrs.GEN_AI_USAGE_INPUT_TOKENS, self.input_tokens)
|
|
143
|
+
if self.output_tokens is not None:
|
|
144
|
+
span.set_attribute(genai_attrs.GEN_AI_USAGE_OUTPUT_TOKENS, self.output_tokens)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass(frozen=True)
|
|
148
|
+
class InferenceResponseAttributes:
|
|
149
|
+
"""大模型调用响应元数据(gen_ai.response.*),在 Span 结束时上送。"""
|
|
150
|
+
|
|
151
|
+
response_model: Optional[str] = None
|
|
152
|
+
finish_reasons: Optional[List[str]] = None
|
|
153
|
+
response_id: Optional[str] = None
|
|
154
|
+
|
|
155
|
+
def apply_to_span(self, span: Any) -> None:
|
|
156
|
+
"""将响应元数据写入当前 Span 的 gen_ai.response.* 属性。"""
|
|
157
|
+
if not getattr(span, "is_recording", lambda: False)():
|
|
158
|
+
return
|
|
159
|
+
if self.response_model is not None:
|
|
160
|
+
span.set_attribute(genai_attrs.GEN_AI_RESPONSE_MODEL, self.response_model)
|
|
161
|
+
if self.finish_reasons is not None:
|
|
162
|
+
span.set_attribute(genai_attrs.GEN_AI_RESPONSE_FINISH_REASONS, self.finish_reasons)
|
|
163
|
+
if self.response_id is not None:
|
|
164
|
+
span.set_attribute(genai_attrs.GEN_AI_RESPONSE_ID, self.response_id)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass(frozen=True)
|
|
168
|
+
class InferenceInputOutput:
|
|
169
|
+
"""大模型调用的输入/输出消息体,用于 Span 的 Event 或属性记录(由 io.record_llm_input_output 写入)。"""
|
|
170
|
+
|
|
171
|
+
input_messages: List[Dict[str, Any]] = field(default_factory=list)
|
|
172
|
+
output_messages: List[Dict[str, Any]] = field(default_factory=list)
|