Skip to content

Commit 9ac960e

Browse files
committed
fix: align maf autogen genai semantics
1 parent 8815188 commit 9ac960e

10 files changed

Lines changed: 401 additions & 32 deletions

File tree

instrumentation-loongsuite/loongsuite-instrumentation-autogen/src/opentelemetry/instrumentation/autogen/span_processor.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ def _classify_span(
220220
if op == GenAIOperation.EXECUTE_TOOL:
221221
return GenAISpanKind.TOOL, op
222222
if op == GenAIOperation.CREATE_AGENT:
223-
return None, op
223+
return GenAISpanKind.AGENT, op
224224
if op == GenAIOperation.INVOKE_AGENT:
225225
return GenAISpanKind.AGENT, op
226226
return GenAISpanKind.CHAIN, op or GenAIOperation.INVOKE_AGENT
@@ -284,7 +284,11 @@ def on_end(self, span: Any) -> None:
284284

285285
if span_kind == GenAISpanKind.LLM:
286286
_set_otel_span_kind(live, span, SpanKind.CLIENT)
287-
elif span_kind in {GenAISpanKind.AGENT, GenAISpanKind.TOOL}:
287+
elif span_kind == GenAISpanKind.AGENT and op_name == (
288+
GenAIOperation.INVOKE_AGENT
289+
):
290+
_set_otel_span_kind(live, span, SpanKind.INTERNAL)
291+
elif span_kind == GenAISpanKind.TOOL:
288292
_set_otel_span_kind(live, span, SpanKind.INTERNAL)
289293
except Exception as exc: # pragma: no cover - defensive
290294
logger.warning("AutoGenSemanticProcessor.on_end failed: %s", exc)

instrumentation-loongsuite/loongsuite-instrumentation-autogen/src/opentelemetry/instrumentation/autogen/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ def provider_name(model_client: Any) -> str:
241241

242242
def _finish_reason(value: Any) -> str:
243243
reason = _text(value or "unknown")
244-
if reason == "function_calls":
244+
if reason in {"tool_call", "function_call", "function_calls"}:
245245
return "tool_calls"
246246
return reason
247247

instrumentation-loongsuite/loongsuite-instrumentation-autogen/tests/test_span_processor.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def test_processor_normalizes_native_autogen_invoke_span():
6868
assert span.kind == SpanKind.INTERNAL
6969

7070

71-
def test_processor_keeps_create_agent_as_lifecycle_span():
71+
def test_processor_marks_create_agent_as_agent_lifecycle_span():
7272
exporter = InMemorySpanExporter()
7373
provider = TracerProvider()
7474
provider.add_span_processor(AutoGenSemanticProcessor())
@@ -91,7 +91,7 @@ def test_processor_keeps_create_agent_as_lifecycle_span():
9191

9292
assert attributes[GEN_AI_PROVIDER_NAME] == AUTOGEN_PROVIDER_NAME
9393
assert attributes[GEN_AI_OPERATION_NAME] == GenAIOperation.CREATE_AGENT
94-
assert GEN_AI_SPAN_KIND not in attributes
94+
assert attributes[GEN_AI_SPAN_KIND] == GenAISpanKind.AGENT
9595
assert GEN_AI_SYSTEM not in attributes
9696
assert span.kind == SpanKind.CLIENT
9797

instrumentation-loongsuite/loongsuite-instrumentation-autogen/tests/test_utils.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,22 @@ def test_apply_create_result_normalizes_tool_call_finish_reason():
220220
assert invocation.output_messages[0].finish_reason == "tool_calls"
221221

222222

223+
def test_apply_create_result_normalizes_singular_tool_call_finish_reason():
224+
invocation = make_llm_invocation(ModelClient(), [], [])
225+
226+
apply_create_result(
227+
invocation,
228+
CreateResult(
229+
[FunctionCall("call-1", "lookup", {"q": "x"})],
230+
"tool_call",
231+
Usage(prompt_tokens=3, completion_tokens=5),
232+
),
233+
)
234+
235+
assert invocation.finish_reasons == ["tool_calls"]
236+
assert invocation.output_messages[0].finish_reason == "tool_calls"
237+
238+
223239
def test_make_agent_invocation_uses_assistant_metadata():
224240
class AssistantAgent:
225241
_name = "assistant"

instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/react_step_patch.py

Lines changed: 76 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
import contextvars
4242
import inspect
4343
import logging
44+
from dataclasses import dataclass
45+
from types import TracebackType
4446
from typing import Any
4547

4648
logger = logging.getLogger(__name__)
@@ -54,13 +56,22 @@
5456
_maf_react_step_counter: contextvars.ContextVar[int] = contextvars.ContextVar(
5557
"_maf_react_step_counter", default=0
5658
)
59+
_maf_current_react_step: contextvars.ContextVar[Any] = contextvars.ContextVar(
60+
"_maf_current_react_step", default=None
61+
)
5762

5863
_applied = False
5964
_original_fil_get_response: Any = None
6065
_original_chat_get_response: Any = None
6166
_handler: Any = None
6267

6368

69+
@dataclass
70+
class _OpenReactStep:
71+
cm: Any
72+
invocation: Any
73+
74+
6475
def _mark_maf_live_span(invocation: Any) -> None:
6576
span = getattr(invocation, "span", None)
6677
if span is None:
@@ -139,11 +150,17 @@ def _fil_wrapper(wrapped, instance, args, kwargs): # type: ignore[no-untyped-de
139150
async def _scoped(): # type: ignore[no-untyped-def]
140151
token_active = _maf_react_loop_active.set(True)
141152
token_counter = _maf_react_step_counter.set(0)
153+
token_step = _maf_current_react_step.set(None)
142154
try:
143155
return await result
156+
except Exception as exc:
157+
_close_current_react_step(type(exc), exc, exc.__traceback__)
158+
raise
144159
finally:
160+
_close_current_react_step()
145161
_maf_react_loop_active.reset(token_active)
146162
_maf_react_step_counter.reset(token_counter)
163+
_maf_current_react_step.reset(token_step)
147164

148165
return _scoped()
149166

@@ -166,27 +183,24 @@ def _chat_wrapper(wrapped, instance, args, kwargs): # type: ignore[no-untyped-d
166183
local_handler = handler
167184

168185
async def _step_scoped(): # type: ignore[no-untyped-def]
169-
from opentelemetry.util.genai.extended_types import (
170-
ReactStepInvocation,
171-
)
172-
173-
step_inv = ReactStepInvocation(round=round_num)
174-
token_counter = _maf_react_step_counter.set(round_num)
186+
previous_step = _close_current_react_step()
187+
if previous_step is not None and inspect.isawaitable(
188+
previous_step
189+
):
190+
await previous_step
191+
step_inv = _open_current_react_step(local_handler, round_num)
192+
_maf_react_step_counter.set(round_num)
175193
try:
176-
with local_handler.react_step(step_inv) as step:
177-
_mark_maf_live_span(step)
178-
try:
179-
response = await result
180-
# Best-effort finish_reason extraction from the response.
181-
finish = _extract_finish_reason(response)
182-
if finish is not None:
183-
step.finish_reason = finish
184-
return response
185-
except Exception:
186-
step.finish_reason = "error"
187-
raise
188-
finally:
189-
_maf_react_step_counter.reset(token_counter)
194+
response = await result
195+
# Best-effort finish_reason extraction from the response.
196+
finish = _extract_finish_reason(response)
197+
if finish is not None:
198+
step_inv.finish_reason = finish
199+
return response
200+
except Exception as exc:
201+
step_inv.finish_reason = "error"
202+
_close_current_react_step(type(exc), exc, exc.__traceback__)
203+
raise
190204

191205
return _step_scoped()
192206

@@ -228,6 +242,31 @@ def _unwrap_to_function(func: Any) -> Any:
228242
return cur
229243

230244

245+
def _open_current_react_step(handler: Any, round_num: int) -> Any:
246+
from opentelemetry.util.genai.extended_types import ReactStepInvocation
247+
248+
step_inv = ReactStepInvocation(round=round_num)
249+
cm = handler.react_step(step_inv)
250+
step = cm.__enter__()
251+
_mark_maf_live_span(step)
252+
_maf_current_react_step.set(_OpenReactStep(cm=cm, invocation=step))
253+
return step
254+
255+
256+
def _close_current_react_step(
257+
exc_type: type[BaseException] | None = None,
258+
exc: BaseException | None = None,
259+
tb: TracebackType | None = None,
260+
) -> Any:
261+
current = _maf_current_react_step.get()
262+
if current is None:
263+
return None
264+
_maf_current_react_step.set(None)
265+
if exc_type is not None and current.invocation.finish_reason is None:
266+
current.invocation.finish_reason = "error"
267+
return current.cm.__exit__(exc_type, exc, tb)
268+
269+
231270
def _is_response_stream(result: Any) -> bool:
232271
"""Return True for MAF ResponseStream-like values.
233272
@@ -244,23 +283,38 @@ def _is_response_stream(result: Any) -> bool:
244283
def _extract_finish_reason(result: Any) -> Any:
245284
"""Best-effort extraction of a finish_reason string from a ChatResponse."""
246285
try:
286+
fr = getattr(result, "finish_reason", None)
287+
if fr:
288+
return _normalize_finish_reason(fr)
289+
raw = getattr(result, "raw_representation", None)
290+
if raw is not None:
291+
fr = getattr(raw, "finish_reason", None)
292+
if fr:
293+
return _normalize_finish_reason(fr)
247294
# MAF ChatResponse.messages[-1].finish_reason or choices[0].finish_reason
248295
messages = getattr(result, "messages", None)
249296
if messages:
250297
last = messages[-1]
251298
fr = getattr(last, "finish_reason", None)
252299
if fr:
253-
return fr
300+
return _normalize_finish_reason(fr)
254301
choices = getattr(result, "choices", None)
255302
if choices:
256303
fr = getattr(choices[0], "finish_reason", None)
257304
if fr:
258-
return fr
305+
return _normalize_finish_reason(fr)
259306
except Exception:
260307
return None
261308
return None
262309

263310

311+
def _normalize_finish_reason(value: Any) -> str:
312+
reason = str(value)
313+
if reason in {"tool_call", "function_call", "function_calls"}:
314+
return "tool_calls"
315+
return reason
316+
317+
264318
def revert_react_step_patch() -> None:
265319
"""Revert the ReAct step patch. Safe to call even if not applied."""
266320
global _applied, _original_fil_get_response, _original_chat_get_response

instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
_EDGE_GROUP_PROCESS = "edge_group.process"
9797
_LIVE_SPAN_MAX_AGE_NS = 60 * 1_000_000_000
9898
_GEN_AI_TOOL_NAME = "gen_ai.tool.name"
99+
_GEN_AI_OUTPUT_MESSAGES = "gen_ai.output.messages"
99100
_FRAMEWORK_PROVIDER_NAME = MAF_PROVIDER_NAME
100101
_GEN_AI_SYSTEM = "gen_ai.system"
101102
_MAF_LIVE_SPAN_MARKER = MAF_LIVE_SPAN_MARKER
@@ -106,6 +107,9 @@
106107
_EXECUTOR_PROCESS,
107108
_EDGE_GROUP_PROCESS,
108109
)
110+
_TOOL_CALL_FINISH_REASONS = frozenset(
111+
{"tool_call", "tool_calls", "function_call", "function_calls"}
112+
)
109113

110114

111115
def _attr_value(span: Any, key: str) -> Any:
@@ -391,6 +395,18 @@ def _normalize_provider(value: Any) -> Optional[str]:
391395
def _normalize_finish_reasons(live_span: OtelSpan, readable: Any) -> None:
392396
"""Normalize JSON-encoded finish reasons to an OTel string array."""
393397
value = _attr_value(readable, GEN_AI_RESPONSE_FINISH_REASONS)
398+
if isinstance(value, (list, tuple)) and all(
399+
isinstance(item, str) for item in value
400+
):
401+
normalized = [_normalize_finish_reason_value(item) for item in value]
402+
if list(value) != normalized:
403+
_set_attr_on_both(
404+
live_span,
405+
readable,
406+
GEN_AI_RESPONSE_FINISH_REASONS,
407+
normalized,
408+
)
409+
return
394410
if not isinstance(value, str):
395411
return
396412
try:
@@ -400,11 +416,77 @@ def _normalize_finish_reasons(live_span: OtelSpan, readable: Any) -> None:
400416
if isinstance(parsed, list) and all(
401417
isinstance(item, str) for item in parsed
402418
):
419+
parsed = [_normalize_finish_reason_value(item) for item in parsed]
403420
_set_attr_on_both(
404421
live_span, readable, GEN_AI_RESPONSE_FINISH_REASONS, parsed
405422
)
406423

407424

425+
def _normalize_output_messages(live_span: OtelSpan, readable: Any) -> None:
426+
normalized = _normalized_output_messages_value(
427+
_attr_value(readable, _GEN_AI_OUTPUT_MESSAGES)
428+
)
429+
if normalized is not None:
430+
_set_attr_on_both(
431+
live_span, readable, _GEN_AI_OUTPUT_MESSAGES, normalized
432+
)
433+
434+
435+
def _normalized_output_messages_value(value: Any) -> Optional[str]:
436+
if value is None:
437+
return None
438+
try:
439+
messages = json.loads(value) if isinstance(value, str) else value
440+
except (TypeError, ValueError):
441+
return None
442+
if not isinstance(messages, list):
443+
return None
444+
changed = False
445+
normalized_messages = []
446+
for message in messages:
447+
if not isinstance(message, dict):
448+
normalized_messages.append(message)
449+
continue
450+
normalized = dict(message)
451+
current = normalized.get("finish_reason")
452+
default = _default_finish_reason_for_message(normalized)
453+
finish_reason = _normalize_finish_reason_value(
454+
current, default=default
455+
)
456+
if current != finish_reason:
457+
normalized["finish_reason"] = finish_reason
458+
changed = True
459+
normalized_messages.append(normalized)
460+
if not changed and isinstance(value, str):
461+
return None
462+
try:
463+
return json.dumps(
464+
normalized_messages, ensure_ascii=False, separators=(",", ":")
465+
)
466+
except (TypeError, ValueError):
467+
return None
468+
469+
470+
def _default_finish_reason_for_message(message: Mapping[Any, Any]) -> str:
471+
parts = message.get("parts")
472+
if isinstance(parts, list):
473+
for part in parts:
474+
if isinstance(part, Mapping) and part.get("type") == "tool_call":
475+
return "tool_calls"
476+
return "stop"
477+
478+
479+
def _normalize_finish_reason_value(
480+
value: Any, *, default: str = "stop"
481+
) -> str:
482+
if value is None or value == "":
483+
return default
484+
reason = str(value)
485+
if reason in _TOOL_CALL_FINISH_REASONS:
486+
return "tool_calls"
487+
return reason
488+
489+
408490
def _set_span_kind(live_span: OtelSpan, readable: Any, kind: SpanKind) -> None:
409491
"""Mutate the SDK span kind before downstream exporters receive the span."""
410492
for target in (readable, live_span):
@@ -1029,6 +1111,7 @@ def _apply_semantic_attributes(
10291111

10301112
# 5) Normalize finish reasons written by MAF as a JSON string.
10311113
_normalize_finish_reasons(live, readable)
1114+
_normalize_output_messages(live, readable)
10321115

10331116
# 6) LLM/embedding spans should use CLIENT OTel span kind.
10341117
if span_kind == GenAISpanKind.LLM:

0 commit comments

Comments
 (0)