JerameeUC commited on
Commit
cd7926f
·
1 Parent(s): 57d75c5

2nd missed some stuff

Browse files
app_storefront.py ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app_storefront.py
2
+ import os
3
+ import sys
4
+ import gradio as gr
5
+
6
+ # Ensure "core/" is importable
7
+ sys.path.append(os.path.join(os.path.dirname(__file__), "core"))
8
+
9
+ # Import only functions; core.storefront doesn't export constants
10
+ from core.model import model_generate, MODEL_NAME
11
+ from core.memory import build_prompt_from_history
12
+ from core.storefront import load_storefront, storefront_qna, extract_products, get_rules
13
+ from core.storefront import is_storefront_query
14
+
15
+ def chat_pipeline(history, message, max_new_tokens=96, temperature=0.7, top_p=0.9):
16
+ # 1) Try storefront facts first
17
+ sf = storefront_qna(DATA, message)
18
+ if sf:
19
+ return sf
20
+
21
+ # 2) If not a storefront query, offer guided help (no LLM)
22
+ if not is_storefront_query(message):
23
+ return (
24
+ "I can help with the graduation storefront. Examples:\n"
25
+ "- Parking rules, lots opening times\n"
26
+ "- Attire / dress code\n"
27
+ "- Cap & Gown details and pickup\n"
28
+ "- Parking passes (multiple allowed)\n"
29
+ "Ask one of those, and I’ll answer directly."
30
+ )
31
+
32
+ # 3) Otherwise, generate with memory and hard stops
33
+ prompt = build_prompt_from_history(history, message, k=4)
34
+ gen = model_generate(prompt, max_new_tokens, temperature, top_p)
35
+ return clean_generation(gen)
36
+
37
+ def clean_generation(text: str) -> str:
38
+ return (text or "").strip()
39
+
40
+ # ---------------- Load data + safe fallbacks ----------------
41
+ DATA = load_storefront() # may be None if storefront_data.json missing/empty
42
+
43
+ # Fallbacks used if JSON not present
44
+ FALLBACK_PRODUCTS = [
45
+ {"sku": "CG-SET", "name": "Cap & Gown Set", "price": 59.00,
46
+ "notes": "Tassel included; ships until 10 days before the event"},
47
+ {"sku": "PK-1", "name": "Parking Pass", "price": 10.00,
48
+ "notes": "Multiple passes are allowed per student"}
49
+ ]
50
+ FALLBACK_VENUE = [
51
+ "Formal attire recommended (not required).",
52
+ "No muscle shirts.",
53
+ "No sagging pants."
54
+ ]
55
+ FALLBACK_PARKING = [
56
+ "No double parking.",
57
+ "Vehicles parked in handicap spaces will be towed."
58
+ ]
59
+
60
+ # Normalize products/rules for the tabs
61
+ if DATA:
62
+ PRODUCTS = extract_products(DATA) or FALLBACK_PRODUCTS
63
+ venue_rules, parking_rules = get_rules(DATA)
64
+ VENUE_RULES = venue_rules or FALLBACK_VENUE
65
+ PARKING_RULES = parking_rules or FALLBACK_PARKING
66
+ else:
67
+ PRODUCTS = FALLBACK_PRODUCTS
68
+ VENUE_RULES = FALLBACK_VENUE
69
+ PARKING_RULES = FALLBACK_PARKING
70
+
71
+
72
+
73
+ # ---------------- UI ----------------
74
+ CSS = """
75
+ :root { --bg:#0b0d12; --panel:#0f172a; --border:#1f2940; --text:#e5e7eb; --muted:#9ca3af; }
76
+ .gradio-container { background: var(--bg) !important; color: var(--text) !important; }
77
+ .panel { border:1px solid var(--border); border-radius:16px; background:var(--panel); }
78
+ .small { font-size:12px; color: var(--muted); }
79
+ """
80
+
81
+ with gr.Blocks(title="Storefront Chat", css=CSS) as demo:
82
+ gr.Markdown("## Storefront Chat")
83
+
84
+ # Single history state (kept in sync with Chatbot)
85
+ history_state = gr.State([])
86
+
87
+ with gr.Tabs():
88
+ # --- TAB: Chat ---
89
+ with gr.TabItem("Chat"):
90
+ with gr.Group(elem_classes=["panel"]):
91
+ chat = gr.Chatbot(height=360, bubble_full_width=False, label="Chat")
92
+
93
+ with gr.Row():
94
+ msg = gr.Textbox(placeholder="Ask about parking rules, attire, cap & gown, pickup times…", scale=5)
95
+ send = gr.Button("Send", scale=1)
96
+
97
+ # Quick chips
98
+ with gr.Row():
99
+ chip1 = gr.Button("Parking rules", variant="secondary")
100
+ chip2 = gr.Button("Multiple passes", variant="secondary")
101
+ chip3 = gr.Button("Attire", variant="secondary")
102
+ chip4 = gr.Button("When do lots open?", variant="secondary")
103
+
104
+ # Advanced options (sliders + Health/Capabilities)
105
+ with gr.Accordion("Advanced chat options", open=False):
106
+ max_new = gr.Slider(32, 512, 128, 1, label="Max new tokens")
107
+ temp = gr.Slider(0.1, 1.5, 0.8, 0.05, label="Temperature")
108
+ topp = gr.Slider(0.1, 1.0, 0.95, 0.05, label="Top-p")
109
+
110
+ with gr.Row():
111
+ health_btn = gr.Button("Health", variant="secondary")
112
+ caps_btn = gr.Button("Capabilities", variant="secondary")
113
+ status_md = gr.Markdown("Status: not checked", elem_classes=["small"])
114
+
115
+ # --- TAB: Products ---
116
+ with gr.TabItem("Products"):
117
+ gr.Markdown("### Available Items")
118
+ cols = ["sku", "name", "price", "notes"]
119
+ data = [[p.get(c, "") for c in cols] for p in PRODUCTS]
120
+ gr.Dataframe(headers=[c.upper() for c in cols], value=data, interactive=False, wrap=True, label="Products")
121
+
122
+ # --- TAB: Rules ---
123
+ with gr.TabItem("Rules"):
124
+ gr.Markdown("### Venue rules")
125
+ gr.Markdown("- " + "\n- ".join(VENUE_RULES))
126
+ gr.Markdown("### Parking rules")
127
+ gr.Markdown("- " + "\n- ".join(PARKING_RULES))
128
+
129
+ # --- TAB: Logistics ---
130
+ with gr.TabItem("Logistics"):
131
+ gr.Markdown(
132
+ "### Event Logistics\n"
133
+ "- Shipping available until 10 days before event (typ. 3–5 business days)\n"
134
+ "- Pickup: Student Center Bookstore during the week prior to event\n"
135
+ "- Graduates arrive 90 minutes early; guests 60 minutes early\n"
136
+ "- Lots A & B open 2 hours before; overflow as needed\n"
137
+ "\n*Try asking the bot:* “What time should I arrive?” • “Where do I pick up the gown?”"
138
+ )
139
+
140
+ # ---------- Helpers ----------
141
+ def _append_bot_md(history, md_text):
142
+ history = history or []
143
+ return history + [[None, md_text]]
144
+
145
+ # ---------- Callbacks ----------
146
+ def on_send(history, message, max_new_tokens, temperature, top_p):
147
+ t = (message or "").strip()
148
+ if not t:
149
+ return history, history, "" # no-op; shapes must match
150
+ history = (history or []) + [[t, None]]
151
+ reply = chat_pipeline(history[:-1], t, max_new_tokens, temperature, top_p)
152
+ history[-1][1] = reply
153
+ return history, history, ""
154
+
155
+ def _health_cb(history):
156
+ md = (
157
+ f"### Status: ✅ Healthy\n"
158
+ f"- Model: `{MODEL_NAME}`\n"
159
+ f"- Storefront JSON: {'loaded' if bool(DATA) else 'not found'}"
160
+ )
161
+ new_hist = _append_bot_md(history, md)
162
+ return new_hist, new_hist, "Status: ✅ Healthy"
163
+
164
+ def _caps_cb(history):
165
+ md = (
166
+ "### Capabilities\n"
167
+ "- Chat (LLM text-generation, memory-aware prompt)\n"
168
+ "- Storefront Q&A (parking, attire, products, logistics)\n"
169
+ "- Adjustable: max_new_tokens, temperature, top-p"
170
+ )
171
+ new_hist = _append_bot_md(history, md)
172
+ return new_hist, new_hist
173
+
174
+ # Wire up (state + chatbot)
175
+ send.click(on_send, [history_state, msg, max_new, temp, topp], [history_state, chat, msg])
176
+ msg.submit(on_send, [history_state, msg, max_new, temp, topp], [history_state, chat, msg])
177
+
178
+ # Chips → prefill textbox
179
+ chip1.click(lambda: "What are the parking rules?", outputs=msg)
180
+ chip2.click(lambda: "Can I buy multiple parking passes?", outputs=msg)
181
+ chip3.click(lambda: "Is formal attire required?", outputs=msg)
182
+ chip4.click(lambda: "What time do the parking lots open?", outputs=msg)
183
+
184
+ # Health / Capabilities live inside Advanced
185
+ health_btn.click(_health_cb, inputs=[history_state], outputs=[history_state, chat, status_md])
186
+ caps_btn.click(_caps_cb, inputs=[history_state], outputs=[history_state, chat])
187
+
188
+ def clean_generation(text: str) -> str:
189
+ s = (text or "").strip()
190
+
191
+ # If the prompt contained "Assistant:", keep only what comes after the last one
192
+ last = s.rfind("Assistant:")
193
+ if last != -1:
194
+ s = s[last + len("Assistant:"):].strip()
195
+
196
+ # If it accidentally continued into a new "User:" or instructions, cut there
197
+ cut_marks = ["\nUser:", "\nYOU ARE ANSWERING", "\nProducts:", "\nVenue rules:", "\nParking rules:"]
198
+ cut_positions = [s.find(m) for m in cut_marks if s.find(m) != -1]
199
+ if cut_positions:
200
+ s = s[:min(cut_positions)].strip()
201
+
202
+ # Collapse repeated lines like "Yes, multiple parking passes..." spam
203
+ lines, out = s.splitlines(), []
204
+ seen = set()
205
+ for ln in lines:
206
+ # dedupe only exact consecutive repeats; keep normal conversation lines
207
+ if not out or ln != out[-1]:
208
+ out.append(ln)
209
+ return "\n".join(out).strip()
210
+
211
+ if __name__ == "__main__":
212
+ demo.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", "7860")))
core/memory.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # core/memory.py
2
+
3
+ META_MARKERS = ("### Status:", "### Capabilities", "Status:", "Capabilities", "Model:", "Storefront JSON:")
4
+
5
+ def _is_meta(s: str | None) -> bool:
6
+ if not s: return False
7
+ ss = s.strip()
8
+ return any(m in ss for m in META_MARKERS)
9
+
10
+ def build_prompt_from_history(history, user_text, k=4) -> str:
11
+ """
12
+ history: list[[user, bot], ...] from Gradio Chatbot.
13
+ Keep prompt compact; exclude meta/diagnostic messages.
14
+ """
15
+ lines = [
16
+ "System: Answer questions about the university graduation storefront.",
17
+ "System: Be concise. If unsure, state what is known."
18
+ ]
19
+
20
+ # Keep only the last k turns that aren't meta
21
+ kept = []
22
+ for u, b in (history or []):
23
+ if u and not _is_meta(u):
24
+ kept.append(("User", u))
25
+ if b and not _is_meta(b):
26
+ kept.append(("Assistant", b))
27
+ kept = kept[-(2*k):] # up to k exchanges
28
+
29
+ for role, text in kept:
30
+ lines.append(f"{role}: {text}")
31
+
32
+ lines.append(f"User: {user_text}")
33
+ lines.append("Assistant:")
34
+ return "\n".join(lines)
core/model.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # core/model.py
2
+ import re, os
3
+ from transformers import pipeline, StoppingCriteria, StoppingCriteriaList
4
+
5
+ MODEL_NAME = os.getenv("HF_MODEL_GENERATION", "distilgpt2")
6
+ _pipe = None
7
+
8
+ class StopOnMarkers(StoppingCriteria):
9
+ def __init__(self, tokenizer, stop_strs=("\nUser:", "\nSystem:", "\n###", "\nProducts:", "\nVenue rules:", "\nParking rules:")):
10
+ self.tokenizer = tokenizer
11
+ self.stop_ids = [tokenizer(s, add_special_tokens=False).input_ids for s in stop_strs]
12
+
13
+ def __call__(self, input_ids, scores, **kwargs):
14
+ # stop if any marker sequence just appeared at the end
15
+ for seq in self.stop_ids:
16
+ L = len(seq)
17
+ if L and len(input_ids[0]) >= L and input_ids[0][-L:].tolist() == seq:
18
+ return True
19
+ return False
20
+
21
+ def _get_pipe():
22
+ global _pipe
23
+ if _pipe is None:
24
+ _pipe = pipeline("text-generation", model=MODEL_NAME)
25
+ return _pipe
26
+
27
+ def model_generate(prompt, max_new_tokens=96, temperature=0.7, top_p=0.9):
28
+ pipe = _get_pipe()
29
+ tok = pipe.tokenizer
30
+
31
+ stop = StoppingCriteriaList([StopOnMarkers(tok)])
32
+
33
+ out = pipe(
34
+ prompt,
35
+ max_new_tokens=int(max_new_tokens),
36
+ do_sample=True,
37
+ temperature=float(temperature),
38
+ top_p=float(top_p),
39
+ repetition_penalty=1.15, # discourages exact loops
40
+ no_repeat_ngram_size=3, # blocks short repeats like "Account/Account"
41
+ pad_token_id=tok.eos_token_id or 50256,
42
+ eos_token_id=tok.eos_token_id, # stop at EOS if model supports
43
+ stopping_criteria=stop,
44
+ )
45
+ return out[0]["generated_text"]
core/storefront.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # core/storefront.py
2
+ import json, os
3
+
4
+ def clean_generation(text: str) -> str:
5
+ s = (text or "").strip()
6
+
7
+ # Keep only text after the last "Assistant:"
8
+ last = s.rfind("Assistant:")
9
+ if last != -1:
10
+ s = s[last + len("Assistant:"):].strip()
11
+
12
+ # Cut at the first sign of a new turn or meta
13
+ cut_marks = ["\nUser:", "\nSystem:", "\n###", "\nProducts:", "\nVenue rules:", "\nParking rules:"]
14
+ cuts = [s.find(m) for m in cut_marks if s.find(m) != -1]
15
+ if cuts:
16
+ s = s[:min(cuts)].strip()
17
+
18
+ # Remove egregious token loops like "Account/Account/..."
19
+ s = re.sub(r"(?:\b([A-Z][a-zA-Z0-9_/.-]{2,})\b(?:\s*/\s*\1\b)+)", r"\1", s)
20
+
21
+ # Collapse consecutive duplicate lines
22
+ dedup = []
23
+ for ln in s.splitlines():
24
+ if not dedup or ln.strip() != dedup[-1].strip():
25
+ dedup.append(ln)
26
+ return "\n".join(dedup).strip()
27
+
28
+ HELP_KEYWORDS = {
29
+ "help", "assist", "assistance", "tips", "how do i", "what can you do",
30
+ "graduation help", "help me with graduation", "can you help me with graduation"
31
+ }
32
+
33
+ STORE_KEYWORDS = {
34
+ "cap", "gown", "parking", "pass", "passes", "attire", "dress",
35
+ "venue", "logistics", "shipping", "pickup", "lot", "lots", "arrival", "size", "sizing"
36
+ }
37
+
38
+ def is_storefront_query(text: str) -> bool:
39
+ t = (text or "").lower()
40
+ return any(k in t for k in STORE_KEYWORDS) or any(k in t for k in HELP_KEYWORDS)
41
+
42
+ def _get_lots_open_hours(data) -> int:
43
+ try:
44
+ return int(((data or {}).get("logistics") or {}).get("lots_open_hours_before") or 2)
45
+ except Exception:
46
+ return 2
47
+
48
+ # Main router (drop-in)
49
+ def storefront_qna(data, user_text: str) -> str | None:
50
+ """
51
+ Deterministic storefront answers first:
52
+ - single-word intents (parking / wear / passes)
53
+ - help/capability prompt
54
+ - FAQ (if you have answer_faq)
55
+ - explicit rules queries
56
+ - 'lots open' timing
57
+ - compact products list
58
+ Returns None to allow LLM fallback in your chat pipeline.
59
+ """
60
+ if not user_text:
61
+ return None
62
+ t = user_text.strip().lower()
63
+
64
+ # 1) Single-word / exact intents to avoid LLM hallucinations
65
+ if t in {"parking"}:
66
+ _, pr = get_rules(data)
67
+ if pr:
68
+ return "Parking rules:\n- " + "\n- ".join(pr)
69
+
70
+ # Map 'wear/attire' variants directly to venue rules
71
+ if t in {"venue", "attire", "dress", "dress code", "wear"} or "what should i wear" in t:
72
+ vr, _ = get_rules(data)
73
+ if vr:
74
+ return "Venue rules:\n- " + "\n- ".join(vr)
75
+
76
+ # Parking passes (multiple allowed)
77
+ if t in {"passes", "parking pass", "parking passes"}:
78
+ return "Yes, multiple parking passes are allowed per student."
79
+
80
+ # 2) Help / capability intent → deterministic guidance
81
+ if any(k in t for k in HELP_KEYWORDS):
82
+ return (
83
+ "I can help with the graduation storefront. Try:\n"
84
+ "- “What are the parking rules?”\n"
85
+ "- “Can I buy multiple parking passes?”\n"
86
+ "- “Is formal attire required?”\n"
87
+ "- “Where do I pick up the gown?”\n"
88
+ "- “When do lots open?”"
89
+ )
90
+
91
+ # 3) JSON-driven FAQ (if available)
92
+ try:
93
+ a = answer_faq(data, t)
94
+ if a:
95
+ return a
96
+ except Exception:
97
+ pass # answer_faq may not exist or data may be None
98
+
99
+ # 4) Explicit rules phrasing (keeps answers tight and consistent)
100
+ if "parking" in t and "rule" in t:
101
+ _, pr = get_rules(data)
102
+ if pr:
103
+ return "Parking rules:\n- " + "\n- ".join(pr)
104
+
105
+ if ("venue" in t and "rule" in t) or "attire" in t or "dress code" in t:
106
+ vr, _ = get_rules(data)
107
+ if vr:
108
+ return "Venue rules:\n- " + "\n- ".join(vr)
109
+
110
+ # 5) “When do lots open?” / hours / time
111
+ if "parking" in t and ("hours" in t or "time" in t or "open" in t):
112
+ lots_open = _get_lots_open_hours(data)
113
+ return f"Parking lots open {lots_open} hours before the ceremony."
114
+
115
+ # 6) Product info (cap/gown/parking pass)
116
+ if any(k in t for k in ("cap", "gown", "parking pass", "product", "item", "price")):
117
+ prods = extract_products(data)
118
+ if prods:
119
+ lines = []
120
+ for p in prods:
121
+ name = p.get("name", "Item")
122
+ price = p.get("price", p.get("price_usd", ""))
123
+ notes = p.get("notes", p.get("description", ""))
124
+ price_str = f"${price:.2f}" if isinstance(price, (int, float)) else str(price)
125
+ lines.append(f"{name} — {price_str}: {notes}")
126
+ return "\n".join(lines)
127
+
128
+ # No deterministic match → let the caller fall back to the LLM
129
+ return None
130
+
131
+ def _find_json():
132
+ candidates = [
133
+ os.path.join(os.getcwd(), "storefront_data.json"),
134
+ os.path.join(os.getcwd(), "agenticcore", "storefront_data.json"),
135
+ ]
136
+ for p in candidates:
137
+ if os.path.exists(p):
138
+ return p
139
+ return None
140
+
141
+ def load_storefront():
142
+ p = _find_json()
143
+ if not p:
144
+ return None
145
+ with open(p, "r", encoding="utf-8") as f:
146
+ return json.load(f)
147
+
148
+ def _string_in_any(s, variants):
149
+ s = s.lower()
150
+ return any(v in s for v in variants)
151
+
152
+ def answer_faq(data, text: str):
153
+ """Very small FAQ matcher by substring; safe if faq[] missing."""
154
+ faq = (data or {}).get("faq") or []
155
+ t = text.lower()
156
+ for item in faq:
157
+ qs = item.get("q") or []
158
+ if any(q.lower() in t for q in qs):
159
+ return item.get("a")
160
+ return None
161
+
162
+ def extract_products(data):
163
+ prods = []
164
+ for p in (data or {}).get("products", []):
165
+ prods.append({
166
+ "sku": p.get("sku",""),
167
+ "name": p.get("name",""),
168
+ "price": p.get("price_usd",""),
169
+ "notes": (p.get("description") or "")[:140],
170
+ })
171
+ return prods
172
+
173
+ def get_rules(data):
174
+ pol = (data or {}).get("policies", {}) or {}
175
+ return pol.get("venue_rules", []), pol.get("parking_rules", [])
storefront_data.json ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "products": [
3
+ {
4
+ "sku": "CG-SET",
5
+ "name": "Cap & Gown Set",
6
+ "price_usd": 59.0,
7
+ "description": "Tassel included; ships until 10 days before the event. Sizes available at pickup; exchange allowed on-site if inventory permits."
8
+ },
9
+ {
10
+ "sku": "PK-1",
11
+ "name": "Parking Pass",
12
+ "price_usd": 10.0,
13
+ "description": "One vehicle per pass. Multiple passes are allowed per student for extended family or guests."
14
+ }
15
+ ],
16
+ "policies": {
17
+ "parking_rules": [
18
+ "No double parking.",
19
+ "Vehicles parked in handicap spaces will be towed."
20
+ ],
21
+ "venue_rules": [
22
+ "Formal attire is recommended (not required).",
23
+ "No muscle shirts.",
24
+ "No sagging pants."
25
+ ]
26
+ },
27
+ "logistics": {
28
+ "shipping_cutoff_days": 10,
29
+ "shipping_window_business_days": "3–5",
30
+ "pickup_location": "Student Center Bookstore",
31
+ "arrival_times": {
32
+ "graduates_minutes_early": 90,
33
+ "guests_minutes_early": 60
34
+ },
35
+ "lots_open_hours_before": 2,
36
+ "lots": ["A", "B"],
37
+ "overflow": "As needed"
38
+ },
39
+ "faq": [
40
+ {
41
+ "q": [
42
+ "Can I buy multiple parking passes?",
43
+ "multiple passes",
44
+ "more than one parking pass",
45
+ "extra parking pass"
46
+ ],
47
+ "a": "Yes, multiple parking passes are allowed per student."
48
+ },
49
+ {
50
+ "q": [
51
+ "What time do the parking lots open?",
52
+ "When do lots open",
53
+ "parking hours",
54
+ "what time parking"
55
+ ],
56
+ "a": "Parking lots open 2 hours before the ceremony."
57
+ },
58
+ {
59
+ "q": [
60
+ "Is formal attire required?",
61
+ "dress code",
62
+ "what should I wear",
63
+ "attire rules"
64
+ ],
65
+ "a": "Formal attire is recommended but not required. No muscle shirts or sagging pants."
66
+ },
67
+ {
68
+ "q": [
69
+ "Where do I pick up the gown?",
70
+ "gown pickup",
71
+ "pickup location"
72
+ ],
73
+ "a": "Pickup is at the Student Center Bookstore during the week prior to the event."
74
+ },
75
+ {
76
+ "q": [
77
+ "Shipping cutoff",
78
+ "last day to ship",
79
+ "when is shipping available until"
80
+ ],
81
+ "a": "Shipping is available until 10 days before the event. Typical shipping takes 3–5 business days."
82
+ }
83
+ ]
84
+ }
storefront_tabs_memory_app.py ADDED
@@ -0,0 +1,286 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # storefront_tabs_memory_app.py
2
+ import os, json
3
+ import gradio as gr
4
+ from transformers import pipeline
5
+
6
+ # ---------------- Model ----------------
7
+ MODEL_NAME = os.getenv("HF_MODEL_GENERATION", "distilgpt2")
8
+ _pipe = None
9
+ def _get_pipe():
10
+ global _pipe
11
+ if _pipe is None:
12
+ _pipe = pipeline("text-generation", model=MODEL_NAME)
13
+ return _pipe
14
+
15
+ def model_generate(prompt, max_new_tokens=128, temperature=0.8, top_p=0.95):
16
+ out = _get_pipe()(
17
+ prompt,
18
+ max_new_tokens=int(max_new_tokens),
19
+ do_sample=True,
20
+ temperature=float(temperature),
21
+ top_p=float(top_p),
22
+ pad_token_id=50256,
23
+ )
24
+ return out[0]["generated_text"]
25
+
26
+
27
+ # ---------------- Storefront knowledge (helper module preferred) ----------------
28
+ STORE_DATA, USE_HELPERS = None, False
29
+ try:
30
+ # Optional helper module under agenticcore/
31
+ from agenticcore.storefront_rules import (
32
+ load_storefront, answer_faq, get_parking_rules, get_venue_rules, search_products
33
+ )
34
+ STORE_DATA = load_storefront()
35
+ USE_HELPERS = True
36
+ except Exception:
37
+ # Fallback: try JSON next to this file or under agenticcore/
38
+ CANDIDATES = [
39
+ os.path.join(os.path.dirname(__file__), "storefront_data.json"),
40
+ os.path.join(os.path.dirname(__file__), "agenticcore", "storefront_data.json"),
41
+ ]
42
+ for p in CANDIDATES:
43
+ if os.path.exists(p):
44
+ with open(p, "r", encoding="utf-8") as f:
45
+ STORE_DATA = json.load(f)
46
+ break
47
+
48
+ # Defaults if JSON/module missing
49
+ DEFAULT_PRODUCTS = [
50
+ {"SKU": "CG-SET", "Name": "Cap & Gown Set", "Price": 59.00, "Notes": "Tassel included; ship until 10 days before event"},
51
+ {"SKU": "PK-1", "Name": "Parking Pass", "Price": 10.00, "Notes": "Multiple passes allowed per student"},
52
+ ]
53
+ DEFAULT_PARKING = ["No double parking.", "Vehicles parked in handicap will be towed."]
54
+ DEFAULT_VENUE = ["Formal attire recommended (not required).", "No muscle shirts.", "No sagging pants."]
55
+
56
+ # Normalize JSON to tables for the UI
57
+ if STORE_DATA:
58
+ try:
59
+ DEFAULT_PRODUCTS = [{
60
+ "SKU": p.get("sku",""),
61
+ "Name": p.get("name",""),
62
+ "Price": p.get("price_usd",""),
63
+ "Notes": (p.get("description") or "")[:120],
64
+ } for p in STORE_DATA.get("products", [])]
65
+ DEFAULT_PARKING = STORE_DATA.get("policies", {}).get("parking_rules", DEFAULT_PARKING) or DEFAULT_PARKING
66
+ DEFAULT_VENUE = STORE_DATA.get("policies", {}).get("venue_rules", DEFAULT_VENUE) or DEFAULT_VENUE
67
+ except Exception:
68
+ pass
69
+
70
+
71
+ # ---------------- Memory seeding ----------------
72
+ def seed_storefront_facts() -> str:
73
+ """Small system-like primer injected ahead of chat history to bias the LLM toward storefront truth."""
74
+ lines = ["You are answering questions about a graduation storefront.",
75
+ "Products:"]
76
+ for p in DEFAULT_PRODUCTS:
77
+ price = p.get("Price")
78
+ if isinstance(price, (int, float)):
79
+ price = f"${price:.2f}"
80
+ lines.append(f"- {p.get('Name','Item')} ({p.get('SKU','')}) — {price}: {p.get('Notes','')}")
81
+ lines.append("Venue rules:")
82
+ for r in DEFAULT_VENUE:
83
+ lines.append(f"- {r}")
84
+ lines.append("Parking rules:")
85
+ for r in DEFAULT_PARKING:
86
+ lines.append(f"- {r}")
87
+ lines.append("Answer concisely using these facts. If unsure, say what’s known from the list above.")
88
+ return "\n".join(lines)
89
+
90
+ SEED_TEXT = seed_storefront_facts()
91
+
92
+ def build_prompt_from_history(history, user_text, k=4):
93
+ """history = [[user, bot], ...] — build a short rolling prompt + seed facts."""
94
+ lines = [SEED_TEXT, "", "Conversation so far:"]
95
+ for u, b in (history or [])[-k:]:
96
+ if u: lines.append(f"User: {u}")
97
+ if b: lines.append(f"Assistant: {b}")
98
+ lines.append(f"User: {user_text}")
99
+ lines.append("Assistant:")
100
+ return "\n".join(lines)
101
+
102
+
103
+ # ---------------- Storefront Q&A router (storefront first, then LLM) ----------------
104
+ def storefront_qna(text: str) -> str | None:
105
+ t = (text or "").lower().strip()
106
+ if not t:
107
+ return None
108
+
109
+ # Single-word catches to avoid LLM drift
110
+ if t in {"parking"}:
111
+ return "Parking rules:\n- " + "\n- ".join(DEFAULT_PARKING)
112
+ if t in {"venue", "attire", "dress", "dress code"}:
113
+ return "Venue rules:\n- " + "\n- ".join(DEFAULT_VENUE)
114
+ if t in {"passes", "parking pass", "parking passes"}:
115
+ return "Yes, multiple parking passes are allowed per student."
116
+
117
+ # Prefer helper functions if available
118
+ if USE_HELPERS and STORE_DATA:
119
+ a = answer_faq(STORE_DATA, t)
120
+ if a:
121
+ return a
122
+ if "parking" in t and "rule" in t:
123
+ r = get_parking_rules(STORE_DATA)
124
+ if r:
125
+ return "Parking rules:\n- " + "\n- ".join(r)
126
+ if ("venue" in t and "rule" in t) or "attire" in t or "dress code" in t:
127
+ r = get_venue_rules(STORE_DATA)
128
+ if r:
129
+ return "Venue rules:\n- " + "\n- ".join(r)
130
+ # Specific timing phrasing to avoid hallucinated dates
131
+ if "parking" in t and ("hours" in t or "time" in t or "open" in t):
132
+ return "Parking lots open 2 hours before the ceremony."
133
+ hits = search_products(STORE_DATA, t)
134
+ if hits:
135
+ return "\n".join(
136
+ f"{p.get('name','Item')} — ${p.get('price_usd',0):.2f}: {p.get('description','')}"
137
+ for p in hits
138
+ )
139
+ return None
140
+
141
+ # Fallback rules (no helper module)
142
+ if "parking" in t and ("more than one" in t or "multiple" in t or "extra" in t):
143
+ return "Yes, multiple parking passes are allowed per student."
144
+ if "parking" in t and "rule" in t:
145
+ return "Parking rules:\n- " + "\n- ".join(DEFAULT_PARKING)
146
+ if "parking" in t and ("hours" in t or "time" in t or "open" in t):
147
+ return "Parking lots open 2 hours before the ceremony."
148
+ if "attire" in t or "dress code" in t or ("venue" in t and "rule" in t):
149
+ return "Venue rules:\n- " + "\n- ".join(DEFAULT_VENUE)
150
+ if "cap" in t or "gown" in t:
151
+ return "\n".join(
152
+ f"{p['Name']} — ${p['Price']:.2f}: {p['Notes']}"
153
+ for p in DEFAULT_PRODUCTS
154
+ )
155
+ return None
156
+
157
+
158
+ def chat_pipeline(history, message, max_new_tokens=128, temperature=0.8, top_p=0.95):
159
+ # 1) Try storefront knowledge first
160
+ sf = storefront_qna(message)
161
+ if sf:
162
+ return sf
163
+ # 2) Memory-aware model prompt
164
+ prompt = build_prompt_from_history(history, message, k=4)
165
+ return model_generate(prompt, max_new_tokens, temperature, top_p)
166
+
167
+
168
+ # ---------------- Gradio UI (Tabs + Accordion) ----------------
169
+ CSS = """
170
+ :root { --bg:#0b0d12; --panel:#0f172a; --border:#1f2940; --text:#e5e7eb; --muted:#9ca3af; }
171
+ .gradio-container { background: var(--bg) !important; color: var(--text) !important; }
172
+ .panel { border:1px solid var(--border); border-radius:16px; background:var(--panel); }
173
+ .small { font-size:12px; color: var(--muted); }
174
+ """
175
+
176
+ with gr.Blocks(title="Storefront Chat", css=CSS) as demo:
177
+ gr.Markdown("## Storefront Chat")
178
+
179
+ # Keep a single source of truth for chat history (pairs of [user, bot])
180
+ history_state = gr.State([])
181
+
182
+ with gr.Tabs():
183
+ # --- TAB 1: Chat (sliders tucked into an accordion) ---
184
+ with gr.TabItem("Chat"):
185
+ with gr.Group(elem_classes=["panel"]):
186
+ chat = gr.Chatbot(height=360, bubble_full_width=False, label="Chat")
187
+
188
+ with gr.Row():
189
+ msg = gr.Textbox(placeholder="Ask about parking rules, attire, cap & gown, pickup times…", scale=5)
190
+ send = gr.Button("Send", scale=1)
191
+
192
+ # Quick chips
193
+ with gr.Row():
194
+ chip1 = gr.Button("Parking rules", variant="secondary")
195
+ chip2 = gr.Button("Multiple passes", variant="secondary")
196
+ chip3 = gr.Button("Attire", variant="secondary")
197
+ chip4 = gr.Button("When do lots open?", variant="secondary")
198
+
199
+ # Advanced options hidden
200
+ with gr.Accordion("Advanced chat options", open=False):
201
+ max_new = gr.Slider(32, 512, 128, 1, label="Max new tokens")
202
+ temp = gr.Slider(0.1, 1.5, 0.8, 0.05, label="Temperature")
203
+ topp = gr.Slider(0.1, 1.0, 0.95, 0.05, label="Top-p")
204
+
205
+ # Small utilities
206
+ with gr.Row():
207
+ health_btn = gr.Button("Health", variant="secondary")
208
+ caps_btn = gr.Button("Capabilities", variant="secondary")
209
+ status_md = gr.Markdown("Status: not checked", elem_classes=["small"])
210
+
211
+ # --- TAB 2: Products ---
212
+ with gr.TabItem("Products"):
213
+ gr.Markdown("### Available Items")
214
+ cols = list(DEFAULT_PRODUCTS[0].keys()) if DEFAULT_PRODUCTS else ["SKU","Name","Price","Notes"]
215
+ data = [[p.get(c,"") for c in cols] for p in DEFAULT_PRODUCTS]
216
+ _products_tbl = gr.Dataframe(headers=cols, value=data, interactive=False, wrap=True, label="Products")
217
+
218
+ # --- TAB 3: Rules ---
219
+ with gr.TabItem("Rules"):
220
+ gr.Markdown("### Venue rules")
221
+ gr.Markdown("- " + "\n- ".join(DEFAULT_VENUE))
222
+ gr.Markdown("### Parking rules")
223
+ gr.Markdown("- " + "\n- ".join(DEFAULT_PARKING))
224
+
225
+ # --- TAB 4: Logistics ---
226
+ with gr.TabItem("Logistics"):
227
+ gr.Markdown(
228
+ "### Event Logistics\n"
229
+ "- Shipping available until 10 days before event (typ. 3–5 business days)\n"
230
+ "- Pickup: Student Center Bookstore during the week prior to event\n"
231
+ "- Graduates arrive 90 minutes early; guests 60 minutes early\n"
232
+ "- Lots A & B open 2 hours before; overflow as needed\n"
233
+ "\n*Try asking the bot:* “What time should I arrive?” • “Where do I pick up the gown?”"
234
+ )
235
+
236
+ # ---------- Helpers that keep Chatbot history valid (list of [u,b]) ----------
237
+ def _append_bot_md(history, md_text):
238
+ """Append a bot markdown message without breaking the [user, bot] format."""
239
+ history = history or []
240
+ return history + [[None, md_text]]
241
+
242
+ # ---------- Callbacks ----------
243
+ def on_send(history, message, max_new_tokens, temperature, top_p):
244
+ t = (message or "").strip()
245
+ if not t:
246
+ return history, history, "" # no-op
247
+ history = (history or []) + [[t, None]]
248
+ reply = chat_pipeline(history[:-1], t, max_new_tokens, temperature, top_p)
249
+ history[-1][1] = reply
250
+ # Return updated state AND what the Chatbot should render
251
+ return history, history, ""
252
+
253
+ def _health_cb(history):
254
+ md = (f"### Status: ✅ Healthy\n"
255
+ f"- Model: `{MODEL_NAME}`\n"
256
+ f"- Storefront module: {'yes' if USE_HELPERS else 'no'}\n"
257
+ f"- Storefront JSON: {'loaded' if bool(STORE_DATA) else 'not found'}")
258
+ new_hist = _append_bot_md(history, md)
259
+ return new_hist, new_hist, "Status: ✅ Healthy"
260
+
261
+ def _caps_cb(history):
262
+ caps = [
263
+ "Chat (LLM text-generation, memory-aware prompt)",
264
+ "Storefront Q&A (parking, attire, products, logistics)",
265
+ "Adjustable: max_new_tokens, temperature, top-p",
266
+ ]
267
+ md = "### Capabilities\n- " + "\n- ".join(caps)
268
+ new_hist = _append_bot_md(history, md)
269
+ return new_hist, new_hist
270
+
271
+ # Wire up (note: always update both the state and the Chatbot value)
272
+ send.click(on_send, [history_state, msg, max_new, temp, topp], [history_state, chat, msg])
273
+ msg.submit(on_send, [history_state, msg, max_new, temp, topp], [history_state, chat, msg])
274
+
275
+ # Chips
276
+ chip1.click(lambda: "What are the parking rules?", outputs=msg)
277
+ chip2.click(lambda: "Can I buy multiple parking passes?", outputs=msg)
278
+ chip3.click(lambda: "Is formal attire required?", outputs=msg)
279
+ chip4.click(lambda: "What time do the parking lots open?", outputs=msg)
280
+
281
+ # Health / capabilities (append to chat, keep valid tuple format)
282
+ health_btn.click(_health_cb, inputs=[history_state], outputs=[history_state, chat, status_md])
283
+ caps_btn.click(_caps_cb, inputs=[history_state], outputs=[history_state, chat])
284
+
285
+ if __name__ == "__main__":
286
+ demo.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", "7860")))