Skip to content

Commit 6ed56b6

Browse files
committed
feat(genai-utils): add finish_reasons to AgentInvocation per semconv
1 parent c655e25 commit 6ed56b6

3 files changed

Lines changed: 33 additions & 11 deletions

File tree

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_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`
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 `finish_reasons` field to `AgentInvocation` per semconv `gen_ai.response.finish_reasons` (Recommended); 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`; add `getattr` fallback for `GEN_AI_AGENT_VERSION` semconv constant
1111
([#4274](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4274))
1212
- Add metrics support for EmbeddingInvocation
1313
([#4377](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4377))

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

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,11 @@
3535
ToolDefinition,
3636
)
3737

38-
_GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS: str = getattr(
39-
GenAI,
40-
"GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS",
41-
"gen_ai.usage.cache_creation.input_tokens",
42-
)
43-
_GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS: str = getattr(
44-
GenAI,
45-
"GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS",
46-
"gen_ai.usage.cache_read.input_tokens",
38+
_GEN_AI_AGENT_VERSION = "gen_ai.agent.version"
39+
_GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS = (
40+
"gen_ai.usage.cache_creation.input_tokens"
4741
)
42+
_GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS = "gen_ai.usage.cache_read.input_tokens"
4843

4944

5045
class AgentInvocation(GenAIInvocation):
@@ -108,6 +103,8 @@ def __init__(
108103
self.seed: int | None = None
109104
self.choice_count: int | None = None
110105

106+
self.finish_reasons: list[str] | None = None
107+
111108
self.input_tokens: int | None = None
112109
self.output_tokens: int | None = None
113110
self.cache_creation_input_tokens: int | None = None
@@ -128,7 +125,7 @@ def _get_common_attributes(self) -> dict[str, Any]:
128125
(GenAI.GEN_AI_AGENT_NAME, self.agent_name),
129126
(GenAI.GEN_AI_AGENT_ID, self.agent_id),
130127
(GenAI.GEN_AI_AGENT_DESCRIPTION, self.agent_description),
131-
(GenAI.GEN_AI_AGENT_VERSION, self.agent_version),
128+
(_GEN_AI_AGENT_VERSION, self.agent_version),
132129
)
133130
return {
134131
GenAI.GEN_AI_OPERATION_NAME: self._operation_name,
@@ -152,6 +149,11 @@ def _get_request_attributes(self) -> dict[str, Any]:
152149
)
153150
return {k: v for k, v in optional_attrs if v is not None}
154151

152+
def _get_response_attributes(self) -> dict[str, Any]:
153+
if self.finish_reasons:
154+
return {GenAI.GEN_AI_RESPONSE_FINISH_REASONS: self.finish_reasons}
155+
return {}
156+
155157
def _get_usage_attributes(self) -> dict[str, Any]:
156158
optional_attrs = (
157159
(GenAI.GEN_AI_USAGE_INPUT_TOKENS, self.input_tokens),
@@ -211,6 +213,7 @@ def _apply_finish(self, error: Error | None = None) -> None:
211213
attributes: dict[str, Any] = {}
212214
attributes.update(self._get_common_attributes())
213215
attributes.update(self._get_request_attributes())
216+
attributes.update(self._get_response_attributes())
214217
attributes.update(self._get_usage_attributes())
215218
attributes.update(self._get_content_attributes_for_span())
216219
attributes.update(self.attributes)

util/opentelemetry-util-genai/tests/test_handler_agent.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ def test_all_attributes(self):
8686
invocation.stop_sequences = ["END", "STOP"]
8787
invocation.seed = 42
8888
invocation.choice_count = 3
89+
invocation.finish_reasons = ["stop"]
8990
invocation.input_tokens = 100
9091
invocation.output_tokens = 200
9192
invocation.stop()
@@ -108,6 +109,24 @@ def test_all_attributes(self):
108109
assert attrs[GenAI.GEN_AI_REQUEST_STOP_SEQUENCES] == ("END", "STOP")
109110
assert attrs[GenAI.GEN_AI_REQUEST_SEED] == 42
110111
assert attrs[GenAI.GEN_AI_REQUEST_CHOICE_COUNT] == 3
112+
assert attrs[GenAI.GEN_AI_RESPONSE_FINISH_REASONS] == ("stop",)
113+
114+
def test_finish_reasons_multiple(self):
115+
invocation = self.handler.start_invoke_local_agent("openai")
116+
invocation.finish_reasons = ["stop", "length"]
117+
invocation.stop()
118+
attrs = self.span_exporter.get_finished_spans()[0].attributes
119+
assert attrs[GenAI.GEN_AI_RESPONSE_FINISH_REASONS] == (
120+
"stop",
121+
"length",
122+
)
123+
124+
def test_finish_reasons_empty_list_omitted(self):
125+
invocation = self.handler.start_invoke_local_agent("openai")
126+
invocation.finish_reasons = []
127+
invocation.stop()
128+
attrs = self.span_exporter.get_finished_spans()[0].attributes
129+
assert GenAI.GEN_AI_RESPONSE_FINISH_REASONS not in attrs
111130

112131
def test_no_response_model_or_finish_reasons(self):
113132
invocation = self.handler.start_invoke_local_agent("openai")

0 commit comments

Comments
 (0)