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