Skip to content

[opentelemetry-instrumentation-genai-agno] Add instrumentation for retrieval and make various other improvements - #673

Open
DylanRussell wants to merge 7 commits into
mainfrom
DylanRussell/more_agno_instrumentation
Open

[opentelemetry-instrumentation-genai-agno] Add instrumentation for retrieval and make various other improvements#673
DylanRussell wants to merge 7 commits into
mainfrom
DylanRussell/more_agno_instrumentation

Conversation

@DylanRussell

Copy link
Copy Markdown
Contributor

Description

  • Add GenAI retrieval instrumentation for Agno's Knowledge.search/Knowledge.asearch.
  • Add _safe_wrap_function to defer patching submodules via post-import hooks, preventing eager imports of heavy optional dependencies (e.g. FastAPI for workflows).
  • Add support for streaming tool responses (tools that return sync or async generators/iterators).
  • Added gen_ai.request.model and gen_ai.agent.description attributes to AgentInvocation.

Type of change

Please delete options that are not relevant.

  • New feature (non-breaking change which adds functionality)

How has this been tested?

Unit tests and conformance tests

Checklist

  • Followed the style guidelines of this project
  • Changelog updated if the change requires an entry
  • Unit tests added
  • Documentation updated

Copilot AI lite review requested due to automatic review settings September 10, 2026 20:05
@DylanRussell
DylanRussell requested a review from a team as a code owner September 10, 2026 20:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.asearch as GenAI retrieval spans (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
@opentelemetry-pr-dashboard

opentelemetry-pr-dashboard Bot commented Sep 11, 2026

Copy link
Copy Markdown

Pull request dashboard status

Waiting on reviewers · refreshed 2026-09-11 13:39 UTC

Review the latest changes.

Status above doesn't look right?
  • Just replied or pushed? Anything around or after the refresh time above may not be picked up yet — give it a few minutes.
  • Anything look wrong? Report it with what you expected; it helps us improve the dashboard.

result = wrapped(*args, **kwargs)
_set_tool_invocation_output(invocation, result, capture_content)
return result
except Exception as error:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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())

Comment on lines +367 to +368
if request_model:
invocation.attributes[GenAI.GEN_AI_REQUEST_MODEL] = str(request_model)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for FunctionCall:

  • FunctionCall.aexecute in non-streaming mode (only sync FunctionCall.execute is tested).
  • Tool execution with content capture disabled for both sync and async (verifying gen_ai.tool.call.result and arguments are suppressed).

assert GenAIAttributes.GEN_AI_RETRIEVAL_DOCUMENTS not in span.attributes


def test_knowledge_asearch_content_capture(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for Knowledge:

  • Knowledge.asearch with content capture disabled (only sync Knowledge.search is tested without content capture).
  • Knowledge.asearch on error (only sync Knowledge.search error is tested).

if wrapped_stream is not None:
result.result = wrapped_stream
instance.result = wrapped_stream
return result

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +340 to +345
if isinstance(invocation, AgentInvocation):
model = getattr(result, "model", None)
if model:
invocation.attributes.setdefault(
GenAI.GEN_AI_REQUEST_MODEL, str(model)
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
if isinstance(invocation, AgentInvocation):
model = getattr(result, "model", None)
if model:
invocation.attributes.setdefault(
GenAI.GEN_AI_REQUEST_MODEL, str(model)
)

Comment on lines +55 to +59
model = getattr(chunk, "model", None)
if model:
self._self_agent_invocation.attributes.setdefault(
GenAI.GEN_AI_REQUEST_MODEL, str(model)
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
model = getattr(chunk, "model", None)
if model:
self._self_agent_invocation.attributes.setdefault(
GenAI.GEN_AI_REQUEST_MODEL, str(model)
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants