From 01b46131b481e01e5462c3d022dc1ec862f96194 Mon Sep 17 00:00:00 2001 From: tangwang Date: Thu, 12 Feb 2026 20:04:21 +0800 Subject: [PATCH] 流程跑通 --- .env.example | 2 +- app.py | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------- app/agents/shopping_agent.py | 137 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------ cli_debug.py | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 239 insertions(+), 62 deletions(-) create mode 100644 cli_debug.py diff --git a/.env.example b/.env.example index 13ab59a..746aff3 100644 --- a/.env.example +++ b/.env.example @@ -19,7 +19,7 @@ SIMILARITY_THRESHOLD=0.6 # Search API (see docs/搜索API对接指南.md) SEARCH_API_BASE_URL=http://120.76.41.98:6002 -SEARCH_API_TENANT_ID=162 +SEARCH_API_TENANT_ID=170 # ==================== # Application Configuration diff --git a/app.py b/app.py index 342861a..2b9e2e7 100644 --- a/app.py +++ b/app.py @@ -245,6 +245,10 @@ def initialize_session(): if "show_image_upload" not in st.session_state: st.session_state.show_image_upload = False + # Debug panel toggle + if "show_debug" not in st.session_state: + st.session_state.show_debug = False + def save_uploaded_image(uploaded_file) -> Optional[str]: """Save uploaded image to temp directory""" @@ -394,6 +398,7 @@ def display_message(message: dict): content = message["content"] image_path = message.get("image_path") tool_calls = message.get("tool_calls", []) + debug_steps = message.get("debug_steps", []) if role == "user": st.markdown('
', unsafe_allow_html=True) @@ -411,9 +416,42 @@ def display_message(message: dict): else: # assistant # Display tool calls horizontally - only tool names if tool_calls: - tool_names = [tc['name'] for tc in tool_calls] + tool_names = [tc["name"] for tc in tool_calls] st.caption(" → ".join(tool_names)) st.markdown("") + + # Optional: detailed debug panel (reasoning + tool details) + if debug_steps and st.session_state.get("show_debug"): + with st.expander("思考 & 工具调用详细过程", expanded=False): + for idx, step in enumerate(debug_steps, 1): + node = step.get("node", "unknown") + st.markdown(f"**Step {idx} – {node}**") + + if node == "agent": + msgs = step.get("messages", []) + if msgs: + st.markdown("**Agent Messages**") + for m in msgs: + role = m.get("type", "assistant") + content = m.get("content", "") + st.markdown(f"- `{role}`: {content}") + + tcs = step.get("tool_calls", []) + if tcs: + st.markdown("**Planned Tool Calls**") + for j, tc in enumerate(tcs, 1): + st.markdown(f"- **{j}. {tc.get('name')}**") + st.code(tc.get("args", {}), language="json") + + elif node == "tools": + results = step.get("results", []) + if results: + st.markdown("**Tool Results**") + for j, r in enumerate(results, 1): + st.markdown(f"- **Result {j}:**") + st.code(r.get("content", ""), language="text") + + st.markdown("---") # Extract and display products if any products = extract_products_from_response(content) @@ -486,9 +524,9 @@ def display_welcome(): st.markdown( """
-
💬
-
Text Search
-
Describe what you want
+
💗
+
懂你
+
能记住你的偏好,给你推荐适合的
""", unsafe_allow_html=True, @@ -498,9 +536,9 @@ def display_welcome(): st.markdown( """
-
📸
-
Image Search
-
Upload product photos
+
🛍️
+
懂商品
+
深度理解店铺内所有商品,智能匹配你的需求
""", unsafe_allow_html=True, @@ -510,9 +548,9 @@ def display_welcome(): st.markdown( """
-
🔍
-
Visual Analysis
-
AI analyzes prodcut style
+
💭
+
贴心
+
任意聊
""", unsafe_allow_html=True, @@ -522,9 +560,9 @@ def display_welcome(): st.markdown( """
-
💭
-
Conversational
-
Remembers context
+
👗
+
懂时尚
+
穿搭顾问 + 轻松对比
""", unsafe_allow_html=True, @@ -559,6 +597,14 @@ def main(): st.session_state.uploaded_image = None st.rerun() + # Debug toggle + st.markdown("---") + st.checkbox( + "显示调试过程 (debug)", + key="show_debug", + help="展开后可查看中间思考过程及工具调用详情", + ) + st.markdown("---") st.caption(f"Session: `{st.session_state.session_id[:8]}...`") @@ -696,6 +742,7 @@ What are you looking for today?""" "role": "assistant", "content": response, "tool_calls": tool_calls, + "debug_steps": result.get("debug_steps", []), } ) diff --git a/app/agents/shopping_agent.py b/app/agents/shopping_agent.py index 481fd74..e2a6963 100644 --- a/app/agents/shopping_agent.py +++ b/app/agents/shopping_agent.py @@ -78,38 +78,34 @@ class ShoppingAgent: """Build the LangGraph StateGraph""" # System prompt for the agent - system_prompt = """You are an intelligent fashion shopping assistant. You can: -1. Search for products by text description (use search_products) -2. Analyze image style and attributes (use analyze_image_style) - -When a user asks about products: -- For text queries: use search_products directly -- For image uploads: use analyze_image_style first to understand the product, then use search_products with the extracted description -- You can call multiple tools in sequence if needed -- Always provide helpful, friendly responses - -CRITICAL FORMATTING RULES: -When presenting product results, you MUST use this EXACT format for EACH product: - -1. [Product Name] - ID: [Product ID Number] - Category: [Category] - Color: [Color] - Gender: [Gender] - (Include Season, Usage, Relevance if available) - -Example: + system_prompt = """你是一位智能时尚购物助手,你可以: +1. 根据文字描述搜索商品(使用 search_products) +2. 分析图片风格和属性(使用 analyze_image_style) + +当用户咨询商品时: +- 文字提问:直接使用 search_products 搜索 +- 图片上传:先用 analyze_image_style 理解商品,再用提取的描述调用 search_products 搜索 +- 可按需连续调用多个工具 +- 始终保持有用、友好的回复风格 + +关键格式规则: +展示商品结果时,每个商品必须严格按以下格式输出: + +1. [标题 title] + ID: [商品ID] + 分类: [category_path] + 中文名: [title_cn](如有) + 标签: [tags](如有) + +示例: 1. Puma Men White 3/4 Length Pants ID: 12345 - Category: Apparel > Bottomwear > Track Pants - Color: White - Gender: Men - Season: Summer - Usage: Sports - Relevance: 95.2% + 分类: 服饰 > 裤装 > 运动裤 + 中文名: 彪马男士白色九分运动裤 + 标签: 运动,夏季,白色 -DO NOT skip the ID field! It is essential for displaying product images. -Be conversational in your introduction, but preserve the exact product format.""" +不可省略 ID 字段!它是展示商品图片的关键。 +介绍要口语化,但必须保持上述商品格式。""" def agent_node(state: AgentState): """Agent decision node - decides which tools to call or when to respond""" @@ -165,7 +161,9 @@ Be conversational in your introduction, but preserve the exact product format."" image_path: Optional path to uploaded image Returns: - Dict with response and metadata + Dict with response and metadata, including: + - tool_calls: list of tool calls with args and (truncated) results + - debug_steps: detailed intermediate reasoning & tool execution steps """ try: logger.info( @@ -191,32 +189,74 @@ Be conversational in your introduction, but preserve the exact product format."" "current_image_path": image_path, } - # Track tool calls + # Track tool calls (high-level) and detailed debug steps tool_calls = [] + debug_steps = [] - # Stream events to capture tool calls + # Stream events to capture tool calls and intermediate reasoning for event in self.graph.stream(input_state, config=config): logger.info(f"Event: {event}") - - # Check for agent node (tool calls) + + # Agent node: LLM reasoning & tool decisions if "agent" in event: agent_output = event["agent"] - if "messages" in agent_output: - for msg in agent_output["messages"]: - if hasattr(msg, "tool_calls") and msg.tool_calls: - for tc in msg.tool_calls: - tool_calls.append({ - "name": tc["name"], - "args": tc.get("args", {}), - }) - - # Check for tool node (tool results) + messages = agent_output.get("messages", []) + + step_messages = [] + step_tool_calls = [] + + for msg in messages: + msg_text = _extract_message_text(msg) + msg_entry = { + "type": getattr(msg, "type", "assistant"), + "content": msg_text[:500], # truncate for safety + } + step_messages.append(msg_entry) + + # Capture tool calls from this agent message + if hasattr(msg, "tool_calls") and msg.tool_calls: + for tc in msg.tool_calls: + tc_entry = { + "name": tc.get("name"), + "args": tc.get("args", {}), + } + tool_calls.append(tc_entry) + step_tool_calls.append(tc_entry) + + debug_steps.append( + { + "node": "agent", + "messages": step_messages, + "tool_calls": step_tool_calls, + } + ) + + # Tool node: actual tool execution results if "tools" in event: tools_output = event["tools"] - if "messages" in tools_output: - for i, msg in enumerate(tools_output["messages"]): - if i < len(tool_calls): - tool_calls[i]["result"] = str(msg.content)[:200] + "..." + messages = tools_output.get("messages", []) + + step_tool_results = [] + + for i, msg in enumerate(messages): + content_text = _extract_message_text(msg) + result_preview = content_text[:500] + ("..." if len(content_text) > 500 else "") + + if i < len(tool_calls): + tool_calls[i]["result"] = result_preview + + step_tool_results.append( + { + "content": result_preview, + } + ) + + debug_steps.append( + { + "node": "tools", + "results": step_tool_results, + } + ) # Get final state final_state = self.graph.get_state(config) @@ -228,6 +268,7 @@ Be conversational in your introduction, but preserve the exact product format."" return { "response": response_text, "tool_calls": tool_calls, + "debug_steps": debug_steps, "error": False, } diff --git a/cli_debug.py b/cli_debug.py new file mode 100644 index 0000000..705dac0 --- /dev/null +++ b/cli_debug.py @@ -0,0 +1,89 @@ +import json +from typing import Optional + +from app.agents.shopping_agent import ShoppingAgent + + +def run_once(agent: ShoppingAgent, query: str, image_path: Optional[str] = None) -> None: + """Run a single query through the agent and pretty-print details.""" + result = agent.chat(query=query, image_path=image_path) + + print("\n=== Assistant Response ===") + print(result.get("response", "")) + + print("\n=== Tool Calls ===") + tool_calls = result.get("tool_calls", []) or [] + if not tool_calls: + print("(no tool calls)") + else: + for i, tc in enumerate(tool_calls, 1): + print(f"[{i}] {tc.get('name')}") + print(" args:") + print(" " + json.dumps(tc.get("args", {}), ensure_ascii=False, indent=2).replace("\n", "\n ")) + if "result" in tc: + print(" result (truncated):") + print(" " + str(tc.get("result")).replace("\n", "\n ")) + + print("\n=== Debug Steps ===") + debug_steps = result.get("debug_steps", []) or [] + if not debug_steps: + print("(no debug steps)") + else: + for idx, step in enumerate(debug_steps, 1): + node = step.get("node", "unknown") + print(f"\n--- Step {idx} [{node}] ---") + + if node == "agent": + msgs = step.get("messages", []) or [] + if msgs: + print(" Agent messages:") + for m in msgs: + role = m.get("type", "assistant") + content = m.get("content", "") + print(f" - {role}: {content}") + + tcs = step.get("tool_calls", []) or [] + if tcs: + print(" Planned tool calls:") + for j, tc in enumerate(tcs, 1): + print(f" [{j}] {tc.get('name')}") + print( + " args: " + + json.dumps(tc.get("args", {}), ensure_ascii=False) + ) + + elif node == "tools": + results = step.get("results", []) or [] + if results: + print(" Tool results:") + for j, r in enumerate(results, 1): + content = r.get("content", "") + print(f" [{j}] {content}") + + +def main() -> None: + """Simple CLI debugger to inspect agent reasoning and tool usage.""" + agent = ShoppingAgent(session_id="cli-debug") + print("ShopAgent CLI Debugger") + print("输入你的问题,或者输入 `exit` 退出。\n") + + while True: + try: + query = input("你:").strip() + except (EOFError, KeyboardInterrupt): + print("\n再见 👋") + break + + if not query: + continue + if query.lower() in {"exit", "quit"}: + print("再见 👋") + break + + run_once(agent, query=query) + print("\n" + "=" * 60 + "\n") + + +if __name__ == "__main__": + main() + -- libgit2 0.21.2