Spaces:
Sleeping
Sleeping
Commit ·
06ef3e6
1
Parent(s): dcbc8e9
init
Browse files- .gitignore +3 -0
- Dockerfile +35 -0
- app.py +48 -0
- config.json +4 -0
- outline.md +248 -0
- requirements.txt +1 -0
- run.py +79 -0
- settings.json +3 -0
- solver.py +194 -0
- spawn.py +85 -0
- static/solver.html +289 -0
.gitignore
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
workspaces/
|
| 2 |
+
*.pyc
|
| 3 |
+
__pycache__/
|
Dockerfile
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:18-slim
|
| 2 |
+
|
| 3 |
+
# Install Python3, pip
|
| 4 |
+
RUN apt-get update && apt-get install -y python3 python3-pip && rm -rf /var/lib/apt/lists/*
|
| 5 |
+
|
| 6 |
+
WORKDIR /app
|
| 7 |
+
|
| 8 |
+
# Install Python dependencies
|
| 9 |
+
COPY requirements.txt .
|
| 10 |
+
RUN pip3 install --no-cache-dir -r requirements.txt
|
| 11 |
+
|
| 12 |
+
# Global install Claude Code CLI
|
| 13 |
+
RUN npm install -g @anthropic-ai/claude-code
|
| 14 |
+
|
| 15 |
+
# Copy solver source files
|
| 16 |
+
COPY run.py spawn.py outline.md config.json /app/solver/
|
| 17 |
+
|
| 18 |
+
# Copy web app files
|
| 19 |
+
COPY app.py solver.py /app/
|
| 20 |
+
COPY static/ /app/static/
|
| 21 |
+
|
| 22 |
+
# Create workspaces directory
|
| 23 |
+
RUN mkdir -p /app/workspaces
|
| 24 |
+
|
| 25 |
+
# Claude Code config
|
| 26 |
+
RUN mkdir -p /root/.claude
|
| 27 |
+
COPY settings.json /root/.claude/settings.json
|
| 28 |
+
|
| 29 |
+
# Environment variables (set via Hugging Face Space secrets)
|
| 30 |
+
ENV ANTHROPIC_BASE_URL=""
|
| 31 |
+
ENV ANTHROPIC_API_KEY=""
|
| 32 |
+
|
| 33 |
+
EXPOSE 7860
|
| 34 |
+
|
| 35 |
+
CMD ["python3", "/app/app.py"]
|
app.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, request, jsonify, send_from_directory
|
| 2 |
+
import solver
|
| 3 |
+
|
| 4 |
+
app = Flask(__name__, static_folder='static')
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
@app.route('/')
|
| 8 |
+
def index():
|
| 9 |
+
return send_from_directory('static', 'solver.html')
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@app.route('/api/submit', methods=['POST'])
|
| 13 |
+
def api_submit():
|
| 14 |
+
data = request.json
|
| 15 |
+
problem = data.get('problem', '').strip()
|
| 16 |
+
if not problem:
|
| 17 |
+
return jsonify({'error': 'No problem provided'}), 400
|
| 18 |
+
|
| 19 |
+
task_id = solver.submit_task(problem)
|
| 20 |
+
if task_id is None:
|
| 21 |
+
return jsonify({'error': 'Server is busy, please try again later'}), 503
|
| 22 |
+
|
| 23 |
+
return jsonify({'task_id': task_id})
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@app.route('/api/status/<task_id>')
|
| 27 |
+
def api_status(task_id):
|
| 28 |
+
task = solver.get_task(task_id)
|
| 29 |
+
if not task:
|
| 30 |
+
return jsonify({'error': 'Task not found'}), 404
|
| 31 |
+
return jsonify(task)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@app.route('/api/result/<task_id>')
|
| 35 |
+
def api_result(task_id):
|
| 36 |
+
content = solver.get_task_result(task_id)
|
| 37 |
+
if content is None:
|
| 38 |
+
return jsonify({'error': 'Result not available yet'}), 404
|
| 39 |
+
return jsonify({'content': content})
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@app.route('/api/tasks')
|
| 43 |
+
def api_tasks():
|
| 44 |
+
return jsonify({'tasks': solver.list_tasks()})
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
if __name__ == '__main__':
|
| 48 |
+
app.run(host='0.0.0.0', port=7860)
|
config.json
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model": "qwen3.6-plus",
|
| 3 |
+
"timeout_seconds": 86400
|
| 4 |
+
}
|
outline.md
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Outline — 多Agent协作物理解题系统
|
| 2 |
+
|
| 3 |
+
## System: Orchestrator
|
| 4 |
+
|
| 5 |
+
你是 Orchestrator(编排者),负责协调多个 sub-Agent 解决物理题目。
|
| 6 |
+
|
| 7 |
+
**工作方式:**
|
| 8 |
+
1. 用 read_file 读取项目根目录的 outline.md,理解完整的工作流架构和各 Agent 定义
|
| 9 |
+
2. 从 outline.md 的 `## Architecture` 部分了解执行顺序、数据流和反馈规则
|
| 10 |
+
3. 从 outline.md 的 `## Agent: <name>` 部分提取对应 Agent 的 prompt
|
| 11 |
+
4. **自动识别输入结构**:
|
| 12 |
+
- 如果指定目录下存在若干子文件夹,每个子文件夹内含 problem.md → 视为多题目录,依次串行处理每个子文件夹
|
| 13 |
+
- 否则 → 视为单题目录,读取该目录下 problem.md 作为唯一题目
|
| 14 |
+
- 多题场景下,每道题独立执行步骤 5-9,全部完成后在**父目录**生成 `batch_summary.md` 汇总所有子题目结果
|
| 15 |
+
5. 对每一道题,按 Architecture 定义的顺序,用 Bash 调用 spawn.py 逐个创建 sub-Agent:
|
| 16 |
+
```
|
| 17 |
+
python3 spawn.py <role> <workspace> <prompt_file> <task_file>
|
| 18 |
+
```
|
| 19 |
+
- `<role>`: Agent 角色名(Planner / Builder / Evaluator)
|
| 20 |
+
- `<workspace>`: 工作目录路径
|
| 21 |
+
- `<prompt_file>`: 临时文件,先写入从 outline.md 提取的 Agent prompt
|
| 22 |
+
- `<task_file>`: 临时文件,先写入任务描述(要读什么文件、输出到什么文件)
|
| 23 |
+
- spawn.py 会创建一个 Claude Code 子进程,完成后将结果写入 `<workspace>/.<role>.result`
|
| 24 |
+
6. 记录每个 sub-Agent 的调用轮次、用时和结果
|
| 25 |
+
7. 全部阶段完成后,检查 Evaluator 的输出文件:
|
| 26 |
+
- 包含 "PASS" → 将解题结果按合理格式写入 {workspace}/final_summary.md,结束
|
| 27 |
+
- 包含 "REVISE" → 按 Architecture 反馈规则重新执行相关 Agent,最多迭代 2 次
|
| 28 |
+
8. 迭代时,将审查意见作为额外上下文加入 Builder 的 task 描述
|
| 29 |
+
9. 第二次迭代仍 REVISE → 将当前最佳方案和未解决问题列表写入 {workspace}/final_summary.md,结束
|
| 30 |
+
|
| 31 |
+
**输出格式:**
|
| 32 |
+
- **单题**:在 final_summary.md 中,请包含以下信息:
|
| 33 |
+
- 各阶段的执行统计:读每个 `.{role}.metrics` 文件(JSON),提取 duration_ms、usage 中的 tokens,汇总轮次、总用时、总 Token 消耗
|
| 34 |
+
- 最终答案的完整呈现
|
| 35 |
+
- 格式清晰、易读
|
| 36 |
+
- **多题**:在父目录生成 `batch_summary.md`,包含每道题的子目录名、是否 PASS、最终答案摘要、轮次和用时。
|
| 37 |
+
|
| 38 |
+
**原则:**
|
| 39 |
+
- 你自己不做具体的物理解题——所有分析、求解、审查都委托给 sub-Agent
|
| 40 |
+
- 你只负责编排:读配置、分配任务、传递上下文、判断是否迭代
|
| 41 |
+
- 每个 sub-Agent 是独立的 Claude Code 进程,完成后返回结果文本
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
## Agent: Planner
|
| 45 |
+
|
| 46 |
+
你是 Planner(规划者),负责分析物理题目并制定解题计划。
|
| 47 |
+
|
| 48 |
+
**输入:** 用 read_file 读取 task 中指定的文件(problem.md,可能还有前置输出)。
|
| 49 |
+
**输出:** 用 write_file 将解题计划写入 task 中指定的输出文件。
|
| 50 |
+
|
| 51 |
+
**解题计划应包含:**
|
| 52 |
+
|
| 53 |
+
### 物理情景
|
| 54 |
+
- 用文字描述物理过程和示意图(标注物体、力/场的方向、坐标系)
|
| 55 |
+
- 明确物理过程的阶段划分(如:加速阶段、碰撞前后等)
|
| 56 |
+
|
| 57 |
+
### 符号约定表
|
| 58 |
+
| 符号 | 含义 | 数值 | 单位 |
|
| 59 |
+
- 已知量和未知量分开列出
|
| 60 |
+
|
| 61 |
+
### 适用物理定律
|
| 62 |
+
- 列出每条定律名称和对应公式
|
| 63 |
+
- 说明为什么适用(满足什么前提条件)
|
| 64 |
+
|
| 65 |
+
### 逐步解题路线
|
| 66 |
+
- 从待求量出发逆向推导:需要什么中间量 → 用什么定律得到 → 写出对应方程
|
| 67 |
+
- 验证方程可解性:未知量个数 = 独立方程个数
|
| 68 |
+
|
| 69 |
+
### 量纲预检
|
| 70 |
+
- 关键等式两边的量纲是否一致
|
| 71 |
+
|
| 72 |
+
### 特殊情况
|
| 73 |
+
- 边界条件、极端情况、多解可能性
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
## Agent: Builder
|
| 77 |
+
|
| 78 |
+
你是 Builder(求解者),负责基于解题计划执行完整推导。
|
| 79 |
+
|
| 80 |
+
**输入:** 用 read_file 读取 task 中指定的文件(problem.md + plan.md)。
|
| 81 |
+
**输出:** 用 write_file 将求解过程写入 task 中指定的输出文件。
|
| 82 |
+
**可选技能:** 你可以在推导过程中运用 outline.md 中定义的 Skills(如 calculation、dimension_check),不需要调用外部工具——凭自身能力按 Skill 描述执行即可。
|
| 83 |
+
|
| 84 |
+
**求解过程应包含:**
|
| 85 |
+
|
| 86 |
+
### 逐步推导
|
| 87 |
+
每一步格式:
|
| 88 |
+
> **Step N: [定律名]**
|
| 89 |
+
> 公式:...
|
| 90 |
+
> 代入:... = ...
|
| 91 |
+
> 结果:... = ... (单位)
|
| 92 |
+
|
| 93 |
+
- 代数推导完整,不跳步
|
| 94 |
+
- 每个数值结果带单位和有效数字
|
| 95 |
+
- 关键步骤后穿插量纲检查
|
| 96 |
+
|
| 97 |
+
### 最终答案
|
| 98 |
+
- 醒目标注,带完整单位
|
| 99 |
+
- 对应题目要求的每个小问逐一回答
|
| 100 |
+
|
| 101 |
+
### 合理性检验
|
| 102 |
+
- 数量级是否合理(与常识对比)
|
| 103 |
+
- 方向是否正确
|
| 104 |
+
- 极端参数退化是否合理(如 μ→0, θ→0 等)
|
| 105 |
+
|
| 106 |
+
如果 task 中包含审查反馈(REVISE 上下文),请先根据反馈定位问题,再修正推导。
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
## Agent: Evaluator
|
| 110 |
+
|
| 111 |
+
你是 Evaluator(审查者),负责严格审查物理求解过程。
|
| 112 |
+
|
| 113 |
+
**输入:** 用 read_file 读取 task 中指定的文件(problem.md + solution.md,可能还有 plan.md)。
|
| 114 |
+
**输出:** 用 write_file 将审查结果写入 task 中指定的输出文件。
|
| 115 |
+
|
| 116 |
+
**审查清单:**
|
| 117 |
+
1. **题意覆盖** — 是否遗漏已知条件?是否误解题意?是否回答了所有小问?
|
| 118 |
+
2. **模型合理性** — 坐标系/参考系选择是否合理?近似条件是否成立?
|
| 119 |
+
3. **推导正确性** — 每步定律适用是否正确?公式是否有误?代数运算是否正确?
|
| 120 |
+
4. **量纲一致性** — 每步等式两边量纲?最终答案单位?
|
| 121 |
+
5. **数值合理性** — 有效数字?数量级可信?
|
| 122 |
+
6. **合理性检验** — 是否做了充分的自洽检查?
|
| 123 |
+
|
| 124 |
+
**输出格式(严格遵守):**
|
| 125 |
+
- 解答正确完整 → 第一行写 `PASS`,后面附简要肯定说明
|
| 126 |
+
- 存在问题 → 第一行写 `REVISE`,后面逐条列出:
|
| 127 |
+
- 在哪一步
|
| 128 |
+
- 错在何处
|
| 129 |
+
- 应如何修正
|
| 130 |
+
描述要足够具体,让 Builder 能直接定位并修正
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
## Architecture
|
| 134 |
+
|
| 135 |
+
### 执行顺序
|
| 136 |
+
```
|
| 137 |
+
Orchestrator
|
| 138 |
+
│
|
| 139 |
+
├─→ spawn Planner
|
| 140 |
+
│ 读 problem.md → 写 {workspace}/plan.md
|
| 141 |
+
│
|
| 142 |
+
├─→ spawn Builder
|
| 143 |
+
│ 读 problem.md + {workspace}/plan.md → 写 {workspace}/solution.md
|
| 144 |
+
│
|
| 145 |
+
├─→ spawn Evaluator
|
| 146 |
+
│ 读 problem.md + {workspace}/solution.md → 写 {workspace}/review.md
|
| 147 |
+
│
|
| 148 |
+
└─→ 检查 review.md
|
| 149 |
+
PASS → 写 {workspace}/final_summary.md,结束
|
| 150 |
+
REVISE → 迭代(见下方反馈规则)
|
| 151 |
+
```
|
| 152 |
+
|
| 153 |
+
### 反馈规则
|
| 154 |
+
- 若 review.md 第一行为 `REVISE`:
|
| 155 |
+
1. Orchestrator 重新 spawn Builder,task 中附带审查意见原文
|
| 156 |
+
2. Builder 修正后写 {workspace}/solution.md
|
| 157 |
+
3. Orchestrator spawn Evaluator 重新审查
|
| 158 |
+
4. 最多迭代 **2** 次
|
| 159 |
+
- 第二次迭代仍 REVISE → 将当前最佳方案和未解决问题列表写入 {workspace}/final_summary.md,结束
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
## Format
|
| 163 |
+
|
| 164 |
+
各 Agent 输出文件使用 Markdown 格式,遵循以下结构。
|
| 165 |
+
|
| 166 |
+
### plan.md
|
| 167 |
+
```markdown
|
| 168 |
+
# 解题计划 — [题目简述]
|
| 169 |
+
|
| 170 |
+
## 物理情景
|
| 171 |
+
[文字描述 + 示意图]
|
| 172 |
+
|
| 173 |
+
## 符号约定
|
| 174 |
+
| 符号 | 含义 | 数值 | 单位 |
|
| 175 |
+
|------|------|------|------|
|
| 176 |
+
|
| 177 |
+
## 适用定律
|
| 178 |
+
1. [定律名]: [公式] — 适用原因: ...
|
| 179 |
+
|
| 180 |
+
## 解题路线
|
| 181 |
+
Step 1: [定律] → [方程] → [求出量]
|
| 182 |
+
Step 2: ...
|
| 183 |
+
|
| 184 |
+
## 可解性检查
|
| 185 |
+
未知量 N 个 = 独立方程 N 个 ✓/✗
|
| 186 |
+
|
| 187 |
+
## 量纲预检
|
| 188 |
+
[关键等式的量纲验证]
|
| 189 |
+
|
| 190 |
+
## 特殊情况
|
| 191 |
+
[边界/极端/多解]
|
| 192 |
+
```
|
| 193 |
+
|
| 194 |
+
### solution.md
|
| 195 |
+
```markdown
|
| 196 |
+
# 求解过程
|
| 197 |
+
|
| 198 |
+
**Step 1: [定律名]**
|
| 199 |
+
公式: ...
|
| 200 |
+
代入: ...
|
| 201 |
+
结果: ... = ... (单位)
|
| 202 |
+
|
| 203 |
+
...
|
| 204 |
+
|
| 205 |
+
## 最终答案
|
| 206 |
+
1. [小问1]: ... = ... (单位)
|
| 207 |
+
2. [小问2]: ...
|
| 208 |
+
|
| 209 |
+
## 合理性检验
|
| 210 |
+
[数量级/方向/极端情况验证]
|
| 211 |
+
```
|
| 212 |
+
|
| 213 |
+
### review.md
|
| 214 |
+
```markdown
|
| 215 |
+
PASS
|
| 216 |
+
[简要肯定说明]
|
| 217 |
+
|
| 218 |
+
或
|
| 219 |
+
|
| 220 |
+
REVISE
|
| 221 |
+
1. [步骤]: [问题描述] → [修正建议]
|
| 222 |
+
2. ...
|
| 223 |
+
```
|
| 224 |
+
|
| 225 |
+
### 文件命名约定
|
| 226 |
+
- 输入文件统一为 `{workspace}/problem.md`
|
| 227 |
+
- 各阶段输出文件名固定: `plan.md`, `solution.md`, `review.md`
|
| 228 |
+
- workspace 目录路径由 Orchestrator 在 task 中指定
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
## Skills
|
| 232 |
+
|
| 233 |
+
Skills 是 Agent 可调用的问题解决能力,由模型自身执行(无外部脚本)。
|
| 234 |
+
Orchestrator 在 spawn Agent 时会将 Agent prompt 中引用的 Skill 内容一并传入。
|
| 235 |
+
新增 Skill 只需在本节添加定义,并在对应 Agent prompt 中声明引用。
|
| 236 |
+
|
| 237 |
+
### Skill: calculation
|
| 238 |
+
数值计算与代数推导验证。
|
| 239 |
+
- 对关键数值结果做手算复核:列出完整的数值代入过程,逐步计算
|
| 240 |
+
- 对代数推导做交叉验证:用不同路线(如能量法 vs 力学法)验证同一结论
|
| 241 |
+
- 结果标注有效数字,与粗略估计对比数量级
|
| 242 |
+
|
| 243 |
+
### Skill: dimension_check
|
| 244 |
+
量纲一致性检查。
|
| 245 |
+
- 逐步验证每个物理等式:左右两边量纲是否相同
|
| 246 |
+
- 验证最终答案的量纲是否与待求量的物理意义匹配
|
| 247 |
+
- 常见SI量纲:m(L), kg(M), s(T), N(M·L·T⁻²), J(M·L²·T⁻²), m/s(L·T⁻¹), m/s²(L·T⁻²)
|
| 248 |
+
- 检查时将所有量展开为基本量纲(L,M,T)后对比
|
requirements.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
Flask==2.3.3
|
run.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Minimal bootstrap — launches Orchestrator Agent via Claude Code CLI."""
|
| 3 |
+
|
| 4 |
+
import sys, os, json, re, subprocess
|
| 5 |
+
|
| 6 |
+
ROOT = os.path.dirname(os.path.abspath(__file__))
|
| 7 |
+
os.chdir(ROOT)
|
| 8 |
+
|
| 9 |
+
CONFIG = json.loads(open(os.path.join(ROOT, "config.json"), encoding="utf-8").read())
|
| 10 |
+
MODEL = CONFIG.get("model", "sonnet")
|
| 11 |
+
TIMEOUT = CONFIG.get("timeout_seconds", 600)
|
| 12 |
+
|
| 13 |
+
SUMMARY_FILE = "problems/001/final_summary.md"
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def extract_section(md, heading):
|
| 17 |
+
"""Extract content after '## <heading>' until the next ## or EOF."""
|
| 18 |
+
pattern = rf"^##\s+{re.escape(heading)}\s*\n(.*?)(?=^##\s|\Z)"
|
| 19 |
+
m = re.search(pattern, md, re.MULTILINE | re.DOTALL)
|
| 20 |
+
return m.group(1).strip() if m else ""
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def main():
|
| 24 |
+
workspace = sys.argv[1] if len(sys.argv) > 1 else "problems/001"
|
| 25 |
+
|
| 26 |
+
outline = open(os.path.join(ROOT, "outline.md"), encoding="utf-8").read()
|
| 27 |
+
orchestrator_prompt = extract_section(outline, "System: Orchestrator")
|
| 28 |
+
|
| 29 |
+
if not orchestrator_prompt:
|
| 30 |
+
print("Error: cannot find '## System: Orchestrator' in outline.md")
|
| 31 |
+
sys.exit(1)
|
| 32 |
+
|
| 33 |
+
agents_json = json.dumps({
|
| 34 |
+
"Orchestrator": {
|
| 35 |
+
"description": "Orchestrator — 编排多个Agent解决物理题目",
|
| 36 |
+
"prompt": orchestrator_prompt,
|
| 37 |
+
}
|
| 38 |
+
})
|
| 39 |
+
|
| 40 |
+
cmd = [
|
| 41 |
+
"claude",
|
| 42 |
+
"--print",
|
| 43 |
+
"--output-format", "json",
|
| 44 |
+
"--permission-mode", "bypassPermissions",
|
| 45 |
+
"--bare",
|
| 46 |
+
"--agents", agents_json,
|
| 47 |
+
"--agent", "Orchestrator",
|
| 48 |
+
"--allowed-tools", "Bash,Read,Write",
|
| 49 |
+
"--add-dir", workspace,
|
| 50 |
+
"--model", MODEL,
|
| 51 |
+
f"请解决 {workspace}/problem.md 中的物理题目。\n"
|
| 52 |
+
f"先读取 outline.md 了解工作流架构和各 Agent 定义,然后按照架构执行。\n"
|
| 53 |
+
f"创建子 Agent 的方法:Bash 调用 spawn.py <role> <workspace> <prompt_file> <task_file>\n"
|
| 54 |
+
f"全部阶段完成后,将最终解题结果(包含轮次统计、用时、Token消耗等关键指标和解题内容)写入 {workspace}/final_summary.md 文件。\n"
|
| 55 |
+
f"工作目录: {workspace}",
|
| 56 |
+
]
|
| 57 |
+
|
| 58 |
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=TIMEOUT)
|
| 59 |
+
|
| 60 |
+
try:
|
| 61 |
+
data = json.loads(result.stdout)
|
| 62 |
+
except json.JSONDecodeError:
|
| 63 |
+
print(f"Error: JSON parse failed\n{result.stdout[-500:]}")
|
| 64 |
+
sys.exit(1)
|
| 65 |
+
|
| 66 |
+
if data.get("is_error"):
|
| 67 |
+
print(f"Error: {data.get('result', 'Unknown')[:300]}")
|
| 68 |
+
sys.exit(1)
|
| 69 |
+
|
| 70 |
+
# Print whatever Orchestrator wrote
|
| 71 |
+
summary_path = os.path.join(workspace, "final_summary.md")
|
| 72 |
+
if os.path.exists(summary_path):
|
| 73 |
+
print(open(summary_path, encoding="utf-8").read())
|
| 74 |
+
else:
|
| 75 |
+
print("No summary file found.")
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
if __name__ == "__main__":
|
| 79 |
+
main()
|
settings.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"apiKeyHelper": "echo $ANTHROPIC_API_KEY"
|
| 3 |
+
}
|
solver.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Background task manager for the physics solver."""
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
import json
|
| 7 |
+
import time
|
| 8 |
+
import shutil
|
| 9 |
+
import subprocess
|
| 10 |
+
import threading
|
| 11 |
+
from datetime import datetime, timezone
|
| 12 |
+
|
| 13 |
+
# Solver source location: Docker uses /app/solver, local falls back to project root
|
| 14 |
+
SOLVER_SRC = "/app/solver" if os.path.isdir("/app/solver") else os.path.dirname(os.path.abspath(__file__))
|
| 15 |
+
WORKSPACE_BASE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "workspaces")
|
| 16 |
+
DEFAULT_TIMEOUT = int(os.environ.get("SOLVER_TIMEOUT", "600"))
|
| 17 |
+
DEFAULT_MODEL = os.environ.get("SOLVER_MODEL", "qwen3.6-plus")
|
| 18 |
+
MAX_TASKS = int(os.environ.get("SOLVER_MAX_TASKS", "1"))
|
| 19 |
+
|
| 20 |
+
tasks = {}
|
| 21 |
+
tasks_lock = threading.Lock()
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _now():
|
| 25 |
+
return datetime.now(timezone.utc).isoformat()
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def active_count():
|
| 29 |
+
with tasks_lock:
|
| 30 |
+
return sum(1 for t in tasks.values() if t["status"] in ("pending", "running"))
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def submit_task(problem_text):
|
| 34 |
+
"""Create a new task, return task_id or None if at capacity."""
|
| 35 |
+
with tasks_lock:
|
| 36 |
+
if active_count() >= MAX_TASKS:
|
| 37 |
+
return None
|
| 38 |
+
|
| 39 |
+
task_id = f"task-{int(time.time()*1000)}"
|
| 40 |
+
workspace = os.path.join(WORKSPACE_BASE, task_id)
|
| 41 |
+
os.makedirs(workspace, exist_ok=True)
|
| 42 |
+
|
| 43 |
+
# Write problem
|
| 44 |
+
with open(os.path.join(workspace, "problem.md"), "w", encoding="utf-8") as f:
|
| 45 |
+
f.write(problem_text)
|
| 46 |
+
|
| 47 |
+
# Copy solver source into workspace
|
| 48 |
+
for name in ("run.py", "spawn.py", "outline.md", "config.json"):
|
| 49 |
+
src = os.path.join(SOLVER_SRC, name)
|
| 50 |
+
if os.path.exists(src):
|
| 51 |
+
shutil.copy2(src, os.path.join(workspace, name))
|
| 52 |
+
|
| 53 |
+
# Override config.json with env-driven values
|
| 54 |
+
config_path = os.path.join(workspace, "config.json")
|
| 55 |
+
config = {"model": DEFAULT_MODEL, "timeout_seconds": DEFAULT_TIMEOUT}
|
| 56 |
+
with open(config_path, "w", encoding="utf-8") as f:
|
| 57 |
+
json.dump(config, f)
|
| 58 |
+
|
| 59 |
+
task = {
|
| 60 |
+
"task_id": task_id,
|
| 61 |
+
"status": "pending",
|
| 62 |
+
"phase": "waiting",
|
| 63 |
+
"workspace": workspace,
|
| 64 |
+
"problem": problem_text[:200],
|
| 65 |
+
"created_at": _now(),
|
| 66 |
+
"started_at": None,
|
| 67 |
+
"finished_at": None,
|
| 68 |
+
"error": None,
|
| 69 |
+
}
|
| 70 |
+
with tasks_lock:
|
| 71 |
+
tasks[task_id] = task
|
| 72 |
+
|
| 73 |
+
# Spawn background thread
|
| 74 |
+
t = threading.Thread(target=_run_solver, args=(task_id, workspace), daemon=True)
|
| 75 |
+
t.start()
|
| 76 |
+
return task_id
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _run_solver(task_id, workspace):
|
| 80 |
+
with tasks_lock:
|
| 81 |
+
tasks[task_id].update({"status": "running", "started_at": _now()})
|
| 82 |
+
|
| 83 |
+
env = os.environ.copy()
|
| 84 |
+
start = time.time()
|
| 85 |
+
|
| 86 |
+
try:
|
| 87 |
+
proc = subprocess.Popen(
|
| 88 |
+
[sys.executable, os.path.join(workspace, "run.py"), workspace],
|
| 89 |
+
stdout=subprocess.PIPE,
|
| 90 |
+
stderr=subprocess.PIPE,
|
| 91 |
+
cwd=workspace,
|
| 92 |
+
env=env,
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
# Monitor workspace for progress
|
| 96 |
+
while proc.poll() is None:
|
| 97 |
+
elapsed = time.time() - start
|
| 98 |
+
if elapsed > DEFAULT_TIMEOUT:
|
| 99 |
+
proc.kill()
|
| 100 |
+
with tasks_lock:
|
| 101 |
+
tasks[task_id].update({
|
| 102 |
+
"status": "error",
|
| 103 |
+
"error": f"Timeout after {elapsed:.0f}s",
|
| 104 |
+
"finished_at": _now(),
|
| 105 |
+
})
|
| 106 |
+
return
|
| 107 |
+
|
| 108 |
+
# Infer phase from workspace files
|
| 109 |
+
phase = _infer_phase(workspace)
|
| 110 |
+
with tasks_lock:
|
| 111 |
+
tasks[task_id]["phase"] = phase
|
| 112 |
+
|
| 113 |
+
time.sleep(2)
|
| 114 |
+
|
| 115 |
+
stdout, stderr = proc.communicate()
|
| 116 |
+
|
| 117 |
+
if proc.returncode != 0:
|
| 118 |
+
with tasks_lock:
|
| 119 |
+
tasks[task_id].update({
|
| 120 |
+
"status": "error",
|
| 121 |
+
"error": stderr.decode("utf-8", errors="replace")[-500:],
|
| 122 |
+
"finished_at": _now(),
|
| 123 |
+
})
|
| 124 |
+
return
|
| 125 |
+
|
| 126 |
+
# Check for final summary
|
| 127 |
+
summary = os.path.join(workspace, "final_summary.md")
|
| 128 |
+
if os.path.exists(summary):
|
| 129 |
+
with tasks_lock:
|
| 130 |
+
tasks[task_id].update({
|
| 131 |
+
"status": "complete",
|
| 132 |
+
"phase": "complete",
|
| 133 |
+
"finished_at": _now(),
|
| 134 |
+
})
|
| 135 |
+
else:
|
| 136 |
+
with tasks_lock:
|
| 137 |
+
tasks[task_id].update({
|
| 138 |
+
"status": "error",
|
| 139 |
+
"error": "Solver finished but no summary file was produced.",
|
| 140 |
+
"finished_at": _now(),
|
| 141 |
+
})
|
| 142 |
+
|
| 143 |
+
except Exception as e:
|
| 144 |
+
with tasks_lock:
|
| 145 |
+
tasks[task_id].update({
|
| 146 |
+
"status": "error",
|
| 147 |
+
"error": str(e),
|
| 148 |
+
"finished_at": _now(),
|
| 149 |
+
})
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def _infer_phase(workspace):
|
| 153 |
+
"""Check workspace files to determine current solver phase."""
|
| 154 |
+
if os.path.exists(os.path.join(workspace, "final_summary.md")):
|
| 155 |
+
return "complete"
|
| 156 |
+
if os.path.exists(os.path.join(workspace, "review.md")):
|
| 157 |
+
return "evaluating"
|
| 158 |
+
if os.path.exists(os.path.join(workspace, "solution.md")):
|
| 159 |
+
return "building"
|
| 160 |
+
if os.path.exists(os.path.join(workspace, "plan.md")):
|
| 161 |
+
return "planning"
|
| 162 |
+
return "starting"
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def get_task(task_id):
|
| 166 |
+
with tasks_lock:
|
| 167 |
+
return dict(tasks.get(task_id, {}))
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def get_task_result(task_id):
|
| 171 |
+
"""Read final_summary.md if available."""
|
| 172 |
+
task = get_task(task_id)
|
| 173 |
+
if not task:
|
| 174 |
+
return None
|
| 175 |
+
summary = os.path.join(task.get("workspace", ""), "final_summary.md")
|
| 176 |
+
if os.path.exists(summary):
|
| 177 |
+
with open(summary, encoding="utf-8") as f:
|
| 178 |
+
return f.read()
|
| 179 |
+
return None
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def list_tasks():
|
| 183 |
+
with tasks_lock:
|
| 184 |
+
return [
|
| 185 |
+
{
|
| 186 |
+
"task_id": t["task_id"],
|
| 187 |
+
"status": t["status"],
|
| 188 |
+
"phase": t["phase"],
|
| 189 |
+
"problem": t["problem"],
|
| 190 |
+
"created_at": t["created_at"],
|
| 191 |
+
"finished_at": t.get("finished_at"),
|
| 192 |
+
}
|
| 193 |
+
for t in sorted(tasks.values(), key=lambda x: x["created_at"], reverse=True)
|
| 194 |
+
]
|
spawn.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Helper script — spawn a sub-Agent via Claude Code CLI.
|
| 3 |
+
Called by the Orchestrator through Bash tool use.
|
| 4 |
+
|
| 5 |
+
Usage: spawn.py <role> <workspace> <prompt_file> <task_file> [--tools Read,Write]
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import sys, os, json, subprocess
|
| 9 |
+
|
| 10 |
+
ROOT = os.path.dirname(os.path.abspath(__file__))
|
| 11 |
+
CONFIG = json.loads(open(os.path.join(ROOT, "config.json"), encoding="utf-8").read())
|
| 12 |
+
MODEL = CONFIG.get("model", "sonnet")
|
| 13 |
+
TIMEOUT = CONFIG.get("timeout_seconds", 600)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def main():
|
| 17 |
+
if len(sys.argv) < 5:
|
| 18 |
+
print("Usage: spawn.py <role> <workspace> <prompt_file> <task_file> [--tools Read,Write]")
|
| 19 |
+
sys.exit(1)
|
| 20 |
+
|
| 21 |
+
role = sys.argv[1]
|
| 22 |
+
workspace = sys.argv[2]
|
| 23 |
+
prompt_file = sys.argv[3]
|
| 24 |
+
task_file = sys.argv[4]
|
| 25 |
+
|
| 26 |
+
# Parse optional --tools
|
| 27 |
+
allowed_tools = "Read,Write"
|
| 28 |
+
for i, arg in enumerate(sys.argv):
|
| 29 |
+
if arg == "--tools" and i + 1 < len(sys.argv):
|
| 30 |
+
allowed_tools = sys.argv[i + 1]
|
| 31 |
+
|
| 32 |
+
system_prompt = open(prompt_file, encoding="utf-8").read()
|
| 33 |
+
task = open(task_file, encoding="utf-8").read()
|
| 34 |
+
if workspace:
|
| 35 |
+
task += f"\n工作目录: {workspace}"
|
| 36 |
+
|
| 37 |
+
agents_json = json.dumps({role: {"description": f"{role} Agent", "prompt": system_prompt}})
|
| 38 |
+
|
| 39 |
+
cmd = [
|
| 40 |
+
"claude",
|
| 41 |
+
"--print",
|
| 42 |
+
"--output-format", "json",
|
| 43 |
+
"--permission-mode", "bypassPermissions",
|
| 44 |
+
"--bare",
|
| 45 |
+
"--agents", agents_json,
|
| 46 |
+
"--agent", role,
|
| 47 |
+
"--allowed-tools", allowed_tools,
|
| 48 |
+
"--add-dir", workspace,
|
| 49 |
+
"--model", MODEL,
|
| 50 |
+
task,
|
| 51 |
+
]
|
| 52 |
+
|
| 53 |
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=TIMEOUT)
|
| 54 |
+
|
| 55 |
+
try:
|
| 56 |
+
data = json.loads(result.stdout)
|
| 57 |
+
except json.JSONDecodeError:
|
| 58 |
+
print(f"[spawn:{role}] error: JSON parse failed — {result.stdout[-300:]}")
|
| 59 |
+
sys.exit(1)
|
| 60 |
+
|
| 61 |
+
if data.get("is_error"):
|
| 62 |
+
print(f"[spawn:{role}] error: {data.get('result', 'Unknown')[:300]}")
|
| 63 |
+
sys.exit(1)
|
| 64 |
+
|
| 65 |
+
# Write result text for Orchestrator
|
| 66 |
+
result_path = os.path.join(workspace, f".{role}.result")
|
| 67 |
+
with open(result_path, "w", encoding="utf-8") as f:
|
| 68 |
+
f.write(data.get("result", ""))
|
| 69 |
+
|
| 70 |
+
# Write metrics for Orchestrator to collect
|
| 71 |
+
metrics = {
|
| 72 |
+
"role": role,
|
| 73 |
+
"duration_ms": data.get("duration_ms", 0),
|
| 74 |
+
"duration_api_ms": data.get("duration_api_ms", 0),
|
| 75 |
+
"usage": data.get("usage", {}),
|
| 76 |
+
}
|
| 77 |
+
metrics_path = os.path.join(workspace, f".{role}.metrics")
|
| 78 |
+
with open(metrics_path, "w", encoding="utf-8") as f:
|
| 79 |
+
json.dump(metrics, f, ensure_ascii=False)
|
| 80 |
+
|
| 81 |
+
print(f"[spawn:{role}] done → {result_path}")
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
if __name__ == "__main__":
|
| 85 |
+
main()
|
static/solver.html
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Physics Problem Solver</title>
|
| 7 |
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
| 8 |
+
<style>
|
| 9 |
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 10 |
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f0f2f5; color: #333; min-height: 100vh; }
|
| 11 |
+
|
| 12 |
+
.container { max-width: 960px; margin: 0 auto; padding: 20px; }
|
| 13 |
+
h1 { text-align: center; padding: 20px 0 10px; color: #1a1a2e; font-size: 1.8em; }
|
| 14 |
+
.subtitle { text-align: center; color: #666; margin-bottom: 24px; font-size: 0.95em; }
|
| 15 |
+
|
| 16 |
+
.card { background: #fff; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); padding: 24px; margin-bottom: 20px; }
|
| 17 |
+
|
| 18 |
+
textarea { width: 100%; min-height: 180px; border: 1px solid #ddd; border-radius: 8px; padding: 12px; font-size: 14px; font-family: inherit; resize: vertical; line-height: 1.6; }
|
| 19 |
+
textarea:focus { outline: none; border-color: #4361ee; box-shadow: 0 0 0 3px rgba(67,97,238,0.15); }
|
| 20 |
+
|
| 21 |
+
.btn-row { display: flex; gap: 12px; margin-top: 12px; }
|
| 22 |
+
.btn { padding: 10px 28px; border: none; border-radius: 8px; font-size: 15px; cursor: pointer; font-weight: 600; transition: all 0.15s; }
|
| 23 |
+
.btn-primary { background: #4361ee; color: #fff; }
|
| 24 |
+
.btn-primary:hover:not(:disabled) { background: #3a56d4; }
|
| 25 |
+
.btn-primary:disabled { background: #a0b0f0; cursor: not-allowed; }
|
| 26 |
+
.btn-secondary { background: #e8e8e8; color: #333; }
|
| 27 |
+
.btn-secondary:hover { background: #d8d8d8; }
|
| 28 |
+
|
| 29 |
+
/* Progress Steps */
|
| 30 |
+
.progress-bar { display: none; margin: 16px 0 8px; }
|
| 31 |
+
.progress-bar.visible { display: block; }
|
| 32 |
+
.steps { display: flex; justify-content: space-between; position: relative; margin-bottom: 8px; }
|
| 33 |
+
.steps::before { content: ''; position: absolute; top: 14px; left: 40px; right: 40px; height: 3px; background: #e0e0e0; z-index: 0; }
|
| 34 |
+
.step { position: relative; z-index: 1; text-align: center; flex: 1; }
|
| 35 |
+
.step-circle { width: 28px; height: 28px; border-radius: 50%; background: #e0e0e0; margin: 0 auto 6px; display: flex; align-items: center; justify-content: center; font-size: 13px; color: #999; transition: all 0.3s; }
|
| 36 |
+
.step.active .step-circle { background: #4361ee; color: #fff; }
|
| 37 |
+
.step.done .step-circle { background: #2ecc71; color: #fff; }
|
| 38 |
+
.step-label { font-size: 12px; color: #888; }
|
| 39 |
+
.step.active .step-label { color: #4361ee; font-weight: 600; }
|
| 40 |
+
.step.done .step-label { color: #2ecc71; }
|
| 41 |
+
|
| 42 |
+
.elapsed { text-align: center; color: #888; font-size: 13px; margin-top: 4px; }
|
| 43 |
+
|
| 44 |
+
/* Result */
|
| 45 |
+
.result { display: none; margin-top: 16px; }
|
| 46 |
+
.result.visible { display: block; }
|
| 47 |
+
.result h2 { margin: 12px 0 8px; font-size: 1.2em; color: #1a1a2e; }
|
| 48 |
+
.result-content { line-height: 1.7; }
|
| 49 |
+
.result-content table { border-collapse: collapse; width: 100%; margin: 12px 0; }
|
| 50 |
+
.result-content th, .result-content td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
| 51 |
+
.result-content th { background: #f5f5f5; }
|
| 52 |
+
.result-content pre { background: #f8f8f8; padding: 12px; border-radius: 6px; overflow-x: auto; font-size: 13px; }
|
| 53 |
+
.result-content code { background: #f0f0f0; padding: 1px 4px; border-radius: 3px; font-size: 13px; }
|
| 54 |
+
.result-content pre code { background: none; padding: 0; }
|
| 55 |
+
.result-content blockquote { border-left: 3px solid #4361ee; padding-left: 12px; color: #555; margin: 8px 0; }
|
| 56 |
+
|
| 57 |
+
/* Task list */
|
| 58 |
+
.task-list { max-height: 200px; overflow-y: auto; }
|
| 59 |
+
.task-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid #f0f0f0; cursor: pointer; }
|
| 60 |
+
.task-item:last-child { border-bottom: none; }
|
| 61 |
+
.task-item:hover { background: #fafafa; margin: 0 -24px; padding: 8px 24px; border-radius: 4px; }
|
| 62 |
+
.task-id { font-family: monospace; font-size: 13px; color: #666; }
|
| 63 |
+
.badge { font-size: 11px; padding: 2px 8px; border-radius: 10px; font-weight: 600; }
|
| 64 |
+
.badge-pending { background: #fff3cd; color: #856404; }
|
| 65 |
+
.badge-running { background: #cce5ff; color: #004085; }
|
| 66 |
+
.badge-complete { background: #d4edda; color: #155724; }
|
| 67 |
+
.badge-error { background: #f8d7da; color: #721c24; }
|
| 68 |
+
|
| 69 |
+
.error-msg { background: #f8d7da; color: #721c24; padding: 12px; border-radius: 8px; margin-top: 12px; display: none; }
|
| 70 |
+
.error-msg.visible { display: block; }
|
| 71 |
+
</style>
|
| 72 |
+
</head>
|
| 73 |
+
<body>
|
| 74 |
+
<div class="container">
|
| 75 |
+
<h1>Physics Problem Solver</h1>
|
| 76 |
+
<p class="subtitle">Multi-agent AI system: Plan → Build → Evaluate</p>
|
| 77 |
+
|
| 78 |
+
<div class="card">
|
| 79 |
+
<label style="font-weight:600;margin-bottom:8px;display:block;">Input Physics Problem</label>
|
| 80 |
+
<textarea id="problem-input" placeholder="e.g. A 2.0 kg object slides down a 30-degree rough incline..."></textarea>
|
| 81 |
+
<div class="btn-row">
|
| 82 |
+
<button class="btn btn-primary" id="submit-btn" onclick="submitProblem()">Solve</button>
|
| 83 |
+
</div>
|
| 84 |
+
|
| 85 |
+
<!-- Progress -->
|
| 86 |
+
<div class="progress-bar" id="progress-bar">
|
| 87 |
+
<div class="steps">
|
| 88 |
+
<div class="step" id="step-starting"><div class="step-circle">1</div><div class="step-label">Starting</div></div>
|
| 89 |
+
<div class="step" id="step-planning"><div class="step-circle">2</div><div class="step-label">Planning</div></div>
|
| 90 |
+
<div class="step" id="step-building"><div class="step-circle">3</div><div class="step-label">Building</div></div>
|
| 91 |
+
<div class="step" id="step-evaluating"><div class="step-circle">4</div><div class="step-label">Evaluating</div></div>
|
| 92 |
+
<div class="step" id="step-complete"><div class="step-circle">5</div><div class="step-label">Complete</div></div>
|
| 93 |
+
</div>
|
| 94 |
+
<div class="elapsed" id="elapsed"></div>
|
| 95 |
+
</div>
|
| 96 |
+
|
| 97 |
+
<div class="error-msg" id="error-msg"></div>
|
| 98 |
+
|
| 99 |
+
<!-- Result -->
|
| 100 |
+
<div class="result" id="result">
|
| 101 |
+
<div class="result-content" id="result-content"></div>
|
| 102 |
+
<div class="btn-row">
|
| 103 |
+
<button class="btn btn-secondary" onclick="resetForm()">New Problem</button>
|
| 104 |
+
</div>
|
| 105 |
+
</div>
|
| 106 |
+
</div>
|
| 107 |
+
|
| 108 |
+
<!-- Task History -->
|
| 109 |
+
<div class="card">
|
| 110 |
+
<h2 style="font-size:1em;margin-bottom:12px;">Recent Tasks</h2>
|
| 111 |
+
<div class="task-list" id="task-list">
|
| 112 |
+
<p style="color:#999;font-size:13px;">No tasks yet.</p>
|
| 113 |
+
</div>
|
| 114 |
+
</div>
|
| 115 |
+
</div>
|
| 116 |
+
|
| 117 |
+
<script>
|
| 118 |
+
let currentTaskId = null;
|
| 119 |
+
let pollTimer = null;
|
| 120 |
+
let startTime = null;
|
| 121 |
+
let elapsedTimer = null;
|
| 122 |
+
|
| 123 |
+
const phaseOrder = ['starting', 'planning', 'building', 'evaluating', 'complete'];
|
| 124 |
+
|
| 125 |
+
function setStatus(status) {
|
| 126 |
+
document.getElementById('submit-btn').disabled = (status === 'running');
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
function updateProgress(phase) {
|
| 130 |
+
phaseOrder.forEach(p => {
|
| 131 |
+
const el = document.getElementById('step-' + p);
|
| 132 |
+
el.classList.remove('active', 'done');
|
| 133 |
+
if (p === phase) el.classList.add('active');
|
| 134 |
+
else if (phaseOrder.indexOf(p) < phaseOrder.indexOf(phase)) el.classList.add('done');
|
| 135 |
+
});
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
function showError(msg) {
|
| 139 |
+
const el = document.getElementById('error-msg');
|
| 140 |
+
el.textContent = msg;
|
| 141 |
+
el.classList.add('visible');
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
function hideError() {
|
| 145 |
+
document.getElementById('error-msg').classList.remove('visible');
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
async function submitProblem() {
|
| 149 |
+
const text = document.getElementById('problem-input').value.trim();
|
| 150 |
+
if (!text) return;
|
| 151 |
+
|
| 152 |
+
hideError();
|
| 153 |
+
document.getElementById('result').classList.remove('visible');
|
| 154 |
+
document.getElementById('progress-bar').classList.add('visible');
|
| 155 |
+
setStatus('running');
|
| 156 |
+
|
| 157 |
+
try {
|
| 158 |
+
const resp = await fetch('/api/submit', {
|
| 159 |
+
method: 'POST',
|
| 160 |
+
headers: {'Content-Type': 'application/json'},
|
| 161 |
+
body: JSON.stringify({problem: text})
|
| 162 |
+
});
|
| 163 |
+
const data = await resp.json();
|
| 164 |
+
if (data.error) { showError(data.error); setStatus('idle'); return; }
|
| 165 |
+
|
| 166 |
+
currentTaskId = data.task_id;
|
| 167 |
+
startTime = Date.now();
|
| 168 |
+
startElapsed();
|
| 169 |
+
pollStatus();
|
| 170 |
+
} catch (e) {
|
| 171 |
+
showError('Network error: ' + e.message);
|
| 172 |
+
setStatus('idle');
|
| 173 |
+
}
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
function pollStatus() {
|
| 177 |
+
if (pollTimer) clearInterval(pollTimer);
|
| 178 |
+
pollTimer = setInterval(async () => {
|
| 179 |
+
if (!currentTaskId) return;
|
| 180 |
+
try {
|
| 181 |
+
const resp = await fetch('/api/status/' + currentTaskId);
|
| 182 |
+
const data = await resp.json();
|
| 183 |
+
if (data.error) { showError(data.error); stopPolling(); return; }
|
| 184 |
+
|
| 185 |
+
updateProgress(data.phase || 'starting');
|
| 186 |
+
|
| 187 |
+
if (data.status === 'complete') {
|
| 188 |
+
stopPolling();
|
| 189 |
+
await loadResult();
|
| 190 |
+
} else if (data.status === 'error') {
|
| 191 |
+
stopPolling();
|
| 192 |
+
showError(data.error || 'Task failed');
|
| 193 |
+
setStatus('idle');
|
| 194 |
+
}
|
| 195 |
+
} catch (e) { /* retry next poll */ }
|
| 196 |
+
}, 3000);
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
function stopPolling() {
|
| 200 |
+
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
| 201 |
+
if (elapsedTimer) { clearInterval(elapsedTimer); elapsedTimer = null; }
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
function startElapsed() {
|
| 205 |
+
if (elapsedTimer) clearInterval(elapsedTimer);
|
| 206 |
+
elapsedTimer = setInterval(() => {
|
| 207 |
+
if (!startTime) return;
|
| 208 |
+
const s = Math.floor((Date.now() - startTime) / 1000);
|
| 209 |
+
const m = Math.floor(s / 60);
|
| 210 |
+
document.getElementById('elapsed').textContent = `Elapsed: ${m}m ${s % 60}s`;
|
| 211 |
+
}, 1000);
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
async function loadResult() {
|
| 215 |
+
try {
|
| 216 |
+
const resp = await fetch('/api/result/' + currentTaskId);
|
| 217 |
+
const data = await resp.json();
|
| 218 |
+
if (data.content) {
|
| 219 |
+
document.getElementById('result-content').innerHTML = marked.parse(data.content);
|
| 220 |
+
document.getElementById('result').classList.add('visible');
|
| 221 |
+
}
|
| 222 |
+
setStatus('idle');
|
| 223 |
+
} catch (e) {
|
| 224 |
+
showError('Failed to load result');
|
| 225 |
+
setStatus('idle');
|
| 226 |
+
}
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
function resetForm() {
|
| 230 |
+
currentTaskId = null;
|
| 231 |
+
startTime = null;
|
| 232 |
+
stopPolling();
|
| 233 |
+
document.getElementById('problem-input').value = '';
|
| 234 |
+
document.getElementById('progress-bar').classList.remove('visible');
|
| 235 |
+
document.getElementById('result').classList.remove('visible');
|
| 236 |
+
hideError();
|
| 237 |
+
setStatus('idle');
|
| 238 |
+
phaseOrder.forEach(p => {
|
| 239 |
+
document.getElementById('step-' + p).classList.remove('active', 'done');
|
| 240 |
+
});
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
async function loadTaskList() {
|
| 244 |
+
try {
|
| 245 |
+
const resp = await fetch('/api/tasks');
|
| 246 |
+
const data = await resp.json();
|
| 247 |
+
const el = document.getElementById('task-list');
|
| 248 |
+
if (!data.tasks || data.tasks.length === 0) {
|
| 249 |
+
el.innerHTML = '<p style="color:#999;font-size:13px;">No tasks yet.</p>';
|
| 250 |
+
return;
|
| 251 |
+
}
|
| 252 |
+
el.innerHTML = data.tasks.slice(0, 20).map(t => {
|
| 253 |
+
const badgeClass = 'badge-' + t.status;
|
| 254 |
+
const time = t.created_at ? new Date(t.created_at).toLocaleTimeString() : '';
|
| 255 |
+
return `<div class="task-item" onclick="viewTask('${t.task_id}')">
|
| 256 |
+
<span class="task-id">${t.task_id}</span>
|
| 257 |
+
<span>${time}</span>
|
| 258 |
+
<span class="badge ${badgeClass}">${t.status}</span>
|
| 259 |
+
</div>`;
|
| 260 |
+
}).join('');
|
| 261 |
+
} catch (e) {}
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
async function viewTask(taskId) {
|
| 265 |
+
const resp = await fetch('/api/status/' + taskId);
|
| 266 |
+
const data = await resp.json();
|
| 267 |
+
if (data.status === 'complete') {
|
| 268 |
+
currentTaskId = taskId;
|
| 269 |
+
startTime = Date.now();
|
| 270 |
+
document.getElementById('progress-bar').classList.add('visible');
|
| 271 |
+
updateProgress(data.phase || 'complete');
|
| 272 |
+
await loadResult();
|
| 273 |
+
} else if (data.status === 'error') {
|
| 274 |
+
showError(data.error || 'Task failed');
|
| 275 |
+
} else {
|
| 276 |
+
currentTaskId = taskId;
|
| 277 |
+
startTime = Date.now();
|
| 278 |
+
document.getElementById('progress-bar').classList.add('visible');
|
| 279 |
+
updateProgress(data.phase || 'starting');
|
| 280 |
+
startElapsed();
|
| 281 |
+
pollStatus();
|
| 282 |
+
}
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
loadTaskList();
|
| 286 |
+
setInterval(loadTaskList, 5000);
|
| 287 |
+
</script>
|
| 288 |
+
</body>
|
| 289 |
+
</html>
|