[opentelemetry-instrumentation-genai-agno] Add instrumentation for retrieval and make various other improvements - #673
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Deferred patching uses post-import hooks that are not removed/disabled on uninstrument, which can unintentionally re-enable instrumentation after unpatching.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds/extends Agno GenAI instrumentation to cover retrieval operations, improve patching behavior for optional/heavy submodules, and support streaming tool executions, with corresponding unit + conformance coverage updates.
Changes:
- Instrument
Knowledge.search/Knowledge.asearchas GenAIretrievalspans (including optional query/doc capture). - Introduce deferred patching via post-import hooks to avoid eager imports of optional dependencies.
- Add streaming support for tool executions (sync + async iterators/generators) and enrich agent spans with request model + description.
File summaries
| File | Description |
|---|---|
| instrumentation/opentelemetry-instrumentation-genai-agno/tests/test_tools.py | Adds tests for sync + streaming tool execution spans, including stream/caller error finalization. |
| instrumentation/opentelemetry-instrumentation-genai-agno/tests/test_knowledge.py | Adds tests for Knowledge.search/asearch retrieval spans with/without content capture and error handling. |
| instrumentation/opentelemetry-instrumentation-genai-agno/tests/test_conformance.py | Registers new retrieval conformance scenario. |
| instrumentation/opentelemetry-instrumentation-genai-agno/tests/test_agent.py | Extends tests for tool failure handling + agent span attributes (request.model, agent.description). |
| instrumentation/opentelemetry-instrumentation-genai-agno/tests/conformance/retrieval.py | Adds retrieval conformance scenario exercising Knowledge.search. |
| instrumentation/opentelemetry-instrumentation-genai-agno/src/opentelemetry/instrumentation/genai/agno/utils.py | Adds helper to format retrieval documents for span attributes. |
| instrumentation/opentelemetry-instrumentation-genai-agno/src/opentelemetry/instrumentation/genai/agno/stream.py | Adds stream wrappers for tool executions and sets request model from streamed chunks when missing. |
| instrumentation/opentelemetry-instrumentation-genai-agno/src/opentelemetry/instrumentation/genai/agno/patch.py | Implements deferred wrapping, retrieval instrumentation, streaming tool result wrapping, and agent attribute extraction. |
| instrumentation/opentelemetry-instrumentation-genai-agno/README.rst | Documents new instrumentation coverage for Knowledge retrieval methods. |
| instrumentation/opentelemetry-instrumentation-genai-agno/.changelog/673.added | Adds changelog fragment describing retrieval + streaming tool support. |
Review details
- Files reviewed: 10/10 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…ttributes - Guard deferred post-import hooks with generation tracking and instrumentation state - Prevent stacking wrappers in _safe_wrap_function - Avoid mutating private AgentInvocation._request_model by using public attributes - Add unit tests for deferred uninstrument/re-instrument cycles and chunk model extraction Assisted-by: Antigravity
Assisted-by: Antigravity
Pull request dashboard statusWaiting on reviewers · refreshed 2026-09-11 13:39 UTC Review the latest changes. Status above doesn't look right?
|
| result = wrapped(*args, **kwargs) | ||
| _set_tool_invocation_output(invocation, result, capture_content) | ||
| return result | ||
| except Exception as error: |
There was a problem hiding this comment.
Catching Exception leaves cancelled tool invocations open and leaks their context. Catch BaseException, finalize the invocation, and re-raise unchanged in both tool wrappers and both retrieval wrappers.
This test fails on the PR and passes on its base (Agno 2.9):
def test_cancelled_tool_finishes(instrument_agno, span_exporter):
error = asyncio.CancelledError('cancelled')
async def cancelled():
raise error
async def run():
call = FunctionCall(function=Function.from_callable(cancelled), arguments={})
with pytest.raises(asyncio.CancelledError) as caught:
await call.aexecute()
assert caught.value is error
spans = span_exporter.get_finished_spans()
assert len(spans) == 1
assert spans[0].attributes['error.type'] == 'asyncio.exceptions.CancelledError'
asyncio.run(run())| if request_model: | ||
| invocation.attributes[GenAI.GEN_AI_REQUEST_MODEL] = str(request_model) |
There was a problem hiding this comment.
nit: invoke_local_agent(request_model=...) already records this attribute. Remove the redundant assignment.
| assert GenAIAttributes.GEN_AI_TOOL_DEFINITIONS not in span.attributes | ||
|
|
||
|
|
||
| def test_tool_call_execute_sync( |
There was a problem hiding this comment.
Missing test coverage for FunctionCall:
FunctionCall.aexecutein non-streaming mode (only syncFunctionCall.executeis tested).- Tool execution with content capture disabled for both sync and async (verifying
gen_ai.tool.call.resultandargumentsare suppressed).
| assert GenAIAttributes.GEN_AI_RETRIEVAL_DOCUMENTS not in span.attributes | ||
|
|
||
|
|
||
| def test_knowledge_asearch_content_capture( |
There was a problem hiding this comment.
Missing test coverage for Knowledge:
Knowledge.asearchwith content capture disabled (only syncKnowledge.searchis tested without content capture).Knowledge.asearchon error (only syncKnowledge.searcherror is tested).
| if wrapped_stream is not None: | ||
| result.result = wrapped_stream | ||
| instance.result = wrapped_stream | ||
| return result |
There was a problem hiding this comment.
Returning a tool stream leaves its span active in the caller context, so unrelated work before the stream is drained becomes its child. Restore the caller context before returning, and activate the tool context while consuming the stream.
Add this test to tests/test_tools.py. It fails on the PR and passes on its base (Agno 2.9):
def test_tool_stream_restores_caller_context(instrument_agno) -> None:
from collections.abc import Iterator
from opentelemetry.trace import get_current_span
def streaming_tool() -> Iterator[str]:
yield "chunk"
caller = get_current_span()
call = FunctionCall(
function=Function.from_callable(streaming_tool), arguments={}
)
result = call.execute()
current_after_return = get_current_span()
assert list(result.result) == ["chunk"]
assert current_after_return is caller| if isinstance(invocation, AgentInvocation): | ||
| model = getattr(result, "model", None) | ||
| if model: | ||
| invocation.attributes.setdefault( | ||
| GenAI.GEN_AI_REQUEST_MODEL, str(model) | ||
| ) |
There was a problem hiding this comment.
Agno sets RunOutput.model from agent.model.id before execution, so this is configured-model metadata, not a provider response model (source). Capture it at invocation start when applicable; remove this fallback rather than rename it to gen_ai.response.model, which is not defined for internal agent spans.
| if isinstance(invocation, AgentInvocation): | |
| model = getattr(result, "model", None) | |
| if model: | |
| invocation.attributes.setdefault( | |
| GenAI.GEN_AI_REQUEST_MODEL, str(model) | |
| ) |
| model = getattr(chunk, "model", None) | ||
| if model: | ||
| self._self_agent_invocation.attributes.setdefault( | ||
| GenAI.GEN_AI_REQUEST_MODEL, str(model) | ||
| ) |
There was a problem hiding this comment.
Please remove the stream fallback too. The run model comes from Agno's configured agent.model.id, not a provider response. Keep configured-model capture at invocation start; do not replace this with gen_ai.response.model.
| model = getattr(chunk, "model", None) | |
| if model: | |
| self._self_agent_invocation.attributes.setdefault( | |
| GenAI.GEN_AI_REQUEST_MODEL, str(model) | |
| ) |
Description
retrievalinstrumentation for Agno's Knowledge.search/Knowledge.asearch._safe_wrap_functionto defer patching submodules via post-import hooks, preventing eager imports of heavy optional dependencies (e.g. FastAPI for workflows).gen_ai.request.modelandgen_ai.agent.descriptionattributes toAgentInvocation.Type of change
Please delete options that are not relevant.
How has this been tested?
Unit tests and conformance tests
Checklist