Spaces:
Sleeping
Sleeping
File size: 22,395 Bytes
6675588 | 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 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 | """
main.py — SentinelAI-RAG FastAPI Production Server.
Endpoints:
- POST /v1/rag/query: Guardrails -> Cache -> SIMD Top-K RAG -> LLM Router -> Output Guard -> Cache Store
- POST /v1/rag/stream: Server-Sent Events (SSE) streaming query response
- GET /v1/health: Readiness, liveness, SIMD engine status, and SLA compliance
- GET /v1/metrics: Real-time Prometheus/JSON metrics (cost reduction, cache hit rate, latencies)
"""
import time
import json
import logging
from contextlib import asynccontextmanager
from typing import Optional, List, Dict, Any
from pathlib import Path
from fastapi import FastAPI, HTTPException, Request, Depends, status
from fastapi.responses import JSONResponse, StreamingResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from app.config import (
PDF_PATH,
CACHE_SIMILARITY_THRESHOLD,
RAG_TOP_K,
PII_REDACTION_ENABLED,
PROMPT_INJECTION_ENABLED,
OUTPUT_GUARD_ENABLED,
LLM_PRICING,
AVG_INPUT_TOKENS,
AVG_OUTPUT_TOKENS,
)
from app.guardrails.pii_redactor import PIIRedactor
from app.guardrails.prompt_injection import PromptInjectionGuard
from app.guardrails.output_guard import OutputGuard
from app.guardrails.rate_limiter import RateLimiter
from app.embeddings.onnx_embedder import ONNXEmbedder
from app.rag.pdf_chunker import PDFChunker
from app.rag.retriever import SIMDRetriever, SIMD_AVAILABLE
from app.rag.query_rewriter import QueryRewriter
from app.rag.quality_evaluator import RetrievalQualityEvaluator
from app.gateway.semantic_cache import SemanticCache, is_negative_or_refusal_response
from app.gateway.circuit_breaker import CircuitBreaker
from app.gateway.llm_router import LLMRouter
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
logger = logging.getLogger("sentinelai.server")
# Global subsystem state
embedder: Optional[ONNXEmbedder] = None
retriever: Optional[SIMDRetriever] = None
query_rewriter: Optional[QueryRewriter] = None
quality_evaluator: Optional[RetrievalQualityEvaluator] = None
cache: Optional[SemanticCache] = None
llm_router: Optional[LLMRouter] = None
pii_redactor: Optional[PIIRedactor] = None
injection_guard: Optional[PromptInjectionGuard] = None
output_guard: Optional[OutputGuard] = None
rate_limiter: Optional[RateLimiter] = None
# Metrics aggregation
SERVER_START_TIME = time.time()
query_latencies: List[float] = []
total_queries_served = 0
total_cost_saved_usd = 0.0
total_baseline_cost_usd = 0.0
def initialize_pipeline():
"""Initialize embedder, PDF chunker, retriever, cache, and guardrails."""
global embedder, retriever, cache, llm_router
global pii_redactor, injection_guard, output_guard, rate_limiter
global query_rewriter, quality_evaluator
logger.info("Initializing SentinelAI-RAG subsystems...")
# Guardrails
pii_redactor = PIIRedactor()
injection_guard = PromptInjectionGuard()
output_guard = OutputGuard()
rate_limiter = RateLimiter(max_requests_per_minute=120, burst=20)
# Advanced RAG: Query Rewriter & Quality Evaluator
query_rewriter = QueryRewriter()
quality_evaluator = RetrievalQualityEvaluator()
# Embedder
embedder = ONNXEmbedder()
# Retriever & Cache
retriever = SIMDRetriever(top_k=RAG_TOP_K)
cache = SemanticCache(similarity_threshold=CACHE_SIMILARITY_THRESHOLD)
cache.clear() # Ensure pristine state on startup without stale or negative entries
# Router
llm_router = LLMRouter()
# Ingest PDF if found
if PDF_PATH.exists():
logger.info(f"Ingesting PDF manual from {PDF_PATH}...")
chunker = PDFChunker()
parsed_doc = chunker.parse(str(PDF_PATH))
texts = [chunk.text for chunk in parsed_doc.chunks]
if texts:
logger.info(f"Embedding {len(texts)} chunks from PDF...")
chunk_embeddings = embedder.embed(texts)
retriever.ingest(parsed_doc.chunks, chunk_embeddings)
logger.info(f"Successfully ingested {len(texts)} PDF chunks into SIMD retriever.")
else:
logger.warning(f"PDF corpus not found at {PDF_PATH}. Starting with empty retriever.")
@asynccontextmanager
async def lifespan(app: FastAPI):
initialize_pipeline()
yield
logger.info("Shutting down SentinelAI-RAG service.")
app = FastAPI(
title="SentinelAI-RAG Enterprise Gateway",
description="High-Throughput AI Gateway, Guardrail Engine & C++17 AVX2 Semantic Cache Proxy",
version="1.0.0",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
STATIC_DIR = Path(__file__).resolve().parent / "static"
INDEX_HTML = STATIC_DIR / "index.html"
# Serve ChatGPT-Style conversational interface at GET /
@app.get("/", response_class=FileResponse)
async def serve_chat_ui():
"""Serve the ChatGPT-style conversational web interface."""
if INDEX_HTML.exists():
return FileResponse(str(INDEX_HTML), media_type="text/html")
raise HTTPException(status_code=404, detail="Web UI index.html not found")
if STATIC_DIR.exists():
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
# --- Request & Response Models ---
class QueryRequest(BaseModel):
query: str = Field(..., description="The user's enterprise query string")
client_id: Optional[str] = Field("default_client", description="Client identifier for rate limiting")
top_k: Optional[int] = Field(None, description="Override default Top-K retrieval count")
bypass_cache: Optional[bool] = Field(False, description="Force bypass of semantic cache")
user_provider: Optional[str] = Field(None, description="Dynamic BYOK provider override (groq, openai, simulator)")
user_model: Optional[str] = Field(None, description="Dynamic model override")
user_api_key: Optional[str] = Field(None, description="Ephemeral BYOK API key")
class RetrievedContextItem(BaseModel):
chunk_id: int
page_number: int
section_title: str
similarity_score: float
snippet: str
class GuardrailStats(BaseModel):
pii_detected_count: int
pii_findings: List[str]
prompt_injection_blocked: bool
injection_risk_score: float
output_guard_passed: bool
output_issues: List[str]
class LatencyBreakdown(BaseModel):
guardrail_latency_ms: float
embedding_latency_ms: float
cache_lookup_latency_ms: float
retrieval_latency_ms: float
llm_latency_ms: float
output_guard_latency_ms: float
total_pipeline_latency_ms: float
class QueryResponse(BaseModel):
model_config = {"protected_namespaces": ()}
query: str
sanitized_query: str
response: str
cache_hit: bool
cache_similarity: float
retrieved_contexts: List[RetrievedContextItem]
guardrails: GuardrailStats
timing: LatencyBreakdown
estimated_cost_usd: float
cost_saved_usd: float
model_used: str
retrieval_quality_score: Optional[float] = Field(None, description="Pre-generation retrieval quality score (0.0 to 1.0)")
rewritten_query: Optional[str] = Field(None, description="Expanded/rewritten query used for hybrid retrieval")
# --- API Endpoints ---
@app.post("/v1/rag/query", response_model=QueryResponse)
async def query_endpoint(req: QueryRequest, request: Request):
"""
Execute full Enterprise RAG lifecycle:
Rate Limit -> PII Redact -> Prompt Injection Check -> Embedding ->
AVX2 Semantic Cache Lookup -> SIMD Top-K RAG -> LLM Router -> Output Guard -> Cache Store
"""
global total_queries_served, total_cost_saved_usd, total_baseline_cost_usd
t_start = time.perf_counter()
# 1. Rate Limiting
rl_res = rate_limiter.check(client_id=req.client_id)
if not rl_res.allowed:
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail=f"Rate limit exceeded. Retry after {rl_res.retry_after_seconds:.1f}s",
)
# 2. Input Guardrails: PII Redaction
t_g_start = time.perf_counter()
pii_res = pii_redactor.redact(req.query) if PII_REDACTION_ENABLED else None
active_query = pii_res.redacted_text if pii_res else req.query
pii_count = pii_res.redaction_count if pii_res else 0
pii_findings = [f"{t}: {val}" for t, val in (pii_res.findings if pii_res else [])]
# 3. Input Guardrails: Prompt Injection Detection
inj_res = injection_guard.analyze(active_query) if PROMPT_INJECTION_ENABLED else None
t_guardrail = (time.perf_counter() - t_g_start) * 1000.0
if inj_res and inj_res.is_blocked:
t_total = (time.perf_counter() - t_start) * 1000.0
return QueryResponse(
query=req.query,
sanitized_query=active_query,
response="[BLOCKED] Your query violated enterprise security policies (Prompt Injection / Jailbreak attempt).",
cache_hit=False,
cache_similarity=0.0,
retrieved_contexts=[],
guardrails=GuardrailStats(
pii_detected_count=pii_count,
pii_findings=pii_findings,
prompt_injection_blocked=True,
injection_risk_score=inj_res.risk_score,
output_guard_passed=True,
output_issues=["PROMPT_INJECTION_INTERCEPTED"],
),
timing=LatencyBreakdown(
guardrail_latency_ms=t_guardrail,
embedding_latency_ms=0.0,
cache_lookup_latency_ms=0.0,
retrieval_latency_ms=0.0,
llm_latency_ms=0.0,
output_guard_latency_ms=0.0,
total_pipeline_latency_ms=t_total,
),
estimated_cost_usd=0.0,
cost_saved_usd=0.0,
model_used="guardrail-block",
)
# 4. Generate Single Query Embedding (Zero-double-embedding principle)
t_emb_start = time.perf_counter()
query_vec = embedder.embed_single(active_query)
t_embedding = (time.perf_counter() - t_emb_start) * 1000.0
# 5. Semantic Cache Lookup (AVX2 SIMD)
t_cache_start = time.perf_counter()
cache_hit = False
cache_sim = 0.0
cached_response = None
if not req.bypass_cache:
cache_res = cache.lookup(query_vec)
if cache_res.hit:
cache_hit = True
cache_sim = cache_res.similarity_score
cached_response = cache_res.response
t_cache = (time.perf_counter() - t_cache_start) * 1000.0
# Baseline cost estimation for non-cached call
baseline_cost = ((AVG_INPUT_TOKENS / 1_000_000) * LLM_PRICING["gpt-4o-mini"]["input"] +
(AVG_OUTPUT_TOKENS / 1_000_000) * LLM_PRICING["gpt-4o-mini"]["output"])
if cache_hit and cached_response:
t_total = (time.perf_counter() - t_start) * 1000.0
query_latencies.append(t_total)
total_queries_served += 1
total_cost_saved_usd += baseline_cost
total_baseline_cost_usd += baseline_cost
return QueryResponse(
query=req.query,
sanitized_query=active_query,
response=cached_response,
cache_hit=True,
cache_similarity=cache_sim,
retrieved_contexts=[],
guardrails=GuardrailStats(
pii_detected_count=pii_count,
pii_findings=pii_findings,
prompt_injection_blocked=False,
injection_risk_score=inj_res.risk_score if inj_res else 0.0,
output_guard_passed=True,
output_issues=[],
),
timing=LatencyBreakdown(
guardrail_latency_ms=t_guardrail,
embedding_latency_ms=t_embedding,
cache_lookup_latency_ms=t_cache,
retrieval_latency_ms=0.0,
llm_latency_ms=0.0,
output_guard_latency_ms=0.0,
total_pipeline_latency_ms=t_total,
),
estimated_cost_usd=0.0,
cost_saved_usd=baseline_cost,
model_used="semantic-cache-avx2",
)
# 6. Pre-Retrieval Query Rewriting & Expansion (Level 3 Production RAG)
rewritten = query_rewriter.rewrite(active_query) if query_rewriter else None
expanded_search_query = rewritten.expanded_query if rewritten else active_query
# 7. Production Hybrid Search (BM25 + AVX2 SIMD + RRF + Cross-Attention Reranking)
t_ret_start = time.perf_counter()
rag_res = retriever.hybrid_search(query_vec, query_text=expanded_search_query, top_k=req.top_k)
t_retrieval = (time.perf_counter() - t_ret_start) * 1000.0
# 8. Pre-Generation Retrieval Quality Gating
quality_report = quality_evaluator.evaluate(active_query, rag_res.chunks, rag_res.scores) if quality_evaluator else None
quality_score = quality_report.quality_score if quality_report else 0.85
context_str = "\n\n".join([f"[{c.section_title} | Page {c.page_number}]: {c.text}" for c in rag_res.chunks])
context_items = [
RetrievedContextItem(
chunk_id=c.chunk_id,
page_number=c.page_number,
section_title=c.section_title,
similarity_score=score,
snippet=c.text[:200] + "..." if len(c.text) > 200 else c.text,
)
for c, score in zip(rag_res.chunks, rag_res.scores)
]
# 9. LLM Generation via Router (with dynamic user BYOK credentials)
user_provider = request.headers.get("X-User-Provider") or req.user_provider
user_model = request.headers.get("X-User-Model") or req.user_model
user_api_key = request.headers.get("X-User-Api-Key") or req.user_api_key
t_llm_start = time.perf_counter()
llm_resp = await llm_router.generate(
prompt=active_query,
context=context_str,
query=active_query,
user_provider=user_provider,
user_model=user_model,
user_api_key=user_api_key,
)
t_llm = (time.perf_counter() - t_llm_start) * 1000.0
# 10. Output Guardrails
t_og_start = time.perf_counter()
og_res = output_guard.validate(llm_resp.text) if OUTPUT_GUARD_ENABLED else None
t_og = (time.perf_counter() - t_og_start) * 1000.0
final_text = og_res.sanitized_text if og_res else llm_resp.text
# 11. Store In Semantic Cache for Future Hits (Guarded against negative/refusal caching)
if (
og_res
and og_res.is_safe
and len(rag_res.chunks) > 0
and not is_negative_or_refusal_response(final_text)
):
cache.store(
query_text=active_query,
response_text=final_text,
query_embedding=query_vec,
model_used=llm_resp.model,
metadata={"chunks_count": len(rag_res.chunks)},
)
t_total = (time.perf_counter() - t_start) * 1000.0
query_latencies.append(t_total)
total_queries_served += 1
total_baseline_cost_usd += baseline_cost
return QueryResponse(
query=req.query,
sanitized_query=active_query,
response=final_text,
cache_hit=False,
cache_similarity=cache_sim,
retrieved_contexts=context_items,
guardrails=GuardrailStats(
pii_detected_count=pii_count,
pii_findings=pii_findings,
prompt_injection_blocked=False,
injection_risk_score=inj_res.risk_score if inj_res else 0.0,
output_guard_passed=og_res.is_safe if og_res else True,
output_issues=og_res.issues if og_res else [],
),
timing=LatencyBreakdown(
guardrail_latency_ms=t_guardrail,
embedding_latency_ms=t_embedding,
cache_lookup_latency_ms=t_cache,
retrieval_latency_ms=t_retrieval,
llm_latency_ms=t_llm,
output_guard_latency_ms=t_og,
total_pipeline_latency_ms=t_total,
),
estimated_cost_usd=llm_resp.estimated_cost_usd,
cost_saved_usd=0.0,
model_used=llm_resp.model,
retrieval_quality_score=quality_score,
rewritten_query=expanded_search_query,
)
@app.post("/v1/rag/stream")
async def stream_endpoint(req: QueryRequest, request: Request):
"""
Server-Sent Events (SSE) streaming query response with guardrail checks.
"""
user_provider = request.headers.get("X-User-Provider") or req.user_provider
user_model = request.headers.get("X-User-Model") or req.user_model
user_api_key = request.headers.get("X-User-Api-Key") or req.user_api_key
# Rate Limiting
rl_res = rate_limiter.check(client_id=req.client_id)
if not rl_res.allowed:
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail=f"Rate limit exceeded. Retry after {rl_res.retry_after_seconds:.1f}s",
)
# Input Guardrails
pii_res = pii_redactor.redact(req.query) if PII_REDACTION_ENABLED else None
active_query = pii_res.redacted_text if pii_res else req.query
inj_res = injection_guard.analyze(active_query) if PROMPT_INJECTION_ENABLED else None
if inj_res and inj_res.is_blocked:
async def block_gen():
yield "data: " + json.dumps({"token": "[BLOCKED: Prompt injection detected]"}) + "\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(block_gen(), media_type="text/event-stream")
query_vec = embedder.embed_single(active_query)
# Check cache first
if not req.bypass_cache:
cache_res = cache.lookup(query_vec)
if cache_res.hit:
async def cache_gen():
for word in cache_res.response.split():
yield "data: " + json.dumps({"token": word + " "}) + "\n\n"
yield "data: [DONE]\n\n"
# Hybrid Retrieval (BM25 + AVX2 SIMD + RRF + Cross-Attention Reranking)
rewritten = query_rewriter.rewrite(active_query) if query_rewriter else None
expanded_search_query = rewritten.expanded_query if rewritten else active_query
rag_res = retriever.hybrid_search(query_vec, query_text=expanded_search_query, top_k=req.top_k)
context_str = "\n\n".join([f"[{c.section_title}]: {c.text}" for c in rag_res.chunks])
async def token_generator():
collected_tokens = []
async for token in llm_router.generate_stream(
prompt=active_query,
context=context_str,
query=active_query,
user_provider=user_provider,
user_model=user_model,
user_api_key=user_api_key,
):
collected_tokens.append(token)
yield f"data: {json.dumps({'token': token})}\n\n"
full_resp = "".join(collected_tokens)
cache.store(
query_text=active_query,
response_text=full_resp,
query_embedding=query_vec,
model_used=user_model or "streaming-model",
)
yield "data: [DONE]\n\n"
return StreamingResponse(token_generator(), media_type="text/event-stream")
@app.get("/v1/health")
async def health_check():
"""
Comprehensive health status of all subsystems.
"""
uptime = time.time() - SERVER_START_TIME
simd_active = SIMD_AVAILABLE
if simd_active:
try:
import simd_engine
simd_active = simd_engine.is_simd_available()
except Exception:
simd_active = False
return {
"status": "healthy",
"uptime_seconds": round(uptime, 2),
"subsystems": {
"simd_avx2_engine": {
"compiled_and_loaded": SIMD_AVAILABLE,
"hardware_avx2_fma": simd_active,
},
"retriever": {
"indexed_chunks": retriever.chunk_count if retriever else 0,
"ready": retriever is not None and retriever.chunk_count > 0,
},
"semantic_cache": {
"current_entries": cache.size if cache else 0,
"hit_rate": cache.stats["hit_rate"] if cache else 0.0,
},
"llm_router": {
"providers_active": llm_router.stats["providers_configured"] if llm_router else 0,
"local_simulator_ready": True,
},
"guardrails": {
"pii_redactor": "active",
"prompt_injection_guard": "active",
"output_guard": "active",
"rate_limiter": "active",
},
},
}
@app.get("/v1/metrics")
async def metrics_endpoint():
"""
Export full operational metrics for monitoring and SLAs.
"""
lats = sorted(query_latencies) if query_latencies else [0.0]
n = len(lats)
p50 = lats[int(n * 0.50)]
p95 = lats[int(n * 0.95)] if n > 1 else lats[0]
p99 = lats[int(n * 0.99)] if n > 1 else lats[0]
cost_decrease_pct = 0.0
if total_baseline_cost_usd > 0:
cost_decrease_pct = (total_cost_saved_usd / total_baseline_cost_usd) * 100.0
return {
"total_queries_served": total_queries_served,
"latency_ms": {
"p50": round(p50, 2),
"p95": round(p95, 2),
"p99": round(p99, 2),
"mean": round(sum(lats) / max(n, 1), 2),
},
"cache_metrics": cache.stats if cache else {},
"cost_metrics": {
"total_baseline_cost_usd": round(total_baseline_cost_usd, 6),
"total_cost_saved_usd": round(total_cost_saved_usd, 6),
"cost_decrease_percentage": round(cost_decrease_pct, 2),
},
"guardrail_metrics": {
"pii": pii_redactor.stats if pii_redactor else {},
"prompt_injection": injection_guard.stats if injection_guard else {},
"output_guard": output_guard.stats if output_guard else {},
"rate_limiter": rate_limiter.stats if rate_limiter else {},
},
"retriever_metrics": retriever.stats if retriever else {},
}
@app.post("/v1/cache/clear")
async def clear_cache_endpoint():
"""Clear all entries in the semantic cache."""
global cache
if cache:
cache.clear()
logger.info("Semantic cache cleared via /v1/cache/clear")
return {"status": "ok", "message": "Semantic cache cleared successfully", "current_size": 0}
if __name__ == "__main__":
import uvicorn
uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=False)
|