feat: 生成图片提示词agent结构封装

This commit is contained in:
yangshilin
2026-03-19 10:43:34 +08:00
parent 4a1d6f0dcc
commit c21cda3e78
7 changed files with 131 additions and 12 deletions

View File

@@ -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",
]

View File

@@ -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",
]

View File

@@ -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,
)

View File

@@ -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,
)

View File

@@ -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", ""),

View File

@@ -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(

View File

@@ -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(