arize-phoenix 2.11.1__py3-none-any.whl → 3.0.1__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.
Potentially problematic release.
This version of arize-phoenix might be problematic. Click here for more details.
- {arize_phoenix-2.11.1.dist-info → arize_phoenix-3.0.1.dist-info}/METADATA +25 -22
- {arize_phoenix-2.11.1.dist-info → arize_phoenix-3.0.1.dist-info}/RECORD +15 -16
- phoenix/config.py +5 -3
- phoenix/trace/exporter.py +24 -9
- phoenix/trace/langchain/__init__.py +25 -3
- phoenix/trace/langchain/instrumentor.py +23 -29
- phoenix/trace/langchain/tracer.py +32 -418
- phoenix/trace/llama_index/callback.py +21 -658
- phoenix/trace/openai/instrumentor.py +19 -676
- phoenix/trace/otel.py +8 -1
- phoenix/trace/tracer.py +84 -107
- phoenix/version.py +1 -1
- phoenix/trace/llama_index/streaming.py +0 -92
- {arize_phoenix-2.11.1.dist-info → arize_phoenix-3.0.1.dist-info}/WHEEL +0 -0
- {arize_phoenix-2.11.1.dist-info → arize_phoenix-3.0.1.dist-info}/licenses/IP_NOTICE +0 -0
- {arize_phoenix-2.11.1.dist-info → arize_phoenix-3.0.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,240 +1,27 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Callback handler for emitting trace data in OpenInference tracing format.
|
|
3
|
-
OpenInference tracing is an open standard for capturing and storing
|
|
4
|
-
LLM Application execution logs.
|
|
5
|
-
|
|
6
|
-
It enables production LLMapp servers to seamlessly integrate with LLM
|
|
7
|
-
observability solutions such as Arize and Phoenix.
|
|
8
|
-
|
|
9
|
-
For more information on the specification, see
|
|
10
|
-
https://github.com/Arize-ai/openinference
|
|
11
|
-
"""
|
|
12
|
-
import json
|
|
13
1
|
import logging
|
|
14
|
-
from
|
|
15
|
-
from
|
|
16
|
-
from datetime import datetime, timezone
|
|
2
|
+
from importlib.metadata import PackageNotFoundError
|
|
3
|
+
from importlib.util import find_spec
|
|
17
4
|
from typing import (
|
|
18
5
|
Any,
|
|
19
|
-
Callable,
|
|
20
|
-
Dict,
|
|
21
|
-
Iterable,
|
|
22
|
-
Iterator,
|
|
23
|
-
List,
|
|
24
|
-
Mapping,
|
|
25
|
-
Optional,
|
|
26
|
-
Tuple,
|
|
27
|
-
Union,
|
|
28
|
-
cast,
|
|
29
|
-
)
|
|
30
|
-
from uuid import uuid4
|
|
31
|
-
|
|
32
|
-
import llama_index
|
|
33
|
-
from llama_index.callbacks.base_handler import BaseCallbackHandler
|
|
34
|
-
from llama_index.callbacks.schema import (
|
|
35
|
-
TIMESTAMP_FORMAT,
|
|
36
|
-
CBEvent,
|
|
37
|
-
CBEventType,
|
|
38
|
-
EventPayload,
|
|
39
6
|
)
|
|
40
|
-
from llama_index.llms.types import ChatMessage, ChatResponse
|
|
41
|
-
from llama_index.response.schema import Response, StreamingResponse
|
|
42
|
-
from llama_index.tools import ToolMetadata
|
|
43
|
-
from typing_extensions import TypeGuard
|
|
44
7
|
|
|
45
|
-
from
|
|
46
|
-
|
|
47
|
-
instrument_streaming_response as _instrument_streaming_response,
|
|
48
|
-
)
|
|
49
|
-
from phoenix.trace.schemas import (
|
|
50
|
-
MimeType,
|
|
51
|
-
Span,
|
|
52
|
-
SpanEvent,
|
|
53
|
-
SpanException,
|
|
54
|
-
SpanID,
|
|
55
|
-
SpanKind,
|
|
56
|
-
SpanStatusCode,
|
|
57
|
-
TraceID,
|
|
8
|
+
from openinference.instrumentation.llama_index._callback import (
|
|
9
|
+
OpenInferenceTraceCallbackHandler as _OpenInferenceTraceCallbackHandler,
|
|
58
10
|
)
|
|
59
|
-
from
|
|
60
|
-
|
|
61
|
-
DOCUMENT_ID,
|
|
62
|
-
DOCUMENT_METADATA,
|
|
63
|
-
DOCUMENT_SCORE,
|
|
64
|
-
EMBEDDING_EMBEDDINGS,
|
|
65
|
-
EMBEDDING_MODEL_NAME,
|
|
66
|
-
EMBEDDING_TEXT,
|
|
67
|
-
EMBEDDING_VECTOR,
|
|
68
|
-
INPUT_MIME_TYPE,
|
|
69
|
-
INPUT_VALUE,
|
|
70
|
-
LLM_INPUT_MESSAGES,
|
|
71
|
-
LLM_INVOCATION_PARAMETERS,
|
|
72
|
-
LLM_MODEL_NAME,
|
|
73
|
-
LLM_OUTPUT_MESSAGES,
|
|
74
|
-
LLM_PROMPT_TEMPLATE,
|
|
75
|
-
LLM_PROMPT_TEMPLATE_VARIABLES,
|
|
76
|
-
LLM_PROMPTS,
|
|
77
|
-
LLM_TOKEN_COUNT_COMPLETION,
|
|
78
|
-
LLM_TOKEN_COUNT_PROMPT,
|
|
79
|
-
LLM_TOKEN_COUNT_TOTAL,
|
|
80
|
-
MESSAGE_CONTENT,
|
|
81
|
-
MESSAGE_NAME,
|
|
82
|
-
MESSAGE_ROLE,
|
|
83
|
-
MESSAGE_TOOL_CALLS,
|
|
84
|
-
OUTPUT_MIME_TYPE,
|
|
85
|
-
OUTPUT_VALUE,
|
|
86
|
-
RERANKER_INPUT_DOCUMENTS,
|
|
87
|
-
RERANKER_MODEL_NAME,
|
|
88
|
-
RERANKER_OUTPUT_DOCUMENTS,
|
|
89
|
-
RERANKER_QUERY,
|
|
90
|
-
RERANKER_TOP_K,
|
|
91
|
-
RETRIEVAL_DOCUMENTS,
|
|
92
|
-
TOOL_CALL_FUNCTION_ARGUMENTS_JSON,
|
|
93
|
-
TOOL_CALL_FUNCTION_NAME,
|
|
94
|
-
TOOL_DESCRIPTION,
|
|
95
|
-
TOOL_NAME,
|
|
96
|
-
TOOL_PARAMETERS,
|
|
11
|
+
from openinference.instrumentation.llama_index.version import (
|
|
12
|
+
__version__,
|
|
97
13
|
)
|
|
98
|
-
from
|
|
99
|
-
from
|
|
100
|
-
from
|
|
101
|
-
|
|
102
|
-
LLAMA_INDEX_MINIMUM_VERSION_TRIPLET = (0, 9, 8)
|
|
103
|
-
logger = logging.getLogger(__name__)
|
|
104
|
-
logger.addHandler(logging.NullHandler())
|
|
105
|
-
|
|
106
|
-
CBEventID = str
|
|
107
|
-
_LOCAL_TZINFO = datetime.now().astimezone().tzinfo
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
@dataclass
|
|
111
|
-
class CBEventData:
|
|
112
|
-
name: Optional[str] = field(default=None)
|
|
113
|
-
event_type: Optional[CBEventType] = field(default=None)
|
|
114
|
-
start_event: Optional[CBEvent] = field(default=None)
|
|
115
|
-
end_event: Optional[CBEvent] = field(default=None)
|
|
116
|
-
attributes: Dict[str, Any] = field(default_factory=dict)
|
|
117
|
-
span_id: Optional[CBEventID] = field(default=None)
|
|
118
|
-
parent_id: Optional[CBEventID] = field(default=None)
|
|
119
|
-
trace_id: Optional[TraceID] = field(default=None)
|
|
120
|
-
streaming_event: bool = field(default=False)
|
|
121
|
-
|
|
122
|
-
def set_if_unset(self, key: str, value: Any) -> None:
|
|
123
|
-
if not getattr(self, key):
|
|
124
|
-
setattr(self, key, value)
|
|
125
|
-
|
|
14
|
+
from opentelemetry import trace as trace_api
|
|
15
|
+
from opentelemetry.sdk import trace as trace_sdk
|
|
16
|
+
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
|
|
126
17
|
|
|
127
|
-
|
|
128
|
-
|
|
18
|
+
from phoenix.trace.exporter import _OpenInferenceExporter
|
|
19
|
+
from phoenix.trace.tracer import _show_deprecation_warnings
|
|
129
20
|
|
|
130
|
-
|
|
131
|
-
def payload_to_semantic_attributes(
|
|
132
|
-
event_type: CBEventType,
|
|
133
|
-
payload: Dict[str, Any],
|
|
134
|
-
is_event_end: bool = False,
|
|
135
|
-
) -> Dict[str, Any]:
|
|
136
|
-
"""
|
|
137
|
-
Converts a LLMapp payload to a dictionary of semantic conventions compliant attributes.
|
|
138
|
-
"""
|
|
139
|
-
attributes: Dict[str, Any] = {}
|
|
140
|
-
if event_type in (CBEventType.NODE_PARSING, CBEventType.CHUNKING):
|
|
141
|
-
# TODO(maybe): handle these events
|
|
142
|
-
return attributes
|
|
143
|
-
if EventPayload.CHUNKS in payload and EventPayload.EMBEDDINGS in payload:
|
|
144
|
-
attributes[EMBEDDING_EMBEDDINGS] = [
|
|
145
|
-
{EMBEDDING_TEXT: text, EMBEDDING_VECTOR: vector}
|
|
146
|
-
for text, vector in zip(payload[EventPayload.CHUNKS], payload[EventPayload.EMBEDDINGS])
|
|
147
|
-
]
|
|
148
|
-
if event_type is not CBEventType.RERANKING and EventPayload.QUERY_STR in payload:
|
|
149
|
-
attributes[INPUT_VALUE] = payload[EventPayload.QUERY_STR]
|
|
150
|
-
attributes[INPUT_MIME_TYPE] = MimeType.TEXT
|
|
151
|
-
if event_type is not CBEventType.RERANKING and EventPayload.NODES in payload:
|
|
152
|
-
attributes[RETRIEVAL_DOCUMENTS] = [
|
|
153
|
-
{
|
|
154
|
-
DOCUMENT_ID: node_with_score.node.node_id,
|
|
155
|
-
DOCUMENT_SCORE: node_with_score.score,
|
|
156
|
-
DOCUMENT_CONTENT: node_with_score.node.text,
|
|
157
|
-
DOCUMENT_METADATA: node_with_score.node.metadata,
|
|
158
|
-
}
|
|
159
|
-
for node_with_score in payload[EventPayload.NODES]
|
|
160
|
-
]
|
|
161
|
-
if EventPayload.PROMPT in payload:
|
|
162
|
-
attributes[LLM_PROMPTS] = [payload[EventPayload.PROMPT]]
|
|
163
|
-
if EventPayload.MESSAGES in payload:
|
|
164
|
-
messages = payload[EventPayload.MESSAGES]
|
|
165
|
-
# Messages is only relevant to the LLM invocation
|
|
166
|
-
if event_type is CBEventType.LLM:
|
|
167
|
-
attributes[LLM_INPUT_MESSAGES] = [
|
|
168
|
-
_message_payload_to_attributes(message_data) for message_data in messages
|
|
169
|
-
]
|
|
170
|
-
elif event_type is CBEventType.AGENT_STEP and len(messages):
|
|
171
|
-
# the agent step contains a message that is actually the input
|
|
172
|
-
# akin to the query_str
|
|
173
|
-
attributes[INPUT_VALUE] = _message_payload_to_str(messages[0])
|
|
174
|
-
if response := (payload.get(EventPayload.RESPONSE) or payload.get(EventPayload.COMPLETION)):
|
|
175
|
-
attributes.update(_get_response_output(response))
|
|
176
|
-
if raw := getattr(response, "raw", None):
|
|
177
|
-
assert hasattr(raw, "get"), f"raw must be Mapping, found {type(raw)}"
|
|
178
|
-
attributes.update(_get_output_messages(raw))
|
|
179
|
-
if usage := raw.get("usage"):
|
|
180
|
-
# OpenAI token counts are available on raw.usage but can also be
|
|
181
|
-
# found in additional_kwargs. Thus the duplicate handling.
|
|
182
|
-
attributes.update(_get_token_counts(usage))
|
|
183
|
-
# Look for token counts in additional_kwargs of the completion payload
|
|
184
|
-
# This is needed for non-OpenAI models
|
|
185
|
-
if (additional_kwargs := getattr(response, "additional_kwargs", None)) is not None:
|
|
186
|
-
attributes.update(_get_token_counts(additional_kwargs))
|
|
187
|
-
if event_type is CBEventType.RERANKING:
|
|
188
|
-
if EventPayload.TOP_K in payload:
|
|
189
|
-
attributes[RERANKER_TOP_K] = payload[EventPayload.TOP_K]
|
|
190
|
-
if EventPayload.MODEL_NAME in payload:
|
|
191
|
-
attributes[RERANKER_MODEL_NAME] = payload[EventPayload.MODEL_NAME]
|
|
192
|
-
if EventPayload.QUERY_STR in payload:
|
|
193
|
-
attributes[RERANKER_QUERY] = payload[EventPayload.QUERY_STR]
|
|
194
|
-
if nodes := payload.get(EventPayload.NODES):
|
|
195
|
-
attributes[RERANKER_OUTPUT_DOCUMENTS if is_event_end else RERANKER_INPUT_DOCUMENTS] = [
|
|
196
|
-
{
|
|
197
|
-
DOCUMENT_ID: node_with_score.node.node_id,
|
|
198
|
-
DOCUMENT_SCORE: node_with_score.score,
|
|
199
|
-
DOCUMENT_CONTENT: node_with_score.node.text,
|
|
200
|
-
DOCUMENT_METADATA: node_with_score.node.metadata,
|
|
201
|
-
}
|
|
202
|
-
for node_with_score in nodes
|
|
203
|
-
]
|
|
204
|
-
if EventPayload.TOOL in payload:
|
|
205
|
-
tool_metadata = cast(ToolMetadata, payload.get(EventPayload.TOOL))
|
|
206
|
-
attributes[TOOL_NAME] = tool_metadata.name
|
|
207
|
-
attributes[TOOL_DESCRIPTION] = tool_metadata.description
|
|
208
|
-
attributes[TOOL_PARAMETERS] = tool_metadata.to_openai_tool()["function"]["parameters"]
|
|
209
|
-
if EventPayload.SERIALIZED in payload:
|
|
210
|
-
serialized = payload[EventPayload.SERIALIZED]
|
|
211
|
-
if event_type is CBEventType.EMBEDDING:
|
|
212
|
-
if model_name := serialized.get("model_name"):
|
|
213
|
-
attributes[EMBEDDING_MODEL_NAME] = model_name
|
|
214
|
-
if event_type is CBEventType.LLM:
|
|
215
|
-
if model_name := serialized.get("model"):
|
|
216
|
-
attributes[LLM_MODEL_NAME] = model_name
|
|
217
|
-
invocation_parameters = _extract_invocation_parameters(serialized)
|
|
218
|
-
invocation_parameters["model"] = model_name
|
|
219
|
-
attributes[LLM_INVOCATION_PARAMETERS] = json.dumps(invocation_parameters)
|
|
220
|
-
return attributes
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
def _extract_invocation_parameters(serialized: Mapping[str, Any]) -> Dict[str, Any]:
|
|
224
|
-
# FIXME: this is only based on openai. Other models have different parameters.
|
|
225
|
-
if not hasattr(serialized, "get"):
|
|
226
|
-
return {}
|
|
227
|
-
invocation_parameters: Dict[str, Any] = {}
|
|
228
|
-
additional_kwargs = serialized.get("additional_kwargs")
|
|
229
|
-
if additional_kwargs and isinstance(additional_kwargs, Mapping):
|
|
230
|
-
invocation_parameters.update(additional_kwargs)
|
|
231
|
-
for key in ("temperature", "max_tokens"):
|
|
232
|
-
if (value := serialized.get(key)) is not None:
|
|
233
|
-
invocation_parameters[key] = value
|
|
234
|
-
return invocation_parameters
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
235
22
|
|
|
236
23
|
|
|
237
|
-
class OpenInferenceTraceCallbackHandler(
|
|
24
|
+
class OpenInferenceTraceCallbackHandler(_OpenInferenceTraceCallbackHandler):
|
|
238
25
|
"""Callback handler for storing LLM application trace data in OpenInference format.
|
|
239
26
|
OpenInference is an open standard for capturing and storing AI model
|
|
240
27
|
inferences. It enables production LLMapp servers to seamlessly integrate
|
|
@@ -244,436 +31,12 @@ class OpenInferenceTraceCallbackHandler(BaseCallbackHandler):
|
|
|
244
31
|
https://github.com/Arize-ai/openinference
|
|
245
32
|
"""
|
|
246
33
|
|
|
247
|
-
def __init__(
|
|
248
|
-
self,
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (
|
|
253
|
-
hasattr(llama_index, "__version__")
|
|
254
|
-
and (version_triplet := extract_version_triplet(llama_index.__version__))
|
|
255
|
-
and version_triplet < LLAMA_INDEX_MINIMUM_VERSION_TRIPLET
|
|
256
|
-
):
|
|
257
|
-
raise NotImplementedError(
|
|
258
|
-
f"minimum supported version of llama-index is "
|
|
259
|
-
f"{'.'.join(map(str, LLAMA_INDEX_MINIMUM_VERSION_TRIPLET))}, "
|
|
260
|
-
f"but yours is {llama_index.__version__}. "
|
|
261
|
-
f"you can update to the latest using `pip install llama-index --upgrade`, "
|
|
262
|
-
f"or install the minimum required for Phoenix using "
|
|
263
|
-
f'`pip install "arize-phoenix[llama-index]"`',
|
|
34
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
35
|
+
_show_deprecation_warnings(self, *args, **kwargs)
|
|
36
|
+
if find_spec("llama_index") is None:
|
|
37
|
+
raise PackageNotFoundError(
|
|
38
|
+
"Missing `llama-index`. Install with `pip install llama-index`."
|
|
264
39
|
)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
def _null_fallback(self, *args: Any, **kwargs: Any) -> None:
|
|
270
|
-
return
|
|
271
|
-
|
|
272
|
-
def _on_event_fallback(
|
|
273
|
-
self,
|
|
274
|
-
event_type: CBEventType,
|
|
275
|
-
payload: Optional[Dict[str, Any]] = None,
|
|
276
|
-
event_id: CBEventID = "",
|
|
277
|
-
**kwargs: Any,
|
|
278
|
-
) -> CBEventID:
|
|
279
|
-
return event_id or str(uuid4())
|
|
280
|
-
|
|
281
|
-
@graceful_fallback(_on_event_fallback)
|
|
282
|
-
def on_event_start(
|
|
283
|
-
self,
|
|
284
|
-
event_type: CBEventType,
|
|
285
|
-
payload: Optional[Dict[str, Any]] = None,
|
|
286
|
-
event_id: CBEventID = "",
|
|
287
|
-
parent_id: CBEventID = "",
|
|
288
|
-
**kwargs: Any,
|
|
289
|
-
) -> CBEventID:
|
|
290
|
-
event_id = event_id or str(uuid4())
|
|
291
|
-
if parent_data := self._event_id_to_event_data.get(parent_id):
|
|
292
|
-
trace_id = parent_data.trace_id
|
|
293
|
-
else:
|
|
294
|
-
trace_id = TraceID(uuid4())
|
|
295
|
-
event_data = self._event_id_to_event_data[event_id]
|
|
296
|
-
event_data.name = event_type.value
|
|
297
|
-
event_data.event_type = event_type
|
|
298
|
-
event_data.parent_id = None if parent_id == "root" else parent_id
|
|
299
|
-
event_data.span_id = event_id
|
|
300
|
-
event_data.trace_id = trace_id
|
|
301
|
-
event_data.start_event = CBEvent(
|
|
302
|
-
event_type=event_type,
|
|
303
|
-
payload=payload,
|
|
304
|
-
id_=event_id,
|
|
305
|
-
)
|
|
306
|
-
# Parse the payload to extract the parameters
|
|
307
|
-
if payload is not None:
|
|
308
|
-
event_data.attributes.update(
|
|
309
|
-
payload_to_semantic_attributes(event_type, payload),
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
return event_id
|
|
313
|
-
|
|
314
|
-
@graceful_fallback(_null_fallback)
|
|
315
|
-
def on_event_end(
|
|
316
|
-
self,
|
|
317
|
-
event_type: CBEventType,
|
|
318
|
-
payload: Optional[Dict[str, Any]] = None,
|
|
319
|
-
event_id: CBEventID = "",
|
|
320
|
-
**kwargs: Any,
|
|
321
|
-
) -> None:
|
|
322
|
-
event_data = self._event_id_to_event_data[event_id]
|
|
323
|
-
event_data.set_if_unset("name", event_type.value)
|
|
324
|
-
event_data.set_if_unset("event_type", event_type)
|
|
325
|
-
event_data.end_event = CBEvent(
|
|
326
|
-
event_type=event_type,
|
|
327
|
-
payload=payload,
|
|
328
|
-
id_=event_id,
|
|
329
|
-
)
|
|
330
|
-
|
|
331
|
-
# Parse the payload to extract the parameters
|
|
332
|
-
if payload is not None:
|
|
333
|
-
event_data.attributes.update(
|
|
334
|
-
payload_to_semantic_attributes(event_type, payload, is_event_end=True),
|
|
335
|
-
)
|
|
336
|
-
response = payload.get(EventPayload.RESPONSE)
|
|
337
|
-
if _is_streaming_response(response):
|
|
338
|
-
event_data.streaming_event = True
|
|
339
|
-
response = _instrument_streaming_response(response, self._tracer, event_data)
|
|
340
|
-
|
|
341
|
-
@graceful_fallback(_null_fallback)
|
|
342
|
-
def start_trace(self, trace_id: Optional[str] = None) -> None:
|
|
343
|
-
self._event_id_to_event_data = defaultdict(lambda: CBEventData())
|
|
344
|
-
|
|
345
|
-
@graceful_fallback(_null_fallback)
|
|
346
|
-
def end_trace(
|
|
347
|
-
self,
|
|
348
|
-
trace_id: Optional[str] = None,
|
|
349
|
-
trace_map: Optional[ChildEventIds] = None,
|
|
350
|
-
) -> None:
|
|
351
|
-
if not trace_map:
|
|
352
|
-
return # TODO: investigate when empty or None trace_map is passed
|
|
353
|
-
_add_spans_to_tracer(
|
|
354
|
-
event_id_to_event_data=self._event_id_to_event_data,
|
|
355
|
-
trace_map=trace_map,
|
|
356
|
-
tracer=self._tracer,
|
|
357
|
-
)
|
|
358
|
-
self._event_id_to_event_data = defaultdict(lambda: CBEventData())
|
|
359
|
-
|
|
360
|
-
def get_spans(self) -> Iterator[Span]:
|
|
361
|
-
"""
|
|
362
|
-
Returns the spans stored in the tracer. This is useful if you are running
|
|
363
|
-
LlamaIndex in a notebook environment and you want to inspect the spans.
|
|
364
|
-
"""
|
|
365
|
-
return self._tracer.get_spans()
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
def _add_spans_to_tracer(
|
|
369
|
-
event_id_to_event_data: EventData,
|
|
370
|
-
trace_map: ChildEventIds,
|
|
371
|
-
tracer: Tracer,
|
|
372
|
-
) -> None:
|
|
373
|
-
"""
|
|
374
|
-
Adds event data to the tracer, where it is converted to a span and stored in a buffer.
|
|
375
|
-
|
|
376
|
-
Args:
|
|
377
|
-
event_id_to_event_data (EventData): A map of event IDs to event data.
|
|
378
|
-
|
|
379
|
-
trace_map (ChildEventIds): A map of parent event IDs to child event IDs. The root event IDs
|
|
380
|
-
are stored under the key "root".
|
|
381
|
-
|
|
382
|
-
tracer (Tracer): The tracer that stores spans.
|
|
383
|
-
"""
|
|
384
|
-
|
|
385
|
-
parent_child_id_stack: List[Tuple[Optional[SpanID], CBEventID]] = [
|
|
386
|
-
(None, root_event_id) for root_event_id in trace_map["root"]
|
|
387
|
-
]
|
|
388
|
-
while parent_child_id_stack:
|
|
389
|
-
parent_span_id, event_id = parent_child_id_stack.pop()
|
|
390
|
-
event_data = event_id_to_event_data[event_id]
|
|
391
|
-
event_type = event_data.event_type
|
|
392
|
-
attributes = event_data.attributes
|
|
393
|
-
if not (start_event := event_data.start_event):
|
|
394
|
-
# if the callback system has broken its contract by calling
|
|
395
|
-
# on_event_end without on_event_start, do not create a span
|
|
396
|
-
continue
|
|
397
|
-
|
|
398
|
-
if event_type is CBEventType.LLM:
|
|
399
|
-
while parent_child_id_stack:
|
|
400
|
-
preceding_event_parent_span_id, preceding_event_id = parent_child_id_stack[-1]
|
|
401
|
-
if preceding_event_parent_span_id != parent_span_id:
|
|
402
|
-
break
|
|
403
|
-
preceding_event_data = event_id_to_event_data[preceding_event_id]
|
|
404
|
-
if preceding_event_data.event_type is not CBEventType.TEMPLATING:
|
|
405
|
-
break
|
|
406
|
-
parent_child_id_stack.pop()
|
|
407
|
-
if preceding_event_start := preceding_event_data.start_event:
|
|
408
|
-
if preceding_payload := preceding_event_start.payload:
|
|
409
|
-
# Add template attributes to the LLM span to which they belong.
|
|
410
|
-
attributes.update(_template_attributes(preceding_payload))
|
|
411
|
-
|
|
412
|
-
start_time = _timestamp_to_tz_aware_datetime(start_event.time)
|
|
413
|
-
span_exceptions = _get_span_exceptions(event_data, start_time)
|
|
414
|
-
if event_data.streaming_event:
|
|
415
|
-
# Do not set the end time for streaming events so we can update the event later
|
|
416
|
-
end_time = None
|
|
417
|
-
else:
|
|
418
|
-
end_time = _get_end_time(event_data, span_exceptions)
|
|
419
|
-
start_time = start_time or end_time or datetime.now(timezone.utc)
|
|
420
|
-
|
|
421
|
-
name = event_name if (event_name := event_data.name) is not None else "unknown"
|
|
422
|
-
span_kind = _get_span_kind(event_type)
|
|
423
|
-
span = tracer.create_span(
|
|
424
|
-
name=name,
|
|
425
|
-
span_kind=span_kind,
|
|
426
|
-
trace_id=event_data.trace_id,
|
|
427
|
-
start_time=start_time,
|
|
428
|
-
end_time=end_time,
|
|
429
|
-
status_code=SpanStatusCode.ERROR if span_exceptions else SpanStatusCode.OK,
|
|
430
|
-
status_message="",
|
|
431
|
-
parent_id=parent_span_id,
|
|
432
|
-
attributes=attributes,
|
|
433
|
-
events=sorted(span_exceptions, key=lambda event: event.timestamp) or None,
|
|
434
|
-
conversation=None,
|
|
435
|
-
span_id=SpanID(event_data.span_id),
|
|
436
|
-
)
|
|
437
|
-
new_parent_span_id = span.context.span_id
|
|
438
|
-
for new_child_event_id in trace_map.get(event_id, []):
|
|
439
|
-
parent_child_id_stack.append((new_parent_span_id, new_child_event_id))
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
def _get_span_kind(event_type: Optional[CBEventType]) -> SpanKind:
|
|
443
|
-
"""Maps a CBEventType to a SpanKind.
|
|
444
|
-
|
|
445
|
-
Args:
|
|
446
|
-
event_type (CBEventType): LlamaIndex callback event type.
|
|
447
|
-
|
|
448
|
-
Returns:
|
|
449
|
-
SpanKind: The corresponding span kind.
|
|
450
|
-
"""
|
|
451
|
-
if event_type is None:
|
|
452
|
-
return SpanKind.UNKNOWN
|
|
453
|
-
return {
|
|
454
|
-
CBEventType.EMBEDDING: SpanKind.EMBEDDING,
|
|
455
|
-
CBEventType.LLM: SpanKind.LLM,
|
|
456
|
-
CBEventType.RETRIEVE: SpanKind.RETRIEVER,
|
|
457
|
-
CBEventType.FUNCTION_CALL: SpanKind.TOOL,
|
|
458
|
-
CBEventType.AGENT_STEP: SpanKind.AGENT,
|
|
459
|
-
CBEventType.RERANKING: SpanKind.RERANKER,
|
|
460
|
-
}.get(event_type, SpanKind.CHAIN)
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
def _message_payload_to_attributes(message: Any) -> Dict[str, Optional[str]]:
|
|
464
|
-
if isinstance(message, ChatMessage):
|
|
465
|
-
message_attributes = {
|
|
466
|
-
MESSAGE_ROLE: message.role.value,
|
|
467
|
-
MESSAGE_CONTENT: message.content,
|
|
468
|
-
}
|
|
469
|
-
# Parse the kwargs to extract the function name and parameters for function calling
|
|
470
|
-
# NB: these additional kwargs exist both for 'agent' and 'function' roles
|
|
471
|
-
if "name" in message.additional_kwargs:
|
|
472
|
-
message_attributes[MESSAGE_NAME] = message.additional_kwargs["name"]
|
|
473
|
-
if tool_calls := message.additional_kwargs.get("tool_calls"):
|
|
474
|
-
assert isinstance(
|
|
475
|
-
tool_calls, Iterable
|
|
476
|
-
), f"tool_calls must be Iterable, found {type(tool_calls)}"
|
|
477
|
-
message_tool_calls = []
|
|
478
|
-
for tool_call in tool_calls:
|
|
479
|
-
if message_tool_call := dict(_get_tool_call(tool_call)):
|
|
480
|
-
message_tool_calls.append(message_tool_call)
|
|
481
|
-
if message_tool_calls:
|
|
482
|
-
message_attributes[MESSAGE_TOOL_CALLS] = message_tool_calls
|
|
483
|
-
return message_attributes
|
|
484
|
-
|
|
485
|
-
return {
|
|
486
|
-
MESSAGE_ROLE: "user", # assume user if not ChatMessage
|
|
487
|
-
MESSAGE_CONTENT: str(message),
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
def _message_payload_to_str(message: Any) -> Optional[str]:
|
|
492
|
-
"""Converts a message payload to a string, if possible"""
|
|
493
|
-
if isinstance(message, ChatMessage):
|
|
494
|
-
return message.content
|
|
495
|
-
|
|
496
|
-
return str(message)
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
class _CustomJSONEncoder(json.JSONEncoder):
|
|
500
|
-
def default(self, obj: object) -> Any:
|
|
501
|
-
try:
|
|
502
|
-
return super().default(obj)
|
|
503
|
-
except TypeError:
|
|
504
|
-
if callable(as_dict := getattr(obj, "dict", None)):
|
|
505
|
-
return as_dict()
|
|
506
|
-
raise
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
def _get_response_output(response: Any) -> Iterator[Tuple[str, Any]]:
|
|
510
|
-
"""
|
|
511
|
-
Gets output from response objects. This is needed since the string representation of some
|
|
512
|
-
response objects includes extra information in addition to the content itself. In the
|
|
513
|
-
case of an agent's ChatResponse the output may be a `function_call` object specifying
|
|
514
|
-
the name of the function to call and the arguments to call it with.
|
|
515
|
-
"""
|
|
516
|
-
if isinstance(response, ChatResponse):
|
|
517
|
-
message = response.message
|
|
518
|
-
if content := message.content:
|
|
519
|
-
yield OUTPUT_VALUE, content
|
|
520
|
-
yield OUTPUT_MIME_TYPE, MimeType.TEXT
|
|
521
|
-
else:
|
|
522
|
-
yield OUTPUT_VALUE, json.dumps(message.additional_kwargs, cls=_CustomJSONEncoder)
|
|
523
|
-
yield OUTPUT_MIME_TYPE, MimeType.JSON
|
|
524
|
-
elif isinstance(response, Response):
|
|
525
|
-
yield OUTPUT_VALUE, response.response or ""
|
|
526
|
-
yield OUTPUT_MIME_TYPE, MimeType.TEXT
|
|
527
|
-
elif isinstance(response, StreamingResponse):
|
|
528
|
-
# We cannot get the output from a streaming response without exhausting
|
|
529
|
-
# the stream, so we initially return an empty string. Additional work is
|
|
530
|
-
# needed to instrument the returned response object to update the span
|
|
531
|
-
# with the actual response once the stream has been exhausted:
|
|
532
|
-
# https://github.com/Arize-ai/phoenix/issues/1867
|
|
533
|
-
yield OUTPUT_VALUE, ""
|
|
534
|
-
yield OUTPUT_MIME_TYPE, MimeType.TEXT
|
|
535
|
-
else: # if the response has unknown type, make a best-effort attempt to get the output
|
|
536
|
-
yield OUTPUT_VALUE, str(response)
|
|
537
|
-
yield OUTPUT_MIME_TYPE, MimeType.TEXT
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
def _get_end_time(event_data: CBEventData, span_events: Iterable[SpanEvent]) -> Optional[datetime]:
|
|
541
|
-
"""
|
|
542
|
-
A best-effort attempt to get the end time of an event.
|
|
543
|
-
|
|
544
|
-
LlamaIndex's callback system does not guarantee that the on_event_end hook is always called, for
|
|
545
|
-
example, when an error occurs mid-event.
|
|
546
|
-
"""
|
|
547
|
-
if end_event := event_data.end_event:
|
|
548
|
-
tz_naive_end_time = _timestamp_to_tz_naive_datetime(end_event.time)
|
|
549
|
-
elif span_events:
|
|
550
|
-
last_span_event = sorted(span_events, key=lambda event: event.timestamp)[-1]
|
|
551
|
-
tz_naive_end_time = last_span_event.timestamp
|
|
552
|
-
else:
|
|
553
|
-
return None
|
|
554
|
-
return _tz_naive_to_tz_aware_datetime(tz_naive_end_time)
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
def _get_span_exceptions(event_data: CBEventData, start_time: datetime) -> List[SpanException]:
|
|
558
|
-
"""Collects exceptions from the start and end events, if present."""
|
|
559
|
-
span_exceptions = []
|
|
560
|
-
for event in [event_data.start_event, event_data.end_event]:
|
|
561
|
-
if event and (payload := event.payload) and (error := payload.get(EventPayload.EXCEPTION)):
|
|
562
|
-
span_exceptions.append(
|
|
563
|
-
SpanException(
|
|
564
|
-
message=str(error),
|
|
565
|
-
timestamp=start_time,
|
|
566
|
-
exception_type=type(error).__name__,
|
|
567
|
-
exception_stacktrace=get_stacktrace(error),
|
|
568
|
-
)
|
|
569
|
-
)
|
|
570
|
-
return span_exceptions
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
def _timestamp_to_tz_aware_datetime(timestamp: str) -> datetime:
|
|
574
|
-
"""Converts a timestamp string to a timezone-aware datetime."""
|
|
575
|
-
return _tz_naive_to_tz_aware_datetime(_timestamp_to_tz_naive_datetime(timestamp))
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
def _timestamp_to_tz_naive_datetime(timestamp: str) -> datetime:
|
|
579
|
-
"""Converts a timestamp string to a timezone-naive datetime."""
|
|
580
|
-
return datetime.strptime(timestamp, TIMESTAMP_FORMAT)
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
def _tz_naive_to_tz_aware_datetime(timestamp: datetime) -> datetime:
|
|
584
|
-
"""Converts a timezone-naive datetime to a timezone-aware datetime."""
|
|
585
|
-
return timestamp.replace(tzinfo=_LOCAL_TZINFO)
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
def _get_message(message: object) -> Iterator[Tuple[str, Any]]:
|
|
589
|
-
if role := getattr(message, "role", None):
|
|
590
|
-
assert isinstance(role, str), f"content must be str, found {type(role)}"
|
|
591
|
-
yield MESSAGE_ROLE, role
|
|
592
|
-
if content := getattr(message, "content", None):
|
|
593
|
-
assert isinstance(content, str), f"content must be str, found {type(content)}"
|
|
594
|
-
yield MESSAGE_CONTENT, content
|
|
595
|
-
if tool_calls := getattr(message, "tool_calls", None):
|
|
596
|
-
assert isinstance(
|
|
597
|
-
tool_calls, Iterable
|
|
598
|
-
), f"tool_calls must be Iterable, found {type(tool_calls)}"
|
|
599
|
-
message_tool_calls = []
|
|
600
|
-
for tool_call in tool_calls:
|
|
601
|
-
if message_tool_call := dict(_get_tool_call(tool_call)):
|
|
602
|
-
message_tool_calls.append(message_tool_call)
|
|
603
|
-
if message_tool_calls:
|
|
604
|
-
yield MESSAGE_TOOL_CALLS, message_tool_calls
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
def _get_output_messages(raw: Mapping[str, Any]) -> Iterator[Tuple[str, Any]]:
|
|
608
|
-
assert hasattr(raw, "get"), f"raw must be Mapping, found {type(raw)}"
|
|
609
|
-
if not (choices := raw.get("choices")):
|
|
610
|
-
return
|
|
611
|
-
assert isinstance(choices, Iterable), f"choices must be Iterable, found {type(choices)}"
|
|
612
|
-
messages = [
|
|
613
|
-
dict(_get_message(message))
|
|
614
|
-
for choice in choices
|
|
615
|
-
if (message := getattr(choice, "message", None)) is not None
|
|
616
|
-
]
|
|
617
|
-
yield LLM_OUTPUT_MESSAGES, messages
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
def _get_token_counts(usage: Union[object, Mapping[str, Any]]) -> Iterator[Tuple[str, Any]]:
|
|
621
|
-
"""
|
|
622
|
-
Yields token count attributes from a object or mapping
|
|
623
|
-
"""
|
|
624
|
-
# Call the appropriate function based on the type of usage
|
|
625
|
-
if isinstance(usage, Mapping):
|
|
626
|
-
yield from _get_token_counts_from_mapping(usage)
|
|
627
|
-
elif isinstance(usage, object):
|
|
628
|
-
yield from _get_token_counts_from_object(usage)
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
def _get_token_counts_from_object(usage: object) -> Iterator[Tuple[str, Any]]:
|
|
632
|
-
"""
|
|
633
|
-
Yields token count attributes from response.raw.usage
|
|
634
|
-
"""
|
|
635
|
-
if (prompt_tokens := getattr(usage, "prompt_tokens", None)) is not None:
|
|
636
|
-
yield LLM_TOKEN_COUNT_PROMPT, prompt_tokens
|
|
637
|
-
if (completion_tokens := getattr(usage, "completion_tokens", None)) is not None:
|
|
638
|
-
yield LLM_TOKEN_COUNT_COMPLETION, completion_tokens
|
|
639
|
-
if (total_tokens := getattr(usage, "total_tokens", None)) is not None:
|
|
640
|
-
yield LLM_TOKEN_COUNT_TOTAL, total_tokens
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
def _get_token_counts_from_mapping(
|
|
644
|
-
usage_mapping: Mapping[str, Any],
|
|
645
|
-
) -> Iterator[Tuple[str, Any]]:
|
|
646
|
-
"""
|
|
647
|
-
Yields token count attributes from a mapping (e.x. completion kwargs payload)
|
|
648
|
-
"""
|
|
649
|
-
if (prompt_tokens := usage_mapping.get("prompt_tokens")) is not None:
|
|
650
|
-
yield LLM_TOKEN_COUNT_PROMPT, prompt_tokens
|
|
651
|
-
if (completion_tokens := usage_mapping.get("completion_tokens")) is not None:
|
|
652
|
-
yield LLM_TOKEN_COUNT_COMPLETION, completion_tokens
|
|
653
|
-
if (total_tokens := usage_mapping.get("total_tokens")) is not None:
|
|
654
|
-
yield LLM_TOKEN_COUNT_TOTAL, total_tokens
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
def _template_attributes(payload: Dict[str, Any]) -> Iterator[Tuple[str, Any]]:
|
|
658
|
-
"""Yields template attributes if present"""
|
|
659
|
-
if template := payload.get(EventPayload.TEMPLATE):
|
|
660
|
-
yield LLM_PROMPT_TEMPLATE, template
|
|
661
|
-
if template_vars := payload.get(EventPayload.TEMPLATE_VARS):
|
|
662
|
-
yield LLM_PROMPT_TEMPLATE_VARIABLES, template_vars
|
|
663
|
-
# TODO(maybe): other keys in the same payload
|
|
664
|
-
# EventPayload.SYSTEM_PROMPT
|
|
665
|
-
# EventPayload.QUERY_WRAPPER_PROMPT
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
def _get_tool_call(tool_call: object) -> Iterator[Tuple[str, Any]]:
|
|
669
|
-
if function := getattr(tool_call, "function", None):
|
|
670
|
-
if name := getattr(function, "name", None):
|
|
671
|
-
assert isinstance(name, str), f"name must be str, found {type(name)}"
|
|
672
|
-
yield TOOL_CALL_FUNCTION_NAME, name
|
|
673
|
-
if arguments := getattr(function, "arguments", None):
|
|
674
|
-
assert isinstance(arguments, str), f"arguments must be str, found {type(arguments)}"
|
|
675
|
-
yield TOOL_CALL_FUNCTION_ARGUMENTS_JSON, arguments
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
def _is_streaming_response(response: Any) -> TypeGuard[StreamingResponse]:
|
|
679
|
-
return isinstance(response, StreamingResponse)
|
|
40
|
+
tracer_provider = trace_sdk.TracerProvider()
|
|
41
|
+
tracer_provider.add_span_processor(SimpleSpanProcessor(_OpenInferenceExporter()))
|
|
42
|
+
super().__init__(trace_api.get_tracer(__name__, __version__, tracer_provider))
|