ask.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. """CLI 交互式 Agent 问答(含 human-in-the-loop 确认)。
  2. 用法:
  3. uv run python scripts/ask.py # 交互模式(多轮,可确认/缩小范围)
  4. uv run python scripts/ask.py "问题" --auto # 单次自动确认(批处理/评测用)
  5. """
  6. from __future__ import annotations
  7. import argparse
  8. import json
  9. import sys
  10. import time
  11. from langgraph.checkpoint.memory import InMemorySaver
  12. from langgraph.types import Command
  13. def _make_checkpointer():
  14. """上下文记忆:当前存内存(InMemorySaver,进程内多轮有效)。
  15. 后续持久化/图数据库化时只改这里(如 SqliteSaver / PostgresSaver / 自研存储)。"""
  16. return InMemorySaver()
  17. def _print_result(st: dict, debug: bool = False,
  18. total_sec: float | None = None,
  19. process_sec: float | None = None) -> None:
  20. print("=" * 50)
  21. print("分类:", st.get("category"))
  22. if st.get("chat_answer"):
  23. print("闲聊回复:", st["chat_answer"])
  24. _print_elapsed(total_sec, process_sec)
  25. return
  26. print("槽位:", json.dumps(st.get("slots", {}), ensure_ascii=False))
  27. print("接地:", json.dumps(st.get("grounded", {}), ensure_ascii=False))
  28. if debug and st.get("capability"):
  29. print("能力拓展:", json.dumps(st.get("capability"), ensure_ascii=False))
  30. cap_check = st.get("cap_check") or {}
  31. if cap_check:
  32. tail = cap_check.get("note") or (";".join(cap_check.get("issues", [])) if not cap_check.get("ok") else "")
  33. print(f"能力拓展检查: {'✅ 通过' if cap_check.get('ok') else '⚠️ 未通过'} {tail}")
  34. cap_llm = st.get("cap_llm_check") or {}
  35. if cap_llm:
  36. tail = cap_llm.get("reason") or cap_llm.get("note") or ""
  37. print(f"能力拓展LLM审查: {'✅ 通过' if cap_llm.get('ok') else '⚠️ 未通过'} {tail}")
  38. print("计划:", json.dumps(st.get("plan", {}), ensure_ascii=False))
  39. print("执行详情:")
  40. for s in st.get("subgraph", {}).get("steps", []):
  41. tool = s.get("tool", "")
  42. row_count = s.get("row_count", len(s.get("rows", [])))
  43. note = s.get("note") or ""
  44. dropped = s.get("dropped_params") or []
  45. print(f" {s.get('step_id')} [{tool}] 行数={row_count} "
  46. f"实参={json.dumps(s.get('executed_params', {}), ensure_ascii=False)}"
  47. + (f" 丢弃参数={json.dumps(dropped, ensure_ascii=False)}" if dropped else "")
  48. + (f" 备注={note}" if note else ""))
  49. if debug:
  50. if s.get("query"):
  51. print(f" Cypher: {s['query']}")
  52. if s.get("query_params"):
  53. print(f" 参数: {json.dumps(s['query_params'], ensure_ascii=False)}")
  54. first = s.get("rows", [])[:2]
  55. if first:
  56. print(f" 首行: {json.dumps(first, ensure_ascii=False, default=str)}")
  57. print("子图行数:", st.get("subgraph", {}).get("total_rows"))
  58. run_check = st.get("run_check") or {}
  59. if run_check:
  60. tail = ";".join(run_check.get("issues", []) + run_check.get("warns", []))
  61. print(f"执行检查: {'✅ 通过' if run_check.get('ok') else '⚠️ 未通过'} {tail}")
  62. run_llm = st.get("run_llm_check") or {}
  63. if run_llm:
  64. tail = run_llm.get("reason") or ""
  65. print(f"执行LLM审查: {'✅ 通过' if run_llm.get('ok') else '⚠️ 未通过'} {tail}")
  66. print("迭代次数:", st.get("iterations"))
  67. print("-" * 50)
  68. if st.get("reuse") and not st.get("need_full_query", True):
  69. print("回答:(基于前序对话推导)", st.get("answer"))
  70. else:
  71. print("回答:", st.get("answer"))
  72. _print_elapsed(total_sec, process_sec)
  73. print("=" * 50)
  74. def _print_elapsed(total_sec: float | None, process_sec: float | None) -> None:
  75. if total_sec is not None:
  76. wait_sec = max(total_sec - (process_sec or 0.0), 0.0)
  77. print(f"耗时: 总 {total_sec:.1f}s(处理 {process_sec or 0.0:.1f}s / 确认等待 {wait_sec:.1f}s)")
  78. def _run_once(graph, question: str, thread_id: str, auto: bool, user_id: str, debug: bool = False) -> None:
  79. t0 = time.monotonic()
  80. process_sec = 0.0
  81. config = {"configurable": {"thread_id": thread_id}}
  82. t1 = time.monotonic()
  83. result = graph.invoke({"question": question, "user_id": user_id}, config)
  84. process_sec += time.monotonic() - t1
  85. while "__interrupt__" in result:
  86. intr = result["__interrupt__"][0]
  87. payload = intr.value
  88. print("\n[需要确认]", payload.get("message"))
  89. if payload.get("options"):
  90. for i, o in enumerate(payload["options"], 1):
  91. print(f" {i}. {o}")
  92. if auto and payload.get("type") != "clarify_value":
  93. default = "确认" if payload.get("type") in ("confirm_slots", "confirm_plan") else "继续返回全部"
  94. print(f"(自动确认: {default})")
  95. reply = default
  96. else:
  97. reply = input("你的回复 > ").strip()
  98. t1 = time.monotonic()
  99. result = graph.invoke(Command(resume=reply), config)
  100. process_sec += time.monotonic() - t1
  101. _print_result(result, debug=debug, total_sec=time.monotonic() - t0, process_sec=process_sec)
  102. def main() -> None:
  103. sys.stdout.reconfigure(encoding="utf-8")
  104. ap = argparse.ArgumentParser()
  105. ap.add_argument("question", nargs="?", default=None, help="问题(不填则进入交互模式)")
  106. ap.add_argument("--auto", action="store_true", help="自动确认,跳过人工确认")
  107. ap.add_argument("--debug", action="store_true", help="显示每步实际执行的 Cypher/参数/首行(调试用)")
  108. ap.add_argument("--user", default="admin01", help="提问人身份(工号或用户名,默认 admin01)")
  109. args = ap.parse_args()
  110. from step3_qa_agent.agent.graph import build_agent_graph
  111. graph = build_agent_graph(checkpointer=_make_checkpointer())
  112. thread = "default"
  113. if args.question:
  114. _run_once(graph, args.question, thread, args.auto, args.user, args.debug)
  115. return
  116. print("申勤物业知识助手(交互模式;输入 退出/quit 结束,输入 清空/新话题 重置上下文)")
  117. while True:
  118. try:
  119. q = input("\n问题 > ").strip()
  120. except (EOFError, KeyboardInterrupt):
  121. break
  122. if not q:
  123. continue
  124. if q in ("退出", "quit", "exit"):
  125. break
  126. if q in ("清空", "新话题", "新会话", "重置", "reset", "clear", "new"):
  127. thread = f"t{time.monotonic():.3f}"
  128. print("(已清空上下文,开始新话题)")
  129. continue
  130. _run_once(graph, q, thread, args.auto, args.user, args.debug)
  131. if __name__ == "__main__":
  132. main()