-
Notifications
You must be signed in to change notification settings - Fork 36
⚡ Bolt: Optimized Blockchain for Grievances #566
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a0e5c7a
75ed835
46a56bc
9225705
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| # Modular routers for VishwaGuru Backend | ||
|
|
||
| # Fix for googletrans compatibility with newer httpcore (Issue #290) | ||
| # This monkeypatch must happen before any imports of googletrans or httpx | ||
| try: | ||
| import httpcore | ||
| if not hasattr(httpcore, "SyncHTTPTransport"): | ||
| httpcore.SyncHTTPTransport = object | ||
| except ImportError: | ||
| pass |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,11 +5,13 @@ | |
|
|
||
| import json | ||
| import uuid | ||
| import hashlib | ||
| from typing import Dict, Any, Optional, List | ||
| from sqlalchemy.orm import Session, joinedload | ||
| from datetime import datetime, timezone, timedelta | ||
|
|
||
| from backend.models import Grievance, Jurisdiction, GrievanceStatus, SeverityLevel, Issue | ||
| from backend.cache import grievance_last_hash_cache | ||
| from backend.database import SessionLocal | ||
| from backend.routing_service import RoutingService | ||
| from backend.sla_config_service import SLAConfigService | ||
|
|
@@ -84,6 +86,19 @@ def create_grievance(self, grievance_data: Dict[str, Any], db: Session = None) - | |
| # Generate unique ID | ||
| unique_id = str(uuid.uuid4())[:8].upper() | ||
|
|
||
| # Blockchain chaining logic (Issue #290 optimization) | ||
| # Performance Boost: Use thread-safe cache to eliminate DB query for last hash | ||
| prev_hash = grievance_last_hash_cache.get("last_hash") | ||
| if prev_hash is None: | ||
| # Cache miss: Fetch only the last hash from DB | ||
| prev_grievance = db.query(Grievance.integrity_hash).order_by(Grievance.id.desc()).first() | ||
| prev_hash = prev_grievance[0] if prev_grievance and prev_grievance[0] else "" | ||
| grievance_last_hash_cache.set(data=prev_hash, key="last_hash") | ||
|
Comment on lines
+89
to
+96
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The last-hash cache breaks chain correctness across workers.
Also applies to: 132-133 🤖 Prompt for AI Agents |
||
|
|
||
| # SHA-256 chaining based on key grievance fields | ||
| hash_content = f"{unique_id}|{grievance_data.get('category', 'general')}|{severity.value}|{prev_hash}" | ||
| integrity_hash = hashlib.sha256(hash_content.encode()).hexdigest() | ||
|
Comment on lines
+89
to
+100
|
||
|
|
||
| # Extract location data | ||
| location_data = grievance_data.get('location', {}) | ||
| latitude = location_data.get('latitude') if isinstance(location_data, dict) else None | ||
|
|
@@ -106,11 +121,17 @@ def create_grievance(self, grievance_data: Dict[str, Any], db: Session = None) - | |
| assigned_authority=assigned_authority, | ||
| sla_deadline=sla_deadline, | ||
| status=GrievanceStatus.OPEN, | ||
| issue_id=grievance_data.get('issue_id') | ||
| issue_id=grievance_data.get('issue_id'), | ||
| integrity_hash=integrity_hash, | ||
| previous_integrity_hash=prev_hash | ||
| ) | ||
|
|
||
| db.add(grievance) | ||
| db.commit() | ||
|
|
||
| # Update cache for next grievance only AFTER successful commit (Issue #290 optimization) | ||
| grievance_last_hash_cache.set(data=integrity_hash, key="last_hash") | ||
|
|
||
| db.refresh(grievance) | ||
|
|
||
| return grievance | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -166,6 +166,17 @@ def index_exists(table, index_name): | |
| if not index_exists("grievances", "ix_grievances_category_status"): | ||
| conn.execute(text("CREATE INDEX IF NOT EXISTS ix_grievances_category_status ON grievances (category, status)")) | ||
|
|
||
| if not column_exists("grievances", "integrity_hash"): | ||
| conn.execute(text("ALTER TABLE grievances ADD COLUMN integrity_hash VARCHAR")) | ||
| logger.info("Added integrity_hash column to grievances") | ||
|
|
||
| if not column_exists("grievances", "previous_integrity_hash"): | ||
| conn.execute(text("ALTER TABLE grievances ADD COLUMN previous_integrity_hash VARCHAR")) | ||
| logger.info("Added previous_integrity_hash column to grievances") | ||
|
Comment on lines
+169
to
+175
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These
🤖 Prompt for AI Agents |
||
|
|
||
| if not index_exists("grievances", "ix_grievances_previous_integrity_hash"): | ||
| conn.execute(text("CREATE INDEX IF NOT EXISTS ix_grievances_previous_integrity_hash ON grievances (previous_integrity_hash)")) | ||
|
|
||
| # Field Officer Visits Table (Issue #288) | ||
| # This table is newly created for field officer check-in system | ||
| if not inspector.has_table("field_officer_visits"): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,15 @@ | |
| from pathlib import Path | ||
| from dotenv import load_dotenv | ||
|
|
||
| # Fix for googletrans compatibility with newer httpcore (Issue #290) | ||
| # This monkeypatch must happen before any imports of googletrans or httpx | ||
| try: | ||
| import httpcore | ||
| if not hasattr(httpcore, "SyncHTTPTransport"): | ||
| httpcore.SyncHTTPTransport = object | ||
| except ImportError: | ||
| pass | ||
|
|
||
| load_dotenv() | ||
|
|
||
| # Add project root to sys.path to ensure 'backend.*' imports work | ||
|
|
@@ -85,9 +94,10 @@ async def lifespan(app: FastAPI): | |
| logger.info("Starting database initialization...") | ||
| await run_in_threadpool(Base.metadata.create_all, bind=engine) | ||
| logger.info("Base.metadata.create_all completed.") | ||
| # Temporarily disabled - comment out to debug startup issues | ||
| # await run_in_threadpool(migrate_db) | ||
| logger.info("Database initialized successfully (migrations skipped for local dev).") | ||
|
|
||
| # Run migrations to ensure schema is up-to-date (Issue #290) | ||
| await run_in_threadpool(migrate_db) | ||
| logger.info("Database initialized and migrations applied successfully.") | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| except Exception as e: | ||
| logger.error(f"Database initialization failed: {e}", exc_info=True) | ||
| # We continue to allow health checks even if DB has issues (for debugging) | ||
|
|
@@ -126,9 +136,8 @@ async def lifespan(app: FastAPI): | |
| app = FastAPI( | ||
| title="VishwaGuru Backend", | ||
| description="AI-powered civic issue reporting and resolution platform", | ||
| version="1.0.0" | ||
| # Temporarily disable lifespan for local dev debugging | ||
| # lifespan=lifespan | ||
| version="1.0.0", | ||
| lifespan=lifespan | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Re-enabling the Prompt for AI agents |
||
| ) | ||
|
|
||
| # Add centralized exception handlers | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,3 +22,4 @@ googletrans==4.0.2 | |
| langdetect | ||
| numpy | ||
| scikit-learn | ||
| httpcore | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -459,7 +459,7 @@ async def detect_abandoned_vehicle_endpoint(image: UploadFile = File(...)): | |
| @router.post("/api/detect-emotion") | ||
| async def detect_emotion_endpoint( | ||
| image: UploadFile = File(...), | ||
| client: httpx.AsyncClient = backend.dependencies.Depends(get_http_client) | ||
| client = backend.dependencies.Depends(get_http_client) | ||
| ): | ||
|
Comment on lines
459
to
463
|
||
| """ | ||
| Analyze facial emotions in the image using Hugging Face inference. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| from fastapi.testclient import TestClient | ||
| import pytest | ||
| import hashlib | ||
| from backend.main import app | ||
| from backend.database import get_db, Base, engine | ||
| from backend.models import Grievance, Jurisdiction, JurisdictionLevel, SeverityLevel | ||
| from sqlalchemy.orm import Session | ||
| from datetime import datetime, timedelta, timezone | ||
|
|
||
| @pytest.fixture | ||
| def db_session(): | ||
| Base.metadata.create_all(bind=engine) | ||
| session = Session(bind=engine) | ||
| # Create a jurisdiction which is required for grievance | ||
| jurisdiction = Jurisdiction( | ||
| level=JurisdictionLevel.LOCAL, | ||
| geographic_coverage={"cities": ["Mumbai"]}, | ||
| responsible_authority="Mumbai MC", | ||
| default_sla_hours=24 | ||
| ) | ||
| session.add(jurisdiction) | ||
| session.commit() | ||
| yield session | ||
| session.close() | ||
| Base.metadata.drop_all(bind=engine) | ||
|
Comment on lines
+10
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use a dedicated test database here. Lines 12 and 25 call 🤖 Prompt for AI Agents |
||
|
|
||
|
Comment on lines
+11
to
+26
|
||
| @pytest.fixture | ||
| def client(db_session): | ||
| app.dependency_overrides[get_db] = lambda: db_session | ||
| with TestClient(app) as c: | ||
| yield c | ||
| app.dependency_overrides = {} | ||
|
|
||
| def test_grievance_blockchain_chaining(client, db_session): | ||
| # Create two grievances through the API/Service logic would be better but let's test the chaining manually first then through service | ||
| from backend.grievance_service import GrievanceService | ||
| service = GrievanceService() | ||
|
|
||
| # Reset cache to ensure clean state | ||
| from backend.cache import grievance_last_hash_cache | ||
| grievance_last_hash_cache.clear() | ||
|
|
||
| # Grievance 1 | ||
| g1_data = { | ||
| "category": "Road", | ||
| "severity": "medium", | ||
| "city": "Mumbai", | ||
| "description": "Pothole in sector 5" | ||
| } | ||
| g1 = service.create_grievance(g1_data, db=db_session) | ||
| assert g1 is not None | ||
| assert g1.integrity_hash is not None | ||
| assert g1.previous_integrity_hash == "" | ||
|
|
||
| # Grievance 2 | ||
| g2_data = { | ||
| "category": "Garbage", | ||
| "severity": "high", | ||
| "city": "Mumbai", | ||
| "description": "Waste overflow" | ||
| } | ||
| g2 = service.create_grievance(g2_data, db=db_session) | ||
| assert g2 is not None | ||
| assert g2.integrity_hash is not None | ||
| assert g2.previous_integrity_hash == g1.integrity_hash | ||
|
|
||
| # Verify through API | ||
| response = client.get(f"/grievances/{g1.id}/blockchain-verify") | ||
| assert response.status_code == 200 | ||
| assert response.json()["is_valid"] == True | ||
|
|
||
| response = client.get(f"/grievances/{g2.id}/blockchain-verify") | ||
| assert response.status_code == 200 | ||
| assert response.json()["is_valid"] == True | ||
|
Comment on lines
+68
to
+74
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Switch these assertions to direct truthiness checks. Ruff E712 flags the 🧹 Minimal fix- assert response.json()["is_valid"] == True
+ assert response.json()["is_valid"]
...
- assert response.json()["is_valid"] == True
+ assert response.json()["is_valid"]
...
- assert response.json()["is_valid"] == False
+ assert not response.json()["is_valid"]Also applies to: 92-95 🧰 Tools🪛 Ruff (0.15.6)[error] 70-70: Avoid equality comparisons to Replace with (E712) [error] 74-74: Avoid equality comparisons to Replace with (E712) 🤖 Prompt for AI Agents |
||
|
|
||
| def test_grievance_blockchain_failure(client, db_session): | ||
| # Manually create a tampered grievance | ||
| jurisdiction = db_session.query(Jurisdiction).first() | ||
| g = Grievance( | ||
| unique_id="TAMPERED", | ||
| category="Road", | ||
| severity=SeverityLevel.MEDIUM, | ||
| current_jurisdiction_id=jurisdiction.id, | ||
| assigned_authority="Mumbai MC", | ||
| sla_deadline=datetime.now(timezone.utc) + timedelta(hours=24), | ||
| integrity_hash="fakehash", | ||
| previous_integrity_hash="" | ||
| ) | ||
| db_session.add(g) | ||
| db_session.commit() | ||
|
|
||
| response = client.get(f"/grievances/{g.id}/blockchain-verify") | ||
| assert response.status_code == 200 | ||
| assert response.json()["is_valid"] == False | ||
| assert "Integrity check failed" in response.json()["message"] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| 2026-03-20 14:51:01,153 - backend.adaptive_weights - INFO - Adaptive weights loaded/reloaded. | ||
| 2026-03-20 14:51:01,225 - backend.rag_service - INFO - Loaded 5 civic policies for RAG. | ||
| Starting server on port 10000 | ||
| Traceback (most recent call last): | ||
| File "/app/start-backend.py", line 13, in <module> | ||
| uvicorn.run("backend.main:app", host="0.0.0.0", port=port, log_level="info") | ||
| File "/home/jules/.pyenv/versions/3.12.13/lib/python3.12/site-packages/uvicorn/main.py", line 606, in run | ||
| server.run() | ||
| File "/home/jules/.pyenv/versions/3.12.13/lib/python3.12/site-packages/uvicorn/server.py", line 75, in run | ||
| return asyncio_run(self.serve(sockets=sockets), loop_factory=self.config.get_loop_factory()) | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| File "/home/jules/.pyenv/versions/3.12.13/lib/python3.12/asyncio/runners.py", line 195, in run | ||
| return runner.run(main) | ||
| ^^^^^^^^^^^^^^^^ | ||
| File "/home/jules/.pyenv/versions/3.12.13/lib/python3.12/asyncio/runners.py", line 118, in run | ||
| return self._loop.run_until_complete(task) | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| File "/home/jules/.pyenv/versions/3.12.13/lib/python3.12/asyncio/base_events.py", line 691, in run_until_complete | ||
| return future.result() | ||
| ^^^^^^^^^^^^^^^ | ||
| File "/home/jules/.pyenv/versions/3.12.13/lib/python3.12/site-packages/uvicorn/server.py", line 79, in serve | ||
| await self._serve(sockets) | ||
| File "/home/jules/.pyenv/versions/3.12.13/lib/python3.12/site-packages/uvicorn/server.py", line 86, in _serve | ||
| config.load() | ||
| File "/home/jules/.pyenv/versions/3.12.13/lib/python3.12/site-packages/uvicorn/config.py", line 441, in load | ||
| self.loaded_app = import_from_string(self.app) | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| File "/home/jules/.pyenv/versions/3.12.13/lib/python3.12/site-packages/uvicorn/importer.py", line 19, in import_from_string | ||
| module = importlib.import_module(module_str) | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| File "/home/jules/.pyenv/versions/3.12.13/lib/python3.12/importlib/__init__.py", line 90, in import_module | ||
| return _bootstrap._gcd_import(name[level:], package, level) | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| File "<frozen importlib._bootstrap>", line 1387, in _gcd_import | ||
| File "<frozen importlib._bootstrap>", line 1360, in _find_and_load | ||
| File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked | ||
| File "<frozen importlib._bootstrap>", line 935, in _load_unlocked | ||
| File "<frozen importlib._bootstrap_external>", line 999, in exec_module | ||
| File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed | ||
| File "/app/backend/main.py", line 35, in <module> | ||
| from backend.routers import issues, detection, grievances, utility, auth, admin, analysis, voice, field_officer, hf, resolution_proof | ||
| File "/app/backend/routers/voice.py", line 27, in <module> | ||
| from backend.voice_service import get_voice_service | ||
| File "/app/backend/voice_service.py", line 14, in <module> | ||
| from googletrans import Translator | ||
| File "/home/jules/.pyenv/versions/3.12.13/lib/python3.12/site-packages/googletrans/__init__.py", line 6, in <module> | ||
| from googletrans.client import Translator | ||
| File "/home/jules/.pyenv/versions/3.12.13/lib/python3.12/site-packages/googletrans/client.py", line 30, in <module> | ||
| class Translator: | ||
| File "/home/jules/.pyenv/versions/3.12.13/lib/python3.12/site-packages/googletrans/client.py", line 62, in Translator | ||
| proxies: typing.Dict[str, httpcore.SyncHTTPTransport] = None, | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| AttributeError: module 'httpcore' has no attribute 'SyncHTTPTransport' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
grievance_last_hash_cacheis process-local. In a multi-worker deployment (or if grievances are created from multiple app instances), different workers can computeprev_hashfrom stale cache values, producing non-linear/forked chains that depend on which worker handled the request. If you need a single global chain, deriveprev_hashfrom the DB within the same transaction (or use a DB-backed/centralized last-hash store with appropriate locking) rather than an in-memory cache.