-
Notifications
You must be signed in to change notification settings - Fork 22
fix(agno): fix aresponse missing await and double wrapped() calls #107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
Thanks for the contribution! Please fix the failing CI checks. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Fixes correctness issues in the Agno instrumentation wrapper where async responses could be returned without being awaited and streaming wrappers could invoke the wrapped callable twice.
Changes:
- Ensure
AgnoModelWrapper.aresponse()awaits the wrapped coroutine on the early-return path. - Remove redundant/double
wrapped()invocation inresponse_stream()andaresponse_stream(), and adjust error handling. - Add unit tests covering the above behaviors; ignore local
pyrightconfig.json.
Reviewed changes
Copilot reviewed 2 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| instrumentation-loongsuite/loongsuite-instrumentation-agno/src/opentelemetry/instrumentation/agno/_wrapper.py | Fixes missing await and removes double invocation in streaming wrappers. |
| instrumentation-loongsuite/loongsuite-instrumentation-agno/tests/test_wrapper.py | Adds regression tests for aresponse() awaiting and single-call streaming behavior. |
| .gitignore | Ignores local Pyright config file. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| mock_instance = MagicMock() | ||
| mock_instance.id = "test-model" | ||
|
|
||
| async def mock_async_generator(*args, **kwargs) -> AsyncIterator[str]: | ||
| yield "async_chunk1" | ||
| yield "async_chunk2" | ||
|
|
||
| async def run_test(): | ||
| wrapper = AgnoModelWrapper(tracer=MagicMock()) | ||
| wrapper._enable_genai_capture = lambda: True | ||
| wrapper._request_attributes_extractor = MagicMock() | ||
| wrapper._request_attributes_extractor.extract.return_value = {} | ||
| wrapper._response_attributes_extractor = MagicMock() | ||
| wrapper._response_attributes_extractor.extract.return_value = {} | ||
|
|
||
| with_span_mock = MagicMock() | ||
| with_span_mock.__enter__ = MagicMock(return_value=with_span_mock) | ||
| with_span_mock.__exit__ = MagicMock(return_value=False) | ||
| with_span_mock.finish_tracing = MagicMock() | ||
| wrapper._start_as_current_span = MagicMock(return_value=with_span_mock) | ||
|
|
||
| results = [] | ||
| async for chunk in wrapper.aresponse_stream( | ||
| mock_async_generator, mock_instance, (), {"messages": []} | ||
| ): | ||
| results.append(chunk) | ||
| return results | ||
|
|
||
| results = asyncio.run(run_test()) | ||
| AgnoModelWrapper.aresponse_stream = original_method | ||
|
|
Copilot
AI
Jan 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test monkey-patches AgnoModelWrapper.aresponse_stream globally and restores it only on the success path. If an exception occurs before the restore, the patched method can leak into other tests. Prefer try/finally or pytest’s monkeypatch fixture for safe restoration.
| mock_instance = MagicMock() | |
| mock_instance.id = "test-model" | |
| async def mock_async_generator(*args, **kwargs) -> AsyncIterator[str]: | |
| yield "async_chunk1" | |
| yield "async_chunk2" | |
| async def run_test(): | |
| wrapper = AgnoModelWrapper(tracer=MagicMock()) | |
| wrapper._enable_genai_capture = lambda: True | |
| wrapper._request_attributes_extractor = MagicMock() | |
| wrapper._request_attributes_extractor.extract.return_value = {} | |
| wrapper._response_attributes_extractor = MagicMock() | |
| wrapper._response_attributes_extractor.extract.return_value = {} | |
| with_span_mock = MagicMock() | |
| with_span_mock.__enter__ = MagicMock(return_value=with_span_mock) | |
| with_span_mock.__exit__ = MagicMock(return_value=False) | |
| with_span_mock.finish_tracing = MagicMock() | |
| wrapper._start_as_current_span = MagicMock(return_value=with_span_mock) | |
| results = [] | |
| async for chunk in wrapper.aresponse_stream( | |
| mock_async_generator, mock_instance, (), {"messages": []} | |
| ): | |
| results.append(chunk) | |
| return results | |
| results = asyncio.run(run_test()) | |
| AgnoModelWrapper.aresponse_stream = original_method | |
| try: | |
| mock_instance = MagicMock() | |
| mock_instance.id = "test-model" | |
| async def mock_async_generator(*args, **kwargs) -> AsyncIterator[str]: | |
| yield "async_chunk1" | |
| yield "async_chunk2" | |
| async def run_test(): | |
| wrapper = AgnoModelWrapper(tracer=MagicMock()) | |
| wrapper._enable_genai_capture = lambda: True | |
| wrapper._request_attributes_extractor = MagicMock() | |
| wrapper._request_attributes_extractor.extract.return_value = {} | |
| wrapper._response_attributes_extractor = MagicMock() | |
| wrapper._response_attributes_extractor.extract.return_value = {} | |
| with_span_mock = MagicMock() | |
| with_span_mock.__enter__ = MagicMock(return_value=with_span_mock) | |
| with_span_mock.__exit__ = MagicMock(return_value=False) | |
| with_span_mock.finish_tracing = MagicMock() | |
| wrapper._start_as_current_span = MagicMock(return_value=with_span_mock) | |
| results = [] | |
| async for chunk in wrapper.aresponse_stream( | |
| mock_async_generator, mock_instance, (), {"messages": []} | |
| ): | |
| results.append(chunk) | |
| return results | |
| results = asyncio.run(run_test()) | |
| finally: | |
| AgnoModelWrapper.aresponse_stream = original_method |
| original_method = AgnoModelWrapper.aresponse | ||
|
|
||
| mock_instance = MagicMock() | ||
| mock_instance.id = "test-model" | ||
|
|
||
| async def run_test(): | ||
| wrapper = AgnoModelWrapper(tracer=MagicMock()) | ||
| wrapper._enable_genai_capture = lambda: False | ||
|
|
||
| async def mock_wrapped(*args, **kwargs): | ||
| return "expected_result" | ||
|
|
||
| result = await wrapper.aresponse( | ||
| mock_wrapped, mock_instance, (), {"messages": []} | ||
| ) | ||
| assert not asyncio.iscoroutine(result), \ | ||
| "aresponse() returned coroutine instead of awaited result" | ||
| return result | ||
|
|
||
| asyncio.run(run_test()) | ||
| AgnoModelWrapper.aresponse = original_method | ||
|
|
Copilot
AI
Jan 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
original_method is saved/restored here but the test never monkey-patches AgnoModelWrapper.aresponse, so the restore is redundant and may confuse readers. Consider removing the save/restore block and (optionally) assert the awaited result equals the expected value.
| attributes=dict(resp_attr), | ||
| extra_attributes=dict(resp_attr), | ||
| ) | ||
| except Exception: | ||
| logger.exception( | ||
| f"Failed to finalize response of type {type(response)}" | ||
| except Exception as exception: | ||
| with_span.record_exception(exception) |
Copilot
AI
Jan 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
response_stream wraps both user streaming and instrumentation finalization in the same try/except. As written, an internal failure during response attribute extraction or finish_tracing will be caught and re-raised to the caller, which differs from the established pattern in this module (non-stream and other stream wrappers log + suppress finalization errors to avoid breaking user code). Consider splitting the logic so exceptions from wrapped/iteration propagate, but extractor/finalization exceptions are logged/suppressed while still ending the span.
| from typing import AsyncIterator, Iterator | ||
| from unittest.mock import MagicMock | ||
|
|
||
| import pytest |
Copilot
AI
Jan 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pytest is imported but never used in this test module. Removing the unused import avoids lint/type-check noise.
| import pytest |
| AgnoModelWrapper.response_stream = patched_method | ||
|
|
Copilot
AI
Jan 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test monkey-patches AgnoModelWrapper.response_stream globally and restores it only on the success path. If the test fails/raises before restoration, it can leak the patched method into other tests. Use try/finally around the body or pytest’s monkeypatch fixture to guarantee cleanup.
Description
Fix issues in agno instrumentation wrapper:
aresponse(): Add missingawaiton early return path.response_stream()&aresponse_stream(): Remove redundantwrapped()call that caused double API invocationsType of change
Please delete options that are not relevant.
How Has This Been Tested?
instrumentation-loongsuite/loongsuite-instrumentation-agno/tests/test_wrapper.py
Does This PR Require a Core Repo Change?
Checklist:
See contributing.md for styleguide, changelog guidelines, and more.