Commit 01b46131b481e01e5462c3d022dc1ec862f96194

Authored by tangwang
1 parent bad17b15

流程跑通

.env.example
... ... @@ -19,7 +19,7 @@ SIMILARITY_THRESHOLD=0.6
19 19  
20 20 # Search API (see docs/搜索API对接指南.md)
21 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 25 # Application Configuration
... ...
... ... @@ -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?&quot;&quot;&quot;
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.&quot;&quot;
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.&quot;&quot;
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.&quot;&quot;
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  
... ...
cli_debug.py 0 → 100644
... ... @@ -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 +
... ...