diff --git a/app.py b/app.py index 0b3b4a1..81df70d 100644 --- a/app.py +++ b/app.py @@ -248,9 +248,9 @@ def initialize_session(): if "show_image_upload" not in st.session_state: st.session_state.show_image_upload = False - # Debug panel toggle + # Debug panel toggle (default True so 显示调试过程 is checked by default) if "show_debug" not in st.session_state: - st.session_state.show_debug = False + st.session_state.show_debug = True def save_uploaded_image(uploaded_file) -> Optional[str]: @@ -276,7 +276,7 @@ def save_uploaded_image(uploaded_file) -> Optional[str]: def _load_product_image(product: ProductItem) -> Optional[Image.Image]: - """Try to load a product image: image_url from API → local data/images → None.""" + """Try to load a product image: image_url from API (normalized when stored) → local data/images → None.""" if product.image_url: try: import requests @@ -306,7 +306,7 @@ def display_product_card_from_item(product: ProductItem) -> None: img = ImageOps.fit(img, target, method=Image.Resampling.LANCZOS) except AttributeError: img = ImageOps.fit(img, target, method=Image.LANCZOS) - st.image(img, use_container_width=True) + st.image(img, width="stretch") else: st.markdown( '
str: """Extract plain text from a LangChain message (handles str or content_blocks).""" content = getattr(msg, "content", "") @@ -81,6 +86,23 @@ def _extract_message_text(msg) -> str: return str(content) if content else "" +def _message_for_log(msg: BaseMessage) -> dict: + """Serialize a message for structured logging (content truncated).""" + text = _extract_message_text(msg) + if len(text) > _LOG_CONTENT_MAX: + text = text[:_LOG_CONTENT_MAX] + f"... [truncated, total {len(text)} chars]" + out: dict[str, Any] = { + "type": getattr(msg, "type", "unknown"), + "content": text, + } + if hasattr(msg, "tool_calls") and msg.tool_calls: + out["tool_calls"] = [ + {"name": tc.get("name"), "args": tc.get("args", {})} + for tc in msg.tool_calls + ] + return out + + # ── Agent class ──────────────────────────────────────────────────────────────── class ShoppingAgent: @@ -111,7 +133,18 @@ class ShoppingAgent: messages = state["messages"] if not any(isinstance(m, SystemMessage) for m in messages): messages = [SystemMessage(content=SYSTEM_PROMPT)] + list(messages) + request_log = [_message_for_log(m) for m in messages] + req_json = json.dumps(request_log, ensure_ascii=False) + if len(req_json) > _LOG_CONTENT_MAX: + req_json = req_json[:_LOG_CONTENT_MAX] + f"... [truncated total {len(req_json)}]" + logger.info("[%s] LLM_REQUEST messages=%s", self.session_id, req_json) response = self.llm_with_tools.invoke(messages) + response_log = _message_for_log(response) + logger.info( + "[%s] LLM_RESPONSE %s", + self.session_id, + json.dumps(response_log, ensure_ascii=False), + ) return {"messages": [response]} def should_continue(state: AgentState): @@ -202,6 +235,16 @@ class ShoppingAgent: preview = text[:600] + ("…" if len(text) > 600 else "") if i < len(unresolved): unresolved[i]["result"] = preview + tc_name = unresolved[i].get("name", "") + tc_args = unresolved[i].get("args", {}) + result_log = text if len(text) <= _LOG_TOOL_RESULT_MAX else text[:_LOG_TOOL_RESULT_MAX] + f"... [truncated total {len(text)}]" + logger.info( + "[%s] TOOL_CALL_RESULT name=%s args=%s result=%s", + self.session_id, + tc_name, + json.dumps(tc_args, ensure_ascii=False), + result_log, + ) step_results.append({"content": preview}) debug_steps.append({"node": "tools", "results": step_results}) diff --git a/app/tools/search_tools.py b/app/tools/search_tools.py index 56e889e..4c364e2 100644 --- a/app/tools/search_tools.py +++ b/app/tools/search_tools.py @@ -38,6 +38,21 @@ logger = logging.getLogger(__name__) _openai_client: Optional[OpenAI] = None +def _normalize_image_url(url: Optional[str]) -> Optional[str]: + """Normalize image_url from API (e.g. ////cnres.appracle.com/... → https://cnres.appracle.com/...).""" + if not url or not isinstance(url, str): + return None + url = url.strip() + if not url: + return None + if url.startswith("https://") or url.startswith("http://"): + return url + # // or ////host/path → https://host/path (exactly one "//" after scheme) + if url.startswith("/"): + return "https://" + url.lstrip("/") + return "https://" + url + + def get_openai_client() -> OpenAI: global _openai_client if _openai_client is None: @@ -226,7 +241,7 @@ def make_search_products_tool( raw.get("category_path") or raw.get("category_name") ), vendor=raw.get("vendor"), - image_url=raw.get("image_url"), + image_url=_normalize_image_url(raw.get("image_url")), relevance_score=raw.get("relevance_score"), match_label=label, tags=raw.get("tags") or [], @@ -249,6 +264,41 @@ def make_search_products_tool( products=products, ) registry.register(session_id, result) + + # ── Search result detailed log (ref_id, summary, per-item id + image_url raw/normalized) ── + logger.info( + "[%s] SEARCH_RESULT ref_id=%s query=%s total_api_hits=%s returned_count=%s " + "verdict=%s quality_summary=%s perfect=%s partial=%s irrelevant=%s", + session_id, + ref_id, + query, + total_hits, + len(raw_results), + verdict, + quality_summary, + perfect_count, + partial_count, + irrelevant_count, + ) + for idx, raw in enumerate(raw_results): + raw_img = raw.get("image_url") or "" + logger.info( + "[%s] SEARCH_RESULT_ITEM raw idx=%s spu_id=%s title=%s image_url_raw=%s", + session_id, + idx, + raw.get("spu_id", ""), + (raw.get("title") or "")[:60], + raw_img, + ) + for p in products: + logger.info( + "[%s] SEARCH_RESULT_PRODUCT spu_id=%s match_label=%s image_url_normalized=%s", + session_id, + p.spu_id, + p.match_label, + p.image_url or "", + ) + logger.info( f"[{session_id}] Registered {ref_id}: verdict={verdict}, " f"perfect={perfect_count}, partial={partial_count}, irrel={irrelevant_count}" -- libgit2 0.21.2