vxrachit commited on
Commit
9d6129a
·
1 Parent(s): de8c765

Setup Supabase Auth and Fastapi middleware.

Browse files
Backend/api/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .app import create_app
2
+
3
+ app = create_app()
Backend/api/app.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from contextlib import asynccontextmanager
2
+ from pathlib import Path
3
+ from fastapi import FastAPI, Request
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+ from fastapi.responses import JSONResponse, FileResponse
6
+ from fastapi.staticfiles import StaticFiles
7
+
8
+ from Backend.core.config import settings
9
+ from Backend.core.events import event_bus
10
+ from Backend.core.logging import setup_logging, get_logger
11
+ from Backend.core.security import SecurityHeadersMiddleware, RateLimitMiddleware, RequestValidationMiddleware
12
+ from Backend.database.connection import init_db, close_db
13
+ from Backend.api.routes import api_router
14
+
15
+ logger = get_logger(__name__)
16
+
17
+ STATIC_DIR = Path("static")
18
+
19
+ @asynccontextmanager
20
+ async def lifespan(app: FastAPI):
21
+ setup_logging(debug=settings.debug)
22
+ logger.info("Starting City Issue Resolution Agent")
23
+
24
+ await init_db()
25
+ logger.info("Database initialized")
26
+
27
+ await event_bus.start()
28
+ logger.info("Event bus started")
29
+
30
+
31
+ from Backend.agents.vision import VisionAgent
32
+ try:
33
+ VisionAgent.load_model()
34
+ logger.info("Vision model loaded")
35
+ except Exception as e:
36
+ logger.warning(f"Vision model failed to load: {e}. Running in mock mode.")
37
+
38
+
39
+ import asyncio
40
+ from Backend.database.connection import get_db_context
41
+ from Backend.agents.escalation.agent import EscalationAgent
42
+ from Backend.agents.sla.agent import SLAAgent
43
+
44
+ async def run_periodic_checks():
45
+ while True:
46
+ try:
47
+ logger.info("Running periodic SLA and Escalation checks...")
48
+ async with get_db_context() as db:
49
+
50
+ esc_agent = EscalationAgent(db)
51
+ await esc_agent.check_all_pending()
52
+
53
+
54
+ sla_agent = SLAAgent(db)
55
+ await sla_agent.check_all_active()
56
+ except Exception as e:
57
+ logger.error(f"Error in background task: {e}")
58
+
59
+
60
+ await asyncio.sleep(900)
61
+
62
+ task = asyncio.create_task(run_periodic_checks())
63
+
64
+ yield
65
+
66
+ task.cancel()
67
+ await event_bus.stop()
68
+ await close_db()
69
+ logger.info("Shutdown complete")
70
+
71
+
72
+ def create_app() -> FastAPI:
73
+ app = FastAPI(
74
+ title="City Issue Resolution Agent",
75
+ description="Autonomous urban issue detection and resolution platform",
76
+ version="1.0.0",
77
+ lifespan=lifespan,
78
+ root_path="",
79
+ )
80
+
81
+ # CORS must be added first
82
+ app.add_middleware(
83
+ CORSMiddleware,
84
+ allow_origins=["*"],
85
+ allow_credentials=False,
86
+ allow_methods=["*"],
87
+ allow_headers=["*"],
88
+ expose_headers=["*"],
89
+ )
90
+
91
+ app.add_middleware(SecurityHeadersMiddleware)
92
+ app.add_middleware(RateLimitMiddleware, requests_per_minute=120, burst_limit=20)
93
+ app.add_middleware(RequestValidationMiddleware)
94
+
95
+
96
+ settings.local_temp_dir.mkdir(parents=True, exist_ok=True)
97
+ STATIC_DIR.mkdir(parents=True, exist_ok=True)
98
+
99
+ app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
100
+
101
+ app.include_router(api_router)
102
+
103
+ @app.get("/")
104
+ async def root():
105
+ return FileResponse(STATIC_DIR / "flow.html")
106
+
107
+ @app.get("/dashboard")
108
+ async def dashboard():
109
+ return FileResponse(STATIC_DIR / "flow.html")
110
+
111
+ @app.exception_handler(ValueError)
112
+ async def value_error_handler(request: Request, exc: ValueError):
113
+ return JSONResponse(
114
+ status_code=400,
115
+ content={"detail": str(exc)}
116
+ )
117
+
118
+ @app.exception_handler(Exception)
119
+ async def general_exception_handler(request: Request, exc: Exception):
120
+ logger.error(f"Unhandled exception: {exc}", exc_info=True)
121
+ return JSONResponse(
122
+ status_code=500,
123
+ content={"detail": "Internal server error"}
124
+ )
125
+
126
+ return app
Backend/api/routes/__init__.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter
2
+
3
+ from .health import router as health_router
4
+ from .issues import router as issues_router
5
+ from .admin import router as admin_router
6
+ from .flow import router as flow_router
7
+ from .worker import router as worker_router
8
+
9
+ api_router = APIRouter()
10
+
11
+ api_router.include_router(health_router, prefix="/health", tags=["Health"])
12
+ api_router.include_router(issues_router, prefix="/issues", tags=["Issues"])
13
+ api_router.include_router(admin_router, prefix="/admin", tags=["Admin"])
14
+ api_router.include_router(flow_router, prefix="/flow", tags=["Agent Flow"])
15
+ api_router.include_router(worker_router, prefix="/worker", tags=["Worker"])
16
+
Backend/api/routes/flow.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import json
3
+ from dataclasses import asdict
4
+ from typing import Optional
5
+ from uuid import UUID
6
+ from fastapi import APIRouter, Depends, Query
7
+ from fastapi.responses import StreamingResponse
8
+ from sqlalchemy import select
9
+ from sqlalchemy.ext.asyncio import AsyncSession
10
+
11
+ from Backend.database.connection import get_db
12
+ from Backend.database.models import Issue, IssueEvent
13
+ from Backend.core.flow_tracker import get_flow_tracker, _active_flows
14
+
15
+ router = APIRouter()
16
+
17
+
18
+ async def event_generator(issue_id: UUID, timeout: int = 300):
19
+ tracker = get_flow_tracker(issue_id)
20
+
21
+ if not tracker:
22
+ yield f"data: {json.dumps({'type': 'error', 'message': 'No active flow for this issue'})}\n\n"
23
+ return
24
+
25
+ queue = tracker.subscribe()
26
+
27
+ try:
28
+ start_msg = {
29
+ "type": "connected",
30
+ "issue_id": str(issue_id),
31
+ "message": "Connected to agent flow stream",
32
+ "current_steps": [asdict(s) for s in tracker.flow.steps]
33
+ }
34
+ yield f"data: {json.dumps(start_msg)}\n\n"
35
+
36
+
37
+
38
+
39
+
40
+
41
+
42
+
43
+ if tracker.flow.status in ["completed", "error"]:
44
+ yield f"data: {json.dumps({'type': 'flow_' + tracker.flow.status, 'data': tracker.flow.to_dict()})}\n\n"
45
+ return
46
+
47
+ while True:
48
+ try:
49
+ message = await asyncio.wait_for(queue.get(), timeout=30)
50
+ yield f"data: {json.dumps(message)}\n\n"
51
+
52
+ if message.get("type") in ["flow_completed", "flow_error"]:
53
+ break
54
+ except asyncio.TimeoutError:
55
+ yield f"data: {json.dumps({'type': 'heartbeat'})}\n\n"
56
+ finally:
57
+ tracker.unsubscribe(queue)
58
+
59
+
60
+ @router.get("/flow/{issue_id}")
61
+ async def stream_agent_flow(issue_id: UUID):
62
+ return StreamingResponse(
63
+ event_generator(issue_id),
64
+ media_type="text/event-stream",
65
+ headers={
66
+ "Cache-Control": "no-cache",
67
+ "Connection": "keep-alive",
68
+ "X-Accel-Buffering": "no",
69
+ }
70
+ )
71
+
72
+
73
+ @router.get("/flow/active")
74
+ async def list_active_flows():
75
+ return {
76
+ "active_flows": [
77
+ {
78
+ "issue_id": str(issue_id),
79
+ "status": tracker.flow.status,
80
+ "steps_count": len(tracker.flow.steps),
81
+ "started_at": tracker.flow.started_at,
82
+ }
83
+ for issue_id, tracker in _active_flows.items()
84
+ ]
85
+ }
86
+
87
+
88
+ @router.get("/events/{issue_id}")
89
+ async def get_issue_events(
90
+ issue_id: UUID,
91
+ limit: int = Query(50, ge=1, le=200),
92
+ db: AsyncSession = Depends(get_db),
93
+ ):
94
+ query = (
95
+ select(IssueEvent)
96
+ .where(IssueEvent.issue_id == issue_id)
97
+ .order_by(IssueEvent.created_at.asc())
98
+ .limit(limit)
99
+ )
100
+ result = await db.execute(query)
101
+ events = result.scalars().all()
102
+
103
+ return {
104
+ "issue_id": str(issue_id),
105
+ "events": [
106
+ {
107
+ "id": str(e.id),
108
+ "event_type": e.event_type,
109
+ "agent_name": e.agent_name,
110
+ "event_data": json.loads(e.event_data) if e.event_data else None,
111
+ "created_at": e.created_at.isoformat(),
112
+ }
113
+ for e in events
114
+ ]
115
+ }
116
+
117
+
118
+ @router.get("/timeline/{issue_id}")
119
+ async def get_issue_timeline(
120
+ issue_id: UUID,
121
+ db: AsyncSession = Depends(get_db),
122
+ ):
123
+ issue = await db.get(Issue, issue_id)
124
+ if not issue:
125
+ return {"error": "Issue not found"}
126
+
127
+ query = (
128
+ select(IssueEvent)
129
+ .where(IssueEvent.issue_id == issue_id)
130
+ .order_by(IssueEvent.created_at.asc())
131
+ )
132
+ result = await db.execute(query)
133
+ events = result.scalars().all()
134
+
135
+ timeline = []
136
+
137
+ timeline.append({
138
+ "timestamp": issue.created_at.isoformat(),
139
+ "event": "issue_created",
140
+ "agent": "System",
141
+ "details": {
142
+ "latitude": issue.latitude,
143
+ "longitude": issue.longitude,
144
+ "description": issue.description,
145
+ }
146
+ })
147
+
148
+ for event in events:
149
+ event_data = json.loads(event.event_data) if event.event_data else {}
150
+ timeline.append({
151
+ "timestamp": event.created_at.isoformat(),
152
+ "event": event.event_type,
153
+ "agent": event.agent_name or "Unknown",
154
+ "details": event_data,
155
+ })
156
+
157
+ return {
158
+ "issue_id": str(issue_id),
159
+ "current_state": issue.state,
160
+ "priority": issue.priority,
161
+ "is_duplicate": issue.is_duplicate,
162
+ "timeline": timeline,
163
+ }
Backend/api/routes/health.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter
2
+ from sqlalchemy import text
3
+
4
+ from Backend.database.connection import async_session_factory
5
+
6
+ router = APIRouter()
7
+
8
+
9
+ @router.get("/health")
10
+ async def health_check():
11
+ return {"status": "healthy", "service": "city-issue-agent"}
12
+
13
+
14
+ @router.get("/health/db")
15
+ async def db_health_check():
16
+ try:
17
+ async with async_session_factory() as session:
18
+ await session.execute(text("SELECT 1"))
19
+ return {"status": "healthy", "database": "connected"}
20
+ except Exception as e:
21
+ return {"status": "unhealthy", "database": "disconnected", "error": str(e)}