Spaces:
Running
Running
Update main.py
Browse files
main.py
CHANGED
|
@@ -1,12 +1,13 @@
|
|
| 1 |
"""
|
| 2 |
-
main.py — Pricelyst Shopping Advisor (
|
| 3 |
|
| 4 |
✅ Flask API
|
| 5 |
✅ Firebase Admin persistence (service account JSON via env var)
|
| 6 |
-
✅ Gemini via NEW google-genai SDK (text + multimodal)
|
| 7 |
-
✅ Product intelligence from Pricelyst API
|
| 8 |
-
✅ Graceful conversational handling (
|
| 9 |
-
✅ Call briefing
|
|
|
|
| 10 |
|
| 11 |
ENV VARS YOU NEED
|
| 12 |
- GOOGLE_API_KEY=...
|
|
@@ -14,36 +15,6 @@ ENV VARS YOU NEED
|
|
| 14 |
- PRICE_API_BASE=https://api.pricelyst.co.zw # optional
|
| 15 |
- GEMINI_MODEL=gemini-2.0-flash # optional
|
| 16 |
- PORT=5000 # optional
|
| 17 |
-
|
| 18 |
-
REQUEST SHAPES
|
| 19 |
-
1) POST /chat
|
| 20 |
-
{
|
| 21 |
-
"profile_id": "demo123",
|
| 22 |
-
"username": "Tinashe", # optional
|
| 23 |
-
"message": "Where is cooking oil cheapest?",
|
| 24 |
-
"images": ["data:image/png;base64,...", "https://..."], # optional
|
| 25 |
-
"context": { "budget": 20, "location": "Harare" } # optional
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
2) POST /api/call-briefing
|
| 29 |
-
{
|
| 30 |
-
"profile_id": "demo123",
|
| 31 |
-
"username": "Tinashe"
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
3) POST /api/log-call-usage
|
| 35 |
-
{
|
| 36 |
-
"profile_id": "demo123",
|
| 37 |
-
"transcript": ".... full transcript ...",
|
| 38 |
-
"call_id": "optional-client-id",
|
| 39 |
-
"started_at": "2026-01-23T12:00:00Z",
|
| 40 |
-
"ended_at": "2026-01-23T12:08:05Z",
|
| 41 |
-
"stats": { "duration_sec": 485, "agent": "elevenlabs" }
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
NOTES
|
| 45 |
-
- We DO NOT depend on upstream auth (you said products are open).
|
| 46 |
-
- We keep our own "profile_id" for personalization; when integrated, the host app supplies real profile_id.
|
| 47 |
"""
|
| 48 |
|
| 49 |
import os
|
|
@@ -73,6 +44,7 @@ logger = logging.getLogger("pricelyst-advisor")
|
|
| 73 |
# pip install google-genai
|
| 74 |
try:
|
| 75 |
from google import genai
|
|
|
|
| 76 |
except Exception as e:
|
| 77 |
genai = None
|
| 78 |
logger.error("google-genai not installed. pip install google-genai. Error=%s", e)
|
|
@@ -125,6 +97,16 @@ _product_cache: Dict[str, Any] = {
|
|
| 125 |
"raw_count": 0,
|
| 126 |
}
|
| 127 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
# =========================
|
| 129 |
# Helpers: time / strings
|
| 130 |
# =========================
|
|
@@ -420,6 +402,27 @@ def gemini_generate_text(system: str, user: str, temperature: float = 0.4) -> st
|
|
| 420 |
logger.error("Gemini text error: %s", e)
|
| 421 |
return ""
|
| 422 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
def gemini_generate_multimodal(system: str, user: str, images: List[Dict[str, Any]]) -> str:
|
| 424 |
"""
|
| 425 |
Uses Gemini multimodal:
|
|
@@ -478,6 +481,7 @@ Output schema:
|
|
| 478 |
"product_discovery",
|
| 479 |
"trust_check",
|
| 480 |
"chit_chat",
|
|
|
|
| 481 |
"other"
|
| 482 |
],
|
| 483 |
"items": [{"name": "...", "quantity": 1}],
|
|
@@ -489,14 +493,31 @@ Rules:
|
|
| 489 |
- If user is chatting/social (hi, jokes, thanks, how are you, etc) => actionable=false, intent="chit_chat".
|
| 490 |
- If user asks about prices/stores/basket/what to buy => actionable=true.
|
| 491 |
- If user provided a list, extract items + quantities if obvious.
|
|
|
|
| 492 |
- Keep it conservative: if unclear, actionable=false.
|
| 493 |
"""
|
| 494 |
|
| 495 |
def detect_intent(message: str, images_present: bool, context: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 496 |
ctx_str = json.dumps(context or {}, ensure_ascii=False)
|
| 497 |
user = f"Message: {message}\nImagesPresent: {images_present}\nContext: {ctx_str}"
|
| 498 |
-
|
| 499 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 500 |
if not isinstance(data, dict):
|
| 501 |
return {"actionable": False, "intent": "other", "items": [], "constraints": {}, "notes": "bad_json"}
|
| 502 |
# normalize
|
|
@@ -506,6 +527,30 @@ def detect_intent(message: str, images_present: bool, context: Dict[str, Any]) -
|
|
| 506 |
data.setdefault("constraints", {})
|
| 507 |
return data
|
| 508 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 509 |
# =========================
|
| 510 |
# Matching + analytics
|
| 511 |
# =========================
|
|
@@ -759,34 +804,6 @@ def extract_items_from_images(images: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
| 759 |
data.setdefault("items", [])
|
| 760 |
return data
|
| 761 |
|
| 762 |
-
# =========================
|
| 763 |
-
# Post-call report synthesis (only if actionable)
|
| 764 |
-
# =========================
|
| 765 |
-
CALL_REPORT_SYSTEM = """
|
| 766 |
-
You are Pricelyst AI. You will receive a full call transcript.
|
| 767 |
-
Decide whether there is an actionable request (party planning, shopping needs, budgeting, groceries, etc).
|
| 768 |
-
If actionable, produce a concise MARKDOWN report that the client can turn into a PDF.
|
| 769 |
-
If NOT actionable (just chatting), return an empty string.
|
| 770 |
-
|
| 771 |
-
Rules:
|
| 772 |
-
- Be practical and Zimbabwe-oriented.
|
| 773 |
-
- If planning an event: include (1) Assumptions, (2) Shopping list with quantities, (3) Budget ranges, (4) Simple menu/recipe ideas, (5) Optional restaurant/catering suggestions (generic; do NOT invent addresses).
|
| 774 |
-
- Only output Markdown or empty string. No code blocks.
|
| 775 |
-
"""
|
| 776 |
-
|
| 777 |
-
def build_call_report_markdown(transcript: str) -> str:
|
| 778 |
-
if not transcript or len(transcript.strip()) < 40:
|
| 779 |
-
return ""
|
| 780 |
-
md = gemini_generate_text(CALL_REPORT_SYSTEM, transcript, temperature=0.3)
|
| 781 |
-
md = (md or "").strip()
|
| 782 |
-
# Guardrail: if model returns JSON or obvious nonsense, drop it.
|
| 783 |
-
if md.startswith("{") or md.startswith("["):
|
| 784 |
-
return ""
|
| 785 |
-
# Conservative: must contain at least one list bullet or heading to be “report-like”
|
| 786 |
-
if ("#" not in md) and ("- " not in md) and ("* " not in md):
|
| 787 |
-
return ""
|
| 788 |
-
return md
|
| 789 |
-
|
| 790 |
# =========================
|
| 791 |
# Routes
|
| 792 |
# =========================
|
|
@@ -854,7 +871,21 @@ def chat():
|
|
| 854 |
|
| 855 |
response_payload: Dict[str, Any] = {"type": "unknown", "message": "No result."}
|
| 856 |
|
| 857 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 858 |
# pick first item or treat message as query
|
| 859 |
query = ""
|
| 860 |
if intent.get("items"):
|
|
@@ -961,18 +992,23 @@ def call_briefing():
|
|
| 961 |
for ii in intents:
|
| 962 |
intent_counts[ii] = intent_counts.get(ii, 0) + 1
|
| 963 |
|
| 964 |
-
|
|
|
|
|
|
|
|
|
|
| 965 |
"username": prof.get("username") or "there",
|
| 966 |
"last_best_store": last_store,
|
| 967 |
"top_intents_last_25": sorted(intent_counts.items(), key=lambda x: x[1], reverse=True)[:5],
|
| 968 |
"tone": "practical_zimbabwe",
|
|
|
|
| 969 |
}
|
| 970 |
|
| 971 |
return jsonify({
|
| 972 |
"ok": True,
|
| 973 |
"profile_id": profile_id,
|
| 974 |
"memory_summary": prof.get("memory_summary", ""),
|
| 975 |
-
|
|
|
|
| 976 |
})
|
| 977 |
|
| 978 |
@app.post("/api/log-call-usage")
|
|
@@ -990,23 +1026,30 @@ def log_call_usage():
|
|
| 990 |
|
| 991 |
prof = get_profile(profile_id)
|
| 992 |
|
| 993 |
-
#
|
| 994 |
-
|
| 995 |
-
|
| 996 |
-
planning_keywords = ["party", "birthday", "wedding", "braai", "grocer", "basket", "shopping", "budget", "ingredients", "recipe", "cook", "drinks", "snacks"]
|
| 997 |
-
looks_planning = any(k in transcript.lower() for k in planning_keywords)
|
| 998 |
-
|
| 999 |
report_md = ""
|
| 1000 |
-
if looks_planning and _gemini_client:
|
| 1001 |
-
report_md = build_call_report_markdown(transcript)
|
| 1002 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1003 |
doc_id = log_call(profile_id, {
|
| 1004 |
"call_id": call_id,
|
| 1005 |
"started_at": started_at,
|
| 1006 |
"ended_at": ended_at,
|
| 1007 |
"stats": stats,
|
| 1008 |
"transcript": transcript,
|
| 1009 |
-
"
|
|
|
|
| 1010 |
})
|
| 1011 |
|
| 1012 |
# update counters
|
|
@@ -1016,9 +1059,37 @@ def log_call_usage():
|
|
| 1016 |
return jsonify({
|
| 1017 |
"ok": True,
|
| 1018 |
"logged_call_doc_id": doc_id,
|
| 1019 |
-
"
|
| 1020 |
})
|
| 1021 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1022 |
# =========================
|
| 1023 |
# Run
|
| 1024 |
# =========================
|
|
|
|
| 1 |
"""
|
| 2 |
+
main.py — Pricelyst Shopping Advisor (Jessica Edition)
|
| 3 |
|
| 4 |
✅ Flask API
|
| 5 |
✅ Firebase Admin persistence (service account JSON via env var)
|
| 6 |
+
✅ Gemini via NEW google-genai SDK (text + multimodal + JSON Mode)
|
| 7 |
+
✅ Product intelligence from Pricelyst API
|
| 8 |
+
✅ Graceful conversational handling (Backwards Compatible)
|
| 9 |
+
✅ Call briefing (Zim Essentials Injection)
|
| 10 |
+
✅ Post-call Shopping Plan Generation (PDF-ready)
|
| 11 |
|
| 12 |
ENV VARS YOU NEED
|
| 13 |
- GOOGLE_API_KEY=...
|
|
|
|
| 15 |
- PRICE_API_BASE=https://api.pricelyst.co.zw # optional
|
| 16 |
- GEMINI_MODEL=gemini-2.0-flash # optional
|
| 17 |
- PORT=5000 # optional
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
"""
|
| 19 |
|
| 20 |
import os
|
|
|
|
| 44 |
# pip install google-genai
|
| 45 |
try:
|
| 46 |
from google import genai
|
| 47 |
+
from google.genai import types
|
| 48 |
except Exception as e:
|
| 49 |
genai = None
|
| 50 |
logger.error("google-genai not installed. pip install google-genai. Error=%s", e)
|
|
|
|
| 97 |
"raw_count": 0,
|
| 98 |
}
|
| 99 |
|
| 100 |
+
# ---------- Static Data (New Feature) ----------
|
| 101 |
+
# Injected into Voice Context & used for Text Fallback
|
| 102 |
+
ZIM_ESSENTIALS = {
|
| 103 |
+
"fuel_petrol": "$1.58/L (Blend)",
|
| 104 |
+
"fuel_diesel": "$1.65/L (Diesel 50)",
|
| 105 |
+
"zesa_electricity": "Tiered: First 50 units cheap, then ~14c/kWh",
|
| 106 |
+
"bread_standard": "$1.00/loaf (Fixed)",
|
| 107 |
+
"gas_lpg": "$1.90 - $2.10 per kg"
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
# =========================
|
| 111 |
# Helpers: time / strings
|
| 112 |
# =========================
|
|
|
|
| 402 |
logger.error("Gemini text error: %s", e)
|
| 403 |
return ""
|
| 404 |
|
| 405 |
+
def gemini_generate_json(system: str, user: str, images: List = None) -> Dict[str, Any]:
|
| 406 |
+
"""NEW: Strict JSON generation for reliable Plan/Intent"""
|
| 407 |
+
if not _gemini_client: return {}
|
| 408 |
+
parts = [{"text": system + "\n\n" + user}]
|
| 409 |
+
for img in images or []:
|
| 410 |
+
if "bytes" in img:
|
| 411 |
+
b64 = base64.b64encode(img["bytes"]).decode("utf-8")
|
| 412 |
+
parts.append({"inline_data": {"mime_type": img["mime"], "data": b64}})
|
| 413 |
+
elif "url" in img:
|
| 414 |
+
parts.append({"text": f"Image URL: {img['url']}"})
|
| 415 |
+
try:
|
| 416 |
+
resp = _gemini_client.models.generate_content(
|
| 417 |
+
model=GEMINI_MODEL,
|
| 418 |
+
contents=[{"role": "user", "parts": parts}],
|
| 419 |
+
config={"temperature": 0.2, "response_mime_type": "application/json", "max_output_tokens": 2000}
|
| 420 |
+
)
|
| 421 |
+
return json.loads(resp.text)
|
| 422 |
+
except Exception as e:
|
| 423 |
+
logger.error("Gemini JSON error: %s", e)
|
| 424 |
+
return {}
|
| 425 |
+
|
| 426 |
def gemini_generate_multimodal(system: str, user: str, images: List[Dict[str, Any]]) -> str:
|
| 427 |
"""
|
| 428 |
Uses Gemini multimodal:
|
|
|
|
| 481 |
"product_discovery",
|
| 482 |
"trust_check",
|
| 483 |
"chit_chat",
|
| 484 |
+
"lifestyle_lookup",
|
| 485 |
"other"
|
| 486 |
],
|
| 487 |
"items": [{"name": "...", "quantity": 1}],
|
|
|
|
| 493 |
- If user is chatting/social (hi, jokes, thanks, how are you, etc) => actionable=false, intent="chit_chat".
|
| 494 |
- If user asks about prices/stores/basket/what to buy => actionable=true.
|
| 495 |
- If user provided a list, extract items + quantities if obvious.
|
| 496 |
+
- "How much is fuel/zesa/bread" -> lifestyle_lookup
|
| 497 |
- Keep it conservative: if unclear, actionable=false.
|
| 498 |
"""
|
| 499 |
|
| 500 |
def detect_intent(message: str, images_present: bool, context: Dict[str, Any]) -> Dict[str, Any]:
|
| 501 |
+
# 1. Fast path for ZIM_ESSENTIALS (Optimization)
|
| 502 |
+
msg_lower = message.lower()
|
| 503 |
+
for k in ZIM_ESSENTIALS:
|
| 504 |
+
clean_k = k.split('_')[-1] # fuel_petrol -> petrol
|
| 505 |
+
if clean_k in msg_lower and "price" in msg_lower:
|
| 506 |
+
return {"actionable": True, "intent": "lifestyle_lookup", "items": [{"name": k}]}
|
| 507 |
+
|
| 508 |
+
# 2. Gemini Detection
|
| 509 |
ctx_str = json.dumps(context or {}, ensure_ascii=False)
|
| 510 |
user = f"Message: {message}\nImagesPresent: {images_present}\nContext: {ctx_str}"
|
| 511 |
+
|
| 512 |
+
# Try using the strict JSON helper first for better reliability
|
| 513 |
+
try:
|
| 514 |
+
data = gemini_generate_json(INTENT_SYSTEM, user)
|
| 515 |
+
if not isinstance(data, dict): raise ValueError("Invalid JSON")
|
| 516 |
+
except:
|
| 517 |
+
# Fallback to text parsing if JSON mode fails (Backward Compat)
|
| 518 |
+
out = gemini_generate_text(INTENT_SYSTEM, user, temperature=0.1)
|
| 519 |
+
data = _safe_json_loads(out, fallback={})
|
| 520 |
+
|
| 521 |
if not isinstance(data, dict):
|
| 522 |
return {"actionable": False, "intent": "other", "items": [], "constraints": {}, "notes": "bad_json"}
|
| 523 |
# normalize
|
|
|
|
| 527 |
data.setdefault("constraints", {})
|
| 528 |
return data
|
| 529 |
|
| 530 |
+
# =========================
|
| 531 |
+
# Shopping Plan Generator (NEW)
|
| 532 |
+
# =========================
|
| 533 |
+
PLAN_SYSTEM_PROMPT = """
|
| 534 |
+
You are Jessica, the Pricelyst Shopping Advisor. Analyze the conversation transcript.
|
| 535 |
+
If the user discussed a shopping list, budget plan, or event needs, create a structured plan.
|
| 536 |
+
|
| 537 |
+
OUTPUT JSON SCHEMA:
|
| 538 |
+
{
|
| 539 |
+
"is_actionable": boolean,
|
| 540 |
+
"title": "Short title (e.g. 'Weekend Braai List')",
|
| 541 |
+
"summary": "1 sentence summary",
|
| 542 |
+
"items": [{"name": "string", "qty": "string", "est_price": number|null}],
|
| 543 |
+
"markdown_content": "A clean Markdown report for a PDF. Include headers (#), bullet points, and a budget summary table if applicable. Keep it professional."
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
If no shopping/planning occurred, set is_actionable=false.
|
| 547 |
+
"""
|
| 548 |
+
|
| 549 |
+
def generate_shopping_plan(transcript: str) -> Dict[str, Any]:
|
| 550 |
+
if not transcript or len(transcript) < 30:
|
| 551 |
+
return {"is_actionable": False}
|
| 552 |
+
return gemini_generate_json(PLAN_SYSTEM_PROMPT, f"TRANSCRIPT:\n{transcript}")
|
| 553 |
+
|
| 554 |
# =========================
|
| 555 |
# Matching + analytics
|
| 556 |
# =========================
|
|
|
|
| 804 |
data.setdefault("items", [])
|
| 805 |
return data
|
| 806 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 807 |
# =========================
|
| 808 |
# Routes
|
| 809 |
# =========================
|
|
|
|
| 871 |
|
| 872 |
response_payload: Dict[str, Any] = {"type": "unknown", "message": "No result."}
|
| 873 |
|
| 874 |
+
# --- NEW: Check for Lifestyle/Essentials (Fuel/ZESA) ---
|
| 875 |
+
if intent["intent"] == "lifestyle_lookup":
|
| 876 |
+
# Items are auto-detected in detect_intent
|
| 877 |
+
key = intent["items"][0]["name"]
|
| 878 |
+
# Fuzzy match dict key
|
| 879 |
+
val = ZIM_ESSENTIALS.get(key) or ZIM_ESSENTIALS.get("fuel_petrol") # fallback
|
| 880 |
+
response_payload = {
|
| 881 |
+
"type": "info_card",
|
| 882 |
+
"title": f"Market Rate: {key.replace('_', ' ').title()}",
|
| 883 |
+
"message": str(val),
|
| 884 |
+
"highlights": [f"Current: {val}"]
|
| 885 |
+
}
|
| 886 |
+
|
| 887 |
+
# --- Original Logic ---
|
| 888 |
+
elif intent["intent"] in ("price_lookup", "trust_check", "product_discovery"):
|
| 889 |
# pick first item or treat message as query
|
| 890 |
query = ""
|
| 891 |
if intent.get("items"):
|
|
|
|
| 992 |
for ii in intents:
|
| 993 |
intent_counts[ii] = intent_counts.get(ii, 0) + 1
|
| 994 |
|
| 995 |
+
# --- KPI Snapshot Logic ---
|
| 996 |
+
# We construct a dictionary that the React client will pass as a JSON string
|
| 997 |
+
# We inject ZIM_ESSENTIALS here so the Agent has knowledge of fuel/zesa prices
|
| 998 |
+
kpi_data = {
|
| 999 |
"username": prof.get("username") or "there",
|
| 1000 |
"last_best_store": last_store,
|
| 1001 |
"top_intents_last_25": sorted(intent_counts.items(), key=lambda x: x[1], reverse=True)[:5],
|
| 1002 |
"tone": "practical_zimbabwe",
|
| 1003 |
+
"market_rates_essentials": ZIM_ESSENTIALS # <--- INJECTED KNOWLEDGE
|
| 1004 |
}
|
| 1005 |
|
| 1006 |
return jsonify({
|
| 1007 |
"ok": True,
|
| 1008 |
"profile_id": profile_id,
|
| 1009 |
"memory_summary": prof.get("memory_summary", ""),
|
| 1010 |
+
# This string is passed to ElevenLabs by the React Client
|
| 1011 |
+
"kpi_snapshot": json.dumps(kpi_data)
|
| 1012 |
})
|
| 1013 |
|
| 1014 |
@app.post("/api/log-call-usage")
|
|
|
|
| 1026 |
|
| 1027 |
prof = get_profile(profile_id)
|
| 1028 |
|
| 1029 |
+
# --- UPGRADE: Use Shopping Plan Generator (JSON + Markdown) ---
|
| 1030 |
+
plan_data = generate_shopping_plan(transcript)
|
| 1031 |
+
plan_id = None
|
|
|
|
|
|
|
|
|
|
| 1032 |
report_md = ""
|
|
|
|
|
|
|
| 1033 |
|
| 1034 |
+
if plan_data.get("is_actionable"):
|
| 1035 |
+
# Save structured plan
|
| 1036 |
+
plan_ref = db.collection("pricelyst_profiles").document(profile_id).collection("shopping_plans").document()
|
| 1037 |
+
plan_data["id"] = plan_ref.id
|
| 1038 |
+
plan_data["call_id"] = call_id
|
| 1039 |
+
plan_data["created_at"] = now_utc_iso()
|
| 1040 |
+
plan_ref.set(plan_data)
|
| 1041 |
+
plan_id = plan_ref.id
|
| 1042 |
+
report_md = plan_data.get("markdown_content", "")
|
| 1043 |
+
|
| 1044 |
+
# Log the call (link the plan_id)
|
| 1045 |
doc_id = log_call(profile_id, {
|
| 1046 |
"call_id": call_id,
|
| 1047 |
"started_at": started_at,
|
| 1048 |
"ended_at": ended_at,
|
| 1049 |
"stats": stats,
|
| 1050 |
"transcript": transcript,
|
| 1051 |
+
"generated_plan_id": plan_id,
|
| 1052 |
+
"report_markdown": report_md,
|
| 1053 |
})
|
| 1054 |
|
| 1055 |
# update counters
|
|
|
|
| 1059 |
return jsonify({
|
| 1060 |
"ok": True,
|
| 1061 |
"logged_call_doc_id": doc_id,
|
| 1062 |
+
"shopping_plan": plan_data if plan_id else None # Frontend uses this for PDF
|
| 1063 |
})
|
| 1064 |
|
| 1065 |
+
# --- NEW: Shopping Plans CRUD ---
|
| 1066 |
+
@app.get("/api/shopping-plans")
|
| 1067 |
+
def list_plans():
|
| 1068 |
+
pid = request.args.get("profile_id")
|
| 1069 |
+
if not pid: return jsonify({"ok": False}), 400
|
| 1070 |
+
try:
|
| 1071 |
+
docs = db.collection("pricelyst_profiles").document(pid).collection("shopping_plans")\
|
| 1072 |
+
.order_by("created_at", direction=firestore.Query.DESCENDING).limit(20).stream()
|
| 1073 |
+
plans = [{"id": d.id, **d.to_dict()} for d in docs]
|
| 1074 |
+
return jsonify({"ok": True, "plans": plans})
|
| 1075 |
+
except Exception as e:
|
| 1076 |
+
return jsonify({"ok": False, "error": str(e)}), 500
|
| 1077 |
+
|
| 1078 |
+
@app.get("/api/shopping-plans/<plan_id>")
|
| 1079 |
+
def get_plan(plan_id):
|
| 1080 |
+
pid = request.args.get("profile_id")
|
| 1081 |
+
if not pid: return jsonify({"ok": False}), 400
|
| 1082 |
+
doc = db.collection("pricelyst_profiles").document(pid).collection("shopping_plans").document(plan_id).get()
|
| 1083 |
+
if not doc.exists: return jsonify({"ok": False, "error": "Not found"}), 404
|
| 1084 |
+
return jsonify({"ok": True, "plan": doc.to_dict()})
|
| 1085 |
+
|
| 1086 |
+
@app.delete("/api/shopping-plans/<plan_id>")
|
| 1087 |
+
def delete_plan(plan_id):
|
| 1088 |
+
pid = request.args.get("profile_id")
|
| 1089 |
+
if not pid: return jsonify({"ok": False}), 400
|
| 1090 |
+
db.collection("pricelyst_profiles").document(pid).collection("shopping_plans").document(plan_id).delete()
|
| 1091 |
+
return jsonify({"ok": True})
|
| 1092 |
+
|
| 1093 |
# =========================
|
| 1094 |
# Run
|
| 1095 |
# =========================
|