step3_generate_inspirations.py 16 KB

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