Skip to content

Commit 74b6875

Browse files
committed
feat: Enhance async handling in simple_agent and related tests
- Updated `get_graph` function in `simple_agent.py` to utilize async component initialization for improved responsiveness. - Modified test cases in `test_simple_agent_in_lfx_run.py` to validate the async behavior of `get_graph`. - Refactored various test functions across multiple files to support async execution, ensuring compatibility with the new async workflows. - Improved documentation for async functions to clarify their purpose and usage.
1 parent 777163b commit 74b6875

File tree

5 files changed

+124
-47
lines changed

5 files changed

+124
-47
lines changed

src/backend/tests/data/simple_agent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ async def get_graph() -> Graph:
4242
chat_input = cp.ChatInput()
4343
agent = cp.AgentComponent()
4444

45-
# For this example, we'll use a calculator tool that can be built synchronously
45+
# Use URLComponent for web search capabilities
4646
url_component = cp.URLComponent()
4747
tools = await url_component.to_toolkit()
4848

src/backend/tests/unit/test_simple_agent_in_lfx_run.py

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,15 @@ def simple_agent_script_content(self):
2222
return '''"""A simple agent flow example for Langflow.
2323
2424
This script demonstrates how to set up a conversational agent using Langflow's
25-
Agent component with web search capabilities.
25+
Agent component with proper async handling.
2626
2727
Features:
2828
- Uses the new flattened component access (cp.AgentComponent instead of deep imports)
2929
- Configures logging to 'langflow.log' at INFO level
3030
- Creates an agent with OpenAI GPT model
31-
- Provides web search tools via URLComponent
3231
- Connects ChatInput → Agent → ChatOutput
32+
- Uses async get_graph() function for proper async handling
33+
- Demonstrates the new async script loading pattern
3334
3435
Usage:
3536
uv run lfx run simple_agent.py "How are you?"
@@ -43,27 +44,42 @@ def simple_agent_script_content(self):
4344
from lfx.graph import Graph
4445
from lfx.log.logger import LogConfig
4546
46-
log_config = LogConfig(
47-
log_level="INFO",
48-
log_file=Path("langflow.log"),
49-
)
50-
51-
# Showcase the new flattened component access - no need for deep imports!
52-
chat_input = cp.ChatInput()
53-
agent = cp.AgentComponent()
54-
url_component = cp.URLComponent()
55-
tools = url_component.to_toolkit()
56-
57-
agent.set(
58-
model_name="gpt-4o-mini",
59-
agent_llm="OpenAI",
60-
api_key=os.getenv("OPENAI_API_KEY"),
61-
input_value=chat_input.message_response,
62-
tools=tools,
63-
)
64-
chat_output = cp.ChatOutput().set(input_value=agent.message_response)
65-
66-
graph = Graph(chat_input, chat_output, log_config=log_config)
47+
48+
async def get_graph() -> Graph:
49+
"""Create and return the graph with async component initialization.
50+
51+
This function properly handles async component initialization without
52+
blocking the module loading process. The script loader will detect this
53+
async function and handle it appropriately using run_until_complete.
54+
55+
Returns:
56+
Graph: The configured graph with ChatInput → Agent → ChatOutput flow
57+
"""
58+
log_config = LogConfig(
59+
log_level="INFO",
60+
log_file=Path("langflow.log"),
61+
)
62+
63+
# Showcase the new flattened component access - no need for deep imports!
64+
chat_input = cp.ChatInput()
65+
agent = cp.AgentComponent()
66+
67+
# Use URLComponent for web search capabilities
68+
url_component = cp.URLComponent()
69+
70+
# Properly handle async component initialization
71+
tools = await url_component.to_toolkit()
72+
73+
agent.set(
74+
model_name="gpt-4o-mini",
75+
agent_llm="OpenAI",
76+
api_key=os.getenv("OPENAI_API_KEY"),
77+
input_value=chat_input.message_response,
78+
tools=tools,
79+
)
80+
chat_output = cp.ChatOutput().set(input_value=agent.message_response)
81+
82+
return Graph(chat_input, chat_output, log_config=log_config)
6783
'''
6884

6985
@pytest.fixture
@@ -96,10 +112,11 @@ def test_agent_script_structure_and_syntax(self, simple_agent_script_content):
96112
assert "cp.AgentComponent()" in simple_agent_script_content
97113
assert "cp.URLComponent()" in simple_agent_script_content
98114
assert "cp.ChatOutput()" in simple_agent_script_content
99-
assert "url_component.to_toolkit()" in simple_agent_script_content
115+
assert "async def get_graph()" in simple_agent_script_content
116+
assert "await url_component.to_toolkit()" in simple_agent_script_content
100117
assert 'model_name="gpt-4o-mini"' in simple_agent_script_content
101118
assert 'agent_llm="OpenAI"' in simple_agent_script_content
102-
assert "Graph(chat_input, chat_output" in simple_agent_script_content
119+
assert "return Graph(chat_input, chat_output" in simple_agent_script_content
103120

104121
def test_agent_script_file_validation(self, simple_agent_script_file):
105122
"""Test that the agent script file exists and has valid content."""
@@ -111,7 +128,8 @@ def test_agent_script_file_validation(self, simple_agent_script_file):
111128
content = simple_agent_script_file.read_text()
112129
assert "from lfx import components as cp" in content
113130
assert "cp.AgentComponent()" in content
114-
assert "Graph(chat_input, chat_output" in content
131+
assert "async def get_graph()" in content
132+
assert "return Graph(chat_input, chat_output" in content
115133

116134
def test_agent_script_supports_formats(self, simple_agent_script_file):
117135
"""Test that the script supports logging configuration."""

src/lfx/tests/unit/cli/test_common.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,8 @@ def test_flow_id_from_path_different_paths(self):
168168
class TestLoadGraph:
169169
"""Test graph loading functionality."""
170170

171-
def test_load_graph_from_path_success(self):
171+
@pytest.mark.asyncio
172+
async def test_load_graph_from_path_success(self):
172173
"""Test successful graph loading from JSON."""
173174
mock_graph = MagicMock()
174175
mock_graph.nodes = [1, 2, 3]
@@ -177,21 +178,22 @@ def test_load_graph_from_path_success(self):
177178
verbose_print = Mock()
178179
path = Path("/test/flow.json")
179180

180-
result = load_graph_from_path(path, ".json", verbose_print, verbose=True)
181+
result = await load_graph_from_path(path, ".json", verbose_print, verbose=True)
181182

182183
assert result == mock_graph
183184
mock_load_flow.assert_called_once_with(path, disable_logs=False)
184185
verbose_print.assert_any_call(f"Analyzing JSON flow: {path}")
185186
verbose_print.assert_any_call("Loading JSON flow...")
186187

187-
def test_load_graph_from_path_failure(self):
188+
@pytest.mark.asyncio
189+
async def test_load_graph_from_path_failure(self):
188190
"""Test graph loading failure."""
189191
with patch("lfx.cli.common.load_flow_from_json", side_effect=Exception("Load error")) as mock_load_flow:
190192
verbose_print = Mock()
191193
path = Path("/test/flow.json")
192194

193195
with pytest.raises(typer.Exit) as exc_info:
194-
load_graph_from_path(path, ".json", verbose_print, verbose=False)
196+
await load_graph_from_path(path, ".json", verbose_print, verbose=False)
195197

196198
assert exc_info.value.exit_code == 1
197199
mock_load_flow.assert_called_once_with(path, disable_logs=True)

src/lfx/tests/unit/cli/test_script_loader.py

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -170,10 +170,10 @@ def test_validate_graph_instance_missing_chat_output(self):
170170
class TestLoadGraphFromScript:
171171
"""Test loading graph from script functionality."""
172172

173-
def test_load_graph_from_script_success(self, simple_chat_py):
173+
async def test_load_graph_from_script_success(self, simple_chat_py):
174174
"""Test successful graph loading from script with real Graph object."""
175175
# Use the existing test data file
176-
graph = load_graph_from_script(simple_chat_py)
176+
graph = await load_graph_from_script(simple_chat_py)
177177

178178
# Verify it's a real Graph instance
179179
from lfx.graph import Graph
@@ -185,24 +185,78 @@ def test_load_graph_from_script_success(self, simple_chat_py):
185185
assert "ChatInput" in component_names
186186
assert "ChatOutput" in component_names
187187

188-
def test_load_graph_from_script_no_graph_variable(self):
188+
async def test_load_graph_from_script_no_graph_variable(self):
189189
"""Test error when script has no graph variable."""
190190
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
191191
f.write("other_var = 123\n")
192192
script_path = Path(f.name)
193193

194194
try:
195-
with pytest.raises(RuntimeError, match="No 'graph' variable found"):
196-
load_graph_from_script(script_path)
195+
with pytest.raises(RuntimeError, match="No 'graph' variable or 'get_graph\\(\\)' function found"):
196+
await load_graph_from_script(script_path)
197197
finally:
198198
script_path.unlink()
199199

200-
def test_load_graph_from_script_import_error(self):
200+
async def test_load_graph_from_script_import_error(self):
201201
"""Test error handling for import errors."""
202202
script_path = Path("/non/existent/script.py")
203203

204204
with pytest.raises(RuntimeError, match="Error executing script"):
205-
load_graph_from_script(script_path)
205+
await load_graph_from_script(script_path)
206+
207+
async def test_load_graph_from_script_with_async_get_graph(self):
208+
"""Test loading graph from script with async get_graph function."""
209+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
210+
f.write("from lfx.components.input_output import ChatInput, ChatOutput\n")
211+
f.write("from lfx.graph import Graph\n")
212+
f.write("\n")
213+
f.write("async def get_graph():\n")
214+
f.write(" chat_input = ChatInput()\n")
215+
f.write(" chat_output = ChatOutput().set(input_value=chat_input.message_response)\n")
216+
f.write(" return Graph(chat_input, chat_output)\n")
217+
script_path = Path(f.name)
218+
219+
try:
220+
graph = await load_graph_from_script(script_path)
221+
222+
# Verify it's a real Graph instance
223+
from lfx.graph import Graph
224+
225+
assert isinstance(graph, Graph)
226+
227+
# Verify it has the expected components
228+
component_names = {v.custom_component.__class__.__name__ for v in graph.vertices}
229+
assert "ChatInput" in component_names
230+
assert "ChatOutput" in component_names
231+
finally:
232+
script_path.unlink()
233+
234+
async def test_load_graph_from_script_with_sync_get_graph(self):
235+
"""Test loading graph from script with sync get_graph function."""
236+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
237+
f.write("from lfx.components.input_output import ChatInput, ChatOutput\n")
238+
f.write("from lfx.graph import Graph\n")
239+
f.write("\n")
240+
f.write("def get_graph():\n")
241+
f.write(" chat_input = ChatInput()\n")
242+
f.write(" chat_output = ChatOutput().set(input_value=chat_input.message_response)\n")
243+
f.write(" return Graph(chat_input, chat_output)\n")
244+
script_path = Path(f.name)
245+
246+
try:
247+
graph = await load_graph_from_script(script_path)
248+
249+
# Verify it's a real Graph instance
250+
from lfx.graph import Graph
251+
252+
assert isinstance(graph, Graph)
253+
254+
# Verify it has the expected components
255+
component_names = {v.custom_component.__class__.__name__ for v in graph.vertices}
256+
assert "ChatInput" in component_names
257+
assert "ChatOutput" in component_names
258+
finally:
259+
script_path.unlink()
206260

207261

208262
class TestResultExtraction:
@@ -505,10 +559,10 @@ def test_find_graph_variable_file_not_found(self):
505559
class TestIntegrationWithRealFlows:
506560
"""Integration tests using real flows and minimal mocking."""
507561

508-
def test_load_and_validate_real_script(self, simple_chat_py):
562+
async def test_load_and_validate_real_script(self, simple_chat_py):
509563
"""Test loading and validating a real script file."""
510564
# Load the real graph from the script
511-
graph = load_graph_from_script(simple_chat_py)
565+
graph = await load_graph_from_script(simple_chat_py)
512566

513567
# Verify it's a real Graph
514568
from lfx.graph import Graph
@@ -523,7 +577,7 @@ def test_load_and_validate_real_script(self, simple_chat_py):
523577
async def test_execute_real_flow_with_results(self, simple_chat_py):
524578
"""Test executing a real flow and extracting results."""
525579
# Load the real graph
526-
graph = load_graph_from_script(simple_chat_py)
580+
graph = await load_graph_from_script(simple_chat_py)
527581

528582
# Execute the graph with real input
529583
from lfx.graph.schema import RunOutputs

src/lfx/tests/unit/cli/test_serve_components.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,8 @@ def verbose_print(msg):
200200
validate_script_path("/nonexistent/path.json", verbose_print)
201201

202202
@patch("lfx.cli.common.load_flow_from_json")
203-
def test_load_graph_from_path_success(self, mock_load_flow):
203+
@pytest.mark.asyncio
204+
async def test_load_graph_from_path_success(self, mock_load_flow):
204205
"""Test successful graph loading."""
205206
mock_graph = Mock()
206207
mock_load_flow.return_value = mock_graph
@@ -212,12 +213,13 @@ def test_load_graph_from_path_success(self, mock_load_flow):
212213
def verbose_print(msg):
213214
pass # Real function
214215

215-
graph = load_graph_from_path(Path(tmp.name), ".json", verbose_print, verbose=True)
216+
graph = await load_graph_from_path(Path(tmp.name), ".json", verbose_print, verbose=True)
216217
assert graph == mock_graph
217218
mock_load_flow.assert_called_once_with(Path(tmp.name), disable_logs=False)
218219

219220
@patch("lfx.cli.common.load_flow_from_json")
220-
def test_load_graph_from_path_error(self, mock_load_flow):
221+
@pytest.mark.asyncio
222+
async def test_load_graph_from_path_error(self, mock_load_flow):
221223
"""Test graph loading with error."""
222224
mock_load_flow.side_effect = Exception("Parse error")
223225

@@ -229,7 +231,7 @@ def verbose_print(msg):
229231
pass # Real function
230232

231233
with pytest.raises(typer.Exit):
232-
load_graph_from_path(Path(tmp.name), ".json", verbose_print, verbose=False)
234+
await load_graph_from_path(Path(tmp.name), ".json", verbose_print, verbose=False)
233235
mock_load_flow.assert_called_once_with(Path(tmp.name), disable_logs=True)
234236

235237

@@ -429,7 +431,8 @@ class TestIntegration:
429431
"""Integration tests combining multiple components."""
430432

431433
@patch("lfx.cli.common.load_flow_from_json")
432-
def test_full_app_integration(self, mock_load_flow):
434+
@pytest.mark.asyncio
435+
async def test_full_app_integration(self, mock_load_flow):
433436
"""Test full app integration with realistic data."""
434437
# Setup real graph
435438
real_graph = create_real_graph()
@@ -447,7 +450,7 @@ def verbose_print(msg):
447450
pass # Real function
448451

449452
mock_verbose_print = verbose_print
450-
loaded_graph = load_graph_from_path(flow_path, ".json", mock_verbose_print)
453+
loaded_graph = await load_graph_from_path(flow_path, ".json", mock_verbose_print)
451454
assert loaded_graph == real_graph
452455

453456
# Test flow ID generation

0 commit comments

Comments
 (0)