[opentelemetry-instrumentation-genai-dspy] Instrument tool calls and react agent loops - #529
[opentelemetry-instrumentation-genai-dspy] Instrument tool calls and react agent loops#529DylanRussell wants to merge 6 commits into
opentelemetry-instrumentation-genai-dspy] Instrument tool calls and react agent loops#529Conversation
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Adds OpenTelemetry GenAI span emission for DSPy tool executions and ReAct/ReActV2 agent loops via monkey-patching, plus unit + conformance tests and tox wiring to validate the behavior.
Changes:
- Introduce DSPy monkey patches to emit
execute_toolspans andinvoke_agentspans. - Add unit tests for Tool, ReAct (v1), and ReActV2 instrumentation.
- Add DSPy conformance test scenarios and a dedicated tox environment to run them.
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tox.ini | Adds a dedicated DSPy conformance test environment and command. |
| instrumentation/opentelemetry-instrumentation-genai-dspy/src/opentelemetry/instrumentation/genai/dspy/patch.py | Implements monkey patches for dspy.Tool and ReAct/ReActV2 forward loops to emit spans. |
| instrumentation/opentelemetry-instrumentation-genai-dspy/src/opentelemetry/instrumentation/genai/dspy/utils.py | Adds helpers for extracting input/output content and preparing tool definitions for span attributes. |
| instrumentation/opentelemetry-instrumentation-genai-dspy/src/opentelemetry/instrumentation/genai/dspy/init.py | Wires patch/unpatch into DSPyInstrumentor. |
| instrumentation/opentelemetry-instrumentation-genai-dspy/tests/test_tools.py | Adds unit tests validating tool spans, argument extraction, and error capture. |
| instrumentation/opentelemetry-instrumentation-genai-dspy/tests/test_react.py | Adds unit tests validating ReAct (v1) agent spans and relationships to tool spans. |
| instrumentation/opentelemetry-instrumentation-genai-dspy/tests/test_react_v2.py | Adds unit tests validating ReActV2 agent spans and relationships to tool spans. |
| instrumentation/opentelemetry-instrumentation-genai-dspy/tests/test_conformance.py | Adds conformance test runner for per-scenario validation. |
| instrumentation/opentelemetry-instrumentation-genai-dspy/tests/conformance/react.py | Adds a ReAct conformance scenario. |
| instrumentation/opentelemetry-instrumentation-genai-dspy/tests/conformance/tool.py | Adds a tool execution conformance scenario. |
| instrumentation/opentelemetry-instrumentation-genai-dspy/pyproject.toml | Updates dependency versions (DSPy minimum and util-genai). |
| instrumentation/opentelemetry-instrumentation-genai-dspy/README.rst | Updates README to reflect implemented instrumentation coverage. |
| instrumentation/opentelemetry-instrumentation-genai-dspy/tests/requirements.oldest.txt | Adds an editable util-genai dependency for oldest-factor runs (temporary). |
| instrumentation/opentelemetry-instrumentation-genai-dspy/.changelog/529.added | Adds changelog entry for new DSPy instrumentation. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Pull request dashboard statusWaiting on the author · refreshed 2026-09-04 21:20 UTC Two things need attention:
Status above doesn't look right?
|
d912032 to
93b5d07
Compare
93b5d07 to
9c0e642
Compare
| wrap_function_wrapper( | ||
| _REACT_V2_MODULE, | ||
| f"{_REACT_V2_CLASS}.forward", | ||
| _react_forward(handler, "dspy.ReActV2"), | ||
| ) | ||
| react_v2_cls = getattr( | ||
| sys.modules.get(_REACT_V2_MODULE), _REACT_V2_CLASS, None | ||
| ) | ||
| if react_v2_cls is not None and hasattr(react_v2_cls, "aforward"): | ||
| wrap_function_wrapper( | ||
| _REACT_V2_MODULE, | ||
| f"{_REACT_V2_CLASS}.aforward", | ||
| _react_aforward(handler, "dspy.ReActV2"), | ||
| ) |
There was a problem hiding this comment.
ReActV2 is @experimental in dspy - it can change or go away in any release inside >=3.3.0,<4. When it does, this raises after Tool and ReAct are already wrapped, so all DSPy instrumentation breaks, not just ReActV2. Skip it instead of failing:
| wrap_function_wrapper( | |
| _REACT_V2_MODULE, | |
| f"{_REACT_V2_CLASS}.forward", | |
| _react_forward(handler, "dspy.ReActV2"), | |
| ) | |
| react_v2_cls = getattr( | |
| sys.modules.get(_REACT_V2_MODULE), _REACT_V2_CLASS, None | |
| ) | |
| if react_v2_cls is not None and hasattr(react_v2_cls, "aforward"): | |
| wrap_function_wrapper( | |
| _REACT_V2_MODULE, | |
| f"{_REACT_V2_CLASS}.aforward", | |
| _react_aforward(handler, "dspy.ReActV2"), | |
| ) | |
| react_v2_cls = getattr( | |
| sys.modules.get(_REACT_V2_MODULE), _REACT_V2_CLASS, None | |
| ) | |
| if react_v2_cls is not None: | |
| wrap_function_wrapper( | |
| _REACT_V2_MODULE, | |
| f"{_REACT_V2_CLASS}.forward", | |
| _react_forward(handler, "dspy.ReActV2"), | |
| ) | |
| if hasattr(react_v2_cls, "aforward"): | |
| wrap_function_wrapper( | |
| _REACT_V2_MODULE, | |
| f"{_REACT_V2_CLASS}.aforward", | |
| _react_aforward(handler, "dspy.ReActV2"), | |
| ) |
Cheap to cover: monkeypatch.delattr(dspy.predict.react_v2, "ReActV2"), instrument, assert Tool and ReAct still emit spans. That test also covers the uninstrument side below.
| unwrap(f"{_REACT_V2_MODULE}.{_REACT_V2_CLASS}", "forward") | ||
| unwrap(f"{_REACT_V2_MODULE}.{_REACT_V2_CLASS}", "aforward") |
There was a problem hiding this comment.
Same on the way out - unwrap() with a dotted path raises ImportError when the class is gone (it only tolerates a missing module), so uninstrument breaks too. Pass the resolved class:
| unwrap(f"{_REACT_V2_MODULE}.{_REACT_V2_CLASS}", "forward") | |
| unwrap(f"{_REACT_V2_MODULE}.{_REACT_V2_CLASS}", "aforward") | |
| react_v2_cls = getattr( | |
| sys.modules.get(_REACT_V2_MODULE), _REACT_V2_CLASS, None | |
| ) | |
| if react_v2_cls is not None: | |
| unwrap(react_v2_cls, "forward") | |
| unwrap(react_v2_cls, "aforward") |
|
|
||
|
|
||
| @pytest.mark.anyio | ||
| async def test_react_async_execution( |
There was a problem hiding this comment.
Please consider adding a test for async error path for ReAct.aforward
Description
Monkey patches
dspy.Tool.__call__anddspy.Tool.acallmethods so that an execute tool span is emitted (https://dspy.ai/api/primitives/Tool/)Monkey patches
dspy.predict.react.ReAct.forward(andaforward) anddspy.predict.react_v2.ReActV2.forwardso that an invoke agent span is emitted (https://dspy.ai/diving-deeper/react/).Also, I'm going to require
DsPyv3.3+ -- this gets uslitellmv1.84 which fixed a bad vulnerability in all previous versions. Also it simplifies the implementation a little bit, we don't need to wrap certain imports in try/except blocks because they are guaranteed to exist in 3.3+..Type of change
How has this been tested?
Unit tests, live weaver tests..
Checklist