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
9 changes: 9 additions & 0 deletions backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class Config:
# API Keys
gemini_api_key: str
telegram_bot_token: str
hf_token: Optional[str]

# Database
database_url: str
Expand Down Expand Up @@ -66,6 +67,8 @@ def from_env(cls) -> "Config":
if not telegram_bot_token:
errors.append("TELEGRAM_BOT_TOKEN is required")

hf_token = os.getenv("HF_TOKEN")

# Database with default
database_url = os.getenv(
"DATABASE_URL",
Expand Down Expand Up @@ -121,6 +124,7 @@ def from_env(cls) -> "Config":
return cls(
gemini_api_key=gemini_api_key,
telegram_bot_token=telegram_bot_token,
hf_token=hf_token,
database_url=database_url,
environment=environment,
debug=debug,
Expand Down Expand Up @@ -274,6 +278,11 @@ def get_telegram_bot_token() -> str:
return get_config().telegram_bot_token


def get_hf_token() -> Optional[str]:
"""Get Hugging Face token from config."""
return get_config().hf_token


def get_database_url() -> str:
"""Get database URL from config."""
return get_config().database_url
40 changes: 40 additions & 0 deletions backend/hf_api_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
# Speech-to-Text Model (Whisper)
WHISPER_API_URL = "https://router.huggingface.co/models/openai/whisper-large-v3-turbo"

# Zero-Shot Text Classification Model
ZERO_SHOT_TEXT_API_URL = "https://router.huggingface.co/models/facebook/bart-large-mnli"

async def _make_request(client, url, payload):
try:
response = await client.post(url, headers=headers, json=payload, timeout=20.0)
Expand Down Expand Up @@ -456,3 +459,40 @@ async def detect_abandoned_vehicle_clip(image: Union[Image.Image, bytes], client
labels = ["abandoned car", "rusted vehicle", "car with flat tires", "wrecked car", "normal parked car"]
targets = ["abandoned car", "rusted vehicle", "car with flat tires", "wrecked car"]
return await _detect_clip_generic(image, labels, targets, client)

async def classify_text_category(text: str, client: httpx.AsyncClient = None):
"""
Classifies text into civic issue categories using Zero-Shot Classification.
Returns the top category and confidence.
"""
labels = [
"pothole", "garbage dump", "water leak", "broken street light",
"broken infrastructure", "traffic congestion", "fire accident",
"stray animal threat", "fallen tree hazard", "pest infestation",
"clean area", "noise pollution"
]

payload = {
"inputs": text,
"parameters": {"candidate_labels": labels}
}

if client:
result = await _make_request(client, ZERO_SHOT_TEXT_API_URL, payload)
else:
async with httpx.AsyncClient() as new_client:
result = await _make_request(new_client, ZERO_SHOT_TEXT_API_URL, payload)

# Result format: {'sequence': '...', 'labels': ['pothole', ...], 'scores': [0.9, ...]}
if isinstance(result, dict) and 'labels' in result and 'scores' in result:
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 26, 2026

Choose a reason for hiding this comment

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

P2: Use .get() to safely ensure lists are non-empty before accessing index 0.

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

<comment>Use `.get()` to safely ensure lists are non-empty before accessing index 0.</comment>

<file context>
@@ -456,3 +459,40 @@ async def detect_abandoned_vehicle_clip(image: Union[Image.Image, bytes], client
+            result = await _make_request(new_client, ZERO_SHOT_TEXT_API_URL, payload)
+
+    # Result format: {'sequence': '...', 'labels': ['pothole', ...], 'scores': [0.9, ...]}
+    if isinstance(result, dict) and 'labels' in result and 'scores' in result:
+        top_label = result['labels'][0]
+        top_score = result['scores'][0]
</file context>
Suggested change
if isinstance(result, dict) and 'labels' in result and 'scores' in result:
if isinstance(result, dict) and result.get('labels') and result.get('scores'):
Fix with Cubic

top_label = result['labels'][0]
top_score = result['scores'][0]

Comment on lines +487 to +490
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard against empty labels / scores before indexing.

At Line 488 and Line 489, direct [0] access can raise on valid-but-empty API responses, turning recoverable cases into 500s.

💡 Suggested fix
-    if isinstance(result, dict) and 'labels' in result and 'scores' in result:
-        top_label = result['labels'][0]
-        top_score = result['scores'][0]
+    if (
+        isinstance(result, dict)
+        and isinstance(result.get("labels"), list)
+        and isinstance(result.get("scores"), list)
+        and len(result["labels"]) > 0
+        and len(result["scores"]) > 0
+    ):
+        top_label = result["labels"][0]
+        top_score = result["scores"][0]
📝 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
if isinstance(result, dict) and 'labels' in result and 'scores' in result:
top_label = result['labels'][0]
top_score = result['scores'][0]
if (
isinstance(result, dict)
and isinstance(result.get("labels"), list)
and isinstance(result.get("scores"), list)
and len(result["labels"]) > 0
and len(result["scores"]) > 0
):
top_label = result["labels"][0]
top_score = result["scores"][0]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/hf_api_service.py` around lines 487 - 490, The code currently assumes
result['labels'][0] and result['scores'][0] exist; guard against empty lists by
checking that result is a dict and that result.get('labels') and
result.get('scores') are non-empty (e.g. isinstance(...), and
len(result['labels'])>0 and len(result['scores'])>0) before indexing; if either
list is empty, set top_label/top_score to a safe default (None or a sentinel) or
return/raise a controlled error path so callers don’t get an unhandled
IndexError — update the block that assigns top_label and top_score to perform
these checks and handle the empty-case consistently.

# Simple mapping to internal categories if needed, or return raw
return {
"category": top_label,
"confidence": top_score,
"all_scores": dict(zip(result['labels'][:3], result['scores'][:3]))
}

return {"category": "unknown", "confidence": 0}
23 changes: 22 additions & 1 deletion backend/routers/analysis.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel
from typing import List, Optional, Any

from backend.priority_engine import priority_engine
from backend.hf_api_service import classify_text_category
from backend.dependencies import get_http_client

router = APIRouter()

Expand Down Expand Up @@ -36,3 +38,22 @@ def analyze_issue(request: AnalyzeIssueRequest):
suggested_categories=result["suggested_categories"],
reasoning=result["reasoning"]
)

class CategorySuggestionRequest(BaseModel):
text: str

@router.post("/api/suggest-category-text")
async def suggest_category_text(request: Request, body: CategorySuggestionRequest):
"""
Suggests a category based on the text description using Zero-Shot Classification.
"""
if not body.text or len(body.text) < 5:
raise HTTPException(status_code=400, detail="Text must be at least 5 characters long")

try:
client = get_http_client(request)
result = await classify_text_category(body.text, client=client)
Comment on lines +50 to +55
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Normalize input before length validation.

Line 50 validates untrimmed text, so whitespace-only payloads can pass and still hit the classifier.

💡 Suggested fix
-    if not body.text or len(body.text) < 5:
+    cleaned_text = body.text.strip()
+    if len(cleaned_text) < 5:
         raise HTTPException(status_code=400, detail="Text must be at least 5 characters long")
@@
-        result = await classify_text_category(body.text, client=client)
+        result = await classify_text_category(cleaned_text, client=client)
📝 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
if not body.text or len(body.text) < 5:
raise HTTPException(status_code=400, detail="Text must be at least 5 characters long")
try:
client = get_http_client(request)
result = await classify_text_category(body.text, client=client)
if not body.text:
raise HTTPException(status_code=400, detail="Text must be at least 5 characters long")
cleaned_text = body.text.strip()
if len(cleaned_text) < 5:
raise HTTPException(status_code=400, detail="Text must be at least 5 characters long")
try:
client = get_http_client(request)
result = await classify_text_category(cleaned_text, client=client)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/routers/analysis.py` around lines 50 - 55, The length check currently
uses the raw body.text so whitespace-only inputs bypass validation; update the
handler to trim the input (e.g., text_trimmed = body.text.strip()) before
validating length and raise HTTPException if len(text_trimmed) < 5, then pass
the trimmed text into classify_text_category; reference the existing
variables/functions get_http_client, classify_text_category, body.text and the
HTTPException raise to locate where to replace the checks and the argument
passed to the classifier.

return result
except Exception as e:
# Fallback or error
raise HTTPException(status_code=500, detail=f"Classification service unavailable: {str(e)}")
Comment on lines +57 to +59
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Do not expose raw exception text in API responses.

Line 59 returns str(e) to clients, which can leak internal/service details.

💡 Suggested fix
-    except Exception as e:
-        # Fallback or error
-        raise HTTPException(status_code=500, detail=f"Classification service unavailable: {str(e)}")
+    except Exception as e:
+        # log e server-side
+        raise HTTPException(status_code=500, detail="Classification service unavailable") from e
🧰 Tools
🪛 Ruff (0.15.2)

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

(BLE001)


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

(B904)


[warning] 59-59: Use explicit conversion flag

Replace with conversion flag

(RUF010)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/routers/analysis.py` around lines 57 - 59, The except block in
backend/routers/analysis.py currently raises HTTPException with the raw
exception text (str(e)), which can leak internal details; instead log the full
exception internally (e.g., logger.exception or capture traceback) and raise
HTTPException with a generic, non-sensitive message such as "Classification
service unavailable" (or include a short error ID if you want tracking),
replacing the use of str(e) in the raise HTTPException call while keeping the
internal logging of e for diagnostics.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 26, 2026

Choose a reason for hiding this comment

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

P2: Avoid returning raw exception details in the HTTP response; return a generic error message instead.

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

<comment>Avoid returning raw exception details in the HTTP response; return a generic error message instead.</comment>

<file context>
@@ -36,3 +38,22 @@ def analyze_issue(request: AnalyzeIssueRequest):
+        return result
+    except Exception as e:
+        # Fallback or error
+        raise HTTPException(status_code=500, detail=f"Classification service unavailable: {str(e)}")
</file context>
Suggested change
raise HTTPException(status_code=500, detail=f"Classification service unavailable: {str(e)}")
raise HTTPException(status_code=500, detail="Classification service unavailable")
Fix with Cubic

7 changes: 7 additions & 0 deletions backend/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ class ActionPlan(BaseModel):
class ChatRequest(BaseModel):
query: str = Field(..., min_length=1, max_length=1000, description="Chat query text")

@field_validator('query')
@classmethod
def validate_query(cls, v):
if not v.strip():
raise ValueError('Query cannot be empty or whitespace only')
return v.strip()

class ChatResponse(BaseModel):
response: str

Expand Down
160 changes: 0 additions & 160 deletions frontend/src/AccessibilityDetector.jsx

This file was deleted.

58 changes: 0 additions & 58 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,8 @@ const GrievanceView = React.lazy(() => import('./views/GrievanceView'));
const NotFound = React.lazy(() => import('./views/NotFound'));

// Lazy Load Detectors
const PotholeDetector = React.lazy(() => import('./PotholeDetector'));
const GarbageDetector = React.lazy(() => import('./GarbageDetector'));
const VandalismDetector = React.lazy(() => import('./VandalismDetector'));
const FloodDetector = React.lazy(() => import('./FloodDetector'));
const InfrastructureDetector = React.lazy(() => import('./InfrastructureDetector'));
const IllegalParkingDetector = React.lazy(() => import('./IllegalParkingDetector'));
const StreetLightDetector = React.lazy(() => import('./StreetLightDetector'));
const FireDetector = React.lazy(() => import('./FireDetector'));
const StrayAnimalDetector = React.lazy(() => import('./StrayAnimalDetector'));
const BlockedRoadDetector = React.lazy(() => import('./BlockedRoadDetector'));
const TreeDetector = React.lazy(() => import('./TreeDetector'));
const PestDetector = React.lazy(() => import('./PestDetector'));
const SmartScanner = React.lazy(() => import('./SmartScanner'));
const GrievanceAnalysis = React.lazy(() => import('./views/GrievanceAnalysis'));
const NoiseDetector = React.lazy(() => import('./NoiseDetector'));
const CivicEyeDetector = React.lazy(() => import('./CivicEyeDetector'));
const CivicInsight = React.lazy(() => import('./views/CivicInsight'));
const MyReportsView = React.lazy(() => import('./views/MyReportsView'));

Expand Down Expand Up @@ -296,52 +282,8 @@ function AppContent() {
}
/>
<Route path="/verify/:id" element={<VerifyView />} />
<Route path="/pothole" element={<PotholeDetector onBack={() => navigate('/')} />} />
<Route path="/garbage" element={<GarbageDetector onBack={() => navigate('/')} />} />
<Route
path="/vandalism"
element={
<div className="flex flex-col h-full">
<button onClick={() => navigate('/')} className="self-start text-blue-600 mb-2">
&larr; Back
</button>
<VandalismDetector />
</div>
}
/>
<Route
path="/flood"
element={
<div className="flex flex-col h-full">
<button onClick={() => navigate('/')} className="self-start text-blue-600 mb-2">
&larr; Back
</button>
<FloodDetector />
</div>
}
/>
<Route
path="/infrastructure"
element={<InfrastructureDetector onBack={() => navigate('/')} />}
/>
<Route path="/parking" element={<IllegalParkingDetector onBack={() => navigate('/')} />} />
<Route path="/streetlight" element={<StreetLightDetector onBack={() => navigate('/')} />} />
<Route path="/fire" element={<FireDetector onBack={() => navigate('/')} />} />
<Route path="/animal" element={<StrayAnimalDetector onBack={() => navigate('/')} />} />
<Route path="/blocked" element={<BlockedRoadDetector onBack={() => navigate('/')} />} />
<Route path="/tree" element={<TreeDetector onBack={() => navigate('/')} />} />
<Route path="/pest" element={<PestDetector onBack={() => navigate('/')} />} />
<Route path="/smart-scan" element={<SmartScanner onBack={() => navigate('/')} />} />
<Route path="/grievance-analysis" element={<GrievanceAnalysis onBack={() => navigate('/')} />} />
<Route path="/noise" element={<NoiseDetector onBack={() => navigate('/')} />} />
<Route path="/safety-check" element={
<div className="flex flex-col h-full p-4">
<button onClick={() => navigate('/')} className="self-start text-blue-600 mb-2 font-bold">
&larr; Back
</button>
<CivicEyeDetector onBack={() => navigate('/')} />
</div>
} />
<Route path="/my-reports" element={
<ProtectedRoute>
<MyReportsView />
Expand Down
Loading