demand_build_agent_tools.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. import json
  2. import os
  3. from pathlib import Path
  4. from typing import Any, Dict, List, Optional
  5. from agent import tool
  6. from examples.demand.demand_agent_context import TopicBuildAgentContext
  7. from examples.demand.tool_logging import log_tool_input, log_tool_output
  8. def _get_result_base_dir() -> Path:
  9. """输出到结果目录;local_json 模式可通过环境变量重定向。"""
  10. metadata_dir = TopicBuildAgentContext.get_metadata("result_base_dir")
  11. if metadata_dir:
  12. return Path(metadata_dir)
  13. redirected = os.getenv("DEMAND_RESULT_BASE_DIR")
  14. if redirected:
  15. return Path(redirected)
  16. return Path.cwd() / "result"
  17. def _normalize_evidence_refs(evidence_refs: Any) -> Dict[str, Any]:
  18. if evidence_refs is None:
  19. return {}
  20. if isinstance(evidence_refs, dict):
  21. return evidence_refs
  22. if isinstance(evidence_refs, str) and evidence_refs.strip():
  23. try:
  24. loaded = json.loads(evidence_refs)
  25. if isinstance(loaded, dict):
  26. return loaded
  27. except json.JSONDecodeError:
  28. pass
  29. return {"raw": evidence_refs}
  30. @tool(
  31. "存储需求到结果集。 - element_names - reason(原因)- desc(需求描述)- type(来源类型)- evidence_refs(候选证据引用)"
  32. )
  33. def create_demand_item(
  34. element_names: List[str] = None,
  35. reason: str = None,
  36. desc: str = None,
  37. type: str = None,
  38. evidence_refs: Optional[Dict[str, Any]] = None) -> str:
  39. """
  40. 每次调用向“execution_id 对应的本地 JSON 文件”追加一条记录。
  41. 写入对象包含以下字段:
  42. - element_names
  43. - reason(原因)
  44. - desc(需求描述)
  45. - type(来源类型)
  46. - evidence_refs(候选证据引用,后续由 orchestration/校验逻辑决定是否能升级为 evidence_pack)
  47. """
  48. execution_id: Optional[int] = TopicBuildAgentContext.get_execution_id()
  49. normalized_evidence_refs = _normalize_evidence_refs(evidence_refs)
  50. params: Dict[str, Any] = {
  51. "execution_id": execution_id,
  52. "element_names": element_names,
  53. "reason": reason,
  54. "desc": desc,
  55. "type": type,
  56. "evidence_refs": normalized_evidence_refs,
  57. }
  58. log_tool_input("create_demand_item", params)
  59. if not execution_id:
  60. return log_tool_output("create_demand_item", "错误: 未设置 execution_id")
  61. record: Dict[str, Any] = {
  62. "element_names": element_names,
  63. "reason": reason,
  64. "desc": desc,
  65. "type": type,
  66. "evidence_refs": normalized_evidence_refs,
  67. }
  68. # 按 execution_id 区分文件,避免不同执行互相污染。
  69. # 例如:result/{execution_id}/execution_id_{execution_id}_demand_items.json
  70. output_dir = _get_result_base_dir() / f"{execution_id}"
  71. output_path = output_dir / f"execution_id_{execution_id}_demand_items.json"
  72. output_path.parent.mkdir(parents=True, exist_ok=True)
  73. items: List[Dict[str, Any]] = []
  74. if output_path.exists():
  75. try:
  76. with open(output_path, "r", encoding="utf-8") as f:
  77. loaded = json.load(f)
  78. if isinstance(loaded, list):
  79. items = loaded
  80. elif isinstance(loaded, dict) and isinstance(loaded.get("items"), list):
  81. # 兼容可能的包装格式:{"items":[...]}
  82. items = loaded["items"]
  83. else:
  84. # 兜底:把已有内容当作单条记录追加
  85. items = [loaded]
  86. except json.JSONDecodeError:
  87. # 文件内容损坏时,不阻断执行;从空列表开始追加
  88. items = []
  89. items.append(record)
  90. with open(output_path, "w", encoding="utf-8") as f:
  91. json.dump(items, f, ensure_ascii=False, indent=2)
  92. result = json.dumps(
  93. {"success": True, "execution_id": execution_id, "written_to": str(output_path)},
  94. ensure_ascii=False,
  95. )
  96. return log_tool_output("create_demand_item", result)
  97. @tool(
  98. "批量存储需求到结果集。 - element_names - reason(原因)- desc(需求描述)- type(来源类型)- evidence_refs(候选证据引用)"
  99. )
  100. def create_demand_items(demand_items: List[Dict[str, Any]] = None) -> str:
  101. """
  102. 一次调用追加多条记录到“execution_id 对应的本地 JSON 文件”(JSON 数组)。
  103. 每条记录字段:
  104. - element_names
  105. - reason(原因)
  106. - desc(需求描述)
  107. - type(来源类型)
  108. - evidence_refs(候选证据引用)
  109. """
  110. execution_id: Optional[int] = TopicBuildAgentContext.get_execution_id()
  111. params: Dict[str, Any] = {"execution_id": execution_id, "count": len(demand_items or []),
  112. "demand_items": demand_items}
  113. log_tool_input("create_demand_items", params)
  114. if not execution_id:
  115. return log_tool_output("create_demand_items", "错误: 未设置 execution_id")
  116. if not demand_items or not isinstance(demand_items, list):
  117. return log_tool_output("create_demand_items", "错误: demand_items 必须为非空列表")
  118. output_dir = _get_result_base_dir() / f"{execution_id}"
  119. output_path = output_dir / f"execution_id_{execution_id}_demand_items.json"
  120. output_path.parent.mkdir(parents=True, exist_ok=True)
  121. items: List[Dict[str, Any]] = []
  122. if output_path.exists():
  123. try:
  124. with open(output_path, "r", encoding="utf-8") as f:
  125. loaded = json.load(f)
  126. if isinstance(loaded, list):
  127. items = loaded
  128. elif isinstance(loaded, dict) and isinstance(loaded.get("items"), list):
  129. items = loaded["items"]
  130. else:
  131. items = [loaded]
  132. except json.JSONDecodeError:
  133. items = []
  134. written_records: List[Dict[str, Any]] = []
  135. for i, di in enumerate(demand_items):
  136. if not isinstance(di, dict):
  137. return log_tool_output("create_demand_items", f"错误: demand_items[{i}] 必须为对象(dict)")
  138. record = {
  139. "element_names": di.get("element_names"),
  140. "reason": di.get("reason"),
  141. "desc": di.get("desc"),
  142. "type": di.get("type"),
  143. "evidence_refs": _normalize_evidence_refs(di.get("evidence_refs")),
  144. }
  145. written_records.append(record)
  146. items.extend(written_records)
  147. with open(output_path, "w", encoding="utf-8") as f:
  148. json.dump(items, f, ensure_ascii=False, indent=2)
  149. result = json.dumps(
  150. {
  151. "success": True,
  152. "execution_id": execution_id,
  153. "written_to": str(output_path),
  154. "written_count": len(written_records),
  155. },
  156. ensure_ascii=False,
  157. )
  158. return log_tool_output("create_demand_items", result)
  159. @tool(
  160. "写入本次执行总结(在所有任务执行完成后调用)。"
  161. "\n\n该工具用于把最终总结记录到本地/trace输出中(框架侧通过返回值与日志落盘)。"
  162. )
  163. def write_execution_summary(summary: str) -> str:
  164. """写入本次执行总结。在所有任务执行完成后调用。
  165. Args:
  166. summary: 执行总结(Markdown 格式)。
  167. Returns:
  168. JSON 字符串:
  169. - 成功:`{"success": True, "execution_id": execution_id}`
  170. - 失败:`"错误: 未设置 execution_id"`
  171. """
  172. execution_id: Optional[int] = TopicBuildAgentContext.get_execution_id()
  173. params: Dict[str, str] = {"summary": summary}
  174. log_tool_input("write_execution_summary", params)
  175. if not execution_id:
  176. return log_tool_output("write_execution_summary", "错误: 未设置 execution_id")
  177. result = json.dumps({"success": True, "execution_id": execution_id}, ensure_ascii=False)
  178. return log_tool_output("write_execution_summary", result)