diff --git a/app/api/api_fashion_agent.py b/app/api/api_fashion_agent.py index dc0a779..de98993 100644 --- a/app/api/api_fashion_agent.py +++ b/app/api/api_fashion_agent.py @@ -27,7 +27,7 @@ async def fashion_agent_stream(request_item: FashionAgentRequest): - **call_design**: 是否直接调用 design 生成设计系列 - **design_request_data**: design 请求参数(objects, process_id, requestId, callback_url) - **call_trending**: 是否直接调用 trending 趋势分析 - - **call_explor**: 是否直接调用 explorer 灵感探索 + - **call_explore**: 是否直接调用 explore 灵感探索 - **provider**: 图片源 (pexels/unsplash),默认 unsplash 返回 SSE 事件流: diff --git a/app/schemas/fashion_agent.py b/app/schemas/fashion_agent.py index 245ee65..3699f42 100644 --- a/app/schemas/fashion_agent.py +++ b/app/schemas/fashion_agent.py @@ -6,8 +6,11 @@ from pydantic import BaseModel, Field class FashionAgentRequest(BaseModel): """服装设计 Agent 请求""" + thread_id: str = Field(description="会话id") + message: str = Field(default="", description="用户输入的消息") user_id: str = Field(default="test-agent", description="用户ID,用于生成图片存储路径") + input_images: list[str] = Field(default=[], description="用户上传图片地址") enable_thinking: bool = Field(default=False, description="模型思考是否开启") call_print: bool = Field(default=False, description="是否直接调用 print 生成印花") @@ -22,6 +25,6 @@ class FashionAgentRequest(BaseModel): design_request_data: dict = Field(default={}, description="design 请求参数") call_trending: bool = Field(default=False, description="是否直接调用 trending 趋势分析") - call_explor: bool = Field(default=False, description="是否直接调用 explorer 灵感探索") + call_explore: bool = Field(default=False, description="是否直接调用 explore 灵感探索") provider: Optional[str] = Field(default="unsplash", description="图片源: pexels 或 unsplash") diff --git a/app/service/fashion_agent/graph_node/explorer_graph/graph.py b/app/service/fashion_agent/graph_node/explore_graph/graph.py similarity index 57% rename from app/service/fashion_agent/graph_node/explorer_graph/graph.py rename to app/service/fashion_agent/graph_node/explore_graph/graph.py index 5104d87..bc71d65 100644 --- a/app/service/fashion_agent/graph_node/explorer_graph/graph.py +++ b/app/service/fashion_agent/graph_node/explore_graph/graph.py @@ -9,8 +9,8 @@ from langgraph.graph import END, START, StateGraph from langgraph.graph.message import add_messages from pydantic import BaseModel, Field from langchain_core.runnables import RunnableConfig -from app.service.fashion_agent.graph_node.explorer_graph.tools import explor_tool -from app.service.fashion_agent.init_llm import build_llm +from app.service.fashion_agent.graph_node.explore_graph.tools import explore_tool +from app.service.fashion_agent.init_llm import qwen_plus_llm logger = logging.getLogger() @@ -18,10 +18,10 @@ logger = logging.getLogger() """定义状态""" -class ExplorerState(TypedDict): +class exploreState(TypedDict): messages: Required[Annotated[list[AnyMessage], add_messages]] - input_text: str - search_query: str + input_text: str = "" + search_query: str = "" image_results: list[dict] # 每项包含 image_url 和 minio_path provider: str = "unsplash" # 图片源: "pexels" 或 "unsplash" @@ -29,71 +29,70 @@ class ExplorerState(TypedDict): """节点""" -def extract_input_node(state: ExplorerState) -> dict: +def extract_input_node(state: exploreState) -> dict: """从 messages 中提取用户输入""" - input_text = state["messages"][0].content if state.get("messages") else "" + input_text = state["messages"][-1].content if state.get("messages") else input_text return {"input_text": input_text} class SearchQuery(BaseModel): """搜索关键词""" - query: str = Field(description="用于搜索灵感图片的英文关键词,简洁有力") + query: str = Field(description="用于搜索灵感图片的英文关键词,简洁有力") -# TODO 要考虑搜索图片失败或者图片不存在的情况, 搜索不到 需要调整搜索词或者拆分搜索词,最终失败的话调用mood board生成工具生成, 保证绝对有图片 -async def generate_query_node(state: ExplorerState) -> dict: - """使用 LLM 分析用户输入,生成搜索关键词""" +# TODO 要考虑搜索图片失败或者图片不存在的情况, 搜索不到 需要调整搜索词或者拆分搜索词,最终失败的话调用mood board生成工具生成, 保证绝对有图片 +async def generate_query_node(state: exploreState) -> dict: + """使用 LLM 分析用户输入,生成搜索关键词""" input_text = state["input_text"] - logger.info(f"[Explorer] 用户输入: {input_text}") - llm = build_llm() + logger.info(f"[explore] 用户输入: {input_text}") - structured_llm = llm.with_structured_output(SearchQuery) + structured_llm = qwen_plus_llm.with_structured_output(SearchQuery) messages = [ - SystemMessage(content="""你是一个专业的服装设计师助手。 -根据用户输入,生成一个英文搜索关键词,用于在图片库中搜索服装设计灵感图片(moodboard)。 + SystemMessage(content="""你是专业服装设计师助手. +根据用户中文需求,生成适合时尚灵感图(moodboard)图库搜索的英文关键词短句. -要求: -1. 使用英文,简洁有力 -2. 适合搜索高质量的设计灵感图片 +严格输出规则: +1. 必须返回标准JSON对象,**禁止输出任何额外文字,解释,思考,前言后语**; +2. JSON 只包含一个字段 "query",值为简洁英文搜索词; +3. 关键词简洁,适配高清时尚素材搜索; -例如: -用户输入:"夏季连衣裙,清新风格" -输出:summer dress fresh style"""), +输出格式示例(仅允许输出如下JSON,不要加别的内容): +{"query": "summer dress fresh style"}"""), HumanMessage(content=input_text), ] - result = structured_llm.invoke(messages) - logger.info(f"[Explorer] LLM 生成的搜索关键词: {result.query}") - return {"search_query": result.query} + search_query = result.query + logger.info(f"[explore] LLM 生成的搜索关键词: {search_query}") + return {"search_query": search_query} -async def search_and_upload_node(state: ExplorerState, config: RunnableConfig) -> dict: +async def search_and_upload_node(state: exploreState, config: RunnableConfig) -> dict: """使用搜索关键词获取图片并上传到 minio""" query = state.get("search_query", "") user_id = state.get("user_id", "agent") provider = state.get("provider", "unsplash") try: - results = await explor_tool.ainvoke({"query": query, "per_page": 4, "user_id": user_id, "method": provider}, config=config) + results = await explore_tool.ainvoke({"query": query, "per_page": 4, "user_id": user_id, "method": provider}, config=config) except Exception as e: - logger.error(f"[Explorer] 搜索失败 '{query}': {e}") + logger.error(f"[explore] 搜索失败 '{query}': {e}") results = [] return {"image_results": results} -def summarize_node(state: ExplorerState) -> dict: +def summarize_node(state: exploreState) -> dict: """汇总结果""" input_text = state.get("input_text", "") query = state.get("search_query", "") results = state.get("image_results", []) - result_text = f"【灵感探索 Moodboard】\n\n" - result_text += f"基于您的需求:「{input_text}」\n" - result_text += f"搜索关键词:{query}\n\n" - result_text += f"已为您找到 {len(results)} 张灵感图片:\n" + result_text = f"[灵感探索 Moodboard]\n\n" + result_text += f"基于您的需求: {input_text}\n\n" + result_text += f"搜索关键词:{query}\n\n" + result_text += f"已为您找到 {len(results)} 张灵感图片:\n" for i, item in enumerate(results, 1): result_text += f" {i}. 原图: {item.get('image_url', '')}\n" @@ -105,9 +104,9 @@ def summarize_node(state: ExplorerState) -> dict: """构建图""" -def build_explorer_graph(): +def build_explore_graph(): """构建灵感探索图""" - workflow = StateGraph(ExplorerState) + workflow = StateGraph(exploreState) workflow.add_node("extract_input", extract_input_node) workflow.add_node("generate_query", generate_query_node) @@ -126,10 +125,10 @@ def build_explorer_graph(): if __name__ == "__main__": async def test(): - graph = build_explorer_graph() + graph = build_explore_graph() result = await graph.ainvoke( { - "messages": [HumanMessage(content="夏季连衣裙,清新自然风格")], + "messages": [HumanMessage(content="夏季连衣裙,清新自然风格")], "provider": "unsplash", } ) diff --git a/app/service/fashion_agent/graph_node/explorer_graph/tools.py b/app/service/fashion_agent/graph_node/explore_graph/tools.py similarity index 98% rename from app/service/fashion_agent/graph_node/explorer_graph/tools.py rename to app/service/fashion_agent/graph_node/explore_graph/tools.py index 5e5a3ce..2e84148 100644 --- a/app/service/fashion_agent/graph_node/explorer_graph/tools.py +++ b/app/service/fashion_agent/graph_node/explore_graph/tools.py @@ -15,7 +15,7 @@ class SearchInput(BaseModel): @tool(args_schema=SearchInput) -async def explor_tool( +async def explore_tool( query: str, per_page: int = 4, user_id: str = "agent", method: str = "unsplash", config: RunnableConfig = None ) -> list[dict]: """Search for fashion inspiration images on Unsplash and upload to minio. Returns a list of dicts with image_url and minio_path.""" diff --git a/app/service/fashion_agent/graph_node/logo_graph/graph.py b/app/service/fashion_agent/graph_node/logo_graph/graph.py index 080849c..703a179 100644 --- a/app/service/fashion_agent/graph_node/logo_graph/graph.py +++ b/app/service/fashion_agent/graph_node/logo_graph/graph.py @@ -7,7 +7,7 @@ from langgraph.graph import END, START, StateGraph from langgraph.graph.message import add_messages from pydantic import BaseModel, Field from app.service.fashion_agent.graph_node.logo_graph.tools import generate_logo_tool -from app.service.fashion_agent.init_llm import qwen_plus_llm +from app.service.fashion_agent.init_llm import build_llm, qwen_plus_llm """初始化 LLM TODO 将 API Key 替换为环境变量或者配置文件中的值,避免在代码中硬编码敏感信息""" @@ -17,7 +17,7 @@ from app.service.fashion_agent.init_llm import qwen_plus_llm class LogoState(TypedDict): messages: Required[Annotated[list[AnyMessage], add_messages]] - input_text: str + input_text: str = "" user_id: str = "agent" role: str = "" gender: str = "" @@ -35,14 +35,14 @@ class LogoState(TypedDict): # 定义输出结构 class LogoPrompt(BaseModel): - """生成的 Logo 图像提示词""" + """logo image generation diagram prompt words""" - prompts: list[str] = Field(description="用于生成 Logo 的详细提示词") + prompts: list[str] = Field(description="Array of prompt words, simple English words") def extract_input_node(state: LogoState) -> dict: """从 messages 中提取用户输入""" - input_text = state["messages"][0].content if state.get("messages") else "" + input_text = state["messages"][-1].content if state.get("messages") else state["input_text"] return {"input_text": input_text} @@ -51,18 +51,19 @@ def generate_logo_prompt_node(state: LogoState) -> dict: structured_llm = qwen_plus_llm.with_structured_output(LogoPrompt) messages = [ - SystemMessage(content="""从用户输入中提取核心主题词,只输出一个简单的英文单词。 + SystemMessage(content="""从用户输入中提取核心主题词,用一个简单的英文单词作为 prompts 字段的值。 例如: - - "我想要一个猫咪图案" -> "cat" - - "设计一个花朵" -> "flower" - - "可爱的狗" -> "dog" - 只输出单词,不要其他内容。"""), + - "我想要一个猫咪图案" -> {"prompts": ["cat"]} + - "设计一个花朵" -> {"prompts": ["flower"]} + - "可爱的狗" -> {"prompts": ["dog"]} + 请严格按照 JSON 格式输出,包含 prompts 字段。 + """), HumanMessage(content=state["input_text"]), ] result = structured_llm.invoke(messages) prompts = result.prompts - + print(result) return { "logo_prompts": prompts, } @@ -139,14 +140,14 @@ async def main(test_input, user_id="agent", need_prompt_generation=True): if __name__ == "__main__": - # 测试示例 1: 需要 prompt 生成(默认)- 简单关键词输入 - test_input = "我想要一个金毛图案" - result = asyncio.run(main(test_input, need_prompt_generation=True)) - print("=== 需要 prompt 生成 ===") - print(f"Result: {result}") - # 测试示例 2: 直接使用用户提供的 prompt - user_prompt = "golden retriever" - result = asyncio.run(main(user_prompt, need_prompt_generation=False)) - print("\n=== 直接使用 prompt ===") - print(f"Result: {result}") + async def test(): + graph = build_logo_graph() + result = await graph.ainvoke( + { + "messages": [HumanMessage(content="我想要一个金毛图案")], + } + ) + print(result["messages"][-1].content) + + asyncio.run(test()) diff --git a/app/service/fashion_agent/graph_node/node_tools/generate_image.py b/app/service/fashion_agent/graph_node/node_tools/generate_image.py index c595664..9a98175 100644 --- a/app/service/fashion_agent/graph_node/node_tools/generate_image.py +++ b/app/service/fashion_agent/graph_node/node_tools/generate_image.py @@ -4,13 +4,14 @@ import httpx async def generate_image( bucket_name="fida-public-bucket", object_name=f"furniture/sketches/123456.png", + input_images=[], prompt="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.", ): request_data = { - "input_image_paths": [], + "input_image_paths": input_images, "prompt": prompt, "bucket_name": bucket_name, "object_name": object_name, diff --git a/app/service/fashion_agent/graph_node/node_tools/pexels_search.py b/app/service/fashion_agent/graph_node/node_tools/pexels_search.py index 9896bab..3f0dd71 100644 --- a/app/service/fashion_agent/graph_node/node_tools/pexels_search.py +++ b/app/service/fashion_agent/graph_node/node_tools/pexels_search.py @@ -63,10 +63,10 @@ async def search_photos(query: str, per_page: int = 4, user_id: str = "agent") - # 上传到 minio(使用线程池避免阻塞事件循环) file_name = f"{uuid7()}.jpg" loop = asyncio.get_event_loop() - minio_url = await loop.run_in_executor(executor, upload_SDXL_image, image, user_id, "explorer", file_name) + minio_url = await loop.run_in_executor(executor, upload_SDXL_image, image, user_id, "explore", file_name) results.append({"image_url": image_url, "minio_path": minio_url}) - logger.info(f"[Explorer] 上传成功: {minio_url}") + logger.info(f"[explore] 上传成功: {minio_url}") except Exception as e: - logger.error(f"[Explorer] 上传失败: {e}") + logger.error(f"[explore] 上传失败: {e}") return results diff --git a/app/service/fashion_agent/graph_node/node_tools/unsplash_search.py b/app/service/fashion_agent/graph_node/node_tools/unsplash_search.py index d491dcd..5735349 100644 --- a/app/service/fashion_agent/graph_node/node_tools/unsplash_search.py +++ b/app/service/fashion_agent/graph_node/node_tools/unsplash_search.py @@ -63,11 +63,11 @@ async def get_random_photos(query: str, count: int = 4, user_id: str = "agent") # 上传到 minio(使用线程池避免阻塞事件循环) file_name = f"{uuid7()}.jpg" loop = asyncio.get_event_loop() - minio_url = await loop.run_in_executor(executor, upload_SDXL_image, image, user_id, "explorer", file_name) + minio_url = await loop.run_in_executor(executor, upload_SDXL_image, image, user_id, "explore", file_name) results.append({"image_url": image_url, "minio_path": minio_url}) - logger.info(f"[Explorer] 上传成功: {minio_url}") + logger.info(f"[explore] 上传成功: {minio_url}") except Exception as e: - logger.error(f"[Explorer] 上传失败: {e}") + logger.error(f"[explore] 上传失败: {e}") return results diff --git a/app/service/fashion_agent/graph_node/print_graph/graph.py b/app/service/fashion_agent/graph_node/print_graph/graph.py index 299fa65..7145225 100644 --- a/app/service/fashion_agent/graph_node/print_graph/graph.py +++ b/app/service/fashion_agent/graph_node/print_graph/graph.py @@ -40,7 +40,7 @@ class PrintPrompt(BaseModel): def extract_input_node(state: PrintState) -> dict: """从 messages 中提取用户输入""" - input_text = state["messages"][0].content if state.get("messages") else "" + input_text = state["messages"][-1].content if state.get("messages") else "" return {"input_text": input_text} @@ -49,13 +49,13 @@ def generate_print_prompt_node(state: PrintState) -> dict: structured_llm = qwen_plus_llm.with_structured_output(PrintPrompt) messages = [ - SystemMessage(content=f"""你是一个专业的印花图案设计师。 - 请根据用户输入,生成用于AI图像生成的印花图案提示词。 + SystemMessage(content=f"""你是一个专业的印花图案设计师. + 请根据用户输入,生成用于AI图像生成的印花图案提示词. - 要求: - 1. 提示词应该详细描述印花图案的样式、元素、颜色、布局 + 要求: + 1. 提示词应该详细描述印花图案的样式,元素,颜色,布局 2. 提示词应该适合用于 Stable Diffusion 图像生成模型 - 3. 提示词应该使用英文,因为图像生成模型对英文理解更好 + 3. 提示词应该使用英文,因为图像生成模型对英文理解更好 4. 提示词数量为 {state.get("print_num", 1)} """), HumanMessage(content=state["input_text"]), @@ -73,17 +73,19 @@ def generate_print_prompt_node(state: PrintState) -> dict: async def generate_print_img_node(state: PrintState) -> dict: - """根据生成的提示词,生成印花图案""" - # 如果 print_prompts 为空,使用 input_text 作为 prompt + """根据生成的提示词,生成印花图案""" + # 如果 print_prompts 为空,使用 input_text 作为 prompt if state.get("print_need_prompt_generation", False): prompts = state["print_prompts"] if state["print_prompts"] else [state["input_text"]] else: input_text = state.get("input_text", "") prompts = [input_text] + input_images = state.get("input_images", []) + print_img_urls = [] for prompt in prompts: - image_url = await generate_print_tool.ainvoke({"prompt": prompt}) + image_url = await generate_print_tool.ainvoke({"prompt": prompt, "input_images": input_images}) print_img_urls.append(image_url) logger.info(f"[Print Graph] Generated print image URL: {image_url}") @@ -94,7 +96,7 @@ async def generate_print_img_node(state: PrintState) -> dict: def should_generate_prompt(state: PrintState) -> str: - """条件分支:判断是否需要生成 prompt""" + """条件分支:判断是否需要生成 prompt""" logger.info( f"[Print Graph] should_generate_prompt: print_need_prompt_generation={state.get('print_need_prompt_generation')}, print_prompts={state.get('print_prompts')}" @@ -106,7 +108,6 @@ def should_generate_prompt(state: PrintState) -> str: def build_print_graph(): - workflow = StateGraph(PrintState) workflow.add_node("extract_input", extract_input_node) workflow.add_node("gen_prompt", generate_print_prompt_node) @@ -145,8 +146,8 @@ async def main(test_input, print_need_prompt_generation=True): if __name__ == "__main__": - # 测试示例 1: 需要 prompt 生成(默认) - test_input = "我想要一个优雅的花卉印花,适合用于连衣裙,颜色以粉色和白色为主" + # 测试示例 1: 需要 prompt 生成(默认) + test_input = "我想要一个优雅的花卉印花,适合用于连衣裙,颜色以粉色和白色为主" result = asyncio.run(main(test_input, print_need_prompt_generation=True)) print("=== 需要 prompt 生成 ===") print(f"Result: {result}") diff --git a/app/service/fashion_agent/graph_node/print_graph/tools.py b/app/service/fashion_agent/graph_node/print_graph/tools.py index 86be8f8..ef7e50b 100644 --- a/app/service/fashion_agent/graph_node/print_graph/tools.py +++ b/app/service/fashion_agent/graph_node/print_graph/tools.py @@ -11,15 +11,16 @@ class GenerateImageToolInput(BaseModel): """Input schema for the Generate Image Tool.""" prompt: str = Field(description="Description of the desired image, e.g., 'A cozy living room with warm lighting and natural textures.'") + input_images: list[str] = Field(default=[], description="Input images for the generation.") @tool(args_schema=GenerateImageToolInput) -async def generate_print_tool(prompt: str) -> str: +async def generate_print_tool(prompt: str, input_images: list[str]) -> str: """Generate an image based on the provided prompt.""" bucket_name = "aida-users" object_name = f"agent_generate_print/{uuid7()}.png" - image_url = await generate_image(prompt=prompt, bucket_name=bucket_name, object_name=object_name) + image_url = await generate_image(prompt=prompt, bucket_name=bucket_name, object_name=object_name, input_images=input_images) return [image_url] diff --git a/app/service/fashion_agent/graph_node/sketch_graph/graph.py b/app/service/fashion_agent/graph_node/sketch_graph/graph.py index 5f251fb..df6e734 100644 --- a/app/service/fashion_agent/graph_node/sketch_graph/graph.py +++ b/app/service/fashion_agent/graph_node/sketch_graph/graph.py @@ -42,7 +42,7 @@ class SketchPrompt(BaseModel): def extract_input_node(state: SketchState) -> dict: """从 messages 中提取用户输入""" - input_text = state["messages"][0].content if state.get("messages") else "" + input_text = state["messages"][-1].content if state.get("messages") else "" return {"input_text": input_text} @@ -80,15 +80,16 @@ async def generate_sketch_img_node(state: SketchState) -> dict: """根据生成的提示词,生成服装草图""" # 如果 sketch_need_prompt_generation=False 且 sketch_prompts 为空,使用模板生成 prompt if not state.get("sketch_need_prompt_generation", False) and not state.get("sketch_prompts"): - input_text = state.get("input_text", "") prompts = [build_sketch_template_prompt(input_text)] else: prompts = state["sketch_prompts"] if state["sketch_prompts"] else [state["input_text"]] + input_images = state.get("input_images", []) + sketch_img_urls = [] for prompt in prompts: - image_url = await generate_sketch_tool.ainvoke({"prompt": prompt}) + image_url = await generate_sketch_tool.ainvoke({"prompt": prompt, "input_images": input_images}) sketch_img_urls.append(image_url) return {"sketch_img_urls": sketch_img_urls} @@ -107,33 +108,26 @@ def should_generate_prompt(state: SketchState) -> str: def build_sketch_graph(): workflow = StateGraph(SketchState) + workflow.add_node("extract_input", extract_input_node) + workflow.add_node("gen_prompt", generate_sketch_prompt_node) workflow.add_node("gen_sketch", generate_sketch_img_node) - workflow.add_edge(START, "gen_sketch") + + # 添加边 + workflow.add_edge(START, "extract_input") + workflow.add_conditional_edges( + "extract_input", + should_generate_prompt, + { + "gen_prompt": "gen_prompt", + "gen_sketch": "gen_sketch", + }, + ) + workflow.add_edge("gen_prompt", "gen_sketch") workflow.add_edge("gen_sketch", END) + graph = workflow.compile() return graph - # workflow = StateGraph(SketchState) - # workflow.add_node("extract_input", extract_input_node) - # workflow.add_node("gen_prompt", generate_sketch_prompt_node) - # workflow.add_node("gen_sketch", generate_sketch_img_node) - - # # 添加边 - # workflow.add_edge(START, "extract_input") - # workflow.add_conditional_edges( - # "extract_input", - # should_generate_prompt, - # { - # "gen_prompt": "gen_prompt", - # "gen_sketch": "gen_sketch", - # }, - # ) - # workflow.add_edge("gen_prompt", "gen_sketch") - # workflow.add_edge("gen_sketch", END) - - # graph = workflow.compile() - # return graph - def build_sketch_template_prompt(input_text: str) -> str: """构建 sketch prompt 模板""" diff --git a/app/service/fashion_agent/graph_node/sketch_graph/tools.py b/app/service/fashion_agent/graph_node/sketch_graph/tools.py index 7142b34..5e0a0c3 100644 --- a/app/service/fashion_agent/graph_node/sketch_graph/tools.py +++ b/app/service/fashion_agent/graph_node/sketch_graph/tools.py @@ -11,15 +11,16 @@ class GenerateImageToolInput(BaseModel): """Input schema for the Generate Image Tool.""" prompt: str = Field(description="Description of the desired image, e.g., 'A cozy living room with warm lighting and natural textures.'") + input_images: list[str] = Field(default=[], description="Input images for the generation.") @tool(args_schema=GenerateImageToolInput) -async def generate_sketch_tool(prompt: str) -> str: +async def generate_sketch_tool(prompt: str, input_images: list[str]) -> str: """Generate an image based on the provided prompt.""" bucket_name = "fida-public-bucket" object_name = f"test/{uuid7()}.png" - image_url = await generate_image(prompt=prompt, bucket_name=bucket_name, object_name=object_name) + image_url = await generate_image(prompt=prompt, bucket_name=bucket_name, object_name=object_name, input_images=input_images) return [image_url] diff --git a/app/service/fashion_agent/main_agent.py b/app/service/fashion_agent/main_agent.py index e112733..94b1f9d 100644 --- a/app/service/fashion_agent/main_agent.py +++ b/app/service/fashion_agent/main_agent.py @@ -1,16 +1,11 @@ -import sys -from pathlib import Path from typing import Annotated, Required, TypedDict -from deepagents import CompiledSubAgent, create_deep_agent from langchain.agents import create_agent -from langchain.tools import tool -from langchain_core.messages import AnyMessage, HumanMessage -from langchain_qwq import ChatQwen +from langchain_core.messages import AnyMessage from langgraph.graph import END, START, StateGraph from langgraph.graph.message import add_messages + from app.service.fashion_agent.graph_node.design_graph.graph import build_design_graph -from app.service.fashion_agent.graph_node.design_graph.tools import design_tool -from app.service.fashion_agent.graph_node.explorer_graph.tools import explor_tool +from app.service.fashion_agent.graph_node.explore_graph.tools import explore_tool from app.service.fashion_agent.graph_node.logo_graph.graph import build_logo_graph from app.service.fashion_agent.graph_node.logo_graph.tools import generate_logo_tool from app.service.fashion_agent.graph_node.print_graph.graph import build_print_graph @@ -18,7 +13,7 @@ from app.service.fashion_agent.graph_node.print_graph.tools import generate_prin from app.service.fashion_agent.graph_node.sketch_graph.graph import build_sketch_graph from app.service.fashion_agent.graph_node.sketch_graph.tools import generate_sketch_tool from app.service.fashion_agent.graph_node.trending_graph.trending_graph import build_trending_graph -from app.service.fashion_agent.graph_node.explorer_graph.graph import build_explorer_graph +from app.service.fashion_agent.graph_node.explore_graph.graph import build_explore_graph from app.service.fashion_agent.init_llm import build_llm print_graph = build_print_graph() @@ -26,13 +21,16 @@ logo_graph = build_logo_graph() sketch_graph = build_sketch_graph() design_graph = build_design_graph() trending_graph = build_trending_graph() -explorer_graph = build_explorer_graph() +explore_graph = build_explore_graph() class MainState(TypedDict): # 消息 messages: Required[Annotated[list[AnyMessage], add_messages]] + # 上传图片 + input_images: list[str] = [] + # 模块控制 call_design: bool = False call_print: bool = False @@ -40,7 +38,7 @@ class MainState(TypedDict): call_sketch: bool = False call_design: bool = False call_trending: bool = False - call_explor: bool = False + call_explore: bool = False # design参数 design_request_data: dict = {} @@ -58,7 +56,7 @@ class MainState(TypedDict): print_img_urls: list[str] = [] -tools = [explor_tool, generate_logo_tool, generate_print_tool, generate_sketch_tool] +tools = [explore_tool, generate_logo_tool, generate_print_tool, generate_sketch_tool] def route_node(state: MainState) -> str: @@ -73,12 +71,12 @@ def route_node(state: MainState) -> str: return "direct_design" if state.get("call_trending"): return "direct_trending" - if state.get("call_explor"): - return "direct_explor" + if state.get("call_explore"): + return "direct_explore" return "llm_agent" -def build_main_graph(enable_thinking: bool = False): +async def build_main_graph(enable_thinking: bool = False, checkpointer=None): llm = build_llm(enable_thinking=enable_thinking) chat_agent = create_agent( model=llm, tools=tools, state_schema=MainState, system_prompt="你是一个专业的服装设计助手。根据用户需求,调用合适的工具完成任务." @@ -94,7 +92,7 @@ def build_main_graph(enable_thinking: bool = False): workflow.add_node("direct_sketch", sketch_graph) workflow.add_node("direct_design", design_graph) workflow.add_node("direct_trending", trending_graph) - workflow.add_node("direct_explor", explorer_graph) + workflow.add_node("direct_explore", explore_graph) # 条件分支 workflow.add_conditional_edges( @@ -107,7 +105,7 @@ def build_main_graph(enable_thinking: bool = False): "direct_sketch": "direct_sketch", "direct_design": "direct_design", "direct_trending": "direct_trending", - "direct_explor": "direct_explor", + "direct_explore": "direct_explore", }, ) @@ -118,27 +116,7 @@ def build_main_graph(enable_thinking: bool = False): workflow.add_edge("direct_sketch", END) workflow.add_edge("direct_design", END) workflow.add_edge("direct_trending", END) - workflow.add_edge("direct_explor", END) + workflow.add_edge("direct_explore", END) - return workflow.compile() - - -agent = build_main_graph() - - -if __name__ == "__main__": - import asyncio - - async def test_direct(): - # 直接调用 sketch,跳过 LLM - result = await agent.ainvoke( - { - "messages": [HumanMessage(content="我想设计衬衫,带有猫咪的图案")], - "call_sketch": True, - "sketch_need_prompt_generation": False, - } - ) - print("=== 直接调用 sketch ===") - print(result["messages"][-1].content) - - asyncio.run(test_direct()) + graph = workflow.compile(checkpointer=checkpointer) + return graph diff --git a/app/service/fashion_agent/service.py b/app/service/fashion_agent/service.py index 7df6e70..40bf8e0 100644 --- a/app/service/fashion_agent/service.py +++ b/app/service/fashion_agent/service.py @@ -1,13 +1,12 @@ import json import logging -import sys -from pathlib import Path from langgraph.stream import ProtocolEvent, StreamChannel, StreamTransformer from app.service.fashion_agent.main_agent import build_main_graph from langgraph.prebuilt import ToolCallTransformer -from typing import AsyncGenerator, TypedDict +from typing import AsyncGenerator from langchain_core.messages import HumanMessage, ToolMessage from app.schemas.fashion_agent import FashionAgentRequest +from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver logger = logging.getLogger() @@ -33,73 +32,83 @@ class FashionAgentService: async def run_stream(self, request: FashionAgentRequest) -> AsyncGenerator[str, None]: """流式运行 agent - 使用 v3 projections""" - config = {"configurable": {"user_id": request.user_id}} + config = {"configurable": {"thread_id": request.thread_id, "user_id": request.user_id}} - agent = build_main_graph(enable_thinking=request.enable_thinking) - state = { - "messages": [HumanMessage(content=request.message)], - "call_print": request.call_print, - "call_logo": request.call_logo, - "call_sketch": request.call_sketch, - "call_design": request.call_design, - "call_trending": request.call_trending, - "call_explor": request.call_explor, - "print_need_prompt_generation": request.print_need_prompt_generation, - "sketch_need_prompt_generation": request.sketch_need_prompt_generation, - "design_request_data": request.design_request_data, - } + async with AsyncPostgresSaver.from_conn_string("postgresql://postgres:Aidlab123123!@20.1.1.43:15432/myapp_prod") as checkpointer: + await checkpointer.setup() + agent = await build_main_graph(enable_thinking=request.enable_thinking, checkpointer=checkpointer) - stream = await agent.astream_events(state, config=config, version="v3", transformers=[ToolCallTransformer, CustomTransformer]) + state = { + "messages": [HumanMessage(content=request.message)], + "input_images": request.input_images, + "call_print": request.call_print, + "call_logo": request.call_logo, + "call_sketch": request.call_sketch, + "call_design": request.call_design, + "call_trending": request.call_trending, + "call_explore": request.call_explore, + "print_need_prompt_generation": request.print_need_prompt_generation, + "sketch_need_prompt_generation": request.sketch_need_prompt_generation, + "design_request_data": request.design_request_data, + } - tool_names = {} - filter_tool_name = ["design_tool"] - async for event in stream: - if event["method"] == "tools": - data = event["params"]["data"] - tool_call_id = data.get("tool_call_id") + stream = await agent.astream_events(state, config=config, version="v3", transformers=[ToolCallTransformer, CustomTransformer]) - # 记录 tool_name - if data.get("event") == "tool-started": - tool_names[tool_call_id] = data.get("tool_name") + tool_names = {} + filter_tool_name = ["design_tool"] + async for event in stream: + if event["method"] == "tools": + data = event["params"]["data"] + tool_call_id = data.get("tool_call_id") - # 通过 ID 查找 tool_name - elif data.get("event") == "tool-finished": - tool_name = tool_names.get(tool_call_id, "unknown") + # 记录 tool_name + if data.get("event") == "tool-started": + tool_names[tool_call_id] = data.get("tool_name") - if tool_name in filter_tool_name: + # 通过 ID 查找 tool_name + elif data.get("event") == "tool-finished": + tool_name = tool_names.get(tool_call_id, "unknown") + + if tool_name in filter_tool_name: + continue + + data["tool_name"] = tool_name + + if isinstance(data["output"], ToolMessage): + data["output"] = json.loads(data["output"].content) + + response_event = {"event_type": "tool", "data": data} + yield f"data: {json.dumps(response_event, ensure_ascii=False)}\n\n" + + elif event["method"] == "custom": + data = event["params"]["data"] + response_event = {"event_type": "tool", "data": data} + yield f"data: {json.dumps(response_event, ensure_ascii=False)}\n\n" + + elif event["method"] == "messages": + event_data = event["params"]["data"] + data = event_data[0] if len(event_data) > 0 else {} + # 提取元数据 (如果有的话) + metadata = event_data[1] if len(event_data) > 1 else {} + if not isinstance(data, dict): + continue + if metadata.get("langgraph_node") in {"gen_prompt", "generate_query"}: continue - data["tool_name"] = tool_name + ev = data.get("event") - if isinstance(data["output"], ToolMessage): - data["output"] = json.loads(data["output"].content) - response_event = {"event_type": "tool", "data": data} - yield f"data: {json.dumps(response_event, ensure_ascii=False)}\n\n" + if ev == "content-block-delta": + block = data.get("delta") or {} + if block.get("type") in ("text-delta", "reasoning-delta"): + response_event = {"event_type": "messages", "data": {"event": ev} | block} + yield f"data: {json.dumps(response_event, ensure_ascii=False)}\n\n" - elif event["method"] == "custom": - data = event["params"]["data"] - response_event = {"event_type": "tool", "data": data} - yield f"data: {json.dumps(response_event, ensure_ascii=False)}\n\n" + elif ev in ("message-start", "content-block-start", "content-block-finish", "message-finish"): + response_event = {"event_type": "messages", "data": {"event": ev} | data} + yield f"data: {json.dumps(response_event, ensure_ascii=False)}\n\n" - elif event["method"] == "messages": - data = event["params"]["data"][0] - if not isinstance(data, dict): - continue - if data.get("event") != "content-block-delta": - continue - block = data.get("delta") or {} - - if block.get("type") == "text-delta": - response_event = {"event_type": "messages", "data": {"event": data["event"]} | block} - yield f"data: {json.dumps(response_event, ensure_ascii=False)}\n\n" - elif block.get("type") == "reasoning-delta": - response_event = {"event_type": "messages", "data": {"event": data["event"]} | block} - yield f"data: {json.dumps(response_event, ensure_ascii=False)}\n\n" - else: - pass - # print(f"----------------{event}") - response_event = {"event_type": "done"} - yield f"data: {response_event}" + response_event = {"event_type": "done"} + yield f"data: {response_event}" if __name__ == "__main__": @@ -117,13 +126,15 @@ if __name__ == "__main__": print("测试流式输出") print("=" * 50) request = FashionAgentRequest( - message="生成一张草莓图案", - call_print=True, + thread_id="zhh", + message="落日", + # call_print=True, + # input_images=["test/53d38bd5-f77b-4034-ada2-45f1e2ebe00c.png"], # print_need_prompt_generation=False, # call_sketch=True, # sketch_need_prompt_generation=False, # call_logo=True, - # call_explor=True, + call_explore=True, # call_design=True, # design_request_data=request_data, ) diff --git a/pyproject.toml b/pyproject.toml index 3748643..883600d 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,11 +19,15 @@ dependencies = [ "fastapi[standard]>=0.125.0", "image>=1.5.33", "langchain>=1.2.0", + "langchain-anthropic>=1.4.4", "langchain-community>=0.4.1", + "langchain-core>=1.4.2", + "langchain-ollama>=1.1.0", "langchain-openai>=1.2.2", "langchain-qwq>=0.3.5", "langgraph>=1.0.5", "langgraph-api>=0.4.28", + "langgraph-checkpoint-postgres>=3.1.0", "langgraph-cli[inmem,redis]<=0.4.26", "langsmith>=0.8.11", "load>=1.0.14", @@ -40,6 +44,7 @@ dependencies = [ "pandas-stubs~=2.3.3", "pika>=1.3.2", "pillow>=12.0.0", + "psycopg[binary,pool]>=3.3.4", "pyasyncore>=1.0.4", "pydantic>=2.12.5", "pydantic-core>=2.41.5", diff --git a/uv.lock b/uv.lock index 87e4493..00d1ebc 100755 --- a/uv.lock +++ b/uv.lock @@ -1799,6 +1799,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/a2/5feaf21cfe6fac80eae944f3ac5348d9e5e986813256f74f8dd104617474/langchain_google_genai-4.2.4-py3-none-any.whl", hash = "sha256:0e2c1021a15c91e60b68d813bb3e793bd1d9396b3f8639b943ab4e56e5652e04", size = 68832, upload-time = "2026-05-28T21:22:59.291Z" }, ] +[[package]] +name = "langchain-ollama" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "ollama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/9b/6641afe8a5bf807e454fd464eddfc7eb2f2df53cb0b29744381171f9c609/langchain_ollama-1.1.0.tar.gz", hash = "sha256:f776f56f6782ae4da7692579b94a6575906118318d1023b455d7207f9d059811", size = 133075, upload-time = "2026-04-07T02:48:00.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/b2/c2acb076590a98bee2816ed5f285e00df162a34238f9e276e175e14ebc35/langchain_ollama-1.1.0-py3-none-any.whl", hash = "sha256:43ac83a6eacb0f43855810739794dd55019e0d9b17bdcf3ecb3b1991ac3b59dd", size = 31413, upload-time = "2026-04-07T02:47:59.642Z" }, +] + [[package]] name = "langchain-openai" version = "1.2.2" @@ -1919,6 +1932,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/b4/71425e3e38be92611300b9cc5e46a5bf98ab23f5ea8a75b73d02a2f1413c/langgraph_checkpoint-4.1.1-py3-none-any.whl", hash = "sha256:25d29144b082827218e7bc3f1e9b0566a4bb007895cd6cc26f66a8428739f56e", size = 56212, upload-time = "2026-05-22T16:57:37.203Z" }, ] +[[package]] +name = "langgraph-checkpoint-postgres" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langgraph-checkpoint" }, + { name = "orjson" }, + { name = "psycopg" }, + { name = "psycopg-pool" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/51/5a2dc42e8b5d5942b933b5b7237eae5a4dbc92508a04727c263dd383ad8a/langgraph_checkpoint_postgres-3.1.0.tar.gz", hash = "sha256:02bff4ab63d9dae8eab3a9640fce1d479da8965c9fba7b0dc04cb1f7c56f0a55", size = 148473, upload-time = "2026-05-12T03:40:10.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/cd/eff9b82bc3b5f62d481b437099f44f3ef7b1d907f166fb4ee25e8f84a1e7/langgraph_checkpoint_postgres-3.1.0-py3-none-any.whl", hash = "sha256:814cce2ef35d792bf07b090a95eed004f1acac0724fe6605536b13f6d1e7032c", size = 48988, upload-time = "2026-05-12T03:40:08.925Z" }, +] + [[package]] name = "langgraph-cli" version = "0.4.8" @@ -2831,6 +2859,57 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, ] +[[package]] +name = "psycopg" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799, upload-time = "2026-05-01T23:31:55.179Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001, upload-time = "2026-05-01T23:20:50.816Z" }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] +pool = [ + { name = "psycopg-pool" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/7d/03818e13ba7f36de93573c93ee3482006d3dfa8b0f8d28df511bad0a1a92/psycopg_binary-3.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5ab28a2a7649df3b72e6b674b4c190e448e8e77cf496a65bd846472048de2089", size = 4591122, upload-time = "2026-05-01T23:27:56.162Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b9/11b341edf8d54e2694726b273fe9652b254d989f4f63e3ac6816ad6b55f4/psycopg_binary-3.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6402a9d8146cf4b3974ded3fd28a971e83dc6a0333eb7822524a3aa20b546578", size = 4669943, upload-time = "2026-05-01T23:28:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/18/4665bacd65e7865b4372fcd8abb8b9186ada4b0025f8c2ca691b364a556c/psycopg_binary-3.3.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:580ae30a5f95ccd90008ec697d3ed6a4a2047a516407ad904283fa42086936e9", size = 5469697, upload-time = "2026-05-01T23:28:11.337Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b1/b83136c6e510593d9b0c759ba5384337bc4ad82d19fda675adc4b2703c84/psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7510c37550f91a187e3660a8cc50d4b760f8c3b8b2f89ebc5698cd2c7f2c85d", size = 5152995, upload-time = "2026-05-01T23:28:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/67/8d/a9821e2a648afe6091989929982a3b0f00b2631a859cb81379728f08fb75/psycopg_binary-3.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77df19583501ea288eaf15ac0fe7ad01e6d8091a91d5c41df5c718f307d8e31b", size = 6738180, upload-time = "2026-05-01T23:28:30.654Z" }, + { url = "https://files.pythonhosted.org/packages/7e/58/2e349e8d23905dc2317b80ac65f48fb6f821a4777a4e994a60da91c4850f/psycopg_binary-3.3.4-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:018fbed325936da502feb546642c982dcc4b9ffdea32dfef78dbf3b7f7ad4070", size = 4978828, upload-time = "2026-05-01T23:28:37.277Z" }, + { url = "https://files.pythonhosted.org/packages/45/48/57b00d03b4721878326122a1f1e6b0a90b85bcaec56b5b2f8ea6cfa45235/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17a21953a9e5ff3a16dab692625a3676e2f101db5e40072f39dbee2250194d68", size = 4509757, upload-time = "2026-05-01T23:28:43.078Z" }, + { url = "https://files.pythonhosted.org/packages/25/37/33b47d8c007df69aec500df5889767c4d313748e8e9e27a2fef8a6dabcee/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:eb05ee1c2b817d27c537333224c9e83c7afb86fe7296ba970990068baf819b16", size = 4190546, upload-time = "2026-05-01T23:28:50.016Z" }, + { url = "https://files.pythonhosted.org/packages/ca/c6/32b0835dbc2122617902b649d76a91c1e75406e76bf3d595b0c3bb5ffad6/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:773d573e11f437ce0bdb95b7c18dc58390494f96d43f8b45b9760436114f7652", size = 3926197, upload-time = "2026-05-01T23:28:55.55Z" }, + { url = "https://files.pythonhosted.org/packages/cd/68/d190ef0c0c5b16ded07831dabc8ddd412f4cdab07ec6e30ed38d9bda0e1f/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e55ccbdfae79a2ed9c6369c3008a3025817ff9d7e27b32a2d84e2a4267e66e", size = 4236627, upload-time = "2026-05-01T23:29:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/25/8f/81dcbc2e8454b74d14881275ea45f00791052dac531a9fa8be1730d1685b/psycopg_binary-3.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:494ca54901be8cf9eb7e02c25b731f2317c378efa44f43e8f9bd0e1184ae7be4", size = 3560782, upload-time = "2026-05-01T23:29:11.967Z" }, +] + +[[package]] +name = "psycopg-pool" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/82/7a23d26039827ecd4ebe93905651029ddd307c5182ad59296dfb6f67b528/psycopg_pool-3.3.1.tar.gz", hash = "sha256:b10b10b7a175d5cc1592147dc5b7eec8a9e0834eb3ed2c4a92c858e2f51eb63c", size = 31661, upload-time = "2026-05-01T23:31:59.809Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/ed/89c2c620af0e1660354cd8aabf9f5b21f911597ce22acb37c805d6c86bc8/psycopg_pool-3.3.1-py3-none-any.whl", hash = "sha256:2af5b432941c4c9ad5c87b3fa410aec910ec8f7c122855897983a06c45f2e4b5", size = 40023, upload-time = "2026-05-01T23:31:53.136Z" }, +] + [[package]] name = "psycopg2-binary" version = "2.9.12" @@ -3738,11 +3817,15 @@ dependencies = [ { name = "fastapi", extra = ["standard"] }, { name = "image" }, { name = "langchain" }, + { name = "langchain-anthropic" }, { name = "langchain-community" }, + { name = "langchain-core" }, + { name = "langchain-ollama" }, { name = "langchain-openai" }, { name = "langchain-qwq" }, { name = "langgraph" }, { name = "langgraph-api" }, + { name = "langgraph-checkpoint-postgres" }, { name = "langgraph-cli", extra = ["inmem"] }, { name = "langsmith" }, { name = "load" }, @@ -3759,6 +3842,7 @@ dependencies = [ { name = "pandas-stubs" }, { name = "pika" }, { name = "pillow" }, + { name = "psycopg", extra = ["binary", "pool"] }, { name = "pyasyncore" }, { name = "pydantic" }, { name = "pydantic-core" }, @@ -3797,11 +3881,15 @@ requires-dist = [ { name = "fastapi", extras = ["standard"], specifier = ">=0.125.0" }, { name = "image", specifier = ">=1.5.33" }, { name = "langchain", specifier = ">=1.2.0" }, + { name = "langchain-anthropic", specifier = ">=1.4.4" }, { name = "langchain-community", specifier = ">=0.4.1" }, + { name = "langchain-core", specifier = ">=1.4.2" }, + { name = "langchain-ollama", specifier = ">=1.1.0" }, { name = "langchain-openai", specifier = ">=1.2.2" }, { name = "langchain-qwq", specifier = ">=0.3.5" }, { name = "langgraph", specifier = ">=1.0.5" }, { name = "langgraph-api", specifier = ">=0.4.28" }, + { name = "langgraph-checkpoint-postgres", specifier = ">=3.1.0" }, { name = "langgraph-cli", extras = ["inmem", "redis"], specifier = "<=0.4.26" }, { name = "langsmith", specifier = ">=0.8.11" }, { name = "load", specifier = ">=1.0.14" }, @@ -3818,6 +3906,7 @@ requires-dist = [ { name = "pandas-stubs", specifier = "~=2.3.3" }, { name = "pika", specifier = ">=1.3.2" }, { name = "pillow", specifier = ">=12.0.0" }, + { name = "psycopg", extras = ["binary", "pool"], specifier = ">=3.3.4" }, { name = "pyasyncore", specifier = ">=1.0.4" }, { name = "pydantic", specifier = ">=2.12.5" }, { name = "pydantic-core", specifier = ">=2.41.5" },