Spaces:
Sleeping
Sleeping
Update main.py
Browse files
main.py
CHANGED
|
@@ -1,704 +1,141 @@
|
|
| 1 |
import os
|
| 2 |
import json
|
| 3 |
-
import time
|
| 4 |
-
from datetime import datetime
|
| 5 |
-
from io import BytesIO
|
| 6 |
-
from google.cloud.firestore_v1.base_query import FieldFilter
|
| 7 |
-
import pypdf
|
| 8 |
-
import firebase_admin
|
| 9 |
-
import numpy as np
|
| 10 |
-
import faiss
|
| 11 |
-
import pickle
|
| 12 |
from flask import Flask, request, jsonify
|
| 13 |
from flask_cors import CORS
|
| 14 |
from dotenv import load_dotenv
|
| 15 |
|
| 16 |
-
|
| 17 |
-
from google import genai
|
| 18 |
-
|
| 19 |
-
import os
|
| 20 |
-
import json
|
| 21 |
-
import pickle
|
| 22 |
-
import numpy as np
|
| 23 |
-
from flask import Flask, request, jsonify
|
| 24 |
-
from flask_cors import CORS
|
| 25 |
-
from dotenv import load_dotenv
|
| 26 |
from firebase_admin import credentials, firestore, storage, initialize_app
|
| 27 |
-
from google import genai
|
| 28 |
-
import faiss
|
| 29 |
|
| 30 |
-
|
|
|
|
| 31 |
|
| 32 |
-
#
|
| 33 |
-
|
| 34 |
-
CORS(app)
|
| 35 |
|
| 36 |
-
#
|
| 37 |
-
|
| 38 |
-
if not cred_json:
|
| 39 |
-
raise RuntimeError("Missing FIREBASE env var")
|
| 40 |
-
cred = credentials.Certificate(json.loads(cred_json))
|
| 41 |
-
initialize_app(cred, {"storageBucket": os.environ.get("Firebase_Storage")})
|
| 42 |
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
bucket = storage.bucket()
|
| 45 |
|
| 46 |
-
#
|
| 47 |
-
|
| 48 |
-
model_name = "gemini-2.0-flash-thinking-exp"
|
| 49 |
-
|
| 50 |
-
interventions_offered = {
|
| 51 |
-
"Marketing Support": [
|
| 52 |
-
"Domain & Email Registration",
|
| 53 |
-
"Website Development & Hosting",
|
| 54 |
-
"Logo",
|
| 55 |
-
"Social Media Setup & Page",
|
| 56 |
-
"Industry Memberships",
|
| 57 |
-
"Company Profile",
|
| 58 |
-
"Email Signature",
|
| 59 |
-
"Business Cards",
|
| 60 |
-
"Branded Banner",
|
| 61 |
-
"Pamphlets/Brochures",
|
| 62 |
-
"Market Linkage",
|
| 63 |
-
"Marketing Plan",
|
| 64 |
-
"Digital Marketing Support",
|
| 65 |
-
"Marketing Mentoring"
|
| 66 |
-
],
|
| 67 |
-
"Financial Management": [
|
| 68 |
-
"Management Accounts",
|
| 69 |
-
"Financial Management Templates",
|
| 70 |
-
"Record Keeping",
|
| 71 |
-
"Business Plan/Proposal",
|
| 72 |
-
"Funding Linkages",
|
| 73 |
-
"Financial Literacy Training",
|
| 74 |
-
"Tax Compliance Support",
|
| 75 |
-
"Access to Financial Software",
|
| 76 |
-
"Financial Management Mentorship",
|
| 77 |
-
"Grant Application Support",
|
| 78 |
-
"Cost Management Strategies",
|
| 79 |
-
"Financial Reporting Standards",
|
| 80 |
-
"Product Costing"
|
| 81 |
-
],
|
| 82 |
-
"Compliance": [
|
| 83 |
-
"Insurance",
|
| 84 |
-
"CIPC and Annual Returns Registration",
|
| 85 |
-
"UIF Registration",
|
| 86 |
-
"VAT Registration",
|
| 87 |
-
"Risk Management Plan",
|
| 88 |
-
"HRM Support (i.e., Templates)",
|
| 89 |
-
"Guidance - Food Compliance (Webinar)",
|
| 90 |
-
"PAYE Compliance",
|
| 91 |
-
"COIDA Compliance",
|
| 92 |
-
"Certificate of Acceptability"
|
| 93 |
-
],
|
| 94 |
-
"Business Strategy & Leadership": [
|
| 95 |
-
"Executive Mentoring",
|
| 96 |
-
"Business Ops Plan",
|
| 97 |
-
"Strategic Plan",
|
| 98 |
-
"Business Communication (How to Pitch)",
|
| 99 |
-
"Digital Transformation",
|
| 100 |
-
"Leadership and Personal Development",
|
| 101 |
-
"Design Thinking",
|
| 102 |
-
"Productivity Training"
|
| 103 |
-
],
|
| 104 |
-
"Skills Development & Training": [
|
| 105 |
-
"Excel Skills Training",
|
| 106 |
-
"Industry Seminars",
|
| 107 |
-
"Fireside Chat",
|
| 108 |
-
"Industry Courses/Training",
|
| 109 |
-
"AI Tools Training",
|
| 110 |
-
"PowerPoint Presentation Training"
|
| 111 |
-
],
|
| 112 |
-
"Operations & Tools": [
|
| 113 |
-
"Tools and Equipment",
|
| 114 |
-
"Data Support",
|
| 115 |
-
"Technology Application Support",
|
| 116 |
-
"CRM Solutions"
|
| 117 |
-
],
|
| 118 |
-
"Health & Safety": [
|
| 119 |
-
"OHS Audit",
|
| 120 |
-
"Health & Safety Training"
|
| 121 |
-
],
|
| 122 |
-
"Customer Experience & Sales": [
|
| 123 |
-
"Customer Service – Enhancing service quality to improve client satisfaction and retention",
|
| 124 |
-
"Technology Readiness and Systems Integration",
|
| 125 |
-
"Sales and Marketing (including Export Readiness)"
|
| 126 |
-
]
|
| 127 |
-
}
|
| 128 |
-
|
| 129 |
-
class GenericEvaluator:
|
| 130 |
-
def __init__(self, available_interventions=None):
|
| 131 |
-
self.available_interventions = available_interventions or interventions_offered
|
| 132 |
-
|
| 133 |
-
def generate_prompt(self, participant_info: dict) -> str:
|
| 134 |
-
# Create a simplified version of interventions for the prompt
|
| 135 |
-
interventions_json = json.dumps(self.available_interventions, indent=2)
|
| 136 |
-
|
| 137 |
-
prompt = f"""
|
| 138 |
-
You are an expert evaluator for a small business incubator in South Africa, reviewing candidate applications. Use your expertise, critical thinking, and judgment to assess the following applicant. There are no predefined criteria or weights — your evaluation should be holistic and based on the information provided.
|
| 139 |
-
|
| 140 |
-
Participant Info:
|
| 141 |
-
{json.dumps(participant_info, indent=2)}
|
| 142 |
-
|
| 143 |
-
Based on your assessment, provide:
|
| 144 |
-
1. "AI Recommendation": either "Accept" or "Reject"
|
| 145 |
-
2. "AI Score": a score out of 100 reflecting overall business quality or readiness
|
| 146 |
-
3. "Justification": a brief explanation for your decision (3-5 sentences)
|
| 147 |
-
4. "Recommended Interventions": Select 3-5 appropriate intervention categories and specific interventions that would most benefit this business.
|
| 148 |
-
|
| 149 |
-
Available interventions:
|
| 150 |
-
{interventions_json}
|
| 151 |
-
|
| 152 |
-
Return your output strictly as a JSON dictionary with these keys:
|
| 153 |
-
- "AI Recommendation" (string: "Accept" or "Reject")
|
| 154 |
-
- "AI Score" (integer between 0-100)
|
| 155 |
-
- "Justification" (string)
|
| 156 |
-
- "Recommended Interventions" (object with category names as keys and arrays of specific interventions as values)
|
| 157 |
-
|
| 158 |
-
Example format for "Recommended Interventions":
|
| 159 |
-
{{
|
| 160 |
-
"Branding & Digital Presence": [
|
| 161 |
-
"Website Development & Hosting",
|
| 162 |
-
"Digital Marketing Support"
|
| 163 |
-
],
|
| 164 |
-
"Financial Management & Compliance": [
|
| 165 |
-
"Business Plan/Proposal",
|
| 166 |
-
"Financial Literacy Training"
|
| 167 |
-
]
|
| 168 |
-
}}
|
| 169 |
-
"""
|
| 170 |
-
return prompt
|
| 171 |
-
|
| 172 |
-
def parse_gemini_response(self, response_text: str) -> dict:
|
| 173 |
-
try:
|
| 174 |
-
# Try to find and extract JSON from the response
|
| 175 |
-
response_text = response_text.strip()
|
| 176 |
-
|
| 177 |
-
# Look for JSON content between curly braces
|
| 178 |
-
start_idx = response_text.find('{')
|
| 179 |
-
end_idx = response_text.rfind('}')
|
| 180 |
-
|
| 181 |
-
if start_idx >= 0 and end_idx > start_idx:
|
| 182 |
-
json_str = response_text[start_idx:end_idx+1]
|
| 183 |
-
result = json.loads(json_str)
|
| 184 |
-
|
| 185 |
-
# Validate required fields
|
| 186 |
-
required_fields = ["AI Recommendation", "AI Score", "Justification", "Recommended Interventions"]
|
| 187 |
-
missing_fields = [field for field in required_fields if field not in result]
|
| 188 |
-
|
| 189 |
-
if missing_fields:
|
| 190 |
-
return {
|
| 191 |
-
"error": f"Missing required fields: {', '.join(missing_fields)}",
|
| 192 |
-
"parsed_data": result
|
| 193 |
-
}
|
| 194 |
-
|
| 195 |
-
# Validate AI Recommendation format
|
| 196 |
-
if result["AI Recommendation"] not in ["Accept", "Reject"]:
|
| 197 |
-
return {
|
| 198 |
-
"error": "AI Recommendation must be either 'Accept' or 'Reject'",
|
| 199 |
-
"parsed_data": result
|
| 200 |
-
}
|
| 201 |
-
|
| 202 |
-
# Validate AI Score format
|
| 203 |
-
try:
|
| 204 |
-
score = int(result["AI Score"])
|
| 205 |
-
if not 0 <= score <= 100:
|
| 206 |
-
return {
|
| 207 |
-
"error": "AI Score must be between 0 and 100",
|
| 208 |
-
"parsed_data": result
|
| 209 |
-
}
|
| 210 |
-
except (ValueError, TypeError):
|
| 211 |
-
return {
|
| 212 |
-
"error": "AI Score must be a valid integer",
|
| 213 |
-
"parsed_data": result
|
| 214 |
-
}
|
| 215 |
-
|
| 216 |
-
# Validate Recommended Interventions format
|
| 217 |
-
interventions = result.get("Recommended Interventions", {})
|
| 218 |
-
if not isinstance(interventions, dict):
|
| 219 |
-
return {
|
| 220 |
-
"error": "Recommended Interventions must be an object/dictionary",
|
| 221 |
-
"parsed_data": result
|
| 222 |
-
}
|
| 223 |
-
|
| 224 |
-
# All validations passed
|
| 225 |
-
return result
|
| 226 |
-
else:
|
| 227 |
-
return {"error": "No valid JSON found in response", "raw_response": response_text}
|
| 228 |
-
except json.JSONDecodeError as e:
|
| 229 |
-
return {"error": f"JSON parsing error: {str(e)}", "raw_response": response_text}
|
| 230 |
-
except Exception as e:
|
| 231 |
-
return {"error": f"Unexpected error: {str(e)}", "raw_response": response_text}
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
# --- FAISS Setup ---
|
| 237 |
-
INDEX_PATH = "vector.index"
|
| 238 |
-
DOCS_PATH = "documents.pkl"
|
| 239 |
-
|
| 240 |
-
# --- Role-Aware Firestore Fetch ---
|
| 241 |
-
def fetch_documents(role: str, user_id: str) -> list[str]:
|
| 242 |
-
docs = []
|
| 243 |
-
|
| 244 |
-
# 1) participants
|
| 245 |
-
for snap in fs.collection("participants").stream():
|
| 246 |
-
d = snap.to_dict()
|
| 247 |
-
owner_id = snap.id
|
| 248 |
-
if role == "incubatee" and owner_id != user_id:
|
| 249 |
-
continue
|
| 250 |
-
if role == "consultant" and user_id not in d.get("assignedConsultants", []):
|
| 251 |
-
continue
|
| 252 |
-
name = d.get('beneficiaryName', 'Unknown')
|
| 253 |
-
ent = d.get('enterpriseName', 'Unknown')
|
| 254 |
-
sector = d.get('sector', 'Unknown')
|
| 255 |
-
stage = d.get('stage', 'Unknown')
|
| 256 |
-
devtype = d.get('developmentType', 'Unknown')
|
| 257 |
-
docs.append(f"{name} ({ent}), sector: {sector}, stage: {stage}, type: {devtype}.")
|
| 258 |
-
|
| 259 |
-
# 2) consultants
|
| 260 |
-
for snap in fs.collection("consultants").stream():
|
| 261 |
-
d = snap.to_dict()
|
| 262 |
-
if role == "consultant" and snap.id != user_id:
|
| 263 |
-
continue
|
| 264 |
-
name = d.get("name", "Unknown")
|
| 265 |
-
expertise = ", ".join(d.get("expertise", [])) or "no listed expertise"
|
| 266 |
-
rating = d.get("rating", "no rating")
|
| 267 |
-
docs.append(f"Consultant {name} with expertise in {expertise} and rating {rating}.")
|
| 268 |
-
|
| 269 |
-
# 3) programs
|
| 270 |
-
if role in ["admin", "operations", "funder", "incubatee"]:
|
| 271 |
-
for snap in fs.collection("programs").stream():
|
| 272 |
-
d = snap.to_dict()
|
| 273 |
-
docs.append(f"Program {d.get('name')} ({d.get('status')}): {d.get('type')} - Budget {d.get('budget')}")
|
| 274 |
-
|
| 275 |
-
# 4) interventions
|
| 276 |
-
if role in ["admin", "operations", "incubatee"]:
|
| 277 |
-
for snap in fs.collection("interventions").stream():
|
| 278 |
-
d = snap.to_dict()
|
| 279 |
-
for item in d.get('interventions', []):
|
| 280 |
-
title = item.get("title")
|
| 281 |
-
area = d.get("areaOfSupport", "General")
|
| 282 |
-
if title:
|
| 283 |
-
docs.append(f"Intervention: {title} under {area}.")
|
| 284 |
-
|
| 285 |
-
# 5) assignedInterventions
|
| 286 |
-
for snap in fs.collection("assignedInterventions").stream():
|
| 287 |
-
d = snap.to_dict()
|
| 288 |
-
if role == "consultant" and user_id not in d.get("consultantId", []):
|
| 289 |
-
continue
|
| 290 |
-
if role == "incubatee" and d.get("participantId") != user_id:
|
| 291 |
-
continue
|
| 292 |
-
title = d.get("interventionTitle", "Unknown")
|
| 293 |
-
sme = d.get("smeName", "Unknown")
|
| 294 |
-
status = d.get("status", "Unknown")
|
| 295 |
-
docs.append(f"Assigned intervention '{title}' for {sme} ({status})")
|
| 296 |
-
|
| 297 |
-
# 6) feedbacks
|
| 298 |
-
for snap in fs.collection("feedbacks").stream():
|
| 299 |
-
d = snap.to_dict()
|
| 300 |
-
if role == "consultant" and d.get("consultantId") != user_id:
|
| 301 |
-
continue
|
| 302 |
-
intervention = d.get("interventionTitle", "Unknown")
|
| 303 |
-
comment = d.get("comment")
|
| 304 |
-
if comment:
|
| 305 |
-
docs.append(f"Feedback on {intervention}: {comment}")
|
| 306 |
-
|
| 307 |
-
# 7) complianceDocuments
|
| 308 |
-
for snap in fs.collection("complianceDocuments").stream():
|
| 309 |
-
d = snap.to_dict()
|
| 310 |
-
if role == "incubatee" and d.get("participantId") != user_id:
|
| 311 |
-
continue
|
| 312 |
-
docs.append(f"Compliance document '{d.get('documentType')}' for {d.get('participantName')} is {d.get('status')} (expires {d.get('expiryDate')})")
|
| 313 |
-
|
| 314 |
-
# 8) interventionDatabase
|
| 315 |
-
if role in ["admin", "operations", "director", "funder"]:
|
| 316 |
-
for snap in fs.collection("interventionDatabase").stream():
|
| 317 |
-
d = snap.to_dict()
|
| 318 |
-
title = d.get("interventionTitle", "Unknown")
|
| 319 |
-
status = d.get("status", "Unknown")
|
| 320 |
-
feedback = d.get("feedback", "")
|
| 321 |
-
docs.append(f"Finalized intervention '{title}' ({status}): {feedback}")
|
| 322 |
-
|
| 323 |
-
return docs
|
| 324 |
-
|
| 325 |
-
# --- Embedding ---
|
| 326 |
-
def get_embeddings(texts: list[str]) -> list[list[float]]:
|
| 327 |
-
resp = client.models.embed_content(model="text-embedding-004", contents=texts)
|
| 328 |
-
return [emb.values for emb in resp.embeddings]
|
| 329 |
-
|
| 330 |
-
# --- Dynamic Index ---
|
| 331 |
-
def build_faiss_index(docs: list[str]):
|
| 332 |
-
embs = np.array(get_embeddings(docs), dtype="float32")
|
| 333 |
-
dim = embs.shape[1]
|
| 334 |
-
index = faiss.IndexFlatIP(dim)
|
| 335 |
-
index.add(embs)
|
| 336 |
-
return index
|
| 337 |
-
|
| 338 |
-
# --- Retrieval Helper ---
|
| 339 |
-
def retrieve_and_respond(user_query: str, role: str, user_id: str) -> str:
|
| 340 |
-
docs = fetch_documents(role, user_id)
|
| 341 |
-
if not docs:
|
| 342 |
-
return "No relevant data found for your role or access level."
|
| 343 |
-
|
| 344 |
-
index = build_faiss_index(docs)
|
| 345 |
-
q_emb = np.array(get_embeddings([user_query]), dtype="float32")
|
| 346 |
-
_, idxs = index.search(q_emb, 3)
|
| 347 |
-
ctx = "\n\n".join(docs[i] for i in idxs[0])
|
| 348 |
-
prompt = f"Use the context below to answer:\n\n{ctx}\n\nQuestion: {user_query}\nAnswer:"
|
| 349 |
-
chat = client.chats.create(model="gemini-2.0-flash-thinking-exp")
|
| 350 |
-
resp = chat.send_message(prompt)
|
| 351 |
-
return resp.text
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
# --------- Helpers for Bank-Statement Processing ---------
|
| 355 |
-
|
| 356 |
-
def read_pdf_pages(file_obj):
|
| 357 |
-
file_obj.seek(0)
|
| 358 |
-
reader = pypdf.PdfReader(file_obj)
|
| 359 |
-
return reader, len(reader.pages)
|
| 360 |
-
|
| 361 |
-
def extract_page_text(reader, page_num):
|
| 362 |
-
if page_num < len(reader.pages):
|
| 363 |
-
return reader.pages[page_num].extract_text() or ""
|
| 364 |
-
return ""
|
| 365 |
-
|
| 366 |
-
def process_with_gemini(text: str) -> str:
|
| 367 |
-
prompt = """Analyze this bank statement and extract transactions in JSON format with these fields:
|
| 368 |
-
- Date (format DD/MM/YYYY)
|
| 369 |
-
- Description
|
| 370 |
-
- Amount (just the integer value)
|
| 371 |
-
- Type (is 'income' if 'credit amount', else 'expense')
|
| 372 |
-
- Customer Name (Only If Type is 'income' and if no name is extracted write 'general income' and if type is not 'income' write 'expense')
|
| 373 |
-
- City (In address of bank statement)
|
| 374 |
-
- Category_of_expense (a string, if transaction 'Type' is 'expense' categorize it based on description into: Water and electricity, Salaries and wages, Repairs & Maintenance, Motor vehicle expenses, Projects Expenses, Hardware expenses, Refunds, Accounting fees, Loan interest, Bank charges, Insurance, SARS PAYE UIF, Advertising & Marketing, Logistics and distribution, Fuel, Website hosting fees, Rentals, Subscriptions, Computer internet and Telephone, Staff training, Travel and accommodation, Depreciation, Other expenses. If no category matches, default to 'Other expenses'. If 'Type' is 'income' set Destination_of_funds to 'income'.)
|
| 375 |
-
- ignore opening or closing balances, charts and analysis.
|
| 376 |
-
|
| 377 |
-
Return ONLY valid JSON with this structure:
|
| 378 |
-
{
|
| 379 |
-
"transactions": [
|
| 380 |
-
{
|
| 381 |
-
"Date": "string",
|
| 382 |
-
"Description": "string",
|
| 383 |
-
"Customer_name": "string",
|
| 384 |
-
"City": "string",
|
| 385 |
-
"Amount": number,
|
| 386 |
-
"Type": "string",
|
| 387 |
-
"Category_of_expense": "string"
|
| 388 |
-
}
|
| 389 |
-
]
|
| 390 |
-
}"""
|
| 391 |
-
try:
|
| 392 |
-
|
| 393 |
-
resp = client.models.generate_content(model='gemini-2.0-flash-thinking-exp', contents=[prompt, text])
|
| 394 |
-
time.sleep(6) # match your Streamlit rate-limit workaround
|
| 395 |
-
return resp.text
|
| 396 |
-
except Exception as e:
|
| 397 |
-
# retry once on 504
|
| 398 |
-
if hasattr(e, "response") and getattr(e.response, "status_code", None) == 504:
|
| 399 |
-
time.sleep(6)
|
| 400 |
-
resp = client.models.generate_content(model='gemini-2.0-flash-thinking-exp', contents=[prompt, text])
|
| 401 |
-
return resp.text
|
| 402 |
-
raise
|
| 403 |
-
|
| 404 |
-
def process_pdf_pages(pdf_file):
|
| 405 |
-
"""
|
| 406 |
-
Reads each page of the given PDF file, sends it through Gemini,
|
| 407 |
-
extracts the JSON “transactions” array, and returns the full list.
|
| 408 |
-
"""
|
| 409 |
-
reader, total_pages = read_pdf_pages(pdf_file)
|
| 410 |
-
all_txns = []
|
| 411 |
-
|
| 412 |
-
for pg in range(total_pages):
|
| 413 |
-
txt = extract_page_text(reader, pg).strip()
|
| 414 |
-
if not txt:
|
| 415 |
-
continue
|
| 416 |
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
except Exception:
|
| 421 |
-
# Skip this page on any error (including retries inside process_with_gemini)
|
| 422 |
-
continue
|
| 423 |
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
if start < 0 or end <= 0:
|
| 428 |
-
continue
|
| 429 |
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
data = json.loads(js)
|
| 434 |
-
except json.JSONDecodeError:
|
| 435 |
-
continue
|
| 436 |
-
|
| 437 |
-
# 4) Append all found transactions
|
| 438 |
-
txns = data.get("transactions", [])
|
| 439 |
-
if isinstance(txns, list):
|
| 440 |
-
all_txns.extend(txns)
|
| 441 |
-
|
| 442 |
-
return all_txns
|
| 443 |
|
| 444 |
-
# --------- Chat Endpoint ---------
|
| 445 |
@app.route("/chat", methods=["POST"])
|
| 446 |
-
def
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
except Exception as e:
|
| 459 |
-
return jsonify({"error": str(e)}), 500
|
| 460 |
-
|
| 461 |
-
# --------- Endpoint: Upload & Store Bank Statements ---------
|
| 462 |
-
|
| 463 |
-
@app.route("/upload_statements", methods=["POST"])
|
| 464 |
-
def upload_statements():
|
| 465 |
-
"""
|
| 466 |
-
Expects multipart/form-data:
|
| 467 |
-
- 'business_id': string
|
| 468 |
-
- 'files': one or more PDFs
|
| 469 |
-
Stores each PDF in Storage, extracts transactions, and writes them
|
| 470 |
-
to Firestore (collection 'transactions') with a 'business_id' tag.
|
| 471 |
-
"""
|
| 472 |
-
business_id = request.form.get("business_id", "").strip()
|
| 473 |
-
if not business_id:
|
| 474 |
-
return jsonify({"error": "Missing business_id"}), 400
|
| 475 |
-
|
| 476 |
-
if "files" not in request.files:
|
| 477 |
-
return jsonify({"error": "No files part; upload under key 'files'"}), 400
|
| 478 |
-
|
| 479 |
-
files = request.files.getlist("files")
|
| 480 |
-
if not files:
|
| 481 |
-
return jsonify({"error": "No files uploaded"}), 400
|
| 482 |
-
|
| 483 |
-
stored_count = 0
|
| 484 |
-
for f in files:
|
| 485 |
-
filename = f.filename or "statement.pdf"
|
| 486 |
-
# upload raw PDF to storage
|
| 487 |
-
dest_path = f"{business_id}/bank_statements/{datetime.utcnow().isoformat()}_{filename}"
|
| 488 |
-
blob = bucket.blob(dest_path)
|
| 489 |
-
f.seek(0)
|
| 490 |
-
blob.upload_from_file(f, content_type=f.content_type)
|
| 491 |
-
# rewind for processing
|
| 492 |
-
f.seek(0)
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
# extract + store transactions
|
| 496 |
-
txns= process_pdf_pages(f)
|
| 497 |
-
for txn in txns:
|
| 498 |
-
try:
|
| 499 |
-
dt = datetime.strptime(txn["Date"], "%d/%m/%Y")
|
| 500 |
-
except Exception:
|
| 501 |
-
dt = datetime.utcnow()
|
| 502 |
-
record = {
|
| 503 |
-
"business_id": business_id,
|
| 504 |
-
"Date": datetime.strptime(txn["Date"], "%d/%m/%Y"),
|
| 505 |
-
"Description": txn.get("Description", ""),
|
| 506 |
-
"Amount": txn.get("Amount", 0),
|
| 507 |
-
"Type": txn.get("Type", "expense"),
|
| 508 |
-
"Customer_name": txn.get("Customer_name",
|
| 509 |
-
"general income" if txn.get("Type")=="income" else "expense"),
|
| 510 |
-
"City": txn.get("City", ""),
|
| 511 |
-
"Category_of_expense": txn.get("Category_of_expense", "")
|
| 512 |
-
}
|
| 513 |
-
fs.collection("transactions").add(record)
|
| 514 |
-
stored_count += 1
|
| 515 |
-
|
| 516 |
-
return jsonify({"message": f"Stored {stored_count} transactions"}), 200
|
| 517 |
-
|
| 518 |
-
# --------- Endpoint: Retrieve or Generate Financial Statement ---------
|
| 519 |
-
|
| 520 |
-
@app.route("/financial_statement", methods=["POST"])
|
| 521 |
-
def financial_statement():
|
| 522 |
-
"""
|
| 523 |
-
Expects JSON:
|
| 524 |
-
{
|
| 525 |
-
"business_id": "...",
|
| 526 |
-
"start_date": "YYYY-MM-DD",
|
| 527 |
-
"end_date": "YYYY-MM-DD",
|
| 528 |
-
"statement_type": "Income Statement"|"Cashflow Statement"|"Balance Sheet"
|
| 529 |
-
}
|
| 530 |
-
If a cached report exists for that exact (business_id, start,end), returns it.
|
| 531 |
-
Otherwise generates via Gemini, returns it, and caches it in Firestore.
|
| 532 |
-
"""
|
| 533 |
-
data = request.get_json(force=True) or {}
|
| 534 |
-
biz = data.get("business_id", "").strip()
|
| 535 |
-
sd = data.get("start_date", "")
|
| 536 |
-
ed = data.get("end_date", "")
|
| 537 |
-
stype = data.get("statement_type", "Income Statement")
|
| 538 |
-
|
| 539 |
-
if not (biz and sd and ed):
|
| 540 |
-
return jsonify({"error": "Missing one of business_id, start_date, end_date"}), 400
|
| 541 |
-
|
| 542 |
-
# parse iso dates
|
| 543 |
-
try:
|
| 544 |
-
dt_start = datetime.fromisoformat(sd)
|
| 545 |
-
dt_end = datetime.fromisoformat(ed)
|
| 546 |
-
except ValueError:
|
| 547 |
-
return jsonify({"error": "Dates must be YYYY-MM-DD"}), 400
|
| 548 |
-
|
| 549 |
-
# check cache
|
| 550 |
-
doc_id = f"{biz}__{sd}__{ed}__{stype.replace(' ','_')}"
|
| 551 |
-
doc_ref = fs.collection("financial_statements").document(doc_id)
|
| 552 |
-
cached = doc_ref.get()
|
| 553 |
-
if cached.exists:
|
| 554 |
-
return jsonify({"report": cached.to_dict()["report"], "cached": True}), 200
|
| 555 |
-
|
| 556 |
-
# fetch transactions
|
| 557 |
-
snaps = (
|
| 558 |
-
fs.collection("transactions")
|
| 559 |
-
.where(filter=FieldFilter("business_id", "==", biz))
|
| 560 |
-
.where(filter=FieldFilter("Date", ">=", dt_start))
|
| 561 |
-
.where(filter=FieldFilter("Date", "<=", dt_end))
|
| 562 |
-
.stream()
|
| 563 |
)
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
ts = d.get("Date")
|
| 568 |
-
date_str = ts.strftime("%d/%m/%Y") if hasattr(ts, "strftime") else str(ts)
|
| 569 |
-
txns.append({
|
| 570 |
-
"Date": date_str,
|
| 571 |
-
"Description": d.get("Description",""),
|
| 572 |
-
"Amount": d.get("Amount",0),
|
| 573 |
-
"Type": d.get("Type",""),
|
| 574 |
-
"Customer_name": d.get("Customer_name",""),
|
| 575 |
-
"City": d.get("City",""),
|
| 576 |
-
"Category_of_expense": d.get("Category_of_expense","")
|
| 577 |
-
})
|
| 578 |
-
|
| 579 |
-
if not txns:
|
| 580 |
-
return jsonify({"error": "No transactions found for that period"}), 404
|
| 581 |
-
|
| 582 |
-
# generate with Gemini
|
| 583 |
-
prompt = (
|
| 584 |
-
f"Based on the following transactions JSON data:\n"
|
| 585 |
-
f"{json.dumps({'transactions': txns})}\n"
|
| 586 |
-
f"Generate a detailed {stype} for the period from "
|
| 587 |
-
f"{dt_start.strftime('%d/%m/%Y')} to {dt_end.strftime('%d/%m/%Y')} "
|
| 588 |
-
f"Specific Formatting and Content Requirements:"
|
| 589 |
-
f"Standard Accounting Structure (South Africa Focus): Organize the {stype} according to typical accounting practices followed in South Africa (e.g., for an Income Statement, clearly separate Revenue, Cost of Goods Sold, Gross Profit, Operating Expenses, and Net Income, in nice tables considering local terminology where applicable). If unsure of specific local variations, adhere to widely accepted international accounting structures."
|
| 590 |
-
f"Clear Headings and Subheadings: Use distinct and informative headings and subheadings in English to delineate different sections of the report. Ensure these are visually prominent."
|
| 591 |
-
f"Consistent Formatting: Maintain consistent formatting for monetary values (e.g., using 'R'for South African Rand if applicable and discernible from the data, comma separators for thousands), dates, and alignment."
|
| 592 |
-
f"Totals and Subtotals: Clearly display totals for relevant categories and subtotals where appropriate to provide a clear understanding of the financial performance or position."
|
| 593 |
-
f"Descriptive Line Items: Use clear and concise descriptions for each transaction or aggregated account based on the provided JSON data."
|
| 594 |
-
f"Key Insights: Include a brief section (e.g., 'Key Highlights' or 'Summary') that identifies significant trends, notable figures, or key performance indicators derived from the data within the statement. This should be written in plain, understandable English, potentially highlighting aspects particularly relevant to the economic context of Zimbabwe if discernible from the data."
|
| 595 |
-
f"Concise Summary: Provide a concluding summary paragraph that encapsulates the overall financial picture presented in the {stype}."
|
| 596 |
-
f"Format the report in Markdown for better visual structure."
|
| 597 |
-
f"Do not name the company if name is not there and return just the report and nothing else."
|
| 598 |
-
f"subtotals, totals, key highlights, and a concise summary."
|
| 599 |
)
|
| 600 |
-
chat = client.chats.create(model="gemini-2.0-flash")
|
| 601 |
-
resp = chat.send_message(prompt)
|
| 602 |
-
time.sleep(7)
|
| 603 |
-
report = resp.text
|
| 604 |
-
|
| 605 |
-
# cache it
|
| 606 |
-
doc_ref.set({
|
| 607 |
-
"business_id": biz,
|
| 608 |
-
"start_date": sd,
|
| 609 |
-
"end_date": ed,
|
| 610 |
-
"statement_type": stype,
|
| 611 |
-
"report": report,
|
| 612 |
-
"created_at": firestore.SERVER_TIMESTAMP
|
| 613 |
-
})
|
| 614 |
-
|
| 615 |
-
return jsonify({"report": report, "cached": False}), 200
|
| 616 |
-
|
| 617 |
-
# AI Screening endpoint
|
| 618 |
-
@app.route('/api/evaluate', methods=['POST'])
|
| 619 |
-
def evaluate_participant():
|
| 620 |
try:
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
|
|
|
|
|
|
| 631 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 632 |
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
"
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
except Exception as e:
|
| 642 |
-
return jsonify({
|
| 643 |
-
"status": "error",
|
| 644 |
-
"message": str(e)
|
| 645 |
-
}), 500
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
@app.route('/api/batch-evaluate', methods=['POST'])
|
| 649 |
-
def batch_evaluate():
|
| 650 |
-
try:
|
| 651 |
-
participants = request.json.get('participants', [])
|
| 652 |
-
results = []
|
| 653 |
-
|
| 654 |
-
evaluator = GenericEvaluator()
|
| 655 |
-
|
| 656 |
-
for item in participants:
|
| 657 |
-
participant_id = item.get("participantId")
|
| 658 |
-
participant_info = item.get("participantInfo", {})
|
| 659 |
-
prompt = evaluator.generate_prompt(participant_info)
|
| 660 |
-
|
| 661 |
-
response = client.models.generate_content(
|
| 662 |
-
model=model_name,
|
| 663 |
-
contents=prompt
|
| 664 |
-
)
|
| 665 |
-
|
| 666 |
-
evaluation = evaluator.parse_gemini_response(response.text)
|
| 667 |
-
|
| 668 |
-
results.append({
|
| 669 |
-
"participantId": participant_id,
|
| 670 |
-
"evaluation": evaluation
|
| 671 |
-
})
|
| 672 |
-
|
| 673 |
-
return jsonify({
|
| 674 |
-
"status": "success",
|
| 675 |
-
"evaluations": results
|
| 676 |
-
})
|
| 677 |
-
|
| 678 |
-
except Exception as e:
|
| 679 |
-
return jsonify({
|
| 680 |
-
"status": "error",
|
| 681 |
-
"message": str(e)
|
| 682 |
-
}), 500
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
@app.route('/api/shortlist', methods=['GET'])
|
| 686 |
-
def get_shortlist():
|
| 687 |
-
try:
|
| 688 |
-
# Placeholder logic
|
| 689 |
-
return jsonify({
|
| 690 |
-
"status": "success",
|
| 691 |
-
"shortlist": []
|
| 692 |
-
})
|
| 693 |
-
except Exception as e:
|
| 694 |
-
return jsonify({
|
| 695 |
-
"status": "error",
|
| 696 |
-
"message": str(e)
|
| 697 |
-
}), 500
|
| 698 |
-
|
| 699 |
|
|
|
|
|
|
|
|
|
|
| 700 |
|
| 701 |
|
| 702 |
-
# --------- Run the App ---------
|
| 703 |
if __name__ == "__main__":
|
|
|
|
| 704 |
app.run(host="0.0.0.0", port=7860, debug=True)
|
|
|
|
| 1 |
import os
|
| 2 |
import json
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
from flask import Flask, request, jsonify
|
| 4 |
from flask_cors import CORS
|
| 5 |
from dotenv import load_dotenv
|
| 6 |
|
| 7 |
+
# Firebase
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
from firebase_admin import credentials, firestore, storage, initialize_app
|
|
|
|
|
|
|
| 9 |
|
| 10 |
+
# Exa.ai
|
| 11 |
+
from exa_py import Exa
|
| 12 |
|
| 13 |
+
# Google GenAI (Gemini)
|
| 14 |
+
from google import genai
|
|
|
|
| 15 |
|
| 16 |
+
# (Optional) Faiss for behavioral embeddings
|
| 17 |
+
import faiss
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
+
# ─── Load environment ───────────────────────────────────────────────────────────
|
| 20 |
+
load_dotenv()
|
| 21 |
+
EXA_API_KEY = os.getenv("EXA_API_KEY")
|
| 22 |
+
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
| 23 |
+
FIREBASE_JSON = os.getenv("FIREBASE")
|
| 24 |
+
STORAGE_BUCKET = os.getenv("Firebase_Storage")
|
| 25 |
+
|
| 26 |
+
if not (EXA_API_KEY and GEMINI_API_KEY and FIREBASE_JSON and STORAGE_BUCKET):
|
| 27 |
+
raise RuntimeError("Missing one or more required env vars: EXA_API_KEY, GEMINI_API_KEY, FIREBASE, Firebase_Storage")
|
| 28 |
+
|
| 29 |
+
# ─── Initialize Firebase ───────────────────────────────────────────────────────
|
| 30 |
+
cred = credentials.Certificate(json.loads(FIREBASE_JSON))
|
| 31 |
+
initialize_app(cred, {"storageBucket": STORAGE_BUCKET})
|
| 32 |
+
fs = firestore.client()
|
| 33 |
bucket = storage.bucket()
|
| 34 |
|
| 35 |
+
# ─── Initialize Exa.ai ─────────────────────────────────────────────────────────
|
| 36 |
+
exa = Exa(EXA_API_KEY) # [oai_citation:0‡docs.exa.ai](https://docs.exa.ai/reference/quickstart)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
+
# ─── Initialize Gemini Client ─────────────────────────────────────────────────
|
| 39 |
+
client = genai.Client(api_key=GEMINI_API_KEY) # [oai_citation:1‡Google Cloud](https://cloud.google.com/vertex-ai/generative-ai/docs/sdks/overview?utm_source=chatgpt.com)
|
| 40 |
+
MODEL = "gemini-2.0-flash-001"
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
+
# ─── (Optional) Setup a Faiss index for future behavioral targeting ─────────────
|
| 43 |
+
# dim = 1536 # e.g., if you use a 1536‑dim embedding model
|
| 44 |
+
# faiss_index = faiss.IndexFlatL2(dim)
|
|
|
|
|
|
|
| 45 |
|
| 46 |
+
# ─── Flask App ────────────────────────────────────────────────────────────────
|
| 47 |
+
app = Flask(__name__)
|
| 48 |
+
CORS(app)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
|
|
|
| 50 |
@app.route("/chat", methods=["POST"])
|
| 51 |
+
def chat():
|
| 52 |
+
payload = request.get_json()
|
| 53 |
+
user_message = payload.get("message", "").strip()
|
| 54 |
+
user_ip = request.remote_addr or "0.0.0.0"
|
| 55 |
+
|
| 56 |
+
# 1️⃣ Classify intent
|
| 57 |
+
classify_prompt = (
|
| 58 |
+
"Categorize the user's intent into one or both:\n"
|
| 59 |
+
" - self-help\n"
|
| 60 |
+
" - product_search\n"
|
| 61 |
+
"Return as a JSON list, e.g.: [\"self-help\"] or [\"product_search\"] or [\"self-help\",\"product_search\"]\n\n"
|
| 62 |
+
f"User message: \"{user_message}\""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
)
|
| 64 |
+
classify_resp = client.models.generate_content(
|
| 65 |
+
model=MODEL,
|
| 66 |
+
contents=classify_prompt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
try:
|
| 69 |
+
intents = json.loads(classify_resp.text)
|
| 70 |
+
except json.JSONDecodeError:
|
| 71 |
+
# fallback: treat as a catch‑all
|
| 72 |
+
intents = ["self-help"]
|
| 73 |
+
|
| 74 |
+
response_parts = []
|
| 75 |
+
|
| 76 |
+
# 2️⃣ If self‑help requested, generate guidance
|
| 77 |
+
if "self-help" in intents:
|
| 78 |
+
help_prompt = (
|
| 79 |
+
"You are a friendly assistant. Provide clear, step‑by‑step guidance for:\n\n"
|
| 80 |
+
f"\"{user_message}\""
|
| 81 |
)
|
| 82 |
+
help_resp = client.models.generate_content(
|
| 83 |
+
model=MODEL,
|
| 84 |
+
contents=help_prompt
|
| 85 |
+
)
|
| 86 |
+
response_parts.append(help_resp.text.strip())
|
| 87 |
+
|
| 88 |
+
# Also dynamically lookup “what to buy” where applicable
|
| 89 |
+
# e.g. for “I want to bake a cake” → search “bake a cake ingredients”
|
| 90 |
+
search_query = user_message + " ingredients"
|
| 91 |
+
exa_results = exa.search_and_contents(
|
| 92 |
+
search_query,
|
| 93 |
+
type="auto",
|
| 94 |
+
text=True
|
| 95 |
+
)
|
| 96 |
+
# take top 3 links
|
| 97 |
+
links = [r.url for r in exa_results.results[:3]]
|
| 98 |
+
suggestion_text = (
|
| 99 |
+
"If you need to pick up ingredients, here are some helpful links:\n" +
|
| 100 |
+
"\n".join(f"- {url}" for url in links)
|
| 101 |
+
)
|
| 102 |
+
response_parts.append(suggestion_text)
|
| 103 |
+
|
| 104 |
+
# 3️⃣ If pure product_search requested, search Exa and wrap conversationally
|
| 105 |
+
if "product_search" in intents and "self-help" not in intents:
|
| 106 |
+
exa_results = exa.search_and_contents(
|
| 107 |
+
user_message,
|
| 108 |
+
type="auto",
|
| 109 |
+
text=True
|
| 110 |
+
)
|
| 111 |
+
links = [r.url for r in exa_results.results[:5]]
|
| 112 |
+
rec_prompt = (
|
| 113 |
+
"You are a helpful shopping assistant. A user is looking for:\n\n"
|
| 114 |
+
f"\"{user_message}\"\n\n"
|
| 115 |
+
"From these links:\n" +
|
| 116 |
+
"\n".join(links) +
|
| 117 |
+
"\n\n"
|
| 118 |
+
"Suggest the most relevant products in a friendly, conversational way."
|
| 119 |
+
)
|
| 120 |
+
rec_resp = client.models.generate_content(
|
| 121 |
+
model=MODEL,
|
| 122 |
+
contents=rec_prompt
|
| 123 |
+
)
|
| 124 |
+
response_parts.append(rec_resp.text.strip())
|
| 125 |
|
| 126 |
+
# 4️⃣ Fallback if nothing matched
|
| 127 |
+
if not response_parts:
|
| 128 |
+
default_resp = client.models.generate_content(
|
| 129 |
+
model=MODEL,
|
| 130 |
+
contents=f"Assist the user with: \"{user_message}\""
|
| 131 |
+
)
|
| 132 |
+
response_parts.append(default_resp.text.strip())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
+
# 5️⃣ Combine and return
|
| 135 |
+
final_text = "\n\n".join(response_parts)
|
| 136 |
+
return jsonify({"response": final_text})
|
| 137 |
|
| 138 |
|
|
|
|
| 139 |
if __name__ == "__main__":
|
| 140 |
+
# Host on all interfaces, port 7860
|
| 141 |
app.run(host="0.0.0.0", port=7860, debug=True)
|