Spaces:
Sleeping
Sleeping
| # core/storefront.py | |
| import json, os | |
| def clean_generation(text: str) -> str: | |
| s = (text or "").strip() | |
| # Keep only text after the last "Assistant:" | |
| last = s.rfind("Assistant:") | |
| if last != -1: | |
| s = s[last + len("Assistant:"):].strip() | |
| # Cut at the first sign of a new turn or meta | |
| cut_marks = ["\nUser:", "\nSystem:", "\n###", "\nProducts:", "\nVenue rules:", "\nParking rules:"] | |
| cuts = [s.find(m) for m in cut_marks if s.find(m) != -1] | |
| if cuts: | |
| s = s[:min(cuts)].strip() | |
| # Remove egregious token loops like "Account/Account/..." | |
| s = re.sub(r"(?:\b([A-Z][a-zA-Z0-9_/.-]{2,})\b(?:\s*/\s*\1\b)+)", r"\1", s) | |
| # Collapse consecutive duplicate lines | |
| dedup = [] | |
| for ln in s.splitlines(): | |
| if not dedup or ln.strip() != dedup[-1].strip(): | |
| dedup.append(ln) | |
| return "\n".join(dedup).strip() | |
| HELP_KEYWORDS = { | |
| "help", "assist", "assistance", "tips", "how do i", "what can you do", | |
| "graduation help", "help me with graduation", "can you help me with graduation" | |
| } | |
| STORE_KEYWORDS = { | |
| "cap", "gown", "parking", "pass", "passes", "attire", "dress", | |
| "venue", "logistics", "shipping", "pickup", "lot", "lots", "arrival", "size", "sizing" | |
| } | |
| def is_storefront_query(text: str) -> bool: | |
| t = (text or "").lower() | |
| return any(k in t for k in STORE_KEYWORDS) or any(k in t for k in HELP_KEYWORDS) | |
| def _get_lots_open_hours(data) -> int: | |
| try: | |
| return int(((data or {}).get("logistics") or {}).get("lots_open_hours_before") or 2) | |
| except Exception: | |
| return 2 | |
| # Main router (drop-in) | |
| def storefront_qna(data, user_text: str) -> str | None: | |
| """ | |
| Deterministic storefront answers first: | |
| - single-word intents (parking / wear / passes) | |
| - help/capability prompt | |
| - FAQ (if you have answer_faq) | |
| - explicit rules queries | |
| - 'lots open' timing | |
| - compact products list | |
| Returns None to allow LLM fallback in your chat pipeline. | |
| """ | |
| if not user_text: | |
| return None | |
| t = user_text.strip().lower() | |
| # 1) Single-word / exact intents to avoid LLM hallucinations | |
| if t in {"parking"}: | |
| _, pr = get_rules(data) | |
| if pr: | |
| return "Parking rules:\n- " + "\n- ".join(pr) | |
| # Map 'wear/attire' variants directly to venue rules | |
| if t in {"venue", "attire", "dress", "dress code", "wear"} or "what should i wear" in t: | |
| vr, _ = get_rules(data) | |
| if vr: | |
| return "Venue rules:\n- " + "\n- ".join(vr) | |
| # Parking passes (multiple allowed) | |
| if t in {"passes", "parking pass", "parking passes"}: | |
| return "Yes, multiple parking passes are allowed per student." | |
| # 2) Help / capability intent → deterministic guidance | |
| if any(k in t for k in HELP_KEYWORDS): | |
| return ( | |
| "I can help with the graduation storefront. Try:\n" | |
| "- “What are the parking rules?”\n" | |
| "- “Can I buy multiple parking passes?”\n" | |
| "- “Is formal attire required?”\n" | |
| "- “Where do I pick up the gown?”\n" | |
| "- “When do lots open?”" | |
| ) | |
| # 3) JSON-driven FAQ (if available) | |
| try: | |
| a = answer_faq(data, t) | |
| if a: | |
| return a | |
| except Exception: | |
| pass # answer_faq may not exist or data may be None | |
| # 4) Explicit rules phrasing (keeps answers tight and consistent) | |
| if "parking" in t and "rule" in t: | |
| _, pr = get_rules(data) | |
| if pr: | |
| return "Parking rules:\n- " + "\n- ".join(pr) | |
| if ("venue" in t and "rule" in t) or "attire" in t or "dress code" in t: | |
| vr, _ = get_rules(data) | |
| if vr: | |
| return "Venue rules:\n- " + "\n- ".join(vr) | |
| # 5) “When do lots open?” / hours / time | |
| if "parking" in t and ("hours" in t or "time" in t or "open" in t): | |
| lots_open = _get_lots_open_hours(data) | |
| return f"Parking lots open {lots_open} hours before the ceremony." | |
| # 6) Product info (cap/gown/parking pass) | |
| if any(k in t for k in ("cap", "gown", "parking pass", "product", "item", "price")): | |
| prods = extract_products(data) | |
| if prods: | |
| lines = [] | |
| for p in prods: | |
| name = p.get("name", "Item") | |
| price = p.get("price", p.get("price_usd", "")) | |
| notes = p.get("notes", p.get("description", "")) | |
| price_str = f"${price:.2f}" if isinstance(price, (int, float)) else str(price) | |
| lines.append(f"{name} — {price_str}: {notes}") | |
| return "\n".join(lines) | |
| # No deterministic match → let the caller fall back to the LLM | |
| return None | |
| def _find_json(): | |
| candidates = [ | |
| os.path.join(os.getcwd(), "storefront_data.json"), | |
| os.path.join(os.getcwd(), "agenticcore", "storefront_data.json"), | |
| ] | |
| for p in candidates: | |
| if os.path.exists(p): | |
| return p | |
| return None | |
| def load_storefront(): | |
| p = _find_json() | |
| if not p: | |
| return None | |
| with open(p, "r", encoding="utf-8") as f: | |
| return json.load(f) | |
| def _string_in_any(s, variants): | |
| s = s.lower() | |
| return any(v in s for v in variants) | |
| def answer_faq(data, text: str): | |
| """Very small FAQ matcher by substring; safe if faq[] missing.""" | |
| faq = (data or {}).get("faq") or [] | |
| t = text.lower() | |
| for item in faq: | |
| qs = item.get("q") or [] | |
| if any(q.lower() in t for q in qs): | |
| return item.get("a") | |
| return None | |
| def extract_products(data): | |
| prods = [] | |
| for p in (data or {}).get("products", []): | |
| prods.append({ | |
| "sku": p.get("sku",""), | |
| "name": p.get("name",""), | |
| "price": p.get("price_usd",""), | |
| "notes": (p.get("description") or "")[:140], | |
| }) | |
| return prods | |
| def get_rules(data): | |
| pol = (data or {}).get("policies", {}) or {} | |
| return pol.get("venue_rules", []), pol.get("parking_rules", []) | |