chore: initial commit for Pretor v0.1.0-alpha

正式发布 Pretor 平台的首个 alpha 版本。本项目旨在构建一个基于分布式架构的多智能体协同工作流水线。

核心功能实现:
1. 建立基于 BaseIndividual 的动态插件加载机制。
2. 实现三类核心 worker_individual 子个体。
3. 集成 Ray 框架支持分布式集群调度。
4. 基于 PostgreSQL 的全量持久化存储方案。
5. 提供完整的 FastAPI 后端与 React 前端交互界面。
This commit is contained in:
2026-04-29 10:09:07 +08:00
commit d84212f780
163 changed files with 19251 additions and 0 deletions
+168
View File
@@ -0,0 +1,168 @@
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.get_skill_list.remote = AsyncMock(return_value={"test_skill": ("description", "instructions")})
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:
# Instead of patching hook, we inject it directly
engine.global_state_machine = AsyncMock()
mock_open.return_value.__enter__.return_value.read.return_value = '{}'
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")
@@ -0,0 +1,43 @@
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
@@ -0,0 +1,36 @@
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])
@@ -0,0 +1,57 @@
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")
+47
View File
@@ -0,0 +1,47 @@
import pytest
from pretor.core.workflow.workflow import WorkStep, PretorWorkflow, WorkflowStatus, LogicGate
def test_work_step():
ws = WorkStep(
step=1,
name="step1",
node="control_node",
action="coding",
desc="Write some code"
)
assert ws.step == 1
assert ws.name == "step1"
assert ws.node == "control_node"
assert ws.action == "coding"
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"})
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"})
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)
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"})
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)
with pytest.raises(ValueError, match="LogicGate 格式错误"):
PretorWorkflow(title="wf1", work_link=[ws1], trace_id="t", event_info={"platform":"a", "user_name":"b"})
def test_workflow_status():
status = WorkflowStatus()
assert status.step == 1
assert status.status == "waiting_llm_working"