e7f2b240
tangwang
first commit
|
1
|
"""
|
bad17b15
tangwang
调通baseline
|
2
|
ShopAgent - Streamlit UI
|
e7f2b240
tangwang
first commit
|
3
4
5
|
Multi-modal fashion shopping assistant with conversational AI
"""
|
1a7debb3
tangwang
引用商品进行追问
|
6
|
import html
|
e7f2b240
tangwang
first commit
|
7
8
9
|
import logging
import re
import uuid
|
897b5ca9
tangwang
perf: 前端性能优化 + 搜索...
|
10
|
from collections import OrderedDict
|
e7f2b240
tangwang
first commit
|
11
|
from pathlib import Path
|
1a7debb3
tangwang
引用商品进行追问
|
12
|
from typing import Any, Optional
|
e7f2b240
tangwang
first commit
|
13
14
|
import streamlit as st
|
7e985858
tangwang
feat(ui): product...
|
15
|
import streamlit.components.v1 as st_components
|
e7f2b240
tangwang
first commit
|
16
17
18
|
from PIL import Image, ImageOps
from app.agents.shopping_agent import ShoppingAgent
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
19
20
|
from app.search_registry import ProductItem, SearchResult, global_registry
|
897b5ca9
tangwang
perf: 前端性能优化 + 搜索...
|
21
|
# Matches [SEARCH_RESULTS_REF:sr_xxxxxxxx] tokens embedded in AI responses.
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
22
|
# Case-insensitive, optional spaces around the id.
|
897b5ca9
tangwang
perf: 前端性能优化 + 搜索...
|
23
|
SEARCH_RESULTS_REF_PATTERN = re.compile(r"\[SEARCH_RESULTS_REF:\s*([a-zA-Z0-9_]+)\s*\]", re.IGNORECASE)
|
e7f2b240
tangwang
first commit
|
24
25
26
27
28
29
30
31
|
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)
|
897b5ca9
tangwang
perf: 前端性能优化 + 搜索...
|
32
33
34
35
|
# In-memory image cache (url or "local:path" -> PIL Image), max 100 entries
_IMAGE_CACHE: OrderedDict = OrderedDict()
_IMAGE_CACHE_MAX = 100
|
e7f2b240
tangwang
first commit
|
36
37
|
# Page config
st.set_page_config(
|
bad17b15
tangwang
调通baseline
|
38
|
page_title="ShopAgent",
|
e7f2b240
tangwang
first commit
|
39
40
41
42
43
44
45
46
47
|
page_icon="👗",
layout="centered",
initial_sidebar_state="collapsed",
)
# Custom CSS - ChatGPT-like style
st.markdown(
"""
<style>
|
9ad88986
tangwang
up`
|
48
49
|
/* Show default Streamlit elements (sidebar toggle, etc.) */
/* #MainMenu, footer, header no longer hidden */
|
e7f2b240
tangwang
first commit
|
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
|
/* Body and root container */
.main .block-container {
padding-bottom: 180px !important;
padding-top: 2rem;
max-width: 900px;
margin: 0 auto;
}
/* Fixed input container at bottom */
.fixed-input-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
border-top: 1px solid #e5e5e5;
padding: 1rem 0;
z-index: 1000;
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
}
.fixed-input-container .block-container {
max-width: 900px;
margin: 0 auto;
padding: 0 1rem !important;
}
/* Message bubbles */
.message {
margin: 1rem 0;
padding: 1rem 1.5rem;
border-radius: 1rem;
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.user-message {
background: transparent;
margin: 0 0 1rem 0;
padding: 0;
border-radius: 0;
}
.assistant-message {
background: white;
border: 1px solid #e5e5e5;
margin-right: 3rem;
}
/* Product cards - simplified */
.stImage {
border-radius: 0px;
overflow: hidden;
}
.stImage img {
transition: transform 0.2s;
}
.stImage:hover img {
transform: scale(1.05);
}
/* Scroll to bottom behavior */
html {
scroll-behavior: smooth;
}
/* Header */
.app-header {
text-align: center;
padding: 2rem 1rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 1rem;
margin-bottom: 2rem;
}
.app-title {
font-size: 2rem;
font-weight: 700;
margin: 0;
}
.app-subtitle {
font-size: 1rem;
opacity: 0.9;
margin-top: 0.5rem;
}
/* Welcome screen */
.welcome-container {
text-align: center;
padding: 4rem 2rem;
color: #666;
}
.welcome-title {
font-size: 2rem;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 1rem;
}
.welcome-features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin: 2rem 0;
}
.feature-card {
background: #f7f7f8;
padding: 1.5rem;
border-radius: 12px;
transition: all 0.2s;
}
.feature-card:hover {
background: #efefef;
transform: translateY(-2px);
}
.feature-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.feature-title {
font-weight: 600;
margin-bottom: 0.25rem;
}
/* Image preview */
.image-preview {
position: relative;
display: inline-block;
margin: 0.5rem 0;
}
.image-preview img {
max-width: 200px;
border-radius: 8px;
border: 2px solid #e5e5e5;
}
.remove-image-btn {
position: absolute;
top: 5px;
right: 5px;
background: rgba(0,0,0,0.6);
color: white;
border: none;
border-radius: 50%;
width: 24px;
height: 24px;
cursor: pointer;
font-size: 14px;
}
/* Buttons */
.stButton>button {
border-radius: 8px;
border: 1px solid #e5e5e5;
padding: 0.5rem 1rem;
transition: all 0.2s;
}
.stButton>button:hover {
background: #f0f0f0;
border-color: #d0d0d0;
}
/* Hide upload button label */
.uploadedFile {
display: none;
}
|
1a7debb3
tangwang
引用商品进行追问
|
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
|
/* Product card wrapper: hover reveals action bar */
.product-card-wrapper {
position: relative;
border-radius: 8px;
overflow: hidden;
border: 1px solid #e5e5e5;
background: #fff;
}
.product-card-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 6px 8px;
background: rgba(0,0,0,0.04);
border-top: 1px solid #eee;
opacity: 0.85;
transition: opacity 0.2s;
}
.product-card-wrapper:hover .product-card-actions {
opacity: 1;
background: rgba(0,0,0,0.06);
}
/* Right side drawer (off-canvas) */
.side-drawer-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.35);
z-index: 999998;
transition: opacity 0.25s;
}
.side-drawer-panel {
position: fixed;
top: 56px;
right: 0;
width: 85%;
max-width: 560px;
height: calc(100vh - 56px);
background: white;
box-shadow: -4px 0 20px rgba(0,0,0,0.12);
z-index: 999999;
overflow-y: auto;
transition: transform 0.25s ease-out;
}
.side-drawer-panel.open {
transform: translateX(0);
}
.side-drawer-header {
position: sticky;
top: 0;
background: white;
border-bottom: 1px solid #e5e5e5;
padding: 12px 16px;
display: flex;
align-items: center;
justify-content: space-between;
z-index: 1;
}
|
1a7debb3
tangwang
引用商品进行追问
|
295
296
297
|
.side-drawer-content {
padding: 14px 16px 20px 16px;
}
|
1a7debb3
tangwang
引用商品进行追问
|
298
299
300
301
|
.side-drawer-backdrop,
.side-drawer-panel {
position: fixed !important;
}
|
7e985858
tangwang
feat(ui): product...
|
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
|
#side-drawer-close-btn {
display: inline-flex;
align-items: center;
gap: 4px;
border: 1px solid #ccc;
background: #f5f5f5;
color: #333;
font-size: 14px;
font-weight: 500;
padding: 6px 14px;
border-radius: 8px;
cursor: pointer;
}
#side-drawer-close-btn:hover {
background: #e8e8e8;
color: #111;
}
|
1a7debb3
tangwang
引用商品进行追问
|
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
|
/* Bottom floating ask/compare bar */
.bottom-actions-bar {
position: fixed;
bottom: 70px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 12px;
padding: 8px 16px;
background: white;
border: 1px solid #e5e5e5;
border-radius: 24px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
z-index: 1005;
max-width: 90%;
}
.bottom-actions-bar .selected-count {
font-size: 0.85rem;
color: #666;
margin-right: 4px;
}
|
e7f2b240
tangwang
first commit
|
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
|
</style>
""",
unsafe_allow_html=True,
)
# Initialize session state
def initialize_session():
"""Initialize session state variables"""
if "session_id" not in st.session_state:
st.session_state.session_id = str(uuid.uuid4())
if "shopping_agent" not in st.session_state:
st.session_state.shopping_agent = ShoppingAgent(
session_id=st.session_state.session_id
)
if "messages" not in st.session_state:
st.session_state.messages = []
if "uploaded_image" not in st.session_state:
st.session_state.uploaded_image = None
if "show_image_upload" not in st.session_state:
st.session_state.show_image_upload = False
|
825828c4
tangwang
fix: search image...
|
368
|
# Debug panel toggle (default True so 显示调试过程 is checked by default)
|
01b46131
tangwang
流程跑通
|
369
|
if "show_debug" not in st.session_state:
|
825828c4
tangwang
fix: search image...
|
370
|
st.session_state.show_debug = True
|
01b46131
tangwang
流程跑通
|
371
|
|
1a7debb3
tangwang
引用商品进行追问
|
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
|
# Selected products for ask/compare (key -> product info dict)
if "selected_products" not in st.session_state:
st.session_state.selected_products = {}
# Right side panel: visible, mode in ("similar", "compare"), payload (e.g. ref_id, query, or list of selected items)
if "side_panel" not in st.session_state:
st.session_state.side_panel = {
"visible": False,
"mode": None,
"payload": None,
}
# Products currently referenced in chat input (list of product summary dicts)
if "referenced_products" not in st.session_state:
st.session_state.referenced_products = []
|
e7f2b240
tangwang
first commit
|
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
|
def save_uploaded_image(uploaded_file) -> Optional[str]:
"""Save uploaded image to temp directory"""
if uploaded_file is None:
return None
try:
temp_dir = Path("temp_uploads")
temp_dir.mkdir(exist_ok=True)
image_path = temp_dir / f"{st.session_state.session_id}_{uploaded_file.name}"
with open(image_path, "wb") as f:
f.write(uploaded_file.getbuffer())
logger.info(f"Saved uploaded image to {image_path}")
return str(image_path)
except Exception as e:
logger.error(f"Error saving uploaded image: {e}")
st.error(f"Failed to save image: {str(e)}")
return None
|
1a7debb3
tangwang
引用商品进行追问
|
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
|
def _product_key(ref_id: str, index: int, product: ProductItem) -> str:
"""Stable unique key for a product in the session (for selection and side panel)."""
return f"{ref_id}_{index}_{product.spu_id or index}"
def _product_to_info(product: ProductItem, ref_id: str) -> dict:
"""Serialize product to a small dict for selected_products and ask/compare."""
return {
"ref_id": ref_id,
"spu_id": product.spu_id,
"sku_id": product.spu_id,
"title": product.title or "未知商品",
"price": product.price,
"tags": product.tags or [],
"specifications": product.specifications or [],
}
def _compact_field(value: Any) -> str:
"""Format a field into one readable line for chat reference payload."""
if value is None:
return "-"
if isinstance(value, list):
if not value:
return "-"
parts = []
for item in value:
if isinstance(item, dict):
text = ", ".join(f"{k}:{v}" for k, v in item.items())
parts.append(text if text else str(item))
else:
parts.append(str(item))
return " | ".join(p for p in parts if p) or "-"
return str(value)
def _build_reference_prefix(products: list[dict]) -> str:
"""Build backend prompt prefix for 'chat with referenced products'."""
lines = [f"引用 {len(products)} 款商品:"]
for i, p in enumerate(products, 1):
sku_id = _compact_field(p.get("sku_id") or p.get("spu_id"))
title = _compact_field(p.get("title"))
price = _compact_field(p.get("price"))
tags = _compact_field(p.get("tags"))
specifications = _compact_field(p.get("specifications"))
lines.append(
f"{i}. sku_id={sku_id}; title={title}; price={price}; "
f"tags={tags}; specifications={specifications}"
)
return "\n".join(lines)
|
897b5ca9
tangwang
perf: 前端性能优化 + 搜索...
|
463
|
@st.fragment
|
1a7debb3
tangwang
引用商品进行追问
|
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
|
def render_referenced_products_in_input() -> None:
"""Render referenced products above chat input, each with remove button."""
refs = st.session_state.get("referenced_products", [])
if not refs:
return
st.markdown("**已引用商品**")
remove_idx = None
for idx, item in enumerate(refs):
with st.container(border=True):
c1, c2 = st.columns([12, 1])
with c1:
title = (item.get("title") or "未知商品")[:80]
st.markdown(f"**{title}**")
st.caption(
f"sku_id={item.get('sku_id') or item.get('spu_id') or '-'}; "
f"price={_compact_field(item.get('price'))}; "
f"tags={_compact_field(item.get('tags'))}; "
f"specifications={_compact_field(item.get('specifications'))}"
)
with c2:
if st.button("✕", key=f"remove_ref_{idx}", help="删除该引用"):
remove_idx = idx
if remove_idx is not None:
refs.pop(remove_idx)
st.session_state.referenced_products = refs
st.rerun()
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
493
|
def _load_product_image(product: ProductItem) -> Optional[Image.Image]:
|
897b5ca9
tangwang
perf: 前端性能优化 + 搜索...
|
494
495
|
"""Load product image with cache: image_url or local data/images. Cache key = url or 'local:path'."""
cache_key: Optional[str] = None
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
496
|
if product.image_url:
|
897b5ca9
tangwang
perf: 前端性能优化 + 搜索...
|
497
498
499
500
|
cache_key = product.image_url
if cache_key in _IMAGE_CACHE:
_IMAGE_CACHE.move_to_end(cache_key)
return _IMAGE_CACHE[cache_key]
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
501
|
try:
|
897b5ca9
tangwang
perf: 前端性能优化 + 搜索...
|
502
|
import io
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
503
504
505
|
import requests
resp = requests.get(product.image_url, timeout=10)
if resp.status_code == 200:
|
897b5ca9
tangwang
perf: 前端性能优化 + 搜索...
|
506
507
508
509
510
511
|
img = Image.open(io.BytesIO(resp.content))
_IMAGE_CACHE[cache_key] = img
_IMAGE_CACHE.move_to_end(cache_key)
if len(_IMAGE_CACHE) > _IMAGE_CACHE_MAX:
_IMAGE_CACHE.popitem(last=False)
return img
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
512
513
514
515
516
|
except Exception as e:
logger.debug(f"Remote image fetch failed for {product.spu_id}: {e}")
local = Path(f"data/images/{product.spu_id}.jpg")
if local.exists():
|
897b5ca9
tangwang
perf: 前端性能优化 + 搜索...
|
517
518
519
520
|
cache_key = f"local:{local}"
if cache_key in _IMAGE_CACHE:
_IMAGE_CACHE.move_to_end(cache_key)
return _IMAGE_CACHE[cache_key]
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
521
|
try:
|
897b5ca9
tangwang
perf: 前端性能优化 + 搜索...
|
522
523
524
525
526
527
|
img = Image.open(local)
_IMAGE_CACHE[cache_key] = img
_IMAGE_CACHE.move_to_end(cache_key)
if len(_IMAGE_CACHE) > _IMAGE_CACHE_MAX:
_IMAGE_CACHE.popitem(last=False)
return img
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
528
529
530
531
532
|
except Exception as e:
logger.debug(f"Local image load failed {local}: {e}")
return None
|
1a7debb3
tangwang
引用商品进行追问
|
533
534
535
536
|
def display_product_card_from_item(
product: ProductItem,
ref_id: str,
index: int,
|
7e985858
tangwang
feat(ui): product...
|
537
|
widget_prefix: str = "",
|
1a7debb3
tangwang
引用商品进行追问
|
538
539
540
|
) -> None:
"""Render a single product card with hover actions: Similar products + checkbox."""
pkey = _product_key(ref_id, index, product)
|
7e985858
tangwang
feat(ui): product...
|
541
|
key_suffix = f"{widget_prefix}_{pkey}" if widget_prefix else pkey
|
1a7debb3
tangwang
引用商品进行追问
|
542
543
544
545
|
info = _product_to_info(product, ref_id)
selected = st.session_state.selected_products
st.markdown('<div class="product-card-wrapper">', unsafe_allow_html=True)
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
546
|
|
1a7debb3
tangwang
引用商品进行追问
|
547
|
img = _load_product_image(product)
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
548
549
550
551
552
553
|
if img:
target = (220, 220)
try:
img = ImageOps.fit(img, target, method=Image.Resampling.LANCZOS)
except AttributeError:
img = ImageOps.fit(img, target, method=Image.LANCZOS)
|
825828c4
tangwang
fix: search image...
|
554
|
st.image(img, width="stretch")
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
555
556
557
558
559
560
561
562
563
564
565
566
567
568
|
else:
st.markdown(
'<div style="height:120px;background:#f5f5f5;border-radius:6px;'
'display:flex;align-items:center;justify-content:center;'
'color:#bbb;font-size:2rem;">🛍️</div>',
unsafe_allow_html=True,
)
title = product.title or "未知商品"
st.markdown(f"**{title[:40]}**" + ("…" if len(title) > 40 else ""))
if product.price is not None:
st.caption(f"¥{product.price:.2f}")
|
621b6925
tangwang
up
|
569
|
label_style = "⭐" if product.match_label == "Relevant" else "✦"
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
570
|
st.caption(f"{label_style} {product.match_label}")
|
e7f2b240
tangwang
first commit
|
571
|
|
1a7debb3
tangwang
引用商品进行追问
|
572
573
574
575
576
|
st.markdown('<div class="product-card-actions">', unsafe_allow_html=True)
col_a, col_b = st.columns([1, 1])
with col_a:
similar_clicked = st.button(
"Similar products",
|
7e985858
tangwang
feat(ui): product...
|
577
|
key=f"similar_{key_suffix}",
|
1a7debb3
tangwang
引用商品进行追问
|
578
579
580
581
582
|
help="Search by this product title and show in side panel",
)
with col_b:
is_checked = st.checkbox(
"Select",
|
7e985858
tangwang
feat(ui): product...
|
583
|
key=f"select_{key_suffix}",
|
1a7debb3
tangwang
引用商品进行追问
|
584
585
586
587
588
589
590
591
|
value=(pkey in selected),
label_visibility="collapsed",
)
st.markdown("</div>", unsafe_allow_html=True)
st.markdown("</div>", unsafe_allow_html=True)
if similar_clicked:
search_query = (product.title or "").strip() or "商品"
|
897b5ca9
tangwang
perf: 前端性能优化 + 搜索...
|
592
593
594
595
596
|
st.session_state.side_panel = {
"visible": True,
"mode": "similar",
"payload": {"query": search_query, "loading": True},
}
|
1a7debb3
tangwang
引用商品进行追问
|
597
598
599
600
601
602
603
604
|
st.rerun()
if is_checked:
if pkey not in selected:
selected[pkey] = info
else:
selected.pop(pkey, None)
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
605
|
|
7e985858
tangwang
feat(ui): product...
|
606
|
def render_search_result_block(result: SearchResult, widget_prefix: str = "") -> None:
|
e7f2b240
tangwang
first commit
|
607
|
"""
|
897b5ca9
tangwang
perf: 前端性能优化 + 搜索...
|
608
|
Render a full search result block in place of a [SEARCH_RESULTS_REF:ref_id] token.
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
609
|
|
7e985858
tangwang
feat(ui): product...
|
610
|
widget_prefix: unique per (message, ref block) so Streamlit widget keys stay unique.
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
611
|
"""
|
5e3d6d3a
tangwang
refactor(search):...
|
612
|
summary_line = f' · {result.quality_summary}' if result.quality_summary else ''
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
613
614
615
616
617
|
header_html = (
f'<div style="border:1px solid #e0e0e0;border-radius:8px;padding:10px 14px;'
f'margin:8px 0 4px 0;background:#fafafa;">'
f'<span style="font-size:0.8rem;color:#555;">'
f'🔍 <b>{result.query}</b>'
|
621b6925
tangwang
up
|
618
|
f' · Relevant {result.perfect_count} 件'
|
5e3d6d3a
tangwang
refactor(search):...
|
619
620
|
f' · Partially Relevant {result.partial_count} 件'
f'{summary_line}'
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
621
622
623
624
625
|
f'</span></div>'
)
st.markdown(header_html, unsafe_allow_html=True)
# Perfect matches first, fall back to partials if none
|
621b6925
tangwang
up
|
626
|
perfect = [p for p in result.products if p.match_label == "Relevant"]
|
5e3d6d3a
tangwang
refactor(search):...
|
627
|
partial = [p for p in result.products if p.match_label == "Partially Relevant"]
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
628
629
630
631
632
633
634
635
636
|
to_show = (perfect + partial)[:6] if perfect else partial[:6]
if not to_show:
st.caption("(本次搜索未找到可展示的商品)")
return
cols = st.columns(min(len(to_show), 3))
for i, product in enumerate(to_show):
with cols[i % 3]:
|
7e985858
tangwang
feat(ui): product...
|
637
638
639
|
display_product_card_from_item(
product, result.ref_id, i, widget_prefix=widget_prefix
)
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
640
641
|
|
621b6925
tangwang
up
|
642
643
644
645
|
def render_message_with_refs(
content: str,
session_id: str,
fallback_refs: Optional[dict] = None,
|
7e985858
tangwang
feat(ui): product...
|
646
|
msg_index: int = 0,
|
621b6925
tangwang
up
|
647
|
) -> None:
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
648
|
"""
|
897b5ca9
tangwang
perf: 前端性能优化 + 搜索...
|
649
|
Render an assistant message that may contain [SEARCH_RESULTS_REF:ref_id] tokens.
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
650
|
|
7e985858
tangwang
feat(ui): product...
|
651
|
msg_index: message index in chat, used to keep widget keys unique across messages.
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
652
|
"""
|
621b6925
tangwang
up
|
653
|
fallback_refs = fallback_refs or {}
|
897b5ca9
tangwang
perf: 前端性能优化 + 搜索...
|
654
|
parts = SEARCH_RESULTS_REF_PATTERN.split(content)
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
655
656
657
|
for i, segment in enumerate(parts):
if i % 2 == 0:
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
658
659
660
|
text = segment.strip()
if text:
st.markdown(text)
|
e7f2b240
tangwang
first commit
|
661
|
else:
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
662
|
ref_id = segment.strip()
|
621b6925
tangwang
up
|
663
|
result = global_registry.get(session_id, ref_id) or fallback_refs.get(ref_id)
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
664
|
if result:
|
7e985858
tangwang
feat(ui): product...
|
665
666
|
widget_prefix = f"m{msg_index}_r{i}"
render_search_result_block(result, widget_prefix=widget_prefix)
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
667
|
else:
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
668
|
st.caption(f"[搜索结果 {ref_id} 不可用]")
|
e7f2b240
tangwang
first commit
|
669
670
|
|
7e985858
tangwang
feat(ui): product...
|
671
672
|
def display_message(message: dict, msg_index: int = 0):
"""Display a chat message. msg_index keeps widget keys unique across messages."""
|
e7f2b240
tangwang
first commit
|
673
674
675
676
|
role = message["role"]
content = message["content"]
image_path = message.get("image_path")
tool_calls = message.get("tool_calls", [])
|
01b46131
tangwang
流程跑通
|
677
|
debug_steps = message.get("debug_steps", [])
|
e7f2b240
tangwang
first commit
|
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
|
if role == "user":
st.markdown('<div class="message user-message">', unsafe_allow_html=True)
if image_path and Path(image_path).exists():
try:
img = Image.open(image_path)
st.image(img, width=200)
except Exception:
logger.warning(f"Failed to load user uploaded image: {image_path}")
st.markdown(content)
st.markdown("</div>", unsafe_allow_html=True)
else: # assistant
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
693
|
# Tool call breadcrumb
|
e7f2b240
tangwang
first commit
|
694
|
if tool_calls:
|
01b46131
tangwang
流程跑通
|
695
|
tool_names = [tc["name"] for tc in tool_calls]
|
e7f2b240
tangwang
first commit
|
696
697
|
st.caption(" → ".join(tool_names))
st.markdown("")
|
01b46131
tangwang
流程跑通
|
698
|
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
699
|
# Debug panel
|
01b46131
tangwang
流程跑通
|
700
701
702
703
704
705
706
707
708
709
710
|
if debug_steps and st.session_state.get("show_debug"):
with st.expander("思考 & 工具调用详细过程", expanded=False):
for idx, step in enumerate(debug_steps, 1):
node = step.get("node", "unknown")
st.markdown(f"**Step {idx} – {node}**")
if node == "agent":
msgs = step.get("messages", [])
if msgs:
st.markdown("**Agent Messages**")
for m in msgs:
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
711
|
st.markdown(f"- `{m.get('type', 'assistant')}`: {m.get('content', '')}")
|
01b46131
tangwang
流程跑通
|
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
|
tcs = step.get("tool_calls", [])
if tcs:
st.markdown("**Planned Tool Calls**")
for j, tc in enumerate(tcs, 1):
st.markdown(f"- **{j}. {tc.get('name')}**")
st.code(tc.get("args", {}), language="json")
elif node == "tools":
results = step.get("results", [])
if results:
st.markdown("**Tool Results**")
for j, r in enumerate(results, 1):
st.markdown(f"- **Result {j}:**")
st.code(r.get("content", ""), language="text")
st.markdown("---")
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
729
|
|
897b5ca9
tangwang
perf: 前端性能优化 + 搜索...
|
730
|
# Render message: expand [SEARCH_RESULTS_REF:ref_id] tokens into product card blocks
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
731
|
session_id = st.session_state.get("session_id", "")
|
621b6925
tangwang
up
|
732
|
render_message_with_refs(
|
7e985858
tangwang
feat(ui): product...
|
733
|
content, session_id, fallback_refs=message.get("search_refs"), msg_index=msg_index
|
621b6925
tangwang
up
|
734
|
)
|
e7f2b240
tangwang
first commit
|
735
736
737
738
|
st.markdown("</div>", unsafe_allow_html=True)
|
897b5ca9
tangwang
perf: 前端性能优化 + 搜索...
|
739
|
@st.fragment
|
1a7debb3
tangwang
引用商品进行追问
|
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
|
def render_bottom_actions_bar() -> None:
"""Show Ask and Compare when there are selected products. Disabled when none selected."""
selected = st.session_state.selected_products
n = len(selected)
if n == 0:
return
st.markdown(
'<div class="bottom-actions-bar">',
unsafe_allow_html=True,
)
col_sel, col_ask, col_cmp = st.columns([2, 1, 1])
with col_sel:
st.caption(f"Selected: {n}")
with col_ask:
ask_clicked = st.button("Ask", key="bottom_ask", help="Continue conversation with selected products")
with col_cmp:
compare_clicked = st.button("Compare", key="bottom_compare", help="Compare selected products")
st.markdown("</div>", unsafe_allow_html=True)
if ask_clicked:
st.session_state.referenced_products = list(selected.values())
st.rerun()
if compare_clicked:
st.session_state.side_panel = {
"visible": True,
"mode": "compare",
"payload": list(selected.values()),
}
st.rerun()
def render_side_drawer() -> None:
"""Render a fixed overlay side drawer that does not change background layout."""
panel = st.session_state.side_panel
if not panel.get("visible") or not panel.get("mode"):
return
mode = panel["mode"]
payload = panel.get("payload") or {}
session_id = st.session_state.get("session_id", "")
title = "Similar products" if mode == "similar" else "Compare"
body_html = ""
if mode == "similar":
|
897b5ca9
tangwang
perf: 前端性能优化 + 搜索...
|
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
|
query = html.escape((payload.get("query") or ""))
if payload.get("loading"):
body_html = '<p style="color:#666;">加载中…</p>'
elif payload.get("products") is not None:
to_show = payload["products"][:12]
cards = []
for product in to_show:
p_title = html.escape((product.title or "未知商品")[:80])
price = (
f"¥{product.price:.2f}"
if product.price is not None
else "价格待更新"
)
image_html = (
f'<img src="{html.escape(product.image_url)}" alt="{p_title}" '
'style="width:64px;height:64px;object-fit:cover;border-radius:8px;border:1px solid #eee;" />'
if product.image_url
else '<div style="width:64px;height:64px;background:#f5f5f5;border-radius:8px;'
'display:flex;align-items:center;justify-content:center;color:#bbb;">🛍️</div>'
)
cards.append(
'<div style="display:flex;gap:10px;border:1px solid #eee;border-radius:10px;'
'padding:10px;background:#fff;">'
f"{image_html}"
'<div style="flex:1;min-width:0;">'
f'<div style="font-weight:600;color:#111;line-height:1.35;">{p_title}</div>'
f'<div style="font-size:0.9rem;color:#555;margin-top:4px;">{price}</div>'
"</div></div>"
)
cards_html = "".join(cards) if cards else '<p style="color:#666;">(未找到可展示的商品)</p>'
body_html = (
f'<div style="font-size:0.92rem;color:#555;margin-bottom:10px;">'
f'基于「{query}」的搜索结果:</div>'
'<div style="display:grid;gap:10px;">' + cards_html + "</div>"
)
|
1a7debb3
tangwang
引用商品进行追问
|
820
|
else:
|
897b5ca9
tangwang
perf: 前端性能优化 + 搜索...
|
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
|
# Legacy: ref_id from registry (e.g. from chat)
ref_id = payload.get("ref_id")
if ref_id:
result = global_registry.get(session_id, ref_id)
if result:
to_show = (result.products or [])[:12]
cards = []
for product in to_show:
p_title = html.escape((product.title or "未知商品")[:80])
price = f"¥{product.price:.2f}" if product.price is not None else "价格待更新"
image_html = (
f'<img src="{html.escape(product.image_url)}" alt="{p_title}" '
'style="width:64px;height:64px;object-fit:cover;border-radius:8px;border:1px solid #eee;" />'
if product.image_url
else '<div style="width:64px;height:64px;background:#f5f5f5;border-radius:8px;display:flex;align-items:center;justify-content:center;color:#bbb;">🛍️</div>'
)
cards.append(
'<div style="display:flex;gap:10px;border:1px solid #eee;border-radius:10px;padding:10px;background:#fff;">'
f"{image_html}"
f'<div style="flex:1;min-width:0;"><div style="font-weight:600;color:#111;">{p_title}</div>'
f'<div style="font-size:0.9rem;color:#555;">{price}</div></div></div>'
)
body_html = (
f'<div style="font-size:0.92rem;color:#555;margin-bottom:10px;">基于「{query}」的搜索结果:</div>'
'<div style="display:grid;gap:10px;">' + "".join(cards) + "</div>"
|
1a7debb3
tangwang
引用商品进行追问
|
846
|
)
|
897b5ca9
tangwang
perf: 前端性能优化 + 搜索...
|
847
848
849
850
|
else:
body_html = f'<p style="color:#666;">[搜索结果 {html.escape(ref_id)} 不可用]</p>'
else:
body_html = '<p style="color:#666;">搜索失败或暂无结果。</p>'
|
1a7debb3
tangwang
引用商品进行追问
|
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
|
else:
items = payload if isinstance(payload, list) else []
if items:
rows = []
for item in items:
t = html.escape((item.get("title") or "未知商品")[:80])
p = item.get("price")
ptext = f"¥{p:.2f}" if p is not None else "价格待更新"
rows.append(
'<div style="border:1px solid #eee;border-radius:10px;padding:10px;background:#fff;">'
f'<div style="font-weight:600;color:#111;">{t}</div>'
f'<div style="font-size:0.9rem;color:#555;margin-top:4px;">{ptext}</div>'
"</div>"
)
items_html = "".join(rows)
else:
items_html = '<p style="color:#666;">当前未选中商品。</p>'
body_html = (
'<div style="margin-bottom:10px;color:#555;">已选商品:</div>'
f'<div style="display:grid;gap:10px;">{items_html}</div>'
'<div style="margin-top:14px;padding:10px 12px;border-radius:8px;'
'background:#fff3cd;color:#856404;">对比功能暂未实现。</div>'
)
st.markdown(
f"""
|
7e985858
tangwang
feat(ui): product...
|
877
878
|
<div class="side-drawer-backdrop" id="side-drawer-backdrop"></div>
<div class="side-drawer-panel open" id="side-drawer-panel">
|
1a7debb3
tangwang
引用商品进行追问
|
879
880
|
<div class="side-drawer-header">
<div style="font-weight:600;">{html.escape(title)}</div>
|
7e985858
tangwang
feat(ui): product...
|
881
|
<button id="side-drawer-close-btn">✕ 关闭</button>
|
1a7debb3
tangwang
引用商品进行追问
|
882
883
884
885
886
887
888
889
890
|
</div>
<div class="side-drawer-content">
{body_html}
</div>
</div>
""",
unsafe_allow_html=True,
)
|
7e985858
tangwang
feat(ui): product...
|
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
|
st_components.html("""
<script>
(function() {
var pd = window.parent.document;
var btn = pd.getElementById('side-drawer-close-btn');
var backdrop = pd.getElementById('side-drawer-backdrop');
var panel = pd.getElementById('side-drawer-panel');
function closeDrawer() {
if (backdrop) backdrop.style.display = 'none';
if (panel) panel.style.display = 'none';
var url = new URL(window.parent.location);
url.searchParams.set('close_side_panel', '1');
window.parent.history.replaceState({}, '', url);
}
if (btn) btn.onclick = closeDrawer;
if (backdrop) backdrop.onclick = closeDrawer;
})();
</script>
""", height=0)
|
1a7debb3
tangwang
引用商品进行追问
|
911
|
|
e7f2b240
tangwang
first commit
|
912
913
|
def display_welcome():
"""Display welcome screen"""
|
e7f2b240
tangwang
first commit
|
914
|
col1, col2, col3, col4 = st.columns(4)
|
e7f2b240
tangwang
first commit
|
915
916
917
918
|
with col1:
st.markdown(
"""
<div class="feature-card">
|
01b46131
tangwang
流程跑通
|
919
920
921
|
<div class="feature-icon">💗</div>
<div class="feature-title">懂你</div>
<div>能记住你的偏好,给你推荐适合的</div>
|
e7f2b240
tangwang
first commit
|
922
923
924
925
926
927
928
929
930
|
</div>
""",
unsafe_allow_html=True,
)
with col2:
st.markdown(
"""
<div class="feature-card">
|
01b46131
tangwang
流程跑通
|
931
932
933
|
<div class="feature-icon">🛍️</div>
<div class="feature-title">懂商品</div>
<div>深度理解店铺内所有商品,智能匹配你的需求</div>
|
e7f2b240
tangwang
first commit
|
934
935
936
937
938
939
940
941
942
|
</div>
""",
unsafe_allow_html=True,
)
with col3:
st.markdown(
"""
<div class="feature-card">
|
01b46131
tangwang
流程跑通
|
943
944
945
|
<div class="feature-icon">💭</div>
<div class="feature-title">贴心</div>
<div>任意聊</div>
|
e7f2b240
tangwang
first commit
|
946
947
948
949
950
951
952
953
954
|
</div>
""",
unsafe_allow_html=True,
)
with col4:
st.markdown(
"""
<div class="feature-card">
|
01b46131
tangwang
流程跑通
|
955
956
957
|
<div class="feature-icon">👗</div>
<div class="feature-title">懂时尚</div>
<div>穿搭顾问 + 轻松对比</div>
|
e7f2b240
tangwang
first commit
|
958
959
960
961
962
963
964
|
</div>
""",
unsafe_allow_html=True,
)
st.markdown("<br><br>", unsafe_allow_html=True)
|
e7f2b240
tangwang
first commit
|
965
966
967
|
def main():
"""Main Streamlit app"""
initialize_session()
|
7e985858
tangwang
feat(ui): product...
|
968
|
# Sync drawer close state from JS (set by JS via history.replaceState, no reload)
|
1a7debb3
tangwang
引用商品进行追问
|
969
970
|
if st.query_params.get("close_side_panel"):
st.session_state.side_panel = {"visible": False, "mode": None, "payload": None}
|
7e985858
tangwang
feat(ui): product...
|
971
|
st.query_params.clear()
|
1a7debb3
tangwang
引用商品进行追问
|
972
|
|
897b5ca9
tangwang
perf: 前端性能优化 + 搜索...
|
973
974
975
976
977
978
979
980
981
982
983
984
985
986
|
# "Similar" panel: if loading, run API-only search and rerun
panel = st.session_state.side_panel
if panel.get("visible") and panel.get("mode") == "similar":
payload = panel.get("payload") or {}
if payload.get("loading") and payload.get("query"):
from app.tools.search_tools import search_products_api_only
products = search_products_api_only(payload["query"], limit=12)
st.session_state.side_panel["payload"] = {
"query": payload["query"],
"products": products,
"loading": False,
}
st.rerun()
|
7e985858
tangwang
feat(ui): product...
|
987
|
# Drawer rendered early so fixed positioning works from top of DOM
|
1a7debb3
tangwang
引用商品进行追问
|
988
|
render_side_drawer()
|
e7f2b240
tangwang
first commit
|
989
990
991
992
993
|
# Header
st.markdown(
"""
<div class="app-header">
|
bad17b15
tangwang
调通baseline
|
994
|
<div class="app-title">👗 ShopAgent</div>
|
e7f2b240
tangwang
first commit
|
995
996
997
998
999
1000
1001
|
<div class="app-subtitle">AI Fashion Shopping Assistant</div>
</div>
""",
unsafe_allow_html=True,
)
# Sidebar (collapsed by default, but accessible)
|
897b5ca9
tangwang
perf: 前端性能优化 + 搜索...
|
1002
1003
|
@st.fragment
def _sidebar_fragment():
|
e7f2b240
tangwang
first commit
|
1004
|
st.markdown("### ⚙️ Settings")
|
825828c4
tangwang
fix: search image...
|
1005
|
if st.button("🗑️ Clear Chat", width="stretch"):
|
e7f2b240
tangwang
first commit
|
1006
1007
|
if "shopping_agent" in st.session_state:
st.session_state.shopping_agent.clear_history()
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
1008
1009
1010
|
session_id = st.session_state.get("session_id", "")
if session_id:
global_registry.clear_session(session_id)
|
e7f2b240
tangwang
first commit
|
1011
1012
|
st.session_state.messages = []
st.session_state.uploaded_image = None
|
1a7debb3
tangwang
引用商品进行追问
|
1013
1014
1015
|
st.session_state.selected_products = {}
st.session_state.referenced_products = []
st.session_state.side_panel = {"visible": False, "mode": None, "payload": None}
|
e7f2b240
tangwang
first commit
|
1016
|
st.rerun()
|
01b46131
tangwang
流程跑通
|
1017
1018
1019
1020
|
st.markdown("---")
st.checkbox(
"显示调试过程 (debug)",
key="show_debug",
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
1021
|
value=True,
|
01b46131
tangwang
流程跑通
|
1022
1023
|
help="展开后可查看中间思考过程及工具调用详情",
)
|
e7f2b240
tangwang
first commit
|
1024
1025
1026
|
st.markdown("---")
st.caption(f"Session: `{st.session_state.session_id[:8]}...`")
|
897b5ca9
tangwang
perf: 前端性能优化 + 搜索...
|
1027
1028
1029
1030
|
with st.sidebar:
_sidebar_fragment()
MAX_MESSAGES = 50
|
e7f2b240
tangwang
first commit
|
1031
|
messages_container = st.container()
|
e7f2b240
tangwang
first commit
|
1032
1033
1034
1035
|
with messages_container:
if not st.session_state.messages:
display_welcome()
else:
|
897b5ca9
tangwang
perf: 前端性能优化 + 搜索...
|
1036
1037
1038
1039
1040
1041
1042
|
messages = st.session_state.messages
start_idx = max(0, len(messages) - MAX_MESSAGES)
to_show = messages[start_idx:]
if len(messages) > MAX_MESSAGES:
st.caption(f"(仅显示最近 {MAX_MESSAGES} 条,共 {len(messages)} 条消息)")
for i, message in enumerate(to_show):
display_message(message, msg_index=start_idx + i)
|
1a7debb3
tangwang
引用商品进行追问
|
1043
|
render_bottom_actions_bar()
|
e7f2b240
tangwang
first commit
|
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
|
# Fixed input area at bottom (using container to simulate fixed position)
st.markdown('<div class="fixed-input-container">', unsafe_allow_html=True)
input_container = st.container()
with input_container:
# Image upload area (shown when + is clicked)
if st.session_state.show_image_upload:
uploaded_file = st.file_uploader(
"Choose an image",
type=["jpg", "jpeg", "png"],
key="file_uploader",
)
if uploaded_file:
st.session_state.uploaded_image = uploaded_file
# Show preview
col1, col2 = st.columns([1, 4])
with col1:
img = Image.open(uploaded_file)
st.image(img, width=100)
with col2:
if st.button("❌ Remove"):
st.session_state.uploaded_image = None
st.session_state.show_image_upload = False
st.rerun()
|
1a7debb3
tangwang
引用商品进行追问
|
1072
1073
1074
|
# Referenced products area (shown above chat input, each can be removed)
render_referenced_products_in_input()
|
e7f2b240
tangwang
first commit
|
1075
1076
1077
1078
1079
|
# Input row
col1, col2 = st.columns([1, 12])
with col1:
# Image upload toggle button
|
825828c4
tangwang
fix: search image...
|
1080
|
if st.button("➕", help="Add image", width="stretch"):
|
e7f2b240
tangwang
first commit
|
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
|
st.session_state.show_image_upload = (
not st.session_state.show_image_upload
)
st.rerun()
with col2:
# Text input
user_query = st.chat_input(
"Ask about fashion products...",
key="chat_input",
)
st.markdown("</div>", unsafe_allow_html=True)
# Process user input
if user_query:
|
1a7debb3
tangwang
引用商品进行追问
|
1097
1098
1099
1100
1101
1102
|
raw_user_query = user_query
referenced_products = list(st.session_state.get("referenced_products", []))
agent_query = raw_user_query
if referenced_products:
agent_query = f"{_build_reference_prefix(referenced_products)}\n\n{raw_user_query}"
|
e7f2b240
tangwang
first commit
|
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
|
# Ensure shopping agent is initialized
if "shopping_agent" not in st.session_state:
st.error("Session not initialized. Please refresh the page.")
st.stop()
# Save uploaded image if present, or get from recent history
image_path = None
if st.session_state.uploaded_image:
# User explicitly uploaded an image for this query
image_path = save_uploaded_image(st.session_state.uploaded_image)
else:
# Check if query refers to a previous image
|
e7f2b240
tangwang
first commit
|
1115
|
if any(
|
1a7debb3
tangwang
引用商品进行追问
|
1116
|
ref in raw_user_query.lower()
|
e7f2b240
tangwang
first commit
|
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
|
for ref in [
"this",
"that",
"the image",
"the shirt",
"the product",
"it",
]
):
# Find the most recent message with an image
for msg in reversed(st.session_state.messages):
if msg.get("role") == "user" and msg.get("image_path"):
image_path = msg["image_path"]
logger.info(f"Using image from previous message: {image_path}")
break
# Add user message
st.session_state.messages.append(
{
"role": "user",
|
1a7debb3
tangwang
引用商品进行追问
|
1137
|
"content": raw_user_query,
|
e7f2b240
tangwang
first commit
|
1138
1139
1140
1141
|
"image_path": image_path,
}
)
|
1a7debb3
tangwang
引用商品进行追问
|
1142
1143
1144
|
# References are consumed once this message is sent
st.session_state.referenced_products = []
|
e7f2b240
tangwang
first commit
|
1145
1146
1147
1148
1149
1150
1151
|
# Display user message immediately
with messages_container:
display_message(st.session_state.messages[-1])
# Process with shopping agent
try:
shopping_agent = st.session_state.shopping_agent
|
1a7debb3
tangwang
引用商品进行追问
|
1152
|
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
1153
1154
|
# Process with agent
result = shopping_agent.chat(
|
1a7debb3
tangwang
引用商品进行追问
|
1155
|
query=agent_query,
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
1156
1157
1158
1159
1160
|
image_path=image_path,
)
response = result["response"]
tool_calls = result.get("tool_calls", [])
debug_steps = result.get("debug_steps", [])
|
e7f2b240
tangwang
first commit
|
1161
|
|
621b6925
tangwang
up
|
1162
|
# Add assistant message (store search_refs so refs resolve after rerun)
|
e7f2b240
tangwang
first commit
|
1163
1164
1165
1166
1167
|
st.session_state.messages.append(
{
"role": "assistant",
"content": response,
"tool_calls": tool_calls,
|
66442668
tangwang
feat: 搜索结果引用与并行搜索...
|
1168
|
"debug_steps": debug_steps,
|
621b6925
tangwang
up
|
1169
|
"search_refs": result.get("search_refs", {}),
|
e7f2b240
tangwang
first commit
|
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
|
}
)
# Clear uploaded image and hide upload area after sending
st.session_state.uploaded_image = None
st.session_state.show_image_upload = False
# Auto-scroll to bottom with JavaScript
st.markdown(
"""
<script>
window.scrollTo(0, document.body.scrollHeight);
</script>
""",
unsafe_allow_html=True,
)
except Exception as e:
logger.error(f"Error processing query: {e}", exc_info=True)
error_msg = f"I apologize, I encountered an error: {str(e)}"
st.session_state.messages.append(
{
"role": "assistant",
"content": error_msg,
}
)
# Rerun to update UI
st.rerun()
if __name__ == "__main__":
main()
|