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