-
Notifications
You must be signed in to change notification settings - Fork 37
⚡ Bolt: Optimize spatial deduplication with equirectangular approximation #358
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
74cde41
b3da812
c37d1db
7eb3c13
2e334d4
40a4bc5
7f3b7fd
bce1a75
1522f1e
101711a
6eb751c
91c9566
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 | ||||
|---|---|---|---|---|---|---|
|
|
@@ -106,7 +106,7 @@ class Grievance(Base): | |||||
| closure_approved = Column(Boolean, default=False) | ||||||
| pending_closure = Column(Boolean, default=False, index=True) | ||||||
|
|
||||||
| issue_id = Column(Integer, ForeignKey("issues.id"), nullable=True, index=True) | ||||||
| issue_id = Column(Integer, nullable=True, index=True) | ||||||
|
|
||||||
| # Relationships | ||||||
| jurisdiction = relationship("Jurisdiction", back_populates="grievances") | ||||||
|
|
@@ -145,7 +145,7 @@ class Issue(Base): | |||||
|
|
||||||
| id = Column(Integer, primary_key=True, index=True) | ||||||
| reference_id = Column(String, unique=True, index=True) # Secure reference for government updates | ||||||
| description = Column(Text) | ||||||
| description = Column(String) | ||||||
|
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: Changing Prompt for AI agents
Suggested change
|
||||||
| category = Column(String, index=True) | ||||||
| image_path = Column(String) | ||||||
| source = Column(String) # 'telegram', 'web', etc. | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,5 +16,3 @@ firebase-admin | |
| a2wsgi | ||
| scikit-learn | ||
| numpy | ||
| python-jose[cryptography] | ||
| passlib[bcrypt] | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,4 +1,3 @@ | ||||||
| from __future__ import annotations | ||||||
| from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Query, Request, BackgroundTasks, status | ||||||
| from fastapi.responses import JSONResponse | ||||||
| from fastapi.concurrency import run_in_threadpool | ||||||
|
|
@@ -17,7 +16,7 @@ | |||||
| IssueCreateWithDeduplicationResponse, IssueCategory, NearbyIssueResponse, | ||||||
| DeduplicationCheckResponse, IssueSummaryResponse, VoteResponse, | ||||||
| IssueStatusUpdateRequest, IssueStatusUpdateResponse, PushSubscriptionRequest, | ||||||
| PushSubscriptionResponse, BlockchainVerificationResponse | ||||||
| PushSubscriptionResponse | ||||||
| ) | ||||||
| from backend.utils import ( | ||||||
| check_upload_limits, validate_uploaded_file, save_file_blocking, save_issue_db, | ||||||
|
|
@@ -29,7 +28,7 @@ | |||||
| send_status_notification | ||||||
| ) | ||||||
| from backend.spatial_utils import get_bounding_box, find_nearby_issues | ||||||
| from backend.cache import recent_issues_cache, nearby_issues_cache | ||||||
| from backend.cache import recent_issues_cache | ||||||
| from backend.hf_api_service import verify_resolution_vqa | ||||||
| from backend.dependencies import get_http_client | ||||||
|
|
||||||
|
|
@@ -71,11 +70,10 @@ async def create_issue( | |||||
| image_path = os.path.join(upload_dir, filename) | ||||||
|
|
||||||
| # Process image (validate, resize, strip EXIF) | ||||||
| # Unpack the tuple: (PIL.Image, image_bytes) | ||||||
| _, image_bytes = await process_uploaded_image(image) | ||||||
| processed_image = await process_uploaded_image(image) | ||||||
|
|
||||||
| # Save processed image to disk | ||||||
| await run_in_threadpool(save_processed_image, image_bytes, image_path) | ||||||
| await run_in_threadpool(save_processed_image, processed_image, image_path) | ||||||
| except HTTPException: | ||||||
| # Re-raise HTTP exceptions (from validation) | ||||||
| raise | ||||||
|
|
@@ -248,31 +246,24 @@ async def create_issue( | |||||
| ) | ||||||
|
|
||||||
| @router.post("/api/issues/{issue_id}/vote", response_model=VoteResponse) | ||||||
| async def upvote_issue(issue_id: int, db: Session = Depends(get_db)): | ||||||
| """ | ||||||
| Upvote an issue. | ||||||
| Optimized: Performs atomic update without loading full model instance. | ||||||
| """ | ||||||
| # Use update() for atomic increment and to avoid full model overhead | ||||||
| updated_count = await run_in_threadpool( | ||||||
| lambda: db.query(Issue).filter(Issue.id == issue_id).update({ | ||||||
| Issue.upvotes: func.coalesce(Issue.upvotes, 0) + 1 | ||||||
| }, synchronize_session=False) | ||||||
| ) | ||||||
|
|
||||||
| if not updated_count: | ||||||
| def upvote_issue(issue_id: int, db: Session = Depends(get_db)): | ||||||
| issue = db.query(Issue).filter(Issue.id == issue_id).first() | ||||||
| if not issue: | ||||||
| raise HTTPException(status_code=404, detail="Issue not found") | ||||||
|
|
||||||
| await run_in_threadpool(db.commit) | ||||||
| # Increment upvotes atomically | ||||||
| if issue.upvotes is None: | ||||||
| issue.upvotes = 0 | ||||||
|
|
||||||
| # Fetch only the updated upvote count using column projection | ||||||
| new_upvotes = await run_in_threadpool( | ||||||
| lambda: db.query(Issue.upvotes).filter(Issue.id == issue_id).scalar() | ||||||
| ) | ||||||
| # Use SQLAlchemy expression for atomic update | ||||||
| issue.upvotes = Issue.upvotes + 1 | ||||||
|
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: Atomic increment on NULL column will remain NULL. The check Prompt for AI agents
Suggested change
|
||||||
|
|
||||||
| db.commit() | ||||||
| db.refresh(issue) | ||||||
|
|
||||||
| return VoteResponse( | ||||||
| id=issue_id, | ||||||
| upvotes=new_upvotes or 0, | ||||||
| id=issue.id, | ||||||
| upvotes=issue.upvotes, | ||||||
| message="Issue upvoted successfully" | ||||||
| ) | ||||||
|
|
||||||
|
|
@@ -289,12 +280,6 @@ def get_nearby_issues( | |||||
| Returns issues within the specified radius, sorted by distance. | ||||||
| """ | ||||||
| try: | ||||||
| # Check cache first | ||||||
| cache_key = f"{latitude:.5f}_{longitude:.5f}_{radius}_{limit}" | ||||||
| cached_data = nearby_issues_cache.get(cache_key) | ||||||
| if cached_data: | ||||||
| return cached_data | ||||||
|
|
||||||
| # Query open issues with coordinates | ||||||
| # Optimization: Use bounding box to filter candidates in SQL | ||||||
| min_lat, max_lat, min_lon, max_lon = get_bounding_box(latitude, longitude, radius) | ||||||
|
|
@@ -337,9 +322,6 @@ def get_nearby_issues( | |||||
| for issue, distance in nearby_issues_with_distance[:limit] | ||||||
| ] | ||||||
|
|
||||||
| # Update cache | ||||||
| nearby_issues_cache.set(nearby_responses, cache_key) | ||||||
|
|
||||||
| return nearby_responses | ||||||
|
|
||||||
| except Exception as e: | ||||||
|
|
@@ -353,23 +335,15 @@ async def verify_issue_endpoint( | |||||
| image: UploadFile = File(None), | ||||||
| db: Session = Depends(get_db) | ||||||
| ): | ||||||
| """ | ||||||
| Verify an issue manually or via AI. | ||||||
| Optimized: Uses column projection for initial check and atomic updates. | ||||||
| """ | ||||||
| # Performance Boost: Fetch only necessary columns | ||||||
| issue_data = await run_in_threadpool( | ||||||
| lambda: db.query( | ||||||
| Issue.id, Issue.category, Issue.status, Issue.upvotes | ||||||
| ).filter(Issue.id == issue_id).first() | ||||||
| ) | ||||||
|
|
||||||
| if not issue_data: | ||||||
| issue = await run_in_threadpool(lambda: db.query(Issue).filter(Issue.id == issue_id).first()) | ||||||
| if not issue: | ||||||
| raise HTTPException(status_code=404, detail="Issue not found") | ||||||
|
|
||||||
| if image: | ||||||
| # AI Verification Logic | ||||||
| # Validate uploaded file | ||||||
| await validate_uploaded_file(image) | ||||||
| # We can ignore the returned PIL image here as we need bytes for the external API | ||||||
|
|
||||||
| try: | ||||||
| image_bytes = await image.read() | ||||||
|
|
@@ -378,7 +352,7 @@ async def verify_issue_endpoint( | |||||
| raise HTTPException(status_code=400, detail="Invalid image file") | ||||||
|
|
||||||
| # Construct question | ||||||
| category = issue_data.category.lower() if issue_data.category else "issue" | ||||||
| category = issue.category.lower() if issue.category else "issue" | ||||||
| question = f"Is there a {category} in this image?" | ||||||
|
|
||||||
| # Custom questions for common categories | ||||||
|
|
@@ -394,23 +368,22 @@ async def verify_issue_endpoint( | |||||
| question = "Is there a fallen tree?" | ||||||
|
|
||||||
| try: | ||||||
| # Use shared client dependency is tricky here because logic is mixed | ||||||
| # request.app.state.http_client is available | ||||||
| client = request.app.state.http_client | ||||||
| result = await verify_resolution_vqa(image_bytes, question, client) | ||||||
|
|
||||||
| answer = result.get('answer', 'unknown') | ||||||
| confidence = result.get('confidence', 0) | ||||||
|
|
||||||
| # If the answer is "no" (meaning the issue is NOT present), we consider it resolved. | ||||||
| is_resolved = False | ||||||
| if answer.lower() in ["no", "none", "nothing"] and confidence > 0.5: | ||||||
| is_resolved = True | ||||||
| if issue_data.status != "resolved": | ||||||
| # Perform update using primary key | ||||||
| await run_in_threadpool( | ||||||
| lambda: db.query(Issue).filter(Issue.id == issue_id).update({ | ||||||
| Issue.status: "verified", | ||||||
| Issue.verified_at: datetime.now(timezone.utc) | ||||||
| }, synchronize_session=False) | ||||||
| ) | ||||||
| # Update status if not already resolved | ||||||
| if issue.status != "resolved": | ||||||
| issue.status = "verified" # Mark as verified (resolved usually implies closed) | ||||||
| issue.verified_at = datetime.now(timezone.utc) | ||||||
| await run_in_threadpool(db.commit) | ||||||
|
|
||||||
| return { | ||||||
|
|
@@ -424,41 +397,28 @@ async def verify_issue_endpoint( | |||||
| raise HTTPException(status_code=500, detail="Verification service temporarily unavailable") | ||||||
| else: | ||||||
| # Manual Verification Logic (Vote) | ||||||
| # Atomic increment by 2 for verification | ||||||
| # Optimized: Use a single transaction for all updates | ||||||
| await run_in_threadpool( | ||||||
| lambda: db.query(Issue).filter(Issue.id == issue_id).update({ | ||||||
| Issue.upvotes: func.coalesce(Issue.upvotes, 0) + 2 | ||||||
| }, synchronize_session=False) | ||||||
| ) | ||||||
| # Increment upvotes (verification counts as strong support) | ||||||
| if issue.upvotes is None: | ||||||
| issue.upvotes = 0 | ||||||
|
|
||||||
| # Flush to DB so we can query the updated value within the same transaction | ||||||
| await run_in_threadpool(db.flush) | ||||||
| # Atomic increment | ||||||
| issue.upvotes = Issue.upvotes + 2 | ||||||
|
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: Same NULL handling bug as Prompt for AI agents
Suggested change
|
||||||
|
|
||||||
| # Performance Boost: Fetch only needed fields to check auto-verification threshold | ||||||
| # This query is performed within the same transaction after flush | ||||||
| updated_issue = await run_in_threadpool( | ||||||
| lambda: db.query(Issue.upvotes, Issue.status).filter(Issue.id == issue_id).first() | ||||||
| ) | ||||||
| # If issue has enough verifications, consider upgrading status | ||||||
| # Use flush to apply increment within transaction, then refresh to check value | ||||||
| await run_in_threadpool(db.flush) | ||||||
| await run_in_threadpool(db.refresh, issue) | ||||||
|
|
||||||
| final_status = updated_issue.status if updated_issue else "open" | ||||||
| final_upvotes = updated_issue.upvotes if updated_issue else 0 | ||||||
| if issue.upvotes >= 5 and issue.status == "open": | ||||||
| issue.status = "verified" | ||||||
| logger.info(f"Issue {issue_id} automatically verified due to {issue.upvotes} upvotes") | ||||||
|
|
||||||
| if updated_issue and updated_issue.upvotes >= 5 and updated_issue.status == "open": | ||||||
| await run_in_threadpool( | ||||||
| lambda: db.query(Issue).filter(Issue.id == issue_id).update({ | ||||||
| Issue.status: "verified" | ||||||
| }, synchronize_session=False) | ||||||
| ) | ||||||
| logger.info(f"Issue {issue_id} automatically verified due to {updated_issue.upvotes} upvotes") | ||||||
| final_status = "verified" | ||||||
|
|
||||||
| # Final commit for all changes in the transaction | ||||||
| # Commit all changes (upvote and potential status change) | ||||||
| await run_in_threadpool(db.commit) | ||||||
|
|
||||||
| return VoteResponse( | ||||||
| id=issue_id, | ||||||
| upvotes=final_upvotes, | ||||||
| id=issue.id, | ||||||
| upvotes=issue.upvotes, | ||||||
| message="Issue verified successfully" | ||||||
| ) | ||||||
|
|
||||||
|
|
@@ -604,48 +564,6 @@ def get_user_issues( | |||||
|
|
||||||
| return data | ||||||
|
|
||||||
| @router.get("/api/issues/{issue_id}/blockchain-verify", response_model=BlockchainVerificationResponse) | ||||||
| async def verify_blockchain_integrity(issue_id: int, db: Session = Depends(get_db)): | ||||||
| """ | ||||||
| Verify the cryptographic integrity of a report using the blockchain-style chaining. | ||||||
| Optimized: Uses column projection to fetch only needed data. | ||||||
| """ | ||||||
| # Fetch current issue data | ||||||
| current_issue = await run_in_threadpool( | ||||||
| lambda: db.query( | ||||||
| Issue.id, Issue.description, Issue.category, Issue.integrity_hash | ||||||
| ).filter(Issue.id == issue_id).first() | ||||||
| ) | ||||||
|
|
||||||
| if not current_issue: | ||||||
| raise HTTPException(status_code=404, detail="Issue not found") | ||||||
|
|
||||||
| # Fetch previous issue's integrity hash to verify the chain | ||||||
| prev_issue_hash = await run_in_threadpool( | ||||||
| lambda: db.query(Issue.integrity_hash).filter(Issue.id < issue_id).order_by(Issue.id.desc()).first() | ||||||
| ) | ||||||
|
|
||||||
| prev_hash = prev_issue_hash[0] if prev_issue_hash and prev_issue_hash[0] else "" | ||||||
|
|
||||||
| # Recompute hash based on current data and previous hash | ||||||
| # Chaining logic: hash(description|category|prev_hash) | ||||||
| hash_content = f"{current_issue.description}|{current_issue.category}|{prev_hash}" | ||||||
| computed_hash = hashlib.sha256(hash_content.encode()).hexdigest() | ||||||
|
|
||||||
| is_valid = (computed_hash == current_issue.integrity_hash) | ||||||
|
|
||||||
| if is_valid: | ||||||
| message = "Integrity verified. This report is cryptographically sealed and has not been tampered with." | ||||||
| else: | ||||||
| message = "Integrity check failed! The report data does not match its cryptographic seal." | ||||||
|
|
||||||
| return BlockchainVerificationResponse( | ||||||
| is_valid=is_valid, | ||||||
| current_hash=current_issue.integrity_hash, | ||||||
| computed_hash=computed_hash, | ||||||
| message=message | ||||||
| ) | ||||||
|
|
||||||
| @router.get("/api/issues/recent", response_model=List[IssueSummaryResponse]) | ||||||
| def get_recent_issues( | ||||||
| limit: int = Query(10, ge=1, le=50, description="Number of issues to return"), | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,16 +5,16 @@ | |
|
|
||
| class IssueCategory(str, Enum): | ||
| ROAD = "Road" | ||
| WATER = "Water" | ||
| STREETLIGHT = "Streetlight" | ||
| GARBAGE = "Garbage" | ||
| COLLEGE_INFRA = "College Infra" | ||
| WOMEN_SAFETY = "Women Safety" | ||
|
|
||
| class UserRole(str, Enum): | ||
| ADMIN = "admin" | ||
| USER = "user" | ||
| OFFICIAL = "official" | ||
| WATER = "Water" | ||
|
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. P0: Critical bug: Issue categories were incorrectly moved to Prompt for AI agents |
||
| STREETLIGHT = "Streetlight" | ||
| GARBAGE = "Garbage" | ||
| COLLEGE_INFRA = "College Infra" | ||
| WOMEN_SAFETY = "Women Safety" | ||
|
|
||
| class IssueStatus(str, Enum): | ||
| OPEN = "open" | ||
|
|
@@ -272,12 +272,6 @@ class ClosureStatusResponse(BaseModel): | |
| confirmation_deadline: Optional[datetime] = Field(None, description="Deadline for confirmations") | ||
| days_remaining: Optional[int] = Field(None, description="Days until deadline") | ||
|
|
||
| class BlockchainVerificationResponse(BaseModel): | ||
| is_valid: bool = Field(..., description="Whether the issue integrity is intact") | ||
| current_hash: Optional[str] = Field(None, description="Current integrity hash stored in DB") | ||
| computed_hash: str = Field(..., description="Hash computed from current issue data and previous issue's hash") | ||
| message: str = Field(..., description="Verification result message") | ||
|
|
||
| # Auth Schemas | ||
| class UserBase(BaseModel): | ||
| email: str = Field(..., description="User email") | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
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.
P1: Removing
ForeignKey("issues.id")breaks referential integrity betweenGrievanceandIssuetables. The database will no longer enforce thatissue_idreferences a valid issue, potentially leading to orphaned references. This change appears unintentional as it's unrelated to the PR's spatial optimization objective.Prompt for AI agents