diff --git a/src/routers/deep_agent_chat.py b/src/routers/deep_agent_chat.py index 42feafd..165aff0 100644 --- a/src/routers/deep_agent_chat.py +++ b/src/routers/deep_agent_chat.py @@ -158,8 +158,12 @@ async def chat_stream(request: DeepAgentChatRequest): "current_image": "" } # 用户上传图片 + input_image_content = '' if request.input_image_paths: - for path in request.input_image_paths: + input_image_content += "\n【附件上传图片路径】\n" + for i, path in enumerate(request.input_image_paths): + input_image_content += f"- 上传图片{i}: {path}\n" + bucket, object_name = path.split('/', 1) image_url = get_presigned_url(oss_client=minio_client, bucket=bucket, object_name=object_name) content.append({"type": "image_url", "image_url": {"url": image_url}}) @@ -167,11 +171,16 @@ async def chat_stream(request: DeepAgentChatRequest): # 用户引用图片 if request.quote_image_path: + input_image_content += "\n【附件引用图片路径】\n" + input_image_content += f"- 引用图片: {request.quote_image_path}\n" + bucket, object_name = request.quote_image_path.split('/', 1) image_url = get_presigned_url(oss_client=minio_client, bucket=bucket, object_name=object_name) content.append({"type": "image_url", "image_url": {"url": image_url}}) files["quote_image"] = request.quote_image_path + if initial_messages: + content[0]['text'] += input_image_content final_messages = { "messages": [ { @@ -181,6 +190,7 @@ async def chat_stream(request: DeepAgentChatRequest): ], "files": files } + logger.info(final_messages) async for stream in main_agent.astream( final_messages, config=current_config, @@ -233,8 +243,6 @@ async def chat_stream(request: DeepAgentChatRequest): tool_content_blocks = tools_token.content_blocks[0] tool_name = tools_token.name logger.info(f"[updates] {tool_name} -- {tool_content_blocks}") - else: - logger.info(f"[updates] -- {chunks}") elif mode == "messages": # logger.info(f"[messages] -- {chunks}") diff --git a/src/server/deep_agent/agents/main_agent.py b/src/server/deep_agent/agents/main_agent.py index 095aa83..ad4e209 100644 --- a/src/server/deep_agent/agents/main_agent.py +++ b/src/server/deep_agent/agents/main_agent.py @@ -1,17 +1,18 @@ from deepagents import create_deep_agent from deepagents.backends import FilesystemBackend -from langchain.agents.middleware import SummarizationMiddleware +from langchain.agents.middleware import SummarizationMiddleware, ToolRetryMiddleware from langgraph.checkpoint.mongodb import MongoDBSaver from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer from langgraph.store.memory import InMemoryStore from pymongo import MongoClient from src.core.config import MONGO_URI -from src.server.deep_agent.agents.painter import build_painter_subagent +# from src.server.deep_agent.agents.painter import build_painter_subagent from src.server.deep_agent.agents.researcher import build_researcher_subagent from src.server.deep_agent.agents.user_profile import user_profile_subagent from src.server.deep_agent.init_llm import build_main_llm from src.server.deep_agent.init_prompt import build_system_prompt +from src.server.deep_agent.tools.generate_furniture_sketch import edit_furniture, generate_furniture, edit_quote_upload_furniture client = MongoClient(MONGO_URI) checkpointer = MongoDBSaver( @@ -42,9 +43,9 @@ class CanvasMiddleware: def build_main_agent(use_report, workspace_dir, enable_thinking): research_subagent = build_researcher_subagent(workspace_dir) - painter_subagent = build_painter_subagent(workspace_dir) + # painter_subagent = build_painter_subagent(workspace_dir) subagents = [ - painter_subagent, + # painter_subagent, research_subagent, user_profile_subagent ] @@ -54,6 +55,7 @@ def build_main_agent(use_report, workspace_dir, enable_thinking): store=InMemoryStore(), subagents=subagents, checkpointer=checkpointer, + tools=[edit_furniture, generate_furniture, edit_quote_upload_furniture], backend=FilesystemBackend( root_dir=workspace_dir, virtual_mode=True, # 重要:關掉虛擬模式 → 真的寫硬碟 @@ -64,6 +66,11 @@ def build_main_agent(use_report, workspace_dir, enable_thinking): trigger=("tokens", 3000), keep=("messages", 100), ), + ToolRetryMiddleware( + max_retries=3, + backoff_factor=2.0, + initial_delay=1.0, + ), ], ) return main_agent diff --git a/src/server/deep_agent/init_prompt.py b/src/server/deep_agent/init_prompt.py index 1572154..02947ca 100644 --- a/src/server/deep_agent/init_prompt.py +++ b/src/server/deep_agent/init_prompt.py @@ -1,3 +1,82 @@ +# system_prompt = f""" +# 你是主调度 Agent(Supervisor),负责理解用户意图并选择合适的子Agent。 +# 当前参数: +# use_report = {use_report} +# 系统中存在两个相关子Agent: +# 1. user_profile_subagent +# 负责收集和维护用户画像信息,包括但不限于: +# - style(风格) +# - room_type(房间类型) +# - budget(预算) +# - 其他报告生成所需信息 +# +# 2. research-subagent +# 负责生成完整报告、调研、总结、分析。 +# +# 3. painter_subagent +# 专门负责家具 sketch 图像的生成与编辑。 +# - 它内部会使用 generate_furniture(支持 num_images,最多4张)和 edit_furniture 工具。 +# - 生成图片后会自动更新到对话上下文中。 +# +# ======================== +# 核心执行规则(严格遵守) +# ======================== +# +# 【1】图像生成任务处理(最重要规则) +# 当用户请求生成家具图片时(包含“生成”“画”“创建”“给我”“设计”等词): +# - 只能**一次性**调用 painter_subagent **一次**。 +# - 必须在调用时明确告诉它生成的数量。 +# - **无论用户要求多少张,painter_subagent 最多只能生成 4 张**。 +# - 不要多次调用 painter_subagent 来凑数量。 +# - 示例: +# - 用户说“生成10张” → 你应该调用 painter_subagent 并指示“生成4张”(因为上限是4),然后直接结束,不再继续调用。 +# - 用户说“生成3张不同风格的椅子” → 调用一次 painter_subagent 并指示生成3张。 +# +# 【2】调用 painter_subagent 的正确方式 +# 在给 painter_subagent 的指令中必须包含: +# - 用户想要生成的数量(但提醒它上限为4张)。 +# - 详细的生成需求(风格、类型、材质等)。 +# - 明确说:“最多只能生成4张,请根据 num_images 参数处理。” +# +# 禁止行为: +# - ❌ 不要连续多次调用 painter_subagent 来生成更多图片。 +# - ❌ 不要把一次生成任务拆成多次调用。 +# - ❌ 用户要求10张时,不要生成4张后再问“还要继续生成吗”而是直接限制在4张并回复。 +# +# 【3】当用户请求报告 / 调研 / 分析 / 总结时: +# 先判断是否已经具备足够的用户画像信息。 +# 如果用户需求信息不足(例如缺少风格、房间类型、预算、主题、范围等): +# → 调用 user_profile_subagent 收集信息 +# 不要直接生成报告。 +# 如果用户画像信息已经完整: +# → 调用 research-subagent 生成报告。 +# ------------------------ +# 【4】当 use_report = False 时: +# - 严禁调用 research-subagent +# - 如果用户明确请求报告、调研、总结、分析: +# +# 请礼貌回复: +# "报告功能当前未开启,你可以打开 use_report=True 后我来帮你生成报告。" +# - 其他普通问题可以正常回答或调用其他子Agent。 +# ------------------------ +# 【5】用户画像优先级规则 +# 只要用户输入包含以下情况: +# - 表达设计需求 +# - 提供偏好信息(例如风格、预算、房间类型) +# - 修改之前的偏好 +# - 补充报告信息 +# 都应该优先调用: +# user_profile_subagent +# 用于更新或收集用户画像。 +# ------------------------ +# 【6】调度原则 +# - user_profile_subagent 只负责 **信息收集** +# - research-subagent 只负责 **报告生成** +# 不要混用职责。 +# ======================== +# """ + + def build_system_prompt(use_report): system_prompt = f""" 你是主调度 Agent(Supervisor),负责理解用户意图并选择合适的子Agent。 @@ -14,15 +93,37 @@ def build_system_prompt(use_report): 2. research-subagent 负责生成完整报告、调研、总结、分析。 - 3. painter_subagent - 负责根据用户描述,构造适用于 生成家具sketch的prompt或编辑家具sketch的prompt - 1.利用prompt用工具生成图片. - 2.利用prompt和图片路径用工具编辑图片. + ======================== + 核心执行规则(必须严格遵守) + ======================== + + 【1】图像生成与编辑任务处理(最重要规则) + 当用户请求生成或修改家具图片时(包含“生成”“画”“创建”“设计”“修改”“帮我改”等词): + + - 只能**一次性**调用图片相关工具(edit_quote_upload_furniture、edit_furniture、generate_furniture 等)。 + - 无论用户要求多少张,生成类工具最多只能生成 4 张。 + - 如果用户消息中出现有效的 MinIO 图片路径,或明确提到“上传的图片”“我提供的图片”“这张图”等 → 优先使用 `edit_quote_upload_furniture`。 + - 如果是本对话中刚刚生成的图片 → 使用 `edit_furniture`。 + + **重要输出规则**: + - 你**绝对不能**在回复中输出任何文件路径、MinIO 路径、图片 URL 或类似 "uploads/..."、"furniture/sketches/..." 的内容。 + - 所有图片都会通过系统其他方式展示给用户,你不需要也不允许展示路径。 + - 你的最终回复只需要关注**工具是否成功调用**。 + - 如果工具调用成功:可以回复类似“已为你生成/修改图片,请查看”或直接不回复(让系统展示图片)。 + - 如果工具调用失败或返回错误信息:可以礼貌告知用户“图片生成/修改失败,请稍后重试”或具体描述错误原因(但仍不要包含任何路径)。 + + 【2】调用图片工具的正确方式 + - 必须一次性调用工具,不要拆分成多次调用。 + - 在给图片工具的指令中,明确说明生成或修改的数量(但上限为4)。 + - 示例:用户说“生成10张” → 只调用一次工具并限制为4张,然后正常回复。 + + **禁止行为**: + - ❌ 不要在任何回复中输出图片路径或文件路径。 + - ❌ 不要多次调用生成工具来凑数量。 + - ❌ 不要把路径告诉用户。 + - ❌ 工具成功后不要描述“生成了哪些路径的图片”。 - ======================== - 执行规则 - ======================== - 【1】当用户请求报告 / 调研 / 分析 / 总结时: + 【3】当用户请求报告 / 调研 / 分析 / 总结时: 先判断是否已经具备足够的用户画像信息。 如果用户需求信息不足(例如缺少风格、房间类型、预算、主题、范围等): → 调用 user_profile_subagent 收集信息 @@ -30,7 +131,7 @@ def build_system_prompt(use_report): 如果用户画像信息已经完整: → 调用 research-subagent 生成报告。 ------------------------ - 【2】当 use_report = False 时: + 【4】当 use_report = False 时: - 严禁调用 research-subagent - 如果用户明确请求报告、调研、总结、分析: @@ -38,7 +139,7 @@ def build_system_prompt(use_report): "报告功能当前未开启,你可以打开 use_report=True 后我来帮你生成报告。" - 其他普通问题可以正常回答或调用其他子Agent。 ------------------------ - 【3】用户画像优先级规则 + 【5】用户画像优先级规则 只要用户输入包含以下情况: - 表达设计需求 - 提供偏好信息(例如风格、预算、房间类型) @@ -48,11 +149,17 @@ def build_system_prompt(use_report): user_profile_subagent 用于更新或收集用户画像。 ------------------------ - 【4】调度原则 + 【6】调度原则 - user_profile_subagent 只负责 **信息收集** - research-subagent 只负责 **报告生成** 不要混用职责。 ======================== + 重要提醒(最高优先级): + 在整个对话过程中,你**绝对禁止**输出任何包含以下内容的文字: + - 以 "uploads/"、"furniture/"、"projects/"、"sketches/" 开头的路径 + - 任何 .png、.jpg 结尾的路径 + - 任何 http 开头的图片链接(除非系统明确要求) + 所有图片展示均由系统统一处理,你只需负责正确调用工具。 """ return system_prompt @@ -71,6 +178,7 @@ def build_painter_prompt(): - 或任何“基于多张图片做合并提取”的表达 👉 必须使用: edit_furniture + 👉 严格要求: - 不允许调用 generate_furniture - 不允许重新生成整张图 @@ -85,43 +193,53 @@ def build_painter_prompt(): 如果用户输入不明确(例如:“改成绿色”): 👉 一律视为【编辑类】 👉 使用 edit_furniture + -------------------------------- - 【二、关于图片来源(关键规则)】 - - 当前系统已经提供了一张“当前图片”(不需要你生成 image_url) - - ❗禁止你自行编造 image_url - - ❗禁止你猜测 image_url - - edit_furniture 会自动从上下文获取图片 + 【二、generate_furniture 参数规则(重要)】 + 当需要生成多张图片时: + - prompt 必须始终描述 **单张家具**(single furniture piece),不要在 prompt 里写入 "Generate 4 different..."、"multiple chairs"、“4 variations”等数量相关的词。 + - 正确的 prompt 风格示例(单张): + "A modern minimalist dining chair made of light oak wood and white leather, with slim metal legs, clean lines, elegant proportions, photographed in a bright Scandinavian living room with natural sunlight, high detail, 8k resolution, professional furniture photography, neutral background." + + - 如何处理不同风格: + - 如果用户想要多种风格(modern, vintage, industrial, minimalist 等),你应该**多次调用 generate_furniture 工具**(每次调用使用不同风格的 prompt,num_images=1)。 + - 但由于系统限制单次用户请求最多生成4张图片: + - 当用户要求生成超过4张或很多变体时,你最多只调用工具4次(或设置 num_images=4,但 prompt 保持 single)。 + - 优先使用 num_images=4 + 一个高质量的 single prompt,让模型自动生成4个轻微不同的变体。 + - 如果用户明确要“明显不同风格”,则分多次调用(但总数量不超过4张)。 + + - num_images 参数: + - 默认 1 + - 最大只能设置为 4 + - 当用户要求10张、8张等时 → 自动限制为 num_images=4,并说明“由于系统限制,最多生成4张” + + 正确调用示例(推荐): + - 用户想要4张不同风格 → 使用 num_images=4 + 一个清晰的 single chair prompt(让模型自然变体),或分4次调用每次1张不同风格。 + - 永远不要把“4 different designs” “generate 4 chairs”这类词写进 prompt 文本中。 -------------------------------- - 【三、参数构造规则】 - 调用 edit_furniture 时: - - 只需要提供: - { - "prompt": "<英文图像编辑描述>" - } - - prompt 要求: - - 清晰描述修改内容 - - 保留原结构(除非用户明确要求改变) - - 示例: - "Change the sofa to green color while keeping the original lines and structure." + 【三、edit_furniture 参数规则】 + - 只需提供 prompt 参数,格式为详细的英文编辑指令。 + - prompt 示例: + "Change the sofa color to deep green while keeping the original modern minimalist style and structure." + - edit_furniture 会自动使用当前上下文中的最新图片,无需你提供 image_url。 -------------------------------- - 【四、禁止行为(强约束)】 - 你绝对不能: - - ❌ 在编辑场景调用 generate_furniture - - ❌ 编造 image_url - - ❌ 忽略“修改类”意图 - - ❌ 因为信息少就拒绝调用工具 + 【四、禁止行为(严格禁止)】 + - ❌ 在编辑意图时调用 generate_furniture + - ❌ 在生成意图时调用 edit_furniture + - ❌ 自行编造 image_url + - ❌ 输出任何工具调用细节、URL、路径给用户 + - ❌ 拒绝调用工具(除非工具本身不可用) -------------------------------- 【五、用户回复规则(必须遵守)】 - 你对用户的最终回复只能是以下格式之一: - - "图片已成功生成!" - - "已按你的要求完成修改,图片已更新!" - ❗禁止输出: - - 路径 - - URL - - 工具参数 - - 解释过程 - -------------------------------- - 现在开始工作。 + - 生成成功时: + - "已为你生成 {num} 张家具设计图!" + - "图片已成功生成,请查看效果。" + + - 编辑成功时: + - "已按你的要求完成修改,图片已更新!" + - "修改完成,新的版本已生成。" + 请根据实际生成/编辑的数量自然调整回复,不要生硬照抄。 + 现在开始工作,请根据用户下一条输入严格遵循以上规则进行工具调用。 """ return prompt diff --git a/src/server/deep_agent/tools/generate_furniture_sketch.py b/src/server/deep_agent/tools/generate_furniture_sketch.py index f184ccc..e154a7d 100644 --- a/src/server/deep_agent/tools/generate_furniture_sketch.py +++ b/src/server/deep_agent/tools/generate_furniture_sketch.py @@ -1,23 +1,25 @@ import uuid +from typing import Optional + import httpx import logging from langchain_core.runnables import RunnableConfig from minio import Minio -from pathlib import Path -from datetime import datetime +# from pathlib import Path +# from datetime import datetime from langchain_core.tools import tool from langgraph.prebuilt import ToolRuntime from src.core.config import settings, MONGO_URI -from src.server.deep_agent.utils.mongodb_util import ThreadImageMinIOStore +# from src.server.deep_agent.utils.mongodb_util import ThreadImageMinIOStore logger = logging.getLogger(__name__) minio_client = Minio(settings.MINIO_URL, access_key=settings.MINIO_ACCESS, secret_key=settings.MINIO_SECRET, secure=settings.MINIO_SECURE) -image_store = ThreadImageMinIOStore(MONGO_URI, "agent_tool_generate_db") +# image_store = ThreadImageMinIOStore(MONGO_URI, "agent_tool_generate_db") @tool -async def generate_furniture(prompt: str, runtime: ToolRuntime): +async def generate_furniture(runtime: ToolRuntime, prompts: list[str] = None, num_images: Optional[int] = 1): """ 使用图像生成模型根据用户提供的详细英文提示词,从零生成一张全新的家具设计草图。 @@ -28,6 +30,7 @@ async def generate_furniture(prompt: str, runtime: ToolRuntime): 参数说明: - prompt (str): **必须是详细的英文提示词**,越详细越好,包含家具类型、风格、颜色、材质、尺寸比例、背景、视角、光影等具体要求。 示例:"Generate a modern minimalist dining chair made of light oak wood and white leather, with slim metal legs, photographed in a bright Scandinavian living room with natural sunlight, high detail, 8k resolution." + - num_images (int, 可选): 要生成的图片数量,默认 1 张。最大只能是 4 张。如果输入超过 4,会自动限制为 4。 返回值: 返回新生成家具图片的 image_url,后续对话中 Agent 可直接引用该图片进行描述、进一步编辑或分析。 @@ -42,85 +45,232 @@ async def generate_furniture(prompt: str, runtime: ToolRuntime): - 生成的图片会自动携带到整个对话上下文中,支持后续使用 edit_furniture 等工具进行迭代修改。 - 如果需要生成多个方案,可以多次调用本工具或在 prompt 中明确要求生成不同变体。 """ - current_checkpoint_id = runtime.store.get(namespace=("image_history",), key="checkpoint_id", ).value.get("current_checkpoint_id") + if num_images is None or num_images < 1: + num_images = 1 + elif num_images > 4: + num_images = 4 + # current_checkpoint_id = runtime.store.get(namespace=("image_history",), key="checkpoint_id", ).value.get("current_checkpoint_id") logger.info(f"\n[系统日志] 正在调用 generate_furniture ...当前checkpoint_id={current_checkpoint_id}") try: - image_url = await generate_or_edit_image(prompt=prompt) + bucket_name = "fida-public-bucket" + object_name = f"furniture/sketches/{uuid.uuid4()}" + image_urls = [] + for i in range(num_images): + image_urls.append(await generate_or_edit_image(prompt=prompts[i], bucket_name=bucket_name, object_name=f"{object_name}-{i}.png")) - if image_url: - image_store.save_image_path(thread_id=current_checkpoint_id, object_path=image_url, metadata={"prompt": prompt, "generated_at": str(datetime.now())}) - return image_url - else: - return "Image generation failed." + # if image_urls: + # image_store.save_image_path(thread_id=current_checkpoint_id, object_path=image_urls, metadata={"prompt": prompt, "generated_at": str(datetime.now())}) + return image_urls + # else: + # return "Image generation failed." except Exception as e: logger.warning(f"绘图流程异常:{e}") return "generate furniture error" @tool -async def edit_furniture(prompt: str, runtime: ToolRuntime, config: RunnableConfig): +async def edit_furniture(runtime: ToolRuntime, config: RunnableConfig, input_image_paths: list[str] = None, prompts: list[str] = None, ): """ - 使用先进的图像编辑模型(image editing model)对家具设计草图进行精准修改。 + 使用先进的图像编辑模型对家具设计草图进行精准修改。 功能说明: - - 根据用户提供的**详细英文提示词**,生成修改后的新家具图片。 + - 支持批量处理多张家具图片,根据对应的提示词生成修改后的新图片。 + - input_image_paths 和 prompts 必须一一对应,数量完全一致。 + - 最多支持同时处理 4 对图片和提示词(即最多 4 张图片)。 参数说明: - - prompt (str): **必须是详细的英文提示词**,描述想要的具体修改(风格、颜色、材质、形状、添加/删除元素、比例等)。 - 示例:"Change the sofa to a modern minimalist style with dark gray fabric and metal legs, add a matching coffee table, make the overall lighting warmer and more luxurious." + + - input_image_paths (list[str]): + 输入图片在 MinIO 中的存储路径列表。 + 示例:["furniture/designs/sofa_concept_v1.png", "projects/room_2026/chair_v2.jpg"] + 注意:路径必须是有效的 MinIO 对象路径,工具会自动下载对应图片。 + + - prompts (list[str]): + 与图片一一对应的详细英文提示词列表。 + 每个提示词描述对对应图片的具体修改要求(风格、颜色、材质、形状、添加/删除元素等)。 + 示例:["Change the sofa to a modern minimalist style with dark gray fabric and metal legs, add a matching coffee table.", + "Convert the chair to Scandinavian Nordic style with light wood and soft beige upholstery."] + + 使用要求(重要): + - input_image_paths 和 prompts 的长度必须完全相同。 + - 列表长度必须在 1 到 4 之间(最多 4 对)。 + - input_image_paths[0] 对应 prompts[0],以此类推,一一对应进行编辑。 使用场景: - - 家具设计迭代 - - 室内设计方案修改 - - 风格转换(现代/北欧/工业风等) - - 材质/颜色调整 + - 家具设计方案迭代 + - 室内设计多方案对比修改 + - 批量风格转换(现代/北欧/工业/奢华风等) + - 材质、颜色、细节批量调整 + 示例调用: + input_image_paths = ["designs/sofa1.png", "designs/chair1.png"] + prompts = [ + "Make the sofa more luxurious with velvet fabric and gold accents.", + "Change the chair to a sleek modern design with black leather and chrome legs." + ] """ - image_history = runtime.store.get(namespace=("image_history",), key="checkpoint_id", ) - last_checkpoint_id = image_history.value.get("last_checkpoint_id") - current_checkpoint_id = image_history.value.get("current_checkpoint_id") - logger.info(f"\n[系统日志] 正在调用 edit_furniture ...current_checkpoint_id={current_checkpoint_id} --- last_checkpoint_id={last_checkpoint_id}") - - input_path = [] - if image_store.get_image_path(last_checkpoint_id): - current_image_path = image_store.get_image_path(last_checkpoint_id).get("current_image_path", False) - else: - current_image_path = None + # image_history = runtime.store.get(namespace=("image_history",), key="checkpoint_id", ) + # last_checkpoint_id = image_history.value.get("last_checkpoint_id") + # current_checkpoint_id = image_history.value.get("current_checkpoint_id") + # + # logger.info(f"\n[系统日志] 正在调用 edit_furniture ...current_checkpoint_id={current_checkpoint_id} --- last_checkpoint_id={last_checkpoint_id}") + # + # if image_store.get_image_path(last_checkpoint_id): + # current_image_path = image_store.get_image_path(last_checkpoint_id).get("current_image_path", False) + # if current_image_path: + # if isinstance(current_image_path, list): + # # 只取最后一张 + # current_image_path = current_image_path[-1] + # else: + # current_image_path = None + # input_path = [] try: - user_input_image_paths = runtime.state.get("files").get("input_image", []) - user_quote_image_path = runtime.state.get("files").get("quote_image", "") - - if current_image_path: - if len(user_input_image_paths) or current_image_path: - for path in user_input_image_paths: - input_path.append(path) - if user_quote_image_path: - input_path.append(user_quote_image_path) - if not len(user_input_image_paths) and not user_quote_image_path: - input_path = [current_image_path] - + # user_input_image_paths = runtime.state.get("files").get("input_image", []) + # user_quote_image_path = runtime.state.get("files").get("quote_image", "") + result = [] + if len(input_image_paths): + for i in range(len(input_image_paths)): bucket_name = "fida-public-bucket" object_name = f"furniture/sketches/{uuid.uuid4()}.png" - image_url = await generate_or_edit_image(input_path=input_path, prompt=prompt, bucket_name=bucket_name, object_name=object_name) - if image_url: - image_store.save_image_path(thread_id=current_checkpoint_id, object_path=image_url, metadata={"prompt": prompt, "generated_at": str(datetime.now())}) - return image_url - else: - return "Image generation failed." - else: - return "The picture to be edited does not exist." - else: - return "No recent image found, please upload or cite it" + image_url = await generate_or_edit_image(input_path=[input_image_paths[i]], prompt=prompts[i], bucket_name=bucket_name, object_name=f"{object_name}-{i}.png") + result.append(image_url) + # image_url = await generate_or_edit_image(input_path=input_path, prompt=prompt, bucket_name=bucket_name, object_name=object_name) + # if image_url: + # image_store.save_image_path(thread_id=current_checkpoint_id, object_path=[image_url], metadata={"prompt": prompt, "generated_at": str(datetime.now())}) + return result + # else: + # return "Image generation failed." + # else: + # return "The picture to be edited does not exist." + # else: + # return "No recent image found, please upload or cite it" except Exception as e: logger.warning(f"edit_furniture error :{e}") return "edit_furniture error" +@tool +async def edit_quote_upload_furniture(image_paths: list[str] = None, mode: str = "auto", prompts: list[str] = None, ): + """ + 使用先进的图像编辑模型对家具图片进行精准批量修改。 + + 支持四种模式: + - one_to_one(最常用):多张图片 + 多个提示词,一一对应编辑 + - one_to_many:多张图片 + 1个提示词(所有图片统一修改) + - many_to_one:1张图片 + 多个提示词(同一张图生成多个不同变体,例如不同颜色) + - many_to_many(新增):多张图片 + 多个提示词,一一对应(多对多交叉编辑) + + 参数说明: + - image_paths (list[str]): MinIO 图片路径列表,长度建议 1~4 + - prompts (list[str]): 详细英文提示词列表 + - mode (str): "one_to_one", "one_to_many", "many_to_one", "many_to_many", "auto"(默认自动判断) + + 使用要求: + - image_paths 长度必须在 1~4 之间 + - mode="auto" 时会根据长度智能判断 + - many_to_many 模式下:image_paths 和 prompts 的长度必须完全相同 + + 示例: + + 示例1:many_to_many(多对多,一一对应) + image_paths = ["sofa1.png", "chair1.png", "table1.png"] + prompts = [ + "Change to bright yellow modern style.", + "Change to deep green luxury style.", + "Change to soft beige Scandinavian style." + ] + mode = "many_to_many" + + 示例2:many_to_one(同一张图多个颜色版本) + image_paths = ["sofa_original.png"] + prompts = ["yellow version", "green version", "blue version", "black version"] + mode = "many_to_one" + """ + try: + # ====================== 参数校验(直接返回错误信息) ====================== + if not image_paths or len(image_paths) < 1 or len(image_paths) > 4: + return f"参数错误:image_paths 必须提供,且长度需要在 1 到 4 张之间。目前收到 {len(image_paths) if image_paths else 0} 张。" + + if not prompts: + return "参数错误:prompts 不能为空,请至少提供一个修改提示词。" + + if mode not in ["one_to_one", "one_to_many", "many_to_one", "many_to_many", "auto"]: + return f"参数错误:mode 参数无效。可用值:one_to_one, one_to_many, many_to_one, many_to_many, auto。当前收到:{mode}" + + # Auto 模式智能判断 + if mode == "auto": + if len(image_paths) == 1 and len(prompts) > 1: + mode = "many_to_one" + elif len(prompts) == 1: + mode = "one_to_many" + elif len(image_paths) == len(prompts): + mode = "many_to_many" # 新增:数量相等时优先 many_to_many + else: + mode = "one_to_one" + + # 各模式严格校验 + if mode == "many_to_one": + if len(image_paths) != 1: + return f"参数错误:many_to_one 模式只能传入 1 张图片,当前传入了 {len(image_paths)} 张。" + if len(prompts) < 1: + return "参数错误:many_to_one 模式下 prompts 至少需要 1 个。" + + elif mode == "one_to_many": + if len(prompts) != 1: + return f"参数错误:one_to_many 模式下 prompts 必须只有 1 个,当前有 {len(prompts)} 个。" + + elif mode in ["one_to_one", "many_to_many"]: + if len(prompts) != len(image_paths): + return (f"参数错误:{mode} 模式下 image_paths 和 prompts 数量必须完全一致。\n" + f"当前 image_paths 有 {len(image_paths)} 张,prompts 有 {len(prompts)} 个。") + + # ====================== 执行编辑 ====================== + result = [] + bucket_name = "fida-public-bucket" + + if mode == "many_to_one": + # 同一张图片 + 多个 prompt + base_image = image_paths[0] + for i, prompt in enumerate(prompts): + object_name = f"furniture/sketches/{uuid.uuid4()}.png" + image_url = await generate_or_edit_image( + input_path=[base_image], + prompt=prompt, + bucket_name=bucket_name, + object_name=f"{object_name}-var{i}.png" + ) + result.append(image_url) + + else: + # one_to_one、many_to_many、one_to_many 统一处理 + for i in range(len(image_paths)): + # 根据模式决定当前使用的 prompt + if mode == "one_to_many": + current_prompt = prompts[0] + else: + current_prompt = prompts[i] # one_to_one 和 many_to_many 都用对应位置的 prompt + + object_name = f"furniture/sketches/{uuid.uuid4()}.png" + image_url = await generate_or_edit_image( + input_path=[image_paths[i]], + prompt=current_prompt, + bucket_name=bucket_name, + object_name=f"{object_name}-{i}.png" + ) + result.append(image_url) + + return result + + except Exception as e: + logger.error(f"edit_quote_upload_furniture 执行异常: {e}", exc_info=True) + return f"工具执行失败:{str(e)},请检查参数后重试。" + + async def generate_or_edit_image(input_path=None, bucket_name="fida-public-bucket", object_name=f"furniture/sketches/{uuid.uuid4()}.png", prompt="Generate a modern minimalist dining chair made of light " diff --git a/src/server/deep_agent/utils/mongodb_util.py b/src/server/deep_agent/utils/mongodb_util.py index c3984e0..7b92434 100644 --- a/src/server/deep_agent/utils/mongodb_util.py +++ b/src/server/deep_agent/utils/mongodb_util.py @@ -53,7 +53,7 @@ class ThreadImageMinIOStore: def save_image_path( self, thread_id: str, - object_path: str, # MinIO 中的相對路徑,例如 "test/123.png" 或 "images/20250320/abc.png" + object_path: list, # MinIO 中的相對路徑,例如 "test/123.png" 或 "images/20250320/abc.png" metadata: Optional[dict] = None ) -> bool: """ @@ -137,7 +137,7 @@ if __name__ == '__main__': image_store = ThreadImageMinIOStore(MONGO_URI, "agent_tool_generate_db") success = image_store.save_image_path( thread_id="121233", - object_path="test/123.png", + object_path=["test/123.png"], metadata={"prompt": "prompt", "generated_at": str(datetime.now())}) print(success) info = image_store.get_image_path("121233") diff --git a/src/server/utils/new_oss_client.py b/src/server/utils/new_oss_client.py index d174779..15fca5d 100644 --- a/src/server/utils/new_oss_client.py +++ b/src/server/utils/new_oss_client.py @@ -182,10 +182,10 @@ def check_and_extract_minio_image(url: str) -> dict[str, str]: if __name__ == '__main__': - url = 'fida-test/furniture/sketches/1b82b2db-8019-4796-b2cc-11fb24c7799d.png' - read_type = "2" - img = oss_get_image(oss_client=minio_client, bucket=url.split('/')[0], object_name=url[url.find('/') + 1:]) - img.show() - img.save("result.png") + urls = ["fida-public-bucket/furniture/sketches/0193c9b2-d8dd-40fc-b715-3ce0daab7abf.png-0.png", "fida-public-bucket/furniture/sketches/bab54cdf-0a60-4806-8c6b-17b836aec1eb.png-1.png", "fida-public-bucket/furniture/sketches/6c993266-95d2-42ee-826b-933b0e344b81.png-2.png"] + # read_type = "2" + for url in urls: + img = oss_get_image(oss_client=minio_client, bucket=url.split('/')[0], object_name=url[url.find('/') + 1:]) + img.show() + # img.save("result.png") # get_presigned_url(oss_client=minio_client, bucket="fida-test", object_name="furniture/sketches/07bf4cfe-4502-4821-b78f-7727bf409498.png") -#