mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-21 13:58:15 +00:00
* wip restrcuturing agent executor and liteagent * fix: handle None task in AgentExecutor to prevent errors Added a check to ensure that if the task is None, the method returns early without attempting to access task properties. This change improves the robustness of the AgentExecutor by preventing potential errors when the task is not set. * refactor: streamline AgentExecutor initialization by removing redundant parameters Updated the Agent class to simplify the initialization of the AgentExecutor by removing unnecessary task and crew parameters in standalone mode. This change enhances code clarity and maintains backward compatibility by ensuring that the executor is correctly configured without redundant assignments. * ensure executors work inside a flow due to flow in flow async structure * refactor: enhance agent kickoff preparation by separating common logic Updated the Agent class to introduce a new private method that consolidates the common setup logic for both synchronous and asynchronous kickoff executions. This change improves code clarity and maintainability by reducing redundancy in the kickoff process, while ensuring that the agent can still execute effectively within both standalone and flow contexts. * linting and tests * fix test * refactor: improve test for Agent kickoff parameters Updated the test for the Agent class to ensure that the kickoff method correctly preserves parameters. The test now verifies the configuration of the agent after kickoff, enhancing clarity and maintainability. Additionally, the test for asynchronous kickoff within a flow context has been updated to reflect the Agent class instead of LiteAgent. * refactor: update test task guardrail process output for improved validation Refactored the test for task guardrail process output to enhance the validation of the output against the OpenAPI schema. The changes include a more structured request body and updated response handling to ensure compliance with the guardrail requirements. This update aims to improve the clarity and reliability of the test cases, ensuring that task outputs are correctly validated and feedback is appropriately provided. * test fix cassette * test fix cassette * working * working cassette * refactor: streamline agent execution and enhance flow compatibility Refactored the Agent class to simplify the execution method by removing the event loop check and clarifying the behavior when called from synchronous and asynchronous contexts. The changes ensure that the method operates seamlessly within flow methods, improving clarity in the documentation. Additionally, updated the AgentExecutor to set the response model to None, enhancing flexibility. New test cassettes were added to validate the functionality of agents within flow contexts, ensuring robust testing for both synchronous and asynchronous operations. * fixed cassette * Enhance Flow Execution Logic - Introduced conditional execution for start methods in the Flow class. - Unconditional start methods are prioritized during kickoff, while conditional starts are executed only if no unconditional starts are present. - Improved handling of cyclic flows by allowing re-execution of conditional start methods triggered by routers. - Added checks to continue execution chains for completed conditional starts. These changes improve the flexibility and control of flow execution, ensuring that the correct methods are triggered based on the defined conditions. * Enhance Agent and Flow Execution Logic - Updated the Agent class to automatically detect the event loop and return a coroutine when called within a Flow, simplifying async handling for users. - Modified Flow class to execute listeners sequentially, preventing race conditions on shared state during listener execution. - Improved handling of coroutine results from synchronous methods, ensuring proper execution flow and state management. These changes enhance the overall execution logic and user experience when working with agents and flows in CrewAI. * Enhance Flow Listener Logic and Agent Imports - Updated the Flow class to track fired OR listeners, ensuring that multi-source OR listeners only trigger once during execution. This prevents redundant executions and improves flow efficiency. - Cleared fired OR listeners during cyclic flow resets to allow re-execution in new cycles. - Modified the Agent class imports to include Coroutine from collections.abc, enhancing type handling for asynchronous operations. These changes improve the control and performance of flow execution in CrewAI, ensuring more predictable behavior in complex scenarios. * adjusted test due to new cassette * ensure we dont finalize batch on just a liteagent finishing * feat: cancellable parallelized flow methods * feat: allow methods to be cancelled & run parallelized * feat: ensure state is thread safe through proxy * fix: check for proxy state * fix: mimic BaseModel method * chore: update final attr checks; test * better description * fix test * chore: update test assumptions * extra --------- Co-authored-by: Greyson LaLonde <greyson.r.lalonde@gmail.com>
251 lines
7.8 KiB
Python
251 lines
7.8 KiB
Python
"""Test flow state persistence functionality."""
|
|
|
|
import os
|
|
from typing import Dict, List
|
|
|
|
from crewai.flow.flow import Flow, FlowState, listen, start
|
|
from crewai.flow.persistence import persist
|
|
from crewai.flow.persistence.sqlite import SQLiteFlowPersistence
|
|
from pydantic import BaseModel
|
|
|
|
|
|
class TestState(FlowState):
|
|
"""Test state model with required id field."""
|
|
|
|
counter: int = 0
|
|
message: str = ""
|
|
|
|
|
|
def test_persist_decorator_saves_state(tmp_path, caplog):
|
|
"""Test that @persist decorator saves state in SQLite."""
|
|
db_path = os.path.join(tmp_path, "test_flows.db")
|
|
persistence = SQLiteFlowPersistence(db_path)
|
|
|
|
class TestFlow(Flow[Dict[str, str]]):
|
|
initial_state = dict() # Use dict instance as initial state
|
|
|
|
@start()
|
|
@persist(persistence)
|
|
def init_step(self):
|
|
self.state["message"] = "Hello, World!"
|
|
self.state["id"] = "test-uuid" # Ensure we have an ID for persistence
|
|
|
|
# Run flow and verify state is saved
|
|
flow = TestFlow(persistence=persistence)
|
|
flow.kickoff()
|
|
|
|
# Load state from DB and verify
|
|
saved_state = persistence.load_state(flow.state["id"])
|
|
assert saved_state is not None
|
|
assert saved_state["message"] == "Hello, World!"
|
|
|
|
|
|
def test_structured_state_persistence(tmp_path):
|
|
"""Test persistence with Pydantic model state."""
|
|
db_path = os.path.join(tmp_path, "test_flows.db")
|
|
persistence = SQLiteFlowPersistence(db_path)
|
|
|
|
class StructuredFlow(Flow[TestState]):
|
|
initial_state = TestState
|
|
|
|
@start()
|
|
@persist(persistence)
|
|
def count_up(self):
|
|
self.state.counter += 1
|
|
self.state.message = f"Count is {self.state.counter}"
|
|
|
|
# Run flow and verify state changes are saved
|
|
flow = StructuredFlow(persistence=persistence)
|
|
flow.kickoff()
|
|
|
|
# Load and verify state
|
|
saved_state = persistence.load_state(flow.state.id)
|
|
assert saved_state is not None
|
|
assert saved_state["counter"] == 1
|
|
assert saved_state["message"] == "Count is 1"
|
|
|
|
|
|
def test_flow_state_restoration(tmp_path):
|
|
"""Test restoring flow state from persistence with various restoration methods."""
|
|
db_path = os.path.join(tmp_path, "test_flows.db")
|
|
persistence = SQLiteFlowPersistence(db_path)
|
|
|
|
# First flow execution to create initial state
|
|
class RestorableFlow(Flow[TestState]):
|
|
@start()
|
|
@persist(persistence)
|
|
def set_message(self):
|
|
if self.state.message == "":
|
|
self.state.message = "Original message"
|
|
if self.state.counter == 0:
|
|
self.state.counter = 42
|
|
|
|
# Create and persist initial state
|
|
flow1 = RestorableFlow(persistence=persistence)
|
|
flow1.kickoff()
|
|
original_uuid = flow1.state.id
|
|
|
|
# Test case 1: Restore using restore_uuid with field override
|
|
flow2 = RestorableFlow(persistence=persistence)
|
|
flow2.kickoff(inputs={"id": original_uuid, "counter": 43})
|
|
|
|
# Verify state restoration and selective field override
|
|
assert flow2.state.id == original_uuid
|
|
assert flow2.state.message == "Original message" # Preserved
|
|
assert flow2.state.counter == 43 # Overridden
|
|
|
|
# Test case 2: Restore using kwargs['id']
|
|
flow3 = RestorableFlow(persistence=persistence)
|
|
flow3.kickoff(inputs={"id": original_uuid, "message": "Updated message"})
|
|
|
|
# Verify state restoration and selective field override
|
|
assert flow3.state.id == original_uuid
|
|
assert flow3.state.counter == 43 # Preserved
|
|
assert flow3.state.message == "Updated message" # Overridden
|
|
|
|
|
|
def test_multiple_method_persistence(tmp_path):
|
|
"""Test state persistence across multiple method executions."""
|
|
db_path = os.path.join(tmp_path, "test_flows.db")
|
|
persistence = SQLiteFlowPersistence(db_path)
|
|
|
|
class MultiStepFlow(Flow[TestState]):
|
|
@start()
|
|
@persist(persistence)
|
|
def step_1(self):
|
|
if self.state.counter == 1:
|
|
self.state.counter = 99999
|
|
self.state.message = "Step 99999"
|
|
else:
|
|
self.state.counter = 1
|
|
self.state.message = "Step 1"
|
|
|
|
@listen(step_1)
|
|
@persist(persistence)
|
|
def step_2(self):
|
|
if self.state.counter == 1:
|
|
self.state.counter = 2
|
|
self.state.message = "Step 2"
|
|
|
|
flow = MultiStepFlow(persistence=persistence)
|
|
flow.kickoff()
|
|
|
|
flow2 = MultiStepFlow(persistence=persistence)
|
|
flow2.kickoff(inputs={"id": flow.state.id})
|
|
|
|
# Load final state
|
|
final_state = flow2.state
|
|
assert final_state is not None
|
|
assert final_state.counter == 2
|
|
assert final_state.message == "Step 2"
|
|
|
|
class NoPersistenceMultiStepFlow(Flow[TestState]):
|
|
@start()
|
|
@persist(persistence)
|
|
def step_1(self):
|
|
if self.state.counter == 1:
|
|
self.state.counter = 99999
|
|
self.state.message = "Step 99999"
|
|
else:
|
|
self.state.counter = 1
|
|
self.state.message = "Step 1"
|
|
|
|
@listen(step_1)
|
|
def step_2(self):
|
|
if self.state.counter == 1:
|
|
self.state.counter = 2
|
|
self.state.message = "Step 2"
|
|
|
|
flow = NoPersistenceMultiStepFlow(persistence=persistence)
|
|
flow.kickoff()
|
|
|
|
flow2 = NoPersistenceMultiStepFlow(persistence=persistence)
|
|
flow2.kickoff(inputs={"id": flow.state.id})
|
|
|
|
# Load final state
|
|
final_state = flow2.state
|
|
assert final_state.counter == 99999
|
|
assert final_state.message == "Step 99999"
|
|
|
|
|
|
def test_persist_decorator_verbose_logging(tmp_path, caplog):
|
|
"""Test that @persist decorator's verbose parameter controls logging."""
|
|
# Set logging level to ensure we capture all logs
|
|
caplog.set_level("INFO")
|
|
|
|
db_path = os.path.join(tmp_path, "test_flows.db")
|
|
persistence = SQLiteFlowPersistence(db_path)
|
|
|
|
# Test with verbose=False (default)
|
|
class QuietFlow(Flow[Dict[str, str]]):
|
|
initial_state = dict()
|
|
|
|
@start()
|
|
@persist(persistence) # Default verbose=False
|
|
def init_step(self):
|
|
self.state["message"] = "Hello, World!"
|
|
self.state["id"] = "test-uuid-1"
|
|
|
|
flow = QuietFlow(persistence=persistence)
|
|
flow.kickoff()
|
|
assert "Saving flow state" not in caplog.text
|
|
|
|
# Clear the log
|
|
caplog.clear()
|
|
|
|
# Test with verbose=True
|
|
class VerboseFlow(Flow[Dict[str, str]]):
|
|
initial_state = dict()
|
|
|
|
@start()
|
|
@persist(persistence, verbose=True)
|
|
def init_step(self):
|
|
self.state["message"] = "Hello, World!"
|
|
self.state["id"] = "test-uuid-2"
|
|
|
|
flow = VerboseFlow(persistence=persistence)
|
|
flow.kickoff()
|
|
assert "Saving flow state" in caplog.text
|
|
|
|
|
|
def test_persistence_with_base_model(tmp_path):
|
|
db_path = os.path.join(tmp_path, "test_flows.db")
|
|
persistence = SQLiteFlowPersistence(db_path)
|
|
|
|
class Message(BaseModel):
|
|
role: str
|
|
type: str
|
|
content: str
|
|
|
|
class State(FlowState):
|
|
latest_message: Message | None = None
|
|
history: List[Message] = []
|
|
|
|
@persist(persistence)
|
|
class BaseModelFlow(Flow[State]):
|
|
initial_state = State(latest_message=None, history=[])
|
|
|
|
@start()
|
|
def init_step(self):
|
|
self.state.latest_message = Message(
|
|
role="user", type="text", content="Hello, World!"
|
|
)
|
|
self.state.history.append(self.state.latest_message)
|
|
|
|
flow = BaseModelFlow(persistence=persistence)
|
|
flow.kickoff()
|
|
|
|
latest_message = flow.state.latest_message
|
|
(message,) = flow.state.history
|
|
|
|
assert latest_message is not None
|
|
assert latest_message.role == "user"
|
|
assert latest_message.type == "text"
|
|
assert latest_message.content == "Hello, World!"
|
|
|
|
assert len(flow.state.history) == 1
|
|
assert message.role == "user"
|
|
assert message.type == "text"
|
|
assert message.content == "Hello, World!"
|
|
assert isinstance(flow.state._unwrap(), State)
|