From 3f34bb005cd9d084b9fa7356a87c44d0115b8a03 Mon Sep 17 00:00:00 2001 From: zcr Date: Wed, 1 Apr 2026 15:41:27 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=94=BB=E5=B8=83=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E5=8A=A9=E6=89=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 2 + src/routers/canvas_assistant.py | 121 ++++++++++++++++++++++++ src/schemas/canvas_assistant.py | 31 ++++++ src/server/canvas_assistant/__init__.py | 0 src/server/canvas_assistant/graph.py | 74 +++++++++++++++ 5 files changed, 228 insertions(+) mode change 100644 => 100755 main.py create mode 100755 src/routers/canvas_assistant.py create mode 100755 src/schemas/canvas_assistant.py create mode 100755 src/server/canvas_assistant/__init__.py create mode 100755 src/server/canvas_assistant/graph.py diff --git a/main.py b/main.py old mode 100644 new mode 100755 index c938c96..a6a9610 --- a/main.py +++ b/main.py @@ -9,6 +9,7 @@ from src.routers import deep_agent_chat from src.routers import generate_3D from src.routers import flux2_gen_img from src.routers import seg_furniture +from src.routers import canvas_assistant logging.config.dictConfig(LOGGER_CONFIG_DICT) @@ -31,6 +32,7 @@ app_server.include_router(deep_agent_chat.router) app_server.include_router(generate_3D.router) app_server.include_router(flux2_gen_img.router) app_server.include_router(seg_furniture.router) +app_server.include_router(canvas_assistant.router) @app_server.get("/") diff --git a/src/routers/canvas_assistant.py b/src/routers/canvas_assistant.py new file mode 100755 index 0000000..359a8e4 --- /dev/null +++ b/src/routers/canvas_assistant.py @@ -0,0 +1,121 @@ +import json +import logging +from typing import AsyncGenerator + +import uuid +from fastapi import APIRouter +from starlette.responses import StreamingResponse + +from src.schemas.canvas_assistant import TriggerRequest +from src.server.canvas_assistant.graph import graph + +router = APIRouter(prefix="/canvas", tags=["Furniture Canvas assistant"]) +logger = logging.getLogger(__name__) + + +async def stream_fiphant(req: TriggerRequest) -> AsyncGenerator[str, None]: + thread_id = f"canvas_{str(uuid.uuid4())}" + config = {"configurable": {"thread_id": thread_id}} + + input_state = { + "messages": [], + "trigger": req.tool_name if req.action == "tool_trigger" else None, + "language": req.language, + "is_first_enter": req.action == "enter_canvas" + } + + yield f"data: {json.dumps({'thread_id': thread_id, 'status': 'start'}, ensure_ascii=False)}\n\n" + + async for event in graph.astream(input_state, config, stream_mode="messages", version="v2"): + if event["type"] == "messages": + msg, metadata = event["data"] + payload_out = { + "node": metadata.get("langgraph_node", "unknown"), + "is_delta": True, + "content": msg.content, + "type": "text" + } + yield f"data: {json.dumps(payload_out, ensure_ascii=False)}\n\n" + + yield f"data: {json.dumps({'status': 'end'}, ensure_ascii=False)}\n\n" + + +@router.post("/assistant") +async def canvas_assistant(req: TriggerRequest): + """ + ### 接口说明 + 触发 Fiphant 设计助手返回消息。 + + 支持两种场景: + - 用户进入画布时,自动返回欢迎引导语 + - 用户点击画布中的工具按钮时,返回对应工具的使用说明 + + ### 参数说明: + - **action**: 操作类型(必填) + - `enter_canvas`: 用户进入画布时调用(返回欢迎语) + - `tool_trigger`: 用户点击工具按钮时调用(返回工具说明) + + - **tool_name**: 工具名称(当 action 为 tool_trigger 时必填) + 支持以下值: + - `to_real_style` + - `surface_edit_canvas` + - `surface_edit_ai` + - `color_palette` + - `scene_composition` + - `3d_model` + - `to_3d_view` + + - **language**: 返回语言(非必填,默认 zh) + - `zh`: 中文 + - `en`: 英文 + + ### 请求体示例: + + **1. 进入画布时调用** + ```json + { + "action": "enter_canvas", + "language": "zh" + } + ``` + 2. 点击工具时调用(推荐) + ```JSON + { + "action": "tool_trigger", + "tool_name": "3d_model", + "language": "zh" + } + ``` + 3. 点击工具 - 英文版 + ```JSON + { + "action": "tool_trigger", + "tool_name": "scene_composition", + "language": "en" + } + ``` + 输出说明: + 返回 Server-Sent Events (SSE) 流式响应,文字会逐句出现,提升用户体验。 + 流式输出示例(实际返回内容): + + 消息开始结束: + ```json + { + "thread_id": "canvas_be76cb75-18ef-4e84-8e30-5d36aef5b83a", + "status": "start" + } + { + "status": "end" + } + ``` + 正文消息: + ```json + { + "node": "assistant", + "is_delta": true, + "content": "Hi,我是你的设计助手 Fiphant 👋 我来帮你快速上手这个画布。我给你准备了两个起点——你可以用 To Real Style 直接把草图变成效果图,也可以先用 Surface Edit 换个材质或贴上印花。有了产品图之后,我们再一起配色、配场景、看 3D 效果,最后导出三视图就完成了。我建议先从 To Real Style 开始,看看整体感觉 ✨", + "type": "text" + } + ``` + """ + return StreamingResponse(stream_fiphant(req), media_type="text/event-stream") diff --git a/src/schemas/canvas_assistant.py b/src/schemas/canvas_assistant.py new file mode 100755 index 0000000..c28e64c --- /dev/null +++ b/src/schemas/canvas_assistant.py @@ -0,0 +1,31 @@ +from typing import Literal + +from pydantic import BaseModel, Field + + +# ====================== 请求模型 ====================== +class TriggerRequest(BaseModel): + action: Literal["enter_canvas", "tool_trigger"] = Field( + ..., + description="操作类型:enter_canvas = 进入画布,tool_trigger = 点击工具" + ) + + tool_name: str | None = Field( + None, + description="当 action=tool_trigger 时必填。支持的工具:to_real_style, surface_edit_canvas, surface_edit_ai, color_palette, scene_composition, 3d_model, to_3d_view" + ) + + language: Literal["zh", "en"] = Field( + "zh", + description="返回语言:zh=中文,en=英文" + ) + + class Config: + json_schema_extra = { + "example": { + "session_id": "canvas_20260331_001", + "action": "tool_trigger", + "tool_name": "3d_model", + "language": "zh" + } + } diff --git a/src/server/canvas_assistant/__init__.py b/src/server/canvas_assistant/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/src/server/canvas_assistant/graph.py b/src/server/canvas_assistant/graph.py new file mode 100755 index 0000000..a1ad57e --- /dev/null +++ b/src/server/canvas_assistant/graph.py @@ -0,0 +1,74 @@ +from langgraph.graph import StateGraph, START, END +from langgraph.graph.message import MessagesState +from langchain_core.messages import AIMessage + +# ====================== 中英文固定文案 ====================== +PROMPTS = { + # 中文 + "welcome_zh": "Hi,我是你的设计助手 Fiphant 👋 我来帮你快速上手这个画布。我给你准备了两个起点——你可以用 To Real Style 直接把草图变成效果图,也可以先用 Surface Edit 换个材质或贴上印花。有了产品图之后,我们再一起配色、配场景、看 3D 效果,最后导出三视图就完成了。我建议先从 To Real Style 开始,看看整体感觉 ✨", + + "to_real_style_zh": "To Real Style 🎨 这个功能我很喜欢——你只需要把草图丢进来,我来帮你把光影和材质都处理好,直接生成真实感效果图。出来不满意的话就多试几次,每次我都会给你不一样的结果。", + "surface_edit_canvas_zh": "Surface Edit(Canvas 模式) 🪡 如果你对材质有具体想法,可以用这个模式来做。布艺、皮革、木材这些都可以换,也可以把你自己的印花上传进来。这个模式支持你手动精细编辑,想细调哪里都可以。", + "surface_edit_ai_zh": "Surface Edit(AI 模式) 🪡 想快速看看换材质之后的效果?用这个模式,把你想要的材质或印花告诉我,AI 智能贴图帮你一步到位。如果觉得还想再调整细节,随时可以切换到 Canvas 模式继续编辑。", + "color_palette_zh": "Color Palette 🎨 配色交给我来帮你——你选几种喜欢的颜色,我来帮你搭配应用到产品上。我可以一次给你生成好几个方案,你对比着挑就好。", + "scene_composition_zh": "Scene Composition 🛋️ 我来帮你把产品放进一个真实的空间场景里,光影我会自动帮你匹配。我的建议是先出一张背景干净的主图,再出一张有生活感的氛围图,两张配合着用,展示效果会好很多。", + "3d_model_zh": "3D Model 🔄 我把你的效果图变成可以转着看的立体模型,你可以从各个角度检查一下结构。我建议重点看看转角、腿脚比例和座面厚度——这几个地方在草图里不容易发现问题,但打样的时候最容易出偏差,现在发现比较好改。", + "to_3d_view_zh": "To 3D View 📐 我们到最后一步了!我来帮你把 3D 模型导出为前视图、侧视图和俯视图!", + + # English + "welcome_en": "Hi, I'm Fiphant, your design assistant 👋 I'm here to help you get started with the canvas. My suggestion: start with To Real Style ✨", + + "to_real_style_en": "To Real Style 🎨 This is one of my favorite features — just drop in your sketch and I'll handle the lighting and materials to create a photorealistic render.", + "surface_edit_canvas_en": "Surface Edit (Canvas Mode) 🪡 Perfect for precise material control. You can swap fabrics, leather, wood, or upload your own prints.", + "surface_edit_ai_en": "Surface Edit (AI Mode) 🪡 Want quick material preview? Tell me what you want and I'll apply it instantly with AI.", + "color_palette_en": "Color Palette 🎨 Let me handle the colors for you. Pick your favorite colors and I'll generate multiple schemes.", + "scene_composition_en": "Scene Composition 🛋️ I'll place your product in a real scene with matching lighting.", + "3d_model_en": "3D Model 🔄 I'll turn your render into a rotatable 3D model. Check corners, leg proportions, and seat thickness carefully.", + "to_3d_view_en": "To 3D View 📐 Final step! I'll export your 3D model as front, side, and top views." +} + + +class AgentState(MessagesState): + trigger: str | None = None + language: str = "zh" + is_first_enter: bool = True # 新增标志位,控制是否输出欢迎语 + + +# ==================== Nodes ==================== +def assistant_node(state: AgentState): + """路由节点:判断是进入画布还是点击工具""" + if state.get("is_first_enter", True): + # 第一次进入画布 + lang = state.get("language", "zh") + key = f"welcome_{lang}" + content = PROMPTS.get(key, PROMPTS["welcome_zh"]) + return { + "messages": [AIMessage(content=content)], + "is_first_enter": False + } + else: + # 点击工具 + trigger = state.get("trigger") + lang = state.get("language", "zh") + + if trigger: + key = f"{trigger}_{lang}" + content = PROMPTS.get(key, "功能说明加载中...") + else: + content = "请点击工具让我为你说明用法。" if lang == "zh" else "Please click a tool for instructions." + + return { + "messages": [AIMessage(content=content)], + "is_first_enter": False + } + + +# ==================== Graph ==================== +workflow = StateGraph(state_schema=AgentState) + +workflow.add_node("assistant", assistant_node) + +workflow.add_edge(START, "assistant") +workflow.add_edge("assistant", END) + +graph = workflow.compile()