Spaces:
Sleeping
Sleeping
File size: 6,964 Bytes
9cb6912 a605406 fafe05e a605406 9cb6912 62a62f6 9cb6912 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | import asyncio
import json
import uuid
from loguru import logger
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.base import BaseHTTPMiddleware
from celery.result import AsyncResult
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.middleware import SlowAPIMiddleware
from slowapi.errors import RateLimitExceeded
import redis as redis_lib
from api.config import get_settings
from api.schemas import OrchestrationRequest, TaskResponse, StatusResponse, EventCreate
from api.middleware import log_requests
from api.celery_app import celery_app
from worker.tasks import run_gridops_pipeline
class NumpyJSONEncoder(json.JSONEncoder):
def default(self, o):
module = o.__class__.__module__
if module.startswith('numpy') and hasattr(o, 'tolist'):
return o.tolist()
if module.startswith('numpy') and hasattr(o, 'item'):
return o.item()
return super().default(o)
limiter = Limiter(key_func=get_remote_address)
app = FastAPI(title='GridOps AI', version='1.0.0', description='Autonomous Energy Grid Intelligence')
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) # pyrefly: ignore[bad-argument-type]
app.add_middleware(SlowAPIMiddleware)
app.add_middleware(CORSMiddleware, allow_origins=['*'], allow_credentials=True, allow_methods=['*'], allow_headers=['*'])
app.add_middleware(BaseHTTPMiddleware, dispatch=log_requests)
@app.get('/health')
async def health():
try:
redis_lib.from_url(get_settings().redis_url).ping()
redis_status = 'connected'
except Exception:
redis_status = 'disconnected'
# Check if any Celery workers are alive
try:
ping_resp = celery_app.control.ping(timeout=3.0)
celery_status = 'connected' if ping_resp else 'disconnected'
except Exception:
celery_status = 'disconnected'
return {'status': 'healthy', 'redis': redis_status, 'celery': celery_status, 'version': '1.0.0'}
@app.post('/orchestrate', status_code=202)
@limiter.limit('5/minute')
async def orchestrate(request: Request, body: OrchestrationRequest):
task = run_gridops_pipeline.delay(
dataset_path=body.dataset_path,
severity_threshold=body.severity_threshold,
forecast_horizon=body.forecast_horizon,
target_date=body.target_date,
)
return TaskResponse(task_id=task.id, status='QUEUED', message='Pipeline started')
@app.get('/status/{task_id}')
async def get_status(task_id: str):
result = AsyncResult(task_id, app=celery_app)
state = result.state
if state == 'PENDING':
return StatusResponse(status='PENDING')
if state == 'PROGRESS':
info = result.info
return StatusResponse(status='PROGRESS', progress=info.get('progress', 0), stage=info.get('stage', ''), result=None)
if state == 'SUCCESS':
return StatusResponse(status='SUCCESS', progress=100, stage='COMPLETE', result=result.result)
if state == 'FAILURE':
return StatusResponse(status='FAILURE', error=str(result.info))
return StatusResponse(status=state)
# ── Event Management ──────────────────────────
@app.post('/events', status_code=201)
async def add_event(body: EventCreate):
"""Add a custom event to the ChromaDB grid_events collection."""
from rag.retriever import GridEventRetriever
from sentence_transformers import SentenceTransformer
retriever = GridEventRetriever()
event_id = f"user_{uuid.uuid4().hex[:8]}"
# Build the document text (same format as build_index.py)
doc_text = (
f"Event: {body.event_type} | Severity: {body.severity} | "
f"Region: {body.grid_region} | Impact: {body.demand_impact_pct:+.1f}% | "
f"Description: {body.description}"
)
# Embed and add to collection
encoded = retriever._model.encode(doc_text, normalize_embeddings=True)
if hasattr(encoded, 'tolist'):
embedding = encoded.tolist()
else:
embedding = [float(x) for x in encoded] # pyrefly: ignore[reportUnknownVariableType]
retriever._collection.add(
ids=[event_id],
embeddings=[embedding], # pyrefly: ignore[reportArgumentType]
documents=[doc_text],
metadatas=[{
"id": event_id,
"event_type": body.event_type,
"severity": body.severity.upper(),
"description": body.description,
"demand_impact_pct": body.demand_impact_pct,
"grid_region": body.grid_region,
}],
)
logger.info(f"Added custom event: {event_id} | {body.event_type} ({body.severity})")
return {"id": event_id, "message": f"Event '{body.event_type}' added to knowledge base"}
@app.get('/events')
async def list_events():
"""List all events in the ChromaDB collection."""
from rag.retriever import GridEventRetriever
retriever = GridEventRetriever()
stats = retriever.get_stats()
# Get all documents
raw = retriever._collection.get(include=["metadatas"])
events = raw.get("metadatas", [])
return {"total": stats["total_docs"], "events": events}
@app.websocket('/ws/{task_id}')
async def websocket_endpoint(websocket: WebSocket, task_id: str):
await websocket.accept()
while True:
try:
result = AsyncResult(task_id, app=celery_app)
state = result.state
if state == 'PROGRESS':
info = result.info
await websocket.send_text(json.dumps({'status': 'PROGRESS', 'progress': info.get('progress', 0), 'stage': info.get('stage', ''), 'message': info.get('message', '')}, cls=NumpyJSONEncoder))
elif state == 'SUCCESS':
try:
message = json.dumps({'status': 'SUCCESS', 'progress': 100, 'stage': 'COMPLETE', 'result': result.result}, cls=NumpyJSONEncoder)
except TypeError as e:
logger.error(f'Failed to serialize task result for websocket: {e}')
message = json.dumps({'status': 'FAILURE', 'error': 'Task result was not JSON serializable'}, cls=NumpyJSONEncoder)
await websocket.send_text(message)
await websocket.close()
break
elif state == 'FAILURE':
await websocket.send_text(json.dumps({'status': 'FAILURE', 'error': str(result.info)}, cls=NumpyJSONEncoder))
await websocket.close()
break
else:
await websocket.send_text(json.dumps({'status': state, 'progress': 0, 'stage': '', 'message': ''}, cls=NumpyJSONEncoder))
await asyncio.sleep(1.5)
except WebSocketDisconnect:
logger.info(f'WebSocket disconnected for task {task_id}')
break
|