step3_generate_inspirations.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. """
  2. Step3: 基于匹配节点生成灵感点
  3. 基于 Step1 的 Top1 匹配结果,以匹配到的人设要素作为锚点,
  4. 让 Agent 分析可以产生哪些灵感点
  5. """
  6. import os
  7. import sys
  8. import json
  9. import asyncio
  10. from pathlib import Path
  11. from agents import Agent, Runner, trace
  12. from agents.tracing.create import custom_span
  13. from lib.my_trace import set_trace_smith as set_trace
  14. from lib.client import get_model
  15. from lib.data_loader import load_persona_data, load_inspiration_list, select_inspiration
  16. # 模型配置
  17. MODEL_NAME = "google/gemini-2.5-pro"
  18. # ========== System Prompt ==========
  19. GENERATE_INSPIRATIONS_PROMPT = """
  20. # 任务
  21. 你是一个内容创作者,现在要从一个锚点要素出发,模拟灵感产生的完整思维路径。
  22. ## 输入说明
  23. - **<人设体系></人设体系>**: 完整的人设系统,包含所有可用的节点
  24. - **<锚点要素></锚点要素>**: 作为起点的人设要素
  25. - **<要素定义></要素定义>**: 该要素的完整定义
  26. - **<要素上下文></要素上下文>**: 该要素的上下文信息
  27. ## 思维路径规则
  28. 每个灵感的产生都需要完整的思维路径:**[锚点] → [维度] → [节点/联想]**
  29. 1. **[锚点]**: 必须从给定的锚点要素出发
  30. 2. **[维度]**: 从锚点特征自然产生的思维方向(动态的,根据锚点特点决定)
  31. - 维度类型示例:跨节点组合、具体场景联想、情感延伸、时间维度、夸张变形、反差对比等
  32. - 维度要多样化,探索不同的思考方向
  33. 3. **[节点/联想]**:
  34. - **[节点]**: 人设体系中的其他节点
  35. - **[联想]**: 自由联想的具体内容
  36. - 可以有多个,用 + 连接
  37. ## 输出格式(严格JSON)
  38. ```json
  39. {
  40. "灵感点列表": [
  41. {
  42. "路径": "[锚点]xxx → [维度]xxx → [节点/联想]xxx",
  43. "灵感点": "具体的灵感点描述"
  44. },
  45. {
  46. "路径": "[锚点]xxx → [维度]xxx → [联想]xxx",
  47. "灵感点": "具体的灵感点描述"
  48. }
  49. ]
  50. }
  51. ```
  52. **要求**:
  53. 1. 生成 8-15 个灵感点
  54. 2. 路径必须严格按照 `[锚点] → [维度] → [节点/联想]` 格式
  55. 3. 维度要多样化,探索不同的思考方向
  56. 4. 每个灵感点简洁明确(一句话)
  57. 5. 灵感点要具体、可执行、符合人设风格
  58. 6. 必须严格按照 JSON 格式输出
  59. """.strip()
  60. def create_agent(model_name: str, prompt: str, name: str) -> Agent:
  61. """创建 Agent
  62. Args:
  63. model_name: 模型名称
  64. prompt: System prompt
  65. name: Agent 名称
  66. Returns:
  67. Agent 实例
  68. """
  69. agent = Agent(
  70. name=name,
  71. instructions=prompt,
  72. model=get_model(model_name),
  73. tools=[],
  74. )
  75. return agent
  76. def parse_json_response(response_content: str, default_value: dict = None) -> dict:
  77. """解析 JSON 响应
  78. Args:
  79. response_content: Agent 返回的响应内容
  80. default_value: 解析失败时的默认返回值
  81. Returns:
  82. 解析后的字典
  83. """
  84. try:
  85. # 如果响应包含在 markdown 代码块中,提取 JSON 部分
  86. if "```json" in response_content:
  87. json_start = response_content.index("```json") + 7
  88. json_end = response_content.index("```", json_start)
  89. json_text = response_content[json_start:json_end].strip()
  90. elif "```" in response_content:
  91. json_start = response_content.index("```") + 3
  92. json_end = response_content.index("```", json_start)
  93. json_text = response_content[json_start:json_end].strip()
  94. else:
  95. json_text = response_content.strip()
  96. return json.loads(json_text)
  97. except Exception as e:
  98. print(f"解析响应失败: {e}")
  99. return default_value if default_value else {}
  100. def format_persona_system(persona_data: dict) -> str:
  101. """格式化完整人设系统为文本
  102. Args:
  103. persona_data: 人设数据
  104. Returns:
  105. 格式化的人设系统文本
  106. """
  107. lines = ["# 人设系统"]
  108. # 处理三个部分:灵感点列表、目的点、关键点列表
  109. for section_key, section_title in [
  110. ("灵感点列表", "【灵感点】灵感的来源和性质"),
  111. ("目的点", "【目的点】创作的目的和价值导向"),
  112. ("关键点列表", "【关键点】内容的核心主体和表达方式")
  113. ]:
  114. section_data = persona_data.get(section_key, [])
  115. if not section_data:
  116. continue
  117. lines.append(f"\n## {section_title}\n")
  118. for perspective in section_data:
  119. perspective_name = perspective.get("视角名称", "")
  120. lines.append(f"\n### 视角:{perspective_name}")
  121. for pattern in perspective.get("模式列表", []):
  122. pattern_name = pattern.get("分类名称", "")
  123. pattern_def = pattern.get("核心定义", "")
  124. lines.append(f"\n 【一级】{pattern_name}")
  125. if pattern_def:
  126. lines.append(f" 定义:{pattern_def}")
  127. # 二级细分
  128. for sub in pattern.get("二级细分", []):
  129. sub_name = sub.get("分类名称", "")
  130. sub_def = sub.get("分类定义", "")
  131. lines.append(f" 【二级】{sub_name}:{sub_def}")
  132. return "\n".join(lines)
  133. def find_element_definition(persona_data: dict, element_name: str) -> str:
  134. """从人设数据中查找要素的定义
  135. Args:
  136. persona_data: 人设数据
  137. element_name: 要素名称
  138. Returns:
  139. 要素定义文本,如果未找到则返回空字符串
  140. """
  141. # 在灵感点列表中查找
  142. for section_key in ["灵感点列表", "目的点", "关键点列表"]:
  143. section_data = persona_data.get(section_key, [])
  144. for perspective in section_data:
  145. for pattern in perspective.get("模式列表", []):
  146. # 检查一级分类
  147. if pattern.get("分类名称", "") == element_name:
  148. definition = pattern.get("核心定义", "")
  149. if definition:
  150. return definition
  151. # 检查二级分类
  152. for sub in pattern.get("二级细分", []):
  153. if sub.get("分类名称", "") == element_name:
  154. return sub.get("分类定义", "")
  155. return ""
  156. def find_step1_file(persona_dir: str, inspiration: str, model_name: str) -> str:
  157. """查找 step1 输出文件
  158. Args:
  159. persona_dir: 人设目录
  160. inspiration: 灵感点名称
  161. model_name: 模型名称
  162. Returns:
  163. step1 文件路径
  164. Raises:
  165. SystemExit: 找不到文件时退出
  166. """
  167. step1_dir = os.path.join(persona_dir, "how", "灵感点", inspiration)
  168. model_name_short = model_name.replace("google/", "").replace("/", "_")
  169. step1_file_pattern = f"*_step1_*_{model_name_short}.json"
  170. step1_files = list(Path(step1_dir).glob(step1_file_pattern))
  171. if not step1_files:
  172. print(f"❌ 找不到 step1 输出文件")
  173. print(f"查找路径: {step1_dir}/{step1_file_pattern}")
  174. sys.exit(1)
  175. return str(step1_files[0])
  176. async def generate_inspirations_with_paths(
  177. persona_system_text: str,
  178. matched_element: str,
  179. element_definition: str,
  180. element_context: str
  181. ) -> list:
  182. """生成带思维路径的灵感点列表
  183. Args:
  184. persona_system_text: 完整人设系统文本
  185. matched_element: 匹配要素
  186. element_definition: 要素定义
  187. element_context: 要素上下文
  188. Returns:
  189. 灵感点列表 [{"路径": "...", "灵感点": "..."}, ...]
  190. """
  191. task_description = f"""## 本次任务
  192. <人设体系>
  193. {persona_system_text}
  194. </人设体系>
  195. <锚点要素>
  196. {matched_element}
  197. </锚点要素>
  198. <要素定义>
  199. {element_definition if element_definition else '无'}
  200. </要素定义>
  201. <要素上下文>
  202. {element_context}
  203. </要素上下文>
  204. 请从锚点要素出发,模拟灵感产生的完整思维路径,生成多个灵感点,严格按照 JSON 格式输出。"""
  205. messages = [{
  206. "role": "user",
  207. "content": [{"type": "input_text", "text": task_description}]
  208. }]
  209. agent = create_agent(MODEL_NAME, GENERATE_INSPIRATIONS_PROMPT, "Inspiration Path Generator")
  210. result = await Runner.run(agent, input=messages)
  211. parsed = parse_json_response(result.final_output, {"灵感点列表": []})
  212. return parsed.get("灵感点列表", [])
  213. async def process_step3_generate_inspirations(
  214. step1_top1: dict,
  215. persona_data: dict,
  216. current_time: str = None,
  217. log_url: str = None
  218. ) -> dict:
  219. """执行灵感生成分析(核心业务逻辑 - 单层路径生成)
  220. Args:
  221. step1_top1: step1 的 top1 匹配结果
  222. persona_data: 完整的人设数据
  223. current_time: 当前时间戳
  224. log_url: trace URL
  225. Returns:
  226. 生成结果字典
  227. """
  228. # 从 step1 结果中提取信息
  229. business_info = step1_top1.get("业务信息", {})
  230. input_info = step1_top1.get("输入信息", {})
  231. matched_element = business_info.get("匹配要素名称", "") # 修复:字段名从"匹配要素"改为"匹配要素名称"
  232. element_context = input_info.get("A_Context", "")
  233. # 格式化人设系统
  234. persona_system_text = format_persona_system(persona_data)
  235. # 查找要素定义
  236. element_definition = find_element_definition(persona_data, matched_element)
  237. print(f"\n{'=' * 80}")
  238. print(f"Step3: 基于锚点生成灵感路径")
  239. print(f"{'=' * 80}")
  240. print(f"锚点要素: {matched_element}")
  241. print(f"要素定义: {element_definition if element_definition else '(未找到定义)'}")
  242. print(f"模型: {MODEL_NAME}\n")
  243. # 生成灵感点(单层,包含完整思维路径)
  244. with custom_span(name="生成带路径的灵感点", data={"锚点要素": matched_element}):
  245. inspirations = await generate_inspirations_with_paths(
  246. persona_system_text, matched_element, element_definition, element_context
  247. )
  248. print(f"\n{'=' * 80}")
  249. print(f"完成!共生成 {len(inspirations)} 个灵感点")
  250. print(f"{'=' * 80}\n")
  251. # 预览前3个
  252. if inspirations:
  253. print("预览前3个灵感点:")
  254. for i, item in enumerate(inspirations[:3], 1):
  255. print(f" {i}. 路径: {item.get('路径', '')}")
  256. print(f" 灵感: {item.get('灵感点', '')}")
  257. print()
  258. # 构建输出
  259. return {
  260. "元数据": {
  261. "current_time": current_time,
  262. "log_url": log_url,
  263. "model": MODEL_NAME,
  264. "步骤": "Step3: 基于匹配节点生成灵感点"
  265. },
  266. "锚点信息": {
  267. "人设要素": matched_element,
  268. "要素定义": element_definition if element_definition else "无",
  269. "要素上下文": element_context
  270. },
  271. "step1_结果": step1_top1,
  272. "灵感点列表": inspirations
  273. }
  274. async def main(current_time: str, log_url: str, force: bool = False):
  275. """主函数
  276. Args:
  277. current_time: 当前时间戳
  278. log_url: 日志链接
  279. force: 是否强制重新执行(跳过已存在文件检查)
  280. """
  281. # 解析命令行参数
  282. persona_dir = sys.argv[1] if len(sys.argv) > 1 else "data/阿里多多酱/out/人设_1110"
  283. inspiration_arg = sys.argv[2] if len(sys.argv) > 2 else "0"
  284. # 第三个参数:force(如果从命令行调用且有该参数,则覆盖函数参数)
  285. if len(sys.argv) > 3 and sys.argv[3] == "force":
  286. force = True
  287. print(f"{'=' * 80}")
  288. print(f"Step3: 基于匹配节点生成灵感点")
  289. print(f"{'=' * 80}")
  290. print(f"人设目录: {persona_dir}")
  291. print(f"灵感参数: {inspiration_arg}")
  292. # 加载数据
  293. persona_data = load_persona_data(persona_dir)
  294. inspiration_list = load_inspiration_list(persona_dir)
  295. # 选择灵感
  296. try:
  297. inspiration_index = int(inspiration_arg)
  298. if 0 <= inspiration_index < len(inspiration_list):
  299. test_inspiration = inspiration_list[inspiration_index]
  300. print(f"使用灵感[{inspiration_index}]: {test_inspiration}")
  301. else:
  302. print(f"❌ 灵感索引超出范围: {inspiration_index}")
  303. sys.exit(1)
  304. except ValueError:
  305. if inspiration_arg in inspiration_list:
  306. test_inspiration = inspiration_arg
  307. print(f"使用灵感: {test_inspiration}")
  308. else:
  309. print(f"❌ 找不到灵感: {inspiration_arg}")
  310. sys.exit(1)
  311. # 查找并加载 step1 结果
  312. step1_file = find_step1_file(persona_dir, test_inspiration, MODEL_NAME)
  313. step1_filename = os.path.basename(step1_file)
  314. step1_basename = os.path.splitext(step1_filename)[0]
  315. print(f"Step1 输入文件: {step1_file}")
  316. # 构建输出文件路径
  317. output_dir = os.path.join(persona_dir, "how", "灵感点", test_inspiration)
  318. model_name_short = MODEL_NAME.replace("google/", "").replace("/", "_")
  319. scope_prefix = step1_basename.split("_")[0]
  320. result_index = 0
  321. output_filename = f"{scope_prefix}_step3_top{result_index + 1}_生成灵感_{model_name_short}.json"
  322. output_file = os.path.join(output_dir, output_filename)
  323. # 检查文件是否已存在
  324. if not force and os.path.exists(output_file):
  325. print(f"\n✓ 输出文件已存在,跳过执行: {output_file}")
  326. print(f"提示: 如需重新执行,请添加 'force' 参数\n")
  327. return
  328. with open(step1_file, 'r', encoding='utf-8') as f:
  329. step1_data = json.load(f)
  330. actual_inspiration = step1_data.get("灵感", "")
  331. step1_results = step1_data.get("匹配结果列表", [])
  332. if not step1_results:
  333. print("❌ step1 结果为空")
  334. sys.exit(1)
  335. print(f"灵感: {actual_inspiration}")
  336. # 默认处理 top1
  337. selected_result = step1_results[result_index]
  338. print(f"处理第 {result_index + 1} 个匹配结果(Top{result_index + 1})\n")
  339. # 执行核心业务逻辑
  340. output = await process_step3_generate_inspirations(
  341. step1_top1=selected_result,
  342. persona_data=persona_data,
  343. current_time=current_time,
  344. log_url=log_url
  345. )
  346. # 在元数据中添加 step1 匹配索引
  347. output["元数据"]["step1_匹配索引"] = result_index + 1
  348. # 保存结果
  349. os.makedirs(output_dir, exist_ok=True)
  350. with open(output_file, 'w', encoding='utf-8') as f:
  351. json.dump(output, f, ensure_ascii=False, indent=2)
  352. # 输出统计信息
  353. stats = output.get("统计", {})
  354. print(f"\n{'=' * 80}")
  355. print(f"统计信息:")
  356. print(f" 外层维度数: {stats.get('外层维度数', 0)}")
  357. print(f" 内层维度数: {stats.get('内层维度数', 0)}")
  358. print(f" 灵感点总数: {stats.get('灵感点总数', 0)}")
  359. print(f"{'=' * 80}")
  360. print(f"\n完成!结果已保存到: {output_file}")
  361. if log_url:
  362. print(f"Trace: {log_url}\n")
  363. if __name__ == "__main__":
  364. # 设置 trace
  365. current_time, log_url = set_trace()
  366. # 使用 trace 上下文包裹整个执行流程
  367. with trace("Step3: 生成灵感点"):
  368. asyncio.run(main(current_time, log_url))