Skip to content

Commit bd54976

Browse files
committed
feat: Improvements to agno workflows PR
Reworked the Agno wrapper tests into package-local modules, splitting workflow coverage into its own test file and adding shared test helpers plus a dedicated workflow cassette. Updated the Agno Nox session to run the relocated tests and install `fastapi` so workflow integration coverage runs in the supported Agno matrix. Fixed Agno workflow instrumentation by correcting async workflow input extraction, adding workflow-specific stream aggregation, preventing duplicate streamed workflow output, and propagating workflow metadata (`workflow_id` / `workflow_name`) into nested async agent spans. Added regression tests for async input extraction, streamed workflow aggregation, duplicate-content handling, and nested async agent metadata propagation across workflow spans.
1 parent a2e0a60 commit bd54976

7 files changed

Lines changed: 768 additions & 293 deletions

File tree

py/noxfile.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,9 @@ def test_agno(session, version):
143143
_install_test_deps(session)
144144
_install(session, "agno", version)
145145
_install(session, "openai") # Required for agno.models.openai
146-
_run_tests(session, f"{WRAPPER_DIR}/test_agno.py")
146+
_install(session, "fastapi") # Required for agno.workflow
147+
_run_tests(session, f"{WRAPPER_DIR}/agno/test_agno.py")
148+
_run_tests(session, f"{WRAPPER_DIR}/agno/test_workflow.py")
147149
_run_core_tests(session)
148150

149151

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
# pyright: reportMissingParameterType=false
2+
# pyright: reportUnknownMemberType=false
3+
# pyright: reportUnknownParameterType=false
4+
# pyright: reportUnknownVariableType=false
5+
6+
from inspect import isawaitable
7+
8+
import pytest
9+
from braintrust import logger
10+
from braintrust.test_helpers import init_test_logger
11+
from braintrust.wrappers.agno.agent import wrap_agent
12+
13+
PROJECT_NAME = "test-agno-app"
14+
15+
16+
@pytest.fixture
17+
def memory_logger():
18+
init_test_logger(PROJECT_NAME)
19+
with logger._internal_with_memory_background_logger() as bgl:
20+
yield bgl
21+
22+
23+
class FakeMetrics:
24+
def __init__(self):
25+
self.input_tokens = 1
26+
self.output_tokens = 2
27+
self.total_tokens = 3
28+
self.duration = 0.1
29+
self.time_to_first_token = 0.01
30+
31+
32+
class FakeRunOutput:
33+
def __init__(self, content: str):
34+
self.content = content
35+
self.status = "COMPLETED"
36+
self.model = "fake-model"
37+
self.model_provider = "FakeProvider"
38+
self.metrics = FakeMetrics()
39+
40+
41+
class FakeEvent:
42+
def __init__(self, event: str, **kwargs):
43+
self.event = event
44+
for k, v in kwargs.items():
45+
setattr(self, k, v)
46+
47+
48+
class FakeExecutionInput:
49+
def __init__(self, input):
50+
self.input = input
51+
self.kind = "workflow-execution"
52+
53+
54+
class FakeWorkflowRunResponse:
55+
def __init__(self, input=None, content: str | None = None):
56+
self.input = input
57+
self.content = content
58+
self.status = "COMPLETED"
59+
self.metrics = FakeMetrics()
60+
61+
62+
def make_fake_workflow(name: str):
63+
class FakeWorkflow:
64+
def __init__(self):
65+
self.name = name
66+
self.steps = ["first-step"]
67+
68+
async def _aexecute(self, session_id, user_id, execution_input, workflow_run_response, run_context=None):
69+
return FakeWorkflowRunResponse(input=execution_input.input, content="workflow-async")
70+
71+
def _execute_stream(self, session, execution_input, workflow_run_response, run_context=None):
72+
yield FakeEvent("WorkflowStarted", content=None)
73+
yield FakeEvent("StepStarted", content=None)
74+
yield FakeEvent("StepCompleted", content="hello ")
75+
yield FakeEvent("WorkflowCompleted", content="world", metrics=FakeMetrics(), status="COMPLETED")
76+
77+
return FakeWorkflow
78+
79+
80+
def make_fake_duplicate_content_workflow(name: str):
81+
class FakeWorkflow:
82+
def __init__(self):
83+
self.name = name
84+
self.steps = ["first-step"]
85+
86+
def _execute_stream(self, session, execution_input, workflow_run_response, run_context=None):
87+
yield FakeEvent("StepCompleted", content="hello")
88+
yield FakeEvent("WorkflowCompleted", content="hello", metrics=FakeMetrics(), status="COMPLETED")
89+
90+
return FakeWorkflow
91+
92+
93+
def make_fake_workflow_with_async_agent(name: str, agent_name: str):
94+
class FakeAgent:
95+
def __init__(self):
96+
self.name = agent_name
97+
98+
async def arun(self, input, stream=False, **kwargs):
99+
return FakeRunOutput(f"{input}-async")
100+
101+
WrappedAgent = wrap_agent(FakeAgent)
102+
103+
class FakeWorkflow:
104+
def __init__(self):
105+
self.name = name
106+
self.id = "workflow-123"
107+
self.steps = ["agent-step"]
108+
self.agent = WrappedAgent()
109+
110+
async def _aexecute(self, session_id, user_id, execution_input, workflow_run_response, run_context=None):
111+
return await self.agent.arun(execution_input.input)
112+
113+
return FakeWorkflow
114+
115+
116+
def make_fake_component(name: str):
117+
class FakeComponent:
118+
def __init__(self):
119+
self.name = name
120+
121+
def run(self, input, stream=False, **kwargs):
122+
if stream:
123+
124+
def _stream():
125+
yield FakeEvent("RunStarted", model="fake-model", model_provider="FakeProvider")
126+
yield FakeEvent("RunContent", content=f"{input}-sync")
127+
yield FakeEvent("RunCompleted", metrics=FakeMetrics())
128+
129+
return _stream()
130+
return FakeRunOutput(f"{input}-sync")
131+
132+
def arun(self, input, stream=False, **kwargs):
133+
if stream:
134+
135+
async def _astream():
136+
yield FakeEvent("RunStarted", model="fake-model", model_provider="FakeProvider")
137+
yield FakeEvent("RunContent", content=f"{input}-async")
138+
yield FakeEvent("RunCompleted", metrics=FakeMetrics())
139+
140+
return _astream()
141+
142+
async def _result():
143+
return FakeRunOutput(f"{input}-async")
144+
145+
return _result()
146+
147+
return FakeComponent
148+
149+
150+
def make_fake_async_dispatch_component(name: str):
151+
class FakeComponent:
152+
def __init__(self):
153+
self.name = name
154+
155+
async def arun(self, input, stream=False, **kwargs):
156+
if stream:
157+
158+
async def _astream():
159+
yield FakeEvent("RunStarted", model="fake-model", model_provider="FakeProvider")
160+
yield FakeEvent("RunContent", content=f"{input}-awaited-async")
161+
yield FakeEvent("RunCompleted", metrics=FakeMetrics())
162+
163+
return _astream()
164+
return {"content": f"{input}-awaited-async"}
165+
166+
return FakeComponent
167+
168+
169+
def make_fake_error_component(name: str):
170+
class FakeComponent:
171+
def __init__(self):
172+
self.name = name
173+
174+
def run(self, input, stream=False, **kwargs):
175+
if stream:
176+
177+
def _stream():
178+
yield FakeEvent("RunStarted", model="fake-model", model_provider="FakeProvider")
179+
raise RuntimeError("sync-stream-error")
180+
181+
return _stream()
182+
return FakeRunOutput(f"{input}-sync")
183+
184+
def arun(self, input, stream=False, **kwargs):
185+
if stream:
186+
187+
async def _astream():
188+
yield FakeEvent("RunStarted", model="fake-model", model_provider="FakeProvider")
189+
raise RuntimeError("async-stream-error")
190+
191+
return _astream()
192+
193+
async def _result():
194+
return FakeRunOutput(f"{input}-async")
195+
196+
return _result()
197+
198+
return FakeComponent
199+
200+
201+
def make_fake_private_public_component(name: str):
202+
class FakeComponent:
203+
def __init__(self):
204+
self.name = name
205+
self.calls = []
206+
207+
def _run(self, run_response=None, run_messages=None, **kwargs):
208+
self.calls.append("_run")
209+
return FakeRunOutput("private-run")
210+
211+
def run(self, input, **kwargs):
212+
self.calls.append("run")
213+
return FakeRunOutput("public-run")
214+
215+
async def _arun(self, run_response=None, input=None, **kwargs):
216+
self.calls.append("_arun")
217+
return FakeRunOutput("private-arun")
218+
219+
def arun(self, input, **kwargs):
220+
self.calls.append("arun")
221+
222+
async def _result():
223+
return FakeRunOutput("public-arun")
224+
225+
return _result()
226+
227+
return FakeComponent
228+
229+
230+
class StrictSpan:
231+
def __init__(self):
232+
self.ended = False
233+
234+
def set_current(self):
235+
return None
236+
237+
def unset_current(self):
238+
return None
239+
240+
def log(self, **kwargs):
241+
if self.ended:
242+
raise AssertionError("log called after span.end()")
243+
244+
def end(self):
245+
self.ended = True
246+
247+
248+
__all__ = [
249+
"FakeExecutionInput",
250+
"FakeWorkflowRunResponse",
251+
"PROJECT_NAME",
252+
"StrictSpan",
253+
"isawaitable",
254+
"make_fake_async_dispatch_component",
255+
"make_fake_component",
256+
"make_fake_workflow_with_async_agent",
257+
"make_fake_duplicate_content_workflow",
258+
"make_fake_error_component",
259+
"make_fake_private_public_component",
260+
"make_fake_workflow",
261+
"memory_logger",
262+
]

0 commit comments

Comments
 (0)