Commit 1a7debb320b887dd103c5647642d2db4253165f3

Authored by tangwang
1 parent 621b6925

引用商品进行追问

Showing 2 changed files with 486 additions and 17 deletions   Show diff stats
README_prompts.md
... ... @@ -33,3 +33,41 @@ graphRAG在商品搜索中如何使用?我想将他用于,对商品的模糊
33 33  
34 34 请深度思考如何让 最终 AI 消息 可以引用某次搜索的结果,而不是重新复述,并且废除extract_products_from_response这种方法。要规划一套健全的商品搜索结果的管理、和引用的方法。
35 35  
  36 +
  37 +
  38 +
  39 +
  40 +
  41 +
  42 +
  43 +
  44 +
  45 +
  46 +请帮我补充一个功能:前端,对于渲染的每一个商品([SEARCH_REF:xxx]渲染的搜索结果),鼠标hover上去的时候,悬浮可供点击的两个东西:
  47 +1. Similar products: 点击后,从右侧拉出、覆盖大部分页面、保留部分背景(我不知道这种技术叫什么,实现类似效果即可)展现的内容是,以该商品未标题,发起商品搜索,页面展示搜索结果。
  48 +2. 一个勾选框,点击后为勾选状态。可以勾选多个商品。
  49 +下方也悬浮两个菜单,一个ask,一个compare。
  50 +如果是点击了ask,那么,将引用这两个商品进行继续对话,如果点击了compare,那么,也是从右侧拉出一个页面,覆盖到上面,对这两个商品进行对比,页面内容为空,提示暂未实现即可)
  51 +
  52 +
  53 +
  54 +如果我要引用其中几款商品,进行对话,请给我一个后端(LLM、智能体交互方面)的方案
  55 +前端,对话框里面,也要显示引用的商品,每个引用的商品右上角给一个删除号,下面正常聊天,点击发送后,后端要引用这几款商品进行对话。
  56 +请深度思考、设计智能体如何支持这个 chat with的功能
  57 +
  58 +
  59 +
  60 +
  61 +
  62 +
  63 +
  64 +
  65 +
  66 +
  67 +
  68 +
  69 +请帮我补充一个功能:前端,对于渲染的每一个商品([SEARCH_REF:xxx]渲染的搜索结果),鼠标hover上去的时候,悬浮可供点击的两个东西:
  70 +1. Similar products: 点击后,从右侧拉出、覆盖大部分页面、保留部分背景(我不知道这种技术叫什么,实现类似效果即可)展现的内容是,以该商品未标题,发起商品搜索,页面展示搜索结果。
  71 +2. 一个勾选框,点击后为勾选状态。可以勾选多个商品。
  72 +下方也悬浮两个菜单,一个ask,一个compare。
  73 +如果是点击了ask,那么,将引用这两个商品进行继续对话,如果点击了compare,那么,也是从右侧拉出一个页面,覆盖到上面,对这两个商品进行对比,页面内容为空,提示暂未实现即可)
... ...
... ... @@ -3,11 +3,12 @@ ShopAgent - Streamlit UI
3 3 Multi-modal fashion shopping assistant with conversational AI
4 4 """
5 5  
  6 +import html
6 7 import logging
7 8 import re
8 9 import uuid
9 10 from pathlib import Path
10   -from typing import Optional
  11 +from typing import Any, Optional
11 12  
12 13 import streamlit as st
13 14 from PIL import Image, ImageOps
... ... @@ -222,6 +223,120 @@ st.markdown(
222 223 .uploadedFile {
223 224 display: none;
224 225 }
  226 +
  227 + /* Product card wrapper: hover reveals action bar */
  228 + .product-card-wrapper {
  229 + position: relative;
  230 + border-radius: 8px;
  231 + overflow: hidden;
  232 + border: 1px solid #e5e5e5;
  233 + background: #fff;
  234 + }
  235 + .product-card-actions {
  236 + display: flex;
  237 + align-items: center;
  238 + justify-content: space-between;
  239 + gap: 8px;
  240 + padding: 6px 8px;
  241 + background: rgba(0,0,0,0.04);
  242 + border-top: 1px solid #eee;
  243 + opacity: 0.85;
  244 + transition: opacity 0.2s;
  245 + }
  246 + .product-card-wrapper:hover .product-card-actions {
  247 + opacity: 1;
  248 + background: rgba(0,0,0,0.06);
  249 + }
  250 +
  251 + /* Right side drawer (off-canvas) */
  252 + .side-drawer-backdrop {
  253 + position: fixed;
  254 + top: 0;
  255 + left: 0;
  256 + right: 0;
  257 + bottom: 0;
  258 + background: rgba(0,0,0,0.35);
  259 + z-index: 999998;
  260 + transition: opacity 0.25s;
  261 + }
  262 + .side-drawer-panel {
  263 + position: fixed;
  264 + top: 56px;
  265 + right: 0;
  266 + width: 85%;
  267 + max-width: 560px;
  268 + height: calc(100vh - 56px);
  269 + background: white;
  270 + box-shadow: -4px 0 20px rgba(0,0,0,0.12);
  271 + z-index: 999999;
  272 + overflow-y: auto;
  273 + transition: transform 0.25s ease-out;
  274 + }
  275 + .side-drawer-panel.open {
  276 + transform: translateX(0);
  277 + }
  278 + .side-drawer-header {
  279 + position: sticky;
  280 + top: 0;
  281 + background: white;
  282 + border-bottom: 1px solid #e5e5e5;
  283 + padding: 12px 16px;
  284 + display: flex;
  285 + align-items: center;
  286 + justify-content: space-between;
  287 + z-index: 1;
  288 + }
  289 + .side-drawer-close-link {
  290 + display: inline-flex;
  291 + align-items: center;
  292 + justify-content: center;
  293 + text-decoration: none;
  294 + color: #333 !important;
  295 + font-size: 14px;
  296 + font-weight: 500;
  297 + padding: 8px 14px;
  298 + border-radius: 8px;
  299 + border: 1px solid #ccc;
  300 + background: #f0f0f0 !important;
  301 + min-width: 60px;
  302 + box-shadow: 0 1px 2px rgba(0,0,0,0.06);
  303 + }
  304 + .side-drawer-close-link:hover {
  305 + color: #111 !important;
  306 + background: #e5e5e5 !important;
  307 + border-color: #999;
  308 + }
  309 + .side-drawer-content {
  310 + padding: 14px 16px 20px 16px;
  311 + }
  312 + /* Ensure drawer overlay is on top and not clipped by Streamlit blocks */
  313 + .side-drawer-backdrop,
  314 + .side-drawer-panel {
  315 + position: fixed !important;
  316 + }
  317 +
  318 + /* Bottom floating ask/compare bar */
  319 + .bottom-actions-bar {
  320 + position: fixed;
  321 + bottom: 70px;
  322 + left: 50%;
  323 + transform: translateX(-50%);
  324 + display: flex;
  325 + align-items: center;
  326 + gap: 12px;
  327 + padding: 8px 16px;
  328 + background: white;
  329 + border: 1px solid #e5e5e5;
  330 + border-radius: 24px;
  331 + box-shadow: 0 2px 12px rgba(0,0,0,0.08);
  332 + z-index: 1005;
  333 + max-width: 90%;
  334 + }
  335 + .bottom-actions-bar .selected-count {
  336 + font-size: 0.85rem;
  337 + color: #666;
  338 + margin-right: 4px;
  339 + }
225 340 </style>
226 341 """,
227 342 unsafe_allow_html=True,
... ... @@ -252,6 +367,22 @@ def initialize_session():
252 367 if "show_debug" not in st.session_state:
253 368 st.session_state.show_debug = True
254 369  
  370 + # Selected products for ask/compare (key -> product info dict)
  371 + if "selected_products" not in st.session_state:
  372 + st.session_state.selected_products = {}
  373 +
  374 + # Right side panel: visible, mode in ("similar", "compare"), payload (e.g. ref_id, query, or list of selected items)
  375 + if "side_panel" not in st.session_state:
  376 + st.session_state.side_panel = {
  377 + "visible": False,
  378 + "mode": None,
  379 + "payload": None,
  380 + }
  381 +
  382 + # Products currently referenced in chat input (list of product summary dicts)
  383 + if "referenced_products" not in st.session_state:
  384 + st.session_state.referenced_products = []
  385 +
255 386  
256 387 def save_uploaded_image(uploaded_file) -> Optional[str]:
257 388 """Save uploaded image to temp directory"""
... ... @@ -275,6 +406,87 @@ def save_uploaded_image(uploaded_file) -&gt; Optional[str]:
275 406 return None
276 407  
277 408  
  409 +def _product_key(ref_id: str, index: int, product: ProductItem) -> str:
  410 + """Stable unique key for a product in the session (for selection and side panel)."""
  411 + return f"{ref_id}_{index}_{product.spu_id or index}"
  412 +
  413 +
  414 +def _product_to_info(product: ProductItem, ref_id: str) -> dict:
  415 + """Serialize product to a small dict for selected_products and ask/compare."""
  416 + return {
  417 + "ref_id": ref_id,
  418 + "spu_id": product.spu_id,
  419 + "sku_id": product.spu_id,
  420 + "title": product.title or "未知商品",
  421 + "price": product.price,
  422 + "tags": product.tags or [],
  423 + "specifications": product.specifications or [],
  424 + }
  425 +
  426 +
  427 +def _compact_field(value: Any) -> str:
  428 + """Format a field into one readable line for chat reference payload."""
  429 + if value is None:
  430 + return "-"
  431 + if isinstance(value, list):
  432 + if not value:
  433 + return "-"
  434 + parts = []
  435 + for item in value:
  436 + if isinstance(item, dict):
  437 + text = ", ".join(f"{k}:{v}" for k, v in item.items())
  438 + parts.append(text if text else str(item))
  439 + else:
  440 + parts.append(str(item))
  441 + return " | ".join(p for p in parts if p) or "-"
  442 + return str(value)
  443 +
  444 +
  445 +def _build_reference_prefix(products: list[dict]) -> str:
  446 + """Build backend prompt prefix for 'chat with referenced products'."""
  447 + lines = [f"引用 {len(products)} 款商品:"]
  448 + for i, p in enumerate(products, 1):
  449 + sku_id = _compact_field(p.get("sku_id") or p.get("spu_id"))
  450 + title = _compact_field(p.get("title"))
  451 + price = _compact_field(p.get("price"))
  452 + tags = _compact_field(p.get("tags"))
  453 + specifications = _compact_field(p.get("specifications"))
  454 + lines.append(
  455 + f"{i}. sku_id={sku_id}; title={title}; price={price}; "
  456 + f"tags={tags}; specifications={specifications}"
  457 + )
  458 + return "\n".join(lines)
  459 +
  460 +
  461 +def render_referenced_products_in_input() -> None:
  462 + """Render referenced products above chat input, each with remove button."""
  463 + refs = st.session_state.get("referenced_products", [])
  464 + if not refs:
  465 + return
  466 +
  467 + st.markdown("**已引用商品**")
  468 + remove_idx = None
  469 + for idx, item in enumerate(refs):
  470 + with st.container(border=True):
  471 + c1, c2 = st.columns([12, 1])
  472 + with c1:
  473 + title = (item.get("title") or "未知商品")[:80]
  474 + st.markdown(f"**{title}**")
  475 + st.caption(
  476 + f"sku_id={item.get('sku_id') or item.get('spu_id') or '-'}; "
  477 + f"price={_compact_field(item.get('price'))}; "
  478 + f"tags={_compact_field(item.get('tags'))}; "
  479 + f"specifications={_compact_field(item.get('specifications'))}"
  480 + )
  481 + with c2:
  482 + if st.button("✕", key=f"remove_ref_{idx}", help="删除该引用"):
  483 + remove_idx = idx
  484 + if remove_idx is not None:
  485 + refs.pop(remove_idx)
  486 + st.session_state.referenced_products = refs
  487 + st.rerun()
  488 +
  489 +
278 490 def _load_product_image(product: ProductItem) -> Optional[Image.Image]:
279 491 """Try to load a product image: image_url from API (normalized when stored) → local data/images → None."""
280 492 if product.image_url:
... ... @@ -296,10 +508,39 @@ def _load_product_image(product: ProductItem) -&gt; Optional[Image.Image]:
296 508 return None
297 509  
298 510  
299   -def display_product_card_from_item(product: ProductItem) -> None:
300   - """Render a single product card from a ProductItem (registry entry)."""
301   - img = _load_product_image(product)
  511 +def _run_similar_search(query: str) -> Optional[str]:
  512 + """Run product search with query, register result, return new ref_id or None."""
  513 + if not query or not query.strip():
  514 + return None
  515 + from app.tools.search_tools import make_search_products_tool
  516 +
  517 + session_id = st.session_state.get("session_id", "")
  518 + if not session_id:
  519 + return None
  520 + tool = make_search_products_tool(session_id, global_registry)
  521 + try:
  522 + out = tool.invoke({"query": query.strip(), "limit": 12})
  523 + match = SEARCH_REF_PATTERN.search(out)
  524 + if match:
  525 + return match.group(1).strip()
  526 + except Exception as e:
  527 + logger.warning(f"Similar search failed: {e}")
  528 + return None
302 529  
  530 +
  531 +def display_product_card_from_item(
  532 + product: ProductItem,
  533 + ref_id: str,
  534 + index: int,
  535 +) -> None:
  536 + """Render a single product card with hover actions: Similar products + checkbox."""
  537 + pkey = _product_key(ref_id, index, product)
  538 + info = _product_to_info(product, ref_id)
  539 + selected = st.session_state.selected_products
  540 +
  541 + st.markdown('<div class="product-card-wrapper">', unsafe_allow_html=True)
  542 +
  543 + img = _load_product_image(product)
303 544 if img:
304 545 target = (220, 220)
305 546 try:
... ... @@ -324,6 +565,47 @@ def display_product_card_from_item(product: ProductItem) -&gt; None:
324 565 label_style = "⭐" if product.match_label == "Relevant" else "✦"
325 566 st.caption(f"{label_style} {product.match_label}")
326 567  
  568 + st.markdown('<div class="product-card-actions">', unsafe_allow_html=True)
  569 + col_a, col_b = st.columns([1, 1])
  570 + with col_a:
  571 + similar_clicked = st.button(
  572 + "Similar products",
  573 + key=f"similar_{pkey}",
  574 + help="Search by this product title and show in side panel",
  575 + )
  576 + with col_b:
  577 + is_checked = st.checkbox(
  578 + "Select",
  579 + key=f"select_{pkey}",
  580 + value=(pkey in selected),
  581 + label_visibility="collapsed",
  582 + )
  583 + st.markdown("</div>", unsafe_allow_html=True)
  584 + st.markdown("</div>", unsafe_allow_html=True)
  585 +
  586 + if similar_clicked:
  587 + search_query = (product.title or "").strip() or "商品"
  588 + new_ref = _run_similar_search(search_query)
  589 + if new_ref:
  590 + st.session_state.side_panel = {
  591 + "visible": True,
  592 + "mode": "similar",
  593 + "payload": {"ref_id": new_ref, "query": search_query},
  594 + }
  595 + else:
  596 + st.session_state.side_panel = {
  597 + "visible": True,
  598 + "mode": "similar",
  599 + "payload": {"ref_id": None, "query": search_query, "error": True},
  600 + }
  601 + st.rerun()
  602 +
  603 + if is_checked:
  604 + if pkey not in selected:
  605 + selected[pkey] = info
  606 + else:
  607 + selected.pop(pkey, None)
  608 +
327 609  
328 610 def render_search_result_block(result: SearchResult) -> None:
329 611 """
... ... @@ -358,7 +640,7 @@ def render_search_result_block(result: SearchResult) -&gt; None:
358 640 cols = st.columns(min(len(to_show), 3))
359 641 for i, product in enumerate(to_show):
360 642 with cols[i % 3]:
361   - display_product_card_from_item(product)
  643 + display_product_card_from_item(product, result.ref_id, i)
362 644  
363 645  
364 646 def render_message_with_refs(
... ... @@ -463,11 +745,142 @@ def display_message(message: dict):
463 745 st.markdown("</div>", unsafe_allow_html=True)
464 746  
465 747  
  748 +def render_bottom_actions_bar() -> None:
  749 + """Show Ask and Compare when there are selected products. Disabled when none selected."""
  750 + selected = st.session_state.selected_products
  751 + n = len(selected)
  752 + if n == 0:
  753 + return
  754 + st.markdown(
  755 + '<div class="bottom-actions-bar">',
  756 + unsafe_allow_html=True,
  757 + )
  758 + col_sel, col_ask, col_cmp = st.columns([2, 1, 1])
  759 + with col_sel:
  760 + st.caption(f"Selected: {n}")
  761 + with col_ask:
  762 + ask_clicked = st.button("Ask", key="bottom_ask", help="Continue conversation with selected products")
  763 + with col_cmp:
  764 + compare_clicked = st.button("Compare", key="bottom_compare", help="Compare selected products")
  765 + st.markdown("</div>", unsafe_allow_html=True)
  766 +
  767 + if ask_clicked:
  768 + st.session_state.referenced_products = list(selected.values())
  769 + st.rerun()
  770 + if compare_clicked:
  771 + st.session_state.side_panel = {
  772 + "visible": True,
  773 + "mode": "compare",
  774 + "payload": list(selected.values()),
  775 + }
  776 + st.rerun()
  777 +
  778 +
  779 +def render_side_drawer() -> None:
  780 + """Render a fixed overlay side drawer that does not change background layout."""
  781 + panel = st.session_state.side_panel
  782 + if not panel.get("visible") or not panel.get("mode"):
  783 + return
  784 +
  785 + mode = panel["mode"]
  786 + payload = panel.get("payload") or {}
  787 + session_id = st.session_state.get("session_id", "")
  788 +
  789 + title = "Similar products" if mode == "similar" else "Compare"
  790 + body_html = ""
  791 +
  792 + if mode == "similar":
  793 + ref_id = payload.get("ref_id")
  794 + query = html.escape(payload.get("query", ""))
  795 + if payload.get("error") or not ref_id:
  796 + body_html = '<p style="color:#666;">搜索失败或暂无结果。</p>'
  797 + else:
  798 + result = global_registry.get(session_id, ref_id)
  799 + if not result:
  800 + body_html = f'<p style="color:#666;">[搜索结果 {html.escape(ref_id)} 不可用]</p>'
  801 + else:
  802 + perfect = [p for p in result.products if p.match_label == "Relevant"]
  803 + partial = [p for p in result.products if p.match_label == "Partially Relevant"]
  804 + to_show = (perfect + partial)[:12] if perfect else partial[:12]
  805 + cards = []
  806 + for product in to_show:
  807 + p_title = html.escape((product.title or "未知商品")[:80])
  808 + p_label = html.escape(product.match_label or "Partially Relevant")
  809 + price = (
  810 + f"¥{product.price:.2f}"
  811 + if product.price is not None
  812 + else "价格待更新"
  813 + )
  814 + image_html = (
  815 + f'<img src="{html.escape(product.image_url)}" alt="{p_title}" '
  816 + 'style="width:64px;height:64px;object-fit:cover;border-radius:8px;border:1px solid #eee;" />'
  817 + if product.image_url
  818 + else '<div style="width:64px;height:64px;background:#f5f5f5;border-radius:8px;'
  819 + 'display:flex;align-items:center;justify-content:center;color:#bbb;">🛍️</div>'
  820 + )
  821 + cards.append(
  822 + '<div style="display:flex;gap:10px;border:1px solid #eee;border-radius:10px;'
  823 + 'padding:10px;background:#fff;">'
  824 + f"{image_html}"
  825 + '<div style="flex:1;min-width:0;">'
  826 + f'<div style="font-weight:600;color:#111;line-height:1.35;">{p_title}</div>'
  827 + f'<div style="font-size:0.9rem;color:#555;margin-top:4px;">{price}</div>'
  828 + f'<div style="font-size:0.8rem;color:#777;margin-top:4px;">{p_label}</div>'
  829 + "</div></div>"
  830 + )
  831 +
  832 + cards_html = "".join(cards) if cards else '<p style="color:#666;">(未找到可展示的商品)</p>'
  833 + body_html = (
  834 + f'<div style="font-size:0.92rem;color:#555;margin-bottom:10px;">'
  835 + f'基于「{query}」的搜索结果:</div>'
  836 + '<div style="display:grid;gap:10px;">'
  837 + f"{cards_html}"
  838 + "</div>"
  839 + )
  840 + else:
  841 + items = payload if isinstance(payload, list) else []
  842 + if items:
  843 + rows = []
  844 + for item in items:
  845 + t = html.escape((item.get("title") or "未知商品")[:80])
  846 + p = item.get("price")
  847 + ptext = f"¥{p:.2f}" if p is not None else "价格待更新"
  848 + rows.append(
  849 + '<div style="border:1px solid #eee;border-radius:10px;padding:10px;background:#fff;">'
  850 + f'<div style="font-weight:600;color:#111;">{t}</div>'
  851 + f'<div style="font-size:0.9rem;color:#555;margin-top:4px;">{ptext}</div>'
  852 + "</div>"
  853 + )
  854 + items_html = "".join(rows)
  855 + else:
  856 + items_html = '<p style="color:#666;">当前未选中商品。</p>'
  857 + body_html = (
  858 + '<div style="margin-bottom:10px;color:#555;">已选商品:</div>'
  859 + f'<div style="display:grid;gap:10px;">{items_html}</div>'
  860 + '<div style="margin-top:14px;padding:10px 12px;border-radius:8px;'
  861 + 'background:#fff3cd;color:#856404;">对比功能暂未实现。</div>'
  862 + )
  863 +
  864 + st.markdown(
  865 + f"""
  866 + <div class="side-drawer-backdrop"></div>
  867 + <div class="side-drawer-panel open">
  868 + <div class="side-drawer-header">
  869 + <div style="font-weight:600;">{html.escape(title)}</div>
  870 + <a class="side-drawer-close-link" href="./?close_side_panel=1" target="_self">✕ 关闭</a>
  871 + </div>
  872 + <div class="side-drawer-content">
  873 + {body_html}
  874 + </div>
  875 + </div>
  876 + """,
  877 + unsafe_allow_html=True,
  878 + )
  879 +
  880 +
466 881 def display_welcome():
467 882 """Display welcome screen"""
468   -
469 883 col1, col2, col3, col4 = st.columns(4)
470   -
471 884 with col1:
472 885 st.markdown(
473 886 """
... ... @@ -518,10 +931,17 @@ def display_welcome():
518 931  
519 932 st.markdown("<br><br>", unsafe_allow_html=True)
520 933  
521   -
522 934 def main():
523 935 """Main Streamlit app"""
524 936 initialize_session()
  937 + # Close overlay via query param (used by HTML close link in side drawer)
  938 + if st.query_params.get("close_side_panel"):
  939 + st.session_state.side_panel = {"visible": False, "mode": None, "payload": None}
  940 + del st.query_params["close_side_panel"]
  941 + st.rerun()
  942 +
  943 + # Drawer overlay first so fixed positioning is relative to viewport (not a bottom block)
  944 + render_side_drawer()
525 945  
526 946 # Header
527 947 st.markdown(
... ... @@ -547,6 +967,9 @@ def main():
547 967 global_registry.clear_session(session_id)
548 968 st.session_state.messages = []
549 969 st.session_state.uploaded_image = None
  970 + st.session_state.selected_products = {}
  971 + st.session_state.referenced_products = []
  972 + st.session_state.side_panel = {"visible": False, "mode": None, "payload": None}
550 973 st.rerun()
551 974  
552 975 # Debug toggle
... ... @@ -561,15 +984,14 @@ def main():
561 984 st.markdown("---")
562 985 st.caption(f"Session: `{st.session_state.session_id[:8]}...`")
563 986  
564   - # Chat messages container
565 987 messages_container = st.container()
566   -
567 988 with messages_container:
568 989 if not st.session_state.messages:
569 990 display_welcome()
570 991 else:
571 992 for message in st.session_state.messages:
572 993 display_message(message)
  994 + render_bottom_actions_bar()
573 995  
574 996 # Fixed input area at bottom (using container to simulate fixed position)
575 997 st.markdown('<div class="fixed-input-container">', unsafe_allow_html=True)
... ... @@ -598,6 +1020,9 @@ def main():
598 1020 st.session_state.show_image_upload = False
599 1021 st.rerun()
600 1022  
  1023 + # Referenced products area (shown above chat input, each can be removed)
  1024 + render_referenced_products_in_input()
  1025 +
601 1026 # Input row
602 1027 col1, col2 = st.columns([1, 12])
603 1028  
... ... @@ -620,6 +1045,12 @@ def main():
620 1045  
621 1046 # Process user input
622 1047 if user_query:
  1048 + raw_user_query = user_query
  1049 + referenced_products = list(st.session_state.get("referenced_products", []))
  1050 + agent_query = raw_user_query
  1051 + if referenced_products:
  1052 + agent_query = f"{_build_reference_prefix(referenced_products)}\n\n{raw_user_query}"
  1053 +
623 1054 # Ensure shopping agent is initialized
624 1055 if "shopping_agent" not in st.session_state:
625 1056 st.error("Session not initialized. Please refresh the page.")
... ... @@ -632,9 +1063,8 @@ def main():
632 1063 image_path = save_uploaded_image(st.session_state.uploaded_image)
633 1064 else:
634 1065 # Check if query refers to a previous image
635   - query_lower = user_query.lower()
636 1066 if any(
637   - ref in query_lower
  1067 + ref in raw_user_query.lower()
638 1068 for ref in [
639 1069 "this",
640 1070 "that",
... ... @@ -655,11 +1085,14 @@ def main():
655 1085 st.session_state.messages.append(
656 1086 {
657 1087 "role": "user",
658   - "content": user_query,
  1088 + "content": raw_user_query,
659 1089 "image_path": image_path,
660 1090 }
661 1091 )
662 1092  
  1093 + # References are consumed once this message is sent
  1094 + st.session_state.referenced_products = []
  1095 +
663 1096 # Display user message immediately
664 1097 with messages_container:
665 1098 display_message(st.session_state.messages[-1])
... ... @@ -667,12 +1100,10 @@ def main():
667 1100 # Process with shopping agent
668 1101 try:
669 1102 shopping_agent = st.session_state.shopping_agent
670   -
671   - # Handle greetings without invoking the agent
672   - query_lower = user_query.lower().strip()
  1103 +
673 1104 # Process with agent
674 1105 result = shopping_agent.chat(
675   - query=user_query,
  1106 + query=agent_query,
676 1107 image_path=image_path,
677 1108 )
678 1109 response = result["response"]
... ...