Files
EastonJiang 11f38624cd fix+perf: 绘图中文字体修复、进度自动跟踪、未验证配置刷爆日志 (#102)
* 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>
2026-07-20 10:31:33 +08:00

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()