流程节点完善
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -7,7 +7,62 @@ class APIClient:
|
||||
self.base_url = base_url
|
||||
self.endpoint = "/generate_plan"
|
||||
|
||||
def send_request(self, prompt, timeout=60):
|
||||
def send_debug_stage(
|
||||
self, prompt: str, drone_state: str = "on_ground", target_stage: int = 1, timeout=120
|
||||
):
|
||||
"""
|
||||
Stage 分阶段调试请求。
|
||||
target_stage: 1-6
|
||||
Returns: 同 send_request 结构,data 为 debug 端返回的 {target_stage, stage_name, output}
|
||||
"""
|
||||
url = f"{self.base_url}/debug_stage"
|
||||
payload = {
|
||||
"user_prompt": prompt,
|
||||
"drone_state": drone_state,
|
||||
"target_stage": target_stage,
|
||||
}
|
||||
headers = {"Content-Type": "application/json"}
|
||||
start_time = time.time()
|
||||
try:
|
||||
response = requests.post(url, json=payload, headers=headers, timeout=timeout)
|
||||
latency = time.time() - start_time
|
||||
response.raise_for_status()
|
||||
try:
|
||||
data = response.json()
|
||||
if "error" in data and len(data) == 1:
|
||||
return {
|
||||
"success": False,
|
||||
"data": data,
|
||||
"latency": latency,
|
||||
"error": data["error"],
|
||||
"http_status": response.status_code,
|
||||
}
|
||||
return {
|
||||
"success": True,
|
||||
"data": data,
|
||||
"latency": latency,
|
||||
"error": None,
|
||||
"http_status": response.status_code,
|
||||
}
|
||||
except json.JSONDecodeError:
|
||||
return {
|
||||
"success": False,
|
||||
"data": None,
|
||||
"latency": latency,
|
||||
"error": f"Invalid JSON: {response.text[:200]}",
|
||||
"http_status": response.status_code,
|
||||
}
|
||||
except requests.exceptions.RequestException as e:
|
||||
latency = time.time() - start_time
|
||||
return {
|
||||
"success": False,
|
||||
"data": None,
|
||||
"latency": latency,
|
||||
"error": str(e),
|
||||
"http_status": getattr(e.response, "status_code", None) if hasattr(e, "response") and e.response is not None else None,
|
||||
}
|
||||
|
||||
def send_request(self, prompt, drone_state="on_ground", timeout=60):
|
||||
"""
|
||||
Sends a request to the API and returns a structured result.
|
||||
Returns:
|
||||
@@ -20,7 +75,7 @@ class APIClient:
|
||||
}
|
||||
"""
|
||||
url = f"{self.base_url}{self.endpoint}"
|
||||
payload = {"user_prompt": prompt}
|
||||
payload = {"user_prompt": prompt, "drone_state": drone_state}
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
259
tools/test_validate/modules/stage_debugger.py
Normal file
259
tools/test_validate/modules/stage_debugger.py
Normal file
@@ -0,0 +1,259 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Stage 分阶段调试模块:支持单条指令调试与批量指令调试。
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import csv
|
||||
import time
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from .api_client import APIClient
|
||||
from .visualizer import generate_visualization, sanitize_filename
|
||||
|
||||
# Stage 名称映射
|
||||
STAGE_NAMES = {
|
||||
1: "Stage1 任务理解 (TaskUnderstanding)",
|
||||
2: "Stage2 上下文绑定 (ContextBinding)",
|
||||
3: "Stage3 宏观规划 (BTDraft)",
|
||||
4: "Stage4 中间层解析 (MiddlewareResolution)",
|
||||
5: "Stage5 微观填参 (MicroFilling)",
|
||||
6: "Stage6 校验与后处理 (ValidateAndPostprocess)",
|
||||
}
|
||||
|
||||
|
||||
def _select_stage():
|
||||
"""交互式选择调试 stage"""
|
||||
print("\n可选 Stage:")
|
||||
for k, v in STAGE_NAMES.items():
|
||||
print(f" {k}. {v}")
|
||||
while True:
|
||||
try:
|
||||
choice = int(input("请选择 Stage [1-6]: ").strip())
|
||||
if 1 <= choice <= 6:
|
||||
return choice
|
||||
except ValueError:
|
||||
pass
|
||||
print("❌ 请输入 1-6 之间的数字")
|
||||
|
||||
|
||||
def _infer_drone_state_from_prompt(prompt: str) -> str:
|
||||
"""从指令中简单推断 drone_state(批量模式下若未单独指定则用此)"""
|
||||
if "在空中" in prompt or "已起飞" in prompt:
|
||||
return "in_air"
|
||||
return "on_ground"
|
||||
|
||||
|
||||
def run_stage_debug_single():
|
||||
"""单条指令 Stage 调试"""
|
||||
client = APIClient()
|
||||
print("\n🚀 进入 Stage 分阶段调试 - 单条模式 (输入 'exit' 或 'q' 退出)")
|
||||
|
||||
while True:
|
||||
try:
|
||||
prompt = input("\n请输入测试指令: ").strip()
|
||||
if prompt.lower() in ("exit", "q"):
|
||||
break
|
||||
if not prompt:
|
||||
continue
|
||||
|
||||
target_stage = _select_stage()
|
||||
drone_state = input("无人机状态 [on_ground/in_air,回车默认根据指令推断]: ").strip()
|
||||
if not drone_state:
|
||||
drone_state = _infer_drone_state_from_prompt(prompt)
|
||||
|
||||
print(f"\n⏳ 运行至 {STAGE_NAMES[target_stage]}...")
|
||||
result = client.send_debug_stage(
|
||||
prompt=prompt,
|
||||
drone_state=drone_state,
|
||||
target_stage=target_stage,
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
print(f"✅ 请求成功 (耗时: {result['latency']:.2f}s)")
|
||||
data = result["data"]
|
||||
output = data.get("output", data)
|
||||
print("\n" + "=" * 60)
|
||||
print(json.dumps(output, ensure_ascii=False, indent=2))
|
||||
print("=" * 60)
|
||||
|
||||
# 保存到 validation/temporary
|
||||
base_dir = os.path.dirname(os.path.dirname(__file__))
|
||||
safe_name = sanitize_filename(prompt)[:80]
|
||||
out_dir = os.path.join(
|
||||
base_dir, "validation", "temporary", f"stage{target_stage}_{safe_name}"
|
||||
)
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
out_path = os.path.join(out_dir, "response.json")
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
json.dump(result["data"], f, indent=2, ensure_ascii=False)
|
||||
print(f"\n📂 结果已保存: {out_path}")
|
||||
|
||||
# Stage 6 输出含 root 时生成可视化
|
||||
if target_stage == 6 and isinstance(output, dict) and "root" in output:
|
||||
png_path = os.path.join(out_dir, "plan.png")
|
||||
if generate_visualization(output["root"], png_path):
|
||||
print(f"🖼️ 可视化: {png_path}")
|
||||
else:
|
||||
print(f"❌ 请求失败: {result['error']}")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n已取消")
|
||||
break
|
||||
|
||||
print("\n已返回 Stage 调试菜单")
|
||||
|
||||
|
||||
def run_stage_debug_batch():
|
||||
"""批量指令 Stage 调试(参考 batch_runner)"""
|
||||
base_dir = os.path.dirname(os.path.dirname(__file__))
|
||||
instr_dir = os.path.join(base_dir, "instructions")
|
||||
files = [f for f in os.listdir(instr_dir) if f.endswith(".txt")]
|
||||
|
||||
if not files:
|
||||
print("❌ 未在 instructions 目录下找到 .txt 文件")
|
||||
return
|
||||
|
||||
print("\n请选择测试指令文件:")
|
||||
for i, f in enumerate(files):
|
||||
print(f" {i+1}. {f}")
|
||||
try:
|
||||
idx = int(input("请输入序号: ").strip()) - 1
|
||||
if idx < 0 or idx >= len(files):
|
||||
print("❌ 无效序号")
|
||||
return
|
||||
selected_file = os.path.join(instr_dir, files[idx])
|
||||
except ValueError:
|
||||
print("❌ 输入无效")
|
||||
return
|
||||
|
||||
target_stage = _select_stage()
|
||||
drone_state_override = input(
|
||||
"无人机状态 [回车=每条根据指令推断, on_ground/in_air=全局固定]: "
|
||||
).strip()
|
||||
if drone_state_override and drone_state_override not in ("on_ground", "in_air"):
|
||||
drone_state_override = ""
|
||||
|
||||
try:
|
||||
iterations = int(input("每条指令测试次数 (默认1): ").strip() or "1")
|
||||
except ValueError:
|
||||
iterations = 1
|
||||
|
||||
# 输出目录
|
||||
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
||||
output_dir = os.path.join(
|
||||
base_dir, "validation", f"stage{target_stage}_debug_{timestamp}"
|
||||
)
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
shutil.copy(selected_file, os.path.join(output_dir, "instructions_backup.txt"))
|
||||
|
||||
with open(selected_file, "r", encoding="utf-8") as f:
|
||||
instructions = [
|
||||
line.strip()
|
||||
for line in f
|
||||
if line.strip() and not line.startswith("#")
|
||||
]
|
||||
|
||||
print(
|
||||
f"\n🚀 开始批量 Stage{target_stage} 调试 (共 {len(instructions)} 条, 每条 {iterations} 次)"
|
||||
)
|
||||
print(f"📂 输出: {output_dir}\n")
|
||||
|
||||
client = APIClient()
|
||||
detailed_results = []
|
||||
summary_stats = {}
|
||||
|
||||
for i, prompt in enumerate(instructions, 1):
|
||||
drone_state = (
|
||||
drone_state_override
|
||||
if drone_state_override
|
||||
else _infer_drone_state_from_prompt(prompt)
|
||||
)
|
||||
print(f"[{i}/{len(instructions)}] {prompt[:40]}... (state={drone_state})")
|
||||
|
||||
safe_name = sanitize_filename(prompt)
|
||||
instr_out_dir = os.path.join(output_dir, safe_name)
|
||||
os.makedirs(instr_out_dir, exist_ok=True)
|
||||
|
||||
success_count = 0
|
||||
total_latency = 0
|
||||
|
||||
for k in range(1, iterations + 1):
|
||||
print(f" - 第 {k} 次...", end="", flush=True)
|
||||
result = client.send_debug_stage(
|
||||
prompt=prompt,
|
||||
drone_state=drone_state,
|
||||
target_stage=target_stage,
|
||||
)
|
||||
|
||||
detailed_results.append(
|
||||
{
|
||||
"instruction": prompt,
|
||||
"run_id": k,
|
||||
"success": result["success"],
|
||||
"latency": result["latency"],
|
||||
"error": result.get("error") or "",
|
||||
}
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
print(f" ✅ ({result['latency']:.2f}s)")
|
||||
success_count += 1
|
||||
total_latency += result["latency"]
|
||||
|
||||
out_path = os.path.join(instr_out_dir, f"{k}.json")
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
json.dump(result["data"], f, indent=2, ensure_ascii=False)
|
||||
|
||||
# Stage6 且含 root 时生成可视化
|
||||
data = result["data"]
|
||||
output = data.get("output", data) if isinstance(data, dict) else {}
|
||||
if (
|
||||
target_stage == 6
|
||||
and isinstance(output, dict)
|
||||
and "root" in output
|
||||
):
|
||||
generate_visualization(
|
||||
output["root"],
|
||||
os.path.join(instr_out_dir, f"{k}.png"),
|
||||
)
|
||||
else:
|
||||
print(f" ❌ {result.get('error', 'Unknown')}")
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
avg_lat = total_latency / success_count if success_count > 0 else 0
|
||||
summary_stats[prompt] = {
|
||||
"total_runs": iterations,
|
||||
"success_runs": success_count,
|
||||
"success_rate": f"{(success_count / iterations) * 100:.1f}%",
|
||||
"avg_latency": f"{avg_lat:.2f}s",
|
||||
}
|
||||
|
||||
# 报告
|
||||
with open(os.path.join(output_dir, "test_details.csv"), "w", newline="", encoding="utf-8") as f:
|
||||
writer = csv.DictWriter(
|
||||
f, fieldnames=["instruction", "run_id", "success", "latency", "error"]
|
||||
)
|
||||
writer.writeheader()
|
||||
writer.writerows(detailed_results)
|
||||
|
||||
with open(os.path.join(output_dir, "test_summary.csv"), "w", newline="", encoding="utf-8") as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(
|
||||
["Instruction", "Total Runs", "Success Runs", "Success Rate", "Avg Latency"]
|
||||
)
|
||||
for prompt, stats in summary_stats.items():
|
||||
writer.writerow(
|
||||
[
|
||||
prompt,
|
||||
stats["total_runs"],
|
||||
stats["success_runs"],
|
||||
stats["success_rate"],
|
||||
stats["avg_latency"],
|
||||
]
|
||||
)
|
||||
|
||||
print(f"\n✅ Stage{target_stage} 批量调试完成! 报告: {output_dir}")
|
||||
Reference in New Issue
Block a user