tools.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. """基于 HelloAgents 0.2.9 Tool 协议的确定性检查工具。"""
  2. from __future__ import annotations
  3. import io
  4. import json
  5. import re
  6. from contextlib import redirect_stdout
  7. from typing import Any
  8. from hello_agents.tools import Tool, ToolParameter, ToolRegistry
  9. REQUIREMENT_DIMENSIONS: dict[str, tuple[tuple[str, ...], str]] = {
  10. "目标与价值": (
  11. ("目标", "希望", "解决", "价值", "为了", "痛点"),
  12. "这个需求要解决什么问题,成功后产生什么价值?",
  13. ),
  14. "目标用户": (
  15. ("用户", "成员", "客户", "管理员", "居民", "工作人员", "面向", "使用者"),
  16. "谁会使用系统?不同角色分别能做什么?",
  17. ),
  18. "核心范围": (
  19. ("功能", "支持", "可以", "需要", "浏览", "发布", "报名", "管理"),
  20. "首个版本必须包含和明确不包含哪些功能?",
  21. ),
  22. "约束条件": (
  23. ("预算", "成本", "时间", "上线", "周期", "技术栈", "平台", "中文"),
  24. "交付时间、预算、平台或技术栈有哪些硬约束?",
  25. ),
  26. "数据与集成": (
  27. ("数据", "数据库", "接口", "api", "导入", "导出", "第三方", "同步"),
  28. "需要保存哪些数据,并与哪些现有系统或第三方服务集成?",
  29. ),
  30. "非功能需求": (
  31. ("并发", "性能", "安全", "隐私", "可用性", "响应时间", "人数", "容量"),
  32. "对性能、容量、安全、隐私和可用性有什么要求?",
  33. ),
  34. "验收标准": (
  35. ("验收", "成功标准", "通过", "指标", "完成标准", "可演示"),
  36. "哪些可观察、可测试的条件满足后可以验收?",
  37. ),
  38. }
  39. REQUIRED_REPORT_HEADINGS = (
  40. "1. 需求摘要",
  41. "2. 已确认信息",
  42. "3. 待确认问题",
  43. "4. 范围与优先级",
  44. "5. 技术方案",
  45. "6. 风险与对策",
  46. "7. 验收标准",
  47. "8. 下一步行动",
  48. )
  49. class RequirementAuditTool(Tool):
  50. """扫描原始需求覆盖了哪些关键信息维度。"""
  51. def __init__(self) -> None:
  52. super().__init__(
  53. name="requirement_audit",
  54. description=(
  55. "检查需求文本的完整度,返回已覆盖维度、缺失维度和澄清问题;"
  56. "参数名为 requirement_text"
  57. ),
  58. )
  59. def get_parameters(self) -> list[ToolParameter]:
  60. return [
  61. ToolParameter(
  62. name="requirement_text",
  63. type="string",
  64. description="需要检查的原始需求文本",
  65. required=True,
  66. )
  67. ]
  68. def run(self, parameters: dict[str, Any]) -> str:
  69. requirement_text = parameters.get(
  70. "requirement_text", parameters.get("input", "")
  71. )
  72. if not isinstance(requirement_text, str) or not requirement_text.strip():
  73. return json.dumps(
  74. {
  75. "ok": False,
  76. "error_code": "INVALID_PARAM",
  77. "message": "requirement_text 必须是非空字符串",
  78. },
  79. ensure_ascii=False,
  80. )
  81. normalized = requirement_text.casefold()
  82. covered: list[str] = []
  83. missing: list[str] = []
  84. evidence: dict[str, list[str]] = {}
  85. questions: list[str] = []
  86. for dimension, (keywords, question) in REQUIREMENT_DIMENSIONS.items():
  87. hits = [keyword for keyword in keywords if keyword.casefold() in normalized]
  88. if hits:
  89. covered.append(dimension)
  90. evidence[dimension] = hits
  91. else:
  92. missing.append(dimension)
  93. questions.append(question)
  94. total = len(REQUIREMENT_DIMENSIONS)
  95. coverage = round(len(covered) / total * 100)
  96. summary = (
  97. f"需求完整度初检:{coverage}%({len(covered)}/{total} 个维度)。\n"
  98. f"已覆盖:{'、'.join(covered) if covered else '无'}。\n"
  99. f"待补充:{'、'.join(missing) if missing else '无'}。"
  100. )
  101. return json.dumps(
  102. {
  103. "ok": True,
  104. "summary": summary,
  105. "coverage_percent": coverage,
  106. "covered_dimensions": covered,
  107. "missing_dimensions": missing,
  108. "evidence_keywords": evidence,
  109. "clarifying_questions": questions,
  110. },
  111. ensure_ascii=False,
  112. indent=2,
  113. )
  114. class ReportQualityTool(Tool):
  115. """检查最终报告是否包含模板规定的八个核心章节。"""
  116. def __init__(self) -> None:
  117. super().__init__(
  118. name="report_quality_check",
  119. description=(
  120. "检查需求澄清报告的章节完整性、章节内容和待确认标记;"
  121. "参数名为 report_text"
  122. ),
  123. )
  124. def get_parameters(self) -> list[ToolParameter]:
  125. return [
  126. ToolParameter(
  127. name="report_text",
  128. type="string",
  129. description="Markdown 格式的需求澄清报告",
  130. required=True,
  131. )
  132. ]
  133. def run(self, parameters: dict[str, Any]) -> str:
  134. report_text = parameters.get("report_text", parameters.get("input", ""))
  135. if not isinstance(report_text, str) or not report_text.strip():
  136. return json.dumps(
  137. {
  138. "ok": False,
  139. "error_code": "INVALID_PARAM",
  140. "message": "report_text 必须是非空字符串",
  141. },
  142. ensure_ascii=False,
  143. )
  144. heading_matches = list(
  145. re.finditer(r"^##\s+(.+?)\s*$", report_text, flags=re.MULTILINE)
  146. )
  147. headings = {match.group(1).strip() for match in heading_matches}
  148. missing = [heading for heading in REQUIRED_REPORT_HEADINGS if heading not in headings]
  149. section_content: dict[str, str] = {}
  150. for index, match in enumerate(heading_matches):
  151. heading = match.group(1).strip()
  152. content_end = (
  153. heading_matches[index + 1].start()
  154. if index + 1 < len(heading_matches)
  155. else len(report_text)
  156. )
  157. section_content[heading] = report_text[match.end() : content_end].strip()
  158. empty = [
  159. heading
  160. for heading in REQUIRED_REPORT_HEADINGS
  161. if heading in headings and not section_content.get(heading)
  162. ]
  163. body_without_headings = re.sub(
  164. r"^#{1,6}\s+.*$", "", report_text, flags=re.MULTILINE
  165. )
  166. has_pending_markers = any(
  167. marker in body_without_headings for marker in ("待确认", "假设", "建议")
  168. )
  169. total = len(REQUIRED_REPORT_HEADINGS)
  170. heading_score = (total - len(missing)) / total * 50
  171. content_score = (total - len(missing) - len(empty)) / total * 40
  172. score = round(
  173. heading_score + content_score + (10 if has_pending_markers else 0)
  174. )
  175. summary = (
  176. f"报告结构评分:{score}/100。"
  177. + (f" 缺少章节:{'、'.join(missing)}。" if missing else " 八个章节齐全。")
  178. + (f" 空章节:{'、'.join(empty)}。" if empty else " 章节均有内容。")
  179. + (" 已区分待确认信息。" if has_pending_markers else " 未发现待确认/假设/建议标记。")
  180. )
  181. return json.dumps(
  182. {
  183. "ok": True,
  184. "summary": summary,
  185. "score": score,
  186. "missing_headings": missing,
  187. "empty_headings": empty,
  188. "has_pending_markers": has_pending_markers,
  189. },
  190. ensure_ascii=False,
  191. indent=2,
  192. )
  193. def create_tool_registry() -> ToolRegistry:
  194. """创建并注册项目所需的 HelloAgents 工具。"""
  195. registry = ToolRegistry()
  196. # 0.2.9 注册时会打印包含 emoji 的日志;Windows GBK 终端可能编码失败。
  197. with redirect_stdout(io.StringIO()):
  198. registry.register_tool(RequirementAuditTool())
  199. registry.register_tool(ReportQualityTool())
  200. return registry