Commit 01b46131b481e01e5462c3d022dc1ec862f96194
1 parent
bad17b15
流程跑通
Showing
4 changed files
with
239 additions
and
62 deletions
Show diff stats
.env.example
| @@ -19,7 +19,7 @@ SIMILARITY_THRESHOLD=0.6 | @@ -19,7 +19,7 @@ SIMILARITY_THRESHOLD=0.6 | ||
| 19 | 19 | ||
| 20 | # Search API (see docs/搜索API对接指南.md) | 20 | # Search API (see docs/搜索API对接指南.md) |
| 21 | SEARCH_API_BASE_URL=http://120.76.41.98:6002 | 21 | SEARCH_API_BASE_URL=http://120.76.41.98:6002 |
| 22 | -SEARCH_API_TENANT_ID=162 | 22 | +SEARCH_API_TENANT_ID=170 |
| 23 | 23 | ||
| 24 | # ==================== | 24 | # ==================== |
| 25 | # Application Configuration | 25 | # Application Configuration |
| @@ -245,6 +245,10 @@ def initialize_session(): | @@ -245,6 +245,10 @@ def initialize_session(): | ||
| 245 | if "show_image_upload" not in st.session_state: | 245 | if "show_image_upload" not in st.session_state: |
| 246 | st.session_state.show_image_upload = False | 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 | def save_uploaded_image(uploaded_file) -> Optional[str]: | 253 | def save_uploaded_image(uploaded_file) -> Optional[str]: |
| 250 | """Save uploaded image to temp directory""" | 254 | """Save uploaded image to temp directory""" |
| @@ -394,6 +398,7 @@ def display_message(message: dict): | @@ -394,6 +398,7 @@ def display_message(message: dict): | ||
| 394 | content = message["content"] | 398 | content = message["content"] |
| 395 | image_path = message.get("image_path") | 399 | image_path = message.get("image_path") |
| 396 | tool_calls = message.get("tool_calls", []) | 400 | tool_calls = message.get("tool_calls", []) |
| 401 | + debug_steps = message.get("debug_steps", []) | ||
| 397 | 402 | ||
| 398 | if role == "user": | 403 | if role == "user": |
| 399 | st.markdown('<div class="message user-message">', unsafe_allow_html=True) | 404 | st.markdown('<div class="message user-message">', unsafe_allow_html=True) |
| @@ -411,9 +416,42 @@ def display_message(message: dict): | @@ -411,9 +416,42 @@ def display_message(message: dict): | ||
| 411 | else: # assistant | 416 | else: # assistant |
| 412 | # Display tool calls horizontally - only tool names | 417 | # Display tool calls horizontally - only tool names |
| 413 | if tool_calls: | 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 | st.caption(" → ".join(tool_names)) | 420 | st.caption(" → ".join(tool_names)) |
| 416 | st.markdown("") | 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 | # Extract and display products if any | 456 | # Extract and display products if any |
| 419 | products = extract_products_from_response(content) | 457 | products = extract_products_from_response(content) |
| @@ -486,9 +524,9 @@ def display_welcome(): | @@ -486,9 +524,9 @@ def display_welcome(): | ||
| 486 | st.markdown( | 524 | st.markdown( |
| 487 | """ | 525 | """ |
| 488 | <div class="feature-card"> | 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 | </div> | 530 | </div> |
| 493 | """, | 531 | """, |
| 494 | unsafe_allow_html=True, | 532 | unsafe_allow_html=True, |
| @@ -498,9 +536,9 @@ def display_welcome(): | @@ -498,9 +536,9 @@ def display_welcome(): | ||
| 498 | st.markdown( | 536 | st.markdown( |
| 499 | """ | 537 | """ |
| 500 | <div class="feature-card"> | 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 | </div> | 542 | </div> |
| 505 | """, | 543 | """, |
| 506 | unsafe_allow_html=True, | 544 | unsafe_allow_html=True, |
| @@ -510,9 +548,9 @@ def display_welcome(): | @@ -510,9 +548,9 @@ def display_welcome(): | ||
| 510 | st.markdown( | 548 | st.markdown( |
| 511 | """ | 549 | """ |
| 512 | <div class="feature-card"> | 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 | </div> | 554 | </div> |
| 517 | """, | 555 | """, |
| 518 | unsafe_allow_html=True, | 556 | unsafe_allow_html=True, |
| @@ -522,9 +560,9 @@ def display_welcome(): | @@ -522,9 +560,9 @@ def display_welcome(): | ||
| 522 | st.markdown( | 560 | st.markdown( |
| 523 | """ | 561 | """ |
| 524 | <div class="feature-card"> | 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 | </div> | 566 | </div> |
| 529 | """, | 567 | """, |
| 530 | unsafe_allow_html=True, | 568 | unsafe_allow_html=True, |
| @@ -559,6 +597,14 @@ def main(): | @@ -559,6 +597,14 @@ def main(): | ||
| 559 | st.session_state.uploaded_image = None | 597 | st.session_state.uploaded_image = None |
| 560 | st.rerun() | 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 | st.markdown("---") | 608 | st.markdown("---") |
| 563 | st.caption(f"Session: `{st.session_state.session_id[:8]}...`") | 609 | st.caption(f"Session: `{st.session_state.session_id[:8]}...`") |
| 564 | 610 | ||
| @@ -696,6 +742,7 @@ What are you looking for today?""" | @@ -696,6 +742,7 @@ What are you looking for today?""" | ||
| 696 | "role": "assistant", | 742 | "role": "assistant", |
| 697 | "content": response, | 743 | "content": response, |
| 698 | "tool_calls": tool_calls, | 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,38 +78,34 @@ class ShoppingAgent: | ||
| 78 | """Build the LangGraph StateGraph""" | 78 | """Build the LangGraph StateGraph""" |
| 79 | 79 | ||
| 80 | # System prompt for the agent | 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 | 1. Puma Men White 3/4 Length Pants | 101 | 1. Puma Men White 3/4 Length Pants |
| 103 | ID: 12345 | 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 | def agent_node(state: AgentState): | 110 | def agent_node(state: AgentState): |
| 115 | """Agent decision node - decides which tools to call or when to respond""" | 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,7 +161,9 @@ Be conversational in your introduction, but preserve the exact product format."" | ||
| 165 | image_path: Optional path to uploaded image | 161 | image_path: Optional path to uploaded image |
| 166 | 162 | ||
| 167 | Returns: | 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 | try: | 168 | try: |
| 171 | logger.info( | 169 | logger.info( |
| @@ -191,32 +189,74 @@ Be conversational in your introduction, but preserve the exact product format."" | @@ -191,32 +189,74 @@ Be conversational in your introduction, but preserve the exact product format."" | ||
| 191 | "current_image_path": image_path, | 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 | tool_calls = [] | 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 | for event in self.graph.stream(input_state, config=config): | 197 | for event in self.graph.stream(input_state, config=config): |
| 199 | logger.info(f"Event: {event}") | 198 | logger.info(f"Event: {event}") |
| 200 | - | ||
| 201 | - # Check for agent node (tool calls) | 199 | + |
| 200 | + # Agent node: LLM reasoning & tool decisions | ||
| 202 | if "agent" in event: | 201 | if "agent" in event: |
| 203 | agent_output = event["agent"] | 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 | if "tools" in event: | 235 | if "tools" in event: |
| 215 | tools_output = event["tools"] | 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 | # Get final state | 261 | # Get final state |
| 222 | final_state = self.graph.get_state(config) | 262 | final_state = self.graph.get_state(config) |
| @@ -228,6 +268,7 @@ Be conversational in your introduction, but preserve the exact product format."" | @@ -228,6 +268,7 @@ Be conversational in your introduction, but preserve the exact product format."" | ||
| 228 | return { | 268 | return { |
| 229 | "response": response_text, | 269 | "response": response_text, |
| 230 | "tool_calls": tool_calls, | 270 | "tool_calls": tool_calls, |
| 271 | + "debug_steps": debug_steps, | ||
| 231 | "error": False, | 272 | "error": False, |
| 232 | } | 273 | } |
| 233 | 274 |
| @@ -0,0 +1,89 @@ | @@ -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 | + |