From c21cda3e788af95ef8cdd047e5f60dffe21c68c6 Mon Sep 17 00:00:00 2001 From: yangshilin <2157598560@qq.com> Date: Thu, 19 Mar 2026 10:43:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=94=9F=E6=88=90=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E8=AF=8Dagent=E7=BB=93=E6=9E=84=E5=B0=81?= =?UTF-8?q?=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/app/agents/__init__.py | 5 +- api/app/agents/image_prompt/__init__.py | 8 +++ api/app/agents/image_prompt/orchestrator.py | 57 +++++++++++++++++++ api/app/agents/image_prompt/prompt_agent.py | 50 ++++++++++++++++ api/app/tasks/memoir_tasks.py | 7 ++- ...est_generate_chapter_images_persistence.py | 2 +- .../test_generate_chapter_images_task.py | 14 ++--- 7 files changed, 131 insertions(+), 12 deletions(-) create mode 100644 api/app/agents/image_prompt/__init__.py create mode 100644 api/app/agents/image_prompt/orchestrator.py create mode 100644 api/app/agents/image_prompt/prompt_agent.py diff --git a/api/app/agents/__init__.py b/api/app/agents/__init__.py index b1b613c..839f422 100644 --- a/api/app/agents/__init__.py +++ b/api/app/agents/__init__.py @@ -1,5 +1,5 @@ """ -Agent 模块(按功能拆分:chat / memoir) +Agent 模块(按功能拆分:chat / memoir / image_prompt) """ from app.agents.chat import ( ChatOrchestrator, @@ -7,6 +7,7 @@ from app.agents.chat import ( InterviewAgent, ProfileAgent, ) +from app.agents.image_prompt import ImagePromptOrchestrator, PromptGenerationAgent from app.agents.memoir import BackgroundTaskRunner, MemoryAgent __all__ = [ @@ -16,4 +17,6 @@ __all__ = [ "ProfileAgent", "InterviewAgent", "BackgroundTaskRunner", + "ImagePromptOrchestrator", + "PromptGenerationAgent", ] diff --git a/api/app/agents/image_prompt/__init__.py b/api/app/agents/image_prompt/__init__.py new file mode 100644 index 0000000..3d26b1b --- /dev/null +++ b/api/app/agents/image_prompt/__init__.py @@ -0,0 +1,8 @@ +"""图片提示词模块:ImagePromptOrchestrator + PromptGenerationAgent""" +from app.agents.image_prompt.orchestrator import ImagePromptOrchestrator +from app.agents.image_prompt.prompt_agent import PromptGenerationAgent + +__all__ = [ + "ImagePromptOrchestrator", + "PromptGenerationAgent", +] diff --git a/api/app/agents/image_prompt/orchestrator.py b/api/app/agents/image_prompt/orchestrator.py new file mode 100644 index 0000000..6a879d3 --- /dev/null +++ b/api/app/agents/image_prompt/orchestrator.py @@ -0,0 +1,57 @@ +""" +ImagePromptOrchestrator:图片提示词生成编排器。 +根据调用方(封面/正文)选择 build_prompt 或 build_cover_prompt; +统一异常处理和回退;内部委托 PromptGenerationAgent。 +""" +from __future__ import annotations + +from typing import Any, Optional + +from app.features.memoir.memoir_images.settings import MemoirImageSettings + +from app.agents.image_prompt.prompt_agent import PromptGenerationAgent + + +class ImagePromptOrchestrator: + """ + 图片提示词编排器。 + 区分封面 vs 正文配图,统一调用 PromptGenerationAgent; + 异常与回退由 PromptGenerationAgent(底层 MemoirImagePromptService)处理。 + """ + + def __init__(self, llm: Optional[Any], settings: MemoirImageSettings) -> None: + self._agent = PromptGenerationAgent(llm=llm, settings=settings) + + def build_prompt( + self, + chapter_title: str, + chapter_category: str, + description: str, + context_excerpt: str, + ) -> dict[str, str]: + """ + 生成正文配图的 prompt。 + 委托 PromptGenerationAgent,已含 LLM 调用失败时的 fallback 逻辑。 + """ + return self._agent.build_prompt( + chapter_title=chapter_title, + chapter_category=chapter_category, + description=description, + context_excerpt=context_excerpt, + ) + + def build_cover_prompt( + self, + chapter_title: str, + chapter_category: str, + context_excerpt: str, + ) -> dict[str, str]: + """ + 生成章节封面的 prompt。 + 委托 PromptGenerationAgent,已含 LLM 调用失败时的 fallback 逻辑。 + """ + return self._agent.build_cover_prompt( + chapter_title=chapter_title, + chapter_category=chapter_category, + context_excerpt=context_excerpt, + ) diff --git a/api/app/agents/image_prompt/prompt_agent.py b/api/app/agents/image_prompt/prompt_agent.py new file mode 100644 index 0000000..5983fad --- /dev/null +++ b/api/app/agents/image_prompt/prompt_agent.py @@ -0,0 +1,50 @@ +""" +PromptGenerationAgent:生成回忆录配图的 image-generation prompt。 +接收 chapter_title、chapter_category、description、context_excerpt, +调用 LLM 或 fallback 生成 {prompt, style, size}。 +底层委托 MemoirImagePromptService,保持对外接口兼容。 +""" +from __future__ import annotations + +from typing import Any, Optional + +from app.features.memoir.memoir_images.prompting import MemoirImagePromptService +from app.features.memoir.memoir_images.settings import MemoirImageSettings + + +class PromptGenerationAgent: + """ + 图片提示词生成 Specialist Agent。 + 封装 MemoirImagePromptService,提供 build_prompt / build_cover_prompt 接口。 + """ + + def __init__(self, llm: Optional[Any], settings: MemoirImageSettings) -> None: + self._service = MemoirImagePromptService(llm=llm, settings=settings) + + def build_prompt( + self, + chapter_title: str, + chapter_category: str, + description: str, + context_excerpt: str, + ) -> dict[str, str]: + """生成正文配图的 image-generation prompt。""" + return self._service.build_prompt( + chapter_title=chapter_title, + chapter_category=chapter_category, + description=description, + context_excerpt=context_excerpt, + ) + + def build_cover_prompt( + self, + chapter_title: str, + chapter_category: str, + context_excerpt: str, + ) -> dict[str, str]: + """生成章节封面图的 image-generation prompt。""" + return self._service.build_cover_prompt( + chapter_title=chapter_title, + chapter_category=chapter_category, + context_excerpt=context_excerpt, + ) diff --git a/api/app/tasks/memoir_tasks.py b/api/app/tasks/memoir_tasks.py index 97a5ffc..550e2b1 100644 --- a/api/app/tasks/memoir_tasks.py +++ b/api/app/tasks/memoir_tasks.py @@ -42,6 +42,7 @@ from app.features.memoir.memoir_images.parser import ( ) import hashlib from app.core.dependencies import get_image_generator +from app.agents.image_prompt import ImagePromptOrchestrator from app.features.memoir.memoir_images.prompting import MemoirImagePromptService from app.features.memoir.memoir_images.schema import ( completed_image_assets, @@ -883,7 +884,7 @@ def generate_chapter_images(self, chapter_id: str): logger.info("章节补图跳过: chapter=%s, reason=locked", chapter_id) return {"status": "locked"} - prompt_service = MemoirImagePromptService(_get_llm(), settings) + prompt_orchestrator = ImagePromptOrchestrator(_get_llm(), settings) image_generator = get_image_generator() storage = TencentCosStorageService.from_env() logger.info( @@ -922,7 +923,7 @@ def generate_chapter_images(self, chapter_id: str): sections_ordered = sorted(sections, key=lambda s: getattr(s, "order_index", 0)) first_content = (sections_ordered[0].content or "").strip() if sections_ordered else "" context_excerpt = " ".join(first_content.split("\n")[:5])[:200] - prompt_data = prompt_service.build_cover_prompt( + prompt_data = prompt_orchestrator.build_cover_prompt( chapter_title=chapter.title, chapter_category=chapter.category or "", context_excerpt=context_excerpt, @@ -985,7 +986,7 @@ def generate_chapter_images(self, chapter_id: str): try: context_lines = (section.content or "").strip().split("\n")[:5] context_excerpt = " ".join(context_lines)[:200] - prompt_data = prompt_service.build_prompt( + prompt_data = prompt_orchestrator.build_prompt( chapter_title=chapter.title, chapter_category=chapter.category or "", description=current_item.get("description", ""), diff --git a/api/tests/test_generate_chapter_images_persistence.py b/api/tests/test_generate_chapter_images_persistence.py index 283e544..6af5cd8 100644 --- a/api/tests/test_generate_chapter_images_persistence.py +++ b/api/tests/test_generate_chapter_images_persistence.py @@ -63,7 +63,7 @@ class GenerateChapterImagesPersistenceTest(unittest.TestCase): @patch("app.tasks.memoir_tasks.get_sync_db") @patch("app.tasks.memoir_tasks.TencentCosStorageService") @patch("app.tasks.memoir_tasks.get_image_generator") - @patch("app.tasks.memoir_tasks.MemoirImagePromptService") + @patch("app.tasks.memoir_tasks.ImagePromptOrchestrator") @patch("app.tasks.memoir_tasks._release_chapter_image_lock") @patch("app.tasks.memoir_tasks._acquire_chapter_image_lock", return_value=True) def test_successful_generation_persists_completed_status( diff --git a/api/tests/test_generate_chapter_images_task.py b/api/tests/test_generate_chapter_images_task.py index 5d77c2c..e104abd 100644 --- a/api/tests/test_generate_chapter_images_task.py +++ b/api/tests/test_generate_chapter_images_task.py @@ -92,7 +92,7 @@ class GenerateChapterImagesTaskTest(unittest.TestCase): @patch("app.tasks.memoir_tasks.get_sync_db") @patch("app.tasks.memoir_tasks.TencentCosStorageService") @patch("app.tasks.memoir_tasks.get_image_generator") - @patch("app.tasks.memoir_tasks.MemoirImagePromptService") + @patch("app.tasks.memoir_tasks.ImagePromptOrchestrator") def test_generate_chapter_images_skips_when_lock_is_already_held( self, prompt_service_cls, @@ -120,7 +120,7 @@ class GenerateChapterImagesTaskTest(unittest.TestCase): @patch("app.tasks.memoir_tasks.get_sync_db") @patch("app.tasks.memoir_tasks.TencentCosStorageService") @patch("app.tasks.memoir_tasks.get_image_generator") - @patch("app.tasks.memoir_tasks.MemoirImagePromptService") + @patch("app.tasks.memoir_tasks.ImagePromptOrchestrator") @patch("app.tasks.memoir_tasks._release_chapter_image_lock") @patch("app.tasks.memoir_tasks._acquire_chapter_image_lock", return_value=True) def test_generate_chapter_images_retries_when_any_item_generation_fails( @@ -162,7 +162,7 @@ class GenerateChapterImagesTaskTest(unittest.TestCase): @patch("app.tasks.memoir_tasks.get_sync_db") @patch("app.tasks.memoir_tasks.TencentCosStorageService") @patch("app.tasks.memoir_tasks.get_image_generator") - @patch("app.tasks.memoir_tasks.MemoirImagePromptService") + @patch("app.tasks.memoir_tasks.ImagePromptOrchestrator") @patch("app.tasks.memoir_tasks._release_chapter_image_lock") @patch("app.tasks.memoir_tasks._acquire_chapter_image_lock", return_value=True) def test_generate_chapter_images_marks_successful_item_completed( @@ -203,7 +203,7 @@ class GenerateChapterImagesTaskTest(unittest.TestCase): @patch("app.tasks.memoir_tasks.get_sync_db") @patch("app.tasks.memoir_tasks.TencentCosStorageService") @patch("app.tasks.memoir_tasks.get_image_generator") - @patch("app.tasks.memoir_tasks.MemoirImagePromptService") + @patch("app.tasks.memoir_tasks.ImagePromptOrchestrator") @patch("app.tasks.memoir_tasks.MemoirImageSettings.from_env") def test_generate_chapter_images_returns_disabled_when_feature_flag_is_off( self, @@ -242,7 +242,7 @@ class GenerateChapterImagesTaskTest(unittest.TestCase): @patch("app.tasks.memoir_tasks.get_sync_db") @patch("app.tasks.memoir_tasks.TencentCosStorageService") @patch("app.tasks.memoir_tasks.get_image_generator") - @patch("app.tasks.memoir_tasks.MemoirImagePromptService") + @patch("app.tasks.memoir_tasks.ImagePromptOrchestrator") @patch("app.tasks.memoir_tasks._release_chapter_image_lock") @patch("app.tasks.memoir_tasks._acquire_chapter_image_lock", return_value=True) def test_generate_chapter_images_converts_non_png_payload_before_upload( @@ -287,7 +287,7 @@ class GenerateChapterImagesTaskTest(unittest.TestCase): @patch("app.tasks.memoir_tasks.get_sync_db") @patch("app.tasks.memoir_tasks.TencentCosStorageService") @patch("app.tasks.memoir_tasks.get_image_generator") - @patch("app.tasks.memoir_tasks.MemoirImagePromptService") + @patch("app.tasks.memoir_tasks.ImagePromptOrchestrator") @patch("app.tasks.memoir_tasks._release_chapter_image_lock") @patch("app.tasks.memoir_tasks._acquire_chapter_image_lock", return_value=True) def test_generate_chapter_images_fails_without_retry_on_permanent_cos_error( @@ -330,7 +330,7 @@ class GenerateChapterImagesTaskTest(unittest.TestCase): @patch("app.tasks.memoir_tasks.get_sync_db") @patch("app.tasks.memoir_tasks.TencentCosStorageService") @patch("app.tasks.memoir_tasks.get_image_generator") - @patch("app.tasks.memoir_tasks.MemoirImagePromptService") + @patch("app.tasks.memoir_tasks.ImagePromptOrchestrator") @patch("app.tasks.memoir_tasks._release_chapter_image_lock") @patch("app.tasks.memoir_tasks._acquire_chapter_image_lock", return_value=True) def test_generate_chapter_images_skips_completed_items_for_idempotency(