Skip to content

Commit 39ca34a

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

4 files changed

Lines changed: 404 additions & 1 deletion

File tree

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

Lines changed: 74 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,74 @@ def llm(
208213
raise
209214
self.stop_llm(invocation)
210215

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

212285
def get_telemetry_handler(
213286
tracer_provider: TracerProvider | None = None,

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

Lines changed: 91 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.request_model),
305+
(GenAI.GEN_AI_PROVIDER_NAME, agent.provider),
306+
(_GEN_AI_AGENT_NAME, agent.agent_name),
307+
(_GEN_AI_AGENT_ID, agent.agent_id),
308+
(_GEN_AI_AGENT_DESCRIPTION, agent.agent_description),
309+
(_GEN_AI_AGENT_VERSION, agent.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.agent_name:
323+
return f"{agent.operation_name} {agent.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_instruction
357+
):
358+
attributes[GenAI.GEN_AI_SYSTEM_INSTRUCTIONS] = gen_ai_json_dumps(
359+
[asdict(p) for p in creation.system_instruction]
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,9 @@ def _get_llm_response_attributes(
287373
"_get_llm_response_attributes",
288374
"_get_llm_span_name",
289375
"_maybe_emit_llm_event",
376+
"_get_base_agent_common_attributes",
377+
"_get_base_agent_span_name",
378+
"_apply_creation_finish_attributes",
379+
"_get_creation_common_attributes",
380+
"_get_creation_span_name",
290381
]

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

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

238238

239+
@dataclass
240+
class _BaseAgent(GenAIInvocation):
241+
"""
242+
Shared base class for agent lifecycle types (AgentInvocation, AgentCreation).
243+
244+
Contains fields common to all agent operations: identity, provider,
245+
model, system instructions, server info, and telemetry plumbing.
246+
247+
Follows semconv for GenAI agent spans:
248+
https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-agent-spans.md
249+
250+
Do not instantiate directly — use AgentInvocation or AgentCreation.
251+
"""
252+
253+
# Agent identity
254+
agent_name: str | None = None
255+
agent_id: str | None = None
256+
agent_description: str | None = None
257+
agent_version: str | None = None
258+
259+
# Operation
260+
operation_name: str = ""
261+
provider: str | None = None
262+
263+
# Request
264+
request_model: str | None = None
265+
266+
# Content (Opt-In)
267+
system_instruction: list[MessagePart] = field(
268+
default_factory=_new_system_instruction
269+
)
270+
271+
# Server
272+
server_address: str | None = None
273+
server_port: int | None = None
274+
275+
attributes: dict[str, Any] = field(default_factory=_new_str_any_dict)
276+
"""
277+
Additional attributes to set on spans and/or events.
278+
"""
279+
# Monotonic start time in seconds (from timeit.default_timer) used
280+
# for duration calculations to avoid mixing clock sources. This is
281+
# populated by the TelemetryHandler when starting an invocation.
282+
monotonic_start_s: float | None = None
283+
284+
285+
@dataclass
286+
class AgentCreation(_BaseAgent):
287+
"""
288+
Represents agent creation/initialization (create_agent operation).
289+
290+
Follows semconv for GenAI agent spans:
291+
https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-agent-spans.md#create-agent-span
292+
293+
When creating an AgentCreation object, only update the data attributes.
294+
The span and context_token attributes are set by the TelemetryHandler.
295+
"""
296+
297+
# Override default operation name
298+
operation_name: str = "create_agent"
299+
300+
239301
@dataclass
240302
class Error:
241303
message: str

0 commit comments

Comments
 (0)