Skip to content

Commit 928069a

Browse files
committed
fix(genai-utils): address review comments on AgentInvocation
1 parent 27dcb43 commit 928069a

7 files changed

Lines changed: 429 additions & 195 deletions

File tree

docs/nitpick-exceptions.ini

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ py-class=
4747
fastapi.applications.FastAPI
4848
starlette.applications.Starlette
4949
_contextvars.Token
50-
opentelemetry.util.genai.types._BaseAgent
5150

5251
any=
5352
; API

util/opentelemetry-util-genai/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10-
- Add `AgentInvocation` type with `invoke_agent` span lifecycle via `start_agent(provider, ...)` / `invoke_agent(provider, ...)` factory methods; `provider` is required per semconv, `response_id` removed (not in invoke_agent spec), `finish_reasons` deduced from `output_messages` when not explicitly set
10+
- Add `AgentInvocation` type with `invoke_agent` span lifecycle via `start_invoke_local_agent` / `start_invoke_remote_agent` factory methods and `invoke_local_agent` / `invoke_remote_agent` context managers; add shared `_content.py` helper to deduplicate message serialization between `AgentInvocation` and `InferenceInvocation`; add `tool_definitions`, `cache_creation_input_tokens`, and `cache_read_input_tokens` to `InferenceInvocation`
1111
([#4274](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4274))
1212
- Add support for workflow in genAI utils handler.
1313
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4366](#4366))

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

Lines changed: 24 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -14,41 +14,35 @@
1414

1515
from __future__ import annotations
1616

17-
from dataclasses import asdict
18-
from typing import Any, Union
17+
from typing import Any
1918

2019
from opentelemetry._logs import Logger
2120
from opentelemetry.semconv._incubating.attributes import (
2221
gen_ai_attributes as GenAI,
2322
)
2423
from opentelemetry.semconv.attributes import server_attributes
2524
from opentelemetry.trace import SpanKind, Tracer
25+
from opentelemetry.util.genai._content import _get_content_attributes
2626
from opentelemetry.util.genai._invocation import Error, GenAIInvocation
2727
from opentelemetry.util.genai.metrics import InvocationMetricsRecorder
2828
from opentelemetry.util.genai.types import (
29-
FunctionToolDefinition,
30-
GenericToolDefinition,
3129
InputMessage,
3230
MessagePart,
3331
OutputMessage,
32+
ToolDefinition,
3433
)
35-
from opentelemetry.util.genai.utils import (
36-
ContentCapturingMode,
37-
gen_ai_json_dumps,
38-
get_content_capturing_mode,
39-
is_experimental_mode,
40-
)
41-
42-
ToolDefinition = Union[FunctionToolDefinition, GenericToolDefinition]
4334

4435

4536
class AgentInvocation(GenAIInvocation):
4637
"""Represents a single agent invocation (invoke_agent span).
4738
48-
Use handler.start_agent() or the handler.invoke_agent() context manager
49-
rather than constructing this directly.
39+
Use handler.start_invoke_local_agent() / handler.start_invoke_remote_agent()
40+
or the handler.invoke_local_agent() / handler.invoke_remote_agent() context
41+
managers rather than constructing this directly.
5042
51-
Reference: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-agent-spans.md#invoke-agent-span
43+
Reference:
44+
Client span: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-agent-spans.md#invoke-agent-client-span
45+
Internal span: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-agent-spans.md#invoke-agent-internal-span
5246
"""
5347

5448
def __init__(
@@ -58,24 +52,22 @@ def __init__(
5852
logger: Logger,
5953
provider: str,
6054
*,
55+
span_kind: SpanKind = SpanKind.INTERNAL,
6156
request_model: str | None = None,
6257
server_address: str | None = None,
6358
server_port: int | None = None,
64-
agent_name: str | None = None,
6559
attributes: dict[str, Any] | None = None,
6660
metric_attributes: dict[str, Any] | None = None,
6761
) -> None:
68-
"""Use handler.start_agent() or handler.invoke_agent() instead of calling this directly."""
62+
"""Use handler.start_invoke_local_agent() or handler.start_invoke_remote_agent() instead of calling this directly."""
6963
_operation_name = GenAI.GenAiOperationNameValues.INVOKE_AGENT.value
7064
super().__init__(
7165
tracer,
7266
metrics_recorder,
7367
logger,
7468
operation_name=_operation_name,
75-
span_name=f"{_operation_name} {agent_name}"
76-
if agent_name
77-
else _operation_name,
78-
span_kind=SpanKind.CLIENT,
69+
span_name=_operation_name,
70+
span_kind=span_kind,
7971
attributes=attributes,
8072
metric_attributes=metric_attributes,
8173
)
@@ -84,7 +76,7 @@ def __init__(
8476
self.server_address = server_address
8577
self.server_port = server_port
8678

87-
self.agent_name = agent_name
79+
self.agent_name: str | None = None
8880
self.agent_id: str | None = None
8981
self.agent_description: str | None = None
9082
self.agent_version: str | None = None
@@ -102,8 +94,6 @@ def __init__(
10294
self.seed: int | None = None
10395
self.choice_count: int | None = None
10496

105-
self.finish_reasons: list[str] | None = None
106-
self.response_model_name: str | None = None
10797
self.input_tokens: int | None = None
10898
self.output_tokens: int | None = None
10999
self.cache_creation_input_tokens: int | None = None
@@ -116,18 +106,6 @@ def __init__(
116106

117107
self._start()
118108

119-
def _get_finish_reasons(self) -> list[str] | None:
120-
if self.finish_reasons is not None:
121-
return self.finish_reasons or None
122-
if self.output_messages:
123-
reasons = [
124-
msg.finish_reason
125-
for msg in self.output_messages
126-
if msg.finish_reason
127-
]
128-
return reasons or None
129-
return None
130-
131109
def _get_common_attributes(self) -> dict[str, Any]:
132110
optional_attrs = (
133111
(GenAI.GEN_AI_REQUEST_MODEL, self.request_model),
@@ -160,10 +138,8 @@ def _get_request_attributes(self) -> dict[str, Any]:
160138
)
161139
return {k: v for k, v in optional_attrs if v is not None}
162140

163-
def _get_response_attributes(self) -> dict[str, Any]:
141+
def _get_usage_attributes(self) -> dict[str, Any]:
164142
optional_attrs = (
165-
(GenAI.GEN_AI_RESPONSE_FINISH_REASONS, self._get_finish_reasons()),
166-
(GenAI.GEN_AI_RESPONSE_MODEL, self.response_model_name),
167143
(GenAI.GEN_AI_USAGE_INPUT_TOKENS, self.input_tokens),
168144
(GenAI.GEN_AI_USAGE_OUTPUT_TOKENS, self.output_tokens),
169145
(
@@ -177,47 +153,19 @@ def _get_response_attributes(self) -> dict[str, Any]:
177153
)
178154
return {k: v for k, v in optional_attrs if v is not None}
179155

180-
def _get_messages_for_span(self) -> dict[str, Any]:
181-
if not is_experimental_mode() or get_content_capturing_mode() not in (
182-
ContentCapturingMode.SPAN_ONLY,
183-
ContentCapturingMode.SPAN_AND_EVENT,
184-
):
185-
return {}
186-
optional_attrs = (
187-
(
188-
GenAI.GEN_AI_INPUT_MESSAGES,
189-
gen_ai_json_dumps([asdict(m) for m in self.input_messages])
190-
if self.input_messages
191-
else None,
192-
),
193-
(
194-
GenAI.GEN_AI_OUTPUT_MESSAGES,
195-
gen_ai_json_dumps([asdict(m) for m in self.output_messages])
196-
if self.output_messages
197-
else None,
198-
),
199-
(
200-
GenAI.GEN_AI_SYSTEM_INSTRUCTIONS,
201-
gen_ai_json_dumps([asdict(p) for p in self.system_instruction])
202-
if self.system_instruction
203-
else None,
204-
),
205-
(
206-
GenAI.GEN_AI_TOOL_DEFINITIONS,
207-
gen_ai_json_dumps([asdict(t) for t in self.tool_definitions])
208-
if self.tool_definitions
209-
else None,
210-
),
156+
def _get_content_attributes_for_span(self) -> dict[str, Any]:
157+
return _get_content_attributes(
158+
input_messages=self.input_messages,
159+
output_messages=self.output_messages,
160+
system_instruction=self.system_instruction,
161+
tool_definitions=self.tool_definitions,
162+
for_span=True,
211163
)
212-
return {
213-
key: value for key, value in optional_attrs if value is not None
214-
}
215164

216165
def _get_metric_attributes(self) -> dict[str, Any]:
217166
optional_attrs = (
218167
(GenAI.GEN_AI_PROVIDER_NAME, self.provider),
219168
(GenAI.GEN_AI_REQUEST_MODEL, self.request_model),
220-
(GenAI.GEN_AI_RESPONSE_MODEL, self.response_model_name),
221169
(server_attributes.SERVER_ADDRESS, self.server_address),
222170
(server_attributes.SERVER_PORT, self.server_port),
223171
)
@@ -249,8 +197,8 @@ def _apply_finish(self, error: Error | None = None) -> None:
249197
attributes: dict[str, Any] = {}
250198
attributes.update(self._get_common_attributes())
251199
attributes.update(self._get_request_attributes())
252-
attributes.update(self._get_response_attributes())
253-
attributes.update(self._get_messages_for_span())
200+
attributes.update(self._get_usage_attributes())
201+
attributes.update(self._get_content_attributes_for_span())
254202
attributes.update(self.attributes)
255203
self.span.set_attributes(attributes)
256204
self._metrics_recorder.record(self)
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Shared content serialization helpers for invocation types.
16+
17+
This module provides shared logic for serializing input/output messages,
18+
system instructions, and tool definitions into span and event attributes.
19+
Used by both InferenceInvocation and AgentInvocation to avoid duplication.
20+
"""
21+
22+
from __future__ import annotations
23+
24+
from dataclasses import asdict
25+
from typing import Any, Sequence
26+
27+
from opentelemetry.semconv._incubating.attributes import (
28+
gen_ai_attributes as GenAI,
29+
)
30+
from opentelemetry.util.genai.types import (
31+
InputMessage,
32+
MessagePart,
33+
OutputMessage,
34+
ToolDefinition,
35+
)
36+
from opentelemetry.util.genai.utils import (
37+
ContentCapturingMode,
38+
gen_ai_json_dumps,
39+
get_content_capturing_mode,
40+
is_experimental_mode,
41+
)
42+
43+
44+
def _get_content_attributes(
45+
*,
46+
input_messages: Sequence[InputMessage],
47+
output_messages: Sequence[OutputMessage],
48+
system_instruction: Sequence[MessagePart],
49+
tool_definitions: Sequence[ToolDefinition] | None,
50+
for_span: bool,
51+
) -> dict[str, Any]:
52+
"""Serialize messages, system instructions, and tool definitions into attributes.
53+
54+
Args:
55+
input_messages: Input messages to serialize.
56+
output_messages: Output messages to serialize.
57+
system_instruction: System instructions to serialize.
58+
tool_definitions: Tool definitions to serialize (may be None).
59+
for_span: If True, serialize for span attributes (JSON string);
60+
if False, serialize for event attributes (list of dicts).
61+
"""
62+
if not is_experimental_mode():
63+
return {}
64+
65+
mode = get_content_capturing_mode()
66+
allowed_modes = (
67+
(
68+
ContentCapturingMode.SPAN_ONLY,
69+
ContentCapturingMode.SPAN_AND_EVENT,
70+
)
71+
if for_span
72+
else (
73+
ContentCapturingMode.EVENT_ONLY,
74+
ContentCapturingMode.SPAN_AND_EVENT,
75+
)
76+
)
77+
if mode not in allowed_modes:
78+
return {}
79+
80+
def serialize(items: Sequence[Any]) -> Any:
81+
dicts = [asdict(item) for item in items]
82+
return gen_ai_json_dumps(dicts) if for_span else dicts
83+
84+
optional_attrs = (
85+
(
86+
GenAI.GEN_AI_INPUT_MESSAGES,
87+
serialize(input_messages) if input_messages else None,
88+
),
89+
(
90+
GenAI.GEN_AI_OUTPUT_MESSAGES,
91+
serialize(output_messages) if output_messages else None,
92+
),
93+
(
94+
GenAI.GEN_AI_SYSTEM_INSTRUCTIONS,
95+
serialize(system_instruction) if system_instruction else None,
96+
),
97+
(
98+
GenAI.GEN_AI_TOOL_DEFINITIONS,
99+
serialize(tool_definitions) if tool_definitions else None,
100+
),
101+
)
102+
return {key: value for key, value in optional_attrs if value is not None}

0 commit comments

Comments
 (0)