Commit 01b46131b481e01e5462c3d022dc1ec862f96194
1 parent
bad17b15
流程跑通
Showing
4 changed files
with
239 additions
and
62 deletions
Show diff stats
.env.example
| ... | ... | @@ -245,6 +245,10 @@ def initialize_session(): |
| 245 | 245 | if "show_image_upload" not in st.session_state: |
| 246 | 246 | st.session_state.show_image_upload = False |
| 247 | 247 | |
| 248 | + # Debug panel toggle | |
| 249 | + if "show_debug" not in st.session_state: | |
| 250 | + st.session_state.show_debug = False | |
| 251 | + | |
| 248 | 252 | |
| 249 | 253 | def save_uploaded_image(uploaded_file) -> Optional[str]: |
| 250 | 254 | """Save uploaded image to temp directory""" |
| ... | ... | @@ -394,6 +398,7 @@ def display_message(message: dict): |
| 394 | 398 | content = message["content"] |
| 395 | 399 | image_path = message.get("image_path") |
| 396 | 400 | tool_calls = message.get("tool_calls", []) |
| 401 | + debug_steps = message.get("debug_steps", []) | |
| 397 | 402 | |
| 398 | 403 | if role == "user": |
| 399 | 404 | st.markdown('<div class="message user-message">', unsafe_allow_html=True) |
| ... | ... | @@ -411,9 +416,42 @@ def display_message(message: dict): |
| 411 | 416 | else: # assistant |
| 412 | 417 | # Display tool calls horizontally - only tool names |
| 413 | 418 | if tool_calls: |
| 414 | - tool_names = [tc['name'] for tc in tool_calls] | |
| 419 | + tool_names = [tc["name"] for tc in tool_calls] | |
| 415 | 420 | st.caption(" → ".join(tool_names)) |
| 416 | 421 | st.markdown("") |
| 422 | + | |
| 423 | + # Optional: detailed debug panel (reasoning + tool details) | |
| 424 | + if debug_steps and st.session_state.get("show_debug"): | |
| 425 | + with st.expander("思考 & 工具调用详细过程", expanded=False): | |
| 426 | + for idx, step in enumerate(debug_steps, 1): | |
| 427 | + node = step.get("node", "unknown") | |
| 428 | + st.markdown(f"**Step {idx} – {node}**") | |
| 429 | + | |
| 430 | + if node == "agent": | |
| 431 | + msgs = step.get("messages", []) | |
| 432 | + if msgs: | |
| 433 | + st.markdown("**Agent Messages**") | |
| 434 | + for m in msgs: | |
| 435 | + role = m.get("type", "assistant") | |
| 436 | + content = m.get("content", "") | |
| 437 | + st.markdown(f"- `{role}`: {content}") | |
| 438 | + | |
| 439 | + tcs = step.get("tool_calls", []) | |
| 440 | + if tcs: | |
| 441 | + st.markdown("**Planned Tool Calls**") | |
| 442 | + for j, tc in enumerate(tcs, 1): | |
| 443 | + st.markdown(f"- **{j}. {tc.get('name')}**") | |
| 444 | + st.code(tc.get("args", {}), language="json") | |
| 445 | + | |
| 446 | + elif node == "tools": | |
| 447 | + results = step.get("results", []) | |
| 448 | + if results: | |
| 449 | + st.markdown("**Tool Results**") | |
| 450 | + for j, r in enumerate(results, 1): | |
| 451 | + st.markdown(f"- **Result {j}:**") | |
| 452 | + st.code(r.get("content", ""), language="text") | |
| 453 | + | |
| 454 | + st.markdown("---") | |
| 417 | 455 | |
| 418 | 456 | # Extract and display products if any |
| 419 | 457 | products = extract_products_from_response(content) |
| ... | ... | @@ -486,9 +524,9 @@ def display_welcome(): |
| 486 | 524 | st.markdown( |
| 487 | 525 | """ |
| 488 | 526 | <div class="feature-card"> |
| 489 | - <div class="feature-icon">💬</div> | |
| 490 | - <div class="feature-title">Text Search</div> | |
| 491 | - <div>Describe what you want</div> | |
| 527 | + <div class="feature-icon">💗</div> | |
| 528 | + <div class="feature-title">懂你</div> | |
| 529 | + <div>能记住你的偏好,给你推荐适合的</div> | |
| 492 | 530 | </div> |
| 493 | 531 | """, |
| 494 | 532 | unsafe_allow_html=True, |
| ... | ... | @@ -498,9 +536,9 @@ def display_welcome(): |
| 498 | 536 | st.markdown( |
| 499 | 537 | """ |
| 500 | 538 | <div class="feature-card"> |
| 501 | - <div class="feature-icon">📸</div> | |
| 502 | - <div class="feature-title">Image Search</div> | |
| 503 | - <div>Upload product photos</div> | |
| 539 | + <div class="feature-icon">🛍️</div> | |
| 540 | + <div class="feature-title">懂商品</div> | |
| 541 | + <div>深度理解店铺内所有商品,智能匹配你的需求</div> | |
| 504 | 542 | </div> |
| 505 | 543 | """, |
| 506 | 544 | unsafe_allow_html=True, |
| ... | ... | @@ -510,9 +548,9 @@ def display_welcome(): |
| 510 | 548 | st.markdown( |
| 511 | 549 | """ |
| 512 | 550 | <div class="feature-card"> |
| 513 | - <div class="feature-icon">🔍</div> | |
| 514 | - <div class="feature-title">Visual Analysis</div> | |
| 515 | - <div>AI analyzes prodcut style</div> | |
| 551 | + <div class="feature-icon">💭</div> | |
| 552 | + <div class="feature-title">贴心</div> | |
| 553 | + <div>任意聊</div> | |
| 516 | 554 | </div> |
| 517 | 555 | """, |
| 518 | 556 | unsafe_allow_html=True, |
| ... | ... | @@ -522,9 +560,9 @@ def display_welcome(): |
| 522 | 560 | st.markdown( |
| 523 | 561 | """ |
| 524 | 562 | <div class="feature-card"> |
| 525 | - <div class="feature-icon">💭</div> | |
| 526 | - <div class="feature-title">Conversational</div> | |
| 527 | - <div>Remembers context</div> | |
| 563 | + <div class="feature-icon">👗</div> | |
| 564 | + <div class="feature-title">懂时尚</div> | |
| 565 | + <div>穿搭顾问 + 轻松对比</div> | |
| 528 | 566 | </div> |
| 529 | 567 | """, |
| 530 | 568 | unsafe_allow_html=True, |
| ... | ... | @@ -559,6 +597,14 @@ def main(): |
| 559 | 597 | st.session_state.uploaded_image = None |
| 560 | 598 | st.rerun() |
| 561 | 599 | |
| 600 | + # Debug toggle | |
| 601 | + st.markdown("---") | |
| 602 | + st.checkbox( | |
| 603 | + "显示调试过程 (debug)", | |
| 604 | + key="show_debug", | |
| 605 | + help="展开后可查看中间思考过程及工具调用详情", | |
| 606 | + ) | |
| 607 | + | |
| 562 | 608 | st.markdown("---") |
| 563 | 609 | st.caption(f"Session: `{st.session_state.session_id[:8]}...`") |
| 564 | 610 | |
| ... | ... | @@ -696,6 +742,7 @@ What are you looking for today?""" |
| 696 | 742 | "role": "assistant", |
| 697 | 743 | "content": response, |
| 698 | 744 | "tool_calls": tool_calls, |
| 745 | + "debug_steps": result.get("debug_steps", []), | |
| 699 | 746 | } |
| 700 | 747 | ) |
| 701 | 748 | ... | ... |
app/agents/shopping_agent.py
| ... | ... | @@ -78,38 +78,34 @@ class ShoppingAgent: |
| 78 | 78 | """Build the LangGraph StateGraph""" |
| 79 | 79 | |
| 80 | 80 | # System prompt for the agent |
| 81 | - system_prompt = """You are an intelligent fashion shopping assistant. You can: | |
| 82 | -1. Search for products by text description (use search_products) | |
| 83 | -2. Analyze image style and attributes (use analyze_image_style) | |
| 84 | - | |
| 85 | -When a user asks about products: | |
| 86 | -- For text queries: use search_products directly | |
| 87 | -- For image uploads: use analyze_image_style first to understand the product, then use search_products with the extracted description | |
| 88 | -- You can call multiple tools in sequence if needed | |
| 89 | -- Always provide helpful, friendly responses | |
| 90 | - | |
| 91 | -CRITICAL FORMATTING RULES: | |
| 92 | -When presenting product results, you MUST use this EXACT format for EACH product: | |
| 93 | - | |
| 94 | -1. [Product Name] | |
| 95 | - ID: [Product ID Number] | |
| 96 | - Category: [Category] | |
| 97 | - Color: [Color] | |
| 98 | - Gender: [Gender] | |
| 99 | - (Include Season, Usage, Relevance if available) | |
| 100 | - | |
| 101 | -Example: | |
| 81 | + system_prompt = """你是一位智能时尚购物助手,你可以: | |
| 82 | +1. 根据文字描述搜索商品(使用 search_products) | |
| 83 | +2. 分析图片风格和属性(使用 analyze_image_style) | |
| 84 | + | |
| 85 | +当用户咨询商品时: | |
| 86 | +- 文字提问:直接使用 search_products 搜索 | |
| 87 | +- 图片上传:先用 analyze_image_style 理解商品,再用提取的描述调用 search_products 搜索 | |
| 88 | +- 可按需连续调用多个工具 | |
| 89 | +- 始终保持有用、友好的回复风格 | |
| 90 | + | |
| 91 | +关键格式规则: | |
| 92 | +展示商品结果时,每个商品必须严格按以下格式输出: | |
| 93 | + | |
| 94 | +1. [标题 title] | |
| 95 | + ID: [商品ID] | |
| 96 | + 分类: [category_path] | |
| 97 | + 中文名: [title_cn](如有) | |
| 98 | + 标签: [tags](如有) | |
| 99 | + | |
| 100 | +示例: | |
| 102 | 101 | 1. Puma Men White 3/4 Length Pants |
| 103 | 102 | ID: 12345 |
| 104 | - Category: Apparel > Bottomwear > Track Pants | |
| 105 | - Color: White | |
| 106 | - Gender: Men | |
| 107 | - Season: Summer | |
| 108 | - Usage: Sports | |
| 109 | - Relevance: 95.2% | |
| 103 | + 分类: 服饰 > 裤装 > 运动裤 | |
| 104 | + 中文名: 彪马男士白色九分运动裤 | |
| 105 | + 标签: 运动,夏季,白色 | |
| 110 | 106 | |
| 111 | -DO NOT skip the ID field! It is essential for displaying product images. | |
| 112 | -Be conversational in your introduction, but preserve the exact product format.""" | |
| 107 | +不可省略 ID 字段!它是展示商品图片的关键。 | |
| 108 | +介绍要口语化,但必须保持上述商品格式。""" | |
| 113 | 109 | |
| 114 | 110 | def agent_node(state: AgentState): |
| 115 | 111 | """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."" |
| 165 | 161 | image_path: Optional path to uploaded image |
| 166 | 162 | |
| 167 | 163 | Returns: |
| 168 | - Dict with response and metadata | |
| 164 | + Dict with response and metadata, including: | |
| 165 | + - tool_calls: list of tool calls with args and (truncated) results | |
| 166 | + - debug_steps: detailed intermediate reasoning & tool execution steps | |
| 169 | 167 | """ |
| 170 | 168 | try: |
| 171 | 169 | logger.info( |
| ... | ... | @@ -191,32 +189,74 @@ Be conversational in your introduction, but preserve the exact product format."" |
| 191 | 189 | "current_image_path": image_path, |
| 192 | 190 | } |
| 193 | 191 | |
| 194 | - # Track tool calls | |
| 192 | + # Track tool calls (high-level) and detailed debug steps | |
| 195 | 193 | tool_calls = [] |
| 194 | + debug_steps = [] | |
| 196 | 195 | |
| 197 | - # Stream events to capture tool calls | |
| 196 | + # Stream events to capture tool calls and intermediate reasoning | |
| 198 | 197 | for event in self.graph.stream(input_state, config=config): |
| 199 | 198 | logger.info(f"Event: {event}") |
| 200 | - | |
| 201 | - # Check for agent node (tool calls) | |
| 199 | + | |
| 200 | + # Agent node: LLM reasoning & tool decisions | |
| 202 | 201 | if "agent" in event: |
| 203 | 202 | agent_output = event["agent"] |
| 204 | - if "messages" in agent_output: | |
| 205 | - for msg in agent_output["messages"]: | |
| 206 | - if hasattr(msg, "tool_calls") and msg.tool_calls: | |
| 207 | - for tc in msg.tool_calls: | |
| 208 | - tool_calls.append({ | |
| 209 | - "name": tc["name"], | |
| 210 | - "args": tc.get("args", {}), | |
| 211 | - }) | |
| 212 | - | |
| 213 | - # Check for tool node (tool results) | |
| 203 | + messages = agent_output.get("messages", []) | |
| 204 | + | |
| 205 | + step_messages = [] | |
| 206 | + step_tool_calls = [] | |
| 207 | + | |
| 208 | + for msg in messages: | |
| 209 | + msg_text = _extract_message_text(msg) | |
| 210 | + msg_entry = { | |
| 211 | + "type": getattr(msg, "type", "assistant"), | |
| 212 | + "content": msg_text[:500], # truncate for safety | |
| 213 | + } | |
| 214 | + step_messages.append(msg_entry) | |
| 215 | + | |
| 216 | + # Capture tool calls from this agent message | |
| 217 | + if hasattr(msg, "tool_calls") and msg.tool_calls: | |
| 218 | + for tc in msg.tool_calls: | |
| 219 | + tc_entry = { | |
| 220 | + "name": tc.get("name"), | |
| 221 | + "args": tc.get("args", {}), | |
| 222 | + } | |
| 223 | + tool_calls.append(tc_entry) | |
| 224 | + step_tool_calls.append(tc_entry) | |
| 225 | + | |
| 226 | + debug_steps.append( | |
| 227 | + { | |
| 228 | + "node": "agent", | |
| 229 | + "messages": step_messages, | |
| 230 | + "tool_calls": step_tool_calls, | |
| 231 | + } | |
| 232 | + ) | |
| 233 | + | |
| 234 | + # Tool node: actual tool execution results | |
| 214 | 235 | if "tools" in event: |
| 215 | 236 | tools_output = event["tools"] |
| 216 | - if "messages" in tools_output: | |
| 217 | - for i, msg in enumerate(tools_output["messages"]): | |
| 218 | - if i < len(tool_calls): | |
| 219 | - tool_calls[i]["result"] = str(msg.content)[:200] + "..." | |
| 237 | + messages = tools_output.get("messages", []) | |
| 238 | + | |
| 239 | + step_tool_results = [] | |
| 240 | + | |
| 241 | + for i, msg in enumerate(messages): | |
| 242 | + content_text = _extract_message_text(msg) | |
| 243 | + result_preview = content_text[:500] + ("..." if len(content_text) > 500 else "") | |
| 244 | + | |
| 245 | + if i < len(tool_calls): | |
| 246 | + tool_calls[i]["result"] = result_preview | |
| 247 | + | |
| 248 | + step_tool_results.append( | |
| 249 | + { | |
| 250 | + "content": result_preview, | |
| 251 | + } | |
| 252 | + ) | |
| 253 | + | |
| 254 | + debug_steps.append( | |
| 255 | + { | |
| 256 | + "node": "tools", | |
| 257 | + "results": step_tool_results, | |
| 258 | + } | |
| 259 | + ) | |
| 220 | 260 | |
| 221 | 261 | # Get final state |
| 222 | 262 | final_state = self.graph.get_state(config) |
| ... | ... | @@ -228,6 +268,7 @@ Be conversational in your introduction, but preserve the exact product format."" |
| 228 | 268 | return { |
| 229 | 269 | "response": response_text, |
| 230 | 270 | "tool_calls": tool_calls, |
| 271 | + "debug_steps": debug_steps, | |
| 231 | 272 | "error": False, |
| 232 | 273 | } |
| 233 | 274 | ... | ... |
| ... | ... | @@ -0,0 +1,89 @@ |
| 1 | +import json | |
| 2 | +from typing import Optional | |
| 3 | + | |
| 4 | +from app.agents.shopping_agent import ShoppingAgent | |
| 5 | + | |
| 6 | + | |
| 7 | +def run_once(agent: ShoppingAgent, query: str, image_path: Optional[str] = None) -> None: | |
| 8 | + """Run a single query through the agent and pretty-print details.""" | |
| 9 | + result = agent.chat(query=query, image_path=image_path) | |
| 10 | + | |
| 11 | + print("\n=== Assistant Response ===") | |
| 12 | + print(result.get("response", "")) | |
| 13 | + | |
| 14 | + print("\n=== Tool Calls ===") | |
| 15 | + tool_calls = result.get("tool_calls", []) or [] | |
| 16 | + if not tool_calls: | |
| 17 | + print("(no tool calls)") | |
| 18 | + else: | |
| 19 | + for i, tc in enumerate(tool_calls, 1): | |
| 20 | + print(f"[{i}] {tc.get('name')}") | |
| 21 | + print(" args:") | |
| 22 | + print(" " + json.dumps(tc.get("args", {}), ensure_ascii=False, indent=2).replace("\n", "\n ")) | |
| 23 | + if "result" in tc: | |
| 24 | + print(" result (truncated):") | |
| 25 | + print(" " + str(tc.get("result")).replace("\n", "\n ")) | |
| 26 | + | |
| 27 | + print("\n=== Debug Steps ===") | |
| 28 | + debug_steps = result.get("debug_steps", []) or [] | |
| 29 | + if not debug_steps: | |
| 30 | + print("(no debug steps)") | |
| 31 | + else: | |
| 32 | + for idx, step in enumerate(debug_steps, 1): | |
| 33 | + node = step.get("node", "unknown") | |
| 34 | + print(f"\n--- Step {idx} [{node}] ---") | |
| 35 | + | |
| 36 | + if node == "agent": | |
| 37 | + msgs = step.get("messages", []) or [] | |
| 38 | + if msgs: | |
| 39 | + print(" Agent messages:") | |
| 40 | + for m in msgs: | |
| 41 | + role = m.get("type", "assistant") | |
| 42 | + content = m.get("content", "") | |
| 43 | + print(f" - {role}: {content}") | |
| 44 | + | |
| 45 | + tcs = step.get("tool_calls", []) or [] | |
| 46 | + if tcs: | |
| 47 | + print(" Planned tool calls:") | |
| 48 | + for j, tc in enumerate(tcs, 1): | |
| 49 | + print(f" [{j}] {tc.get('name')}") | |
| 50 | + print( | |
| 51 | + " args: " | |
| 52 | + + json.dumps(tc.get("args", {}), ensure_ascii=False) | |
| 53 | + ) | |
| 54 | + | |
| 55 | + elif node == "tools": | |
| 56 | + results = step.get("results", []) or [] | |
| 57 | + if results: | |
| 58 | + print(" Tool results:") | |
| 59 | + for j, r in enumerate(results, 1): | |
| 60 | + content = r.get("content", "") | |
| 61 | + print(f" [{j}] {content}") | |
| 62 | + | |
| 63 | + | |
| 64 | +def main() -> None: | |
| 65 | + """Simple CLI debugger to inspect agent reasoning and tool usage.""" | |
| 66 | + agent = ShoppingAgent(session_id="cli-debug") | |
| 67 | + print("ShopAgent CLI Debugger") | |
| 68 | + print("输入你的问题,或者输入 `exit` 退出。\n") | |
| 69 | + | |
| 70 | + while True: | |
| 71 | + try: | |
| 72 | + query = input("你:").strip() | |
| 73 | + except (EOFError, KeyboardInterrupt): | |
| 74 | + print("\n再见 👋") | |
| 75 | + break | |
| 76 | + | |
| 77 | + if not query: | |
| 78 | + continue | |
| 79 | + if query.lower() in {"exit", "quit"}: | |
| 80 | + print("再见 👋") | |
| 81 | + break | |
| 82 | + | |
| 83 | + run_once(agent, query=query) | |
| 84 | + print("\n" + "=" * 60 + "\n") | |
| 85 | + | |
| 86 | + | |
| 87 | +if __name__ == "__main__": | |
| 88 | + main() | |
| 89 | + | ... | ... |