#!/usr/bin/env python3 # -*- coding: utf-8 -*- """作业 2 · 步骤 4:三句验收(知识库 / 工单 / 人工)。""" from __future__ import annotations import json import sys import _bootstrap # noqa: F401 import _config as C from dify_api import Dify, pp from 作业2_02_创建工作流空壳 import find_app def parse_sse(text: str) -> dict: """从 Dify workflow 流式响应里抽出分类名、结束输出、错误。""" class_name = "" answer = "" errors: list[str] = [] node_titles: list[str] = [] for raw in text.splitlines(): line = raw.strip() if line.startswith("data:"): line = line[5:].strip() if not line.startswith("{"): continue try: ev = json.loads(line) except json.JSONDecodeError: continue event = ev.get("event") or "" data = ev.get("data") or ev title = (data.get("title") or data.get("node_type") or "") if isinstance(data, dict) else "" if event in ("node_started", "node_finished") and title: node_titles.append("%s:%s" % (event, title)) if event == "node_finished": outputs = data.get("outputs") or {} if outputs.get("class_name"): class_name = str(outputs.get("class_name")) if outputs.get("class_label") and not class_name: class_name = str(outputs.get("class_label")) if outputs.get("intent"): class_name = str(outputs.get("intent")) err = data.get("error") if err: errors.append("%s: %s" % (title or data.get("node_id"), err)) if event in ("workflow_finished", "workflow_succeeded"): outs = (data.get("outputs") or {}) if isinstance(data, dict) else {} if outs.get("answer"): answer = str(outs.get("answer")) elif outs: answer = json.dumps(outs, ensure_ascii=False) if event == "error": errors.append(str(data)) if not answer: # 兜底:全文里找 WO- / 转人工 / 请假 answer = text[-4000:] return { "class_name": class_name, "answer": answer, "errors": errors, "node_titles": node_titles[-30:], } def run_one(d: Dify, app_id: str, query: str) -> dict: r = d.post( "/apps/%s/workflows/draft/run" % app_id, body={"inputs": {"query": query}, "files": []}, extra="-m 420 --max-time 420", t=450, ) parsed = parse_sse(r.get("text") or "") parsed["http"] = r.get("code") parsed["raw_head"] = (r.get("text") or "")[:800] return parsed def main() -> int: cases = [ ( C.HOMEWORK2_PROBE_KB, "知识库问答", ("请假", "知识库", "制度", "流程", "hr", "行政"), ("知识库检索", "生成回答"), ), ( C.HOMEWORK2_PROBE_TICKET, "工单报修", ("WO-", "工单", "报修"), ("调用工单API", "返回工单号"), ), ( C.HOMEWORK2_PROBE_HUMAN, "人工客服", ("转接", "人工", "投诉"), ("转人工提示",), ), ] d = Dify(C.DIFY_EMAIL, C.DIFY_PASSWORD) try: d.login() found = find_app(d) if not found: print("找不到应用") return 1 app_id = found["id"] print("app_id", app_id) failed = 0 for query, expect_cls, markers, path_nodes in cases: print("\n======== 问句:", query, "期望分类:", expect_cls) parsed = run_one(d, app_id, query) print("http", parsed["http"]) print("class_name", parsed["class_name"]) print("errors", parsed["errors"][:3]) print("nodes", parsed["node_titles"]) ans = parsed["answer"] or "" print("answer_head", ans[:500].replace("\n", " / ")) hits = [m for m in markers if m.lower() in ans.lower() or m in (parsed["class_name"] or "")] cls_ok = expect_cls in (parsed["class_name"] or "") or expect_cls in ans path_ok = any(n in str(parsed["node_titles"]) for n in path_nodes) ok = parsed["http"] in ("200",) and (cls_ok or path_ok) and (len(hits) >= 1 or cls_ok) if not parsed["errors"] and ok: print("[PASS]") else: print("[FAIL] hits=", hits, "cls_ok=", cls_ok, "path_ok=", path_ok) failed += 1 print("raw_head", parsed["raw_head"][:800]) if failed: print("\n[FAIL] %d / %d 未通过" % (failed, len(cases))) return 1 print("\n[PASS] 三句验收均走对应路径。") return 0 finally: d.close() if __name__ == "__main__": sys.exit(main())