Skip to content

Commit 08e2b07

Browse files
committed
GenAI Utils | Agent Base Type and Creation Span
1 parent 1eca3e6 commit 08e2b07

5 files changed

Lines changed: 896 additions & 1 deletion

File tree

util/opentelemetry-util-genai/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
- Add `AgentInvocation` type and agent invocation lifecycle support
11+
([#4274](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4274))
1012
- Add support for emitting inference events and enrich message types. ([#3994](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3994))
1113
- Add support for `server.address`, `server.port` on all signals and additional metric-only attributes
1214
([#4069](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4069))

util/opentelemetry-util-genai/src/opentelemetry/util/genai/handler.py

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,18 @@
8080
)
8181
from opentelemetry.util.genai.metrics import InvocationMetricsRecorder
8282
from opentelemetry.util.genai.span_utils import (
83+
_apply_agent_finish_attributes,
84+
_apply_creation_finish_attributes,
8385
_apply_error_attributes,
8486
_apply_llm_finish_attributes,
8587
_maybe_emit_llm_event,
8688
)
87-
from opentelemetry.util.genai.types import Error, LLMInvocation
89+
from opentelemetry.util.genai.types import (
90+
AgentCreation,
91+
AgentInvocation,
92+
Error,
93+
LLMInvocation,
94+
)
8895
from opentelemetry.util.genai.version import __version__
8996

9097

@@ -208,6 +215,143 @@ def llm(
208215
raise
209216
self.stop_llm(invocation)
210217

218+
# ---- Agent invocation lifecycle ----
219+
220+
def start_agent(
221+
self,
222+
invocation: AgentInvocation,
223+
) -> AgentInvocation:
224+
"""Start an agent invocation and create a pending span entry."""
225+
span_name = f"{invocation.operation_name} {invocation.agent_name}".strip()
226+
kind = SpanKind.CLIENT if invocation.is_remote else SpanKind.INTERNAL
227+
span = self._tracer.start_span(
228+
name=span_name,
229+
kind=kind,
230+
)
231+
invocation.monotonic_start_s = timeit.default_timer()
232+
invocation.span = span
233+
invocation.context_token = otel_context.attach(
234+
set_span_in_context(span)
235+
)
236+
return invocation
237+
238+
def stop_agent(self, invocation: AgentInvocation) -> AgentInvocation: # pylint: disable=no-self-use
239+
"""Finalize an agent invocation successfully and end its span."""
240+
if invocation.context_token is None or invocation.span is None:
241+
return invocation
242+
243+
span = invocation.span
244+
_apply_agent_finish_attributes(span, invocation)
245+
otel_context.detach(invocation.context_token)
246+
span.end()
247+
return invocation
248+
249+
def fail_agent( # pylint: disable=no-self-use
250+
self, invocation: AgentInvocation, error: Error
251+
) -> AgentInvocation:
252+
"""Fail an agent invocation and end its span with error status."""
253+
if invocation.context_token is None or invocation.span is None:
254+
return invocation
255+
256+
span = invocation.span
257+
_apply_agent_finish_attributes(span, invocation)
258+
_apply_error_attributes(span, error)
259+
otel_context.detach(invocation.context_token)
260+
span.end()
261+
return invocation
262+
263+
@contextmanager
264+
def agent(
265+
self, invocation: AgentInvocation | None = None
266+
) -> Iterator[AgentInvocation]:
267+
"""Context manager for agent invocations.
268+
269+
Only set data attributes on the invocation object, do not modify the span or context.
270+
271+
Starts the span on entry. On normal exit, finalizes the invocation and ends the span.
272+
If an exception occurs inside the context, marks the span as error, ends it, and
273+
re-raises the original exception.
274+
"""
275+
if invocation is None:
276+
invocation = AgentInvocation()
277+
self.start_agent(invocation)
278+
try:
279+
yield invocation
280+
except Exception as exc:
281+
self.fail_agent(
282+
invocation, Error(message=str(exc), type=type(exc))
283+
)
284+
raise
285+
self.stop_agent(invocation)
286+
287+
# ---- Agent creation lifecycle ----
288+
289+
def start_create_agent(
290+
self,
291+
creation: AgentCreation,
292+
) -> AgentCreation:
293+
"""Start an agent creation and create a pending span entry."""
294+
span_name = f"{creation.operation_name} {creation.agent_name}".strip()
295+
span = self._tracer.start_span(
296+
name=span_name,
297+
kind=SpanKind.CLIENT,
298+
)
299+
creation.monotonic_start_s = timeit.default_timer()
300+
creation.span = span
301+
creation.context_token = otel_context.attach(
302+
set_span_in_context(span)
303+
)
304+
return creation
305+
306+
def stop_create_agent(self, creation: AgentCreation) -> AgentCreation: # pylint: disable=no-self-use
307+
"""Finalize an agent creation successfully and end its span."""
308+
if creation.context_token is None or creation.span is None:
309+
return creation
310+
311+
span = creation.span
312+
_apply_creation_finish_attributes(span, creation)
313+
otel_context.detach(creation.context_token)
314+
span.end()
315+
return creation
316+
317+
def fail_create_agent( # pylint: disable=no-self-use
318+
self, creation: AgentCreation, error: Error
319+
) -> AgentCreation:
320+
"""Fail an agent creation and end its span with error status."""
321+
if creation.context_token is None or creation.span is None:
322+
return creation
323+
324+
span = creation.span
325+
_apply_creation_finish_attributes(span, creation)
326+
_apply_error_attributes(span, error)
327+
otel_context.detach(creation.context_token)
328+
span.end()
329+
return creation
330+
331+
@contextmanager
332+
def create_agent(
333+
self, creation: AgentCreation | None = None
334+
) -> Iterator[AgentCreation]:
335+
"""Context manager for agent creation.
336+
337+
Only set data attributes on the creation object, do not modify the span or context.
338+
339+
Starts the span on entry. On normal exit, finalizes the creation and ends the span.
340+
If an exception occurs inside the context, marks the span as error, ends it, and
341+
re-raises the original exception.
342+
"""
343+
if creation is None:
344+
creation = AgentCreation()
345+
self.start_create_agent(creation)
346+
try:
347+
yield creation
348+
except Exception as exc:
349+
self.fail_create_agent(
350+
creation, Error(message=str(exc), type=type(exc))
351+
)
352+
raise
353+
self.stop_create_agent(creation)
354+
211355

212356
def get_telemetry_handler(
213357
tracer_provider: TracerProvider | None = None,

0 commit comments

Comments
 (0)