File size: 5,957 Bytes
a48e2a2
eedb2c4
 
a48e2a2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eedb2c4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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
# 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", [])