orchestrator.py 4.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. # -*- coding: utf-8 -*-
  2. """调度器: 模型做后台调度 (出 JSON 计划: 调哪些插件) → 确定性执行 → 模型只把事实组织成话 (接地闸) → 前端. 无模型时: 规则调度 + 结构化答案."""
  3. import json, re
  4. from . import llm # 随迁件
  5. from src.windcms import plugins # 仍留 src.windcms
  6. from src.windcms.config import SENSOR_CN
  7. SYSTEM = ('你是风电 CMS 振动诊断系统的后台调度员。你只能通过插件取事实, 判断由插件(确定性算法)给出, 你不判断、不编造。'
  8. '回答只能使用插件返回的事实与知识库原文里的台号和数字; 出处要带插件名或文档名。'
  9. '判级只认六枚举 (定论/准定论·预警/候选/参考/INSUFFICIENT/撤回); P0/P1 不归本系统。')
  10. def _rule_plan(q):
  11. """无模型时的规则调度: 抓台号/测点/意图."""
  12. t = re.findall(r'WTG\s?(\d{1,2})|(\d{1,2})\s?(?:[#号]|(?=\s*(?:和|与|vs|,|,|、)))', q)
  13. tids = []
  14. for a, b in t:
  15. v = a or b
  16. if v and v not in tids:
  17. tids.append(v)
  18. tid = tids[0] if tids else None
  19. sen = next((k for k, v in SENSOR_CN.items() if v in q or k.lower() in q.lower()), None)
  20. ql = q.lower()
  21. meas = 'rms_200' if '200' in q else ('Rms_HP' if 'rms' in ql else ('Kurtosis' if '峭度' in q else 'Peak'))
  22. plan = []
  23. if tid:
  24. plan.append(dict(plugin='turbine_summary', args=dict(turbine=tid)))
  25. if len(tids) >= 2 and sen:
  26. plan.append(dict(plugin='compare', args=dict(turbines=','.join(f'WTG{int(x):02d}' for x in tids), sensor=sen, meas=meas)))
  27. elif tid and sen:
  28. plan.append(dict(plugin='scalar_trend', args=dict(turbine=tid, sensor=sen, meas=meas)))
  29. if tid and sen and ('谱' in q or '特征' in q or 'hz' in ql):
  30. plan.append(dict(plugin='spectrum', args=dict(turbine=tid, sensor=sen)))
  31. if ('排名' in q or '全场' in q or '哪些台' in q) and sen:
  32. plan.append(dict(plugin='fleet_rank', args=dict(sensor=sen, meas='Peak')))
  33. plan.append(dict(plugin='kb_search', args=dict(query=q, k=4)))
  34. return plan
  35. def plan(q, model=None):
  36. man = plugins.manifest()
  37. prompt = (f'用户问题: {q}\n\n可用插件 (JSON):\n{json.dumps(man, ensure_ascii=False)}\n\n'
  38. '输出一个 JSON 数组 (不要别的文字), 每项 {"plugin": 名, "args": {...}}, 最多 4 项, 至少包含一个 kb_search; '
  39. '台号写 WTGxx, 测点用中文名 (主轴承前/主轴承后/行星级/中间轴/高速轴转子侧/高速轴发电机侧/发电机DE/发电机NDE)。')
  40. txt, m = llm.generate(prompt, model=model, system=SYSTEM, temperature=0.0, num_predict=400)
  41. if txt:
  42. try:
  43. j = re.search(r'\[.*\]', txt, flags=re.S).group(0)
  44. p = [x for x in json.loads(j) if isinstance(x, dict) and x.get('plugin') in plugins.REGISTRY][:4]
  45. if p:
  46. return p, m
  47. except Exception:
  48. pass
  49. return _rule_plan(q), m
  50. _CTX = {}
  51. def ctx(cfg):
  52. k = str(cfg['out'])
  53. if k not in _CTX:
  54. _CTX[k] = plugins.context(cfg)
  55. return _CTX[k]
  56. def ask(cfg, q, model=None):
  57. c = ctx(cfg)
  58. p, m = plan(q, model)
  59. results = [dict(plugin=s['plugin'], args=s.get('args', {}), **plugins.run(c, s['plugin'], **s.get('args', {}))) for s in p]
  60. facts = '\n\n'.join(f'### 插件 {r["plugin"]} {json.dumps(r["args"], ensure_ascii=False)}\n{r.get("text", "")}' for r in results)
  61. prompt = (f'用户问题: {q}\n\n事实 (唯一来源, 不许增删台号/数字/结论):\n{facts}\n\n'
  62. '用平实中文给现场/业主写答复: 一句话结论(级别配人话注解: 候选=疑似异常需现场核实, 准定论·预警=基本确认建议尽快处置, 候选·记基线=已记为基线暂不需行动, 参考=轻微偏离记录观察, INSUFFICIENT=数据不足, 撤回=结论已取消; 级别名逐字照抄不得增删字) + 要点(数字+含义, 正文禁内部代号 env/×fleet/G2 与英文通道名) + 末行来源(中文插件名); 事实不足明说')
  63. txt, m2 = llm.generate(prompt, model=model, system=SYSTEM, temperature=0.2, num_predict=600)
  64. answer, source = facts, 'structured'
  65. if txt:
  66. ok, bad = llm.grounding(txt, facts)
  67. if ok:
  68. answer, source = txt, f'llm:{m2}'
  69. else:
  70. answer, source = facts + f'\n\n[LLM 叙述被接地闸拦截: 引入了事实外的 {bad}; 以上为结构化权威版]', 'llm_blocked'
  71. return dict(question=q, plan=p, results=results, answer=answer, source=source, model=m2)