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
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@
## 2025-07-15 - Fast Bounding Box Pre-filter
**Learning:** Calculating great circle distance (Haversine) for every issue against a target location is computationally expensive (O(N) with heavy math ops like sin, cos, atan2). In high-traffic aggregations, this can become a bottleneck.
**Action:** Use a fast bounding box pre-filter (`get_bounding_box` with a 5% epsilon) to quickly discard issues that are definitely outside the search radius before running the expensive exact haversine distance calculation.

## 2025-07-24 - N+1 query optimization in Grievance Escalation
**Learning:** In periodic cron evaluation, retrieving grievances and then traversing relationships (like jurisdiction) causes N+1 query problem, hitting the DB N times.
**Action:** Use joinedload from sqlalchemy.orm to eager-load the related jurisdiction entity.
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
18 changes: 12 additions & 6 deletions backend/escalation_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@

import datetime
from typing import List, Dict, Any, Optional
from sqlalchemy.orm import Session
from sqlalchemy.orm import Session, joinedload
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
Comment on lines +10 to +13

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: Package imports of backend.escalation_engine now fail unless the backend directory is separately injected into sys.path, because these bare imports no longer resolve backend/models.py and the other backend modules. Keeping the backend.* imports (or consistently converting the backend to package-relative imports) preserves package-mode loading.

Prompt for AI agents
Check if this issue is valid β€” if so, understand the root cause and fix it. At backend/escalation_engine.py, line 10:

<comment>Package imports of `backend.escalation_engine` now fail unless the backend directory is separately injected into `sys.path`, because these bare imports no longer resolve `backend/models.py` and the other backend modules. Keeping the `backend.*` imports (or consistently converting the backend to package-relative imports) preserves package-mode loading.</comment>

<file context>
@@ -7,10 +7,10 @@
-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
</file context>
Suggested change
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
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


class EscalationEngine:
"""
Expand Down Expand Up @@ -150,7 +150,13 @@ def _get_grievances_for_evaluation(self, db: Session) -> List[Grievance]:
now = datetime.datetime.now(datetime.timezone.utc)

# Get grievances that are active and past SLA deadline
return db.query(Grievance).filter(
# ⚑ Bolt Optimization: Use joinedload to eager-load the jurisdiction relationship.
# This prevents an N+1 query problem during the evaluation loop where each
# grievance would otherwise trigger a separate query to fetch its jurisdiction.
# Expected Impact: Reduces database queries from 1+N to 1 per evaluation cycle.
return db.query(Grievance).options(
joinedload(Grievance.jurisdiction)

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 joinedload here optimizes the initial fetch, but SQLAlchemy's default expire_on_commit=True means that if _escalate_grievance() calls db.commit() during iteration, all remaining grievance objects in the list will be expired β€” causing grievance.jurisdiction.level access on subsequent iterations to trigger individual lazy-load queries anyway. This partially negates the N+1 fix.

Consider setting expire_on_commit=False on the session used for evaluation, or restructuring to avoid mid-loop commits, so the eagerly-loaded jurisdiction data remains valid throughout the iteration.

Prompt for AI agents
Check if this issue is valid β€” if so, understand the root cause and fix it. At backend/escalation_engine.py, line 158:

<comment>The `joinedload` here optimizes the initial fetch, but SQLAlchemy's default `expire_on_commit=True` means that if `_escalate_grievance()` calls `db.commit()` during iteration, all remaining grievance objects in the list will be expired β€” causing `grievance.jurisdiction.level` access on subsequent iterations to trigger individual lazy-load queries anyway. This partially negates the N+1 fix.

Consider setting `expire_on_commit=False` on the session used for evaluation, or restructuring to avoid mid-loop commits, so the eagerly-loaded jurisdiction data remains valid throughout the iteration.</comment>

<file context>
@@ -150,7 +150,13 @@ def _get_grievances_for_evaluation(self, db: Session) -> List[Grievance]:
+        # grievance would otherwise trigger a separate query to fetch its jurisdiction.
+        # Expected Impact: Reduces database queries from 1+N to 1 per evaluation cycle.
+        return db.query(Grievance).options(
+            joinedload(Grievance.jurisdiction)
+        ).filter(
             and_(
</file context>

).filter(
Comment on lines +153 to +159

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

πŸš€ Performance & Scalability | 🟑 Minor | ⚑ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C3 'SessionLocal|expire_on_commit|\.commit\(\)' backend

Repository: RohanExploit/VishwaGuru

Length of output: 20207


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== backend/database.py =="
cat -n backend/database.py

echo
echo "== backend/escalation_engine.py outline =="
ast-grep outline backend/escalation_engine.py --view compact || true

echo
echo "== backend/escalation_engine.py relevant sections =="
sed -n '1,190p' backend/escalation_engine.py | cat -n
echo
sed -n '190,280p' backend/escalation_engine.py | cat -n

Repository: RohanExploit/VishwaGuru

Length of output: 13109


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== backend/routing_service.py relevant sections =="
sed -n '1,180p' backend/routing_service.py | cat -n
echo
sed -n '250,410p' backend/escalation_engine.py | cat -n

echo
echo "== SQLAlchemy availability and compile output =="
python3 - <<'PY'
import inspect
try:
    import sqlalchemy
    print("sqlalchemy", sqlalchemy.__version__)
    import sqlalchemy.orm
    import sqlalchemy.orm.session
    print("expire_on_commit default from get_state_manager attr may be", getattr(sqlalchemy.orm.session, "_SESSION_DEFAULTS", {}).get("expire_on_commit", "<not exposed"))
    print("SessionLocal defaults:" , hasattr(sqlalchemy.orm.sessionmaker, "Session"))
except Exception as e:
    print("SQLAlchemy import failed:", repr(e))
PY

Repository: RohanExploit/VishwaGuru

Length of output: 9616


🌐 Web query:

SQLAlchemy sessionmaker expire_on_commit default True documentation

πŸ’‘ Result:

In SQLAlchemy, the default value for the expire_on_commit parameter in sessionmaker is True [1][2][3]. When this setting is enabled (the default), all objects associated with a Session are expired after a transaction is committed [1][4]. This means that the internal contents of the objects are erased, and they will be automatically re-loaded from the database upon their next attribute access to ensure they contain the most recent state [1][5][4]. If you prefer to maintain the state of your objects after a commitβ€”for instance, to avoid additional SQL queries for refreshing data when working in web request layersβ€”you can disable this behavior by setting expire_on_commit=False when initializing your sessionmaker [5][4].

Citations:


Keep evaluation objects unexpired through the loop.

joinedload() only optimizes the initial fetch. SessionLocal does not disable expire_on_commit, and _escalate_grievance() calls db.commit() inside evaluate_and_escalate_grievances()’s iteration, so later joinedload-loaded attributes like grievance.jurisdiction.level can be reloaded per object. Set expire_on_commit=False for this session or avoid commits/accessing revoked attributes between iterations.

πŸ€– 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/escalation_engine.py` around lines 153 - 159, Keep Grievance objects
and their joinedloaded jurisdiction data valid throughout
evaluate_and_escalate_grievances, despite _escalate_grievance committing during
iteration. Configure SessionLocal with expire_on_commit=False, or otherwise
prevent commits from expiring/accessing these attributes between iterations
while preserving the existing eager-loading behavior.

and_(
Grievance.status.in_([GrievanceStatus.OPEN, GrievanceStatus.IN_PROGRESS, GrievanceStatus.ESCALATED]),
Grievance.sla_deadline < now
Expand Down
10 changes: 5 additions & 5 deletions backend/grievance_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
from sqlalchemy import and_, desc
from datetime import datetime, timezone, timedelta

from backend.models import Grievance, Jurisdiction, GrievanceStatus, SeverityLevel, GrievanceFollower
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, GrievanceFollower
from database import SessionLocal
from routing_service import RoutingService
from sla_config_service import SLAConfigService
from escalation_engine import EscalationEngine

class GrievanceService:
"""
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

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: Package-style imports of backend.init_db now fail from the repository root because database is only a submodule of backend, not a top-level module. A dual import fallback (relative/package import for package execution, bare import for the documented cd backend execution) would preserve both entry points.

Prompt for AI agents
Check if this issue is valid β€” if so, understand the root cause and fix it. At backend/init_db.py, line 2:

<comment>Package-style imports of `backend.init_db` now fail from the repository root because `database` is only a submodule of `backend`, not a top-level module. A dual import fallback (relative/package import for package execution, bare import for the documented `cd backend` execution) would preserve both entry points.</comment>

<file context>
@@ -1,5 +1,5 @@
 from sqlalchemy import text
-from backend.database import engine
+from database import engine
 import logging
 
</file context>
Suggested change
from database import engine
try:
from .database import engine
except ImportError:
from database import engine

import logging

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

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

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 documented package/module entry points now fail immediately because database and models are imported as top-level modules. Keeping package-qualified/relative imports, or consistently supporting both package and script execution across the grievance modules, would preserve those documented entry points.

Prompt for AI agents
Check if this issue is valid β€” if so, understand the root cause and fix it. At backend/init_grievance_system.py, line 6:

<comment>The documented package/module entry points now fail immediately because `database` and `models` are imported as top-level modules. Keeping package-qualified/relative imports, or consistently supporting both package and script execution across the grievance modules, would preserve those documented entry points.</comment>

<file context>
@@ -3,8 +3,8 @@
 
-from backend.database import SessionLocal, engine
-from backend.models import Jurisdiction, JurisdictionLevel, SLAConfig, SeverityLevel
+from database import SessionLocal, engine
+from models import Jurisdiction, JurisdictionLevel, SLAConfig, SeverityLevel
 from backend.grievance_service import GrievanceService
</file context>

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

Expand All @@ -13,7 +13,7 @@ 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
4 changes: 2 additions & 2 deletions backend/main_fixed.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
import httpx

from backend.cache import recent_issues_cache
from backend.database import engine, Base, SessionLocal, get_db
from backend.models import Issue
from database import engine, Base, SessionLocal, get_db
from models import Issue
from backend.schemas import (
IssueResponse, IssueCreateRequest, IssueCreateResponse, ChatRequest, ChatResponse,
VoteRequest, VoteResponse, DetectionResponse, VisionAnalysisResponse,
Expand Down
4 changes: 2 additions & 2 deletions backend/routing_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
import json
from typing import Optional, Dict, Any
from sqlalchemy.orm import Session
from backend.models import Jurisdiction, JurisdictionLevel, Grievance
from backend.database import SessionLocal
from models import Jurisdiction, JurisdictionLevel, Grievance
from database import SessionLocal

class RoutingService:
"""
Expand Down
4 changes: 2 additions & 2 deletions backend/sla_config_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

from typing import Optional
from sqlalchemy.orm import Session
from backend.models import SLAConfig, JurisdictionLevel, SeverityLevel
from backend.database import SessionLocal
from models import SLAConfig, JurisdictionLevel, SeverityLevel
from database import SessionLocal

class SLAConfigService:
"""
Expand Down
2 changes: 1 addition & 1 deletion backend/spatial_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from sklearn.cluster import DBSCAN
import numpy as np

from backend.models import Issue
from models import Issue


def get_bounding_box(lat: float, lon: float, radius_meters: float) -> Tuple[float, float, float, float]:
Expand Down
2 changes: 1 addition & 1 deletion backend/test_grievance_escalation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"""

from backend.grievance_service import GrievanceService
from backend.models import SeverityLevel
from models import SeverityLevel
from datetime import datetime, timezone, timedelta

def test_escalation():
Expand Down
Loading
Loading