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:
@@ -0,0 +1,11 @@
|
||||
from pretor.worker_individual.base_individual import BaseIndividual
|
||||
from pretor.worker_individual.skill_individual import SkillIndividual
|
||||
from pretor.worker_individual.ordinary_individual import OrdinaryIndividual
|
||||
from pretor.worker_individual.special_individual import SpecialIndividual
|
||||
|
||||
__all__ = [
|
||||
"BaseIndividual",
|
||||
"SkillIndividual",
|
||||
"OrdinaryIndividual",
|
||||
"SpecialIndividual",
|
||||
]
|
||||
@@ -0,0 +1,76 @@
|
||||
# Copyright 2026 zhaoxi826
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from pydantic_ai import Agent, RunContext
|
||||
from pydantic import Field
|
||||
from pretor.adapter.model_adapter.agent_factory import AgentFactory
|
||||
from pretor.core.global_state_machine.model_provider.base_provider import Provider
|
||||
from pretor.utils.agent_model import ResponseModel, InputModel, DepsModel
|
||||
from pretor.utils.ray_hook import ray_actor_hook
|
||||
|
||||
from pretor.utils.logger import get_logger
|
||||
logger = get_logger('worker_individual')
|
||||
|
||||
class WorkerIndividualResponse(ResponseModel):
|
||||
output: str = Field(..., description="Worker执行任务的输出结果")
|
||||
|
||||
class WorkerIndividualDeps(DepsModel):
|
||||
task_event: dict
|
||||
|
||||
class WorkerIndividualInput(InputModel):
|
||||
task_event: dict
|
||||
|
||||
class BaseIndividual:
|
||||
"""
|
||||
Worker Individual 的基类
|
||||
"""
|
||||
|
||||
def __init__(self, agent_config: dict):
|
||||
self.agent_config = agent_config
|
||||
self.agent_id = agent_config.get("agent_id")
|
||||
self.agent: Agent | None = None
|
||||
|
||||
async def _init_agent(self, agent_name: str, system_prompt: str):
|
||||
from pretor.utils.get_tool import load_tools_from_list
|
||||
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||
provider_title = self.agent_config.get("provider_title", "openai") # default fallback
|
||||
model_id = self.agent_config.get("model_id", "gpt-4o") # default fallback
|
||||
tools_list = self.agent_config.get("tools", None)
|
||||
|
||||
provider: Provider = await global_state_machine.get_provider.remote( provider_title)
|
||||
agent_factory = AgentFactory()
|
||||
|
||||
callables = load_tools_from_list(tools_list)
|
||||
|
||||
self.agent = agent_factory.create_agent(
|
||||
provider=provider,
|
||||
model_id=model_id,
|
||||
output_type=WorkerIndividualResponse,
|
||||
system_prompt=system_prompt,
|
||||
deps_type=WorkerIndividualDeps,
|
||||
agent_name=agent_name,
|
||||
tools=callables
|
||||
)
|
||||
|
||||
@self.agent.system_prompt
|
||||
async def dynamic_prompt(ctx: RunContext[WorkerIndividualDeps]):
|
||||
prompt = system_prompt + "\n\n"
|
||||
prompt += (
|
||||
f"=== 当前任务上下文 ===\n"
|
||||
f"{ctx.deps.task_event}\n"
|
||||
)
|
||||
return prompt
|
||||
|
||||
async def run(self, task_event: dict) -> dict:
|
||||
raise NotImplementedError("子类必须实现 run 方法")
|
||||
@@ -0,0 +1,14 @@
|
||||
worker_individual
|
||||
---
|
||||
**worker_individual**是pretor中的基础工作对象,主要分为三类:**skill_individual**,**ordinary_individual**和**special_individual**,庞大的**worker_individual**将负责具体的生产工作。
|
||||
|
||||
---
|
||||
## worker_individual分类
|
||||
### skill_individual(专家子个体)
|
||||
**skill_individual(专家子个体)** 是拥有专业**skill**的agent,通常使用MoE(混合专家模型)或者大参数的专家模型来作为agent的模型。通过装配专业化的知识从而实现完成复杂任务。
|
||||
|
||||
### ordinary_individual(普通子个体)
|
||||
**ordinary_individual(普通子个体)** 是普通的agent,通常使用小参数微调专家模型来作为agent的模型。通过专业化数据的微调,在一定程度上实现比大参数MoE模型在单一方面上的能力。
|
||||
|
||||
### special_individual(特殊子个体)
|
||||
**special_individual(特殊子个体)** 是特殊的agent,这类agent一般不承担普通的生成任务,更多是实现一些特殊的任务,比如生成语音生成视频等。
|
||||
@@ -0,0 +1,43 @@
|
||||
# Copyright 2026 zhaoxi826
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from pretor.worker_individual.base_individual import BaseIndividual, WorkerIndividualDeps
|
||||
from pretor.utils.logger import get_logger
|
||||
|
||||
logger = get_logger('ordinary_individual')
|
||||
|
||||
class OrdinaryIndividual(BaseIndividual):
|
||||
"""
|
||||
普通子个体:普通的 agent。
|
||||
"""
|
||||
|
||||
def __init__(self, agent_config: dict):
|
||||
super().__init__(agent_config)
|
||||
|
||||
async def run(self, task_event: dict) -> dict:
|
||||
if self.agent is None:
|
||||
system_prompt = self.agent_config.get("prompt", "你是一个普通的AI助手,请尽力完成给定的任务。")
|
||||
await self._init_agent("ordinary_individual", system_prompt)
|
||||
|
||||
deps = WorkerIndividualDeps(task_event=task_event)
|
||||
self.agent.retries = 3
|
||||
try:
|
||||
result = await self.agent.run(
|
||||
f"请执行以下任务:\n{task_event}",
|
||||
deps=deps
|
||||
)
|
||||
return {"output": result.data.output}
|
||||
except Exception as e:
|
||||
logger.exception(f"OrdinaryIndividual {self.agent_id} 执行失败: {e}")
|
||||
raise
|
||||
@@ -0,0 +1,110 @@
|
||||
# Copyright 2026 zhaoxi826
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from pretor.worker_individual.base_individual import BaseIndividual, WorkerIndividualDeps
|
||||
from pretor.utils.logger import get_logger
|
||||
import os
|
||||
import json
|
||||
from pydantic_ai import Tool
|
||||
import importlib.util
|
||||
|
||||
logger = get_logger('skill_individual')
|
||||
|
||||
class SkillIndividual(BaseIndividual):
|
||||
"""
|
||||
专家子个体:拥有专业 skill 的 agent。
|
||||
"""
|
||||
|
||||
def __init__(self, agent_config: dict):
|
||||
super().__init__(agent_config)
|
||||
|
||||
async def _load_skill_tools(self):
|
||||
"""动态加载已绑定的 skill 工具。"""
|
||||
tools = []
|
||||
bound_skill = self.agent_config.get("bound_skill", "")
|
||||
# bound_skill can be string or dict {"skill_name": ["file1", "file2"]}
|
||||
skill_mapper = {}
|
||||
if isinstance(bound_skill, str) and bound_skill:
|
||||
try:
|
||||
skill_mapper = json.loads(bound_skill)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
elif isinstance(bound_skill, dict):
|
||||
skill_mapper = bound_skill
|
||||
|
||||
skill_base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "plugin", "skill"))
|
||||
|
||||
for skill_name, _ in skill_mapper.items():
|
||||
skill_path = os.path.join(skill_base_dir, skill_name)
|
||||
metadata_path = os.path.join(skill_path, "metadata.json")
|
||||
if not os.path.exists(metadata_path):
|
||||
continue
|
||||
|
||||
try:
|
||||
with open(metadata_path, 'r', encoding='utf-8') as f:
|
||||
metadata = json.load(f)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load metadata for skill {skill_name}: {e}")
|
||||
continue
|
||||
|
||||
if "functions" in metadata:
|
||||
for func_info in metadata["functions"]:
|
||||
# Ensure path is absolute
|
||||
script_path = func_info.get("file_path", "")
|
||||
if not os.path.isabs(script_path):
|
||||
script_path = os.path.join(skill_path, script_path)
|
||||
|
||||
if not os.path.exists(script_path):
|
||||
logger.warning(f"Skill script not found: {script_path}")
|
||||
continue
|
||||
|
||||
func_name = func_info.get("name")
|
||||
try:
|
||||
# Dynamically load the python module
|
||||
spec = importlib.util.spec_from_file_location(func_name, script_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
func = getattr(module, func_name)
|
||||
if callable(func):
|
||||
# Convert to PydanticAI Tool
|
||||
tool = Tool(func, name=func_name, description=func_info.get("docstring", ""))
|
||||
tools.append(tool)
|
||||
logger.info(f"Loaded skill tool: {func_name} from {skill_name}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load function {func_name} from {script_path}: {e}")
|
||||
|
||||
return tools
|
||||
|
||||
async def run(self, task_event: dict) -> dict:
|
||||
if self.agent is None:
|
||||
system_prompt = self.agent_config.get("prompt",
|
||||
"你是一个拥有专业技能的专家级AI助手,请利用你的专业知识完成给定的任务。")
|
||||
await self._init_agent("skill_individual", system_prompt)
|
||||
|
||||
deps = WorkerIndividualDeps(task_event=task_event)
|
||||
self.agent.retries = 3
|
||||
|
||||
tools = await self._load_skill_tools()
|
||||
|
||||
try:
|
||||
result = await self.agent.run(
|
||||
f"请执行以下任务:\n{task_event}",
|
||||
deps=deps,
|
||||
tools=tools if tools else None
|
||||
)
|
||||
return {"output": result.data.output}
|
||||
except Exception as e:
|
||||
logger.exception(f"SkillIndividual {self.agent_id} 执行失败: {e}")
|
||||
raise
|
||||
@@ -0,0 +1,43 @@
|
||||
# Copyright 2026 zhaoxi826
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from pretor.worker_individual.base_individual import BaseIndividual, WorkerIndividualDeps
|
||||
from pretor.utils.logger import get_logger
|
||||
|
||||
logger = get_logger('special_individual')
|
||||
|
||||
class SpecialIndividual(BaseIndividual):
|
||||
"""
|
||||
特殊子个体:执行特殊任务的 agent,如生成语音、视频等。
|
||||
"""
|
||||
|
||||
def __init__(self, agent_config: dict):
|
||||
super().__init__(agent_config)
|
||||
|
||||
async def run(self, task_event: dict) -> dict:
|
||||
if self.agent is None:
|
||||
system_prompt = self.agent_config.get("prompt", "你是一个特殊的AI助手,负责处理特殊类型的任务。")
|
||||
await self._init_agent("special_individual", system_prompt)
|
||||
|
||||
deps = WorkerIndividualDeps(task_event=task_event)
|
||||
self.agent.retries = 3
|
||||
try:
|
||||
result = await self.agent.run(
|
||||
f"请执行以下任务:\n{task_event}",
|
||||
deps=deps
|
||||
)
|
||||
return {"output": result.data.output}
|
||||
except Exception as e:
|
||||
logger.exception(f"SpecialIndividual {self.agent_id} 执行失败: {e}")
|
||||
raise
|
||||
@@ -0,0 +1,148 @@
|
||||
# Copyright 2026 zhaoxi826
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import ray
|
||||
import time
|
||||
import asyncio
|
||||
from collections import OrderedDict
|
||||
from ray.util.queue import Queue
|
||||
from pretor.utils.ray_hook import ray_actor_hook
|
||||
from pretor.worker_individual.base_individual import BaseIndividual
|
||||
from pretor.worker_individual.skill_individual import SkillIndividual
|
||||
from pretor.worker_individual.ordinary_individual import OrdinaryIndividual
|
||||
from pretor.worker_individual.special_individual import SpecialIndividual
|
||||
|
||||
|
||||
from pretor.utils.logger import get_logger
|
||||
|
||||
|
||||
@ray.remote
|
||||
class WorkerCluster:
|
||||
"""
|
||||
工作集群 Actor:管理和调度所有的 worker_individual
|
||||
设计理念:按需加载,内存 LRU 淘汰,避免 Actor 爆炸
|
||||
"""
|
||||
|
||||
def __init__(self, max_capacity: int = 200, num_runners: int = 10):
|
||||
self.max_capacity = max_capacity
|
||||
self._active_workers: OrderedDict[str, BaseIndividual] = OrderedDict()
|
||||
self.status = "running"
|
||||
self.task_queue = None
|
||||
self.results_futures = {}
|
||||
self.runners = []
|
||||
self.num_runners = num_runners
|
||||
self.logger = get_logger('worker_cluster')
|
||||
|
||||
async def start(self):
|
||||
if self.task_queue is None:
|
||||
self.task_queue = Queue()
|
||||
self.runners = [asyncio.create_task(self._runner(i)) for i in range(self.num_runners)]
|
||||
self.logger.info(f"WorkerCluster 已启动 {self.num_runners} 个 runner 协程。")
|
||||
|
||||
async def _recruit_worker(self, agent_id: str) -> BaseIndividual:
|
||||
"""内部方法:招聘/唤醒一个具体的 Agent 对象"""
|
||||
if agent_id in self._active_workers:
|
||||
self._active_workers.move_to_end(agent_id)
|
||||
return self._active_workers[agent_id]
|
||||
|
||||
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||
agent_config = await global_state_machine.get_individual.remote( agent_id)
|
||||
|
||||
if not agent_config:
|
||||
raise ValueError(f"无法唤醒 Agent {agent_id}:数据库中不存在该档案")
|
||||
|
||||
worker_type = agent_config.get("type", "ordinary")
|
||||
if worker_type == "skill":
|
||||
worker = SkillIndividual(agent_config)
|
||||
elif worker_type == "special":
|
||||
worker = SpecialIndividual(agent_config)
|
||||
else:
|
||||
worker = OrdinaryIndividual(agent_config)
|
||||
|
||||
self._active_workers[agent_id] = worker
|
||||
if len(self._active_workers) > self.max_capacity:
|
||||
evicted_id, _ = self._active_workers.popitem(last=False)
|
||||
self.logger.info(f"[WorkerCluster] 内存池满,休眠老化 Agent: {evicted_id}")
|
||||
|
||||
return worker
|
||||
|
||||
async def _runner(self, runner_id: int):
|
||||
while True:
|
||||
try:
|
||||
if self.task_queue is None:
|
||||
await asyncio.sleep(0.1)
|
||||
continue
|
||||
task = await self.task_queue.get_async()
|
||||
task_id = task.get("task_id")
|
||||
agent_id = task.get("agent_id")
|
||||
task_event = task.get("task_event")
|
||||
|
||||
self.logger.debug(f"[WorkerCluster Runner {runner_id}] 开始处理任务 {task_id} 给 Agent {agent_id}")
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
worker = await self._recruit_worker(agent_id)
|
||||
result = await worker.run(task_event)
|
||||
cost_time = time.time() - start_time
|
||||
|
||||
response = {
|
||||
"success": True,
|
||||
"agent_id": agent_id,
|
||||
"data": result,
|
||||
"metrics": {"cost_time_sec": round(cost_time, 2)}
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.exception(f"[WorkerCluster Runner {runner_id}] 执行任务 {task_id} 时发生错误: {e}")
|
||||
response = {
|
||||
"success": False,
|
||||
"agent_id": agent_id,
|
||||
"error": str(e)
|
||||
}
|
||||
if task_id in self.results_futures:
|
||||
future = self.results_futures[task_id]
|
||||
if not future.done():
|
||||
future.set_result(response)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"[WorkerCluster Runner {runner_id}] 循环发生异常: {e}")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def submit_task(self, task_id: str, agent_id: str, task_event: dict):
|
||||
if not self.runners:
|
||||
await self.start()
|
||||
|
||||
future = asyncio.Future()
|
||||
self.results_futures[task_id] = future
|
||||
|
||||
task = {
|
||||
"task_id": task_id,
|
||||
"agent_id": agent_id,
|
||||
"task_event": task_event
|
||||
}
|
||||
await self.task_queue.put_async(task)
|
||||
self.logger.debug(f"[WorkerCluster] 任务 {task_id} 已加入队列。")
|
||||
|
||||
try:
|
||||
result = await future
|
||||
return result
|
||||
finally:
|
||||
self.results_futures.pop(task_id, None)
|
||||
|
||||
def get_cluster_metrics(self):
|
||||
return {
|
||||
"active_worker_count": len(self._active_workers),
|
||||
"max_capacity": self.max_capacity,
|
||||
"cached_agent_ids": list(self._active_workers.keys()),
|
||||
"queue_size": self.task_queue.size()
|
||||
}
|
||||
Reference in New Issue
Block a user