Skip to content

Commit 734eb1c

Browse files
authored
fix(genai-instrumentors): cleanup code, align with OTel GenAI semconv, add missing attributes and fix deprecated usage (#706)
*Description of changes:* Further aligns GenAI instrumentors with the OTel GenAI semantic conventions spec. - Use `SpanKind.CLIENT` for inference spans in LlamaIndex instrumentation - Fix `retrieval` operation span in LlamaIndex instrumentation - Remove `gen_ai.system_instructions` from `invoke_agent` span - Remove `on_agent_action`/`on_agent_finish` from LangChain as on_chain_start will capture these spans - add `gen_ai.response.finish_reasons` from model responses - add LLM request params: `frequency_penalty`, `presence_penalty`, `stop_sequences`, `top_p`, `top_k` across all instrumentors - add `ToolCallRequestPart` and `ReasoningPart` in LlamaIndex output messages - fixed `error.message` to `error.type` in CrewAI per OTel spec - changed `gen_ai.prompt` to `gen_ai.input.messages` in LangChain per OTel spec - changed `gen_ai.prompt` to `gen_ai.prompt.name` in MCP per OTel spec - add `record_exception()` to LangChain error path *Tests:* - Contract tests: replace hardcoded strings with OTel semconv imports - Contract tests: assert `gen_ai.system_instructions` on all chat spans - add provider subtests for finish_reasons - add OTel JSON schema validation for ToolCallRequestPart and ReasoningPart By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent db1f721 commit 734eb1c

13 files changed

Lines changed: 481 additions & 213 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ If your change does not need a CHANGELOG entry, add the "skip changelog" label t
1212

1313
## Unreleased
1414

15+
- fix(genai-instrumentors): cleanup code, align with OTel GenAI semconv, add missing attributes and fix deprecated usage
16+
([#706](https://github.com/aws-observability/aws-otel-python-instrumentation/pull/706))
1517
- feat(genai-instrumentation): add oldest/latest dependency testing and scheduled instrumentation tests for GenAI libraries
1618
([#708](https://github.com/aws-observability/aws-otel-python-instrumentation/pull/708))
1719
- feat(instrumentors): add invoke_workflow span support for LlamaIndex AgentWorkflow and CrewAI

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/instrumentation/crewai/_event_handler.py

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
skip_instrumentation_if_suppressed,
1515
)
1616
from opentelemetry import context, trace
17-
from opentelemetry.semconv._incubating.attributes.error_attributes import ERROR_MESSAGE
1817
from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import (
1918
GEN_AI_AGENT_DESCRIPTION,
2019
GEN_AI_AGENT_ID,
@@ -23,9 +22,14 @@
2322
GEN_AI_OPERATION_NAME,
2423
GEN_AI_OUTPUT_MESSAGES,
2524
GEN_AI_PROVIDER_NAME,
25+
GEN_AI_REQUEST_FREQUENCY_PENALTY,
2626
GEN_AI_REQUEST_MAX_TOKENS,
2727
GEN_AI_REQUEST_MODEL,
28+
GEN_AI_REQUEST_PRESENCE_PENALTY,
29+
GEN_AI_REQUEST_STOP_SEQUENCES,
2830
GEN_AI_REQUEST_TEMPERATURE,
31+
GEN_AI_REQUEST_TOP_P,
32+
GEN_AI_RESPONSE_FINISH_REASONS,
2933
GEN_AI_RESPONSE_MODEL,
3034
GEN_AI_SYSTEM_INSTRUCTIONS,
3135
GEN_AI_TOOL_CALL_ARGUMENTS,
@@ -38,6 +42,7 @@
3842
GEN_AI_USAGE_OUTPUT_TOKENS,
3943
GenAiOperationNameValues,
4044
)
45+
from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
4146
from opentelemetry.trace import SpanKind, Status, StatusCode
4247

4348
if TYPE_CHECKING:
@@ -201,18 +206,18 @@ def _on_crew_start(self, source: "Crew", event: "CrewKickoffStartedEvent") -> No
201206
self._start_span(span_name, event.event_id, attributes, event.parent_event_id)
202207

203208
def _on_crew_completed(
204-
self, source: "Crew", event: "CrewKickoffCompletedEvent" # pylint: disable=unused-argument
205-
) -> None:
209+
self, source: "Crew", event: "CrewKickoffCompletedEvent"
210+
) -> None: # pylint: disable=unused-argument
206211
self._end_span(event.started_event_id)
207212
self._event_id_to_span.clear()
208213

209214
def _on_crew_failed(
210-
self, source: "Crew", event: "CrewKickoffFailedEvent" # pylint: disable=unused-argument
211-
) -> None:
215+
self, source: "Crew", event: "CrewKickoffFailedEvent"
216+
) -> None: # pylint: disable=unused-argument
212217
self._end_span(event.started_event_id, error=getattr(event, "error", None))
213218
self._event_id_to_span.clear()
214219

215-
def _on_agent_start(
220+
def _on_agent_start( # pylint: disable=too-many-branches
216221
self, source: "BaseAgent", event: "AgentExecutionStartedEvent" # pylint: disable=unused-argument
217222
) -> None:
218223
agent = getattr(event, "agent", None)
@@ -233,9 +238,6 @@ def _on_agent_start(
233238
goal = getattr(agent, "goal", None)
234239
if goal:
235240
attributes[GEN_AI_AGENT_DESCRIPTION] = goal
236-
backstory = getattr(agent, "backstory", None)
237-
if backstory:
238-
attributes[GEN_AI_SYSTEM_INSTRUCTIONS] = backstory
239241

240242
llm = getattr(agent, "llm", None)
241243
provider, model = self._extract_provider_and_model(llm)
@@ -244,12 +246,18 @@ def _on_agent_start(
244246
if model:
245247
attributes[GEN_AI_REQUEST_MODEL] = model
246248
if llm:
247-
temperature = getattr(llm, "temperature", None)
248-
if temperature is not None:
249-
attributes[GEN_AI_REQUEST_TEMPERATURE] = temperature
250-
max_tokens = getattr(llm, "max_tokens", None)
251-
if max_tokens is not None:
252-
attributes[GEN_AI_REQUEST_MAX_TOKENS] = max_tokens
249+
if getattr(llm, "temperature", None) is not None:
250+
attributes[GEN_AI_REQUEST_TEMPERATURE] = llm.temperature
251+
if getattr(llm, "max_tokens", None) is not None:
252+
attributes[GEN_AI_REQUEST_MAX_TOKENS] = llm.max_tokens
253+
if getattr(llm, "top_p", None) is not None:
254+
attributes[GEN_AI_REQUEST_TOP_P] = llm.top_p
255+
if getattr(llm, "frequency_penalty", None) is not None:
256+
attributes[GEN_AI_REQUEST_FREQUENCY_PENALTY] = llm.frequency_penalty
257+
if getattr(llm, "presence_penalty", None) is not None:
258+
attributes[GEN_AI_REQUEST_PRESENCE_PENALTY] = llm.presence_penalty
259+
if getattr(llm, "stop", None):
260+
attributes[GEN_AI_REQUEST_STOP_SEQUENCES] = llm.stop
253261

254262
self._start_span(span_name, event.event_id, attributes, event.parent_event_id)
255263

@@ -259,8 +267,8 @@ def _on_agent_completed(
259267
self._end_span(event.started_event_id)
260268

261269
def _on_agent_failed(
262-
self, source: "BaseAgent", event: "AgentExecutionErrorEvent" # pylint: disable=unused-argument
263-
) -> None:
270+
self, source: "BaseAgent", event: "AgentExecutionErrorEvent"
271+
) -> None: # pylint: disable=unused-argument
264272
self._end_span(event.started_event_id, error=getattr(event, "error", None))
265273

266274
def _on_tool_start(self, source: "ToolUsage", event: "ToolUsageStartedEvent") -> None:
@@ -296,8 +304,8 @@ def _on_tool_start(self, source: "ToolUsage", event: "ToolUsageStartedEvent") ->
296304
self._start_span(span_name, event.event_id, attributes, event.parent_event_id)
297305

298306
def _on_tool_finished(
299-
self, source: "ToolUsage", event: "ToolUsageFinishedEvent" # pylint: disable=unused-argument
300-
) -> None:
307+
self, source: "ToolUsage", event: "ToolUsageFinishedEvent"
308+
) -> None: # pylint: disable=unused-argument
301309
attrs: Dict[str, Any] = {}
302310
output = getattr(event, "output", None)
303311
if output is not None:
@@ -344,6 +352,7 @@ def _on_llm_completed(self, source: "LLM", event: "LLMCallCompletedEvent") -> No
344352
from crewai.events.types.llm_events import LLMCallType # pylint: disable=import-outside-toplevel
345353

346354
finish_reason = "tool_call" if event.call_type == LLMCallType.TOOL_CALL else "stop"
355+
attrs[GEN_AI_RESPONSE_FINISH_REASONS] = [finish_reason]
347356

348357
response = event.response
349358
if response:
@@ -412,7 +421,7 @@ def _end_span(
412421
entry.span.set_attribute(key, value)
413422
if error:
414423
entry.span.set_status(Status(StatusCode.ERROR, error))
415-
entry.span.set_attribute(ERROR_MESSAGE, error)
424+
entry.span.set_attribute(ERROR_TYPE, error)
416425
else:
417426
entry.span.set_status(Status(StatusCode.OK))
418427
entry.span.end()

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/instrumentation/langchain/callback_handler.py

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,16 @@
2323
GEN_AI_INPUT_MESSAGES,
2424
GEN_AI_OPERATION_NAME,
2525
GEN_AI_OUTPUT_MESSAGES,
26-
GEN_AI_PROMPT,
2726
GEN_AI_PROVIDER_NAME,
27+
GEN_AI_REQUEST_FREQUENCY_PENALTY,
2828
GEN_AI_REQUEST_MAX_TOKENS,
2929
GEN_AI_REQUEST_MODEL,
30+
GEN_AI_REQUEST_PRESENCE_PENALTY,
31+
GEN_AI_REQUEST_STOP_SEQUENCES,
3032
GEN_AI_REQUEST_TEMPERATURE,
33+
GEN_AI_REQUEST_TOP_K,
3134
GEN_AI_REQUEST_TOP_P,
35+
GEN_AI_RESPONSE_FINISH_REASONS,
3236
GEN_AI_RESPONSE_ID,
3337
GEN_AI_RESPONSE_MODEL,
3438
GEN_AI_SYSTEM_INSTRUCTIONS,
@@ -49,7 +53,6 @@
4953
from opentelemetry.util.types import AttributeValue
5054

5155
if TYPE_CHECKING:
52-
from langchain_core.agents import AgentAction, AgentFinish
5356
from langchain_core.messages import BaseMessage
5457
from langchain_core.outputs import LLMResult
5558

@@ -152,7 +155,11 @@ def on_llm_start(
152155
self._set_langgraph_span_attributes(span, metadata)
153156
self._set_span_attribute(span, GEN_AI_PROVIDER_NAME, provider)
154157
self._set_span_attribute(span, GEN_AI_OPERATION_NAME, GenAiOperationNameValues.TEXT_COMPLETION.value)
155-
self._set_span_attribute(span, GEN_AI_PROMPT, serialize_to_json_string(prompts))
158+
self._set_span_attribute(
159+
span,
160+
GEN_AI_INPUT_MESSAGES,
161+
serialize_to_json_string([{"role": "user", "parts": [{"type": "text", "content": p}]} for p in prompts]),
162+
)
156163
self._set_llm_request_span_attributes(span, kwargs, serialized=serialized_kwargs, model_name=model_name)
157164

158165
@skip_instrumentation_if_suppressed
@@ -174,9 +181,11 @@ def on_llm_end(self, response: LLMResult, *, run_id: UUID, **kwargs: Any) -> Non
174181
)
175182

176183
if response.generations:
177-
self._set_span_attribute(
178-
span, GEN_AI_OUTPUT_MESSAGES, serialize_to_json_string(self._format_lc_llm_output(response.generations))
179-
)
184+
output_messages = self._format_lc_llm_output(response.generations)
185+
self._set_span_attribute(span, GEN_AI_OUTPUT_MESSAGES, serialize_to_json_string(output_messages))
186+
finish_reasons = self._extract_finish_reasons(response.generations)
187+
if finish_reasons is not None:
188+
self._set_span_attribute(span, GEN_AI_RESPONSE_FINISH_REASONS, finish_reasons)
180189

181190
self._set_span_attribute(span, GEN_AI_RESPONSE_MODEL, model)
182191
self._set_span_attribute(span, GEN_AI_RESPONSE_ID, response_id)
@@ -283,32 +292,6 @@ def on_tool_end(self, output: Any, *, run_id: UUID, **kwargs: Any) -> None:
283292
def on_tool_error(self, error: BaseException, *, run_id: UUID, **kwargs: Any) -> None:
284293
self._handle_error(error, run_id, **kwargs)
285294

286-
@skip_instrumentation_if_suppressed
287-
def on_agent_action(self, action: AgentAction, *, run_id: UUID, **kwargs: Any) -> None:
288-
289-
entry = self._safe_get_span(run_id)
290-
if not entry:
291-
return
292-
span, _ = entry
293-
self._set_span_attribute(
294-
span, GEN_AI_TOOL_CALL_ARGUMENTS, serialize_to_json_string(getattr(action, "tool_input", None))
295-
)
296-
self._set_span_attribute(span, GEN_AI_TOOL_NAME, getattr(action, "tool", None))
297-
self._set_span_attribute(span, GEN_AI_OPERATION_NAME, GenAiOperationNameValues.INVOKE_AGENT.value)
298-
299-
@skip_instrumentation_if_suppressed
300-
def on_agent_finish(self, finish: AgentFinish, *, run_id: UUID, **kwargs: Any) -> None:
301-
302-
entry = self._safe_get_span(run_id)
303-
if not entry:
304-
return
305-
span, _ = entry
306-
self._set_span_attribute(
307-
span,
308-
GEN_AI_TOOL_CALL_RESULT,
309-
serialize_to_json_string(finish.return_values.get("output")),
310-
)
311-
312295
def on_agent_error(self, error: BaseException, *, run_id: UUID, **kwargs: Any) -> None:
313296
self._handle_error(error, run_id, **kwargs)
314297

@@ -470,6 +453,18 @@ def _set_llm_request_span_attributes(
470453
span, GEN_AI_REQUEST_TEMPERATURE, params.get("temperature") or config.get("temperature")
471454
)
472455
self._set_span_attribute(span, GEN_AI_REQUEST_TOP_P, params.get("top_p") or config.get("top_p"))
456+
self._set_span_attribute(span, GEN_AI_REQUEST_TOP_K, params.get("top_k") or config.get("top_k"))
457+
self._set_span_attribute(
458+
span,
459+
GEN_AI_REQUEST_FREQUENCY_PENALTY,
460+
params.get("frequency_penalty") or config.get("frequency_penalty"),
461+
)
462+
self._set_span_attribute(
463+
span, GEN_AI_REQUEST_PRESENCE_PENALTY, params.get("presence_penalty") or config.get("presence_penalty")
464+
)
465+
stop = params.get("stop") or config.get("stop")
466+
if stop:
467+
self._set_span_attribute(span, GEN_AI_REQUEST_STOP_SEQUENCES, stop)
473468

474469
def _should_skip_chain(
475470
self, serialized: dict[str, Any], name: Optional[str], metadata: Optional[dict] = None
@@ -512,6 +507,7 @@ def _handle_error(self, error: BaseException, run_id: UUID, **kwargs: Any) -> No
512507
if not entry:
513508
return
514509
span, _ = entry
510+
span.record_exception(error)
515511
span.set_status(Status(StatusCode.ERROR, str(error)))
516512
span.set_attribute(ERROR_TYPE, type(error).__qualname__)
517513
self._end_span(run_id)
@@ -553,6 +549,18 @@ def _get_name_from_callback(serialized: dict[str, Any], **kwargs: Any) -> Option
553549
return ids[-1]
554550
return kwargs.get("name")
555551

552+
@staticmethod
553+
def _extract_finish_reasons(generations: list) -> list[str] | None:
554+
reasons: list[str] = []
555+
for batch in generations:
556+
for gen in batch:
557+
raw = (getattr(gen, "generation_info", None) or {}).get("finish_reason")
558+
if not raw and (msg := getattr(gen, "message", None)):
559+
raw = (getattr(msg, "response_metadata", None) or {}).get("stop_reason")
560+
if raw:
561+
reasons.append(raw)
562+
return reasons or None
563+
556564
@staticmethod
557565
def _extract_llm_model_id(kwargs: dict, serialized: Optional[dict] = None) -> Optional[str]:
558566
config = serialized or {}

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/instrumentation/llama_index/_handler.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,11 +150,13 @@ def new_span(
150150
parent_id=parent_span_id,
151151
)
152152

153+
kind = SpanKind.CLIENT if isinstance(instance, (BaseLLM, MultiModalLLM, BaseEmbedding)) else SpanKind.INTERNAL
154+
153155
otel_span = self._otel_tracer.start_span(
154156
name="llama_index.operation", # generic operation name, updated in span.end()
155157
start_time=time_ns(),
156158
attributes={},
157-
kind=SpanKind.INTERNAL,
159+
kind=kind,
158160
context=(
159161
parent.context
160162
if parent

0 commit comments

Comments
 (0)