-
Notifications
You must be signed in to change notification settings - Fork 931
GenAI Utils | Add support for ToolCall Invocations #4356
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 41 commits
321ea30
3de0e8d
b277b72
9b3f2a6
0f28588
db3802a
2f009f9
5e8d489
3064499
1287d96
865bdab
1bdd5c4
57898bf
432fc6c
b11de5c
9bd35dc
dc2b1b9
6512305
124abd0
73b8369
24b1721
dc4f7c2
828e954
e1fdd08
978d837
1f336d3
cdec2bc
e7f07f6
3730a3e
dbac1b6
ecfb339
d5997ec
70c7804
01ab8ae
e1bc9a2
7969654
a9f9eef
effdcc7
7175e89
e86926d
c90424b
7391f3b
8e6ce17
56d4373
e3bda93
198a620
e82b88f
1c18014
2fd6b28
d3b09a6
30c5869
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,21 +17,17 @@ | |
|
|
||
| This module exposes the `TelemetryHandler` class, which manages the lifecycle of | ||
| GenAI (Generative AI) invocations and emits telemetry data (spans and related attributes). | ||
| It supports starting, stopping, and failing LLM invocations. | ||
| It supports starting, stopping, and failing LLM invocations and tool call executions. | ||
|
|
||
| Classes: | ||
| - TelemetryHandler: Manages GenAI invocation lifecycles and emits telemetry. | ||
|
|
||
| Functions: | ||
| - get_telemetry_handler: Returns a singleton `TelemetryHandler` instance. | ||
|
|
||
| Usage: | ||
| Usage - LLM Invocations: | ||
| handler = get_telemetry_handler() | ||
|
|
||
| # Create an invocation object with your request data | ||
| # The span and context_token attributes are set by the TelemetryHandler, and | ||
| # managed by the TelemetryHandler during the lifecycle of the span. | ||
|
|
||
| # Use the context manager to manage the lifecycle of an LLM invocation. | ||
| with handler.llm(invocation) as invocation: | ||
| # Populate outputs and any additional attributes | ||
|
|
@@ -45,17 +41,24 @@ | |
| provider="my-provider", | ||
| attributes={"custom": "attr"}, | ||
| ) | ||
|
|
||
| # Start the invocation (opens a span) | ||
| handler.start_llm(invocation) | ||
|
|
||
| # Populate outputs and any additional attributes, then stop (closes the span) | ||
| invocation.output_messages = [...] | ||
| invocation.attributes.update({"more": "attrs"}) | ||
| handler.stop_llm(invocation) | ||
|
|
||
| # Or, in case of error | ||
| handler.fail_llm(invocation, Error(type="...", message="...")) | ||
| Usage - Tool Call Executions: | ||
| handler = get_telemetry_handler() | ||
|
|
||
| # Use the context manager to manage the lifecycle of a tool call. | ||
| tool = ToolCall(name="get_weather", arguments={"location": "Paris"}, id="call_123") | ||
| with handler.tool_call(tool) as tc: | ||
| # Execute tool logic | ||
| tc.tool_result = {"temp": 20, "condition": "sunny"} | ||
|
|
||
| # Or, manage the lifecycle manually | ||
| tool = ToolCall(name="get_weather", arguments={"location": "Paris"}) | ||
| handler.start(tool) | ||
| tool.tool_result = {"temp": 20} | ||
| handler.stop(tool) | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
@@ -78,20 +81,25 @@ | |
| get_tracer, | ||
| set_span_in_context, | ||
| ) | ||
| from opentelemetry.trace.status import Status, StatusCode | ||
| from opentelemetry.util.genai.metrics import InvocationMetricsRecorder | ||
| from opentelemetry.util.genai.span_utils import ( | ||
| _apply_embedding_finish_attributes, | ||
| _apply_error_attributes, | ||
| _apply_llm_finish_attributes, | ||
| _apply_tool_call_attributes, | ||
| _finish_tool_call_span, | ||
| _get_embedding_span_name, | ||
| _get_llm_span_name, | ||
| _get_tool_call_span_name, | ||
| _maybe_emit_llm_event, | ||
| ) | ||
| from opentelemetry.util.genai.types import ( | ||
| EmbeddingInvocation, | ||
| Error, | ||
| GenAIInvocation, | ||
| LLMInvocation, | ||
| ToolCall, | ||
| ) | ||
| from opentelemetry.util.genai.version import __version__ | ||
|
|
||
|
|
@@ -129,46 +137,46 @@ def __init__( | |
| schema_url=schema_url, | ||
| ) | ||
|
|
||
| def _record_llm_metrics( | ||
| def _record_metrics( | ||
| self, | ||
| invocation: LLMInvocation, | ||
| invocation: GenAIInvocation, | ||
| span: Span | None = None, | ||
| *, | ||
| error_type: str | None = None, | ||
| ) -> None: | ||
| """Record metrics for an invocation.""" | ||
| if self._metrics_recorder is None or span is None: | ||
| return | ||
| # Only LLMInvocation and ToolCall metrics are currently supported | ||
| if not isinstance(invocation, (LLMInvocation, ToolCall)): | ||
| return | ||
| self._metrics_recorder.record( | ||
| span, | ||
| invocation, | ||
| error_type=error_type, | ||
| ) | ||
|
|
||
| @staticmethod | ||
| def _record_embedding_metrics( | ||
| invocation: EmbeddingInvocation, | ||
| span: Span | None = None, | ||
| *, | ||
| error_type: str | None = None, | ||
| ) -> None: | ||
| # Metrics recorder currently supports LLMInvocation fields only. | ||
| # Keep embedding metrics as a no-op until dedicated embedding | ||
| # metric support is added. | ||
| return | ||
|
|
||
| def _start(self, invocation: _T) -> _T: | ||
| """Start a GenAI invocation and create a pending span entry.""" | ||
| span_kind = SpanKind.CLIENT | ||
| if isinstance(invocation, LLMInvocation): | ||
| span_name = _get_llm_span_name(invocation) | ||
| elif isinstance(invocation, EmbeddingInvocation): | ||
| span_name = _get_embedding_span_name(invocation) | ||
| elif isinstance(invocation, ToolCall): | ||
| span_name = _get_tool_call_span_name(invocation) | ||
| span_kind = SpanKind.INTERNAL | ||
| else: | ||
| span_name = "" | ||
|
|
||
| span = self._tracer.start_span( | ||
| name=span_name, | ||
| kind=SpanKind.CLIENT, | ||
| kind=span_kind, | ||
| ) | ||
| # Record a monotonic start timestamp (seconds) for duration | ||
| if isinstance(invocation, ToolCall): | ||
| _apply_tool_call_attributes( | ||
| span, invocation, capture_content=False | ||
| ) | ||
| # calculation using timeit.default_timer. | ||
| invocation.monotonic_start_s = timeit.default_timer() | ||
| invocation.span = span | ||
|
|
@@ -187,11 +195,14 @@ def _stop(self, invocation: _T) -> _T: | |
| try: | ||
| if isinstance(invocation, LLMInvocation): | ||
| _apply_llm_finish_attributes(span, invocation) | ||
| self._record_llm_metrics(invocation, span) | ||
| self._record_metrics(invocation, span) | ||
| _maybe_emit_llm_event(self._logger, span, invocation) | ||
| elif isinstance(invocation, EmbeddingInvocation): | ||
| _apply_embedding_finish_attributes(span, invocation) | ||
| self._record_embedding_metrics(invocation, span) | ||
| self._record_metrics(invocation, span) | ||
| elif isinstance(invocation, ToolCall): | ||
| _finish_tool_call_span(span, invocation, capture_content=True) | ||
| self._record_metrics(invocation, span) | ||
| finally: | ||
| # Detach context and end span even if finishing fails | ||
| otel_context.detach(invocation.context_token) | ||
|
|
@@ -210,18 +221,19 @@ def _fail(self, invocation: _T, error: Error) -> _T: | |
| if isinstance(invocation, LLMInvocation): | ||
| _apply_llm_finish_attributes(span, invocation) | ||
| _apply_error_attributes(span, error, error_type) | ||
| self._record_llm_metrics( | ||
| invocation, span, error_type=error_type | ||
| ) | ||
| self._record_metrics(invocation, span, error_type=error_type) | ||
| _maybe_emit_llm_event( | ||
| self._logger, span, invocation, error_type | ||
| ) | ||
| elif isinstance(invocation, EmbeddingInvocation): | ||
| _apply_embedding_finish_attributes(span, invocation) | ||
| _apply_error_attributes(span, error, error_type) | ||
| self._record_embedding_metrics( | ||
| invocation, span, error_type=error_type | ||
| ) | ||
| self._record_metrics(invocation, span, error_type=error_type) | ||
|
keith-decker marked this conversation as resolved.
Outdated
|
||
| elif isinstance(invocation, ToolCall): | ||
| invocation.error_type = error_type | ||
| _finish_tool_call_span(span, invocation, capture_content=True) | ||
| self._record_metrics(invocation, span, error_type=error_type) | ||
| span.set_status(Status(StatusCode.ERROR, error.message)) | ||
|
keith-decker marked this conversation as resolved.
Outdated
|
||
| finally: | ||
| # Detach context and end span even if finishing fails | ||
| otel_context.detach(invocation.context_token) | ||
|
|
@@ -258,6 +270,37 @@ def fail_llm( | |
| """Fail an LLM invocation and end its span with error status.""" | ||
| return self._fail(invocation, error) | ||
|
|
||
| @contextmanager | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the google gen AI instrumentation we monkey patch the actual function call: https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/tool_call_wrapper.py#L164 IDK if it makes sense to do something like that here or not (accept the actual Callable object)? Either way I'll try to update that instrumentation to use this once we submit |
||
| def tool_call( | ||
| self, tool_call: ToolCall | None = None | ||
| ) -> Iterator[ToolCall]: | ||
| """Context manager for tool call invocations. | ||
|
|
||
| Only set data attributes on the tool_call object, do not modify the span or context. | ||
|
|
||
| Starts the span on entry. On normal exit, finalizes the tool call and ends the span. | ||
| If an exception occurs inside the context, marks the span as error, ends it, and | ||
| re-raises the original exception. | ||
|
|
||
| Example: | ||
| with handler.tool_call(ToolCall(name="get_weather", arguments={"location": "Paris"})) as tc: | ||
| # Execute tool logic | ||
| tc.tool_result = {"temp": 20, "condition": "sunny"} | ||
| """ | ||
| if tool_call is None: | ||
|
keith-decker marked this conversation as resolved.
Outdated
|
||
| tool_call = ToolCall( | ||
| name="", | ||
| arguments={}, | ||
| id=None, | ||
| ) | ||
| self.start(tool_call) | ||
| try: | ||
| yield tool_call | ||
| except Exception as exc: | ||
| self.fail(tool_call, Error(message=str(exc), type=type(exc))) | ||
| raise | ||
| self.stop(tool_call) | ||
|
|
||
| @contextmanager | ||
| def llm( | ||
| self, invocation: LLMInvocation | None = None | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.