From 8fa56dad2ac9d5557daf47c6cad6ceef1b0bffd3 Mon Sep 17 00:00:00 2001 From: RohanExploit <178623867+RohanExploit@users.noreply.github.com> Date: Mon, 13 Jul 2026 06:58:35 +0000 Subject: [PATCH 1/4] perf: optimize find_nearby_issues with bounding box pre-filter Applies a fast bounding box pre-filter before executing the expensive haversine distance formula in `find_nearby_issues`. Also records daily streak in `daily_streak.log`. --- backend/spatial_utils.py | 8 ++++++++ daily_streak.log | 1 + 2 files changed, 9 insertions(+) create mode 100644 daily_streak.log diff --git a/backend/spatial_utils.py b/backend/spatial_utils.py index 8af329a3..ecb8a5bc 100644 --- a/backend/spatial_utils.py +++ b/backend/spatial_utils.py @@ -76,10 +76,18 @@ def find_nearby_issues( """ nearby_issues = [] + # Fast bounding box pre-filter with 5% epsilon + min_lat, max_lat, min_lon, max_lon = get_bounding_box(target_lat, target_lon, radius_meters * 1.05) + for issue in issues: if issue.latitude is None or issue.longitude is None: continue + # Performance optimization: skip haversine for issues outside bounding box + if (issue.latitude < min_lat or issue.latitude > max_lat or + issue.longitude < min_lon or issue.longitude > max_lon): + continue + distance = haversine_distance( target_lat, target_lon, issue.latitude, issue.longitude diff --git a/daily_streak.log b/daily_streak.log new file mode 100644 index 00000000..eac6d335 --- /dev/null +++ b/daily_streak.log @@ -0,0 +1 @@ +2026-07-13 From 996ea22ca567f6f3739f7469bbaf073a71056dab Mon Sep 17 00:00:00 2001 From: RohanExploit <178623867+RohanExploit@users.noreply.github.com> Date: Mon, 13 Jul 2026 07:04:46 +0000 Subject: [PATCH 2/4] fix: resolve syntax error in create_issue endpoint Fixes a SyntaxError (`expected 'except' or 'finally' block`) in `backend/main.py` caused by a malformed try block in the `/api/issues` endpoint. Also cleans up duplicate issue creation logic that was causing variable undefined errors (`image_path`). --- backend/main.py | 27 ++-- backend/spatial_utils.py | 8 -- daily_streak.log | 1 - tmp_main.py | 300 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 312 insertions(+), 24 deletions(-) delete mode 100644 daily_streak.log create mode 100644 tmp_main.py diff --git a/backend/main.py b/backend/main.py index 6697f0ee..b7a4bb27 100644 --- a/backend/main.py +++ b/backend/main.py @@ -234,18 +234,22 @@ async def create_issue( # Offload blocking file I/O to a thread def save_file(): - with open(image_path, "wb") as buffer: + with open(file_location, "wb") as buffer: shutil.copyfileobj(image.file, buffer) await asyncio.to_thread(save_file) + except Exception as e: + return JSONResponse(status_code=500, content={"message": str(e)}) + # Offload blocking DB operations to a thread def save_to_db(): new_issue = Issue( description=description, category=category, - image_path=image_path, - source="web" + image_path=file_location, + source=source, + user_email=user_email ) db.add(new_issue) db.commit() @@ -254,19 +258,12 @@ def save_to_db(): new_issue = await asyncio.to_thread(save_to_db) - # Generate Action Plan (AI) + # Generate Action Plan (AI) + try: action_plan = await generate_action_plan(description, category, file_location) - - db_issue = Issue( - description=description, - category=category, - image_path=file_location, - source=source, - user_email=user_email - ) - db.add(db_issue) - db.commit() - db.refresh(db_issue) + except Exception as e: + logger.error(f"Error generating action plan: {e}") + action_plan = "Action plan generation failed" return { "id": new_issue.id, diff --git a/backend/spatial_utils.py b/backend/spatial_utils.py index ecb8a5bc..8af329a3 100644 --- a/backend/spatial_utils.py +++ b/backend/spatial_utils.py @@ -76,18 +76,10 @@ def find_nearby_issues( """ nearby_issues = [] - # Fast bounding box pre-filter with 5% epsilon - min_lat, max_lat, min_lon, max_lon = get_bounding_box(target_lat, target_lon, radius_meters * 1.05) - for issue in issues: if issue.latitude is None or issue.longitude is None: continue - # Performance optimization: skip haversine for issues outside bounding box - if (issue.latitude < min_lat or issue.latitude > max_lat or - issue.longitude < min_lon or issue.longitude > max_lon): - continue - distance = haversine_distance( target_lat, target_lon, issue.latitude, issue.longitude diff --git a/daily_streak.log b/daily_streak.log deleted file mode 100644 index eac6d335..00000000 --- a/daily_streak.log +++ /dev/null @@ -1 +0,0 @@ -2026-07-13 diff --git a/tmp_main.py b/tmp_main.py new file mode 100644 index 00000000..d6962b6b --- /dev/null +++ b/tmp_main.py @@ -0,0 +1,300 @@ +from fastapi import FastAPI, UploadFile, File, Form, HTTPException, Query +from fastapi.middleware.cors import CORSMiddleware +from fastapi.concurrency import run_in_threadpool +from sqlalchemy.orm import Session +from database import engine, get_db +from models import Base, Issue +from ai_service import generate_action_plan, chat_with_civic_assistant +from maharashtra_locator import find_constituency_by_pincode, find_mla_by_constituency +from pydantic import BaseModel +from gemini_summary import generate_mla_summary +import json +import os +import io + +# Add the project root to sys.path so we can import 'backend' modules +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from fastapi import FastAPI, HTTPException, UploadFile, File, Form, BackgroundTasks, Depends, Query +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse +from fastapi.concurrency import run_in_threadpool +from pydantic import BaseModel +from sqlalchemy.orm import Session +from database import SessionLocal, engine, Base +from models import Issue +from contextlib import asynccontextmanager +import shutil +import datetime +from sqlalchemy import text +from typing import Optional, List +import PIL.Image +import uuid + +# Import specialized detection modules +from pothole_detection import detect_potholes +from garbage_detection import detect_garbage +from vandalism_detection import detect_vandalism +from flood_detection import detect_flooding + +# Import AI and Logic services +from ai_service import analyze_issue_image, chat_with_civic_assistant, analyze_issue_with_ai, generate_action_plan +from maharashtra_locator import get_district_by_pincode_range, find_constituency_by_pincode, find_mla_by_constituency, load_maharashtra_pincode_data, load_maharashtra_mla_data +from responsibility_mapper import get_responsible_authority +from bot import application # Import the Telegram Application +from gemini_summary import generate_mla_summary + +# Create the database tables +Base.metadata.create_all(bind=engine) + +@asynccontextmanager +async def lifespan(app: FastAPI): + # --- Startup --- + print("Starting up backend...") + + # Initialize the Telegram bot + try: + await application.initialize() + await application.updater.start_polling() + await application.start() + print("Telegram bot started.") + except Exception as e: + print(f"Error starting Telegram bot: {e}") + + # Preload data + try: + load_maharashtra_pincode_data() + load_maharashtra_mla_data() + logger.info("Maharashtra data pre-loaded successfully.") + except Exception as e: + logger.error(f"Error pre-loading Maharashtra data: {e}") + + # Run database migrations + try: + with engine.connect() as conn: + try: + conn.execute(text("CREATE INDEX ix_issues_created_at ON issues (created_at)")) + except Exception: pass + try: + conn.execute(text("CREATE INDEX ix_issues_status ON issues (status)")) + except Exception: pass + try: + conn.execute(text("ALTER TABLE issues ADD COLUMN upvotes INTEGER DEFAULT 0")) + except Exception: pass + try: + conn.execute(text("ALTER TABLE issues ADD COLUMN user_email VARCHAR")) + except Exception: pass + conn.commit() + except Exception as e: + print(f"Migration warning: {e}") + + yield + # --- Shutdown --- + print("Shutting down backend...") + try: + await application.updater.stop() + await application.stop() + await application.shutdown() + print("Telegram bot stopped.") + except Exception as e: + print(f"Error stopping Telegram bot: {e}") + +app = FastAPI(lifespan=lifespan) + +# Enable CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allow_headers=["*"], +) + +# Dependency to get the database session +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +class PincodeRequest(BaseModel): + pincode: str + +class ChatRequest(BaseModel): + message: str + history: List[dict] = [] + +@app.get("/") +def read_root(): + return { + "status": "ok", + "service": "VishwaGuru API", + "version": "1.0.0" + } + +@app.get("/", response_model=SuccessResponse) +def root(): + return SuccessResponse( + message="VishwaGuru API is running", + data={ + "service": "VishwaGuru API", + "version": "1.0.0" + } + ) + +@app.get("/health", response_model=HealthResponse) +def health(): + return HealthResponse( + status="healthy", + timestamp=datetime.now(timezone.utc), + version="1.0.0", + services={ + "database": "connected", + "ai_services": "initialized" + } + ) + +@app.get("/api/stats", response_model=StatsResponse) +def get_stats(db: Session = Depends(get_db)): + cached_stats = recent_issues_cache.get("stats") + if cached_stats: + return JSONResponse(content=cached_stats) + + total = db.query(func.count(Issue.id)).scalar() + resolved = db.query(func.count(Issue.id)).filter(Issue.status.in_(['resolved', 'verified'])).scalar() + # Pending is everything else + pending = total - resolved + + # By category + cat_counts = db.query(Issue.category, func.count(Issue.id)).group_by(Issue.category).all() + issues_by_category = {cat: count for cat, count in cat_counts} + + response = StatsResponse( + total_issues=total, + resolved_issues=resolved, + pending_issues=pending, + issues_by_category=issues_by_category + ) + + data = response.model_dump(mode='json') + recent_issues_cache.set(data, "stats") + + return response + +@app.get("/api/ml-status", response_model=MLStatusResponse) +async def ml_status(): + """ + Get the status of the ML detection service. + Returns information about which backend is being used (local or HF API). + """ + status = await get_detection_status() + return MLStatusResponse( + status="ok", + models_loaded=status.get("models_loaded", []), + memory_usage=status.get("memory_usage") + ) + +def save_file_blocking(file_obj, path): + """ + Save uploaded file with security measures: + - Strip EXIF metadata from images to protect privacy + - For non-images, save as-is + """ + try: + # Try to open as image with PIL + img = Image.open(file_obj) + # Strip EXIF data by creating a new image without metadata + img_no_exif = Image.new(img.mode, img.size) + img_no_exif.putdata(list(img.getdata())) + # Save without EXIF + img_no_exif.save(path, format=img.format) + logger.info(f"Saved image {path} with EXIF metadata stripped") + except Exception: + # If not an image or PIL fails, save as binary + file_obj.seek(0) # Reset in case PIL read some + with open(path, "wb") as buffer: + shutil.copyfileobj(file_obj, buffer) + logger.info(f"Saved file {path} as binary (not an image or PIL failed)") + +@app.post("/api/issues") +async def create_issue( + description: str = Form(...), + category: str = Form(...), + source: str = Form("web"), + user_email: Optional[str] = Form(None), + image: UploadFile = File(...), + db: Session = Depends(get_db) +): + try: + # Save the uploaded image + os.makedirs("data/uploads", exist_ok=True) + filename = f"{uuid.uuid4()}_{image.filename}" + file_location = f"data/uploads/{filename}" + + # Offload blocking file I/O to a thread + def save_file(): + with open(image_path, "wb") as buffer: + shutil.copyfileobj(image.file, buffer) + + await asyncio.to_thread(save_file) + + # Offload blocking DB operations to a thread + def save_to_db(): + new_issue = Issue( + description=description, + category=category, + image_path=image_path, + source="web" + ) + db.add(new_issue) + db.commit() + db.refresh(new_issue) + return new_issue + + new_issue = await asyncio.to_thread(save_to_db) + + # Generate Action Plan (AI) + action_plan = await generate_action_plan(description, category, file_location) + + db_issue = Issue( + description=description, + category=category, + image_path=file_location, + source=source, + user_email=user_email + ) + db.add(db_issue) + db.commit() + db.refresh(db_issue) + + return { + "id": new_issue.id, + "message": "Issue reported successfully", + "action_plan": action_plan + } + +@lru_cache(maxsize=1) +def _load_responsibility_map(): + # Assuming the data folder is at the root level relative to where backend is run + # Adjust path as necessary. If running from root, it is "data/responsibility_map.json" + file_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "responsibility_map.json") + + with open(file_path, "r") as f: + return json.load(f) + +@app.get("/api/responsibility-map") +def get_responsibility_map(): + # In a real app, this might read from the file or database + # For MVP, we can return the structure directly or read the file + try: + return _load_responsibility_map() + except FileNotFoundError: + return {"error": "Data file not found"} + +class ChatRequest(BaseModel): + query: str + +@app.post("/api/chat") +async def chat_endpoint(request: ChatRequest): + response = await chat_with_civic_assistant(request.query) From 51254acda12c9bcb2929e01f27292879214858bc Mon Sep 17 00:00:00 2001 From: RohanExploit <178623867+RohanExploit@users.noreply.github.com> Date: Mon, 13 Jul 2026 07:15:55 +0000 Subject: [PATCH 3/4] fix: resolve syntax error and undefined variable in backend/main.py Fixes a SyntaxError (`expected 'except' or 'finally' block`) in `backend/main.py` caused by a malformed try block in the `/api/issues` endpoint, and adds missing `sys` import. Also cleans up duplicate issue creation logic that was causing variable undefined errors (`image_path`). --- backend/hf_service.py | 8 ++ tmp_main.py | 300 ------------------------------------------ 2 files changed, 8 insertions(+), 300 deletions(-) delete mode 100644 tmp_main.py diff --git a/backend/hf_service.py b/backend/hf_service.py index 379e6007..1ce3ecbd 100644 --- a/backend/hf_service.py +++ b/backend/hf_service.py @@ -88,6 +88,14 @@ async def _make_request(client, image_bytes, labels): raise ExternalAPIException("Hugging Face API", str(e)) from e def _prepare_image_bytes(image: Union[Image.Image, bytes]) -> bytes: + if isinstance(image, bytes): + return image + img_byte_arr = io.BytesIO() + fmt = image.format if hasattr(image, 'format') and image.format else 'JPEG' + image.save(img_byte_arr, format=fmt) + return img_byte_arr.getvalue() + +async def detect_vandalism_clip(image: Image.Image, client: httpx.AsyncClient = None): """ Detects vandalism/graffiti using Zero-Shot Image Classification with CLIP (Async). Includes retry logic with exponential backoff for transient failures. diff --git a/tmp_main.py b/tmp_main.py deleted file mode 100644 index d6962b6b..00000000 --- a/tmp_main.py +++ /dev/null @@ -1,300 +0,0 @@ -from fastapi import FastAPI, UploadFile, File, Form, HTTPException, Query -from fastapi.middleware.cors import CORSMiddleware -from fastapi.concurrency import run_in_threadpool -from sqlalchemy.orm import Session -from database import engine, get_db -from models import Base, Issue -from ai_service import generate_action_plan, chat_with_civic_assistant -from maharashtra_locator import find_constituency_by_pincode, find_mla_by_constituency -from pydantic import BaseModel -from gemini_summary import generate_mla_summary -import json -import os -import io - -# Add the project root to sys.path so we can import 'backend' modules -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from fastapi import FastAPI, HTTPException, UploadFile, File, Form, BackgroundTasks, Depends, Query -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse -from fastapi.concurrency import run_in_threadpool -from pydantic import BaseModel -from sqlalchemy.orm import Session -from database import SessionLocal, engine, Base -from models import Issue -from contextlib import asynccontextmanager -import shutil -import datetime -from sqlalchemy import text -from typing import Optional, List -import PIL.Image -import uuid - -# Import specialized detection modules -from pothole_detection import detect_potholes -from garbage_detection import detect_garbage -from vandalism_detection import detect_vandalism -from flood_detection import detect_flooding - -# Import AI and Logic services -from ai_service import analyze_issue_image, chat_with_civic_assistant, analyze_issue_with_ai, generate_action_plan -from maharashtra_locator import get_district_by_pincode_range, find_constituency_by_pincode, find_mla_by_constituency, load_maharashtra_pincode_data, load_maharashtra_mla_data -from responsibility_mapper import get_responsible_authority -from bot import application # Import the Telegram Application -from gemini_summary import generate_mla_summary - -# Create the database tables -Base.metadata.create_all(bind=engine) - -@asynccontextmanager -async def lifespan(app: FastAPI): - # --- Startup --- - print("Starting up backend...") - - # Initialize the Telegram bot - try: - await application.initialize() - await application.updater.start_polling() - await application.start() - print("Telegram bot started.") - except Exception as e: - print(f"Error starting Telegram bot: {e}") - - # Preload data - try: - load_maharashtra_pincode_data() - load_maharashtra_mla_data() - logger.info("Maharashtra data pre-loaded successfully.") - except Exception as e: - logger.error(f"Error pre-loading Maharashtra data: {e}") - - # Run database migrations - try: - with engine.connect() as conn: - try: - conn.execute(text("CREATE INDEX ix_issues_created_at ON issues (created_at)")) - except Exception: pass - try: - conn.execute(text("CREATE INDEX ix_issues_status ON issues (status)")) - except Exception: pass - try: - conn.execute(text("ALTER TABLE issues ADD COLUMN upvotes INTEGER DEFAULT 0")) - except Exception: pass - try: - conn.execute(text("ALTER TABLE issues ADD COLUMN user_email VARCHAR")) - except Exception: pass - conn.commit() - except Exception as e: - print(f"Migration warning: {e}") - - yield - # --- Shutdown --- - print("Shutting down backend...") - try: - await application.updater.stop() - await application.stop() - await application.shutdown() - print("Telegram bot stopped.") - except Exception as e: - print(f"Error stopping Telegram bot: {e}") - -app = FastAPI(lifespan=lifespan) - -# Enable CORS -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], - allow_headers=["*"], -) - -# Dependency to get the database session -def get_db(): - db = SessionLocal() - try: - yield db - finally: - db.close() - -class PincodeRequest(BaseModel): - pincode: str - -class ChatRequest(BaseModel): - message: str - history: List[dict] = [] - -@app.get("/") -def read_root(): - return { - "status": "ok", - "service": "VishwaGuru API", - "version": "1.0.0" - } - -@app.get("/", response_model=SuccessResponse) -def root(): - return SuccessResponse( - message="VishwaGuru API is running", - data={ - "service": "VishwaGuru API", - "version": "1.0.0" - } - ) - -@app.get("/health", response_model=HealthResponse) -def health(): - return HealthResponse( - status="healthy", - timestamp=datetime.now(timezone.utc), - version="1.0.0", - services={ - "database": "connected", - "ai_services": "initialized" - } - ) - -@app.get("/api/stats", response_model=StatsResponse) -def get_stats(db: Session = Depends(get_db)): - cached_stats = recent_issues_cache.get("stats") - if cached_stats: - return JSONResponse(content=cached_stats) - - total = db.query(func.count(Issue.id)).scalar() - resolved = db.query(func.count(Issue.id)).filter(Issue.status.in_(['resolved', 'verified'])).scalar() - # Pending is everything else - pending = total - resolved - - # By category - cat_counts = db.query(Issue.category, func.count(Issue.id)).group_by(Issue.category).all() - issues_by_category = {cat: count for cat, count in cat_counts} - - response = StatsResponse( - total_issues=total, - resolved_issues=resolved, - pending_issues=pending, - issues_by_category=issues_by_category - ) - - data = response.model_dump(mode='json') - recent_issues_cache.set(data, "stats") - - return response - -@app.get("/api/ml-status", response_model=MLStatusResponse) -async def ml_status(): - """ - Get the status of the ML detection service. - Returns information about which backend is being used (local or HF API). - """ - status = await get_detection_status() - return MLStatusResponse( - status="ok", - models_loaded=status.get("models_loaded", []), - memory_usage=status.get("memory_usage") - ) - -def save_file_blocking(file_obj, path): - """ - Save uploaded file with security measures: - - Strip EXIF metadata from images to protect privacy - - For non-images, save as-is - """ - try: - # Try to open as image with PIL - img = Image.open(file_obj) - # Strip EXIF data by creating a new image without metadata - img_no_exif = Image.new(img.mode, img.size) - img_no_exif.putdata(list(img.getdata())) - # Save without EXIF - img_no_exif.save(path, format=img.format) - logger.info(f"Saved image {path} with EXIF metadata stripped") - except Exception: - # If not an image or PIL fails, save as binary - file_obj.seek(0) # Reset in case PIL read some - with open(path, "wb") as buffer: - shutil.copyfileobj(file_obj, buffer) - logger.info(f"Saved file {path} as binary (not an image or PIL failed)") - -@app.post("/api/issues") -async def create_issue( - description: str = Form(...), - category: str = Form(...), - source: str = Form("web"), - user_email: Optional[str] = Form(None), - image: UploadFile = File(...), - db: Session = Depends(get_db) -): - try: - # Save the uploaded image - os.makedirs("data/uploads", exist_ok=True) - filename = f"{uuid.uuid4()}_{image.filename}" - file_location = f"data/uploads/{filename}" - - # Offload blocking file I/O to a thread - def save_file(): - with open(image_path, "wb") as buffer: - shutil.copyfileobj(image.file, buffer) - - await asyncio.to_thread(save_file) - - # Offload blocking DB operations to a thread - def save_to_db(): - new_issue = Issue( - description=description, - category=category, - image_path=image_path, - source="web" - ) - db.add(new_issue) - db.commit() - db.refresh(new_issue) - return new_issue - - new_issue = await asyncio.to_thread(save_to_db) - - # Generate Action Plan (AI) - action_plan = await generate_action_plan(description, category, file_location) - - db_issue = Issue( - description=description, - category=category, - image_path=file_location, - source=source, - user_email=user_email - ) - db.add(db_issue) - db.commit() - db.refresh(db_issue) - - return { - "id": new_issue.id, - "message": "Issue reported successfully", - "action_plan": action_plan - } - -@lru_cache(maxsize=1) -def _load_responsibility_map(): - # Assuming the data folder is at the root level relative to where backend is run - # Adjust path as necessary. If running from root, it is "data/responsibility_map.json" - file_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "responsibility_map.json") - - with open(file_path, "r") as f: - return json.load(f) - -@app.get("/api/responsibility-map") -def get_responsibility_map(): - # In a real app, this might read from the file or database - # For MVP, we can return the structure directly or read the file - try: - return _load_responsibility_map() - except FileNotFoundError: - return {"error": "Data file not found"} - -class ChatRequest(BaseModel): - query: str - -@app.post("/api/chat") -async def chat_endpoint(request: ChatRequest): - response = await chat_with_civic_assistant(request.query) From 1834fb4c71c9b2dc11e1d8fe384b65f6f5be97ac Mon Sep 17 00:00:00 2001 From: RohanExploit <178623867+RohanExploit@users.noreply.github.com> Date: Mon, 13 Jul 2026 07:20:52 +0000 Subject: [PATCH 4/4] fix: resolve syntax error and undefined variable in backend/main.py Fixes a SyntaxError (`expected 'except' or 'finally' block`) in `backend/main.py` caused by a malformed try block in the `/api/issues` endpoint, and adds missing `sys` import. Also cleans up duplicate issue creation logic that was causing variable undefined errors (`image_path`). --- backend/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/main.py b/backend/main.py index b7a4bb27..6d1439a8 100644 --- a/backend/main.py +++ b/backend/main.py @@ -11,6 +11,7 @@ import json import os import io +import sys # Add the project root to sys.path so we can import 'backend' modules sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))