Skip to content

Commit bbcbded

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

5 files changed

Lines changed: 396 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 `_BaseAgent` shared base class and `AgentCreation` type for agent creation lifecycle spans
11+
([#4217](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4217))
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: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,16 @@
8080
)
8181
from opentelemetry.util.genai.metrics import InvocationMetricsRecorder
8282
from opentelemetry.util.genai.span_utils import (
83+
_apply_creation_finish_attributes,
8384
_apply_error_attributes,
8485
_apply_llm_finish_attributes,
8586
_maybe_emit_llm_event,
8687
)
87-
from opentelemetry.util.genai.types import Error, LLMInvocation
88+
from opentelemetry.util.genai.types import (
89+
AgentCreation,
90+
Error,
91+
LLMInvocation,
92+
)
8893
from opentelemetry.util.genai.version import __version__
8994

9095

@@ -208,6 +213,70 @@ def llm(
208213
raise
209214
self.stop_llm(invocation)
210215

216+
# ---- Agent lifecycle ----
217+
218+
def start_agent(
219+
self,
220+
agent: AgentCreation,
221+
) -> AgentCreation:
222+
"""Start an agent operation (create or invoke) and create a pending span entry."""
223+
span_name = f"{agent.operation_name} {agent.name}".strip()
224+
span = self._tracer.start_span(
225+
name=span_name,
226+
kind=SpanKind.CLIENT,
227+
)
228+
agent.monotonic_start_s = timeit.default_timer()
229+
agent.span = span
230+
agent.context_token = otel_context.attach(set_span_in_context(span))
231+
return agent
232+
233+
def stop_agent(self, agent: AgentCreation) -> AgentCreation: # pylint: disable=no-self-use
234+
"""Finalize an agent operation successfully and end its span."""
235+
if agent.context_token is None or agent.span is None:
236+
return agent
237+
238+
span = agent.span
239+
_apply_creation_finish_attributes(span, agent)
240+
otel_context.detach(agent.context_token)
241+
span.end()
242+
return agent
243+
244+
def fail_agent( # pylint: disable=no-self-use
245+
self, agent: AgentCreation, error: Error
246+
) -> AgentCreation:
247+
"""Fail an agent operation and end its span with error status."""
248+
if agent.context_token is None or agent.span is None:
249+
return agent
250+
251+
span = agent.span
252+
_apply_creation_finish_attributes(span, agent)
253+
_apply_error_attributes(span, error)
254+
otel_context.detach(agent.context_token)
255+
span.end()
256+
return agent
257+
258+
@contextmanager
259+
def create_agent(
260+
self, creation: AgentCreation | None = None
261+
) -> Iterator[AgentCreation]:
262+
"""Context manager for agent creation.
263+
264+
Only set data attributes on the creation object, do not modify the span or context.
265+
266+
Starts the span on entry. On normal exit, finalizes the creation and ends the span.
267+
If an exception occurs inside the context, marks the span as error, ends it, and
268+
re-raises the original exception.
269+
"""
270+
if creation is None:
271+
creation = AgentCreation()
272+
self.start_agent(creation)
273+
try:
274+
yield creation
275+
except Exception as exc:
276+
self.fail_agent(creation, Error(message=str(exc), type=type(exc)))
277+
raise
278+
self.stop_agent(creation)
279+
211280

212281
def get_telemetry_handler(
213282
tracer_provider: TracerProvider | None = None,

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

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,13 @@
3232
from opentelemetry.trace.propagation import set_span_in_context
3333
from opentelemetry.trace.status import Status, StatusCode
3434
from opentelemetry.util.genai.types import (
35+
AgentCreation,
3536
Error,
3637
InputMessage,
3738
LLMInvocation,
3839
MessagePart,
3940
OutputMessage,
41+
_BaseAgent,
4042
)
4143
from opentelemetry.util.genai.utils import (
4244
ContentCapturingMode,
@@ -47,6 +49,21 @@
4749
)
4850

4951

52+
def _agent_attr(name: str, fallback: str) -> str:
53+
"""Get a semconv attribute, falling back to a string literal if not yet in the package."""
54+
return getattr(GenAI, name, fallback)
55+
56+
57+
_GEN_AI_AGENT_NAME = _agent_attr("GEN_AI_AGENT_NAME", "gen_ai.agent.name")
58+
_GEN_AI_AGENT_ID = _agent_attr("GEN_AI_AGENT_ID", "gen_ai.agent.id")
59+
_GEN_AI_AGENT_DESCRIPTION = _agent_attr(
60+
"GEN_AI_AGENT_DESCRIPTION", "gen_ai.agent.description"
61+
)
62+
_GEN_AI_AGENT_VERSION = _agent_attr(
63+
"GEN_AI_AGENT_VERSION", "gen_ai.agent.version"
64+
)
65+
66+
5067
def _get_llm_common_attributes(
5168
invocation: LLMInvocation,
5269
) -> dict[str, Any]:
@@ -279,6 +296,75 @@ def _get_llm_response_attributes(
279296
return {key: value for key, value in optional_attrs if value is not None}
280297

281298

299+
def _get_base_agent_common_attributes(
300+
agent: _BaseAgent,
301+
) -> dict[str, Any]:
302+
"""Get common attributes shared by all agent operations (invoke_agent, create_agent)."""
303+
optional_attrs = (
304+
(GenAI.GEN_AI_REQUEST_MODEL, agent.model),
305+
(GenAI.GEN_AI_PROVIDER_NAME, agent.provider),
306+
(_GEN_AI_AGENT_NAME, agent.name),
307+
(_GEN_AI_AGENT_ID, agent.agent_id),
308+
(_GEN_AI_AGENT_DESCRIPTION, agent.description),
309+
(_GEN_AI_AGENT_VERSION, agent.version),
310+
(server_attributes.SERVER_ADDRESS, agent.server_address),
311+
(server_attributes.SERVER_PORT, agent.server_port),
312+
)
313+
314+
return {
315+
GenAI.GEN_AI_OPERATION_NAME: agent.operation_name,
316+
**{key: value for key, value in optional_attrs if value is not None},
317+
}
318+
319+
320+
def _get_base_agent_span_name(agent: _BaseAgent) -> str:
321+
"""Get the span name for any agent operation."""
322+
if agent.name:
323+
return f"{agent.operation_name} {agent.name}"
324+
return agent.operation_name
325+
326+
327+
def _get_creation_common_attributes(
328+
creation: AgentCreation,
329+
) -> dict[str, Any]:
330+
"""Get common agent creation attributes."""
331+
return _get_base_agent_common_attributes(creation)
332+
333+
334+
def _get_creation_span_name(creation: AgentCreation) -> str:
335+
"""Get the span name for an agent creation."""
336+
return _get_base_agent_span_name(creation)
337+
338+
339+
def _apply_creation_finish_attributes(
340+
span: Span, creation: AgentCreation
341+
) -> None:
342+
"""Apply attributes common to agent creation finish() paths."""
343+
span.update_name(_get_creation_span_name(creation))
344+
345+
attributes: dict[str, Any] = {}
346+
attributes.update(_get_creation_common_attributes(creation))
347+
348+
# System instructions (Opt-In)
349+
if (
350+
is_experimental_mode()
351+
and get_content_capturing_mode()
352+
in (
353+
ContentCapturingMode.SPAN_ONLY,
354+
ContentCapturingMode.SPAN_AND_EVENT,
355+
)
356+
and creation.system_instructions
357+
):
358+
attributes[GenAI.GEN_AI_SYSTEM_INSTRUCTIONS] = gen_ai_json_dumps(
359+
[asdict(p) for p in creation.system_instructions]
360+
)
361+
362+
attributes.update(creation.attributes)
363+
364+
if attributes:
365+
span.set_attributes(attributes)
366+
367+
282368
__all__ = [
283369
"_apply_llm_finish_attributes",
284370
"_apply_error_attributes",
@@ -287,4 +373,13 @@ def _get_llm_response_attributes(
287373
"_get_llm_response_attributes",
288374
"_get_llm_span_name",
289375
"_maybe_emit_llm_event",
376+
"_GEN_AI_AGENT_NAME",
377+
"_GEN_AI_AGENT_ID",
378+
"_GEN_AI_AGENT_DESCRIPTION",
379+
"_GEN_AI_AGENT_VERSION",
380+
"_get_base_agent_common_attributes",
381+
"_get_base_agent_span_name",
382+
"_apply_creation_finish_attributes",
383+
"_get_creation_common_attributes",
384+
"_get_creation_span_name",
290385
]

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,57 @@ class LLMInvocation(GenAIInvocation):
236236
monotonic_start_s: float | None = None
237237

238238

239+
@dataclass
240+
class _BaseAgent(GenAIInvocation):
241+
"""Shared base class for agent lifecycle types. Do not instantiate
242+
directly — use AgentCreation (or AgentInvocation in future).
243+
The span and context_token attributes are set by the TelemetryHandler.
244+
"""
245+
246+
# Agent identity
247+
name: str | None = None
248+
agent_id: str | None = None
249+
description: str | None = None
250+
version: str | None = None
251+
252+
# Operation
253+
operation_name: str = ""
254+
provider: str | None = None
255+
256+
# Request
257+
model: str | None = None # primary model if applicable
258+
259+
# Content (Opt-In)
260+
system_instructions: list[MessagePart] = field(
261+
default_factory=_new_system_instruction
262+
)
263+
264+
# Server
265+
server_address: str | None = None
266+
server_port: int | None = None
267+
268+
attributes: dict[str, Any] = field(default_factory=_new_str_any_dict)
269+
"""
270+
Additional attributes to set on spans and/or events.
271+
"""
272+
# Monotonic start time in seconds (from timeit.default_timer) used
273+
# for duration calculations to avoid mixing clock sources. This is
274+
# populated by the TelemetryHandler when starting an invocation.
275+
monotonic_start_s: float | None = None
276+
277+
278+
@dataclass
279+
class AgentCreation(_BaseAgent):
280+
"""
281+
Represents an agent creation/initialization. When creating an AgentCreation
282+
object, only update the data attributes. The span and context_token
283+
attributes are set by the TelemetryHandler.
284+
"""
285+
286+
# Override default operation name
287+
operation_name: str = GenAI.GenAiOperationNameValues.CREATE_AGENT.value
288+
289+
239290
@dataclass
240291
class Error:
241292
message: str

0 commit comments

Comments
 (0)