Spaces:
Sleeping
Sleeping
Download app.py from UzoAgroAI/uzoagro-api: direct link, hf CLI and curl.
- Browser
- Download file 10.4 kB
-
https://huggingface.co/spaces/UzoAgroAI/uzoagro-api/resolve/main/app.py
- Command line
-
hf download hf://spaces/UzoAgroAI/uzoagro-api/app.py
-
curl -L -o app.py https://huggingface.co/spaces/UzoAgroAI/uzoagro-api/resolve/main/app.py
10.4 kB
| import logging | |
| import psycopg2 | |
| from psycopg2.errors import UniqueViolation | |
| import pandas as pd | |
| import requests | |
| import os | |
| from pathlib import Path | |
| from dotenv import load_dotenv | |
| import smtplib | |
| from email.mime.text import MIMEText | |
| from email.mime.multipart import MIMEMultipart | |
| from fastapi import FastAPI, HTTPException, UploadFile, File, BackgroundTasks, Depends | |
| from fastapi.responses import JSONResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.encoders import jsonable_encoder | |
| from pydantic import BaseModel | |
| from typing import Optional | |
| # Import our new database manager and the matching engine | |
| from database import get_db_connection, init_db | |
| from matching_engine import run_matching_engine | |
| from diagnostics_engine import run_botanical_diagnosis | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger("uzoagro-api") | |
| load_dotenv(Path(__file__).resolve().with_name('.env')) | |
| PAYSTACK_SECRET_KEY = os.getenv("PAYSTACK_SECRET_KEY") | |
| def verify_paystack_payment(reference: str) -> bool: | |
| """Securely asks Paystack if the transaction was actually successful.""" | |
| url = f"https://api.paystack.co/transaction/verify/{reference}" | |
| headers = { | |
| "Authorization": f"Bearer {PAYSTACK_SECRET_KEY}" | |
| } | |
| try: | |
| response = requests.get(url, headers=headers) | |
| if response.status_code == 200: | |
| data = response.json() | |
| # Double-check that the transaction status is literally "success" | |
| if data.get("data", {}).get("status") == "success": | |
| return True | |
| except Exception as e: | |
| print(f"Paystack verification error: {e}") | |
| return False | |
| app = FastAPI(title="UzoAgro AI Matching API", version="0.1.0") | |
| # Trace shares the existing application and PostgreSQL configuration. | |
| from trace_api import router as trace_router | |
| app.include_router(trace_router) | |
| from auth_api import router as auth_router | |
| from auth_api import farmer | |
| app.include_router(auth_router) | |
| from logistics_api import router as logistics_router | |
| app.include_router(logistics_router) | |
| # CORS config explicitly allowing your Vercel frontend (and everything else) to talk to this API | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # Expanded 15-City Geographic Dictionary | |
| CITIES = { | |
| # West / Mid-West | |
| "Lagos": (6.5244, 3.3792), "Ibadan": (7.3775, 3.9470), "Ilorin": (8.4799, 4.5418), "Akure": (7.2571, 5.2058), | |
| # North / Middle-Belt | |
| "Abuja": (9.0765, 7.3986), "Kano": (12.0022, 8.5920), "Kaduna": (10.5105, 7.4165), "Jos": (9.8965, 8.8583), | |
| "Sokoto": (13.0059, 5.2476), | |
| # East | |
| "Enugu": (6.4402, 7.4943), "Awka": (6.2100, 7.0700), "Onitsha": (6.1500, 6.7800), "Owerri": (5.4833, 7.0333), | |
| # South-South | |
| "Port Harcourt": (4.8156, 7.0498), "Uyo": (5.0380, 7.9098) | |
| } | |
| # --- Pydantic Data Models --- | |
| class DiagnosisRequest(BaseModel): | |
| crop_type: str | |
| symptoms: str | |
| class FarmerRequest(BaseModel): | |
| farmer_name: str | |
| pickup_city: str | |
| dropoff_city: str | |
| crop_type: str | |
| required_capacity: float | |
| requested_date: str | |
| class UserSignup(BaseModel): | |
| role: str | |
| name: str | |
| phone: str | |
| nin: str | |
| primary_city: str | |
| pin: str | |
| email: Optional[str] = None | |
| class UserLogin(BaseModel): | |
| phone: str | |
| pin: str | |
| # --- API Endpoints --- | |
| def startup_load_data(): | |
| """Ensure database is ready when the server starts.""" | |
| try: | |
| if os.getenv("DATABASE_URL"): | |
| init_db() | |
| else: | |
| logger.warning("DATABASE_URL is not configured; account endpoints will return 503.") | |
| except Exception as e: | |
| logger.exception("Failed to initialize database on startup") | |
| raise | |
| def read_root(): | |
| """Health check endpoint to prove the API is alive to Hugging Face.""" | |
| return {"message": "UzoAgro API is running smoothly!"} | |
| # ... (keep your existing imports and database connection setup) ... | |
| # 1. THE MOCK KYC FUNCTION (Place this above your routes) | |
| def verify_identity_mock(nin: str, name: str) -> bool: | |
| """ | |
| Simulates a call to Prembly/Dojah. | |
| In the future, we replace this logic with the real API request. | |
| """ | |
| # Simulate a small delay like a real server | |
| import time | |
| time.sleep(1) | |
| # Simple validation rule for testing: | |
| # If the NIN is exactly 11 digits, we assume it's valid. | |
| if len(nin) == 11 and nin.isdigit(): | |
| return True | |
| return False | |
| def send_welcome_email(user_email: str, user_name: str, role: str): | |
| """ | |
| Sends a welcome email via SMTP. | |
| Skips execution if credentials are missing or email is invalid. | |
| """ | |
| sender_email = os.getenv("EMAIL_SENDER") | |
| sender_password = os.getenv("EMAIL_PASSWORD") | |
| if not sender_email or not sender_password or not user_email: | |
| return | |
| msg = MIMEMultipart() | |
| msg['From'] = f"UzoAgro AI <{sender_email}>" | |
| msg['To'] = user_email | |
| msg['Subject'] = "Welcome to UzoAgro AI! 🌿" | |
| html_content = f""" | |
| <html> | |
| <body style="font-family: Arial, sans-serif; color: #143621;"> | |
| <div style="padding: 20px; border: 1px solid #e2e8f0; border-radius: 10px;"> | |
| <h2>Welcome, {user_name}!</h2> | |
| <p>You have successfully registered as a <strong>{role.capitalize()}</strong>.</p> | |
| <a href="https://uzoagro-ai.vercel.app/login.html" | |
| style="background-color: #D4ED6D; color: #143621; padding: 10px 20px; text-decoration: none; border-radius: 5px;"> | |
| Go to Dashboard | |
| </a> | |
| </div> | |
| </body> | |
| </html> | |
| """ | |
| msg.attach(MIMEText(html_content, 'html')) | |
| async def analyze_crop_image(file: UploadFile = File(...), owner=Depends(farmer)): | |
| """Receives a crop image from the frontend and passes it to the AI.""" | |
| try: | |
| # Read the image file into memory | |
| image_bytes = await file.read() | |
| # Pass the raw image to your friend's wrapper | |
| results = run_botanical_diagnosis(image_bytes) | |
| return JSONResponse(content=results) | |
| except Exception as e: | |
| logger.exception("Failed to process uploaded image.") | |
| raise HTTPException(status_code=500, detail="Image processing failed.") | |
| def match_custom_request(req: FarmerRequest, owner=Depends(farmer)): | |
| """Saves the request to the DB and returns the top 3 AI matches.""" | |
| req.farmer_name = owner['name'] | |
| if req.pickup_city not in CITIES or req.dropoff_city not in CITIES: | |
| raise HTTPException(status_code=400, detail="Invalid city selected") | |
| p_lat, p_lon = CITIES[req.pickup_city] | |
| d_lat, d_lon = CITIES[req.dropoff_city] | |
| # Generate a new unique request ID | |
| req_id = f"REQ-LIVE-{pd.Timestamp.now().strftime('%H%M%S')}" | |
| # 1. Save the new request permanently to the PostgreSQL database | |
| conn = get_db_connection() | |
| cursor = conn.cursor() | |
| # Crucial Fix: Changed SQLite's '?' to PostgreSQL's '%s' | |
| cursor.execute(''' | |
| INSERT INTO requests (request_id, sender_name, phone, pickup_city, pickup_lat, pickup_lon, | |
| dropoff_city, dropoff_lat, dropoff_lon, requested_date, required_capacity, | |
| crop_type) | |
| VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) | |
| ''', ( | |
| req_id, req.farmer_name, "PENDING", req.pickup_city, p_lat, p_lon, | |
| req.dropoff_city, d_lat, d_lon, req.requested_date, req.required_capacity, req.crop_type | |
| )) | |
| conn.commit() | |
| # 2. Safely pull drivers OR inject live demo data if DB is empty | |
| cursor.execute("SELECT * FROM drivers") | |
| rows = cursor.fetchall() | |
| if not rows: | |
| # EMERGENCY DEMO DATA: Guarantees your pitch works perfectly on stage! | |
| drivers_df = pd.DataFrame([ | |
| {"driver_id": "DRV-001", "driver_name": "Oluwaseun Logistics", "phone": "080111", "current_city": "Lagos", | |
| "current_lat": 6.5244, "current_lon": 3.3792, "home_base": "Kano", "capacity": 30.0, | |
| "allowed_crops": "Maize, Yam", "available_date": pd.Timestamp.now()}, | |
| {"driver_id": "DRV-002", "driver_name": "Northern Freight", "phone": "080222", "current_city": "Ibadan", | |
| "current_lat": 7.3775, "current_lon": 3.9470, "home_base": "Kano", "capacity": 15.0, | |
| "allowed_crops": "Maize, Tomatoes", "available_date": pd.Timestamp.now()}, | |
| {"driver_id": "DRV-003", "driver_name": "Emeka Transports", "phone": "080333", "current_city": "Abuja", | |
| "current_lat": 9.0765, "current_lon": 7.3986, "home_base": "Lagos", "capacity": 20.0, | |
| "allowed_crops": "Yam, Cassava", "available_date": pd.Timestamp.now()} | |
| ]) | |
| else: | |
| # The bulletproof way to read PostgreSQL into Pandas without SQLAlchemy | |
| colnames = [desc[0] for desc in cursor.description] | |
| drivers_df = pd.DataFrame(rows, columns=colnames) | |
| drivers_df["available_date"] = pd.to_datetime(drivers_df["available_date"]) | |
| conn.close() | |
| # 3. Create a DataFrame for this specific request to feed the AI Engine | |
| single_req_df = pd.DataFrame([{ | |
| "request_id": req_id, | |
| "pickup_lat": p_lat, | |
| "pickup_lon": p_lon, | |
| "dropoff_lat": d_lat, | |
| "dropoff_lon": d_lon, | |
| "requested_date": pd.to_datetime(req.requested_date), | |
| "required_capacity": req.required_capacity, | |
| "crop_type": req.crop_type | |
| }]) | |
| # 4. Run the matching intelligence | |
| try: | |
| matches_df = run_matching_engine(drivers_df, single_req_df, top_k=3) | |
| payload = jsonable_encoder(matches_df.to_dict(orient="records")) | |
| return JSONResponse(content=payload) | |
| except Exception as e: | |
| logger.exception("Error computing live matches") | |
| raise HTTPException(status_code=500, detail=str(e)) | |