重构图像生成和搜索工具;更新主代理来处理输入图像

- 更新了“generate_image.py”以接受输入图像以增强图像生成。
- 修改了`pexels_search.py​​`和`unsplash_search.py​​`以将日志记录和上传路径从“explorer”更改为“explore”。
- 调整了“print_graph”和“sketch_graph”以提取最新的用户输入并处理输入图像以生成打印和草图图像。
- 重构“generate_print_tool”和“generate_sketch_tool”以接受输入图像。
- 更新了“main_agent.py”以包含状态中的输入图像并调整了图形构建过程。
- 增强了“service.py”来管理输入图像并改进了流媒体期间的事件处理。
- 更新了新软件包和版本的“pyproject.toml”和“uv.lock”中的依赖项。
This commit is contained in:
zcr
2026-06-17 11:56:53 +08:00
parent 35e791b4e2
commit b9163f0b46
16 changed files with 296 additions and 212 deletions

View File

@@ -27,7 +27,7 @@ async def fashion_agent_stream(request_item: FashionAgentRequest):
- **call_design**: 是否直接调用 design 生成设计系列 - **call_design**: 是否直接调用 design 生成设计系列
- **design_request_data**: design 请求参数(objects, process_id, requestId, callback_url) - **design_request_data**: design 请求参数(objects, process_id, requestId, callback_url)
- **call_trending**: 是否直接调用 trending 趋势分析 - **call_trending**: 是否直接调用 trending 趋势分析
- **call_explor**: 是否直接调用 explorer 灵感探索 - **call_explore**: 是否直接调用 explore 灵感探索
- **provider**: 图片源 (pexels/unsplash),默认 unsplash - **provider**: 图片源 (pexels/unsplash),默认 unsplash
返回 SSE 事件流: 返回 SSE 事件流:

View File

@@ -6,8 +6,11 @@ from pydantic import BaseModel, Field
class FashionAgentRequest(BaseModel): class FashionAgentRequest(BaseModel):
"""服装设计 Agent 请求""" """服装设计 Agent 请求"""
thread_id: str = Field(description="会话id")
message: str = Field(default="", description="用户输入的消息") message: str = Field(default="", description="用户输入的消息")
user_id: str = Field(default="test-agent", description="用户ID,用于生成图片存储路径") user_id: str = Field(default="test-agent", description="用户ID,用于生成图片存储路径")
input_images: list[str] = Field(default=[], description="用户上传图片地址")
enable_thinking: bool = Field(default=False, description="模型思考是否开启") enable_thinking: bool = Field(default=False, description="模型思考是否开启")
call_print: bool = Field(default=False, description="是否直接调用 print 生成印花") call_print: bool = Field(default=False, description="是否直接调用 print 生成印花")
@@ -22,6 +25,6 @@ class FashionAgentRequest(BaseModel):
design_request_data: dict = Field(default={}, description="design 请求参数") design_request_data: dict = Field(default={}, description="design 请求参数")
call_trending: bool = Field(default=False, description="是否直接调用 trending 趋势分析") 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") provider: Optional[str] = Field(default="unsplash", description="图片源: pexels 或 unsplash")

View File

@@ -9,8 +9,8 @@ from langgraph.graph import END, START, StateGraph
from langgraph.graph.message import add_messages from langgraph.graph.message import add_messages
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from langchain_core.runnables import RunnableConfig from langchain_core.runnables import RunnableConfig
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.init_llm import build_llm from app.service.fashion_agent.init_llm import qwen_plus_llm
logger = logging.getLogger() logger = logging.getLogger()
@@ -18,10 +18,10 @@ logger = logging.getLogger()
"""定义状态""" """定义状态"""
class ExplorerState(TypedDict): class exploreState(TypedDict):
messages: Required[Annotated[list[AnyMessage], add_messages]] messages: Required[Annotated[list[AnyMessage], add_messages]]
input_text: str input_text: str = ""
search_query: str search_query: str = ""
image_results: list[dict] # 每项包含 image_url 和 minio_path image_results: list[dict] # 每项包含 image_url 和 minio_path
provider: str = "unsplash" # 图片源: "pexels" 或 "unsplash" 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 中提取用户输入""" """从 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} return {"input_text": input_text}
class SearchQuery(BaseModel): class SearchQuery(BaseModel):
"""搜索关键词""" """搜索关键词"""
query: str = Field(description="用于搜索灵感图片的英文关键词简洁有力") query: str = Field(description="用于搜索灵感图片的英文关键词,简洁有力")
# TODO 要考虑搜索图片失败或者图片不存在的情况 搜索不到 需要调整搜索词或者拆分搜索词最终失败的话调用mood board生成工具生成 保证绝对有图片 # TODO 要考虑搜索图片失败或者图片不存在的情况, 搜索不到 需要调整搜索词或者拆分搜索词,最终失败的话调用mood board生成工具生成, 保证绝对有图片
async def generate_query_node(state: ExplorerState) -> dict: async def generate_query_node(state: exploreState) -> dict:
"""使用 LLM 分析用户输入生成搜索关键词""" """使用 LLM 分析用户输入,生成搜索关键词"""
input_text = state["input_text"] input_text = state["input_text"]
logger.info(f"[Explorer] 用户输入: {input_text}") logger.info(f"[explore] 用户输入: {input_text}")
llm = build_llm()
structured_llm = llm.with_structured_output(SearchQuery) structured_llm = qwen_plus_llm.with_structured_output(SearchQuery)
messages = [ messages = [
SystemMessage(content="""你是一个专业服装设计师助手 SystemMessage(content="""你是专业服装设计师助手.
根据用户输入生成一个英文搜索关键词用于在图片库中搜索服装设计灵感图片moodboard 根据用户中文需求,生成适合时尚灵感图(moodboard)图库搜索的英文关键词短句.
要求 严格输出规则:
1. 使用英文简洁有力 1. 必须返回标准JSON对象,**禁止输出任何额外文字,解释,思考,前言后语**;
2. 适合搜索高质量的设计灵感图片 2. JSON 只包含一个字段 "query",值为简洁英文搜索词;
3. 关键词简洁,适配高清时尚素材搜索;
例如 输出格式示例(仅允许输出如下JSON,不要加别的内容):
用户输入"夏季连衣裙,清新风格" {"query": "summer dress fresh style"}"""),
输出summer dress fresh style"""),
HumanMessage(content=input_text), HumanMessage(content=input_text),
] ]
result = structured_llm.invoke(messages) result = structured_llm.invoke(messages)
logger.info(f"[Explorer] LLM 生成的搜索关键词: {result.query}") search_query = result.query
return {"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""" """使用搜索关键词获取图片并上传到 minio"""
query = state.get("search_query", "") query = state.get("search_query", "")
user_id = state.get("user_id", "agent") user_id = state.get("user_id", "agent")
provider = state.get("provider", "unsplash") provider = state.get("provider", "unsplash")
try: 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: except Exception as e:
logger.error(f"[Explorer] 搜索失败 '{query}': {e}") logger.error(f"[explore] 搜索失败 '{query}': {e}")
results = [] results = []
return {"image_results": results} return {"image_results": results}
def summarize_node(state: ExplorerState) -> dict: def summarize_node(state: exploreState) -> dict:
"""汇总结果""" """汇总结果"""
input_text = state.get("input_text", "") input_text = state.get("input_text", "")
query = state.get("search_query", "") query = state.get("search_query", "")
results = state.get("image_results", []) results = state.get("image_results", [])
result_text = f"灵感探索 Moodboard\n\n" result_text = f"[灵感探索 Moodboard]\n\n"
result_text += f"基于您的需求:「{input_text}\n" result_text += f"基于您的需求: {input_text}\n\n"
result_text += f"搜索关键词{query}\n\n" result_text += f"搜索关键词:{query}\n\n"
result_text += f"已为您找到 {len(results)} 张灵感图片\n" result_text += f"已为您找到 {len(results)} 张灵感图片:\n"
for i, item in enumerate(results, 1): for i, item in enumerate(results, 1):
result_text += f" {i}. 原图: {item.get('image_url', '')}\n" 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("extract_input", extract_input_node)
workflow.add_node("generate_query", generate_query_node) workflow.add_node("generate_query", generate_query_node)
@@ -126,10 +125,10 @@ def build_explorer_graph():
if __name__ == "__main__": if __name__ == "__main__":
async def test(): async def test():
graph = build_explorer_graph() graph = build_explore_graph()
result = await graph.ainvoke( result = await graph.ainvoke(
{ {
"messages": [HumanMessage(content="夏季连衣裙清新自然风格")], "messages": [HumanMessage(content="夏季连衣裙,清新自然风格")],
"provider": "unsplash", "provider": "unsplash",
} }
) )

View File

@@ -15,7 +15,7 @@ class SearchInput(BaseModel):
@tool(args_schema=SearchInput) @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 query: str, per_page: int = 4, user_id: str = "agent", method: str = "unsplash", config: RunnableConfig = None
) -> list[dict]: ) -> list[dict]:
"""Search for fashion inspiration images on Unsplash and upload to minio. Returns a list of dicts with image_url and minio_path.""" """Search for fashion inspiration images on Unsplash and upload to minio. Returns a list of dicts with image_url and minio_path."""

View File

@@ -7,7 +7,7 @@ from langgraph.graph import END, START, StateGraph
from langgraph.graph.message import add_messages from langgraph.graph.message import add_messages
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.service.fashion_agent.graph_node.logo_graph.tools import generate_logo_tool 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 替换为环境变量或者配置文件中的值,避免在代码中硬编码敏感信息""" """初始化 LLM TODO 将 API Key 替换为环境变量或者配置文件中的值,避免在代码中硬编码敏感信息"""
@@ -17,7 +17,7 @@ from app.service.fashion_agent.init_llm import qwen_plus_llm
class LogoState(TypedDict): class LogoState(TypedDict):
messages: Required[Annotated[list[AnyMessage], add_messages]] messages: Required[Annotated[list[AnyMessage], add_messages]]
input_text: str input_text: str = ""
user_id: str = "agent" user_id: str = "agent"
role: str = "" role: str = ""
gender: str = "" gender: str = ""
@@ -35,14 +35,14 @@ class LogoState(TypedDict):
# 定义输出结构 # 定义输出结构
class LogoPrompt(BaseModel): 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: def extract_input_node(state: LogoState) -> dict:
"""从 messages 中提取用户输入""" """从 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} 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) structured_llm = qwen_plus_llm.with_structured_output(LogoPrompt)
messages = [ messages = [
SystemMessage(content="""从用户输入中提取核心主题词,只输出一个简单的英文单词。 SystemMessage(content="""从用户输入中提取核心主题词,一个简单的英文单词作为 prompts 字段的值
例如: 例如:
- "我想要一个猫咪图案" -> "cat" - "我想要一个猫咪图案" -> {"prompts": ["cat"]}
- "设计一个花朵" -> "flower" - "设计一个花朵" -> {"prompts": ["flower"]}
- "可爱的狗" -> "dog" - "可爱的狗" -> {"prompts": ["dog"]}
只输出单词,不要其他内容。"""), 请严格按照 JSON 格式输出,包含 prompts 字段。
"""),
HumanMessage(content=state["input_text"]), HumanMessage(content=state["input_text"]),
] ]
result = structured_llm.invoke(messages) result = structured_llm.invoke(messages)
prompts = result.prompts prompts = result.prompts
print(result)
return { return {
"logo_prompts": prompts, "logo_prompts": prompts,
} }
@@ -139,14 +140,14 @@ async def main(test_input, user_id="agent", need_prompt_generation=True):
if __name__ == "__main__": 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 async def test():
user_prompt = "golden retriever" graph = build_logo_graph()
result = asyncio.run(main(user_prompt, need_prompt_generation=False)) result = await graph.ainvoke(
print("\n=== 直接使用 prompt ===") {
print(f"Result: {result}") "messages": [HumanMessage(content="我想要一个金毛图案")],
}
)
print(result["messages"][-1].content)
asyncio.run(test())

View File

@@ -4,13 +4,14 @@ import httpx
async def generate_image( async def generate_image(
bucket_name="fida-public-bucket", bucket_name="fida-public-bucket",
object_name=f"furniture/sketches/123456.png", object_name=f"furniture/sketches/123456.png",
input_images=[],
prompt="Generate a modern minimalist dining chair made of light " prompt="Generate a modern minimalist dining chair made of light "
"oak wood and white leather, with slim metal legs, photographed " "oak wood and white leather, with slim metal legs, photographed "
"in a bright Scandinavian living room with natural sunlight, high detail, " "in a bright Scandinavian living room with natural sunlight, high detail, "
"8k resolution.", "8k resolution.",
): ):
request_data = { request_data = {
"input_image_paths": [], "input_image_paths": input_images,
"prompt": prompt, "prompt": prompt,
"bucket_name": bucket_name, "bucket_name": bucket_name,
"object_name": object_name, "object_name": object_name,

View File

@@ -63,10 +63,10 @@ async def search_photos(query: str, per_page: int = 4, user_id: str = "agent") -
# 上传到 minio使用线程池避免阻塞事件循环 # 上传到 minio使用线程池避免阻塞事件循环
file_name = f"{uuid7()}.jpg" file_name = f"{uuid7()}.jpg"
loop = asyncio.get_event_loop() 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}) 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: except Exception as e:
logger.error(f"[Explorer] 上传失败: {e}") logger.error(f"[explore] 上传失败: {e}")
return results return results

View File

@@ -63,11 +63,11 @@ async def get_random_photos(query: str, count: int = 4, user_id: str = "agent")
# 上传到 minio使用线程池避免阻塞事件循环 # 上传到 minio使用线程池避免阻塞事件循环
file_name = f"{uuid7()}.jpg" file_name = f"{uuid7()}.jpg"
loop = asyncio.get_event_loop() 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}) 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: except Exception as e:
logger.error(f"[Explorer] 上传失败: {e}") logger.error(f"[explore] 上传失败: {e}")
return results return results

View File

@@ -40,7 +40,7 @@ class PrintPrompt(BaseModel):
def extract_input_node(state: PrintState) -> dict: def extract_input_node(state: PrintState) -> dict:
"""从 messages 中提取用户输入""" """从 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} 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) structured_llm = qwen_plus_llm.with_structured_output(PrintPrompt)
messages = [ messages = [
SystemMessage(content=f"""你是一个专业的印花图案设计师 SystemMessage(content=f"""你是一个专业的印花图案设计师.
请根据用户输入生成用于AI图像生成的印花图案提示词 请根据用户输入,生成用于AI图像生成的印花图案提示词.
要求 要求:
1. 提示词应该详细描述印花图案的样式元素颜色布局 1. 提示词应该详细描述印花图案的样式,元素,颜色,布局
2. 提示词应该适合用于 Stable Diffusion 图像生成模型 2. 提示词应该适合用于 Stable Diffusion 图像生成模型
3. 提示词应该使用英文因为图像生成模型对英文理解更好 3. 提示词应该使用英文,因为图像生成模型对英文理解更好
4. 提示词数量为 {state.get("print_num", 1)} 4. 提示词数量为 {state.get("print_num", 1)}
"""), """),
HumanMessage(content=state["input_text"]), 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: 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): if state.get("print_need_prompt_generation", False):
prompts = state["print_prompts"] if state["print_prompts"] else [state["input_text"]] prompts = state["print_prompts"] if state["print_prompts"] else [state["input_text"]]
else: else:
input_text = state.get("input_text", "") input_text = state.get("input_text", "")
prompts = [input_text] prompts = [input_text]
input_images = state.get("input_images", [])
print_img_urls = [] print_img_urls = []
for prompt in prompts: 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) print_img_urls.append(image_url)
logger.info(f"[Print Graph] Generated print image URL: {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: def should_generate_prompt(state: PrintState) -> str:
"""条件分支判断是否需要生成 prompt""" """条件分支:判断是否需要生成 prompt"""
logger.info( logger.info(
f"[Print Graph] should_generate_prompt: print_need_prompt_generation={state.get('print_need_prompt_generation')}, print_prompts={state.get('print_prompts')}" 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(): def build_print_graph():
workflow = StateGraph(PrintState) workflow = StateGraph(PrintState)
workflow.add_node("extract_input", extract_input_node) workflow.add_node("extract_input", extract_input_node)
workflow.add_node("gen_prompt", generate_print_prompt_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__": if __name__ == "__main__":
# 测试示例 1: 需要 prompt 生成默认 # 测试示例 1: 需要 prompt 生成(默认)
test_input = "我想要一个优雅的花卉印花适合用于连衣裙颜色以粉色和白色为主" test_input = "我想要一个优雅的花卉印花,适合用于连衣裙,颜色以粉色和白色为主"
result = asyncio.run(main(test_input, print_need_prompt_generation=True)) result = asyncio.run(main(test_input, print_need_prompt_generation=True))
print("=== 需要 prompt 生成 ===") print("=== 需要 prompt 生成 ===")
print(f"Result: {result}") print(f"Result: {result}")

View File

@@ -11,15 +11,16 @@ class GenerateImageToolInput(BaseModel):
"""Input schema for the Generate Image Tool.""" """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.'") 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) @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.""" """Generate an image based on the provided prompt."""
bucket_name = "aida-users" bucket_name = "aida-users"
object_name = f"agent_generate_print/{uuid7()}.png" 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] return [image_url]

View File

@@ -42,7 +42,7 @@ class SketchPrompt(BaseModel):
def extract_input_node(state: SketchState) -> dict: def extract_input_node(state: SketchState) -> dict:
"""从 messages 中提取用户输入""" """从 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} 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 # 如果 sketch_need_prompt_generation=False 且 sketch_prompts 为空,使用模板生成 prompt
if not state.get("sketch_need_prompt_generation", False) and not state.get("sketch_prompts"): if not state.get("sketch_need_prompt_generation", False) and not state.get("sketch_prompts"):
input_text = state.get("input_text", "") input_text = state.get("input_text", "")
prompts = [build_sketch_template_prompt(input_text)] prompts = [build_sketch_template_prompt(input_text)]
else: else:
prompts = state["sketch_prompts"] if state["sketch_prompts"] else [state["input_text"]] prompts = state["sketch_prompts"] if state["sketch_prompts"] else [state["input_text"]]
input_images = state.get("input_images", [])
sketch_img_urls = [] sketch_img_urls = []
for prompt in prompts: 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) sketch_img_urls.append(image_url)
return {"sketch_img_urls": sketch_img_urls} return {"sketch_img_urls": sketch_img_urls}
@@ -107,33 +108,26 @@ def should_generate_prompt(state: SketchState) -> str:
def build_sketch_graph(): def build_sketch_graph():
workflow = StateGraph(SketchState) 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_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) workflow.add_edge("gen_sketch", END)
graph = workflow.compile() graph = workflow.compile()
return graph 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: def build_sketch_template_prompt(input_text: str) -> str:
"""构建 sketch prompt 模板""" """构建 sketch prompt 模板"""

View File

@@ -11,15 +11,16 @@ class GenerateImageToolInput(BaseModel):
"""Input schema for the Generate Image Tool.""" """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.'") 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) @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.""" """Generate an image based on the provided prompt."""
bucket_name = "fida-public-bucket" bucket_name = "fida-public-bucket"
object_name = f"test/{uuid7()}.png" 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] return [image_url]

View File

@@ -1,16 +1,11 @@
import sys
from pathlib import Path
from typing import Annotated, Required, TypedDict from typing import Annotated, Required, TypedDict
from deepagents import CompiledSubAgent, create_deep_agent
from langchain.agents import create_agent from langchain.agents import create_agent
from langchain.tools import tool from langchain_core.messages import AnyMessage
from langchain_core.messages import AnyMessage, HumanMessage
from langchain_qwq import ChatQwen
from langgraph.graph import END, START, StateGraph from langgraph.graph import END, START, StateGraph
from langgraph.graph.message import add_messages 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.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.explore_graph.tools import explore_tool
from app.service.fashion_agent.graph_node.explorer_graph.tools import explor_tool
from app.service.fashion_agent.graph_node.logo_graph.graph import build_logo_graph 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.logo_graph.tools import generate_logo_tool
from app.service.fashion_agent.graph_node.print_graph.graph import build_print_graph 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.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.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.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 from app.service.fashion_agent.init_llm import build_llm
print_graph = build_print_graph() print_graph = build_print_graph()
@@ -26,13 +21,16 @@ logo_graph = build_logo_graph()
sketch_graph = build_sketch_graph() sketch_graph = build_sketch_graph()
design_graph = build_design_graph() design_graph = build_design_graph()
trending_graph = build_trending_graph() trending_graph = build_trending_graph()
explorer_graph = build_explorer_graph() explore_graph = build_explore_graph()
class MainState(TypedDict): class MainState(TypedDict):
# 消息 # 消息
messages: Required[Annotated[list[AnyMessage], add_messages]] messages: Required[Annotated[list[AnyMessage], add_messages]]
# 上传图片
input_images: list[str] = []
# 模块控制 # 模块控制
call_design: bool = False call_design: bool = False
call_print: bool = False call_print: bool = False
@@ -40,7 +38,7 @@ class MainState(TypedDict):
call_sketch: bool = False call_sketch: bool = False
call_design: bool = False call_design: bool = False
call_trending: bool = False call_trending: bool = False
call_explor: bool = False call_explore: bool = False
# design参数 # design参数
design_request_data: dict = {} design_request_data: dict = {}
@@ -58,7 +56,7 @@ class MainState(TypedDict):
print_img_urls: list[str] = [] 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: def route_node(state: MainState) -> str:
@@ -73,12 +71,12 @@ def route_node(state: MainState) -> str:
return "direct_design" return "direct_design"
if state.get("call_trending"): if state.get("call_trending"):
return "direct_trending" return "direct_trending"
if state.get("call_explor"): if state.get("call_explore"):
return "direct_explor" return "direct_explore"
return "llm_agent" 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) llm = build_llm(enable_thinking=enable_thinking)
chat_agent = create_agent( chat_agent = create_agent(
model=llm, tools=tools, state_schema=MainState, system_prompt="你是一个专业的服装设计助手。根据用户需求,调用合适的工具完成任务." 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_sketch", sketch_graph)
workflow.add_node("direct_design", design_graph) workflow.add_node("direct_design", design_graph)
workflow.add_node("direct_trending", trending_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( workflow.add_conditional_edges(
@@ -107,7 +105,7 @@ def build_main_graph(enable_thinking: bool = False):
"direct_sketch": "direct_sketch", "direct_sketch": "direct_sketch",
"direct_design": "direct_design", "direct_design": "direct_design",
"direct_trending": "direct_trending", "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_sketch", END)
workflow.add_edge("direct_design", END) workflow.add_edge("direct_design", END)
workflow.add_edge("direct_trending", END) workflow.add_edge("direct_trending", END)
workflow.add_edge("direct_explor", END) workflow.add_edge("direct_explore", END)
return workflow.compile() graph = workflow.compile(checkpointer=checkpointer)
return graph
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())

View File

@@ -1,13 +1,12 @@
import json import json
import logging import logging
import sys
from pathlib import Path
from langgraph.stream import ProtocolEvent, StreamChannel, StreamTransformer from langgraph.stream import ProtocolEvent, StreamChannel, StreamTransformer
from app.service.fashion_agent.main_agent import build_main_graph from app.service.fashion_agent.main_agent import build_main_graph
from langgraph.prebuilt import ToolCallTransformer from langgraph.prebuilt import ToolCallTransformer
from typing import AsyncGenerator, TypedDict from typing import AsyncGenerator
from langchain_core.messages import HumanMessage, ToolMessage from langchain_core.messages import HumanMessage, ToolMessage
from app.schemas.fashion_agent import FashionAgentRequest from app.schemas.fashion_agent import FashionAgentRequest
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
logger = logging.getLogger() logger = logging.getLogger()
@@ -33,73 +32,83 @@ class FashionAgentService:
async def run_stream(self, request: FashionAgentRequest) -> AsyncGenerator[str, None]: async def run_stream(self, request: FashionAgentRequest) -> AsyncGenerator[str, None]:
"""流式运行 agent - 使用 v3 projections""" """流式运行 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) async with AsyncPostgresSaver.from_conn_string("postgresql://postgres:Aidlab123123!@20.1.1.43:15432/myapp_prod") as checkpointer:
state = { await checkpointer.setup()
"messages": [HumanMessage(content=request.message)], agent = await build_main_graph(enable_thinking=request.enable_thinking, checkpointer=checkpointer)
"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,
}
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 = {} stream = await agent.astream_events(state, config=config, version="v3", transformers=[ToolCallTransformer, CustomTransformer])
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")
# 记录 tool_name tool_names = {}
if data.get("event") == "tool-started": filter_tool_name = ["design_tool"]
tool_names[tool_call_id] = data.get("tool_name") async for event in stream:
if event["method"] == "tools":
data = event["params"]["data"]
tool_call_id = data.get("tool_call_id")
# 通过 ID 查找 tool_name # 记录 tool_name
elif data.get("event") == "tool-finished": if data.get("event") == "tool-started":
tool_name = tool_names.get(tool_call_id, "unknown") 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 continue
data["tool_name"] = tool_name ev = data.get("event")
if isinstance(data["output"], ToolMessage): if ev == "content-block-delta":
data["output"] = json.loads(data["output"].content) block = data.get("delta") or {}
response_event = {"event_type": "tool", "data": data} if block.get("type") in ("text-delta", "reasoning-delta"):
yield f"data: {json.dumps(response_event, ensure_ascii=False)}\n\n" 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": elif ev in ("message-start", "content-block-start", "content-block-finish", "message-finish"):
data = event["params"]["data"] response_event = {"event_type": "messages", "data": {"event": ev} | data}
response_event = {"event_type": "tool", "data": data} yield f"data: {json.dumps(response_event, ensure_ascii=False)}\n\n"
yield f"data: {json.dumps(response_event, ensure_ascii=False)}\n\n"
elif event["method"] == "messages": response_event = {"event_type": "done"}
data = event["params"]["data"][0] yield f"data: {response_event}"
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}"
if __name__ == "__main__": if __name__ == "__main__":
@@ -117,13 +126,15 @@ if __name__ == "__main__":
print("测试流式输出") print("测试流式输出")
print("=" * 50) print("=" * 50)
request = FashionAgentRequest( request = FashionAgentRequest(
message="生成一张草莓图案", thread_id="zhh",
call_print=True, message="落日",
# call_print=True,
# input_images=["test/53d38bd5-f77b-4034-ada2-45f1e2ebe00c.png"],
# print_need_prompt_generation=False, # print_need_prompt_generation=False,
# call_sketch=True, # call_sketch=True,
# sketch_need_prompt_generation=False, # sketch_need_prompt_generation=False,
# call_logo=True, # call_logo=True,
# call_explor=True, call_explore=True,
# call_design=True, # call_design=True,
# design_request_data=request_data, # design_request_data=request_data,
) )

View File

@@ -19,11 +19,15 @@ dependencies = [
"fastapi[standard]>=0.125.0", "fastapi[standard]>=0.125.0",
"image>=1.5.33", "image>=1.5.33",
"langchain>=1.2.0", "langchain>=1.2.0",
"langchain-anthropic>=1.4.4",
"langchain-community>=0.4.1", "langchain-community>=0.4.1",
"langchain-core>=1.4.2",
"langchain-ollama>=1.1.0",
"langchain-openai>=1.2.2", "langchain-openai>=1.2.2",
"langchain-qwq>=0.3.5", "langchain-qwq>=0.3.5",
"langgraph>=1.0.5", "langgraph>=1.0.5",
"langgraph-api>=0.4.28", "langgraph-api>=0.4.28",
"langgraph-checkpoint-postgres>=3.1.0",
"langgraph-cli[inmem,redis]<=0.4.26", "langgraph-cli[inmem,redis]<=0.4.26",
"langsmith>=0.8.11", "langsmith>=0.8.11",
"load>=1.0.14", "load>=1.0.14",
@@ -40,6 +44,7 @@ dependencies = [
"pandas-stubs~=2.3.3", "pandas-stubs~=2.3.3",
"pika>=1.3.2", "pika>=1.3.2",
"pillow>=12.0.0", "pillow>=12.0.0",
"psycopg[binary,pool]>=3.3.4",
"pyasyncore>=1.0.4", "pyasyncore>=1.0.4",
"pydantic>=2.12.5", "pydantic>=2.12.5",
"pydantic-core>=2.41.5", "pydantic-core>=2.41.5",

89
uv.lock generated
View File

@@ -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" }, { 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]] [[package]]
name = "langchain-openai" name = "langchain-openai"
version = "1.2.2" 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" }, { 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]] [[package]]
name = "langgraph-cli" name = "langgraph-cli"
version = "0.4.8" 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" }, { 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]] [[package]]
name = "psycopg2-binary" name = "psycopg2-binary"
version = "2.9.12" version = "2.9.12"
@@ -3738,11 +3817,15 @@ dependencies = [
{ name = "fastapi", extra = ["standard"] }, { name = "fastapi", extra = ["standard"] },
{ name = "image" }, { name = "image" },
{ name = "langchain" }, { name = "langchain" },
{ name = "langchain-anthropic" },
{ name = "langchain-community" }, { name = "langchain-community" },
{ name = "langchain-core" },
{ name = "langchain-ollama" },
{ name = "langchain-openai" }, { name = "langchain-openai" },
{ name = "langchain-qwq" }, { name = "langchain-qwq" },
{ name = "langgraph" }, { name = "langgraph" },
{ name = "langgraph-api" }, { name = "langgraph-api" },
{ name = "langgraph-checkpoint-postgres" },
{ name = "langgraph-cli", extra = ["inmem"] }, { name = "langgraph-cli", extra = ["inmem"] },
{ name = "langsmith" }, { name = "langsmith" },
{ name = "load" }, { name = "load" },
@@ -3759,6 +3842,7 @@ dependencies = [
{ name = "pandas-stubs" }, { name = "pandas-stubs" },
{ name = "pika" }, { name = "pika" },
{ name = "pillow" }, { name = "pillow" },
{ name = "psycopg", extra = ["binary", "pool"] },
{ name = "pyasyncore" }, { name = "pyasyncore" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "pydantic-core" }, { name = "pydantic-core" },
@@ -3797,11 +3881,15 @@ requires-dist = [
{ name = "fastapi", extras = ["standard"], specifier = ">=0.125.0" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.125.0" },
{ name = "image", specifier = ">=1.5.33" }, { name = "image", specifier = ">=1.5.33" },
{ name = "langchain", specifier = ">=1.2.0" }, { name = "langchain", specifier = ">=1.2.0" },
{ name = "langchain-anthropic", specifier = ">=1.4.4" },
{ name = "langchain-community", specifier = ">=0.4.1" }, { 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-openai", specifier = ">=1.2.2" },
{ name = "langchain-qwq", specifier = ">=0.3.5" }, { name = "langchain-qwq", specifier = ">=0.3.5" },
{ name = "langgraph", specifier = ">=1.0.5" }, { name = "langgraph", specifier = ">=1.0.5" },
{ name = "langgraph-api", specifier = ">=0.4.28" }, { 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 = "langgraph-cli", extras = ["inmem", "redis"], specifier = "<=0.4.26" },
{ name = "langsmith", specifier = ">=0.8.11" }, { name = "langsmith", specifier = ">=0.8.11" },
{ name = "load", specifier = ">=1.0.14" }, { name = "load", specifier = ">=1.0.14" },
@@ -3818,6 +3906,7 @@ requires-dist = [
{ name = "pandas-stubs", specifier = "~=2.3.3" }, { name = "pandas-stubs", specifier = "~=2.3.3" },
{ name = "pika", specifier = ">=1.3.2" }, { name = "pika", specifier = ">=1.3.2" },
{ name = "pillow", specifier = ">=12.0.0" }, { name = "pillow", specifier = ">=12.0.0" },
{ name = "psycopg", extras = ["binary", "pool"], specifier = ">=3.3.4" },
{ name = "pyasyncore", specifier = ">=1.0.4" }, { name = "pyasyncore", specifier = ">=1.0.4" },
{ name = "pydantic", specifier = ">=2.12.5" }, { name = "pydantic", specifier = ">=2.12.5" },
{ name = "pydantic-core", specifier = ">=2.41.5" }, { name = "pydantic-core", specifier = ">=2.41.5" },