Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions backend/ai_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
import os
from typing import Literal

from backend.ai_interfaces import ActionPlanService, ChatService, MLASummaryService
from backend.gemini_services import (
from ai_interfaces import ActionPlanService, ChatService, MLASummaryService
from gemini_services import (
create_gemini_action_plan_service,
create_gemini_chat_service,
create_gemini_mla_summary_service,
)
from backend.mock_services import (
from mock_services import (
create_mock_action_plan_service,
create_mock_chat_service,
create_mock_mla_summary_service,
Expand Down
4 changes: 2 additions & 2 deletions backend/ai_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@

import httpx

from backend.retry_utils import exponential_backoff_retry
from backend.exceptions import AIServiceException
from retry_utils import exponential_backoff_retry
from exceptions import AIServiceException

# Configure logging
logger = logging.getLogger(__name__)
Expand Down
4 changes: 2 additions & 2 deletions backend/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
import threading
from telegram import Update, ReplyKeyboardMarkup, ReplyKeyboardRemove
from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler, MessageHandler, filters, ConversationHandler
from backend.database import engine, SessionLocal
from database import engine, SessionLocal

from backend.models import Base, Issue
from models import Base, Issue


# Enable logging
Expand Down
8 changes: 4 additions & 4 deletions backend/escalation_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
from typing import List, Dict, Any, Optional
from sqlalchemy.orm import Session
from sqlalchemy import and_, or_
from backend.models import Grievance, Jurisdiction, EscalationAudit, GrievanceStatus, JurisdictionLevel, EscalationReason, SeverityLevel
from backend.database import SessionLocal
from backend.routing_service import RoutingService
from backend.sla_config_service import SLAConfigService
from models import Grievance, Jurisdiction, EscalationAudit, GrievanceStatus, JurisdictionLevel, EscalationReason, SeverityLevel
from database import SessionLocal
from routing_service import RoutingService
from sla_config_service import SLAConfigService

class EscalationEngine:
"""
Expand Down
2 changes: 1 addition & 1 deletion backend/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
import httpx

from backend.schemas import ErrorResponse
from schemas import ErrorResponse

logger = logging.getLogger(__name__)

Expand Down
29 changes: 22 additions & 7 deletions backend/flood_detection.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from hf_service import client
from PIL import Image
import io
import httpx
from hf_service import query_hf_api
import asyncio

_hf_client = httpx.AsyncClient()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Repeated /api/detect-flooding requests can silently return no detections after the first request because _hf_client is reused across event loops that asyncio.run() closes. Scope an AsyncClient to each coroutine run (or own it in one long-lived event loop) instead of sharing this instance across those runs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/flood_detection.py, line 7:

<comment>Repeated `/api/detect-flooding` requests can silently return no detections after the first request because `_hf_client` is reused across event loops that `asyncio.run()` closes. Scope an `AsyncClient` to each coroutine run (or own it in one long-lived event loop) instead of sharing this instance across those runs.</comment>

<file context>
@@ -1,6 +1,10 @@
+from hf_service import query_hf_api
+import asyncio
+
+_hf_client = httpx.AsyncClient()
 
 def detect_flooding(image: Image.Image):
</file context>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The module-level _hf_client = httpx.AsyncClient() is never explicitly closed. In a long-running server process, this can trigger unclosed-transport warnings and leak connection-pool resources on shutdown. Since flood_detection.py is a synchronous module that wraps async HF calls, consider managing the client through the application's lifespan (e.g., a startup/shutdown hook) or using async with at the call site when possible, to ensure connections are properly cleaned up.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/flood_detection.py, line 7:

<comment>The module-level `_hf_client = httpx.AsyncClient()` is never explicitly closed. In a long-running server process, this can trigger unclosed-transport warnings and leak connection-pool resources on shutdown. Since `flood_detection.py` is a synchronous module that wraps async HF calls, consider managing the client through the application's lifespan (e.g., a startup/shutdown hook) or using `async with` at the call site when possible, to ensure connections are properly cleaned up.</comment>

<file context>
@@ -1,6 +1,10 @@
+from hf_service import query_hf_api
+import asyncio
+
+_hf_client = httpx.AsyncClient()
 
 def detect_flooding(image: Image.Image):
</file context>

Comment on lines +3 to +7

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== File map ==\n'
git ls-files backend | sed -n '1,200p'

printf '\n== Relevant symbols in backend/flood_detection.py ==\n'
ast-grep outline backend/flood_detection.py --view expanded || true

printf '\n== Read backend/flood_detection.py ==\n'
wc -l backend/flood_detection.py
cat -n backend/flood_detection.py | sed -n '1,240p'

printf '\n== Find query_hf_api definition and uses ==\n'
rg -n "def query_hf_api|query_hf_api\(" backend . -g '!**/.git/**' || true

printf '\n== Read hf_service module ==\n'
fd -a "hf_service.py" . || true
for f in $(fd -a "hf_service.py" .); do
  echo "--- $f"
  wc -l "$f"
  cat -n "$f" | sed -n '1,260p'
done

printf '\n== Search for AsyncClient lifecycle/close usage ==\n'
rg -n "AsyncClient\(|aclose\(|close\(" backend . -g '!**/.git/**' || true

printf '\n== Search for asyncio.run and thread usage in backend ==\n'
rg -n "asyncio\.run|threading|Thread\(" backend -g '!**/.git/**' || true

Repository: RohanExploit/VishwaGuru

Length of output: 23576


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Uses of detect_flooding ==\n'
rg -n "\bdetect_flooding\b" backend tests -g '!**/.git/**' || true

printf '\n== Uses of flood_detection module ==\n'
rg -n "\bflood_detection\b" backend tests -g '!**/.git/**' || true

printf '\n== Search for any close/aclose on _hf_client or flood client ==\n'
rg -n "_hf_client|flood.*aclose|flood.*close|query_hf_api\(.*_hf_client" backend tests -g '!**/.git/**' || true

Repository: RohanExploit/VishwaGuru

Length of output: 1616


🏁 Script executed:

#!/bin/bash
set -euo pipefail

wc -l backend/main.py
cat -n backend/main.py | sed -n '430,475p'

printf '\n== flood_detection import context ==\n'
cat -n backend/main.py | sed -n '35,55p'

Repository: RohanExploit/VishwaGuru

Length of output: 3333


Avoid reusing _hf_client across asyncio.run calls
backend/flood_detection.py:7 creates one httpx.AsyncClient() at import, but /api/detect-flooding reaches this path through run_in_threadpool and then asyncio.run(...) per request. That can cross event-loop boundaries, and the client is never closed. Create the client per call (or let query_hf_api manage it) and close it explicitly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/flood_detection.py` around lines 3 - 7, The module-level _hf_client
in flood detection is reused across separate asyncio.run event loops and never
closed. Remove the global client, update the relevant
flooding-detection/query_hf_api call path to create an httpx.AsyncClient within
the coroutine’s event loop, and close it explicitly with async context
management (or delegate lifecycle management to query_hf_api).


def detect_flooding(image: Image.Image):
"""
Expand All @@ -15,12 +19,23 @@ def detect_flooding(image: Image.Image):
image.save(img_byte_arr, format=image.format if image.format else 'JPEG')
img_byte_arr = img_byte_arr.getvalue()

# Using the same model as vandalism for consistency and caching benefits
results = client.zero_shot_image_classification(
image=img_byte_arr,
labels=labels,
model="openai/clip-vit-base-patch32"
)
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = None

if loop and loop.is_running():
# If we're already in an async context, this synchronous function won't work easily with our async HF client wrapper.
# But we can try to fall back or spawn a thread if really needed. For now, try running synchronously using asyncio.run if possible.
import threading
def _run():
return asyncio.run(query_hf_api(img_byte_arr, labels, _hf_client))
# Just do a blocking call inside a thread
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
results = pool.submit(_run).result()
else:
results = asyncio.run(query_hf_api(img_byte_arr, labels, _hf_client))
Comment on lines +22 to +38

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## backend/flood_detection.py (outline)\n'
ast-grep outline backend/flood_detection.py --view expanded || true

printf '\n## relevant lines\n'
nl -ba backend/flood_detection.py | sed -n '1,220p'

printf '\n## search for _hf_client and query_hf_api\n'
rg -n "_hf_client|query_hf_api|ThreadPoolExecutor|asyncio.run|get_running_loop" backend -S

Repository: RohanExploit/VishwaGuru

Length of output: 368


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

path = Path("backend/flood_detection.py")
text = path.read_text()
lines = text.splitlines()
for i, line in enumerate(lines, 1):
    if 1 <= i <= 220:
        print(f"{i:4d}: {line}")
PY

printf '\n## references\n'
rg -n "_hf_client|query_hf_api|detect_flooding|ThreadPoolExecutor|asyncio.run|get_running_loop" backend -S

Repository: RohanExploit/VishwaGuru

Length of output: 6111


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

for path_str in ["backend/hf_service.py", "backend/main.py"]:
    path = Path(path_str)
    print(f"\n## {path_str}")
    text = path.read_text().splitlines()
    for i, line in enumerate(text, 1):
        if path_str.endswith("hf_service.py") and 1 <= i <= 260:
            print(f"{i:4d}: {line}")
        elif path_str.endswith("main.py") and 430 <= i <= 490:
            print(f"{i:4d}: {line}")
PY

Repository: RohanExploit/VishwaGuru

Length of output: 10915


🌐 Web query:

httpx.AsyncClient share across event loops threads documentation

💡 Result:

You cannot share an httpx.AsyncClient instance across multiple event loops [1][2][3]. While AsyncClient is designed to be task-safe within a single event loop [4][5], it is strictly bound to the event loop in which it was created [1][2]. Attempting to reuse an AsyncClient across different event loops (for instance, by calling asyncio.run multiple times with the same client instance) will typically result in a RuntimeError: Event loop is closed, because the underlying connection pool and transport layer cannot be shared across different loop contexts [1][2]. Key guidelines for usage: 1. One Client Per Loop: If your application uses multiple event loops or runs in environments like AWS Lambda where the event loop may be recreated across invocations, you must ensure that each invocation or loop has its own dedicated AsyncClient instance [2][6]. 2. Task Safety: Within a single event loop, AsyncClient is safe to be shared across multiple concurrent tasks (e.g., using asyncio.gather or TaskGroup) [5][7]. This is the recommended pattern to benefit from connection pooling [8][9]. 3. Synchronous vs Asynchronous: If you have a requirement to share a client across multiple threads in a way that is not bound to a single async event loop, consider using the synchronous httpx.Client, which is thread-safe and designed for such scenarios [4][3][10]. In summary, always associate the lifetime of your AsyncClient with the lifetime of the event loop that creates it [6]. Using the async context manager (async with httpx.AsyncClient as client:) is the best practice to ensure proper resource management and lifetime scoping [6][8].

Citations:


Avoid reusing _hf_client across asyncio.run calls. httpx.AsyncClient is bound to the event loop that created it, so the module-level client can fail here with RuntimeError when detect_flooding() is executed in the threadpool and then calls asyncio.run(...). Create a fresh client inside the coroutine (or make the whole path async and scope the client to that loop) instead of sharing _hf_client.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/flood_detection.py` around lines 22 - 38, Stop reusing the
module-level `_hf_client` across event loops in `detect_flooding()` and
`query_hf_api`. Create and scope a fresh `httpx.AsyncClient` within each
coroutine/event loop, pass it only for that call, and close it afterward; update
the synchronous and threaded `asyncio.run` paths accordingly.


# Filter for flooding related
flood_labels = ["flooded street", "waterlogging", "submerged car"]
Expand Down
2 changes: 1 addition & 1 deletion backend/flooding_detection.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from PIL import Image
from backend.local_ml_service import detect_flooding_local
from local_ml_service import detect_flooding_local

async def detect_flooding(image: Image.Image):
"""
Expand Down
8 changes: 4 additions & 4 deletions backend/gemini_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
"""
from typing import Dict, Optional
import asyncio
from backend.ai_interfaces import ActionPlanService, ChatService, MLASummaryService
from backend.ai_service import (
from ai_interfaces import ActionPlanService, ChatService, MLASummaryService
from ai_service import (
generate_action_plan as _generate_action_plan,
chat_with_civic_assistant as _chat_with_civic_assistant
)
from backend.gemini_summary import generate_mla_summary as _generate_mla_summary
from backend.exceptions import AIServiceException
from gemini_summary import generate_mla_summary as _generate_mla_summary
from exceptions import AIServiceException


class GeminiActionPlanService(ActionPlanService):
Expand Down
2 changes: 1 addition & 1 deletion backend/gemini_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

import httpx

from backend.retry_utils import exponential_backoff_retry
from retry_utils import exponential_backoff_retry

# Configure logging
logger = logging.getLogger(__name__)
Expand Down
10 changes: 5 additions & 5 deletions backend/grievance_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
from sqlalchemy.orm import Session, joinedload
from datetime import datetime, timezone, timedelta

from backend.models import Grievance, Jurisdiction, GrievanceStatus, SeverityLevel
from backend.database import SessionLocal
from backend.routing_service import RoutingService
from backend.sla_config_service import SLAConfigService
from backend.escalation_engine import EscalationEngine
from models import Grievance, Jurisdiction, GrievanceStatus, SeverityLevel
from database import SessionLocal
from routing_service import RoutingService
from sla_config_service import SLAConfigService
from escalation_engine import EscalationEngine

class GrievanceService:
"""
Expand Down
8 changes: 8 additions & 0 deletions backend/hf_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
import io
img_byte_arr = io.BytesIO()
image.save(img_byte_arr, format='JPEG')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Saving PIL Image as JPEG without RGB conversion will crash for images with alpha channels (RGBA, P, LA modes). Call image.convert('RGB') before saving to handle all common image modes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/hf_service.py, line 95:

<comment>Saving PIL Image as JPEG without RGB conversion will crash for images with alpha channels (RGBA, P, LA modes). Call `image.convert('RGB')` before saving to handle all common image modes.</comment>

<file context>
@@ -88,6 +88,14 @@ async def _make_request(client, image_bytes, labels):
+        return image
+    import io
+    img_byte_arr = io.BytesIO()
+    image.save(img_byte_arr, format='JPEG')
+    return img_byte_arr.getvalue()
+
</file context>
Suggested change
image.save(img_byte_arr, format='JPEG')
image.convert('RGB').save(img_byte_arr, format='JPEG')

return img_byte_arr.getvalue()
Comment on lines 90 to +96

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== backend/hf_service.py excerpt ==\n'
nl -ba backend/hf_service.py | sed -n '1,180p'

printf '\n== usages of _prepare_image_bytes ==\n'
rg -n "_prepare_image_bytes|detect_vandalism_clip|Image\.open|convert\\('RGB'\\)|convert\\(\"RGB\"\\)" backend -S

Repository: RohanExploit/VishwaGuru

Length of output: 238


🏁 Script executed:

#!/bin/bash
set -euo pipefail

nl -ba backend/hf_service.py | sed -n '1,180p'
rg -n "_prepare_image_bytes|detect_vandalism_clip|convert\\('RGB'\\)|convert\\(\"RGB\"\\)" backend -S

Repository: RohanExploit/VishwaGuru

Length of output: 201


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== backend/hf_service.py excerpt ==\n'
cat -n backend/hf_service.py | sed -n '1,180p'

printf '\n== usages of _prepare_image_bytes / image conversion ==\n'
rg -n "_prepare_image_bytes|detect_vandalism_clip|convert\\('RGB'\\)|convert\\(\"RGB\"\\)" backend -S

Repository: RohanExploit/VishwaGuru

Length of output: 9741


Convert images to RGB before JPEG encoding. image.save(..., format='JPEG') can raise for RGBA/P images, and this helper is still reachable from backend/unified_detection_service.py without a prior RGB conversion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/hf_service.py` around lines 90 - 96, The _prepare_image_bytes
function must convert non-byte images to RGB before JPEG encoding. Update the
PIL image handling to call convert("RGB") before save, preserving the existing
bytes passthrough.


async def detect_vandalism_clip(image: Union[Image.Image, bytes], client: httpx.AsyncClient) -> List[Dict[str, Any]]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0: The new detect_vandalism_clip function declares client as a required parameter with no default, but UnifiedDetectionService.detect_vandalism() calls it as detect_vandalism_clip(image) without the argument. This will raise a TypeError at runtime whenever the huggingface backend is used for vandalism detection. The sibling functions detect_infrastructure_clip and detect_flooding_clip use client=None as a default, so making client optional here would be consistent and fix the mismatch.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/hf_service.py, line 98:

<comment>The new `detect_vandalism_clip` function declares `client` as a required parameter with no default, but `UnifiedDetectionService.detect_vandalism()` calls it as `detect_vandalism_clip(image)` without the argument. This will raise a `TypeError` at runtime whenever the huggingface backend is used for vandalism detection. The sibling functions `detect_infrastructure_clip` and `detect_flooding_clip` use `client=None` as a default, so making `client` optional here would be consistent and fix the mismatch.</comment>

<file context>
@@ -88,6 +88,14 @@ async def _make_request(client, image_bytes, labels):
+    image.save(img_byte_arr, format='JPEG')
+    return img_byte_arr.getvalue()
+
+async def detect_vandalism_clip(image: Union[Image.Image, bytes], client: httpx.AsyncClient) -> List[Dict[str, Any]]:
     """
     Detects vandalism/graffiti using Zero-Shot Image Classification with CLIP (Async).
</file context>
Suggested change
async def detect_vandalism_clip(image: Union[Image.Image, bytes], client: httpx.AsyncClient) -> List[Dict[str, Any]]:
async def detect_vandalism_clip(image: Union[Image.Image, bytes], client: httpx.AsyncClient = None) -> List[Dict[str, Any]]:

"""
Detects vandalism/graffiti using Zero-Shot Image Classification with CLIP (Async).
Includes retry logic with exponential backoff for transient failures.
Expand Down
2 changes: 1 addition & 1 deletion backend/infrastructure_detection.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from backend.local_ml_service import detect_infrastructure_local
from local_ml_service import detect_infrastructure_local
from PIL import Image

async def detect_infrastructure(image: Image.Image):
Expand Down
2 changes: 1 addition & 1 deletion backend/init_db.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from sqlalchemy import text
from backend.database import engine
from database import engine
import logging

logger = logging.getLogger(__name__)
Expand Down
8 changes: 4 additions & 4 deletions backend/init_grievance_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
Sets up sample jurisdictions, SLA configurations, and test data.
"""

from backend.database import SessionLocal, engine
from backend.models import Jurisdiction, JurisdictionLevel, SLAConfig, SeverityLevel
from backend.grievance_service import GrievanceService
from database import SessionLocal, engine
from models import Jurisdiction, JurisdictionLevel, SLAConfig, SeverityLevel
from grievance_service import GrievanceService
import json

def initialize_grievance_system():
"""
Initialize the grievance system with sample data.
"""
# Create tables
from backend.models import Base
from models import Base
Base.metadata.create_all(bind=engine)

db = SessionLocal()
Expand Down
2 changes: 1 addition & 1 deletion backend/local_ml_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import threading
from fastapi.concurrency import run_in_threadpool

from backend.exceptions import DetectionException
from exceptions import DetectionException

# Configure logging
logger = logging.getLogger(__name__)
Expand Down
90 changes: 40 additions & 50 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import sys
from fastapi import FastAPI, UploadFile, File, Form, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.concurrency import run_in_threadpool
Expand All @@ -11,8 +12,10 @@
import json
import os
import io
from functools import lru_cache

# Add the project root to sys.path so we can import 'backend' modules
import sys
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from fastapi import FastAPI, HTTPException, UploadFile, File, Form, BackgroundTasks, Depends, Query
Expand All @@ -21,7 +24,9 @@
from fastapi.concurrency import run_in_threadpool
from pydantic import BaseModel
from sqlalchemy.orm import Session
from schemas import StatsResponse, MLStatusResponse
from database import SessionLocal, engine, Base
from ai_factory import create_all_ai_services
from models import Issue
from contextlib import asynccontextmanager
import shutil
Expand Down Expand Up @@ -65,9 +70,9 @@ async def lifespan(app: FastAPI):
try:
load_maharashtra_pincode_data()
load_maharashtra_mla_data()
logger.info("Maharashtra data pre-loaded successfully.")
print("Maharashtra data pre-loaded successfully.")
except Exception as e:
logger.error(f"Error pre-loading Maharashtra data: {e}")
print(f"Error pre-loading Maharashtra data: {e}")

# Run database migrations
try:
Expand Down Expand Up @@ -133,27 +138,18 @@ def read_root():
"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)
@app.get("/health")
def health():
return HealthResponse(
status="healthy",
timestamp=datetime.now(timezone.utc),
version="1.0.0",
services={
from datetime import datetime, timezone
return {
"status": "healthy",
"timestamp": datetime.now(timezone.utc).isoformat(),
"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)):
Expand Down Expand Up @@ -234,45 +230,39 @@ 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP '^\s*import\s+asyncio\b|^\s*from\s+asyncio\b' backend/main.py

Repository: RohanExploit/VishwaGuru

Length of output: 161


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- imports / top of file ---'
sed -n '1,80p' backend/main.py

echo
echo '--- around first call site ---'
sed -n '220,245p' backend/main.py

echo
echo '--- around second call site ---'
sed -n '248,265p' backend/main.py

Repository: RohanExploit/VishwaGuru

Length of output: 4584


Import asyncio before calling to_thread
backend/main.py uses asyncio.to_thread(...) at lines 236 and 255, but there’s no import asyncio in the module. That will raise NameError when this endpoint runs; add the missing import near the top.

🧰 Tools
🪛 Ruff (0.15.20)

[error] 236-236: Undefined name asyncio

(F821)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/main.py` at line 236, Add the missing `asyncio` import near the top
of `backend/main.py` so the `asyncio.to_thread` calls in the endpoint execute
without `NameError`.

Source: Linters/SAST tools


# 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)
# Offload blocking DB operations to a thread
def save_to_db():
new_issue = Issue(
description=description,
category=category,
image_path=file_location,
source=source,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Web clients can now forge source (for example, telegram), corrupting source-based issue data and analytics. Keep this endpoint's persisted source server-controlled unless source is authenticated by a trusted integration.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/main.py, line 252:

<comment>Web clients can now forge `source` (for example, `telegram`), corrupting source-based issue data and analytics. Keep this endpoint's persisted source server-controlled unless source is authenticated by a trusted integration.</comment>

<file context>
@@ -234,45 +235,39 @@ async def create_issue(
+                description=description,
+                category=category,
+                image_path=file_location,
+                source=source,
+                user_email=user_email
+            )
</file context>
Suggested change
source=source,
source="web",

user_email=user_email
)
db.add(new_issue)
db.commit()
db.refresh(new_issue)
return new_issue

return {
"id": new_issue.id,
"message": "Issue reported successfully",
"action_plan": action_plan
}
new_issue = await asyncio.to_thread(save_to_db)

return {
"id": new_issue.id,
"message": "Issue reported successfully",
"action_plan": action_plan
}
except Exception as e:
import traceback
traceback.print_exc()
raise HTTPException(status_code=500, detail=str(e))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Failed issue submissions expose raw internal exception text to callers. Return a generic 500 detail and retain traceback logging server-side.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/main.py, line 270:

<comment>Failed issue submissions expose raw internal exception text to callers. Return a generic 500 detail and retain traceback logging server-side.</comment>

<file context>
@@ -234,45 +235,39 @@ async def create_issue(
+    except Exception as e:
+        import traceback
+        traceback.print_exc()
+        raise HTTPException(status_code=500, detail=str(e))
 
 @lru_cache(maxsize=1)
</file context>
Suggested change
raise HTTPException(status_code=500, detail=str(e))
raise HTTPException(status_code=500, detail="Unable to create issue")

Comment on lines +262 to +265

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Avoid leaking raw exception text to clients and preserve the cause chain.

detail=str(e) returns internal error messages (paths, DB errors) to the API caller — an information-disclosure risk. Also re-raise with from e to keep the traceback context (Ruff B904). Log the detail server-side and return a generic message.

🛡️ Proposed change
     except Exception as e:
-        import traceback
-        traceback.print_exc()
-        raise HTTPException(status_code=500, detail=str(e))
+        logger.exception("Failed to create issue")
+        raise HTTPException(status_code=500, detail="Internal server error") from e
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
except Exception as e:
import traceback
traceback.print_exc()
raise HTTPException(status_code=500, detail=str(e))
except Exception as e:
logger.exception("Failed to create issue")
raise HTTPException(status_code=500, detail="Internal server error") from e
🧰 Tools
🪛 Ruff (0.15.20)

[warning] 262-262: Do not catch blind exception: Exception

(BLE001)


[warning] 265-265: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/main.py` around lines 262 - 265, Update the exception handler in the
relevant endpoint to log the original exception details server-side, then raise
HTTPException with a generic client-safe message instead of detail=str(e); use
“raise ... from e” to preserve the cause chain and satisfy Ruff B904, replacing
the direct traceback.print_exc() approach as appropriate.

Source: Linters/SAST tools


@lru_cache(maxsize=1)
def _load_responsibility_map():
Expand Down
Loading
Loading