mirror of
https://github.com/jihe520/MathModelAgent.git
synced 2026-09-19 08:09:48 +08:00
11f38624cd
* fix: 加固 Docker matplotlib 中文字体配置 Co-authored-by: Cursor <cursoragent@cursor.com> * fix: 提前读取路径 * perf: 进度时间线与代码执行区自动跟踪最新消息 Co-authored-by: Cursor <cursoragent@cursor.com> * fix: 未验证模型配置直接启动任务时无限重试刷爆日志 - 前端保存配置时始终推送到后端,不再依赖验证通过 - 后端 workflow 入口预校验四个 Agent 的 model/apiKey - _validate_config 改抛 LLMConfigError 避免被 JSON 重试捕获 - coordinator/modeler 的 JSON 解析重试加上限 (MAX_JSON_RETRIES=3) Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
244 lines
8.5 KiB
Python
244 lines
8.5 KiB
Python
"""工作流模块,编排多 Agent 协作完成数学建模任务。"""
|
|
|
|
import asyncio
|
|
from app.core.agents import WriterAgent, CoderAgent, CoordinatorAgent, ModelerAgent
|
|
from app.schemas.request import Problem
|
|
from app.schemas.response import SystemMessage
|
|
from app.tools.openalex_scholar import OpenAlexScholar
|
|
from app.utils.log_util import logger
|
|
from app.utils.common_utils import create_work_dir, get_config_template
|
|
from app.models.user_output import UserOutput
|
|
from app.config.setting import settings
|
|
from app.tools.interpreter_factory import create_interpreter
|
|
from app.services.redis_manager import redis_manager
|
|
from app.tools.notebook_serializer import NotebookSerializer
|
|
from app.core.flows import Flows
|
|
from app.core.llm.llm_factory import LLMFactory
|
|
|
|
|
|
class WorkFlow:
|
|
"""工作流基类。"""
|
|
|
|
def __init__(self):
|
|
pass
|
|
|
|
def execute(self) -> None:
|
|
"""执行工作流。"""
|
|
# RichPrinter.workflow_start()
|
|
# RichPrinter.workflow_end()
|
|
pass
|
|
|
|
|
|
class MathModelWorkFlow(WorkFlow):
|
|
"""数学建模工作流,协调协调者、建模手、代码手和写作手完成完整建模任务。"""
|
|
task_id: str #
|
|
work_dir: str # worklow work dir
|
|
ques_count: int = 0 # 问题数量
|
|
questions: dict[str, str | int] = {} # 问题
|
|
cancel_event: asyncio.Event | None = None # 取消信号
|
|
|
|
async def _check_cancelled(self) -> None:
|
|
"""检查是否收到取消信号,若已取消则发布通知并抛出 CancelledError。"""
|
|
if self.cancel_event and self.cancel_event.is_set():
|
|
await redis_manager.publish_message(
|
|
self.task_id,
|
|
SystemMessage(content="任务已停止", type="warning"),
|
|
)
|
|
raise asyncio.CancelledError("任务被用户停止")
|
|
|
|
async def execute(self, problem: Problem): # type: ignore[reportIncompatibleMethodOverride]
|
|
"""执行数学建模工作流。
|
|
|
|
Args:
|
|
problem: 包含题目信息、模板配置等的 Problem 对象。
|
|
"""
|
|
self.task_id = problem.task_id
|
|
self.work_dir = create_work_dir(self.task_id)
|
|
|
|
# 在创建 LLM 前预校验配置,避免进入 Agent 循环后才发现缺配置
|
|
missing = []
|
|
for name, model_val, key_val in [
|
|
("Coordinator", settings.COORDINATOR_MODEL, settings.COORDINATOR_API_KEY),
|
|
("Modeler", settings.MODELER_MODEL, settings.MODELER_API_KEY),
|
|
("Coder", settings.CODER_MODEL, settings.CODER_API_KEY),
|
|
("Writer", settings.WRITER_MODEL, settings.WRITER_API_KEY),
|
|
]:
|
|
if not model_val or not str(model_val).strip():
|
|
missing.append(f"{name} 模型 ID")
|
|
if not key_val or not str(key_val).strip():
|
|
missing.append(f"{name} API Key")
|
|
if missing:
|
|
raise ValueError(f"以下配置缺失,请先在设置中填写并保存:{', '.join(missing)}")
|
|
|
|
llm_factory = LLMFactory(self.task_id)
|
|
coordinator_llm, modeler_llm, coder_llm, writer_llm = llm_factory.get_all_llms()
|
|
|
|
coordinator_agent = CoordinatorAgent(
|
|
self.task_id, coordinator_llm,
|
|
context_window=settings.COORDINATOR_CONTEXT_WINDOW,
|
|
cancel_event=self.cancel_event,
|
|
)
|
|
|
|
await redis_manager.publish_message(
|
|
self.task_id,
|
|
SystemMessage(content="识别用户意图和拆解问题ing..."),
|
|
)
|
|
|
|
await self._check_cancelled()
|
|
|
|
try:
|
|
coordinator_response = await coordinator_agent.run(problem.ques_all)
|
|
self.questions = coordinator_response.questions
|
|
self.ques_count = coordinator_response.ques_count
|
|
except Exception as e:
|
|
# 非数学建模问题
|
|
logger.error(f"CoordinatorAgent 执行失败: {e}")
|
|
raise e
|
|
|
|
await redis_manager.publish_message(
|
|
self.task_id,
|
|
SystemMessage(content="识别用户意图和拆解问题完成,任务转交给建模手"),
|
|
)
|
|
|
|
await redis_manager.publish_message(
|
|
self.task_id,
|
|
SystemMessage(content="建模手开始建模ing..."),
|
|
)
|
|
|
|
await self._check_cancelled()
|
|
|
|
modeler_agent = ModelerAgent(
|
|
self.task_id, modeler_llm,
|
|
context_window=settings.MODELER_CONTEXT_WINDOW,
|
|
cancel_event=self.cancel_event,
|
|
)
|
|
|
|
modeler_response = await modeler_agent.run(coordinator_response)
|
|
|
|
user_output = UserOutput(work_dir=self.work_dir, ques_count=self.ques_count)
|
|
|
|
await redis_manager.publish_message(
|
|
self.task_id,
|
|
SystemMessage(content="正在创建代码沙盒环境"),
|
|
)
|
|
|
|
notebook_serializer = NotebookSerializer(work_dir=self.work_dir)
|
|
code_interpreter = await create_interpreter(
|
|
kind="local",
|
|
task_id=self.task_id,
|
|
work_dir=self.work_dir,
|
|
notebook_serializer=notebook_serializer,
|
|
timeout=3000,
|
|
)
|
|
|
|
assert settings.OPENALEX_EMAIL is not None, "OPENALEX_EMAIL 未配置"
|
|
scholar = OpenAlexScholar(
|
|
task_id=self.task_id,
|
|
email=settings.OPENALEX_EMAIL,
|
|
api_key=settings.OPENALEX_API_KEY,
|
|
)
|
|
|
|
await redis_manager.publish_message(
|
|
self.task_id,
|
|
SystemMessage(content="创建完成"),
|
|
)
|
|
|
|
await redis_manager.publish_message(
|
|
self.task_id,
|
|
SystemMessage(content="初始化代码手"),
|
|
)
|
|
|
|
# modeler_agent
|
|
coder_agent = CoderAgent(
|
|
task_id=problem.task_id,
|
|
model=coder_llm,
|
|
work_dir=self.work_dir,
|
|
max_chat_turns=settings.MAX_CHAT_TURNS,
|
|
max_retries=settings.MAX_RETRIES,
|
|
code_interpreter=code_interpreter,
|
|
context_window=settings.CODER_CONTEXT_WINDOW,
|
|
cancel_event=self.cancel_event,
|
|
)
|
|
|
|
writer_agent = WriterAgent(
|
|
task_id=problem.task_id,
|
|
model=writer_llm,
|
|
comp_template=problem.comp_template,
|
|
format_output=problem.format_output,
|
|
scholar=scholar,
|
|
context_window=settings.WRITER_CONTEXT_WINDOW,
|
|
cancel_event=self.cancel_event,
|
|
)
|
|
|
|
flows = Flows(self.questions)
|
|
|
|
################################################ solution steps
|
|
solution_flows = flows.get_solution_flows(self.questions, modeler_response)
|
|
config_template = get_config_template(problem.comp_template)
|
|
|
|
for key, value in solution_flows.items():
|
|
await self._check_cancelled()
|
|
|
|
await redis_manager.publish_message(
|
|
self.task_id,
|
|
SystemMessage(content=f"代码手开始求解{key}"),
|
|
)
|
|
|
|
coder_response = await coder_agent.run(
|
|
prompt=value["coder_prompt"], subtask_title=key
|
|
)
|
|
|
|
await redis_manager.publish_message(
|
|
self.task_id,
|
|
SystemMessage(content=f"代码手求解成功{key}", type="success"),
|
|
)
|
|
|
|
writer_prompt = flows.get_writer_prompt(
|
|
key, coder_response.code_response or "", code_interpreter, config_template
|
|
)
|
|
|
|
await redis_manager.publish_message(
|
|
self.task_id,
|
|
SystemMessage(content=f"论文手开始写{key}部分"),
|
|
)
|
|
|
|
## TODO: 图片引用错误
|
|
writer_response = await writer_agent.run(
|
|
writer_prompt,
|
|
available_images=coder_response.created_images,
|
|
sub_title=key,
|
|
)
|
|
|
|
await redis_manager.publish_message(
|
|
self.task_id,
|
|
SystemMessage(content=f"论文手完成{key}部分"),
|
|
)
|
|
|
|
user_output.set_res(key, writer_response)
|
|
|
|
# 关闭沙盒
|
|
|
|
await code_interpreter.cleanup()
|
|
logger.info(user_output.get_res())
|
|
|
|
################################################ write steps
|
|
|
|
write_flows = flows.get_write_flows(
|
|
user_output, config_template, problem.ques_all
|
|
)
|
|
for key, value in write_flows.items():
|
|
await self._check_cancelled()
|
|
|
|
await redis_manager.publish_message(
|
|
self.task_id,
|
|
SystemMessage(content=f"论文手开始写{key}部分"),
|
|
)
|
|
|
|
writer_response = await writer_agent.run(prompt=value, sub_title=key)
|
|
|
|
user_output.set_res(key, writer_response)
|
|
|
|
logger.info(user_output.get_res())
|
|
|
|
user_output.save_result()
|