agent_tools.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. DecodeProcess Agent 的工具集
  5. 工具均为纯 Python 函数(无 @tool 装饰器),LangChain `create_agent`
  6. 会从函数签名 + docstring 推断 schema。
  7. 所有写操作都通过 WorkflowContext 落到本地 JSON 文件;返回值统一为
  8. {"success": bool, "message"/"error": str, ...} 让 LLM 能感知失败并修正。
  9. ⚠️ 关于字段留空:「传空字符串」指的是**长度为 0 的字符串**(实际传 `tool_name=""`、
  10. `value=""`),**不要**把两个引号字符当字面值写进字段(即 `tool_name='""'` —— 这是
  11. 长度为 2 的字符串,前端会把它当作真实内容渲染出来)。同理 `"-"` / `"N/A"` / `"无"`
  12. 这类占位字符串也不是"空"。**没东西可写 → 传真正的 ""**。
  13. ⚠️ 关于 `actions` / `roles` / `purposes` 数组:每一项是**单一标签**,不要把多个候选用
  14. `/` 或 `、` 拼到同一项里。错例:`actions=["拼接/识别/提取"]`;正例:`actions=["拼接",
  15. "识别", "提取"]`。系统 prompt 里推荐取值用 `/` 列出只是文档分隔形式,不是字段值语法。
  16. """
  17. from typing import Any, Dict, List, Optional
  18. from workflow_store import WorkflowContext, WorkflowStoreError
  19. def _ok(message: str, **extra: Any) -> Dict[str, Any]:
  20. out: Dict[str, Any] = {"success": True, "message": message}
  21. out.update(extra)
  22. return out
  23. def _err(error: str) -> Dict[str, Any]:
  24. return {"success": False, "error": error}
  25. def think_and_plan(thought: str, plan: str, action: str) -> str:
  26. """系统化思考与规划工具:在做工序拆解决策前,先梳理思路、整体计划与下一步行动。
  27. 本工具不修改任何状态,只负责打印记录,便于事后追溯 Agent 的推理过程。
  28. 在开始 add_step 之前应至少调用一次。
  29. Args:
  30. thought: 当前思考内容,例如"这是一张化妆教程,整体可分为底妆/眼妆/唇妆三阶段"。
  31. plan: 整体方案,**必须包含初步的 step name 列表**作为工序主体脉络(脉络锚)。
  32. 初步 name 是工作假设,只需粗略反映"动作+对象";后续 I/O 写齐后会回头集中浓缩。
  33. 例:
  34. 1. 生成基础人像底图
  35. 2. 局部重绘眼妆细节
  36. 3. 应用整体风格统一画面
  37. action: 紧接着要执行的具体动作,例如"调用 add_step 添加 step_id=1 的生成步骤"。
  38. Returns:
  39. 将上述三段拼接后的纯文本回显。
  40. """
  41. response = (
  42. f"[think_and_plan]\n"
  43. f"thought: {thought}\n"
  44. f"plan: {plan}\n"
  45. f"action: {action}\n"
  46. )
  47. print(response)
  48. return response
  49. def add_step(
  50. step_id: str,
  51. name: str,
  52. actions: List[str],
  53. roles: List[str],
  54. purposes: List[str],
  55. source_ref: str,
  56. tool_name: str,
  57. tool_method: str,
  58. ) -> Dict[str, Any]:
  59. """追加一个工序步骤的骨架(不含 inputs / outputs)。
  60. 必须在调用 add_step_input / add_step_output 之前先把骨架建好。
  61. Args:
  62. step_id: 步骤 id,建议自然数字符串,如 "1"、"2"。在同一工序内必须唯一。
  63. name: 步骤名称(6~20 字可读自然语言短语,"动作 + 对象(模态/类型) + 通用作用/目的"的浓缩)。
  64. 示例:「生成基础人像底图」、「局部重绘眼妆细节」、「应用整体风格统一画面」、
  65. 「生成人像底图为后续编辑提供锚定」。
  66. **两阶段产物**:在 add_step 时可用 think_and_plan 里列出的**初步 name** 即可;
  67. 所有 step + I/O 写齐后,工作流要求在 finalize 前**集中**用 update_step 把 name 浓缩为
  68. "actions + 主要 I/O 的 modality+category + roles + purposes 的最佳可读投影"。
  69. 禁止:只写孤立动词("生成")、写工具名("Midjourney")、写内容细节(角色身份/风格词/色彩/参数等)。
  70. actions: **数组**。这一步执行的**纯动作**标签(剥离对象/目的后的动词)。
  71. 例:`["生成"]` / `["局部重绘", "风格化"]` / `["分析", "提取"]`。
  72. 1 项最常见;只在同一次工具调用确实合并了多个动作时填多项。
  73. 无法判断时传 `[]`。
  74. roles: **数组**。这一步在整体工序里担任的**抽象节点类型**(pipeline 层级标签)。
  75. 例:`["底图构建"]` / `["细节加工", "风格调优"]` / `["素材采集"]`。
  76. 与 actions 区别:actions 是纯动词;roles 是 pipeline 节点类别。
  77. **禁止**让 roles 只是 actions 的名词化(如 actions=["生成"]、roles=["生成"])。
  78. 无法判断时传 `[]`。
  79. purposes: **数组**。这一步在追求的**质量维度 / 目标属性标签**。
  80. 写成短标签(2~6 字名词性短语),**不是长句**,**不是关系描述**。
  81. 例:`["角色一致性"]` / `["细节还原度", "风格一致性"]` /
  82. `["真实感"]` / `["可复用性"]` / `["多样性"]`。
  83. 常见维度:一致性 / 还原度 / 多样性 / 可控性 / 美观度 / 真实感 /
  84. 可复用性 / 完整性 / 效率。
  85. **硬约束**:
  86. 1) 形态必须是「质量属性标签」,不是动词、不是流程类别、不是长句。
  87. 2) 必须能从输入推断;推不出 → `[]`。留空时 name 也要同步省略目的片段。
  88. **`name` / `actions` / `roles` / `purposes` 四者同源**:
  89. `name` 是 `actions` + 主要 I/O 的 modality+category + `roles` + `purposes` 浓缩出的可读自然语言;
  90. 反过来,`actions` / `roles` / `purposes` 是 `name` 的结构化反向分解。
  91. **结构化字段是完备真源;`name` 是它们的有损可读子集**。
  92. 写完一个 step 必须自检:
  93. - 向前对齐:name 的每个语义片段都能定位到某个结构化字段;
  94. - 向后对齐:actions/roles/purposes 的每一项都能在 name 中被同义/近义地体现。
  95. source_ref: 该步骤的来源依据,可以引用图序号或正文片段,如 "图2 / 正文第3句"。
  96. 无据则不应新增该步骤;难以精确表达可留空字符串。
  97. tool_name: 工具/产品名称(不含方法/参数)。例:「Midjourney」/「Nano Banana」/「manus」。
  98. 完全判断不出来时留空字符串或填 "文生图类工具" 等能力兜底。
  99. tool_method: 在该工具里**实际可观察到**的具体方法/功能/参数。
  100. 只允许:图中可见的 UI 标签/按钮/参数/模型挡位,或正文里明确点名的功能名。
  101. **禁止**脑补描述工具用途(如写 "视频分析与提示词提取");
  102. 没观察到就传空字符串 `""`。
  103. Returns:
  104. {"success": True, "step": {...}, "current_step_count": N};失败时返回 error。
  105. """
  106. try:
  107. step = WorkflowContext.add_step(
  108. step_id=step_id,
  109. name=name,
  110. actions=actions,
  111. roles=roles,
  112. purposes=purposes,
  113. source_ref=source_ref,
  114. tool_name=tool_name,
  115. tool_method=tool_method,
  116. )
  117. except WorkflowStoreError as e:
  118. return _err(str(e))
  119. snapshot = WorkflowContext.get()
  120. print(f"[add_step] step_id={step_id} name={name} ok(共{len(snapshot['steps'])}步)")
  121. return _ok(
  122. f"step_id={step_id} 已添加",
  123. step=step,
  124. current_step_count=len(snapshot["steps"]),
  125. )
  126. def add_step_input(
  127. step_id: str,
  128. modality: str,
  129. category: str,
  130. source_id: str,
  131. value: str,
  132. ) -> Dict[str, Any]:
  133. """为指定步骤追加一项输入。
  134. Args:
  135. step_id: 目标步骤 id,必须已通过 add_step 创建。
  136. modality: 数据模态(受控)。从 "文本" / "图片" / "视频" / "音频" / "文件" 中选取。
  137. category: 该输入在制作领域里的**专有类型**(领域内通用短语)。
  138. 例:`prompt` / `负向提示词` / `脚本` / `分镜脚本` / `人物参考图` /
  139. `场景参考图` / `底图` / `分镜图` / `运镜描述` / `知识库` / `配音` 等。
  140. 无法用领域专有名词归类时传 `""`。
  141. source_id: 输入来源 id。规则:
  142. - 若为该工序的初始输入,使用 "init_input_<N>",N 从 1 开始;
  143. - 若来自前一步的输出,必须填那一步的某个 outputs[*].id(如 "p1_output")。
  144. value: 实例化后的具体内容。文本类填原文片段或凝练;图片类填可见物体的简洁描述。
  145. Returns:
  146. {"success": True, "input": {...}};source_id 不可解析或 step 不存在时返回 error。
  147. """
  148. try:
  149. item = WorkflowContext.add_step_input(
  150. step_id=step_id,
  151. modality=modality,
  152. category=category,
  153. source_id=source_id,
  154. value=value,
  155. )
  156. except WorkflowStoreError as e:
  157. return _err(str(e))
  158. print(f"[add_step_input] step_id={step_id} source_id={source_id} ok")
  159. return _ok(f"step_id={step_id} 已追加输入", input=item)
  160. def add_step_output(
  161. step_id: str,
  162. output_id: str,
  163. modality: str,
  164. category: str,
  165. value: str,
  166. ) -> Dict[str, Any]:
  167. """为指定步骤追加一项输出。
  168. Args:
  169. step_id: 目标步骤 id,必须已通过 add_step 创建。
  170. output_id: 输出 id,命名约定:
  171. - 单一输出:使用 "p<step_id>_output",例如 step_id=1 -> "p1_output";
  172. - 多输出:使用 "p<step_id>_output_<M>",M 从 1 开始,例如 "p2_output_1"。
  173. - 全局唯一,不能与任何已有输出 id 冲突。
  174. modality: 数据模态(受控)。从 "文本" / "图片" / "视频" / "音频" / "文件" 中选取。
  175. category: 输出在制作领域里的**专有类型**。
  176. 例:`人物参考图` / `场景参考图` / `分镜图` / `成品图` / `视频片段` /
  177. `妆容描述` / `脚本` / `prompt` / `提示词库`。
  178. 无法用领域专有名词归类时传 `""`。
  179. value: 实例化后的具体内容。
  180. Returns:
  181. {"success": True, "output": {...}};id 冲突或 step 不存在时返回 error。
  182. """
  183. try:
  184. item = WorkflowContext.add_step_output(
  185. step_id=step_id,
  186. output_id=output_id,
  187. modality=modality,
  188. category=category,
  189. value=value,
  190. )
  191. except WorkflowStoreError as e:
  192. return _err(str(e))
  193. print(f"[add_step_output] step_id={step_id} output_id={output_id} ok")
  194. return _ok(f"step_id={step_id} 已追加输出 {output_id}", output=item)
  195. def get_current_workflow() -> Dict[str, Any]:
  196. """读取当前已写入的工序(含所有已添加的 step / input / output)。
  197. 用于 Agent 自检:例如在追加引用某 output_id 的输入前,先确认其确实存在。
  198. Returns:
  199. 当前工序的完整快照(深拷贝)。
  200. """
  201. try:
  202. wf = WorkflowContext.get()
  203. except WorkflowStoreError as e:
  204. return _err(str(e))
  205. print(f"[get_current_workflow] steps={len(wf['steps'])} status={wf['status']}")
  206. return wf
  207. def update_step(
  208. step_id: str,
  209. name: Optional[str] = None,
  210. actions: Optional[List[str]] = None,
  211. roles: Optional[List[str]] = None,
  212. purposes: Optional[List[str]] = None,
  213. source_ref: Optional[str] = None,
  214. tool_name: Optional[str] = None,
  215. tool_method: Optional[str] = None,
  216. ) -> Dict[str, Any]:
  217. """部分更新某个步骤的元信息字段。step_id 本身不可改。
  218. 任何字段传 None(或省略)表示**保持原值**。
  219. 标量字段(name/source_ref/tool_name/tool_method):传 `""` 表示清空。
  220. 数组字段(actions/roles/purposes):传 `[]` 表示清空;传 `["a", "b"]` **整体替换**为该数组
  221. (不是追加,是替换)。
  222. 典型用法:
  223. - I/O 阶段补全结构化字段:`update_step(step_id="2", actions=["局部重绘", "风格化"], purposes=["风格一致性"])`
  224. - finalize 前**集中浓缩 name**:所有 step + I/O 都写齐后,根据完整的 actions/roles/purposes +
  225. 主要 I/O 的 modality+category,调 `update_step(step_id="2", name="为底图局部重绘眼妆细节")`
  226. 把 name 重写成最佳浓缩可读自然语言(参见系统提示中"必须遵守的工作流" step 8)。
  227. Args:
  228. step_id: 目标步骤 id,必须已存在。
  229. name: 新的步骤名称;不改则 None。
  230. actions: 新的动作数组(整体替换);不改则 None。
  231. roles: 新的流程角色数组(整体替换);不改则 None。
  232. purposes: 新的目的数组(整体替换);不改则 None。
  233. source_ref: 新的来源依据;不改则 None;传 "" 清空。
  234. tool_name: 新的工具名称;不改则 None;传 "" 清空。
  235. tool_method: 新的工具方法;不改则 None;传 "" 清空。
  236. Returns:
  237. {"success": True, "step": {更新后的完整 step}};失败时返回 error。
  238. """
  239. try:
  240. step = WorkflowContext.update_step(
  241. step_id=step_id,
  242. name=name,
  243. actions=actions,
  244. roles=roles,
  245. purposes=purposes,
  246. source_ref=source_ref,
  247. tool_name=tool_name,
  248. tool_method=tool_method,
  249. )
  250. except WorkflowStoreError as e:
  251. return _err(str(e))
  252. print(f"[update_step] step_id={step_id} ok")
  253. return _ok(f"step_id={step_id} 已更新", step=step)
  254. def update_step_input(
  255. step_id: str,
  256. input_index: int,
  257. modality: Optional[str] = None,
  258. category: Optional[str] = None,
  259. source_id: Optional[str] = None,
  260. value: Optional[str] = None,
  261. ) -> Dict[str, Any]:
  262. """部分更新某个步骤的某一项输入。
  263. 输入用 (step_id, input_index) 定位,input_index 是该步骤 inputs 数组的 0-based 下标。
  264. 若不确定下标,请先调用 get_current_workflow 查看当前状态。
  265. 若 source_id 也被修改了,新值会重新做依赖校验:必须是 init_input_<N>,或本步骤之前
  266. 已经存在的某个 output_id。
  267. Args:
  268. step_id: 目标步骤 id。
  269. input_index: 输入项在 inputs 数组中的下标(从 0 开始)。
  270. modality: 新的数据模态(文本/图片/视频/音频/文件);不改则 None。
  271. category: 新的领域类型(如 `prompt` / `人物参考图`);不改则 None;传 "" 清空。
  272. source_id: 新的输入来源 id;不改则 None。
  273. value: 新的实例化内容;不改则 None。
  274. Returns:
  275. {"success": True, "input": {更新后的输入项}};失败时返回 error。
  276. """
  277. try:
  278. item = WorkflowContext.update_step_input(
  279. step_id=step_id,
  280. input_index=input_index,
  281. modality=modality,
  282. category=category,
  283. source_id=source_id,
  284. value=value,
  285. )
  286. except WorkflowStoreError as e:
  287. return _err(str(e))
  288. print(f"[update_step_input] step_id={step_id} index={input_index} ok")
  289. return _ok(f"step_id={step_id} 第 {input_index} 项输入已更新", input=item)
  290. def update_step_output(
  291. step_id: str,
  292. output_id: str,
  293. modality: Optional[str] = None,
  294. category: Optional[str] = None,
  295. value: Optional[str] = None,
  296. ) -> Dict[str, Any]:
  297. """部分更新某个步骤的某一项输出。output_id 本身不可改(其它输入可能引用它)。
  298. 如果一定要换 output_id,请用 add_step_output 创建一个新的,再 update_step_input
  299. 把所有引用切到新 id,最后 delete_step_output 删旧的。
  300. Args:
  301. step_id: 目标步骤 id。
  302. output_id: 目标输出 id(用作定位 key,不会被修改)。
  303. modality: 新的数据模态;不改则 None。
  304. category: 新的领域类型;不改则 None;传 "" 清空。
  305. value: 新的实例化内容;不改则 None。
  306. Returns:
  307. {"success": True, "output": {更新后的输出项}};失败时返回 error。
  308. """
  309. try:
  310. item = WorkflowContext.update_step_output(
  311. step_id=step_id,
  312. output_id=output_id,
  313. modality=modality,
  314. category=category,
  315. value=value,
  316. )
  317. except WorkflowStoreError as e:
  318. return _err(str(e))
  319. print(f"[update_step_output] step_id={step_id} output_id={output_id} ok")
  320. return _ok(f"step_id={step_id} 输出 {output_id} 已更新", output=item)
  321. def delete_step(step_id: str) -> Dict[str, Any]:
  322. """删除整个步骤(包含其所有输入和输出)。
  323. 若该步骤的任何输出仍被后续步骤的输入引用,会拒绝并提示先解除依赖。
  324. Args:
  325. step_id: 要删除的步骤 id。
  326. Returns:
  327. {"success": True, "removed_step": {被删的完整 step 快照}};失败时返回 error。
  328. """
  329. try:
  330. removed = WorkflowContext.delete_step(step_id=step_id)
  331. except WorkflowStoreError as e:
  332. return _err(str(e))
  333. print(f"[delete_step] step_id={step_id} ok")
  334. return _ok(f"step_id={step_id} 已删除", removed_step=removed)
  335. def delete_step_input(step_id: str, input_index: int) -> Dict[str, Any]:
  336. """删除某个步骤的某一项输入。输入没有反向引用,删除不会牵连其他位置。
  337. Args:
  338. step_id: 目标步骤 id。
  339. input_index: 输入项在 inputs 数组中的下标(从 0 开始)。注意:删除后后面项的下标会前移。
  340. Returns:
  341. {"success": True, "removed_input": {被删的输入项}};失败时返回 error。
  342. """
  343. try:
  344. removed = WorkflowContext.delete_step_input(
  345. step_id=step_id, input_index=input_index
  346. )
  347. except WorkflowStoreError as e:
  348. return _err(str(e))
  349. print(f"[delete_step_input] step_id={step_id} index={input_index} ok")
  350. return _ok(f"step_id={step_id} 第 {input_index} 项输入已删除", removed_input=removed)
  351. def delete_step_output(step_id: str, output_id: str) -> Dict[str, Any]:
  352. """删除某个步骤的某一项输出。
  353. 若该输出仍被任何输入引用,会拒绝并提示先解除依赖(update_step_input 或
  354. delete_step_input 把那些输入处理掉)。
  355. Args:
  356. step_id: 目标步骤 id。
  357. output_id: 要删除的输出 id。
  358. Returns:
  359. {"success": True, "removed_output": {被删的输出项}};失败时返回 error。
  360. """
  361. try:
  362. removed = WorkflowContext.delete_step_output(
  363. step_id=step_id, output_id=output_id
  364. )
  365. except WorkflowStoreError as e:
  366. return _err(str(e))
  367. print(f"[delete_step_output] step_id={step_id} output_id={output_id} ok")
  368. return _ok(
  369. f"step_id={step_id} 输出 {output_id} 已删除", removed_output=removed
  370. )
  371. def finalize_workflow(summary: str) -> Dict[str, Any]:
  372. """收尾:写入整体摘要并把 status 置为 "finalized",落盘最终 JSON。
  373. 所有步骤都已添加完毕后调用一次。调用后不能再继续追加步骤。
  374. Args:
  375. summary: 一段不超过 200 字的摘要,概括整套工序的目标与产物。
  376. Returns:
  377. {"success": True, "workflow": {...}};当前没有任何步骤时返回 error。
  378. """
  379. try:
  380. wf = WorkflowContext.finalize(summary=summary)
  381. except WorkflowStoreError as e:
  382. return _err(str(e))
  383. print(f"[finalize_workflow] finalized(共{len(wf['steps'])}步)-> {WorkflowContext.output_path()}")
  384. return _ok("工序已 finalize", workflow=wf)