rairo commited on
Commit
18a37d3
·
verified ·
1 Parent(s): 7374317

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +113 -676
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
- from firebase_admin import credentials, firestore, storage
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
- load_dotenv()
 
31
 
32
- # --- Flask Setup ---
33
- app = Flask(__name__)
34
- CORS(app)
35
 
36
- # --- Firebase Initialization ---
37
- cred_json = os.environ.get("FIREBASE")
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
- fs = firestore.client()
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  bucket = storage.bucket()
45
 
46
- # --- Gemini Client ---
47
- client = genai.Client(api_key=os.getenv("Gemini"))
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
- # 1) Call Gemini
418
- try:
419
- raw = process_with_gemini(txt)
420
- except Exception:
421
- # Skip this page on any error (including retries inside process_with_gemini)
422
- continue
423
 
424
- # 2) Locate the JSON payload
425
- start = raw.find("{")
426
- end = raw.rfind("}") + 1
427
- if start < 0 or end <= 0:
428
- continue
429
 
430
- # 3) Clean up any markdown fences and parse
431
- js = raw[start:end].replace("```json", "").replace("```", "")
432
- try:
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 chat_endpoint():
447
- data = request.get_json(force=True)
448
- q = data.get("user_query")
449
- role = data.get("role")
450
- user_id = data.get("user_id")
451
-
452
- if not q or not role or not user_id:
453
- return jsonify({"error": "Missing user_query, role, or user_id"}), 400
454
-
455
- try:
456
- reply = retrieve_and_respond(q, role.lower(), user_id)
457
- return jsonify({"reply": reply})
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
- txns = []
565
- for s in snaps:
566
- d = s.to_dict()
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
- data = request.json
622
- participant_id = data.get("participantId")
623
- participant_info = data.get("participantInfo", {})
624
-
625
- evaluator = GenericEvaluator()
626
- prompt = evaluator.generate_prompt(participant_info)
627
-
628
- response = client.models.generate_content(
629
- model=model_name,
630
- contents=prompt
 
 
631
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
632
 
633
- evaluation = evaluator.parse_gemini_response(response.text)
634
-
635
- return jsonify({
636
- "status": "success",
637
- "participantId": participant_id,
638
- "evaluation": evaluation
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)