refactor(core): decouple actors and remove workflow templates (#67)
Removes the deprecated `workflow_template` concept entirely across both backend API routers, internal logic handling within the `supervisory_node` and `consciousness_node`, and front-end components. Enables `consciousness_node` to work autonomously. Also refactors core package structure to enforce the "one python package, one Ray Actor" architectural rule. `GlobalWorkflowManager`, `WorkflowRunningEngine`, `PostgresDatabase`, and `WorkerCluster` have been moved to their own top-level decoupled package directories with properly exported `__init__.py` modules. Test suites have been relocated and import paths updated across the system. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com>
This commit is contained in:
@@ -1,173 +0,0 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, AsyncMock, patch
|
||||
import asyncio
|
||||
|
||||
import sys
|
||||
import builtins
|
||||
|
||||
real_import = builtins.__import__
|
||||
|
||||
|
||||
def mock_import(name, globals=None, locals=None, fromlist=(), level=0):
|
||||
if name == 'ray':
|
||||
mock_ray = MagicMock()
|
||||
|
||||
def mock_remote(*args, **kwargs):
|
||||
if len(args) == 1 and callable(args[0]):
|
||||
return args[0]
|
||||
def decorator(cls):
|
||||
return cls
|
||||
return decorator
|
||||
|
||||
mock_ray.remote = mock_remote
|
||||
return mock_ray
|
||||
return real_import(name, globals, locals, fromlist, level)
|
||||
|
||||
|
||||
builtins.__import__ = mock_import
|
||||
for mod in list(sys.modules.keys()):
|
||||
if 'pretor.core.workflow.workflow_runner' in mod or 'ray' in mod:
|
||||
del sys.modules[mod]
|
||||
from pretor.core.workflow.workflow_runner import WorkflowEngine, WorkflowRunningEngine
|
||||
|
||||
builtins.__import__ = real_import
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_ray():
|
||||
with patch("pretor.core.workflow.workflow_runner.ray") as mock_ray:
|
||||
mock_ray.get = lambda x: x
|
||||
yield mock_ray
|
||||
|
||||
|
||||
def test_workflow_engine_init():
|
||||
mock_wf = MagicMock()
|
||||
mock_wf.work_link = []
|
||||
engine = WorkflowEngine(mock_wf, "conscious", "control", "supervisor")
|
||||
assert engine.workflow == mock_wf
|
||||
assert engine.consciousness_node == "conscious"
|
||||
assert engine.control_node == "control"
|
||||
assert engine.supervisory_node == "supervisor"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_workflow_engine_run():
|
||||
from pretor.core.workflow.workflow import PretorWorkflow, WorkStep, WorkflowStatus
|
||||
|
||||
mock_wf = MagicMock(spec=PretorWorkflow)
|
||||
|
||||
step1 = MagicMock(spec=WorkStep)
|
||||
step1.step = 1
|
||||
step1.status = "waiting"
|
||||
step1.node = "control_node"
|
||||
step1.name = "mock_name"
|
||||
step1.desc = "mock_desc"
|
||||
step1.action = "mock_action"
|
||||
step1.inputs = []
|
||||
step1.outputs = "res"
|
||||
step1.logic_gate = None
|
||||
mock_wf.work_link = [step1]
|
||||
|
||||
mock_status = MagicMock(spec=WorkflowStatus)
|
||||
mock_status.step = 1
|
||||
mock_status.status = "running"
|
||||
mock_wf.status = mock_status
|
||||
mock_wf.context_memory = {}
|
||||
mock_wf.title = "mock_title"
|
||||
mock_wf.trace_id = "mock_trace_id"
|
||||
mock_wf.command = "mock_command"
|
||||
mock_wf.event_info = MagicMock()
|
||||
mock_wf.event_info.platform = "test"
|
||||
mock_wf.event_info.user_name = "test_user"
|
||||
|
||||
mock_control = MagicMock()
|
||||
mock_control.working.remote = AsyncMock(return_value="process_result")
|
||||
|
||||
mock_conscious = MagicMock()
|
||||
mock_conscious.working.remote = AsyncMock(return_value="report")
|
||||
|
||||
mock_supervisor = MagicMock()
|
||||
mock_supervisor.working.remote = AsyncMock(return_value="response")
|
||||
|
||||
engine = WorkflowEngine(mock_wf, mock_conscious, mock_control, mock_supervisor)
|
||||
|
||||
with patch("pretor.core.workflow.workflow_runner.ray") as mock_ray_patch:
|
||||
mock_gsm = MagicMock()
|
||||
mock_ray_patch.get_actor.return_value = mock_gsm
|
||||
await engine.run()
|
||||
|
||||
assert step1.status == "completed"
|
||||
assert mock_wf.context_memory["res"] == "process_result"
|
||||
|
||||
|
||||
def test_workflow_running_engine_init():
|
||||
engine = WorkflowRunningEngine("conscious", "control", "supervisor")
|
||||
assert engine.consciousness_node == "conscious"
|
||||
assert engine.control_node == "control"
|
||||
assert engine.supervisory_node == "supervisor"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_workflow_running_engine_submit():
|
||||
engine = WorkflowRunningEngine("conscious", "control", "supervisor")
|
||||
engine.workflow_queue = asyncio.Queue()
|
||||
|
||||
mock_wf = MagicMock()
|
||||
await engine.workflow_queue.put(mock_wf)
|
||||
|
||||
item = await engine.workflow_queue.get()
|
||||
assert item == mock_wf
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_workflow_running_engine_runner():
|
||||
from pretor.api.platform.event import PretorEvent
|
||||
from pretor.core.individual.consciousness_node.template import ForWorkflowEngine
|
||||
|
||||
mock_consciousness = MagicMock()
|
||||
mock_wf = MagicMock()
|
||||
mock_wf.trace_id = "test_trace"
|
||||
mock_wf.title = "test_title"
|
||||
|
||||
mock_result = MagicMock(spec=ForWorkflowEngine)
|
||||
mock_result.workflow = mock_wf
|
||||
mock_consciousness.working.remote = AsyncMock(return_value=mock_result)
|
||||
|
||||
engine = WorkflowRunningEngine(mock_consciousness, "control", "supervisor")
|
||||
engine.workflow_queue = asyncio.Queue()
|
||||
|
||||
mock_event = PretorEvent(
|
||||
platform="test_platform",
|
||||
user_id="test_user",
|
||||
user_name="test_user",
|
||||
message="test_message",
|
||||
context={"workflow_template": "test_template"}
|
||||
)
|
||||
await engine.workflow_queue.put(mock_event)
|
||||
|
||||
# Mock the global_state_machine get_skill_list.remote method properly
|
||||
mock_gsm = MagicMock()
|
||||
mock_gsm.list_individuals.remote = AsyncMock(return_value={"test_skill": {"agent_type": "skill_individual", "agent_name": "TestSkill", "description": "desc"}})
|
||||
engine.global_state_machine = mock_gsm
|
||||
|
||||
with patch("pretor.core.workflow.workflow_runner.WorkflowEngine") as mock_wf_engine_cls, patch("builtins.open", new_callable=MagicMock) as mock_open, \
|
||||
patch("pretor.core.workflow.workflow_runner.ray_actor_hook") as mock_hook:
|
||||
|
||||
# Instead of patching hook, we inject it directly
|
||||
# engine.global_state_machine = AsyncMock()
|
||||
|
||||
mock_open.return_value.__enter__.return_value.read.return_value = '{}'
|
||||
|
||||
mock_gwm = MagicMock()
|
||||
mock_gwm.update_workflow.remote = AsyncMock()
|
||||
mock_hook.return_value.global_workflow_manager = mock_gwm
|
||||
|
||||
mock_engine_instance = MagicMock()
|
||||
mock_engine_instance.run = AsyncMock()
|
||||
mock_wf_engine_cls.return_value = mock_engine_instance
|
||||
|
||||
task = asyncio.create_task(engine.runner(1))
|
||||
await asyncio.sleep(0.05)
|
||||
task.cancel()
|
||||
|
||||
mock_wf_engine_cls.assert_called_with(mock_wf, mock_consciousness, "control", "supervisor")
|
||||
@@ -1,43 +0,0 @@
|
||||
from unittest.mock import patch, MagicMock
|
||||
from pretor.core.workflow.workflow_template_generator.workflow_template_generator import WorkflowTemplateGenerator
|
||||
|
||||
|
||||
@patch("pretor.core.workflow.workflow_template_generator.workflow_template_generator.Path")
|
||||
def test_generate_workflow_template(mock_path):
|
||||
mock_dir = MagicMock()
|
||||
mock_dir.exists.return_value = False
|
||||
|
||||
mock_file = MagicMock()
|
||||
mock_dir.__truediv__.return_value = mock_file
|
||||
|
||||
mock_open_ctx = MagicMock()
|
||||
mock_file.open.return_value.__enter__.return_value = mock_open_ctx
|
||||
|
||||
mock_path_root = MagicMock()
|
||||
mock_path_root.__truediv__.return_value = mock_dir
|
||||
mock_path.return_value = mock_path_root
|
||||
|
||||
from pretor.core.workflow.workflow_template_generator.workflow_template import WorkflowTemplate
|
||||
generator = WorkflowTemplateGenerator()
|
||||
mock_template = MagicMock(spec=WorkflowTemplate)
|
||||
mock_template.name = "test_wf"
|
||||
mock_template.desc = "test_desc"
|
||||
import json
|
||||
mock_template.model_dump_json.return_value = json.dumps({
|
||||
"name": "test_wf",
|
||||
"desc": "test_desc",
|
||||
"work_link": [{"step": 1, "node": "n", "action": "a", "desc": "d", "input": [], "output": [], "logic_gate": {}}]
|
||||
})
|
||||
generator.generate_workflow_template(
|
||||
workflow_template=mock_template
|
||||
)
|
||||
|
||||
mock_dir.mkdir.assert_called_once_with(parents=True)
|
||||
mock_file.open.assert_called_once_with("w", encoding="utf-8")
|
||||
|
||||
mock_open_ctx.write.assert_called_once()
|
||||
write_arg = mock_open_ctx.write.call_args[0][0]
|
||||
written_data = json.loads(write_arg)
|
||||
assert written_data["name"] == "test_wf"
|
||||
assert written_data["desc"] == "test_desc"
|
||||
assert written_data["work_link"][0]["step"] == 1
|
||||
@@ -1,36 +0,0 @@
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
from pretor.core.workflow.workflow_template_generator.workflow_template import WorkflowTemplateStep, WorkflowTemplate
|
||||
|
||||
def test_workflow_template_step():
|
||||
step = WorkflowTemplateStep(
|
||||
step=1,
|
||||
node="node_type",
|
||||
action="act",
|
||||
desc="desc",
|
||||
input=["in1"],
|
||||
output=["out1"],
|
||||
logic_gate={"if_pass": "next"}
|
||||
)
|
||||
assert step.step == 1
|
||||
assert step.node == "node_type"
|
||||
|
||||
def test_workflow_template_success():
|
||||
step1 = WorkflowTemplateStep(
|
||||
step=1, node="node1", action="a1", desc="d1", input=[], output=[], logic_gate={}
|
||||
)
|
||||
step2 = WorkflowTemplateStep(
|
||||
step=2, node="node2", action="a2", desc="d2", input=[], output=[], logic_gate={}
|
||||
)
|
||||
wt = WorkflowTemplate(name="temp", desc="desc", work_link=[step1, step2])
|
||||
assert wt.name == "temp"
|
||||
|
||||
def test_workflow_template_error_duplicate_steps():
|
||||
step1 = WorkflowTemplateStep(
|
||||
step=1, node="node1", action="a1", desc="d1", input=[], output=[], logic_gate={}
|
||||
)
|
||||
step2 = WorkflowTemplateStep(
|
||||
step=1, node="node2", action="a2", desc="d2", input=[], output=[], logic_gate={}
|
||||
)
|
||||
with pytest.raises(ValidationError, match="Step numbers in work_link must be unique"):
|
||||
WorkflowTemplate(name="temp", desc="desc", work_link=[step1, step2])
|
||||
@@ -1,57 +0,0 @@
|
||||
import json
|
||||
from unittest.mock import MagicMock, patch, mock_open
|
||||
from pathlib import Path
|
||||
from pretor.core.workflow.workflow_template_manager import WorkflowManager
|
||||
|
||||
|
||||
def test_workflow_manager_init_success():
|
||||
mock_file1 = MagicMock(spec=Path)
|
||||
mock_file1.open = mock_open(read_data=json.dumps({"name": "test1", "desc": "desc1"}))
|
||||
|
||||
mock_file2 = MagicMock(spec=Path)
|
||||
mock_file2.open = mock_open(read_data=json.dumps({"name": "test2", "desc": "desc2"}))
|
||||
|
||||
with patch("pretor.core.workflow.workflow_template_manager.Path.glob", return_value=[mock_file1, mock_file2]):
|
||||
with patch("pretor.core.workflow.workflow_template_manager.WorkflowTemplateGenerator"):
|
||||
manager = WorkflowManager()
|
||||
assert manager.workflow_templates_registry == {"test1": "desc1", "test2": "desc2"}
|
||||
|
||||
|
||||
def test_workflow_manager_init_json_error():
|
||||
mock_file1 = MagicMock(spec=Path)
|
||||
mock_file1.open = mock_open(read_data="{invalid_json}")
|
||||
|
||||
with patch("pretor.core.workflow.workflow_template_manager.Path.glob", return_value=[mock_file1]):
|
||||
with patch("pretor.core.workflow.workflow_template_manager.logger") as mock_logger:
|
||||
with patch("pretor.core.workflow.workflow_template_manager.WorkflowTemplateGenerator"):
|
||||
manager = WorkflowManager()
|
||||
assert manager.workflow_templates_registry == {}
|
||||
mock_logger.warning.assert_called_once()
|
||||
assert "不是json文件或格式错误" in mock_logger.warning.call_args[0][0]
|
||||
|
||||
|
||||
from pretor.core.workflow.workflow_template_generator.workflow_template import WorkflowTemplate
|
||||
|
||||
@patch("pretor.core.workflow.workflow_template_manager.WorkflowTemplateGenerator")
|
||||
@patch("pretor.core.workflow.workflow_template_manager.Path.glob", return_value=[])
|
||||
def test_generate_workflow_template_success(mock_glob, mock_generator_cls):
|
||||
manager = WorkflowManager()
|
||||
mock_template = MagicMock(spec=WorkflowTemplate)
|
||||
mock_template.name = "name"
|
||||
mock_template.desc = "desc"
|
||||
mock_generator_cls.return_value.generate_workflow_template.return_value = mock_template
|
||||
|
||||
manager.generate_workflow_template(mock_template)
|
||||
mock_generator_cls.return_value.generate_workflow_template.assert_called_once_with(workflow_template=mock_template)
|
||||
assert manager.workflow_templates_registry["name"] == "desc"
|
||||
|
||||
|
||||
@patch("pretor.core.workflow.workflow_template_manager.WorkflowTemplateGenerator")
|
||||
@patch("pretor.core.workflow.workflow_template_manager.Path.glob", return_value=[])
|
||||
@patch("pretor.core.workflow.workflow_template_manager.logger")
|
||||
def test_generate_workflow_template_exception(mock_logger, mock_glob, mock_generator_cls):
|
||||
mock_generator_cls.return_value.generate_workflow_template.side_effect = Exception("error")
|
||||
manager = WorkflowManager()
|
||||
mock_template = MagicMock(spec=WorkflowTemplate)
|
||||
manager.generate_workflow_template(mock_template)
|
||||
mock_logger.exception.assert_called_once_with("Failed to generate workflow template")
|
||||
@@ -1,5 +1,11 @@
|
||||
import pytest
|
||||
from pretor.core.workflow.workflow import WorkStep, PretorWorkflow, WorkflowStatus, LogicGate
|
||||
from pretor.core.workflow.workflow import (
|
||||
WorkStep,
|
||||
PretorWorkflow,
|
||||
WorkflowStatus,
|
||||
LogicGate,
|
||||
)
|
||||
|
||||
|
||||
def test_work_step():
|
||||
ws = WorkStep(
|
||||
@@ -7,7 +13,7 @@ def test_work_step():
|
||||
name="step1",
|
||||
node="control_node",
|
||||
action="coding",
|
||||
desc="Write some code"
|
||||
desc="Write some code",
|
||||
)
|
||||
assert ws.step == 1
|
||||
assert ws.name == "step1"
|
||||
@@ -16,30 +22,59 @@ def test_work_step():
|
||||
assert ws.desc == "Write some code"
|
||||
assert ws.status == "waiting"
|
||||
|
||||
|
||||
def test_pretor_workflow_validation_success():
|
||||
ws1 = WorkStep(step=1, name="s1", node="control_node", action="a1", desc="d1")
|
||||
ws2 = WorkStep(step=2, name="s2", node="supervisory_node", action="a2", desc="d2")
|
||||
wf = PretorWorkflow(title="wf1", work_link=[ws1, ws2], trace_id="t", event_info={"platform":"a", "user_name":"b"})
|
||||
wf = PretorWorkflow(
|
||||
title="wf1",
|
||||
work_link=[ws1, ws2],
|
||||
trace_id="t",
|
||||
event_info={"platform": "a", "user_name": "b"},
|
||||
)
|
||||
assert wf.title == "wf1"
|
||||
|
||||
|
||||
def test_pretor_workflow_validation_error_step_discontinuous():
|
||||
ws1 = WorkStep(step=1, name="s1", node="control_node", action="a1", desc="d1")
|
||||
ws2 = WorkStep(step=3, name="s3", node="supervisory_node", action="a2", desc="d2")
|
||||
with pytest.raises(ValueError, match="工作链步数不连续"):
|
||||
PretorWorkflow(title="wf1", work_link=[ws1, ws2], trace_id="t", event_info={"platform":"a", "user_name":"b"})
|
||||
PretorWorkflow(
|
||||
title="wf1",
|
||||
work_link=[ws1, ws2],
|
||||
trace_id="t",
|
||||
event_info={"platform": "a", "user_name": "b"},
|
||||
)
|
||||
|
||||
|
||||
def test_pretor_workflow_validation_error_jump_out_of_bounds():
|
||||
lg = LogicGate(if_fail="jump_to_step_3", if_pass="continue")
|
||||
ws1 = WorkStep(step=1, name="s1", node="control_node", action="a1", desc="d1", logic_gate=lg)
|
||||
ws1 = WorkStep(
|
||||
step=1, name="s1", node="control_node", action="a1", desc="d1", logic_gate=lg
|
||||
)
|
||||
ws2 = WorkStep(step=2, name="s2", node="supervisory_node", action="a2", desc="d2")
|
||||
with pytest.raises(ValueError, match="跳转目标 Step 3 越界了"):
|
||||
PretorWorkflow(title="wf1", work_link=[ws1, ws2], trace_id="t", event_info={"platform":"a", "user_name":"b"})
|
||||
PretorWorkflow(
|
||||
title="wf1",
|
||||
work_link=[ws1, ws2],
|
||||
trace_id="t",
|
||||
event_info={"platform": "a", "user_name": "b"},
|
||||
)
|
||||
|
||||
|
||||
def test_pretor_workflow_validation_error_jump_format_error():
|
||||
lg = LogicGate(if_fail="jump_to_step_invalid", if_pass="continue")
|
||||
ws1 = WorkStep(step=1, name="s1", node="control_node", action="a1", desc="d1", logic_gate=lg)
|
||||
ws1 = WorkStep(
|
||||
step=1, name="s1", node="control_node", action="a1", desc="d1", logic_gate=lg
|
||||
)
|
||||
with pytest.raises(ValueError, match="LogicGate 格式错误"):
|
||||
PretorWorkflow(title="wf1", work_link=[ws1], trace_id="t", event_info={"platform":"a", "user_name":"b"})
|
||||
PretorWorkflow(
|
||||
title="wf1",
|
||||
work_link=[ws1],
|
||||
trace_id="t",
|
||||
event_info={"platform": "a", "user_name": "b"},
|
||||
)
|
||||
|
||||
|
||||
def test_workflow_status():
|
||||
status = WorkflowStatus()
|
||||
|
||||
Reference in New Issue
Block a user