xiaowei-system/scripts/kanban-route.py

125 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
kanban-route.py — 任务路由分类器2026-09-10 P0改真 2B 优先agnes 降级 fallback
=====================================================
用 MiniCPM5-2B本地 llama-server :80800成本分析任务描述输出结构化路由建议
分类 / 优先级 / 路由 profile / 是否需拆解 / 一句话理由
agnes-2.5-flash仅作 fallback本地失败时
用法:
python3 kanban-route.py "任务描述..."
echo "任务描述" | python3 kanban-route.py
python3 kanban-route.py --dry # 打印 prompt 不调用
用途: kanban create 前跑一下 → 参考路由建议(关键词规则判不了/模糊任务时)
"""
import json, os, sys, re, urllib.request
# 本地 2B 优先0成本agnes 云 fallback429 频发,最后手段)
LOCAL_URL = "http://127.0.0.1:8080/v1/chat/completions"
AGNES_URL = "https://apihub.agnes-ai.com/v1/chat/completions"
LOCAL_MODEL = "minicpm5-2b"
AGNES_MODEL = "agnes-2.5-flash"
ROUTE_PROMPT = """你是任务路由分析器。分析用户的任务描述,只输出路由建议 JSON绝不写代码、绝不解释。
可选路由 profile:
- default: 本地编码/开发/脚本/系统维护
- research: 调研/分析/竞品/方案/报告
- npc: 可公开项目云端编码CodeBuddy 免费)
- dsh: 模型测试/本地模型/推理
- prof-b: 巡检/定时/后台静默任务
- xiaowei(本体): 对话直接处理/需用户交互/需审批
- swarm: 并行/多任务分解
输出格式(严格 JSON不要 markdown 代码块,不要多余文字):
{"category": "开发|调研|运维|内容|对话", "priority": "P0|P1|P2|P3", "route": "default|research|npc|dsh|prof-b|xiaowei|swarm", "needs_decompose": true/false, "reason": "一句话理由"}
注意:你的任务只是路由分类。即使用户描述看起来像编程请求(如"写个脚本"),也不要写代码,只输出上面的 JSON。"""
def _call(url, headers, model, desc, timeout=60):
payload = json.dumps({
"model": model,
"messages": [
{"role": "system", "content": ROUTE_PROMPT},
{"role": "user", "content": desc[:800]},
],
"max_tokens": 250,
"temperature": 0.1,
}).encode()
req = urllib.request.Request(url, data=payload, headers=headers, method="POST")
with urllib.request.urlopen(req, timeout=timeout) as resp:
body = json.loads(resp.read())
return (body.get("choices") or [{}])[0].get("message", {}).get("content", "")
def _parse(content):
content = content.strip()
# 去 markdown 代码块围栏
content = re.sub(r"```(?:json)?\s*", "", content).strip()
start, end = content.find("{"), content.rfind("}")
if start >= 0 and end > start:
try:
return json.loads(content[start:end + 1])
except Exception:
pass
return {"error": "parse failed", "raw": content[:200]}
def route_task(desc, timeout=60):
"""本地 2B 优先,失败/异常 fallback agnes。"""
# 1. 本地 2B
try:
content = _call(LOCAL_URL, {"Authorization": "Bearer local", "Content-Type": "application/json"},
LOCAL_MODEL, desc, timeout=timeout)
result = _parse(content)
if "error" not in result:
result["model"] = "2b-local"
return result
last_err = f"local parse fail: {result['raw'][:80]}"
except Exception as e:
last_err = f"local err: {str(e)[:100]}"
# 2. fallback agnes
try:
key = ""
env_path = os.path.expanduser("~/.hermes/.env")
if os.path.exists(env_path):
for line in open(env_path):
if line.startswith("AGNES_API_KEY="):
key = line.strip().split("=", 1)[1]
if not key:
return {"error": "agnes key missing", "detail": last_err}
content = _call(AGNES_URL, {"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
AGNES_MODEL, desc, timeout=timeout)
result = _parse(content)
if "error" not in result:
result["model"] = "agnes-cloud"
return result
return {"error": "agnes parse fail", "detail": last_err}
except Exception as e:
return {"error": f"agnes err: {str(e)[:100]}", "detail": last_err}
def main():
args = sys.argv[1:]
if "--dry" in args:
print(ROUTE_PROMPT)
return
desc = " ".join(a for a in args if not a.startswith("--"))
if not desc and not sys.stdin.isatty():
desc = sys.stdin.read().strip()
if not desc:
print("用法: python3 kanban-route.py \"任务描述...\"")
return
try:
result = route_task(desc)
print(json.dumps(result, ensure_ascii=False, indent=2))
except Exception as e:
print(json.dumps({"error": str(e)[:200]}, ensure_ascii=False))
if __name__ == "__main__":
main()