Spaces:
Sleeping
Sleeping
feat: add Agentic RAG OS — full-stack RL B2B RaaS platform
Browse files- Full backend API (FastAPI): auth, RAG pipeline, Rewards-as-a-Service
- Modern dark-theme SPA frontend with animations
- GitHub OAuth + username/password auth with JWT
- RAG pipeline: document upload → embedding → FAISS → retrieval
- Rewards-as-a-Service: GRPO, PPO, DPO, REINFORCE, Custom
- User management, API keys, dashboard
- Architecture documentation
- Google Cloud Run deployment config
- Updated README with links and Wild Card section
- .gitignore +2 -0
- Dockerfile.ragos +32 -0
- README.md +3 -1
- agentic_rag_os/__init__.py +3 -0
- agentic_rag_os/api/__init__.py +1 -0
- agentic_rag_os/api/app.py +91 -0
- agentic_rag_os/api/auth_routes.py +38 -0
- agentic_rag_os/api/deps.py +51 -0
- agentic_rag_os/api/rag_routes.py +86 -0
- agentic_rag_os/api/reward_routes.py +102 -0
- agentic_rag_os/api/user_routes.py +72 -0
- agentic_rag_os/config.py +60 -0
- agentic_rag_os/frontend/index.html +1936 -0
- agentic_rag_os/models/__init__.py +142 -0
- agentic_rag_os/models/schemas.py +172 -0
- agentic_rag_os/run.py +21 -0
- agentic_rag_os/services/__init__.py +195 -0
- agentic_rag_os/services/rag_service.py +169 -0
- agentic_rag_os/services/reward_service.py +216 -0
- documents/agentic_rag_os_architecture.md +378 -0
- pyproject.toml +6 -1
.gitignore
CHANGED
|
@@ -29,3 +29,5 @@ node_modules/
|
|
| 29 |
# Training checkpoints (large files)
|
| 30 |
checkpoints/
|
| 31 |
.venv-1/
|
|
|
|
|
|
|
|
|
| 29 |
# Training checkpoints (large files)
|
| 30 |
checkpoints/
|
| 31 |
.venv-1/
|
| 32 |
+
*.db
|
| 33 |
+
agentic_rag_os/data/
|
Dockerfile.ragos
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
RUN apt-get update && \
|
| 6 |
+
apt-get install -y --no-install-recommends build-essential curl && \
|
| 7 |
+
rm -rf /var/lib/apt/lists/*
|
| 8 |
+
|
| 9 |
+
COPY pyproject.toml README.md ./
|
| 10 |
+
COPY rag_master/ rag_master/
|
| 11 |
+
COPY server/ server/
|
| 12 |
+
COPY domains/ domains/
|
| 13 |
+
COPY agentic_rag_os/ agentic_rag_os/
|
| 14 |
+
|
| 15 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
| 16 |
+
pip install --no-cache-dir ".[ragos]"
|
| 17 |
+
|
| 18 |
+
# Pre-warm embedding model
|
| 19 |
+
RUN python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')"
|
| 20 |
+
|
| 21 |
+
RUN mkdir -p /app/agentic_rag_os/data
|
| 22 |
+
|
| 23 |
+
ENV RAGOS_HOST=0.0.0.0
|
| 24 |
+
ENV RAGOS_PORT=8080
|
| 25 |
+
ENV PYTHONUNBUFFERED=1
|
| 26 |
+
|
| 27 |
+
EXPOSE 8080
|
| 28 |
+
|
| 29 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
| 30 |
+
CMD curl -f http://localhost:8080/api/v1/health || exit 1
|
| 31 |
+
|
| 32 |
+
CMD ["python", "-m", "agentic_rag_os.run"]
|
README.md
CHANGED
|
@@ -29,6 +29,8 @@ pinned: true
|
|
| 29 |
|---|---|
|
| 30 |
| **HF Space** | [huggingface.co/spaces/williyam/agentic-rag-gym](https://huggingface.co/spaces/williyam/agentic-rag-gym) |
|
| 31 |
| **Fine-Tuned Model** | [huggingface.co/williyam/agentic-rag-aerospace-grpo](https://huggingface.co/williyam/agentic-rag-aerospace-grpo) |
|
|
|
|
|
|
|
| 32 |
|
| 33 |
> **We didn't just build another RAG pipeline — we redesigned the foundation of how agentic RAG systems learn, decide, and improve.**
|
| 34 |
|
|
@@ -60,7 +62,7 @@ Traditional RAG systems are static pipelines: retrieve → generate → done. Th
|
|
| 60 |
| **#2 Long-Horizon Planning** | Tasks require 10-20 step trajectories with planning, iterative retrieval, reasoning, critique, and verification |
|
| 61 |
| **#3.1 World Modeling (Professional)** | Dynamic RAG environment with FAISS vector store, LLM reasoning, and tool orchestration |
|
| 62 |
| **#4 Self-Improvement** | Adversarial critique loops, iterative refinement, GRPO fine-tuning with real graders |
|
| 63 |
-
| **#5 Wild Card** | **
|
| 64 |
|
| 65 |
---
|
| 66 |
|
|
|
|
| 29 |
|---|---|
|
| 30 |
| **HF Space** | [huggingface.co/spaces/williyam/agentic-rag-gym](https://huggingface.co/spaces/williyam/agentic-rag-gym) |
|
| 31 |
| **Fine-Tuned Model** | [huggingface.co/williyam/agentic-rag-aerospace-grpo](https://huggingface.co/williyam/agentic-rag-aerospace-grpo) |
|
| 32 |
+
| **Agentic RAG OS** | [agentic-rag-os (Google Cloud)](https://agentic-rag-os-441459779686.us-central1.run.app) |
|
| 33 |
+
| **Training Notebook** | [Google Colab — GRPO Fine-Tuning](https://colab.research.google.com/drive/14il2JQmy9-id_fSGpmYbssp-j975DSDo?usp=sharing) |
|
| 34 |
|
| 35 |
> **We didn't just build another RAG pipeline — we redesigned the foundation of how agentic RAG systems learn, decide, and improve.**
|
| 36 |
|
|
|
|
| 62 |
| **#2 Long-Horizon Planning** | Tasks require 10-20 step trajectories with planning, iterative retrieval, reasoning, critique, and verification |
|
| 63 |
| **#3.1 World Modeling (Professional)** | Dynamic RAG environment with FAISS vector store, LLM reasoning, and tool orchestration |
|
| 64 |
| **#4 Self-Improvement** | Adversarial critique loops, iterative refinement, GRPO fine-tuning with real graders |
|
| 65 |
+
| **#5 Wild Card** | **Agentic RAG OS** — An RL B2B Rewards-as-a-Service (RaaS) startup platform built as a full-stack operating system for agentic RAG. **RAG Master** — A new orchestrator/framework designed from scratch (like LangChain/LangGraph) as a domain-agnostic engine for multi-agent RAG with RL-driven process supervision. |
|
| 66 |
|
| 67 |
---
|
| 68 |
|
agentic_rag_os/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Agentic RAG OS — The Operating System for Agentic RAG."""
|
| 2 |
+
|
| 3 |
+
__version__ = "1.0.0"
|
agentic_rag_os/api/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""API package for Agentic RAG OS."""
|
agentic_rag_os/api/app.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Main FastAPI application for Agentic RAG OS."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
from contextlib import asynccontextmanager
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import AsyncGenerator
|
| 9 |
+
|
| 10 |
+
from fastapi import FastAPI, Request
|
| 11 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 12 |
+
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
|
| 13 |
+
from fastapi.staticfiles import StaticFiles
|
| 14 |
+
|
| 15 |
+
from agentic_rag_os.config import get_settings
|
| 16 |
+
from agentic_rag_os.models import close_db, get_db
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@asynccontextmanager
|
| 20 |
+
async def lifespan(app: FastAPI) -> AsyncGenerator:
|
| 21 |
+
"""Startup/shutdown lifecycle."""
|
| 22 |
+
await get_db()
|
| 23 |
+
yield
|
| 24 |
+
await close_db()
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
app = FastAPI(
|
| 28 |
+
title="Agentic RAG OS",
|
| 29 |
+
description="The Operating System for Agentic RAG — RL B2B Rewards-as-a-Service",
|
| 30 |
+
version="1.0.0",
|
| 31 |
+
docs_url="/api/docs",
|
| 32 |
+
redoc_url="/api/redoc",
|
| 33 |
+
openapi_url="/api/openapi.json",
|
| 34 |
+
lifespan=lifespan,
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
app.add_middleware(
|
| 38 |
+
CORSMiddleware,
|
| 39 |
+
allow_origins=["*"],
|
| 40 |
+
allow_credentials=True,
|
| 41 |
+
allow_methods=["*"],
|
| 42 |
+
allow_headers=["*"],
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
# --- Register API routers ---
|
| 46 |
+
from agentic_rag_os.api.auth_routes import router as auth_router
|
| 47 |
+
from agentic_rag_os.api.rag_routes import router as rag_router
|
| 48 |
+
from agentic_rag_os.api.reward_routes import router as reward_router
|
| 49 |
+
from agentic_rag_os.api.user_routes import router as user_router
|
| 50 |
+
|
| 51 |
+
app.include_router(auth_router, prefix="/api/v1")
|
| 52 |
+
app.include_router(rag_router, prefix="/api/v1")
|
| 53 |
+
app.include_router(reward_router, prefix="/api/v1")
|
| 54 |
+
app.include_router(user_router, prefix="/api/v1")
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
# --- Health check ---
|
| 58 |
+
@app.get("/api/v1/health")
|
| 59 |
+
async def health():
|
| 60 |
+
return {"status": "healthy", "service": "agentic-rag-os", "version": "1.0.0"}
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
# --- Serve static files ---
|
| 64 |
+
_FRONTEND_DIR = Path(__file__).parent / "frontend"
|
| 65 |
+
_STATIC_DIR = _FRONTEND_DIR / "static"
|
| 66 |
+
|
| 67 |
+
if _STATIC_DIR.exists():
|
| 68 |
+
app.mount("/static", StaticFiles(directory=str(_STATIC_DIR)), name="static")
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
# --- Serve SPA ---
|
| 72 |
+
@app.get("/{full_path:path}")
|
| 73 |
+
async def serve_spa(request: Request, full_path: str):
|
| 74 |
+
"""Serve the single-page application for all non-API routes."""
|
| 75 |
+
# Don't intercept API paths
|
| 76 |
+
if full_path.startswith("api/"):
|
| 77 |
+
return JSONResponse({"detail": "Not found"}, status_code=404)
|
| 78 |
+
|
| 79 |
+
index = _FRONTEND_DIR / "index.html"
|
| 80 |
+
if index.exists():
|
| 81 |
+
return FileResponse(str(index), media_type="text/html")
|
| 82 |
+
return HTMLResponse("<h1>Agentic RAG OS</h1><p>Frontend not built yet.</p>")
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
# --- Global error handler ---
|
| 86 |
+
@app.exception_handler(Exception)
|
| 87 |
+
async def global_exception_handler(request: Request, exc: Exception):
|
| 88 |
+
return JSONResponse(
|
| 89 |
+
status_code=500,
|
| 90 |
+
content={"detail": f"Internal server error: {str(exc)}"},
|
| 91 |
+
)
|
agentic_rag_os/api/auth_routes.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Authentication routes — register, login, GitHub OAuth."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from fastapi import APIRouter, HTTPException, Query
|
| 6 |
+
|
| 7 |
+
from agentic_rag_os.models.schemas import LoginRequest, RegisterRequest, TokenResponse
|
| 8 |
+
from agentic_rag_os.services import github_oauth_callback, login_user, register_user
|
| 9 |
+
|
| 10 |
+
router = APIRouter(prefix="/auth", tags=["Authentication"])
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@router.post("/register", response_model=TokenResponse)
|
| 14 |
+
async def register(req: RegisterRequest):
|
| 15 |
+
try:
|
| 16 |
+
result = await register_user(req.username, req.email, req.password)
|
| 17 |
+
return result
|
| 18 |
+
except ValueError as e:
|
| 19 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@router.post("/login", response_model=TokenResponse)
|
| 23 |
+
async def login(req: LoginRequest):
|
| 24 |
+
try:
|
| 25 |
+
result = await login_user(req.username, req.password)
|
| 26 |
+
return result
|
| 27 |
+
except ValueError as e:
|
| 28 |
+
raise HTTPException(status_code=401, detail=str(e))
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@router.get("/github/callback")
|
| 32 |
+
async def github_callback(code: str = Query(...)):
|
| 33 |
+
"""GitHub OAuth callback — exchange code for JWT."""
|
| 34 |
+
try:
|
| 35 |
+
result = await github_oauth_callback(code)
|
| 36 |
+
return result
|
| 37 |
+
except ValueError as e:
|
| 38 |
+
raise HTTPException(status_code=400, detail=str(e))
|
agentic_rag_os/api/deps.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Authentication dependency for FastAPI routes."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import Any, Dict, Optional
|
| 6 |
+
|
| 7 |
+
from fastapi import Depends, HTTPException, Request, status
|
| 8 |
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
| 9 |
+
|
| 10 |
+
from agentic_rag_os.services import decode_token, validate_api_key
|
| 11 |
+
|
| 12 |
+
_bearer = HTTPBearer(auto_error=False)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
async def get_current_user(
|
| 16 |
+
request: Request,
|
| 17 |
+
credentials: Optional[HTTPAuthorizationCredentials] = Depends(_bearer),
|
| 18 |
+
) -> Dict[str, Any]:
|
| 19 |
+
"""Extract and validate the current user from JWT or API key."""
|
| 20 |
+
|
| 21 |
+
# Try Bearer token (JWT)
|
| 22 |
+
if credentials and credentials.credentials:
|
| 23 |
+
try:
|
| 24 |
+
payload = decode_token(credentials.credentials)
|
| 25 |
+
return {"user_id": payload["sub"], "username": payload["username"], "role": payload.get("role", "user")}
|
| 26 |
+
except Exception:
|
| 27 |
+
pass
|
| 28 |
+
|
| 29 |
+
# Try API key in X-API-Key header
|
| 30 |
+
api_key = request.headers.get("X-API-Key", "")
|
| 31 |
+
if api_key:
|
| 32 |
+
user_info = await validate_api_key(api_key)
|
| 33 |
+
if user_info:
|
| 34 |
+
return user_info
|
| 35 |
+
|
| 36 |
+
raise HTTPException(
|
| 37 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 38 |
+
detail="Not authenticated. Provide a Bearer token or X-API-Key header.",
|
| 39 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
async def get_optional_user(
|
| 44 |
+
request: Request,
|
| 45 |
+
credentials: Optional[HTTPAuthorizationCredentials] = Depends(_bearer),
|
| 46 |
+
) -> Optional[Dict[str, Any]]:
|
| 47 |
+
"""Optionally get the current user — returns None if not authenticated."""
|
| 48 |
+
try:
|
| 49 |
+
return await get_current_user(request, credentials)
|
| 50 |
+
except HTTPException:
|
| 51 |
+
return None
|
agentic_rag_os/api/rag_routes.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""RAG pipeline routes — domains, documents, queries."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import Any, Dict, List
|
| 6 |
+
|
| 7 |
+
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
| 8 |
+
|
| 9 |
+
from agentic_rag_os.api.deps import get_current_user
|
| 10 |
+
from agentic_rag_os.models.schemas import (
|
| 11 |
+
DomainCreate,
|
| 12 |
+
DomainOut,
|
| 13 |
+
DocumentOut,
|
| 14 |
+
RAGQueryRequest,
|
| 15 |
+
RAGQueryResponse,
|
| 16 |
+
)
|
| 17 |
+
from agentic_rag_os.services.rag_service import (
|
| 18 |
+
create_domain,
|
| 19 |
+
delete_document,
|
| 20 |
+
delete_domain,
|
| 21 |
+
list_documents,
|
| 22 |
+
list_domains,
|
| 23 |
+
rag_query,
|
| 24 |
+
upload_document,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
router = APIRouter(prefix="/rag", tags=["RAG Pipeline"])
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# --- Domains ---
|
| 31 |
+
|
| 32 |
+
@router.post("/domains", response_model=DomainOut)
|
| 33 |
+
async def create_domain_route(body: DomainCreate, user: Dict = Depends(get_current_user)):
|
| 34 |
+
return await create_domain(user["user_id"], body.name, body.description)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@router.get("/domains", response_model=List[DomainOut])
|
| 38 |
+
async def list_domains_route(user: Dict = Depends(get_current_user)):
|
| 39 |
+
return await list_domains(user["user_id"])
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@router.delete("/domains/{domain_id}")
|
| 43 |
+
async def delete_domain_route(domain_id: str, user: Dict = Depends(get_current_user)):
|
| 44 |
+
ok = await delete_domain(user["user_id"], domain_id)
|
| 45 |
+
if not ok:
|
| 46 |
+
raise HTTPException(status_code=404, detail="Domain not found")
|
| 47 |
+
return {"ok": True}
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
# --- Documents ---
|
| 51 |
+
|
| 52 |
+
@router.get("/domains/{domain_id}/documents", response_model=List[DocumentOut])
|
| 53 |
+
async def list_docs_route(domain_id: str, user: Dict = Depends(get_current_user)):
|
| 54 |
+
return await list_documents(user["user_id"], domain_id)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
@router.post("/domains/{domain_id}/documents", response_model=DocumentOut)
|
| 58 |
+
async def upload_doc_route(domain_id: str, file: UploadFile = File(...), user: Dict = Depends(get_current_user)):
|
| 59 |
+
"""Upload a text file to a domain. Free tier: max 2MB total."""
|
| 60 |
+
if not file.filename:
|
| 61 |
+
raise HTTPException(status_code=400, detail="Filename required")
|
| 62 |
+
content = (await file.read()).decode("utf-8", errors="replace")
|
| 63 |
+
if not content.strip():
|
| 64 |
+
raise HTTPException(status_code=400, detail="File is empty")
|
| 65 |
+
try:
|
| 66 |
+
return await upload_document(user["user_id"], domain_id, file.filename, content)
|
| 67 |
+
except ValueError as e:
|
| 68 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
@router.delete("/domains/{domain_id}/documents/{doc_id}")
|
| 72 |
+
async def delete_doc_route(domain_id: str, doc_id: str, user: Dict = Depends(get_current_user)):
|
| 73 |
+
ok = await delete_document(user["user_id"], domain_id, doc_id)
|
| 74 |
+
if not ok:
|
| 75 |
+
raise HTTPException(status_code=404, detail="Document not found")
|
| 76 |
+
return {"ok": True}
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
# --- RAG Query ---
|
| 80 |
+
|
| 81 |
+
@router.post("/domains/{domain_id}/query", response_model=RAGQueryResponse)
|
| 82 |
+
async def query_route(domain_id: str, body: RAGQueryRequest, user: Dict = Depends(get_current_user)):
|
| 83 |
+
try:
|
| 84 |
+
return await rag_query(user["user_id"], domain_id, body.query, body.top_k)
|
| 85 |
+
except ValueError as e:
|
| 86 |
+
raise HTTPException(status_code=400, detail=str(e))
|
agentic_rag_os/api/reward_routes.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Rewards-as-a-Service routes."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import Any, Dict, List, Optional
|
| 6 |
+
|
| 7 |
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
| 8 |
+
|
| 9 |
+
from agentic_rag_os.api.deps import get_current_user
|
| 10 |
+
from agentic_rag_os.models.schemas import (
|
| 11 |
+
RewardComputeRequest,
|
| 12 |
+
RewardComputeResponse,
|
| 13 |
+
RewardConfigCreate,
|
| 14 |
+
RewardConfigOut,
|
| 15 |
+
RewardJobCreate,
|
| 16 |
+
RewardJobOut,
|
| 17 |
+
)
|
| 18 |
+
from agentic_rag_os.services.reward_service import (
|
| 19 |
+
compute_reward,
|
| 20 |
+
create_reward_config,
|
| 21 |
+
create_reward_job,
|
| 22 |
+
delete_reward_config,
|
| 23 |
+
list_reward_configs,
|
| 24 |
+
list_reward_jobs,
|
| 25 |
+
get_reward_config,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
router = APIRouter(prefix="/rewards", tags=["Rewards-as-a-Service"])
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# --- Reward Configs ---
|
| 32 |
+
|
| 33 |
+
@router.post("/configs", response_model=RewardConfigOut)
|
| 34 |
+
async def create_config_route(body: RewardConfigCreate, user: Dict = Depends(get_current_user)):
|
| 35 |
+
return await create_reward_config(user["user_id"], body.name, body.algorithm, body.domain_id, body.config)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@router.get("/configs", response_model=List[RewardConfigOut])
|
| 39 |
+
async def list_configs_route(user: Dict = Depends(get_current_user)):
|
| 40 |
+
return await list_reward_configs(user["user_id"])
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
@router.get("/configs/{config_id}", response_model=RewardConfigOut)
|
| 44 |
+
async def get_config_route(config_id: str, user: Dict = Depends(get_current_user)):
|
| 45 |
+
cfg = await get_reward_config(user["user_id"], config_id)
|
| 46 |
+
if not cfg:
|
| 47 |
+
raise HTTPException(status_code=404, detail="Config not found")
|
| 48 |
+
return cfg
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
@router.delete("/configs/{config_id}")
|
| 52 |
+
async def delete_config_route(config_id: str, user: Dict = Depends(get_current_user)):
|
| 53 |
+
ok = await delete_reward_config(user["user_id"], config_id)
|
| 54 |
+
if not ok:
|
| 55 |
+
raise HTTPException(status_code=404, detail="Config not found")
|
| 56 |
+
return {"ok": True}
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
# --- On-the-fly reward computation ---
|
| 60 |
+
|
| 61 |
+
@router.post("/compute", response_model=RewardComputeResponse)
|
| 62 |
+
async def compute_reward_route(body: RewardComputeRequest, user: Dict = Depends(get_current_user)):
|
| 63 |
+
"""Compute reward on-the-fly — no stored config needed."""
|
| 64 |
+
result = compute_reward(
|
| 65 |
+
algorithm=body.algorithm,
|
| 66 |
+
query=body.query,
|
| 67 |
+
answer=body.answer,
|
| 68 |
+
retrieved_docs=body.retrieved_docs,
|
| 69 |
+
config=body.config,
|
| 70 |
+
)
|
| 71 |
+
return result
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
# --- Reward Jobs ---
|
| 75 |
+
|
| 76 |
+
@router.post("/jobs", response_model=RewardJobOut)
|
| 77 |
+
async def create_job_route(body: RewardJobCreate, user: Dict = Depends(get_current_user)):
|
| 78 |
+
try:
|
| 79 |
+
return await create_reward_job(user["user_id"], body.reward_config_id, body.input_data)
|
| 80 |
+
except ValueError as e:
|
| 81 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
@router.get("/jobs", response_model=List[RewardJobOut])
|
| 85 |
+
async def list_jobs_route(config_id: Optional[str] = Query(None), user: Dict = Depends(get_current_user)):
|
| 86 |
+
return await list_reward_jobs(user["user_id"], config_id)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
# --- Supported Algorithms ---
|
| 90 |
+
|
| 91 |
+
@router.get("/algorithms")
|
| 92 |
+
async def list_algorithms():
|
| 93 |
+
"""List all supported reward algorithms."""
|
| 94 |
+
return {
|
| 95 |
+
"algorithms": [
|
| 96 |
+
{"id": "grpo", "name": "GRPO", "description": "Group Relative Policy Optimization — rewards are relative within groups for stable training"},
|
| 97 |
+
{"id": "ppo", "name": "PPO", "description": "Proximal Policy Optimization — clipped surrogate objective for monotonic improvement"},
|
| 98 |
+
{"id": "dpo", "name": "DPO", "description": "Direct Preference Optimization — reward from preference pairs, no reward model needed"},
|
| 99 |
+
{"id": "reinforce", "name": "REINFORCE", "description": "Vanilla policy gradient with baseline subtraction"},
|
| 100 |
+
{"id": "custom", "name": "Custom", "description": "Define your own reward weights and scoring rules"},
|
| 101 |
+
]
|
| 102 |
+
}
|
agentic_rag_os/api/user_routes.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""User management routes — profile, API keys, dashboard stats."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import Any, Dict, List
|
| 6 |
+
|
| 7 |
+
from fastapi import APIRouter, Depends, HTTPException
|
| 8 |
+
|
| 9 |
+
from agentic_rag_os.api.deps import get_current_user
|
| 10 |
+
from agentic_rag_os.models.schemas import APIKeyCreate, APIKeyCreated, APIKeyOut, DashboardStats, UserOut
|
| 11 |
+
from agentic_rag_os.services import create_api_key, get_user_by_id, list_api_keys, revoke_api_key
|
| 12 |
+
from agentic_rag_os.models import fetch_all, fetch_one
|
| 13 |
+
from agentic_rag_os.config import get_settings
|
| 14 |
+
|
| 15 |
+
router = APIRouter(prefix="/user", tags=["User Management"])
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@router.get("/me", response_model=UserOut)
|
| 19 |
+
async def get_profile(user: Dict = Depends(get_current_user)):
|
| 20 |
+
u = await get_user_by_id(user["user_id"])
|
| 21 |
+
if not u:
|
| 22 |
+
raise HTTPException(status_code=404, detail="User not found")
|
| 23 |
+
return u
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@router.get("/dashboard", response_model=DashboardStats)
|
| 27 |
+
async def dashboard_stats(user: Dict = Depends(get_current_user)):
|
| 28 |
+
uid = user["user_id"]
|
| 29 |
+
settings = get_settings()
|
| 30 |
+
|
| 31 |
+
domains = await fetch_all("SELECT id FROM domains WHERE user_id=?", (uid,))
|
| 32 |
+
domain_ids = [d["id"] for d in domains]
|
| 33 |
+
|
| 34 |
+
doc_count = 0
|
| 35 |
+
total_size = 0
|
| 36 |
+
if domain_ids:
|
| 37 |
+
placeholders = ",".join("?" for _ in domain_ids)
|
| 38 |
+
docs = await fetch_all(f"SELECT size_bytes FROM documents WHERE domain_id IN ({placeholders})", tuple(domain_ids))
|
| 39 |
+
doc_count = len(docs)
|
| 40 |
+
total_size = sum(d["size_bytes"] for d in docs)
|
| 41 |
+
|
| 42 |
+
queries = await fetch_all("SELECT id FROM rag_queries WHERE user_id=?", (uid,))
|
| 43 |
+
configs = await fetch_all("SELECT id FROM reward_configs WHERE user_id=?", (uid,))
|
| 44 |
+
|
| 45 |
+
return {
|
| 46 |
+
"total_domains": len(domains),
|
| 47 |
+
"total_documents": doc_count,
|
| 48 |
+
"total_queries": len(queries),
|
| 49 |
+
"total_reward_configs": len(configs),
|
| 50 |
+
"storage_used_bytes": total_size,
|
| 51 |
+
"storage_limit_bytes": int(settings.max_upload_mb * 1024 * 1024),
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
# --- API Keys ---
|
| 56 |
+
|
| 57 |
+
@router.post("/api-keys", response_model=APIKeyCreated)
|
| 58 |
+
async def create_key_route(body: APIKeyCreate, user: Dict = Depends(get_current_user)):
|
| 59 |
+
return await create_api_key(user["user_id"], body.name)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
@router.get("/api-keys", response_model=List[APIKeyOut])
|
| 63 |
+
async def list_keys_route(user: Dict = Depends(get_current_user)):
|
| 64 |
+
return await list_api_keys(user["user_id"])
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
@router.delete("/api-keys/{key_id}")
|
| 68 |
+
async def revoke_key_route(key_id: str, user: Dict = Depends(get_current_user)):
|
| 69 |
+
ok = await revoke_api_key(user["user_id"], key_id)
|
| 70 |
+
if not ok:
|
| 71 |
+
raise HTTPException(status_code=404, detail="API key not found")
|
| 72 |
+
return {"ok": True}
|
agentic_rag_os/config.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Configuration for Agentic RAG OS."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
import secrets
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Optional
|
| 9 |
+
|
| 10 |
+
from pydantic import Field
|
| 11 |
+
from pydantic_settings import BaseSettings
|
| 12 |
+
|
| 13 |
+
_ROOT = Path(__file__).resolve().parent.parent
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class RagOSSettings(BaseSettings):
|
| 17 |
+
"""Central settings for the Agentic RAG OS application."""
|
| 18 |
+
|
| 19 |
+
# --- Application ---
|
| 20 |
+
app_name: str = "Agentic RAG OS"
|
| 21 |
+
secret_key: str = Field(default_factory=lambda: os.getenv("RAGOS_SECRET_KEY", secrets.token_hex(32)))
|
| 22 |
+
debug: bool = False
|
| 23 |
+
base_url: str = os.getenv("RAGOS_BASE_URL", "http://localhost:8000")
|
| 24 |
+
|
| 25 |
+
# --- Auth ---
|
| 26 |
+
jwt_algorithm: str = "HS256"
|
| 27 |
+
jwt_expiry_minutes: int = 1440 # 24h
|
| 28 |
+
github_client_id: str = os.getenv("GITHUB_CLIENT_ID", "")
|
| 29 |
+
github_client_secret: str = os.getenv("GITHUB_CLIENT_SECRET", "")
|
| 30 |
+
github_redirect_uri: str = os.getenv("GITHUB_REDIRECT_URI", "")
|
| 31 |
+
|
| 32 |
+
# --- Database (SQLite for simplicity, file-based) ---
|
| 33 |
+
db_path: Path = _ROOT / "agentic_rag_os" / "data" / "ragos.db"
|
| 34 |
+
|
| 35 |
+
# --- Upload limits ---
|
| 36 |
+
max_upload_mb: float = 2.0 # Free tier
|
| 37 |
+
premium_upload_mb: float = 100.0
|
| 38 |
+
|
| 39 |
+
# --- RAG Master bridge ---
|
| 40 |
+
gym_base_url: str = os.getenv("GYM_BASE_URL", "http://localhost:7860")
|
| 41 |
+
|
| 42 |
+
# --- Embedding ---
|
| 43 |
+
embedding_model: str = "sentence-transformers/all-MiniLM-L6-v2"
|
| 44 |
+
faiss_dimension: int = 384
|
| 45 |
+
|
| 46 |
+
# --- Server ---
|
| 47 |
+
host: str = "0.0.0.0"
|
| 48 |
+
port: int = 8000
|
| 49 |
+
|
| 50 |
+
model_config = {"env_prefix": "RAGOS_", "env_file": ".env", "extra": "ignore"}
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
_settings: Optional[RagOSSettings] = None
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def get_settings() -> RagOSSettings:
|
| 57 |
+
global _settings
|
| 58 |
+
if _settings is None:
|
| 59 |
+
_settings = RagOSSettings()
|
| 60 |
+
return _settings
|
agentic_rag_os/frontend/index.html
ADDED
|
@@ -0,0 +1,1936 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Agentic RAG OS — The Operating System for Agentic RAG</title>
|
| 7 |
+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚡</text></svg>">
|
| 8 |
+
<style>
|
| 9 |
+
/* ============================================================
|
| 10 |
+
AGENTIC RAG OS — Dark Theme Premium UI
|
| 11 |
+
============================================================ */
|
| 12 |
+
:root {
|
| 13 |
+
--bg-primary: #09090b;
|
| 14 |
+
--bg-secondary: #111113;
|
| 15 |
+
--bg-card: #16161a;
|
| 16 |
+
--bg-card-hover: #1c1c21;
|
| 17 |
+
--border: #27272a;
|
| 18 |
+
--border-light: #3f3f46;
|
| 19 |
+
--text-primary: #fafafa;
|
| 20 |
+
--text-secondary: #a1a1aa;
|
| 21 |
+
--text-muted: #71717a;
|
| 22 |
+
--accent: #6d28d9;
|
| 23 |
+
--accent-light: #8b5cf6;
|
| 24 |
+
--accent-glow: rgba(139, 92, 246, 0.15);
|
| 25 |
+
--accent-2: #06b6d4;
|
| 26 |
+
--accent-3: #10b981;
|
| 27 |
+
--gradient-1: linear-gradient(135deg, #6d28d9 0%, #06b6d4 100%);
|
| 28 |
+
--gradient-2: linear-gradient(135deg, #8b5cf6 0%, #06b6d4 50%, #10b981 100%);
|
| 29 |
+
--radius: 12px;
|
| 30 |
+
--radius-lg: 20px;
|
| 31 |
+
--shadow: 0 4px 6px -1px rgba(0,0,0,0.3);
|
| 32 |
+
--shadow-lg: 0 20px 40px -12px rgba(0,0,0,0.5);
|
| 33 |
+
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 37 |
+
|
| 38 |
+
html {
|
| 39 |
+
scroll-behavior: smooth;
|
| 40 |
+
font-size: 16px;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
body {
|
| 44 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', sans-serif;
|
| 45 |
+
background: var(--bg-primary);
|
| 46 |
+
color: var(--text-primary);
|
| 47 |
+
line-height: 1.6;
|
| 48 |
+
overflow-x: hidden;
|
| 49 |
+
-webkit-font-smoothing: antialiased;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
/* ---- Scrollbar ---- */
|
| 53 |
+
::-webkit-scrollbar { width: 6px; }
|
| 54 |
+
::-webkit-scrollbar-track { background: transparent; }
|
| 55 |
+
::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 3px; }
|
| 56 |
+
|
| 57 |
+
/* ---- Animations ---- */
|
| 58 |
+
@keyframes fadeInUp {
|
| 59 |
+
from { opacity: 0; transform: translateY(30px); }
|
| 60 |
+
to { opacity: 1; transform: translateY(0); }
|
| 61 |
+
}
|
| 62 |
+
@keyframes fadeIn {
|
| 63 |
+
from { opacity: 0; }
|
| 64 |
+
to { opacity: 1; }
|
| 65 |
+
}
|
| 66 |
+
@keyframes float {
|
| 67 |
+
0%, 100% { transform: translateY(0); }
|
| 68 |
+
50% { transform: translateY(-10px); }
|
| 69 |
+
}
|
| 70 |
+
@keyframes pulse-glow {
|
| 71 |
+
0%, 100% { box-shadow: 0 0 20px rgba(139, 92, 246, 0.1); }
|
| 72 |
+
50% { box-shadow: 0 0 40px rgba(139, 92, 246, 0.3); }
|
| 73 |
+
}
|
| 74 |
+
@keyframes gradient-shift {
|
| 75 |
+
0% { background-position: 0% 50%; }
|
| 76 |
+
50% { background-position: 100% 50%; }
|
| 77 |
+
100% { background-position: 0% 50%; }
|
| 78 |
+
}
|
| 79 |
+
@keyframes slide-in-right {
|
| 80 |
+
from { opacity: 0; transform: translateX(40px); }
|
| 81 |
+
to { opacity: 1; transform: translateX(0); }
|
| 82 |
+
}
|
| 83 |
+
@keyframes orbit {
|
| 84 |
+
from { transform: rotate(0deg) translateX(120px) rotate(0deg); }
|
| 85 |
+
to { transform: rotate(360deg) translateX(120px) rotate(-360deg); }
|
| 86 |
+
}
|
| 87 |
+
@keyframes typewriter {
|
| 88 |
+
from { width: 0; }
|
| 89 |
+
to { width: 100%; }
|
| 90 |
+
}
|
| 91 |
+
@keyframes blink {
|
| 92 |
+
50% { border-color: transparent; }
|
| 93 |
+
}
|
| 94 |
+
@keyframes count-up {
|
| 95 |
+
from { opacity: 0; transform: scale(0.5); }
|
| 96 |
+
to { opacity: 1; transform: scale(1); }
|
| 97 |
+
}
|
| 98 |
+
@keyframes shimmer {
|
| 99 |
+
0% { background-position: -200% 0; }
|
| 100 |
+
100% { background-position: 200% 0; }
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
.animate-in { animation: fadeInUp 0.7s ease-out forwards; opacity: 0; }
|
| 104 |
+
.animate-in:nth-child(2) { animation-delay: 0.1s; }
|
| 105 |
+
.animate-in:nth-child(3) { animation-delay: 0.2s; }
|
| 106 |
+
.animate-in:nth-child(4) { animation-delay: 0.3s; }
|
| 107 |
+
|
| 108 |
+
/* ---- Grid Background ---- */
|
| 109 |
+
.grid-bg {
|
| 110 |
+
position: fixed;
|
| 111 |
+
inset: 0;
|
| 112 |
+
background-image:
|
| 113 |
+
linear-gradient(rgba(139,92,246,0.03) 1px, transparent 1px),
|
| 114 |
+
linear-gradient(90deg, rgba(139,92,246,0.03) 1px, transparent 1px);
|
| 115 |
+
background-size: 60px 60px;
|
| 116 |
+
pointer-events: none;
|
| 117 |
+
z-index: 0;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
/* ---- Glow Orbs ---- */
|
| 121 |
+
.glow-orb {
|
| 122 |
+
position: absolute;
|
| 123 |
+
border-radius: 50%;
|
| 124 |
+
filter: blur(80px);
|
| 125 |
+
pointer-events: none;
|
| 126 |
+
z-index: 0;
|
| 127 |
+
}
|
| 128 |
+
.glow-orb-1 { width: 400px; height: 400px; background: rgba(109,40,217,0.15); top: -100px; left: -100px; }
|
| 129 |
+
.glow-orb-2 { width: 300px; height: 300px; background: rgba(6,182,212,0.1); top: 200px; right: -80px; }
|
| 130 |
+
.glow-orb-3 { width: 350px; height: 350px; background: rgba(16,185,129,0.08); bottom: -100px; left: 30%; }
|
| 131 |
+
|
| 132 |
+
/* ---- Container ---- */
|
| 133 |
+
.container {
|
| 134 |
+
max-width: 1200px;
|
| 135 |
+
margin: 0 auto;
|
| 136 |
+
padding: 0 24px;
|
| 137 |
+
position: relative;
|
| 138 |
+
z-index: 1;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
/* ============ NAVBAR ============ */
|
| 142 |
+
.navbar {
|
| 143 |
+
position: fixed;
|
| 144 |
+
top: 0;
|
| 145 |
+
left: 0;
|
| 146 |
+
right: 0;
|
| 147 |
+
z-index: 100;
|
| 148 |
+
background: rgba(9,9,11,0.8);
|
| 149 |
+
backdrop-filter: blur(20px);
|
| 150 |
+
border-bottom: 1px solid var(--border);
|
| 151 |
+
transition: var(--transition);
|
| 152 |
+
}
|
| 153 |
+
.navbar .container {
|
| 154 |
+
display: flex;
|
| 155 |
+
align-items: center;
|
| 156 |
+
justify-content: space-between;
|
| 157 |
+
height: 64px;
|
| 158 |
+
}
|
| 159 |
+
.nav-brand {
|
| 160 |
+
display: flex;
|
| 161 |
+
align-items: center;
|
| 162 |
+
gap: 10px;
|
| 163 |
+
font-size: 1.2rem;
|
| 164 |
+
font-weight: 700;
|
| 165 |
+
text-decoration: none;
|
| 166 |
+
color: var(--text-primary);
|
| 167 |
+
}
|
| 168 |
+
.nav-brand .logo {
|
| 169 |
+
width: 32px;
|
| 170 |
+
height: 32px;
|
| 171 |
+
background: var(--gradient-1);
|
| 172 |
+
border-radius: 8px;
|
| 173 |
+
display: flex;
|
| 174 |
+
align-items: center;
|
| 175 |
+
justify-content: center;
|
| 176 |
+
font-size: 18px;
|
| 177 |
+
}
|
| 178 |
+
.nav-links {
|
| 179 |
+
display: flex;
|
| 180 |
+
align-items: center;
|
| 181 |
+
gap: 32px;
|
| 182 |
+
list-style: none;
|
| 183 |
+
}
|
| 184 |
+
.nav-links a {
|
| 185 |
+
color: var(--text-secondary);
|
| 186 |
+
text-decoration: none;
|
| 187 |
+
font-size: 0.9rem;
|
| 188 |
+
font-weight: 500;
|
| 189 |
+
transition: var(--transition);
|
| 190 |
+
}
|
| 191 |
+
.nav-links a:hover { color: var(--text-primary); }
|
| 192 |
+
.nav-actions { display: flex; gap: 12px; align-items: center; }
|
| 193 |
+
|
| 194 |
+
/* ---- Buttons ---- */
|
| 195 |
+
.btn {
|
| 196 |
+
display: inline-flex;
|
| 197 |
+
align-items: center;
|
| 198 |
+
gap: 8px;
|
| 199 |
+
padding: 10px 20px;
|
| 200 |
+
border-radius: var(--radius);
|
| 201 |
+
font-size: 0.9rem;
|
| 202 |
+
font-weight: 600;
|
| 203 |
+
cursor: pointer;
|
| 204 |
+
border: none;
|
| 205 |
+
transition: var(--transition);
|
| 206 |
+
text-decoration: none;
|
| 207 |
+
}
|
| 208 |
+
.btn-primary {
|
| 209 |
+
background: var(--gradient-1);
|
| 210 |
+
color: white;
|
| 211 |
+
box-shadow: 0 4px 15px rgba(109,40,217,0.3);
|
| 212 |
+
}
|
| 213 |
+
.btn-primary:hover {
|
| 214 |
+
transform: translateY(-2px);
|
| 215 |
+
box-shadow: 0 8px 25px rgba(109,40,217,0.4);
|
| 216 |
+
}
|
| 217 |
+
.btn-secondary {
|
| 218 |
+
background: transparent;
|
| 219 |
+
color: var(--text-primary);
|
| 220 |
+
border: 1px solid var(--border-light);
|
| 221 |
+
}
|
| 222 |
+
.btn-secondary:hover {
|
| 223 |
+
background: var(--bg-card);
|
| 224 |
+
border-color: var(--accent-light);
|
| 225 |
+
}
|
| 226 |
+
.btn-ghost {
|
| 227 |
+
background: transparent;
|
| 228 |
+
color: var(--text-secondary);
|
| 229 |
+
padding: 8px 16px;
|
| 230 |
+
}
|
| 231 |
+
.btn-ghost:hover { color: var(--text-primary); }
|
| 232 |
+
.btn-lg { padding: 14px 32px; font-size: 1rem; border-radius: 14px; }
|
| 233 |
+
.btn-sm { padding: 6px 14px; font-size: 0.8rem; }
|
| 234 |
+
.btn-icon { padding: 8px; border-radius: 8px; }
|
| 235 |
+
|
| 236 |
+
/* ============ HERO ============ */
|
| 237 |
+
.hero {
|
| 238 |
+
padding: 160px 0 100px;
|
| 239 |
+
position: relative;
|
| 240 |
+
overflow: hidden;
|
| 241 |
+
text-align: center;
|
| 242 |
+
}
|
| 243 |
+
.hero-badge {
|
| 244 |
+
display: inline-flex;
|
| 245 |
+
align-items: center;
|
| 246 |
+
gap: 8px;
|
| 247 |
+
padding: 6px 16px;
|
| 248 |
+
background: var(--accent-glow);
|
| 249 |
+
border: 1px solid rgba(139,92,246,0.2);
|
| 250 |
+
border-radius: 100px;
|
| 251 |
+
font-size: 0.8rem;
|
| 252 |
+
color: var(--accent-light);
|
| 253 |
+
margin-bottom: 28px;
|
| 254 |
+
animation: fadeIn 0.8s ease-out;
|
| 255 |
+
}
|
| 256 |
+
.hero-badge .dot {
|
| 257 |
+
width: 6px;
|
| 258 |
+
height: 6px;
|
| 259 |
+
background: var(--accent-light);
|
| 260 |
+
border-radius: 50%;
|
| 261 |
+
animation: pulse-glow 2s ease-in-out infinite;
|
| 262 |
+
}
|
| 263 |
+
.hero h1 {
|
| 264 |
+
font-size: clamp(2.5rem, 6vw, 4.5rem);
|
| 265 |
+
font-weight: 800;
|
| 266 |
+
line-height: 1.1;
|
| 267 |
+
letter-spacing: -0.03em;
|
| 268 |
+
margin-bottom: 24px;
|
| 269 |
+
animation: fadeInUp 0.8s ease-out;
|
| 270 |
+
}
|
| 271 |
+
.hero h1 .gradient-text {
|
| 272 |
+
background: var(--gradient-2);
|
| 273 |
+
background-size: 200% auto;
|
| 274 |
+
-webkit-background-clip: text;
|
| 275 |
+
-webkit-text-fill-color: transparent;
|
| 276 |
+
animation: gradient-shift 4s ease infinite;
|
| 277 |
+
}
|
| 278 |
+
.hero-subtitle {
|
| 279 |
+
font-size: 1.25rem;
|
| 280 |
+
color: var(--text-secondary);
|
| 281 |
+
max-width: 640px;
|
| 282 |
+
margin: 0 auto 40px;
|
| 283 |
+
animation: fadeInUp 0.8s ease-out 0.2s forwards;
|
| 284 |
+
opacity: 0;
|
| 285 |
+
}
|
| 286 |
+
.hero-actions {
|
| 287 |
+
display: flex;
|
| 288 |
+
gap: 16px;
|
| 289 |
+
justify-content: center;
|
| 290 |
+
animation: fadeInUp 0.8s ease-out 0.4s forwards;
|
| 291 |
+
opacity: 0;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
/* Hero visual */
|
| 295 |
+
.hero-visual {
|
| 296 |
+
margin-top: 80px;
|
| 297 |
+
position: relative;
|
| 298 |
+
animation: fadeInUp 1s ease-out 0.6s forwards;
|
| 299 |
+
opacity: 0;
|
| 300 |
+
}
|
| 301 |
+
.hero-terminal {
|
| 302 |
+
background: var(--bg-card);
|
| 303 |
+
border: 1px solid var(--border);
|
| 304 |
+
border-radius: var(--radius-lg);
|
| 305 |
+
padding: 0;
|
| 306 |
+
max-width: 800px;
|
| 307 |
+
margin: 0 auto;
|
| 308 |
+
overflow: hidden;
|
| 309 |
+
box-shadow: var(--shadow-lg);
|
| 310 |
+
}
|
| 311 |
+
.terminal-header {
|
| 312 |
+
display: flex;
|
| 313 |
+
align-items: center;
|
| 314 |
+
gap: 8px;
|
| 315 |
+
padding: 12px 16px;
|
| 316 |
+
background: rgba(255,255,255,0.02);
|
| 317 |
+
border-bottom: 1px solid var(--border);
|
| 318 |
+
}
|
| 319 |
+
.terminal-dot {
|
| 320 |
+
width: 12px;
|
| 321 |
+
height: 12px;
|
| 322 |
+
border-radius: 50%;
|
| 323 |
+
}
|
| 324 |
+
.terminal-dot.red { background: #ff5f56; }
|
| 325 |
+
.terminal-dot.yellow { background: #ffbd2e; }
|
| 326 |
+
.terminal-dot.green { background: #27c93f; }
|
| 327 |
+
.terminal-title {
|
| 328 |
+
flex: 1;
|
| 329 |
+
text-align: center;
|
| 330 |
+
font-size: 0.8rem;
|
| 331 |
+
color: var(--text-muted);
|
| 332 |
+
}
|
| 333 |
+
.terminal-body {
|
| 334 |
+
padding: 24px;
|
| 335 |
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
| 336 |
+
font-size: 0.85rem;
|
| 337 |
+
line-height: 1.8;
|
| 338 |
+
color: var(--text-secondary);
|
| 339 |
+
}
|
| 340 |
+
.terminal-body .cmd { color: var(--accent-2); }
|
| 341 |
+
.terminal-body .str { color: var(--accent-3); }
|
| 342 |
+
.terminal-body .comment { color: var(--text-muted); }
|
| 343 |
+
.terminal-body .num { color: #f59e0b; }
|
| 344 |
+
|
| 345 |
+
/* ============ LOGOS BAR ============ */
|
| 346 |
+
.logos-bar {
|
| 347 |
+
padding: 60px 0;
|
| 348 |
+
border-top: 1px solid var(--border);
|
| 349 |
+
border-bottom: 1px solid var(--border);
|
| 350 |
+
}
|
| 351 |
+
.logos-bar p {
|
| 352 |
+
text-align: center;
|
| 353 |
+
font-size: 0.8rem;
|
| 354 |
+
color: var(--text-muted);
|
| 355 |
+
text-transform: uppercase;
|
| 356 |
+
letter-spacing: 0.1em;
|
| 357 |
+
margin-bottom: 30px;
|
| 358 |
+
}
|
| 359 |
+
.logos-grid {
|
| 360 |
+
display: flex;
|
| 361 |
+
justify-content: center;
|
| 362 |
+
align-items: center;
|
| 363 |
+
gap: 48px;
|
| 364 |
+
flex-wrap: wrap;
|
| 365 |
+
}
|
| 366 |
+
.logo-item {
|
| 367 |
+
font-size: 1rem;
|
| 368 |
+
font-weight: 600;
|
| 369 |
+
color: var(--text-muted);
|
| 370 |
+
transition: var(--transition);
|
| 371 |
+
display: flex;
|
| 372 |
+
align-items: center;
|
| 373 |
+
gap: 8px;
|
| 374 |
+
}
|
| 375 |
+
.logo-item:hover { color: var(--text-secondary); }
|
| 376 |
+
.logo-item span { font-size: 1.3rem; }
|
| 377 |
+
|
| 378 |
+
/* ============ SECTION BASE ============ */
|
| 379 |
+
section {
|
| 380 |
+
padding: 100px 0;
|
| 381 |
+
position: relative;
|
| 382 |
+
}
|
| 383 |
+
.section-label {
|
| 384 |
+
font-size: 0.8rem;
|
| 385 |
+
text-transform: uppercase;
|
| 386 |
+
letter-spacing: 0.15em;
|
| 387 |
+
color: var(--accent-light);
|
| 388 |
+
font-weight: 600;
|
| 389 |
+
margin-bottom: 12px;
|
| 390 |
+
}
|
| 391 |
+
.section-title {
|
| 392 |
+
font-size: clamp(1.8rem, 4vw, 2.8rem);
|
| 393 |
+
font-weight: 800;
|
| 394 |
+
letter-spacing: -0.02em;
|
| 395 |
+
margin-bottom: 16px;
|
| 396 |
+
}
|
| 397 |
+
.section-desc {
|
| 398 |
+
font-size: 1.1rem;
|
| 399 |
+
color: var(--text-secondary);
|
| 400 |
+
max-width: 600px;
|
| 401 |
+
margin-bottom: 60px;
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
/* ============ FEATURES ============ */
|
| 405 |
+
.features-grid {
|
| 406 |
+
display: grid;
|
| 407 |
+
grid-template-columns: repeat(3, 1fr);
|
| 408 |
+
gap: 24px;
|
| 409 |
+
}
|
| 410 |
+
.feature-card {
|
| 411 |
+
background: var(--bg-card);
|
| 412 |
+
border: 1px solid var(--border);
|
| 413 |
+
border-radius: var(--radius-lg);
|
| 414 |
+
padding: 32px;
|
| 415 |
+
transition: var(--transition);
|
| 416 |
+
position: relative;
|
| 417 |
+
overflow: hidden;
|
| 418 |
+
}
|
| 419 |
+
.feature-card::before {
|
| 420 |
+
content: '';
|
| 421 |
+
position: absolute;
|
| 422 |
+
top: 0;
|
| 423 |
+
left: 0;
|
| 424 |
+
right: 0;
|
| 425 |
+
height: 2px;
|
| 426 |
+
background: var(--gradient-1);
|
| 427 |
+
opacity: 0;
|
| 428 |
+
transition: var(--transition);
|
| 429 |
+
}
|
| 430 |
+
.feature-card:hover {
|
| 431 |
+
border-color: var(--border-light);
|
| 432 |
+
transform: translateY(-4px);
|
| 433 |
+
box-shadow: var(--shadow-lg);
|
| 434 |
+
}
|
| 435 |
+
.feature-card:hover::before { opacity: 1; }
|
| 436 |
+
.feature-icon {
|
| 437 |
+
width: 48px;
|
| 438 |
+
height: 48px;
|
| 439 |
+
border-radius: 12px;
|
| 440 |
+
display: flex;
|
| 441 |
+
align-items: center;
|
| 442 |
+
justify-content: center;
|
| 443 |
+
font-size: 24px;
|
| 444 |
+
margin-bottom: 20px;
|
| 445 |
+
background: var(--accent-glow);
|
| 446 |
+
}
|
| 447 |
+
.feature-card h3 {
|
| 448 |
+
font-size: 1.1rem;
|
| 449 |
+
font-weight: 700;
|
| 450 |
+
margin-bottom: 8px;
|
| 451 |
+
}
|
| 452 |
+
.feature-card p {
|
| 453 |
+
color: var(--text-secondary);
|
| 454 |
+
font-size: 0.9rem;
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
/* ============ HOW IT WORKS ============ */
|
| 458 |
+
.steps-container {
|
| 459 |
+
display: grid;
|
| 460 |
+
grid-template-columns: repeat(4, 1fr);
|
| 461 |
+
gap: 24px;
|
| 462 |
+
position: relative;
|
| 463 |
+
}
|
| 464 |
+
.steps-container::before {
|
| 465 |
+
content: '';
|
| 466 |
+
position: absolute;
|
| 467 |
+
top: 36px;
|
| 468 |
+
left: 60px;
|
| 469 |
+
right: 60px;
|
| 470 |
+
height: 2px;
|
| 471 |
+
background: linear-gradient(90deg, var(--accent) 0%, var(--accent-2) 50%, var(--accent-3) 100%);
|
| 472 |
+
opacity: 0.3;
|
| 473 |
+
}
|
| 474 |
+
.step-item {
|
| 475 |
+
text-align: center;
|
| 476 |
+
position: relative;
|
| 477 |
+
}
|
| 478 |
+
.step-number {
|
| 479 |
+
width: 72px;
|
| 480 |
+
height: 72px;
|
| 481 |
+
border-radius: 50%;
|
| 482 |
+
background: var(--bg-card);
|
| 483 |
+
border: 2px solid var(--border);
|
| 484 |
+
display: flex;
|
| 485 |
+
align-items: center;
|
| 486 |
+
justify-content: center;
|
| 487 |
+
margin: 0 auto 20px;
|
| 488 |
+
font-size: 1.5rem;
|
| 489 |
+
font-weight: 800;
|
| 490 |
+
color: var(--accent-light);
|
| 491 |
+
transition: var(--transition);
|
| 492 |
+
position: relative;
|
| 493 |
+
z-index: 1;
|
| 494 |
+
}
|
| 495 |
+
.step-item:hover .step-number {
|
| 496 |
+
border-color: var(--accent-light);
|
| 497 |
+
box-shadow: 0 0 30px rgba(139,92,246,0.2);
|
| 498 |
+
}
|
| 499 |
+
.step-item h3 {
|
| 500 |
+
font-size: 1rem;
|
| 501 |
+
font-weight: 700;
|
| 502 |
+
margin-bottom: 8px;
|
| 503 |
+
}
|
| 504 |
+
.step-item p {
|
| 505 |
+
font-size: 0.85rem;
|
| 506 |
+
color: var(--text-secondary);
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
/* ============ PRICING ============ */
|
| 510 |
+
.pricing-grid {
|
| 511 |
+
display: grid;
|
| 512 |
+
grid-template-columns: repeat(2, 1fr);
|
| 513 |
+
gap: 24px;
|
| 514 |
+
max-width: 800px;
|
| 515 |
+
margin: 0 auto;
|
| 516 |
+
}
|
| 517 |
+
.price-card {
|
| 518 |
+
background: var(--bg-card);
|
| 519 |
+
border: 1px solid var(--border);
|
| 520 |
+
border-radius: var(--radius-lg);
|
| 521 |
+
padding: 40px;
|
| 522 |
+
transition: var(--transition);
|
| 523 |
+
}
|
| 524 |
+
.price-card.featured {
|
| 525 |
+
border-color: var(--accent);
|
| 526 |
+
box-shadow: 0 0 40px rgba(109,40,217,0.15);
|
| 527 |
+
position: relative;
|
| 528 |
+
}
|
| 529 |
+
.price-card.featured::before {
|
| 530 |
+
content: 'POPULAR';
|
| 531 |
+
position: absolute;
|
| 532 |
+
top: -12px;
|
| 533 |
+
right: 24px;
|
| 534 |
+
background: var(--gradient-1);
|
| 535 |
+
color: white;
|
| 536 |
+
font-size: 0.7rem;
|
| 537 |
+
font-weight: 700;
|
| 538 |
+
padding: 4px 12px;
|
| 539 |
+
border-radius: 100px;
|
| 540 |
+
}
|
| 541 |
+
.price-card h3 {
|
| 542 |
+
font-size: 1.2rem;
|
| 543 |
+
font-weight: 700;
|
| 544 |
+
margin-bottom: 8px;
|
| 545 |
+
}
|
| 546 |
+
.price-card .price {
|
| 547 |
+
font-size: 2.5rem;
|
| 548 |
+
font-weight: 800;
|
| 549 |
+
margin: 16px 0;
|
| 550 |
+
}
|
| 551 |
+
.price-card .price span { font-size: 1rem; color: var(--text-muted); font-weight: 400; }
|
| 552 |
+
.price-card ul {
|
| 553 |
+
list-style: none;
|
| 554 |
+
margin: 24px 0;
|
| 555 |
+
}
|
| 556 |
+
.price-card li {
|
| 557 |
+
padding: 8px 0;
|
| 558 |
+
font-size: 0.9rem;
|
| 559 |
+
color: var(--text-secondary);
|
| 560 |
+
display: flex;
|
| 561 |
+
align-items: center;
|
| 562 |
+
gap: 8px;
|
| 563 |
+
}
|
| 564 |
+
.price-card li::before {
|
| 565 |
+
content: '✓';
|
| 566 |
+
color: var(--accent-3);
|
| 567 |
+
font-weight: 700;
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
/* ============ API DOCS ============ */
|
| 571 |
+
.api-section { background: var(--bg-secondary); border-radius: var(--radius-lg); padding: 60px; margin-top: 40px; }
|
| 572 |
+
.api-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; }
|
| 573 |
+
.api-card {
|
| 574 |
+
background: var(--bg-primary);
|
| 575 |
+
border: 1px solid var(--border);
|
| 576 |
+
border-radius: var(--radius);
|
| 577 |
+
padding: 20px;
|
| 578 |
+
transition: var(--transition);
|
| 579 |
+
}
|
| 580 |
+
.api-card:hover {
|
| 581 |
+
border-color: var(--border-light);
|
| 582 |
+
}
|
| 583 |
+
.api-method {
|
| 584 |
+
display: inline-block;
|
| 585 |
+
padding: 2px 8px;
|
| 586 |
+
border-radius: 4px;
|
| 587 |
+
font-size: 0.7rem;
|
| 588 |
+
font-weight: 700;
|
| 589 |
+
font-family: monospace;
|
| 590 |
+
margin-right: 8px;
|
| 591 |
+
}
|
| 592 |
+
.api-method.get { background: rgba(16,185,129,0.15); color: #10b981; }
|
| 593 |
+
.api-method.post { background: rgba(59,130,246,0.15); color: #3b82f6; }
|
| 594 |
+
.api-method.delete { background: rgba(239,68,68,0.15); color: #ef4444; }
|
| 595 |
+
.api-path {
|
| 596 |
+
font-family: 'SF Mono', monospace;
|
| 597 |
+
font-size: 0.85rem;
|
| 598 |
+
color: var(--text-primary);
|
| 599 |
+
}
|
| 600 |
+
.api-desc {
|
| 601 |
+
font-size: 0.8rem;
|
| 602 |
+
color: var(--text-muted);
|
| 603 |
+
margin-top: 8px;
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
/* ============ CTA ============ */
|
| 607 |
+
.cta-section {
|
| 608 |
+
text-align: center;
|
| 609 |
+
padding: 120px 0;
|
| 610 |
+
position: relative;
|
| 611 |
+
}
|
| 612 |
+
.cta-section::before {
|
| 613 |
+
content: '';
|
| 614 |
+
position: absolute;
|
| 615 |
+
inset: 0;
|
| 616 |
+
background: radial-gradient(ellipse at center, rgba(109,40,217,0.08) 0%, transparent 70%);
|
| 617 |
+
pointer-events: none;
|
| 618 |
+
}
|
| 619 |
+
.cta-section h2 {
|
| 620 |
+
font-size: clamp(2rem, 4vw, 3rem);
|
| 621 |
+
font-weight: 800;
|
| 622 |
+
margin-bottom: 16px;
|
| 623 |
+
}
|
| 624 |
+
.cta-section p {
|
| 625 |
+
font-size: 1.1rem;
|
| 626 |
+
color: var(--text-secondary);
|
| 627 |
+
margin-bottom: 40px;
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
/* ============ FOOTER ============ */
|
| 631 |
+
.footer {
|
| 632 |
+
padding: 60px 0 30px;
|
| 633 |
+
border-top: 1px solid var(--border);
|
| 634 |
+
}
|
| 635 |
+
.footer-grid {
|
| 636 |
+
display: grid;
|
| 637 |
+
grid-template-columns: 2fr 1fr 1fr 1fr;
|
| 638 |
+
gap: 40px;
|
| 639 |
+
margin-bottom: 40px;
|
| 640 |
+
}
|
| 641 |
+
.footer-brand h3 { font-size: 1.1rem; font-weight: 700; margin-bottom: 8px; }
|
| 642 |
+
.footer-brand p { color: var(--text-muted); font-size: 0.85rem; }
|
| 643 |
+
.footer h4 { font-size: 0.85rem; font-weight: 600; margin-bottom: 16px; color: var(--text-secondary); }
|
| 644 |
+
.footer ul { list-style: none; }
|
| 645 |
+
.footer li { margin-bottom: 8px; }
|
| 646 |
+
.footer a { color: var(--text-muted); text-decoration: none; font-size: 0.85rem; transition: var(--transition); }
|
| 647 |
+
.footer a:hover { color: var(--text-primary); }
|
| 648 |
+
.footer-bottom {
|
| 649 |
+
display: flex;
|
| 650 |
+
justify-content: space-between;
|
| 651 |
+
align-items: center;
|
| 652 |
+
padding-top: 24px;
|
| 653 |
+
border-top: 1px solid var(--border);
|
| 654 |
+
font-size: 0.8rem;
|
| 655 |
+
color: var(--text-muted);
|
| 656 |
+
}
|
| 657 |
+
|
| 658 |
+
/* ============ DASHBOARD (SPA views) ============ */
|
| 659 |
+
.app-view { display: none; min-height: 100vh; padding-top: 80px; }
|
| 660 |
+
.app-view.active { display: block; }
|
| 661 |
+
|
| 662 |
+
.dashboard-header {
|
| 663 |
+
display: flex;
|
| 664 |
+
justify-content: space-between;
|
| 665 |
+
align-items: center;
|
| 666 |
+
margin-bottom: 32px;
|
| 667 |
+
}
|
| 668 |
+
.dashboard-header h1 { font-size: 1.8rem; font-weight: 700; }
|
| 669 |
+
|
| 670 |
+
.stats-grid {
|
| 671 |
+
display: grid;
|
| 672 |
+
grid-template-columns: repeat(4, 1fr);
|
| 673 |
+
gap: 20px;
|
| 674 |
+
margin-bottom: 40px;
|
| 675 |
+
}
|
| 676 |
+
.stat-card {
|
| 677 |
+
background: var(--bg-card);
|
| 678 |
+
border: 1px solid var(--border);
|
| 679 |
+
border-radius: var(--radius);
|
| 680 |
+
padding: 24px;
|
| 681 |
+
transition: var(--transition);
|
| 682 |
+
}
|
| 683 |
+
.stat-card:hover { border-color: var(--border-light); }
|
| 684 |
+
.stat-label { font-size: 0.8rem; color: var(--text-muted); margin-bottom: 8px; }
|
| 685 |
+
.stat-value { font-size: 2rem; font-weight: 800; }
|
| 686 |
+
.stat-value.accent { color: var(--accent-light); }
|
| 687 |
+
|
| 688 |
+
.card {
|
| 689 |
+
background: var(--bg-card);
|
| 690 |
+
border: 1px solid var(--border);
|
| 691 |
+
border-radius: var(--radius-lg);
|
| 692 |
+
padding: 32px;
|
| 693 |
+
margin-bottom: 24px;
|
| 694 |
+
}
|
| 695 |
+
.card h2 { font-size: 1.3rem; font-weight: 700; margin-bottom: 20px; }
|
| 696 |
+
|
| 697 |
+
/* Tables */
|
| 698 |
+
.data-table { width: 100%; border-collapse: collapse; }
|
| 699 |
+
.data-table th {
|
| 700 |
+
text-align: left;
|
| 701 |
+
padding: 12px 16px;
|
| 702 |
+
font-size: 0.8rem;
|
| 703 |
+
font-weight: 600;
|
| 704 |
+
color: var(--text-muted);
|
| 705 |
+
text-transform: uppercase;
|
| 706 |
+
letter-spacing: 0.05em;
|
| 707 |
+
border-bottom: 1px solid var(--border);
|
| 708 |
+
}
|
| 709 |
+
.data-table td {
|
| 710 |
+
padding: 12px 16px;
|
| 711 |
+
font-size: 0.9rem;
|
| 712 |
+
border-bottom: 1px solid var(--border);
|
| 713 |
+
color: var(--text-secondary);
|
| 714 |
+
}
|
| 715 |
+
.data-table tr:hover td { background: rgba(255,255,255,0.02); }
|
| 716 |
+
|
| 717 |
+
/* Forms */
|
| 718 |
+
.form-group { margin-bottom: 20px; }
|
| 719 |
+
.form-group label {
|
| 720 |
+
display: block;
|
| 721 |
+
font-size: 0.85rem;
|
| 722 |
+
font-weight: 600;
|
| 723 |
+
margin-bottom: 6px;
|
| 724 |
+
color: var(--text-secondary);
|
| 725 |
+
}
|
| 726 |
+
.form-input {
|
| 727 |
+
width: 100%;
|
| 728 |
+
padding: 10px 14px;
|
| 729 |
+
background: var(--bg-primary);
|
| 730 |
+
border: 1px solid var(--border);
|
| 731 |
+
border-radius: 8px;
|
| 732 |
+
color: var(--text-primary);
|
| 733 |
+
font-size: 0.9rem;
|
| 734 |
+
transition: var(--transition);
|
| 735 |
+
}
|
| 736 |
+
.form-input:focus {
|
| 737 |
+
outline: none;
|
| 738 |
+
border-color: var(--accent-light);
|
| 739 |
+
box-shadow: 0 0 0 3px var(--accent-glow);
|
| 740 |
+
}
|
| 741 |
+
.form-select {
|
| 742 |
+
appearance: none;
|
| 743 |
+
width: 100%;
|
| 744 |
+
padding: 10px 14px;
|
| 745 |
+
background: var(--bg-primary) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' fill='%2371717a'%3E%3Cpath d='M6 8L0 0h12z'/%3E%3C/svg%3E") no-repeat right 14px center;
|
| 746 |
+
border: 1px solid var(--border);
|
| 747 |
+
border-radius: 8px;
|
| 748 |
+
color: var(--text-primary);
|
| 749 |
+
font-size: 0.9rem;
|
| 750 |
+
cursor: pointer;
|
| 751 |
+
}
|
| 752 |
+
textarea.form-input {
|
| 753 |
+
resize: vertical;
|
| 754 |
+
min-height: 100px;
|
| 755 |
+
font-family: 'SF Mono', monospace;
|
| 756 |
+
font-size: 0.85rem;
|
| 757 |
+
}
|
| 758 |
+
|
| 759 |
+
/* Tags */
|
| 760 |
+
.tag {
|
| 761 |
+
display: inline-block;
|
| 762 |
+
padding: 2px 10px;
|
| 763 |
+
border-radius: 100px;
|
| 764 |
+
font-size: 0.75rem;
|
| 765 |
+
font-weight: 600;
|
| 766 |
+
}
|
| 767 |
+
.tag-success { background: rgba(16,185,129,0.15); color: #10b981; }
|
| 768 |
+
.tag-info { background: rgba(59,130,246,0.15); color: #3b82f6; }
|
| 769 |
+
.tag-warning { background: rgba(245,158,11,0.15); color: #f59e0b; }
|
| 770 |
+
|
| 771 |
+
/* Modal */
|
| 772 |
+
.modal-overlay {
|
| 773 |
+
display: none;
|
| 774 |
+
position: fixed;
|
| 775 |
+
inset: 0;
|
| 776 |
+
background: rgba(0,0,0,0.6);
|
| 777 |
+
backdrop-filter: blur(4px);
|
| 778 |
+
z-index: 200;
|
| 779 |
+
align-items: center;
|
| 780 |
+
justify-content: center;
|
| 781 |
+
}
|
| 782 |
+
.modal-overlay.active { display: flex; }
|
| 783 |
+
.modal {
|
| 784 |
+
background: var(--bg-card);
|
| 785 |
+
border: 1px solid var(--border);
|
| 786 |
+
border-radius: var(--radius-lg);
|
| 787 |
+
padding: 32px;
|
| 788 |
+
width: 90%;
|
| 789 |
+
max-width: 500px;
|
| 790 |
+
animation: fadeInUp 0.3s ease-out;
|
| 791 |
+
}
|
| 792 |
+
.modal h2 { font-size: 1.3rem; margin-bottom: 20px; }
|
| 793 |
+
.modal-actions { display: flex; gap: 12px; justify-content: flex-end; margin-top: 24px; }
|
| 794 |
+
|
| 795 |
+
/* Toast */
|
| 796 |
+
.toast-container {
|
| 797 |
+
position: fixed;
|
| 798 |
+
top: 80px;
|
| 799 |
+
right: 24px;
|
| 800 |
+
z-index: 300;
|
| 801 |
+
display: flex;
|
| 802 |
+
flex-direction: column;
|
| 803 |
+
gap: 8px;
|
| 804 |
+
}
|
| 805 |
+
.toast {
|
| 806 |
+
background: var(--bg-card);
|
| 807 |
+
border: 1px solid var(--border);
|
| 808 |
+
border-radius: var(--radius);
|
| 809 |
+
padding: 12px 20px;
|
| 810 |
+
font-size: 0.85rem;
|
| 811 |
+
animation: slide-in-right 0.3s ease-out;
|
| 812 |
+
box-shadow: var(--shadow-lg);
|
| 813 |
+
max-width: 360px;
|
| 814 |
+
}
|
| 815 |
+
.toast.success { border-left: 3px solid var(--accent-3); }
|
| 816 |
+
.toast.error { border-left: 3px solid #ef4444; }
|
| 817 |
+
.toast.info { border-left: 3px solid var(--accent-2); }
|
| 818 |
+
|
| 819 |
+
/* Auth Modal */
|
| 820 |
+
.auth-tabs { display: flex; gap: 0; margin-bottom: 24px; }
|
| 821 |
+
.auth-tab {
|
| 822 |
+
flex: 1;
|
| 823 |
+
padding: 12px;
|
| 824 |
+
text-align: center;
|
| 825 |
+
cursor: pointer;
|
| 826 |
+
font-weight: 600;
|
| 827 |
+
font-size: 0.9rem;
|
| 828 |
+
color: var(--text-muted);
|
| 829 |
+
border-bottom: 2px solid var(--border);
|
| 830 |
+
transition: var(--transition);
|
| 831 |
+
background: none;
|
| 832 |
+
border-top: none;
|
| 833 |
+
border-left: none;
|
| 834 |
+
border-right: none;
|
| 835 |
+
}
|
| 836 |
+
.auth-tab.active { color: var(--accent-light); border-bottom-color: var(--accent-light); }
|
| 837 |
+
.auth-divider {
|
| 838 |
+
display: flex;
|
| 839 |
+
align-items: center;
|
| 840 |
+
gap: 16px;
|
| 841 |
+
margin: 20px 0;
|
| 842 |
+
color: var(--text-muted);
|
| 843 |
+
font-size: 0.8rem;
|
| 844 |
+
}
|
| 845 |
+
.auth-divider::before, .auth-divider::after {
|
| 846 |
+
content: '';
|
| 847 |
+
flex: 1;
|
| 848 |
+
height: 1px;
|
| 849 |
+
background: var(--border);
|
| 850 |
+
}
|
| 851 |
+
.github-btn {
|
| 852 |
+
width: 100%;
|
| 853 |
+
padding: 12px;
|
| 854 |
+
background: #24292e;
|
| 855 |
+
color: white;
|
| 856 |
+
border: 1px solid #444;
|
| 857 |
+
border-radius: 8px;
|
| 858 |
+
font-size: 0.9rem;
|
| 859 |
+
font-weight: 600;
|
| 860 |
+
cursor: pointer;
|
| 861 |
+
display: flex;
|
| 862 |
+
align-items: center;
|
| 863 |
+
justify-content: center;
|
| 864 |
+
gap: 10px;
|
| 865 |
+
transition: var(--transition);
|
| 866 |
+
}
|
| 867 |
+
.github-btn:hover { background: #2d333b; }
|
| 868 |
+
.github-btn svg { width: 20px; height: 20px; fill: white; }
|
| 869 |
+
|
| 870 |
+
/* Reward playground */
|
| 871 |
+
.reward-playground {
|
| 872 |
+
background: var(--bg-secondary);
|
| 873 |
+
border: 1px solid var(--border);
|
| 874 |
+
border-radius: var(--radius-lg);
|
| 875 |
+
padding: 32px;
|
| 876 |
+
}
|
| 877 |
+
.reward-result {
|
| 878 |
+
margin-top: 24px;
|
| 879 |
+
padding: 24px;
|
| 880 |
+
background: var(--bg-primary);
|
| 881 |
+
border: 1px solid var(--border);
|
| 882 |
+
border-radius: var(--radius);
|
| 883 |
+
}
|
| 884 |
+
.reward-score {
|
| 885 |
+
font-size: 3rem;
|
| 886 |
+
font-weight: 800;
|
| 887 |
+
text-align: center;
|
| 888 |
+
margin: 16px 0;
|
| 889 |
+
}
|
| 890 |
+
.reward-breakdown {
|
| 891 |
+
display: grid;
|
| 892 |
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
| 893 |
+
gap: 12px;
|
| 894 |
+
}
|
| 895 |
+
.reward-bar {
|
| 896 |
+
background: var(--bg-card);
|
| 897 |
+
border-radius: 8px;
|
| 898 |
+
padding: 12px;
|
| 899 |
+
}
|
| 900 |
+
.reward-bar-label {
|
| 901 |
+
font-size: 0.75rem;
|
| 902 |
+
color: var(--text-muted);
|
| 903 |
+
margin-bottom: 8px;
|
| 904 |
+
}
|
| 905 |
+
.reward-bar-track {
|
| 906 |
+
width: 100%;
|
| 907 |
+
height: 6px;
|
| 908 |
+
background: var(--border);
|
| 909 |
+
border-radius: 3px;
|
| 910 |
+
overflow: hidden;
|
| 911 |
+
}
|
| 912 |
+
.reward-bar-fill {
|
| 913 |
+
height: 100%;
|
| 914 |
+
border-radius: 3px;
|
| 915 |
+
transition: width 0.5s ease;
|
| 916 |
+
}
|
| 917 |
+
|
| 918 |
+
/* Mobile sidebar for dashboard */
|
| 919 |
+
.sidebar { display: none; }
|
| 920 |
+
|
| 921 |
+
/* ============ RESPONSIVE ============ */
|
| 922 |
+
@media (max-width: 1024px) {
|
| 923 |
+
.features-grid { grid-template-columns: repeat(2, 1fr); }
|
| 924 |
+
.steps-container { grid-template-columns: repeat(2, 1fr); }
|
| 925 |
+
.steps-container::before { display: none; }
|
| 926 |
+
.footer-grid { grid-template-columns: repeat(2, 1fr); }
|
| 927 |
+
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
| 928 |
+
.api-grid { grid-template-columns: 1fr; }
|
| 929 |
+
}
|
| 930 |
+
@media (max-width: 768px) {
|
| 931 |
+
.nav-links { display: none; }
|
| 932 |
+
.features-grid { grid-template-columns: 1fr; }
|
| 933 |
+
.pricing-grid { grid-template-columns: 1fr; }
|
| 934 |
+
.steps-container { grid-template-columns: 1fr; }
|
| 935 |
+
.footer-grid { grid-template-columns: 1fr; }
|
| 936 |
+
.hero h1 { font-size: 2.2rem; }
|
| 937 |
+
.stats-grid { grid-template-columns: 1fr 1fr; }
|
| 938 |
+
}
|
| 939 |
+
</style>
|
| 940 |
+
</head>
|
| 941 |
+
<body>
|
| 942 |
+
<div class="grid-bg"></div>
|
| 943 |
+
<div class="toast-container" id="toastContainer"></div>
|
| 944 |
+
|
| 945 |
+
<!-- ============ NAVBAR ============ -->
|
| 946 |
+
<nav class="navbar" id="navbar">
|
| 947 |
+
<div class="container">
|
| 948 |
+
<a href="#" class="nav-brand" onclick="showView('landing')">
|
| 949 |
+
<div class="logo">⚡</div>
|
| 950 |
+
<span>Agentic RAG OS</span>
|
| 951 |
+
</a>
|
| 952 |
+
<ul class="nav-links" id="navLinks">
|
| 953 |
+
<li><a href="#features" onclick="showView('landing')">Features</a></li>
|
| 954 |
+
<li><a href="#how-it-works" onclick="showView('landing')">How It Works</a></li>
|
| 955 |
+
<li><a href="#pricing" onclick="showView('landing')">Pricing</a></li>
|
| 956 |
+
<li><a href="#api-reference" onclick="showView('landing')">API</a></li>
|
| 957 |
+
<li><a href="/api/docs" target="_blank">Docs</a></li>
|
| 958 |
+
</ul>
|
| 959 |
+
<div class="nav-actions" id="navActions">
|
| 960 |
+
<button class="btn btn-ghost" id="navLoginBtn" onclick="showAuthModal()">Log In</button>
|
| 961 |
+
<button class="btn btn-primary" id="navSignupBtn" onclick="showAuthModal('register')">Get Started</button>
|
| 962 |
+
<div style="display:none" id="navUserMenu">
|
| 963 |
+
<button class="btn btn-secondary btn-sm" onclick="showView('dashboard')">Dashboard</button>
|
| 964 |
+
<button class="btn btn-ghost btn-sm" onclick="logout()">Logout</button>
|
| 965 |
+
</div>
|
| 966 |
+
</div>
|
| 967 |
+
</div>
|
| 968 |
+
</nav>
|
| 969 |
+
|
| 970 |
+
<!-- ============ AUTH MODAL ============ -->
|
| 971 |
+
<div class="modal-overlay" id="authModal">
|
| 972 |
+
<div class="modal">
|
| 973 |
+
<div class="auth-tabs">
|
| 974 |
+
<button class="auth-tab active" id="tabLogin" onclick="switchAuthTab('login')">Sign In</button>
|
| 975 |
+
<button class="auth-tab" id="tabRegister" onclick="switchAuthTab('register')">Create Account</button>
|
| 976 |
+
</div>
|
| 977 |
+
|
| 978 |
+
<button class="github-btn" onclick="githubLogin()">
|
| 979 |
+
<svg viewBox="0 0 24 24"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
|
| 980 |
+
Continue with GitHub
|
| 981 |
+
</button>
|
| 982 |
+
|
| 983 |
+
<div class="auth-divider">or</div>
|
| 984 |
+
|
| 985 |
+
<form id="authForm" onsubmit="handleAuth(event)">
|
| 986 |
+
<div class="form-group" id="usernameGroup">
|
| 987 |
+
<label>Username</label>
|
| 988 |
+
<input type="text" class="form-input" id="authUsername" placeholder="your_username" required>
|
| 989 |
+
</div>
|
| 990 |
+
<div class="form-group" id="emailGroup" style="display:none">
|
| 991 |
+
<label>Email</label>
|
| 992 |
+
<input type="email" class="form-input" id="authEmail" placeholder="you@company.com">
|
| 993 |
+
</div>
|
| 994 |
+
<div class="form-group">
|
| 995 |
+
<label>Password</label>
|
| 996 |
+
<input type="password" class="form-input" id="authPassword" placeholder="••••••••" required minlength="8">
|
| 997 |
+
</div>
|
| 998 |
+
<button type="submit" class="btn btn-primary" style="width:100%; justify-content:center; margin-top:8px;" id="authSubmitBtn">Sign In</button>
|
| 999 |
+
</form>
|
| 1000 |
+
|
| 1001 |
+
<div style="text-align:center; margin-top:16px">
|
| 1002 |
+
<button class="btn btn-ghost btn-sm" onclick="closeAuthModal()">Cancel</button>
|
| 1003 |
+
</div>
|
| 1004 |
+
</div>
|
| 1005 |
+
</div>
|
| 1006 |
+
|
| 1007 |
+
<!-- ============ LANDING PAGE ============ -->
|
| 1008 |
+
<div class="app-view active" id="viewLanding">
|
| 1009 |
+
<!-- HERO -->
|
| 1010 |
+
<section class="hero">
|
| 1011 |
+
<div class="glow-orb glow-orb-1"></div>
|
| 1012 |
+
<div class="glow-orb glow-orb-2"></div>
|
| 1013 |
+
<div class="container">
|
| 1014 |
+
<div class="hero-badge">
|
| 1015 |
+
<span class="dot"></span>
|
| 1016 |
+
RL B2B Rewards-as-a-Service
|
| 1017 |
+
</div>
|
| 1018 |
+
<h1>
|
| 1019 |
+
The Operating System<br>
|
| 1020 |
+
for <span class="gradient-text">Agentic RAG</span>
|
| 1021 |
+
</h1>
|
| 1022 |
+
<p class="hero-subtitle">
|
| 1023 |
+
Transform your data into intelligent retrieval pipelines and generate production-ready reward signals for LLM fine-tuning — all from one platform.
|
| 1024 |
+
</p>
|
| 1025 |
+
<div class="hero-actions">
|
| 1026 |
+
<button class="btn btn-primary btn-lg" onclick="showAuthModal('register')">Start Building Free →</button>
|
| 1027 |
+
<a href="/api/docs" target="_blank" class="btn btn-secondary btn-lg">View API Docs</a>
|
| 1028 |
+
</div>
|
| 1029 |
+
|
| 1030 |
+
<div class="hero-visual">
|
| 1031 |
+
<div class="hero-terminal">
|
| 1032 |
+
<div class="terminal-header">
|
| 1033 |
+
<span class="terminal-dot red"></span>
|
| 1034 |
+
<span class="terminal-dot yellow"></span>
|
| 1035 |
+
<span class="terminal-dot green"></span>
|
| 1036 |
+
<span class="terminal-title">agentic-rag-os — rewards-as-a-service</span>
|
| 1037 |
+
</div>
|
| 1038 |
+
<div class="terminal-body">
|
| 1039 |
+
<div><span class="comment"># Upload your data, generate rewards in seconds</span></div>
|
| 1040 |
+
<div><span class="cmd">POST</span> /api/v1/rag/domains/my-app/documents <span class="str">← upload</span></div>
|
| 1041 |
+
<div><span class="cmd">POST</span> /api/v1/rag/domains/my-app/query <span class="str">← retrieve</span></div>
|
| 1042 |
+
<div><span class="cmd">POST</span> /api/v1/rewards/compute <span class="str">← generate reward</span></div>
|
| 1043 |
+
<div style="margin-top:12px"><span class="comment"># Response</span></div>
|
| 1044 |
+
<div>{ <span class="str">"total_reward"</span>: <span class="num">0.847</span>, <span class="str">"algorithm"</span>: <span class="str">"grpo"</span>,</div>
|
| 1045 |
+
<div> <span class="str">"breakdown"</span>: { <span class="str">"retrieval"</span>: <span class="num">0.92</span>, <span class="str">"reasoning"</span>: <span class="num">0.78</span>, <span class="str">"completeness"</span>: <span class="num">0.85</span> }}</div>
|
| 1046 |
+
</div>
|
| 1047 |
+
</div>
|
| 1048 |
+
</div>
|
| 1049 |
+
</div>
|
| 1050 |
+
</section>
|
| 1051 |
+
|
| 1052 |
+
<!-- LOGOS BAR -->
|
| 1053 |
+
<section class="logos-bar">
|
| 1054 |
+
<div class="container">
|
| 1055 |
+
<p>Built with industry-leading technologies</p>
|
| 1056 |
+
<div class="logos-grid">
|
| 1057 |
+
<div class="logo-item"><span>🤗</span> HuggingFace</div>
|
| 1058 |
+
<div class="logo-item"><span>🔥</span> PyTorch</div>
|
| 1059 |
+
<div class="logo-item"><span>⚡</span> FastAPI</div>
|
| 1060 |
+
<div class="logo-item"><span>🔍</span> FAISS</div>
|
| 1061 |
+
<div class="logo-item"><span>🧠</span> Sentence Transformers</div>
|
| 1062 |
+
<div class="logo-item"><span>☁️</span> Google Cloud</div>
|
| 1063 |
+
</div>
|
| 1064 |
+
</div>
|
| 1065 |
+
</section>
|
| 1066 |
+
|
| 1067 |
+
<!-- FEATURES -->
|
| 1068 |
+
<section id="features">
|
| 1069 |
+
<div class="container">
|
| 1070 |
+
<div class="section-label">Platform</div>
|
| 1071 |
+
<h2 class="section-title">Everything you need to build<br>intelligent RAG systems</h2>
|
| 1072 |
+
<p class="section-desc">From data ingestion to reward generation — a complete operating system for agentic retrieval-augmented generation.</p>
|
| 1073 |
+
|
| 1074 |
+
<div class="features-grid">
|
| 1075 |
+
<div class="feature-card animate-in">
|
| 1076 |
+
<div class="feature-icon">📄</div>
|
| 1077 |
+
<h3>Data → Embeddings Pipeline</h3>
|
| 1078 |
+
<p>Upload text documents, automatically generate embeddings with sentence-transformers, and store them in high-performance FAISS indices.</p>
|
| 1079 |
+
</div>
|
| 1080 |
+
<div class="feature-card animate-in">
|
| 1081 |
+
<div class="feature-icon">🎯</div>
|
| 1082 |
+
<h3>Rewards-as-a-Service</h3>
|
| 1083 |
+
<p>Generate production-ready reward signals for GRPO, PPO, DPO, REINFORCE — choose your algorithm, define your rules, get rewards via API.</p>
|
| 1084 |
+
</div>
|
| 1085 |
+
<div class="feature-card animate-in">
|
| 1086 |
+
<div class="feature-icon">🤖</div>
|
| 1087 |
+
<h3>Multi-Agent Orchestrator</h3>
|
| 1088 |
+
<p>Five specialized agents — Retriever, Reasoner, Critic, Planner, Verifier — collaborate through structured message-passing.</p>
|
| 1089 |
+
</div>
|
| 1090 |
+
<div class="feature-card animate-in">
|
| 1091 |
+
<div class="feature-icon">🛡️</div>
|
| 1092 |
+
<h3>Anti-Reward Hacking</h3>
|
| 1093 |
+
<p>Built-in adversarial guards detect and penalize repetition, keyword stuffing, degenerate content, and copy-paste exploitation.</p>
|
| 1094 |
+
</div>
|
| 1095 |
+
<div class="feature-card animate-in">
|
| 1096 |
+
<div class="feature-icon">🔌</div>
|
| 1097 |
+
<h3>API-First Design</h3>
|
| 1098 |
+
<p>Every capability is an API endpoint. Integrate with any LLM training pipeline — compatible with TRL, OpenRLHF, and custom stacks.</p>
|
| 1099 |
+
</div>
|
| 1100 |
+
<div class="feature-card animate-in">
|
| 1101 |
+
<div class="feature-icon">🌐</div>
|
| 1102 |
+
<h3>Domain Agnostic</h3>
|
| 1103 |
+
<p>Works with any knowledge domain. Built-in support for Aerospace Research and Legal Research with extensible adapter pattern.</p>
|
| 1104 |
+
</div>
|
| 1105 |
+
</div>
|
| 1106 |
+
</div>
|
| 1107 |
+
</section>
|
| 1108 |
+
|
| 1109 |
+
<!-- HOW IT WORKS -->
|
| 1110 |
+
<section id="how-it-works">
|
| 1111 |
+
<div class="container">
|
| 1112 |
+
<div class="section-label">Workflow</div>
|
| 1113 |
+
<h2 class="section-title">From raw data to reward signals<br>in four steps</h2>
|
| 1114 |
+
<p class="section-desc">The simplest path from your proprietary data to RL-ready reward functions for LLM fine-tuning.</p>
|
| 1115 |
+
|
| 1116 |
+
<div class="steps-container">
|
| 1117 |
+
<div class="step-item animate-in">
|
| 1118 |
+
<div class="step-number">1</div>
|
| 1119 |
+
<h3>Create Domain</h3>
|
| 1120 |
+
<p>Define your knowledge domain — aerospace, legal, medical, finance, or anything custom.</p>
|
| 1121 |
+
</div>
|
| 1122 |
+
<div class="step-item animate-in">
|
| 1123 |
+
<div class="step-number">2</div>
|
| 1124 |
+
<h3>Upload Documents</h3>
|
| 1125 |
+
<p>Feed your text data. We embed, index, and store it in high-performance FAISS vector stores.</p>
|
| 1126 |
+
</div>
|
| 1127 |
+
<div class="step-item animate-in">
|
| 1128 |
+
<div class="step-number">3</div>
|
| 1129 |
+
<h3>Configure Rewards</h3>
|
| 1130 |
+
<p>Choose algorithm (GRPO/PPO/DPO), set weights, define rules. Your reward function, your way.</p>
|
| 1131 |
+
</div>
|
| 1132 |
+
<div class="step-item animate-in">
|
| 1133 |
+
<div class="step-number">4</div>
|
| 1134 |
+
<h3>Generate & Train</h3>
|
| 1135 |
+
<p>Call our API to compute rewards at scale. Use them in your RL training loop to fine-tune any LLM.</p>
|
| 1136 |
+
</div>
|
| 1137 |
+
</div>
|
| 1138 |
+
</div>
|
| 1139 |
+
</section>
|
| 1140 |
+
|
| 1141 |
+
<!-- PRICING -->
|
| 1142 |
+
<section id="pricing">
|
| 1143 |
+
<div class="container">
|
| 1144 |
+
<div style="text-align:center">
|
| 1145 |
+
<div class="section-label">Pricing</div>
|
| 1146 |
+
<h2 class="section-title">Start free. Scale when ready.</h2>
|
| 1147 |
+
<p class="section-desc" style="margin:0 auto 60px">Simple, transparent pricing that grows with your team.</p>
|
| 1148 |
+
</div>
|
| 1149 |
+
|
| 1150 |
+
<div class="pricing-grid">
|
| 1151 |
+
<div class="price-card featured">
|
| 1152 |
+
<h3>Free</h3>
|
| 1153 |
+
<p style="color:var(--text-muted);font-size:0.9rem">Perfect for experiments & prototypes</p>
|
| 1154 |
+
<div class="price">$0<span>/month</span></div>
|
| 1155 |
+
<ul>
|
| 1156 |
+
<li>2 MB document storage per domain</li>
|
| 1157 |
+
<li>Unlimited reward computations</li>
|
| 1158 |
+
<li>All 5 RL algorithms (GRPO, PPO, DPO, ...)</li>
|
| 1159 |
+
<li>Full API access</li>
|
| 1160 |
+
<li>Community support</li>
|
| 1161 |
+
</ul>
|
| 1162 |
+
<button class="btn btn-primary" style="width:100%;justify-content:center" onclick="showAuthModal('register')">Get Started Free</button>
|
| 1163 |
+
</div>
|
| 1164 |
+
<div class="price-card">
|
| 1165 |
+
<h3>Premium</h3>
|
| 1166 |
+
<p style="color:var(--text-muted);font-size:0.9rem">For teams building production RAG systems</p>
|
| 1167 |
+
<div class="price" style="color:var(--text-muted)">Coming Soon</div>
|
| 1168 |
+
<ul>
|
| 1169 |
+
<li>100 MB+ document storage</li>
|
| 1170 |
+
<li>Custom embedding models</li>
|
| 1171 |
+
<li>Priority reward compute</li>
|
| 1172 |
+
<li>Team collaboration</li>
|
| 1173 |
+
<li>Dedicated support</li>
|
| 1174 |
+
<li>Custom domain graders</li>
|
| 1175 |
+
<li>SSO & advanced auth</li>
|
| 1176 |
+
</ul>
|
| 1177 |
+
<button class="btn btn-secondary" style="width:100%;justify-content:center; opacity:0.6" disabled>Coming Soon</button>
|
| 1178 |
+
</div>
|
| 1179 |
+
</div>
|
| 1180 |
+
</div>
|
| 1181 |
+
</section>
|
| 1182 |
+
|
| 1183 |
+
<!-- API REFERENCE -->
|
| 1184 |
+
<section id="api-reference">
|
| 1185 |
+
<div class="container">
|
| 1186 |
+
<div class="section-label">Developer</div>
|
| 1187 |
+
<h2 class="section-title">API Reference</h2>
|
| 1188 |
+
<p class="section-desc">RESTful API with full OpenAPI documentation. Integrate in minutes.</p>
|
| 1189 |
+
|
| 1190 |
+
<div class="api-section">
|
| 1191 |
+
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:24px">
|
| 1192 |
+
<h3 style="font-size:1.1rem">Core Endpoints</h3>
|
| 1193 |
+
<a href="/api/docs" target="_blank" class="btn btn-secondary btn-sm">Full Docs →</a>
|
| 1194 |
+
</div>
|
| 1195 |
+
<div class="api-grid">
|
| 1196 |
+
<div class="api-card">
|
| 1197 |
+
<span class="api-method post">POST</span>
|
| 1198 |
+
<span class="api-path">/api/v1/auth/register</span>
|
| 1199 |
+
<p class="api-desc">Create account with username/password</p>
|
| 1200 |
+
</div>
|
| 1201 |
+
<div class="api-card">
|
| 1202 |
+
<span class="api-method post">POST</span>
|
| 1203 |
+
<span class="api-path">/api/v1/auth/login</span>
|
| 1204 |
+
<p class="api-desc">Authenticate and receive JWT token</p>
|
| 1205 |
+
</div>
|
| 1206 |
+
<div class="api-card">
|
| 1207 |
+
<span class="api-method post">POST</span>
|
| 1208 |
+
<span class="api-path">/api/v1/rag/domains</span>
|
| 1209 |
+
<p class="api-desc">Create a new knowledge domain</p>
|
| 1210 |
+
</div>
|
| 1211 |
+
<div class="api-card">
|
| 1212 |
+
<span class="api-method post">POST</span>
|
| 1213 |
+
<span class="api-path">/api/v1/rag/domains/:id/documents</span>
|
| 1214 |
+
<p class="api-desc">Upload document to domain (2MB free limit)</p>
|
| 1215 |
+
</div>
|
| 1216 |
+
<div class="api-card">
|
| 1217 |
+
<span class="api-method post">POST</span>
|
| 1218 |
+
<span class="api-path">/api/v1/rag/domains/:id/query</span>
|
| 1219 |
+
<p class="api-desc">Run RAG query against domain docs</p>
|
| 1220 |
+
</div>
|
| 1221 |
+
<div class="api-card">
|
| 1222 |
+
<span class="api-method post">POST</span>
|
| 1223 |
+
<span class="api-path">/api/v1/rewards/compute</span>
|
| 1224 |
+
<p class="api-desc">Compute reward on-the-fly</p>
|
| 1225 |
+
</div>
|
| 1226 |
+
<div class="api-card">
|
| 1227 |
+
<span class="api-method post">POST</span>
|
| 1228 |
+
<span class="api-path">/api/v1/rewards/configs</span>
|
| 1229 |
+
<p class="api-desc">Create reusable reward configuration</p>
|
| 1230 |
+
</div>
|
| 1231 |
+
<div class="api-card">
|
| 1232 |
+
<span class="api-method get">GET</span>
|
| 1233 |
+
<span class="api-path">/api/v1/rewards/algorithms</span>
|
| 1234 |
+
<p class="api-desc">List supported RL algorithms</p>
|
| 1235 |
+
</div>
|
| 1236 |
+
<div class="api-card">
|
| 1237 |
+
<span class="api-method get">GET</span>
|
| 1238 |
+
<span class="api-path">/api/v1/user/dashboard</span>
|
| 1239 |
+
<p class="api-desc">Get dashboard statistics</p>
|
| 1240 |
+
</div>
|
| 1241 |
+
<div class="api-card">
|
| 1242 |
+
<span class="api-method post">POST</span>
|
| 1243 |
+
<span class="api-path">/api/v1/user/api-keys</span>
|
| 1244 |
+
<p class="api-desc">Generate API key for programmatic access</p>
|
| 1245 |
+
</div>
|
| 1246 |
+
<div class="api-card">
|
| 1247 |
+
<span class="api-method get">GET</span>
|
| 1248 |
+
<span class="api-path">/api/v1/health</span>
|
| 1249 |
+
<p class="api-desc">Service health check</p>
|
| 1250 |
+
</div>
|
| 1251 |
+
<div class="api-card">
|
| 1252 |
+
<span class="api-method delete">DELETE</span>
|
| 1253 |
+
<span class="api-path">/api/v1/rag/domains/:id</span>
|
| 1254 |
+
<p class="api-desc">Delete domain and all associated data</p>
|
| 1255 |
+
</div>
|
| 1256 |
+
</div>
|
| 1257 |
+
</div>
|
| 1258 |
+
</div>
|
| 1259 |
+
</section>
|
| 1260 |
+
|
| 1261 |
+
<!-- CTA -->
|
| 1262 |
+
<section class="cta-section">
|
| 1263 |
+
<div class="container">
|
| 1264 |
+
<h2>Ready to build the future of RAG?</h2>
|
| 1265 |
+
<p>Join the next generation of AI companies using Rewards-as-a-Service.</p>
|
| 1266 |
+
<div style="display:flex; gap:16px; justify-content:center">
|
| 1267 |
+
<button class="btn btn-primary btn-lg" onclick="showAuthModal('register')">Create Free Account</button>
|
| 1268 |
+
<a href="/api/docs" target="_blank" class="btn btn-secondary btn-lg">Explore API →</a>
|
| 1269 |
+
</div>
|
| 1270 |
+
</div>
|
| 1271 |
+
</section>
|
| 1272 |
+
|
| 1273 |
+
<!-- FOOTER -->
|
| 1274 |
+
<footer class="footer">
|
| 1275 |
+
<div class="container">
|
| 1276 |
+
<div class="footer-grid">
|
| 1277 |
+
<div class="footer-brand">
|
| 1278 |
+
<h3>⚡ Agentic RAG OS</h3>
|
| 1279 |
+
<p>The Operating System for Agentic RAG. RL B2B Rewards-as-a-Service platform for building intelligent retrieval systems.</p>
|
| 1280 |
+
</div>
|
| 1281 |
+
<div>
|
| 1282 |
+
<h4>Product</h4>
|
| 1283 |
+
<ul>
|
| 1284 |
+
<li><a href="#features">Features</a></li>
|
| 1285 |
+
<li><a href="#pricing">Pricing</a></li>
|
| 1286 |
+
<li><a href="/api/docs" target="_blank">API Docs</a></li>
|
| 1287 |
+
<li><a href="#api-reference">Endpoints</a></li>
|
| 1288 |
+
</ul>
|
| 1289 |
+
</div>
|
| 1290 |
+
<div>
|
| 1291 |
+
<h4>Resources</h4>
|
| 1292 |
+
<ul>
|
| 1293 |
+
<li><a href="https://github.com/williyam-m/agentic-rag-gym" target="_blank">GitHub</a></li>
|
| 1294 |
+
<li><a href="https://huggingface.co/spaces/williyam/agentic-rag-gym" target="_blank">HF Space</a></li>
|
| 1295 |
+
<li><a href="https://huggingface.co/williyam/agentic-rag-aerospace-grpo" target="_blank">Models</a></li>
|
| 1296 |
+
</ul>
|
| 1297 |
+
</div>
|
| 1298 |
+
<div>
|
| 1299 |
+
<h4>Company</h4>
|
| 1300 |
+
<ul>
|
| 1301 |
+
<li><a href="#">About</a></li>
|
| 1302 |
+
<li><a href="#">Blog</a></li>
|
| 1303 |
+
<li><a href="#">Careers</a></li>
|
| 1304 |
+
<li><a href="#">Contact</a></li>
|
| 1305 |
+
</ul>
|
| 1306 |
+
</div>
|
| 1307 |
+
</div>
|
| 1308 |
+
<div class="footer-bottom">
|
| 1309 |
+
<span>© 2025 Agentic RAG OS. All rights reserved.</span>
|
| 1310 |
+
<span>Built with ❤️ for the AI community</span>
|
| 1311 |
+
</div>
|
| 1312 |
+
</div>
|
| 1313 |
+
</footer>
|
| 1314 |
+
</div>
|
| 1315 |
+
|
| 1316 |
+
<!-- ============ DASHBOARD VIEW ============ -->
|
| 1317 |
+
<div class="app-view" id="viewDashboard">
|
| 1318 |
+
<div class="container" style="padding-top:20px">
|
| 1319 |
+
<div class="dashboard-header">
|
| 1320 |
+
<h1>Dashboard</h1>
|
| 1321 |
+
<div style="display:flex;gap:12px">
|
| 1322 |
+
<button class="btn btn-secondary btn-sm" onclick="showView('domains')">Domains</button>
|
| 1323 |
+
<button class="btn btn-secondary btn-sm" onclick="showView('rewards')">Rewards</button>
|
| 1324 |
+
<button class="btn btn-secondary btn-sm" onclick="showView('apikeys')">API Keys</button>
|
| 1325 |
+
</div>
|
| 1326 |
+
</div>
|
| 1327 |
+
|
| 1328 |
+
<div class="stats-grid" id="dashStats">
|
| 1329 |
+
<div class="stat-card"><div class="stat-label">Domains</div><div class="stat-value accent" id="statDomains">0</div></div>
|
| 1330 |
+
<div class="stat-card"><div class="stat-label">Documents</div><div class="stat-value" id="statDocs">0</div></div>
|
| 1331 |
+
<div class="stat-card"><div class="stat-label">RAG Queries</div><div class="stat-value" id="statQueries">0</div></div>
|
| 1332 |
+
<div class="stat-card"><div class="stat-label">Storage Used</div><div class="stat-value" id="statStorage">0 KB</div></div>
|
| 1333 |
+
</div>
|
| 1334 |
+
|
| 1335 |
+
<div class="card">
|
| 1336 |
+
<h2>Quick Start</h2>
|
| 1337 |
+
<div class="terminal-body" style="background:var(--bg-primary);border-radius:8px;padding:20px;margin-top:12px">
|
| 1338 |
+
<div><span class="comment"># 1. Get your API token</span></div>
|
| 1339 |
+
<div><span class="cmd">curl</span> -X POST /api/v1/auth/login -d '{"username":"you","password":"pw"}'</div>
|
| 1340 |
+
<div style="margin-top:12px"><span class="comment"># 2. Create a domain</span></div>
|
| 1341 |
+
<div><span class="cmd">curl</span> -X POST /api/v1/rag/domains -H "Authorization: Bearer $TOKEN" \</div>
|
| 1342 |
+
<div> -d '{"name":"my-project","description":"My RAG domain"}'</div>
|
| 1343 |
+
<div style="margin-top:12px"><span class="comment"># 3. Upload documents</span></div>
|
| 1344 |
+
<div><span class="cmd">curl</span> -X POST /api/v1/rag/domains/$ID/documents -F "file=@data.txt"</div>
|
| 1345 |
+
<div style="margin-top:12px"><span class="comment"># 4. Generate rewards</span></div>
|
| 1346 |
+
<div><span class="cmd">curl</span> -X POST /api/v1/rewards/compute \</div>
|
| 1347 |
+
<div> -d '{"algorithm":"grpo","query":"...","answer":"...","retrieved_docs":[...]}'</div>
|
| 1348 |
+
</div>
|
| 1349 |
+
</div>
|
| 1350 |
+
</div>
|
| 1351 |
+
</div>
|
| 1352 |
+
|
| 1353 |
+
<!-- ============ DOMAINS VIEW ============ -->
|
| 1354 |
+
<div class="app-view" id="viewDomains">
|
| 1355 |
+
<div class="container" style="padding-top:20px">
|
| 1356 |
+
<div class="dashboard-header">
|
| 1357 |
+
<h1>RAG Domains</h1>
|
| 1358 |
+
<button class="btn btn-primary btn-sm" onclick="showCreateDomain()">+ New Domain</button>
|
| 1359 |
+
</div>
|
| 1360 |
+
<div class="card" id="domainsList">
|
| 1361 |
+
<p style="color:var(--text-muted)">Loading domains...</p>
|
| 1362 |
+
</div>
|
| 1363 |
+
|
| 1364 |
+
<!-- Domain Detail -->
|
| 1365 |
+
<div id="domainDetail" style="display:none">
|
| 1366 |
+
<div class="card">
|
| 1367 |
+
<div style="display:flex;justify-content:space-between;align-items:center">
|
| 1368 |
+
<h2 id="domainDetailName">Domain</h2>
|
| 1369 |
+
<button class="btn btn-ghost btn-sm" onclick="closeDomainDetail()">← Back</button>
|
| 1370 |
+
</div>
|
| 1371 |
+
|
| 1372 |
+
<!-- Upload -->
|
| 1373 |
+
<div style="margin:20px 0;padding:20px;border:2px dashed var(--border);border-radius:12px;text-align:center">
|
| 1374 |
+
<p style="color:var(--text-muted);margin-bottom:12px">Upload text files (max 2MB total · Premium for more storage)</p>
|
| 1375 |
+
<input type="file" id="fileInput" accept=".txt,.md,.csv,.json" style="display:none" onchange="handleFileUpload(event)">
|
| 1376 |
+
<button class="btn btn-secondary btn-sm" onclick="document.getElementById('fileInput').click()">Choose File</button>
|
| 1377 |
+
</div>
|
| 1378 |
+
|
| 1379 |
+
<!-- Documents table -->
|
| 1380 |
+
<div id="documentsList"></div>
|
| 1381 |
+
|
| 1382 |
+
<!-- RAG Query -->
|
| 1383 |
+
<div style="margin-top:24px">
|
| 1384 |
+
<h3 style="font-size:1rem;margin-bottom:12px">Query Documents</h3>
|
| 1385 |
+
<div style="display:flex;gap:12px">
|
| 1386 |
+
<input type="text" class="form-input" id="ragQueryInput" placeholder="Enter your search query..." style="flex:1">
|
| 1387 |
+
<button class="btn btn-primary btn-sm" onclick="runRAGQuery()">Search</button>
|
| 1388 |
+
</div>
|
| 1389 |
+
<div id="ragResults" style="margin-top:16px"></div>
|
| 1390 |
+
</div>
|
| 1391 |
+
</div>
|
| 1392 |
+
</div>
|
| 1393 |
+
</div>
|
| 1394 |
+
</div>
|
| 1395 |
+
|
| 1396 |
+
<!-- ============ REWARDS VIEW ============ -->
|
| 1397 |
+
<div class="app-view" id="viewRewards">
|
| 1398 |
+
<div class="container" style="padding-top:20px">
|
| 1399 |
+
<div class="dashboard-header">
|
| 1400 |
+
<h1>Rewards-as-a-Service</h1>
|
| 1401 |
+
<button class="btn btn-primary btn-sm" onclick="showCreateRewardConfig()">+ New Config</button>
|
| 1402 |
+
</div>
|
| 1403 |
+
|
| 1404 |
+
<!-- Reward Playground -->
|
| 1405 |
+
<div class="reward-playground" style="margin-bottom:24px">
|
| 1406 |
+
<h3 style="margin-bottom:16px">⚡ Reward Playground</h3>
|
| 1407 |
+
<p style="color:var(--text-secondary);margin-bottom:20px;font-size:0.9rem">Compute rewards on-the-fly. Choose your algorithm, provide input, and see the reward breakdown instantly.</p>
|
| 1408 |
+
|
| 1409 |
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
|
| 1410 |
+
<div>
|
| 1411 |
+
<div class="form-group">
|
| 1412 |
+
<label>Algorithm</label>
|
| 1413 |
+
<select class="form-select" id="playAlgorithm">
|
| 1414 |
+
<option value="grpo">GRPO — Group Relative Policy Optimization</option>
|
| 1415 |
+
<option value="ppo">PPO — Proximal Policy Optimization</option>
|
| 1416 |
+
<option value="dpo">DPO — Direct Preference Optimization</option>
|
| 1417 |
+
<option value="reinforce">REINFORCE — Policy Gradient</option>
|
| 1418 |
+
<option value="custom">Custom</option>
|
| 1419 |
+
</select>
|
| 1420 |
+
</div>
|
| 1421 |
+
<div class="form-group">
|
| 1422 |
+
<label>Query</label>
|
| 1423 |
+
<input type="text" class="form-input" id="playQuery" placeholder="What is the specific impulse of ion propulsion?">
|
| 1424 |
+
</div>
|
| 1425 |
+
<div class="form-group">
|
| 1426 |
+
<label>Answer</label>
|
| 1427 |
+
<textarea class="form-input" id="playAnswer" placeholder="Ion propulsion systems achieve specific impulse values of 3000-5000 seconds, significantly higher than chemical propulsion..."></textarea>
|
| 1428 |
+
</div>
|
| 1429 |
+
<div class="form-group">
|
| 1430 |
+
<label>Retrieved Documents (one per line)</label>
|
| 1431 |
+
<textarea class="form-input" id="playDocs" placeholder="Ion engines produce low thrust but extremely high specific impulse... Nuclear thermal propulsion offers thrust-to-weight ratios..."></textarea>
|
| 1432 |
+
</div>
|
| 1433 |
+
<button class="btn btn-primary" style="width:100%;justify-content:center" onclick="computePlaygroundReward()">Compute Reward →</button>
|
| 1434 |
+
</div>
|
| 1435 |
+
<div id="playgroundResult" style="display:flex;align-items:center;justify-content:center">
|
| 1436 |
+
<p style="color:var(--text-muted)">Configure inputs and click compute to see reward breakdown</p>
|
| 1437 |
+
</div>
|
| 1438 |
+
</div>
|
| 1439 |
+
</div>
|
| 1440 |
+
|
| 1441 |
+
<!-- Reward Configs List -->
|
| 1442 |
+
<div class="card" id="rewardConfigsList">
|
| 1443 |
+
<h2>Saved Configurations</h2>
|
| 1444 |
+
<p style="color:var(--text-muted)">Loading...</p>
|
| 1445 |
+
</div>
|
| 1446 |
+
</div>
|
| 1447 |
+
</div>
|
| 1448 |
+
|
| 1449 |
+
<!-- ============ API KEYS VIEW ============ -->
|
| 1450 |
+
<div class="app-view" id="viewApikeys">
|
| 1451 |
+
<div class="container" style="padding-top:20px">
|
| 1452 |
+
<div class="dashboard-header">
|
| 1453 |
+
<h1>API Keys</h1>
|
| 1454 |
+
<button class="btn btn-primary btn-sm" onclick="createNewAPIKey()">+ Generate Key</button>
|
| 1455 |
+
</div>
|
| 1456 |
+
<div class="card" id="apiKeysList">
|
| 1457 |
+
<p style="color:var(--text-muted)">Loading...</p>
|
| 1458 |
+
</div>
|
| 1459 |
+
</div>
|
| 1460 |
+
</div>
|
| 1461 |
+
|
| 1462 |
+
<!-- ============ CREATE DOMAIN MODAL ============ -->
|
| 1463 |
+
<div class="modal-overlay" id="createDomainModal">
|
| 1464 |
+
<div class="modal">
|
| 1465 |
+
<h2>Create Domain</h2>
|
| 1466 |
+
<form onsubmit="createDomain(event)">
|
| 1467 |
+
<div class="form-group">
|
| 1468 |
+
<label>Domain Name</label>
|
| 1469 |
+
<input type="text" class="form-input" id="newDomainName" placeholder="e.g., aerospace-research" required>
|
| 1470 |
+
</div>
|
| 1471 |
+
<div class="form-group">
|
| 1472 |
+
<label>Description</label>
|
| 1473 |
+
<input type="text" class="form-input" id="newDomainDesc" placeholder="Knowledge base for aerospace research">
|
| 1474 |
+
</div>
|
| 1475 |
+
<div class="modal-actions">
|
| 1476 |
+
<button type="button" class="btn btn-ghost" onclick="closeModal('createDomainModal')">Cancel</button>
|
| 1477 |
+
<button type="submit" class="btn btn-primary">Create</button>
|
| 1478 |
+
</div>
|
| 1479 |
+
</form>
|
| 1480 |
+
</div>
|
| 1481 |
+
</div>
|
| 1482 |
+
|
| 1483 |
+
<!-- ============ CREATE REWARD CONFIG MODAL ============ -->
|
| 1484 |
+
<div class="modal-overlay" id="createRewardModal">
|
| 1485 |
+
<div class="modal">
|
| 1486 |
+
<h2>Create Reward Config</h2>
|
| 1487 |
+
<form onsubmit="createRewardConfig(event)">
|
| 1488 |
+
<div class="form-group">
|
| 1489 |
+
<label>Config Name</label>
|
| 1490 |
+
<input type="text" class="form-input" id="newRewardName" placeholder="e.g., my-grpo-config" required>
|
| 1491 |
+
</div>
|
| 1492 |
+
<div class="form-group">
|
| 1493 |
+
<label>Algorithm</label>
|
| 1494 |
+
<select class="form-select" id="newRewardAlgo">
|
| 1495 |
+
<option value="grpo">GRPO</option>
|
| 1496 |
+
<option value="ppo">PPO</option>
|
| 1497 |
+
<option value="dpo">DPO</option>
|
| 1498 |
+
<option value="reinforce">REINFORCE</option>
|
| 1499 |
+
<option value="custom">Custom</option>
|
| 1500 |
+
</select>
|
| 1501 |
+
</div>
|
| 1502 |
+
<div class="form-group">
|
| 1503 |
+
<label>Retrieval Weight</label>
|
| 1504 |
+
<input type="number" class="form-input" id="rwRetrieval" value="0.25" step="0.05" min="0" max="1">
|
| 1505 |
+
</div>
|
| 1506 |
+
<div class="form-group">
|
| 1507 |
+
<label>Reasoning Weight</label>
|
| 1508 |
+
<input type="number" class="form-input" id="rwReasoning" value="0.20" step="0.05" min="0" max="1">
|
| 1509 |
+
</div>
|
| 1510 |
+
<div class="form-group">
|
| 1511 |
+
<label>Completeness Weight</label>
|
| 1512 |
+
<input type="number" class="form-input" id="rwCompleteness" value="0.30" step="0.05" min="0" max="1">
|
| 1513 |
+
</div>
|
| 1514 |
+
<div class="modal-actions">
|
| 1515 |
+
<button type="button" class="btn btn-ghost" onclick="closeModal('createRewardModal')">Cancel</button>
|
| 1516 |
+
<button type="submit" class="btn btn-primary">Create</button>
|
| 1517 |
+
</div>
|
| 1518 |
+
</form>
|
| 1519 |
+
</div>
|
| 1520 |
+
</div>
|
| 1521 |
+
|
| 1522 |
+
<!-- ============ JAVASCRIPT ============ -->
|
| 1523 |
+
<script>
|
| 1524 |
+
// ==== State ====
|
| 1525 |
+
const API = '/api/v1';
|
| 1526 |
+
let token = localStorage.getItem('ragos_token') || '';
|
| 1527 |
+
let currentUser = null;
|
| 1528 |
+
let currentDomainId = null;
|
| 1529 |
+
let authMode = 'login';
|
| 1530 |
+
|
| 1531 |
+
// ==== Init ====
|
| 1532 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 1533 |
+
if (token) {
|
| 1534 |
+
loadUser();
|
| 1535 |
+
}
|
| 1536 |
+
initScrollAnimations();
|
| 1537 |
+
});
|
| 1538 |
+
|
| 1539 |
+
// ==== Scroll animations ====
|
| 1540 |
+
function initScrollAnimations() {
|
| 1541 |
+
const obs = new IntersectionObserver((entries) => {
|
| 1542 |
+
entries.forEach(e => {
|
| 1543 |
+
if (e.isIntersecting) {
|
| 1544 |
+
e.target.classList.add('animate-in');
|
| 1545 |
+
obs.unobserve(e.target);
|
| 1546 |
+
}
|
| 1547 |
+
});
|
| 1548 |
+
}, { threshold: 0.1 });
|
| 1549 |
+
document.querySelectorAll('.feature-card, .step-item').forEach(el => obs.observe(el));
|
| 1550 |
+
}
|
| 1551 |
+
|
| 1552 |
+
// ==== Auth ====
|
| 1553 |
+
function showAuthModal(mode = 'login') {
|
| 1554 |
+
switchAuthTab(mode);
|
| 1555 |
+
document.getElementById('authModal').classList.add('active');
|
| 1556 |
+
}
|
| 1557 |
+
function closeAuthModal() {
|
| 1558 |
+
document.getElementById('authModal').classList.remove('active');
|
| 1559 |
+
}
|
| 1560 |
+
function switchAuthTab(mode) {
|
| 1561 |
+
authMode = mode;
|
| 1562 |
+
document.getElementById('tabLogin').classList.toggle('active', mode === 'login');
|
| 1563 |
+
document.getElementById('tabRegister').classList.toggle('active', mode === 'register');
|
| 1564 |
+
document.getElementById('emailGroup').style.display = mode === 'register' ? 'block' : 'none';
|
| 1565 |
+
document.getElementById('authSubmitBtn').textContent = mode === 'login' ? 'Sign In' : 'Create Account';
|
| 1566 |
+
}
|
| 1567 |
+
async function handleAuth(e) {
|
| 1568 |
+
e.preventDefault();
|
| 1569 |
+
const username = document.getElementById('authUsername').value;
|
| 1570 |
+
const password = document.getElementById('authPassword').value;
|
| 1571 |
+
const email = document.getElementById('authEmail').value;
|
| 1572 |
+
|
| 1573 |
+
try {
|
| 1574 |
+
const endpoint = authMode === 'login' ? '/auth/login' : '/auth/register';
|
| 1575 |
+
const body = authMode === 'login'
|
| 1576 |
+
? { username, password }
|
| 1577 |
+
: { username, email, password };
|
| 1578 |
+
const resp = await apiFetch(endpoint, 'POST', body, false);
|
| 1579 |
+
token = resp.access_token;
|
| 1580 |
+
localStorage.setItem('ragos_token', token);
|
| 1581 |
+
currentUser = resp.user;
|
| 1582 |
+
closeAuthModal();
|
| 1583 |
+
updateNavForUser();
|
| 1584 |
+
showView('dashboard');
|
| 1585 |
+
loadDashboard();
|
| 1586 |
+
toast('Welcome, ' + currentUser.username + '!', 'success');
|
| 1587 |
+
} catch (err) {
|
| 1588 |
+
toast(err.message || 'Authentication failed', 'error');
|
| 1589 |
+
}
|
| 1590 |
+
}
|
| 1591 |
+
function githubLogin() {
|
| 1592 |
+
const clientId = ''; // Will be populated from env
|
| 1593 |
+
if (!clientId) {
|
| 1594 |
+
toast('GitHub OAuth is configured on the deployed version', 'info');
|
| 1595 |
+
return;
|
| 1596 |
+
}
|
| 1597 |
+
window.location.href = `https://github.com/login/oauth/authorize?client_id=${clientId}&scope=user:email`;
|
| 1598 |
+
}
|
| 1599 |
+
function logout() {
|
| 1600 |
+
token = '';
|
| 1601 |
+
currentUser = null;
|
| 1602 |
+
localStorage.removeItem('ragos_token');
|
| 1603 |
+
updateNavForUser();
|
| 1604 |
+
showView('landing');
|
| 1605 |
+
toast('Logged out', 'info');
|
| 1606 |
+
}
|
| 1607 |
+
async function loadUser() {
|
| 1608 |
+
try {
|
| 1609 |
+
currentUser = await apiFetch('/user/me');
|
| 1610 |
+
updateNavForUser();
|
| 1611 |
+
} catch {
|
| 1612 |
+
token = '';
|
| 1613 |
+
localStorage.removeItem('ragos_token');
|
| 1614 |
+
}
|
| 1615 |
+
}
|
| 1616 |
+
function updateNavForUser() {
|
| 1617 |
+
const loggedIn = !!currentUser;
|
| 1618 |
+
document.getElementById('navLoginBtn').style.display = loggedIn ? 'none' : '';
|
| 1619 |
+
document.getElementById('navSignupBtn').style.display = loggedIn ? 'none' : '';
|
| 1620 |
+
document.getElementById('navUserMenu').style.display = loggedIn ? 'flex' : 'none';
|
| 1621 |
+
}
|
| 1622 |
+
|
| 1623 |
+
// ==== View Switching ====
|
| 1624 |
+
function showView(view) {
|
| 1625 |
+
document.querySelectorAll('.app-view').forEach(v => v.classList.remove('active'));
|
| 1626 |
+
const el = document.getElementById('view' + view.charAt(0).toUpperCase() + view.slice(1));
|
| 1627 |
+
if (el) el.classList.add('active');
|
| 1628 |
+
|
| 1629 |
+
if (view === 'dashboard') loadDashboard();
|
| 1630 |
+
if (view === 'domains') loadDomains();
|
| 1631 |
+
if (view === 'rewards') loadRewards();
|
| 1632 |
+
if (view === 'apikeys') loadAPIKeys();
|
| 1633 |
+
}
|
| 1634 |
+
|
| 1635 |
+
// ==== Dashboard ====
|
| 1636 |
+
async function loadDashboard() {
|
| 1637 |
+
try {
|
| 1638 |
+
const stats = await apiFetch('/user/dashboard');
|
| 1639 |
+
document.getElementById('statDomains').textContent = stats.total_domains;
|
| 1640 |
+
document.getElementById('statDocs').textContent = stats.total_documents;
|
| 1641 |
+
document.getElementById('statQueries').textContent = stats.total_queries;
|
| 1642 |
+
const kb = Math.round(stats.storage_used_bytes / 1024);
|
| 1643 |
+
document.getElementById('statStorage').textContent = kb > 1024 ? (kb / 1024).toFixed(1) + ' MB' : kb + ' KB';
|
| 1644 |
+
} catch {}
|
| 1645 |
+
}
|
| 1646 |
+
|
| 1647 |
+
// ==== Domains ====
|
| 1648 |
+
async function loadDomains() {
|
| 1649 |
+
try {
|
| 1650 |
+
const domains = await apiFetch('/rag/domains');
|
| 1651 |
+
const el = document.getElementById('domainsList');
|
| 1652 |
+
if (domains.length === 0) {
|
| 1653 |
+
el.innerHTML = '<p style="color:var(--text-muted)">No domains yet. Create one to get started.</p>';
|
| 1654 |
+
return;
|
| 1655 |
+
}
|
| 1656 |
+
el.innerHTML = `<table class="data-table"><thead><tr><th>Name</th><th>Documents</th><th>Size</th><th>Created</th><th></th></tr></thead><tbody>` +
|
| 1657 |
+
domains.map(d => `<tr>
|
| 1658 |
+
<td><strong style="color:var(--text-primary)">${esc(d.name)}</strong><br><span style="font-size:0.8rem;color:var(--text-muted)">${esc(d.description)}</span></td>
|
| 1659 |
+
<td>${d.document_count}</td>
|
| 1660 |
+
<td>${formatBytes(d.total_size_bytes)}</td>
|
| 1661 |
+
<td>${new Date(d.created_at).toLocaleDateString()}</td>
|
| 1662 |
+
<td><button class="btn btn-ghost btn-sm" onclick="openDomain('${d.id}','${esc(d.name)}')">Open →</button></td>
|
| 1663 |
+
</tr>`).join('') +
|
| 1664 |
+
'</tbody></table>';
|
| 1665 |
+
} catch (err) {
|
| 1666 |
+
toast(err.message, 'error');
|
| 1667 |
+
}
|
| 1668 |
+
}
|
| 1669 |
+
function showCreateDomain() { document.getElementById('createDomainModal').classList.add('active'); }
|
| 1670 |
+
async function createDomain(e) {
|
| 1671 |
+
e.preventDefault();
|
| 1672 |
+
try {
|
| 1673 |
+
await apiFetch('/rag/domains', 'POST', {
|
| 1674 |
+
name: document.getElementById('newDomainName').value,
|
| 1675 |
+
description: document.getElementById('newDomainDesc').value
|
| 1676 |
+
});
|
| 1677 |
+
closeModal('createDomainModal');
|
| 1678 |
+
loadDomains();
|
| 1679 |
+
toast('Domain created', 'success');
|
| 1680 |
+
} catch (err) { toast(err.message, 'error'); }
|
| 1681 |
+
}
|
| 1682 |
+
async function openDomain(id, name) {
|
| 1683 |
+
currentDomainId = id;
|
| 1684 |
+
document.getElementById('domainDetailName').textContent = name;
|
| 1685 |
+
document.getElementById('domainDetail').style.display = 'block';
|
| 1686 |
+
document.getElementById('domainsList').style.display = 'none';
|
| 1687 |
+
await loadDocuments();
|
| 1688 |
+
}
|
| 1689 |
+
function closeDomainDetail() {
|
| 1690 |
+
document.getElementById('domainDetail').style.display = 'none';
|
| 1691 |
+
document.getElementById('domainsList').style.display = 'block';
|
| 1692 |
+
currentDomainId = null;
|
| 1693 |
+
}
|
| 1694 |
+
async function loadDocuments() {
|
| 1695 |
+
if (!currentDomainId) return;
|
| 1696 |
+
try {
|
| 1697 |
+
const docs = await apiFetch(`/rag/domains/${currentDomainId}/documents`);
|
| 1698 |
+
const el = document.getElementById('documentsList');
|
| 1699 |
+
if (docs.length === 0) {
|
| 1700 |
+
el.innerHTML = '<p style="color:var(--text-muted)">No documents yet. Upload a text file above.</p>';
|
| 1701 |
+
return;
|
| 1702 |
+
}
|
| 1703 |
+
el.innerHTML = `<table class="data-table"><thead><tr><th>Filename</th><th>Size</th><th>Uploaded</th><th></th></tr></thead><tbody>` +
|
| 1704 |
+
docs.map(d => `<tr>
|
| 1705 |
+
<td>${esc(d.filename)}</td>
|
| 1706 |
+
<td>${formatBytes(d.size_bytes)}</td>
|
| 1707 |
+
<td>${new Date(d.created_at).toLocaleDateString()}</td>
|
| 1708 |
+
<td><button class="btn btn-ghost btn-sm" onclick="deleteDoc('${d.id}')" style="color:#ef4444">Delete</button></td>
|
| 1709 |
+
</tr>`).join('') +
|
| 1710 |
+
'</tbody></table>';
|
| 1711 |
+
} catch (err) { toast(err.message, 'error'); }
|
| 1712 |
+
}
|
| 1713 |
+
async function handleFileUpload(e) {
|
| 1714 |
+
const file = e.target.files[0];
|
| 1715 |
+
if (!file) return;
|
| 1716 |
+
const form = new FormData();
|
| 1717 |
+
form.append('file', file);
|
| 1718 |
+
try {
|
| 1719 |
+
const resp = await fetch(`${API}/rag/domains/${currentDomainId}/documents`, {
|
| 1720 |
+
method: 'POST',
|
| 1721 |
+
headers: { 'Authorization': `Bearer ${token}` },
|
| 1722 |
+
body: form,
|
| 1723 |
+
});
|
| 1724 |
+
if (!resp.ok) {
|
| 1725 |
+
const err = await resp.json();
|
| 1726 |
+
throw new Error(err.detail || 'Upload failed');
|
| 1727 |
+
}
|
| 1728 |
+
toast('Document uploaded', 'success');
|
| 1729 |
+
await loadDocuments();
|
| 1730 |
+
} catch (err) { toast(err.message, 'error'); }
|
| 1731 |
+
e.target.value = '';
|
| 1732 |
+
}
|
| 1733 |
+
async function deleteDoc(docId) {
|
| 1734 |
+
if (!confirm('Delete this document?')) return;
|
| 1735 |
+
try {
|
| 1736 |
+
await apiFetch(`/rag/domains/${currentDomainId}/documents/${docId}`, 'DELETE');
|
| 1737 |
+
toast('Document deleted', 'success');
|
| 1738 |
+
await loadDocuments();
|
| 1739 |
+
} catch (err) { toast(err.message, 'error'); }
|
| 1740 |
+
}
|
| 1741 |
+
async function runRAGQuery() {
|
| 1742 |
+
const query = document.getElementById('ragQueryInput').value.trim();
|
| 1743 |
+
if (!query) return;
|
| 1744 |
+
try {
|
| 1745 |
+
const result = await apiFetch(`/rag/domains/${currentDomainId}/query`, 'POST', { query, top_k: 5 });
|
| 1746 |
+
const el = document.getElementById('ragResults');
|
| 1747 |
+
if (result.results.length === 0) {
|
| 1748 |
+
el.innerHTML = '<p style="color:var(--text-muted)">No results found.</p>';
|
| 1749 |
+
return;
|
| 1750 |
+
}
|
| 1751 |
+
el.innerHTML = result.results.map((r, i) => `
|
| 1752 |
+
<div style="padding:12px;background:var(--bg-primary);border:1px solid var(--border);border-radius:8px;margin-bottom:8px">
|
| 1753 |
+
<div style="display:flex;justify-content:space-between;margin-bottom:4px">
|
| 1754 |
+
<span style="font-weight:600;font-size:0.85rem">Result ${i + 1}</span>
|
| 1755 |
+
<span class="tag tag-info">Score: ${r.score.toFixed(4)}</span>
|
| 1756 |
+
</div>
|
| 1757 |
+
<p style="font-size:0.85rem;color:var(--text-secondary)">${esc(r.content.substring(0, 300))}${r.content.length > 300 ? '...' : ''}</p>
|
| 1758 |
+
</div>
|
| 1759 |
+
`).join('');
|
| 1760 |
+
} catch (err) { toast(err.message, 'error'); }
|
| 1761 |
+
}
|
| 1762 |
+
|
| 1763 |
+
// ==== Rewards ====
|
| 1764 |
+
async function loadRewards() {
|
| 1765 |
+
try {
|
| 1766 |
+
const configs = await apiFetch('/rewards/configs');
|
| 1767 |
+
const el = document.getElementById('rewardConfigsList');
|
| 1768 |
+
if (configs.length === 0) {
|
| 1769 |
+
el.innerHTML = '<h2>Saved Configurations</h2><p style="color:var(--text-muted)">No reward configs yet. Create one or use the playground above.</p>';
|
| 1770 |
+
return;
|
| 1771 |
+
}
|
| 1772 |
+
el.innerHTML = '<h2>Saved Configurations</h2>' +
|
| 1773 |
+
`<table class="data-table"><thead><tr><th>Name</th><th>Algorithm</th><th>Created</th><th></th></tr></thead><tbody>` +
|
| 1774 |
+
configs.map(c => `<tr>
|
| 1775 |
+
<td><strong style="color:var(--text-primary)">${esc(c.name)}</strong></td>
|
| 1776 |
+
<td><span class="tag tag-info">${c.algorithm.toUpperCase()}</span></td>
|
| 1777 |
+
<td>${new Date(c.created_at).toLocaleDateString()}</td>
|
| 1778 |
+
<td><button class="btn btn-ghost btn-sm" onclick="deleteRewardConfig('${c.id}')" style="color:#ef4444">Delete</button></td>
|
| 1779 |
+
</tr>`).join('') +
|
| 1780 |
+
'</tbody></table>';
|
| 1781 |
+
} catch (err) { toast(err.message, 'error'); }
|
| 1782 |
+
}
|
| 1783 |
+
function showCreateRewardConfig() { document.getElementById('createRewardModal').classList.add('active'); }
|
| 1784 |
+
async function createRewardConfig(e) {
|
| 1785 |
+
e.preventDefault();
|
| 1786 |
+
try {
|
| 1787 |
+
await apiFetch('/rewards/configs', 'POST', {
|
| 1788 |
+
name: document.getElementById('newRewardName').value,
|
| 1789 |
+
algorithm: document.getElementById('newRewardAlgo').value,
|
| 1790 |
+
config: {
|
| 1791 |
+
retrieval_relevance_weight: parseFloat(document.getElementById('rwRetrieval').value),
|
| 1792 |
+
reasoning_quality_weight: parseFloat(document.getElementById('rwReasoning').value),
|
| 1793 |
+
answer_completeness_weight: parseFloat(document.getElementById('rwCompleteness').value),
|
| 1794 |
+
efficiency_weight: 0.15,
|
| 1795 |
+
anti_hack_weight: 0.10,
|
| 1796 |
+
}
|
| 1797 |
+
});
|
| 1798 |
+
closeModal('createRewardModal');
|
| 1799 |
+
loadRewards();
|
| 1800 |
+
toast('Reward config created', 'success');
|
| 1801 |
+
} catch (err) { toast(err.message, 'error'); }
|
| 1802 |
+
}
|
| 1803 |
+
async function deleteRewardConfig(id) {
|
| 1804 |
+
if (!confirm('Delete this reward config?')) return;
|
| 1805 |
+
try {
|
| 1806 |
+
await apiFetch(`/rewards/configs/${id}`, 'DELETE');
|
| 1807 |
+
loadRewards();
|
| 1808 |
+
toast('Config deleted', 'success');
|
| 1809 |
+
} catch (err) { toast(err.message, 'error'); }
|
| 1810 |
+
}
|
| 1811 |
+
async function computePlaygroundReward() {
|
| 1812 |
+
const algo = document.getElementById('playAlgorithm').value;
|
| 1813 |
+
const query = document.getElementById('playQuery').value;
|
| 1814 |
+
const answer = document.getElementById('playAnswer').value;
|
| 1815 |
+
const docsText = document.getElementById('playDocs').value;
|
| 1816 |
+
const docs = docsText.split('\n').filter(l => l.trim());
|
| 1817 |
+
|
| 1818 |
+
try {
|
| 1819 |
+
const result = await apiFetch('/rewards/compute', 'POST', {
|
| 1820 |
+
algorithm: algo,
|
| 1821 |
+
query, answer,
|
| 1822 |
+
retrieved_docs: docs,
|
| 1823 |
+
config: {}
|
| 1824 |
+
});
|
| 1825 |
+
const el = document.getElementById('playgroundResult');
|
| 1826 |
+
const score = result.total_reward;
|
| 1827 |
+
const color = score > 0.7 ? '#10b981' : score > 0.4 ? '#f59e0b' : '#ef4444';
|
| 1828 |
+
el.innerHTML = `
|
| 1829 |
+
<div style="width:100%">
|
| 1830 |
+
<div class="reward-result">
|
| 1831 |
+
<div style="text-align:center;font-size:0.8rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.1em">Total Reward</div>
|
| 1832 |
+
<div class="reward-score" style="color:${color}">${score.toFixed(4)}</div>
|
| 1833 |
+
<div style="text-align:center;margin-bottom:20px"><span class="tag tag-info">${result.algorithm.toUpperCase()}</span></div>
|
| 1834 |
+
<div class="reward-breakdown">
|
| 1835 |
+
${Object.entries(result.breakdown).map(([k, v]) => {
|
| 1836 |
+
const barColor = k === 'anti_hack_penalty' ? '#ef4444' : 'var(--accent-light)';
|
| 1837 |
+
return `<div class="reward-bar">
|
| 1838 |
+
<div class="reward-bar-label">${k.replace(/_/g, ' ')}</div>
|
| 1839 |
+
<div style="display:flex;justify-content:space-between;font-size:0.85rem;margin-bottom:4px">
|
| 1840 |
+
<span>${v.toFixed(4)}</span>
|
| 1841 |
+
</div>
|
| 1842 |
+
<div class="reward-bar-track">
|
| 1843 |
+
<div class="reward-bar-fill" style="width:${Math.min(v * 100, 100)}%;background:${barColor}"></div>
|
| 1844 |
+
</div>
|
| 1845 |
+
</div>`;
|
| 1846 |
+
}).join('')}
|
| 1847 |
+
</div>
|
| 1848 |
+
${result.metadata.description ? `<p style="margin-top:16px;font-size:0.8rem;color:var(--text-muted);text-align:center">${result.metadata.description}</p>` : ''}
|
| 1849 |
+
</div>
|
| 1850 |
+
</div>`;
|
| 1851 |
+
} catch (err) { toast(err.message, 'error'); }
|
| 1852 |
+
}
|
| 1853 |
+
|
| 1854 |
+
// ==== API Keys ====
|
| 1855 |
+
async function loadAPIKeys() {
|
| 1856 |
+
try {
|
| 1857 |
+
const keys = await apiFetch('/user/api-keys');
|
| 1858 |
+
const el = document.getElementById('apiKeysList');
|
| 1859 |
+
if (keys.length === 0) {
|
| 1860 |
+
el.innerHTML = '<p style="color:var(--text-muted)">No API keys. Generate one for programmatic access.</p>';
|
| 1861 |
+
return;
|
| 1862 |
+
}
|
| 1863 |
+
el.innerHTML = `<table class="data-table"><thead><tr><th>Name</th><th>Key</th><th>Created</th><th>Last Used</th><th></th></tr></thead><tbody>` +
|
| 1864 |
+
keys.map(k => `<tr>
|
| 1865 |
+
<td>${esc(k.name)}</td>
|
| 1866 |
+
<td><code style="color:var(--accent-2)">${k.key_prefix}</code></td>
|
| 1867 |
+
<td>${new Date(k.created_at).toLocaleDateString()}</td>
|
| 1868 |
+
<td>${k.last_used ? new Date(k.last_used).toLocaleDateString() : 'Never'}</td>
|
| 1869 |
+
<td><button class="btn btn-ghost btn-sm" onclick="revokeKey('${k.id}')" style="color:#ef4444">Revoke</button></td>
|
| 1870 |
+
</tr>`).join('') +
|
| 1871 |
+
'</tbody></table>';
|
| 1872 |
+
} catch (err) { toast(err.message, 'error'); }
|
| 1873 |
+
}
|
| 1874 |
+
async function createNewAPIKey() {
|
| 1875 |
+
const name = prompt('API key name:');
|
| 1876 |
+
if (!name) return;
|
| 1877 |
+
try {
|
| 1878 |
+
const result = await apiFetch('/user/api-keys', 'POST', { name });
|
| 1879 |
+
alert('Your API key (shown only once):\n\n' + result.key + '\n\nSave it now!');
|
| 1880 |
+
loadAPIKeys();
|
| 1881 |
+
toast('API key created', 'success');
|
| 1882 |
+
} catch (err) { toast(err.message, 'error'); }
|
| 1883 |
+
}
|
| 1884 |
+
async function revokeKey(id) {
|
| 1885 |
+
if (!confirm('Revoke this key?')) return;
|
| 1886 |
+
try {
|
| 1887 |
+
await apiFetch(`/user/api-keys/${id}`, 'DELETE');
|
| 1888 |
+
loadAPIKeys();
|
| 1889 |
+
toast('Key revoked', 'success');
|
| 1890 |
+
} catch (err) { toast(err.message, 'error'); }
|
| 1891 |
+
}
|
| 1892 |
+
|
| 1893 |
+
// ==== API Fetch Helper ====
|
| 1894 |
+
async function apiFetch(path, method = 'GET', body = null, auth = true) {
|
| 1895 |
+
const headers = { 'Content-Type': 'application/json' };
|
| 1896 |
+
if (auth && token) headers['Authorization'] = `Bearer ${token}`;
|
| 1897 |
+
const opts = { method, headers };
|
| 1898 |
+
if (body) opts.body = JSON.stringify(body);
|
| 1899 |
+
const resp = await fetch(API + path, opts);
|
| 1900 |
+
const data = await resp.json();
|
| 1901 |
+
if (!resp.ok) throw new Error(data.detail || 'Request failed');
|
| 1902 |
+
return data;
|
| 1903 |
+
}
|
| 1904 |
+
|
| 1905 |
+
// ==== Utilities ====
|
| 1906 |
+
function closeModal(id) { document.getElementById(id).classList.remove('active'); }
|
| 1907 |
+
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
| 1908 |
+
function formatBytes(b) { if (b === 0) return '0 B'; const k = 1024; const s = ['B','KB','MB']; const i = Math.floor(Math.log(b)/Math.log(k)); return (b/Math.pow(k,i)).toFixed(1)+' '+s[i]; }
|
| 1909 |
+
function toast(msg, type = 'info') {
|
| 1910 |
+
const el = document.createElement('div');
|
| 1911 |
+
el.className = `toast ${type}`;
|
| 1912 |
+
el.textContent = msg;
|
| 1913 |
+
document.getElementById('toastContainer').appendChild(el);
|
| 1914 |
+
setTimeout(() => el.remove(), 4000);
|
| 1915 |
+
}
|
| 1916 |
+
|
| 1917 |
+
// Handle GitHub callback
|
| 1918 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 1919 |
+
if (urlParams.has('code')) {
|
| 1920 |
+
(async () => {
|
| 1921 |
+
try {
|
| 1922 |
+
const resp = await apiFetch(`/auth/github/callback?code=${urlParams.get('code')}`, 'GET', null, false);
|
| 1923 |
+
token = resp.access_token;
|
| 1924 |
+
localStorage.setItem('ragos_token', token);
|
| 1925 |
+
currentUser = resp.user;
|
| 1926 |
+
updateNavForUser();
|
| 1927 |
+
showView('dashboard');
|
| 1928 |
+
loadDashboard();
|
| 1929 |
+
toast('Signed in with GitHub!', 'success');
|
| 1930 |
+
window.history.replaceState({}, '', '/');
|
| 1931 |
+
} catch (err) { toast('GitHub auth failed: ' + err.message, 'error'); }
|
| 1932 |
+
})();
|
| 1933 |
+
}
|
| 1934 |
+
</script>
|
| 1935 |
+
</body>
|
| 1936 |
+
</html>
|
agentic_rag_os/models/__init__.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Database models and helpers — SQLite via aiosqlite."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import aiosqlite
|
| 6 |
+
import json
|
| 7 |
+
import uuid
|
| 8 |
+
from datetime import datetime, timezone
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from typing import Any, Dict, List, Optional
|
| 11 |
+
|
| 12 |
+
from agentic_rag_os.config import get_settings
|
| 13 |
+
|
| 14 |
+
_DB: Optional[aiosqlite.Connection] = None
|
| 15 |
+
|
| 16 |
+
SCHEMA = """
|
| 17 |
+
CREATE TABLE IF NOT EXISTS users (
|
| 18 |
+
id TEXT PRIMARY KEY,
|
| 19 |
+
username TEXT UNIQUE NOT NULL,
|
| 20 |
+
email TEXT UNIQUE,
|
| 21 |
+
password_hash TEXT,
|
| 22 |
+
github_id TEXT UNIQUE,
|
| 23 |
+
avatar_url TEXT DEFAULT '',
|
| 24 |
+
role TEXT DEFAULT 'user',
|
| 25 |
+
tier TEXT DEFAULT 'free',
|
| 26 |
+
created_at TEXT NOT NULL,
|
| 27 |
+
updated_at TEXT NOT NULL
|
| 28 |
+
);
|
| 29 |
+
|
| 30 |
+
CREATE TABLE IF NOT EXISTS api_keys (
|
| 31 |
+
id TEXT PRIMARY KEY,
|
| 32 |
+
user_id TEXT NOT NULL REFERENCES users(id),
|
| 33 |
+
key_hash TEXT UNIQUE NOT NULL,
|
| 34 |
+
name TEXT NOT NULL,
|
| 35 |
+
created_at TEXT NOT NULL,
|
| 36 |
+
last_used TEXT,
|
| 37 |
+
is_active INTEGER DEFAULT 1
|
| 38 |
+
);
|
| 39 |
+
|
| 40 |
+
CREATE TABLE IF NOT EXISTS domains (
|
| 41 |
+
id TEXT PRIMARY KEY,
|
| 42 |
+
user_id TEXT NOT NULL REFERENCES users(id),
|
| 43 |
+
name TEXT NOT NULL,
|
| 44 |
+
description TEXT DEFAULT '',
|
| 45 |
+
created_at TEXT NOT NULL,
|
| 46 |
+
updated_at TEXT NOT NULL,
|
| 47 |
+
UNIQUE(user_id, name)
|
| 48 |
+
);
|
| 49 |
+
|
| 50 |
+
CREATE TABLE IF NOT EXISTS documents (
|
| 51 |
+
id TEXT PRIMARY KEY,
|
| 52 |
+
domain_id TEXT NOT NULL REFERENCES domains(id),
|
| 53 |
+
filename TEXT NOT NULL,
|
| 54 |
+
content TEXT NOT NULL,
|
| 55 |
+
size_bytes INTEGER NOT NULL,
|
| 56 |
+
metadata TEXT DEFAULT '{}',
|
| 57 |
+
created_at TEXT NOT NULL
|
| 58 |
+
);
|
| 59 |
+
|
| 60 |
+
CREATE TABLE IF NOT EXISTS reward_configs (
|
| 61 |
+
id TEXT PRIMARY KEY,
|
| 62 |
+
user_id TEXT NOT NULL REFERENCES users(id),
|
| 63 |
+
domain_id TEXT REFERENCES domains(id),
|
| 64 |
+
name TEXT NOT NULL,
|
| 65 |
+
algorithm TEXT NOT NULL DEFAULT 'grpo',
|
| 66 |
+
config TEXT NOT NULL DEFAULT '{}',
|
| 67 |
+
created_at TEXT NOT NULL,
|
| 68 |
+
updated_at TEXT NOT NULL
|
| 69 |
+
);
|
| 70 |
+
|
| 71 |
+
CREATE TABLE IF NOT EXISTS reward_jobs (
|
| 72 |
+
id TEXT PRIMARY KEY,
|
| 73 |
+
user_id TEXT NOT NULL REFERENCES users(id),
|
| 74 |
+
reward_config_id TEXT NOT NULL REFERENCES reward_configs(id),
|
| 75 |
+
status TEXT NOT NULL DEFAULT 'pending',
|
| 76 |
+
result TEXT DEFAULT '{}',
|
| 77 |
+
created_at TEXT NOT NULL,
|
| 78 |
+
completed_at TEXT
|
| 79 |
+
);
|
| 80 |
+
|
| 81 |
+
CREATE TABLE IF NOT EXISTS rag_queries (
|
| 82 |
+
id TEXT PRIMARY KEY,
|
| 83 |
+
user_id TEXT NOT NULL REFERENCES users(id),
|
| 84 |
+
domain_id TEXT NOT NULL REFERENCES domains(id),
|
| 85 |
+
query TEXT NOT NULL,
|
| 86 |
+
results TEXT DEFAULT '[]',
|
| 87 |
+
reward_score REAL,
|
| 88 |
+
created_at TEXT NOT NULL
|
| 89 |
+
);
|
| 90 |
+
"""
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
async def get_db() -> aiosqlite.Connection:
|
| 94 |
+
"""Get or create the database connection."""
|
| 95 |
+
global _DB
|
| 96 |
+
if _DB is None:
|
| 97 |
+
settings = get_settings()
|
| 98 |
+
settings.db_path.parent.mkdir(parents=True, exist_ok=True)
|
| 99 |
+
_DB = await aiosqlite.connect(str(settings.db_path))
|
| 100 |
+
_DB.row_factory = aiosqlite.Row
|
| 101 |
+
await _DB.executescript(SCHEMA)
|
| 102 |
+
await _DB.commit()
|
| 103 |
+
return _DB
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
async def close_db() -> None:
|
| 107 |
+
global _DB
|
| 108 |
+
if _DB is not None:
|
| 109 |
+
await _DB.close()
|
| 110 |
+
_DB = None
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
# --- Helper functions ---
|
| 114 |
+
|
| 115 |
+
def new_id() -> str:
|
| 116 |
+
return str(uuid.uuid4())
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def now_iso() -> str:
|
| 120 |
+
return datetime.now(timezone.utc).isoformat()
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
async def fetch_one(query: str, params: tuple = ()) -> Optional[Dict[str, Any]]:
|
| 124 |
+
db = await get_db()
|
| 125 |
+
cursor = await db.execute(query, params)
|
| 126 |
+
row = await cursor.fetchone()
|
| 127 |
+
if row is None:
|
| 128 |
+
return None
|
| 129 |
+
return dict(row)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
async def fetch_all(query: str, params: tuple = ()) -> List[Dict[str, Any]]:
|
| 133 |
+
db = await get_db()
|
| 134 |
+
cursor = await db.execute(query, params)
|
| 135 |
+
rows = await cursor.fetchall()
|
| 136 |
+
return [dict(r) for r in rows]
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
async def execute(query: str, params: tuple = ()) -> None:
|
| 140 |
+
db = await get_db()
|
| 141 |
+
await db.execute(query, params)
|
| 142 |
+
await db.commit()
|
agentic_rag_os/models/schemas.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pydantic schemas for API request/response models."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
from typing import Any, Dict, List, Optional
|
| 7 |
+
|
| 8 |
+
from pydantic import BaseModel, Field
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
# --- Auth ---
|
| 12 |
+
|
| 13 |
+
class RegisterRequest(BaseModel):
|
| 14 |
+
username: str = Field(..., min_length=3, max_length=50)
|
| 15 |
+
email: str
|
| 16 |
+
password: str = Field(..., min_length=8)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class LoginRequest(BaseModel):
|
| 20 |
+
username: str
|
| 21 |
+
password: str
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class TokenResponse(BaseModel):
|
| 25 |
+
access_token: str
|
| 26 |
+
token_type: str = "bearer"
|
| 27 |
+
user: "UserOut"
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class UserOut(BaseModel):
|
| 31 |
+
id: str
|
| 32 |
+
username: str
|
| 33 |
+
email: Optional[str] = None
|
| 34 |
+
avatar_url: str = ""
|
| 35 |
+
role: str = "user"
|
| 36 |
+
tier: str = "free"
|
| 37 |
+
created_at: str
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
# --- Domain ---
|
| 41 |
+
|
| 42 |
+
class DomainCreate(BaseModel):
|
| 43 |
+
name: str = Field(..., min_length=1, max_length=100)
|
| 44 |
+
description: str = ""
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class DomainOut(BaseModel):
|
| 48 |
+
id: str
|
| 49 |
+
name: str
|
| 50 |
+
description: str
|
| 51 |
+
document_count: int = 0
|
| 52 |
+
total_size_bytes: int = 0
|
| 53 |
+
created_at: str
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# --- Document ---
|
| 57 |
+
|
| 58 |
+
class DocumentOut(BaseModel):
|
| 59 |
+
id: str
|
| 60 |
+
filename: str
|
| 61 |
+
size_bytes: int
|
| 62 |
+
metadata: Dict[str, Any] = {}
|
| 63 |
+
created_at: str
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
# --- RAG ---
|
| 67 |
+
|
| 68 |
+
class RAGQueryRequest(BaseModel):
|
| 69 |
+
query: str = Field(..., min_length=1)
|
| 70 |
+
top_k: int = Field(default=5, ge=1, le=20)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
class RAGResult(BaseModel):
|
| 74 |
+
content: str
|
| 75 |
+
score: float
|
| 76 |
+
metadata: Dict[str, Any] = {}
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
class RAGQueryResponse(BaseModel):
|
| 80 |
+
query: str
|
| 81 |
+
results: List[RAGResult]
|
| 82 |
+
domain: str
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
# --- Rewards-as-a-Service ---
|
| 86 |
+
|
| 87 |
+
class RewardConfigCreate(BaseModel):
|
| 88 |
+
name: str
|
| 89 |
+
algorithm: str = Field(default="grpo", pattern="^(grpo|ppo|dpo|reinforce|custom)$")
|
| 90 |
+
domain_id: Optional[str] = None
|
| 91 |
+
config: Dict[str, Any] = Field(default_factory=lambda: {
|
| 92 |
+
"retrieval_relevance_weight": 0.25,
|
| 93 |
+
"reasoning_quality_weight": 0.20,
|
| 94 |
+
"answer_completeness_weight": 0.30,
|
| 95 |
+
"efficiency_weight": 0.15,
|
| 96 |
+
"anti_hack_weight": 0.10,
|
| 97 |
+
})
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
class RewardConfigOut(BaseModel):
|
| 101 |
+
id: str
|
| 102 |
+
name: str
|
| 103 |
+
algorithm: str
|
| 104 |
+
domain_id: Optional[str] = None
|
| 105 |
+
config: Dict[str, Any]
|
| 106 |
+
created_at: str
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
class RewardJobCreate(BaseModel):
|
| 110 |
+
reward_config_id: str
|
| 111 |
+
input_data: Dict[str, Any] = {}
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
class RewardJobOut(BaseModel):
|
| 115 |
+
id: str
|
| 116 |
+
reward_config_id: str
|
| 117 |
+
status: str
|
| 118 |
+
result: Dict[str, Any] = {}
|
| 119 |
+
created_at: str
|
| 120 |
+
completed_at: Optional[str] = None
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
class RewardComputeRequest(BaseModel):
|
| 124 |
+
"""Compute reward on-the-fly without creating a job."""
|
| 125 |
+
algorithm: str = "grpo"
|
| 126 |
+
query: str = ""
|
| 127 |
+
answer: str = ""
|
| 128 |
+
retrieved_docs: List[str] = []
|
| 129 |
+
config: Dict[str, Any] = {}
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
class RewardComputeResponse(BaseModel):
|
| 133 |
+
total_reward: float
|
| 134 |
+
breakdown: Dict[str, float] = {}
|
| 135 |
+
algorithm: str
|
| 136 |
+
metadata: Dict[str, Any] = {}
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
# --- API Key ---
|
| 140 |
+
|
| 141 |
+
class APIKeyCreate(BaseModel):
|
| 142 |
+
name: str = Field(..., min_length=1, max_length=100)
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
class APIKeyOut(BaseModel):
|
| 146 |
+
id: str
|
| 147 |
+
name: str
|
| 148 |
+
key_prefix: str
|
| 149 |
+
created_at: str
|
| 150 |
+
last_used: Optional[str] = None
|
| 151 |
+
is_active: bool = True
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
class APIKeyCreated(BaseModel):
|
| 155 |
+
id: str
|
| 156 |
+
name: str
|
| 157 |
+
key: str # Full key, shown only once
|
| 158 |
+
created_at: str
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
# --- Dashboard Stats ---
|
| 162 |
+
|
| 163 |
+
class DashboardStats(BaseModel):
|
| 164 |
+
total_domains: int = 0
|
| 165 |
+
total_documents: int = 0
|
| 166 |
+
total_queries: int = 0
|
| 167 |
+
total_reward_configs: int = 0
|
| 168 |
+
storage_used_bytes: int = 0
|
| 169 |
+
storage_limit_bytes: int = 0
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
TokenResponse.model_rebuild()
|
agentic_rag_os/run.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Entry point for Agentic RAG OS application."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import uvicorn
|
| 6 |
+
|
| 7 |
+
from agentic_rag_os.config import get_settings
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def main() -> None:
|
| 11 |
+
settings = get_settings()
|
| 12 |
+
uvicorn.run(
|
| 13 |
+
"agentic_rag_os.api.app:app",
|
| 14 |
+
host=settings.host,
|
| 15 |
+
port=settings.port,
|
| 16 |
+
log_level="info",
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
if __name__ == "__main__":
|
| 21 |
+
main()
|
agentic_rag_os/services/__init__.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Authentication service — JWT + password hashing + GitHub OAuth."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import hashlib
|
| 6 |
+
import hmac
|
| 7 |
+
import secrets
|
| 8 |
+
from datetime import datetime, timedelta, timezone
|
| 9 |
+
from typing import Optional, Dict, Any
|
| 10 |
+
|
| 11 |
+
import httpx
|
| 12 |
+
import jwt
|
| 13 |
+
|
| 14 |
+
from agentic_rag_os.config import get_settings
|
| 15 |
+
from agentic_rag_os.models import fetch_one, execute, new_id, now_iso
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def _hash_password(password: str) -> str:
|
| 19 |
+
salt = secrets.token_hex(16)
|
| 20 |
+
h = hashlib.pbkdf2_hmac("sha256", password.encode(), salt.encode(), 310_000)
|
| 21 |
+
return f"{salt}${h.hex()}"
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _verify_password(password: str, stored: str) -> bool:
|
| 25 |
+
salt, h = stored.split("$", 1)
|
| 26 |
+
candidate = hashlib.pbkdf2_hmac("sha256", password.encode(), salt.encode(), 310_000)
|
| 27 |
+
return hmac.compare_digest(candidate.hex(), h)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _create_token(user_id: str, username: str, role: str = "user") -> str:
|
| 31 |
+
settings = get_settings()
|
| 32 |
+
payload = {
|
| 33 |
+
"sub": user_id,
|
| 34 |
+
"username": username,
|
| 35 |
+
"role": role,
|
| 36 |
+
"exp": datetime.now(timezone.utc) + timedelta(minutes=settings.jwt_expiry_minutes),
|
| 37 |
+
"iat": datetime.now(timezone.utc),
|
| 38 |
+
}
|
| 39 |
+
return jwt.encode(payload, settings.secret_key, algorithm=settings.jwt_algorithm)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def decode_token(token: str) -> Dict[str, Any]:
|
| 43 |
+
settings = get_settings()
|
| 44 |
+
return jwt.decode(token, settings.secret_key, algorithms=[settings.jwt_algorithm])
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
async def register_user(username: str, email: str, password: str) -> Dict[str, Any]:
|
| 48 |
+
existing = await fetch_one("SELECT id FROM users WHERE username=? OR email=?", (username, email))
|
| 49 |
+
if existing:
|
| 50 |
+
raise ValueError("Username or email already exists")
|
| 51 |
+
|
| 52 |
+
uid = new_id()
|
| 53 |
+
ts = now_iso()
|
| 54 |
+
pw_hash = _hash_password(password)
|
| 55 |
+
await execute(
|
| 56 |
+
"INSERT INTO users (id, username, email, password_hash, created_at, updated_at) VALUES (?,?,?,?,?,?)",
|
| 57 |
+
(uid, username, email, pw_hash, ts, ts),
|
| 58 |
+
)
|
| 59 |
+
user = await fetch_one("SELECT * FROM users WHERE id=?", (uid,))
|
| 60 |
+
token = _create_token(uid, username)
|
| 61 |
+
return {"access_token": token, "token_type": "bearer", "user": _user_out(user)}
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
async def login_user(username: str, password: str) -> Dict[str, Any]:
|
| 65 |
+
user = await fetch_one("SELECT * FROM users WHERE username=?", (username,))
|
| 66 |
+
if not user or not user.get("password_hash"):
|
| 67 |
+
raise ValueError("Invalid credentials")
|
| 68 |
+
if not _verify_password(password, user["password_hash"]):
|
| 69 |
+
raise ValueError("Invalid credentials")
|
| 70 |
+
token = _create_token(user["id"], user["username"], user.get("role", "user"))
|
| 71 |
+
return {"access_token": token, "token_type": "bearer", "user": _user_out(user)}
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
async def github_oauth_callback(code: str) -> Dict[str, Any]:
|
| 75 |
+
"""Exchange GitHub OAuth code for user info and create/login user."""
|
| 76 |
+
settings = get_settings()
|
| 77 |
+
async with httpx.AsyncClient() as client:
|
| 78 |
+
# Exchange code for access token
|
| 79 |
+
resp = await client.post(
|
| 80 |
+
"https://github.com/login/oauth/access_token",
|
| 81 |
+
json={
|
| 82 |
+
"client_id": settings.github_client_id,
|
| 83 |
+
"client_secret": settings.github_client_secret,
|
| 84 |
+
"code": code,
|
| 85 |
+
},
|
| 86 |
+
headers={"Accept": "application/json"},
|
| 87 |
+
)
|
| 88 |
+
data = resp.json()
|
| 89 |
+
gh_token = data.get("access_token")
|
| 90 |
+
if not gh_token:
|
| 91 |
+
raise ValueError("GitHub OAuth failed: " + data.get("error_description", "unknown"))
|
| 92 |
+
|
| 93 |
+
# Get user info
|
| 94 |
+
resp = await client.get(
|
| 95 |
+
"https://api.github.com/user",
|
| 96 |
+
headers={"Authorization": f"Bearer {gh_token}", "Accept": "application/json"},
|
| 97 |
+
)
|
| 98 |
+
gh_user = resp.json()
|
| 99 |
+
|
| 100 |
+
gh_id = str(gh_user["id"])
|
| 101 |
+
username = gh_user.get("login", f"gh_{gh_id}")
|
| 102 |
+
email = gh_user.get("email", "")
|
| 103 |
+
avatar = gh_user.get("avatar_url", "")
|
| 104 |
+
|
| 105 |
+
# Check if user exists by github_id
|
| 106 |
+
user = await fetch_one("SELECT * FROM users WHERE github_id=?", (gh_id,))
|
| 107 |
+
if user:
|
| 108 |
+
token = _create_token(user["id"], user["username"], user.get("role", "user"))
|
| 109 |
+
return {"access_token": token, "token_type": "bearer", "user": _user_out(user)}
|
| 110 |
+
|
| 111 |
+
# Create new user
|
| 112 |
+
uid = new_id()
|
| 113 |
+
ts = now_iso()
|
| 114 |
+
await execute(
|
| 115 |
+
"INSERT INTO users (id, username, email, github_id, avatar_url, created_at, updated_at) VALUES (?,?,?,?,?,?,?)",
|
| 116 |
+
(uid, username, email, gh_id, avatar, ts, ts),
|
| 117 |
+
)
|
| 118 |
+
user = await fetch_one("SELECT * FROM users WHERE id=?", (uid,))
|
| 119 |
+
token = _create_token(uid, username)
|
| 120 |
+
return {"access_token": token, "token_type": "bearer", "user": _user_out(user)}
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
async def get_user_by_id(user_id: str) -> Optional[Dict[str, Any]]:
|
| 124 |
+
user = await fetch_one("SELECT * FROM users WHERE id=?", (user_id,))
|
| 125 |
+
if user:
|
| 126 |
+
return _user_out(user)
|
| 127 |
+
return None
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def _user_out(user: Dict[str, Any]) -> Dict[str, Any]:
|
| 131 |
+
return {
|
| 132 |
+
"id": user["id"],
|
| 133 |
+
"username": user["username"],
|
| 134 |
+
"email": user.get("email", ""),
|
| 135 |
+
"avatar_url": user.get("avatar_url", ""),
|
| 136 |
+
"role": user.get("role", "user"),
|
| 137 |
+
"tier": user.get("tier", "free"),
|
| 138 |
+
"created_at": user["created_at"],
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
# --- API Key management ---
|
| 143 |
+
|
| 144 |
+
def _hash_api_key(key: str) -> str:
|
| 145 |
+
return hashlib.sha256(key.encode()).hexdigest()
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
async def create_api_key(user_id: str, name: str) -> Dict[str, Any]:
|
| 149 |
+
key = f"ragos_{secrets.token_hex(24)}"
|
| 150 |
+
kid = new_id()
|
| 151 |
+
ts = now_iso()
|
| 152 |
+
await execute(
|
| 153 |
+
"INSERT INTO api_keys (id, user_id, key_hash, name, created_at) VALUES (?,?,?,?,?)",
|
| 154 |
+
(kid, user_id, _hash_api_key(key), name, ts),
|
| 155 |
+
)
|
| 156 |
+
return {"id": kid, "name": name, "key": key, "created_at": ts}
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
async def validate_api_key(key: str) -> Optional[Dict[str, Any]]:
|
| 160 |
+
h = _hash_api_key(key)
|
| 161 |
+
row = await fetch_one(
|
| 162 |
+
"SELECT ak.*, u.username, u.role FROM api_keys ak JOIN users u ON ak.user_id=u.id WHERE ak.key_hash=? AND ak.is_active=1",
|
| 163 |
+
(h,),
|
| 164 |
+
)
|
| 165 |
+
if row:
|
| 166 |
+
await execute("UPDATE api_keys SET last_used=? WHERE id=?", (now_iso(), row["id"]))
|
| 167 |
+
return {"user_id": row["user_id"], "username": row["username"], "role": row["role"]}
|
| 168 |
+
return None
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
async def list_api_keys(user_id: str):
|
| 172 |
+
from agentic_rag_os.models import fetch_all
|
| 173 |
+
rows = await fetch_all(
|
| 174 |
+
"SELECT id, name, created_at, last_used, is_active FROM api_keys WHERE user_id=?",
|
| 175 |
+
(user_id,),
|
| 176 |
+
)
|
| 177 |
+
return [
|
| 178 |
+
{
|
| 179 |
+
"id": r["id"],
|
| 180 |
+
"name": r["name"],
|
| 181 |
+
"key_prefix": "ragos_****",
|
| 182 |
+
"created_at": r["created_at"],
|
| 183 |
+
"last_used": r.get("last_used"),
|
| 184 |
+
"is_active": bool(r["is_active"]),
|
| 185 |
+
}
|
| 186 |
+
for r in rows
|
| 187 |
+
]
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
async def revoke_api_key(user_id: str, key_id: str) -> bool:
|
| 191 |
+
row = await fetch_one("SELECT id FROM api_keys WHERE id=? AND user_id=?", (key_id, user_id))
|
| 192 |
+
if not row:
|
| 193 |
+
return False
|
| 194 |
+
await execute("UPDATE api_keys SET is_active=0 WHERE id=?", (key_id,))
|
| 195 |
+
return True
|
agentic_rag_os/services/rag_service.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""RAG service — user document ingestion, embedding, and retrieval."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Any, Dict, List, Optional
|
| 8 |
+
|
| 9 |
+
import numpy as np
|
| 10 |
+
|
| 11 |
+
from agentic_rag_os.config import get_settings
|
| 12 |
+
from agentic_rag_os.models import execute, fetch_all, fetch_one, new_id, now_iso
|
| 13 |
+
|
| 14 |
+
# Lazy-loaded retriever per domain
|
| 15 |
+
_retrievers: Dict[str, Any] = {}
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def _get_retriever(domain_id: str):
|
| 19 |
+
"""Get or create a FAISS retriever for a user domain."""
|
| 20 |
+
if domain_id not in _retrievers:
|
| 21 |
+
from rag_master.retriever import FAISSRetriever
|
| 22 |
+
settings = get_settings()
|
| 23 |
+
index_dir = Path(settings.db_path).parent / "faiss" / domain_id
|
| 24 |
+
index_dir.mkdir(parents=True, exist_ok=True)
|
| 25 |
+
_retrievers[domain_id] = FAISSRetriever(
|
| 26 |
+
index_dir=index_dir,
|
| 27 |
+
embedding_model=settings.embedding_model,
|
| 28 |
+
dimension=settings.faiss_dimension,
|
| 29 |
+
)
|
| 30 |
+
return _retrievers[domain_id]
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
async def create_domain(user_id: str, name: str, description: str = "") -> Dict[str, Any]:
|
| 34 |
+
"""Create a new domain for the user."""
|
| 35 |
+
did = new_id()
|
| 36 |
+
ts = now_iso()
|
| 37 |
+
await execute(
|
| 38 |
+
"INSERT INTO domains (id, user_id, name, description, created_at, updated_at) VALUES (?,?,?,?,?,?)",
|
| 39 |
+
(did, user_id, name, description, ts, ts),
|
| 40 |
+
)
|
| 41 |
+
return {"id": did, "name": name, "description": description, "document_count": 0, "total_size_bytes": 0, "created_at": ts}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
async def list_domains(user_id: str) -> List[Dict[str, Any]]:
|
| 45 |
+
domains = await fetch_all("SELECT * FROM domains WHERE user_id=?", (user_id,))
|
| 46 |
+
result = []
|
| 47 |
+
for d in domains:
|
| 48 |
+
docs = await fetch_all("SELECT id, size_bytes FROM documents WHERE domain_id=?", (d["id"],))
|
| 49 |
+
result.append({
|
| 50 |
+
"id": d["id"],
|
| 51 |
+
"name": d["name"],
|
| 52 |
+
"description": d["description"],
|
| 53 |
+
"document_count": len(docs),
|
| 54 |
+
"total_size_bytes": sum(doc["size_bytes"] for doc in docs),
|
| 55 |
+
"created_at": d["created_at"],
|
| 56 |
+
})
|
| 57 |
+
return result
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
async def get_domain(user_id: str, domain_id: str) -> Optional[Dict[str, Any]]:
|
| 61 |
+
return await fetch_one("SELECT * FROM domains WHERE id=? AND user_id=?", (domain_id, user_id))
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
async def delete_domain(user_id: str, domain_id: str) -> bool:
|
| 65 |
+
d = await fetch_one("SELECT id FROM domains WHERE id=? AND user_id=?", (domain_id, user_id))
|
| 66 |
+
if not d:
|
| 67 |
+
return False
|
| 68 |
+
await execute("DELETE FROM documents WHERE domain_id=?", (domain_id,))
|
| 69 |
+
await execute("DELETE FROM rag_queries WHERE domain_id=?", (domain_id,))
|
| 70 |
+
await execute("DELETE FROM domains WHERE id=?", (domain_id,))
|
| 71 |
+
if domain_id in _retrievers:
|
| 72 |
+
del _retrievers[domain_id]
|
| 73 |
+
return True
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
async def upload_document(user_id: str, domain_id: str, filename: str, content: str) -> Dict[str, Any]:
|
| 77 |
+
"""Upload a text document to a domain."""
|
| 78 |
+
settings = get_settings()
|
| 79 |
+
|
| 80 |
+
# Verify domain ownership
|
| 81 |
+
domain = await fetch_one("SELECT * FROM domains WHERE id=? AND user_id=?", (domain_id, user_id))
|
| 82 |
+
if not domain:
|
| 83 |
+
raise ValueError("Domain not found")
|
| 84 |
+
|
| 85 |
+
# Check storage limits
|
| 86 |
+
docs = await fetch_all("SELECT size_bytes FROM documents WHERE domain_id IN (SELECT id FROM domains WHERE user_id=?)", (user_id,))
|
| 87 |
+
total_used = sum(d["size_bytes"] for d in docs)
|
| 88 |
+
doc_size = len(content.encode("utf-8"))
|
| 89 |
+
limit = int(settings.max_upload_mb * 1024 * 1024)
|
| 90 |
+
if total_used + doc_size > limit:
|
| 91 |
+
raise ValueError(f"Storage limit exceeded. Free tier allows {settings.max_upload_mb}MB. Upgrade to Premium (coming soon) for {settings.premium_upload_mb}MB.")
|
| 92 |
+
|
| 93 |
+
doc_id = new_id()
|
| 94 |
+
ts = now_iso()
|
| 95 |
+
await execute(
|
| 96 |
+
"INSERT INTO documents (id, domain_id, filename, content, size_bytes, created_at) VALUES (?,?,?,?,?,?)",
|
| 97 |
+
(doc_id, domain_id, filename, content, doc_size, ts),
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
# Index in FAISS
|
| 101 |
+
retriever = _get_retriever(domain_id)
|
| 102 |
+
from rag_master.models import Document
|
| 103 |
+
doc_obj = Document(doc_id=doc_id, content=content, source=filename, metadata={"filename": filename})
|
| 104 |
+
retriever.index_documents([doc_obj])
|
| 105 |
+
|
| 106 |
+
return {"id": doc_id, "filename": filename, "size_bytes": doc_size, "metadata": {}, "created_at": ts}
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
async def list_documents(user_id: str, domain_id: str) -> List[Dict[str, Any]]:
|
| 110 |
+
domain = await fetch_one("SELECT id FROM domains WHERE id=? AND user_id=?", (domain_id, user_id))
|
| 111 |
+
if not domain:
|
| 112 |
+
return []
|
| 113 |
+
docs = await fetch_all("SELECT id, filename, size_bytes, metadata, created_at FROM documents WHERE domain_id=?", (domain_id,))
|
| 114 |
+
return [
|
| 115 |
+
{"id": d["id"], "filename": d["filename"], "size_bytes": d["size_bytes"],
|
| 116 |
+
"metadata": json.loads(d.get("metadata", "{}")), "created_at": d["created_at"]}
|
| 117 |
+
for d in docs
|
| 118 |
+
]
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
async def delete_document(user_id: str, domain_id: str, doc_id: str) -> bool:
|
| 122 |
+
domain = await fetch_one("SELECT id FROM domains WHERE id=? AND user_id=?", (domain_id, user_id))
|
| 123 |
+
if not domain:
|
| 124 |
+
return False
|
| 125 |
+
doc = await fetch_one("SELECT id FROM documents WHERE id=? AND domain_id=?", (doc_id, domain_id))
|
| 126 |
+
if not doc:
|
| 127 |
+
return False
|
| 128 |
+
await execute("DELETE FROM documents WHERE id=?", (doc_id,))
|
| 129 |
+
return True
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
async def rag_query(user_id: str, domain_id: str, query: str, top_k: int = 5) -> Dict[str, Any]:
|
| 133 |
+
"""Run a RAG query against user's domain documents."""
|
| 134 |
+
domain = await fetch_one("SELECT * FROM domains WHERE id=? AND user_id=?", (domain_id, user_id))
|
| 135 |
+
if not domain:
|
| 136 |
+
raise ValueError("Domain not found")
|
| 137 |
+
|
| 138 |
+
# Ensure all docs are indexed
|
| 139 |
+
retriever = _get_retriever(domain_id)
|
| 140 |
+
|
| 141 |
+
results = retriever.retrieve(query, top_k=top_k)
|
| 142 |
+
formatted = [
|
| 143 |
+
{"content": r.document.content, "score": round(r.score, 4), "metadata": r.document.metadata}
|
| 144 |
+
for r in results
|
| 145 |
+
]
|
| 146 |
+
|
| 147 |
+
# Log query
|
| 148 |
+
qid = new_id()
|
| 149 |
+
ts = now_iso()
|
| 150 |
+
await execute(
|
| 151 |
+
"INSERT INTO rag_queries (id, user_id, domain_id, query, results, created_at) VALUES (?,?,?,?,?,?)",
|
| 152 |
+
(qid, user_id, domain_id, query, json.dumps(formatted), ts),
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
return {"query": query, "results": formatted, "domain": domain["name"]}
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
async def reindex_domain(domain_id: str) -> None:
|
| 159 |
+
"""Reindex all documents in a domain's FAISS index."""
|
| 160 |
+
retriever = _get_retriever(domain_id)
|
| 161 |
+
retriever.clear_index()
|
| 162 |
+
docs = await fetch_all("SELECT id, filename, content FROM documents WHERE domain_id=?", (domain_id,))
|
| 163 |
+
if docs:
|
| 164 |
+
from rag_master.models import Document
|
| 165 |
+
doc_objs = [
|
| 166 |
+
Document(doc_id=d["id"], content=d["content"], source=d["filename"], metadata={"filename": d["filename"]})
|
| 167 |
+
for d in docs
|
| 168 |
+
]
|
| 169 |
+
retriever.index_documents(doc_objs)
|
agentic_rag_os/services/reward_service.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Rewards-as-a-Service — dynamic reward computation for any algorithm."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import math
|
| 7 |
+
import re
|
| 8 |
+
from typing import Any, Dict, List, Optional
|
| 9 |
+
|
| 10 |
+
from agentic_rag_os.models import execute, fetch_all, fetch_one, new_id, now_iso
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
# --- Reward Config CRUD ---
|
| 14 |
+
|
| 15 |
+
async def create_reward_config(user_id: str, name: str, algorithm: str, domain_id: Optional[str], config: Dict[str, Any]) -> Dict[str, Any]:
|
| 16 |
+
rid = new_id()
|
| 17 |
+
ts = now_iso()
|
| 18 |
+
await execute(
|
| 19 |
+
"INSERT INTO reward_configs (id, user_id, domain_id, name, algorithm, config, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)",
|
| 20 |
+
(rid, user_id, domain_id, name, algorithm, json.dumps(config), ts, ts),
|
| 21 |
+
)
|
| 22 |
+
return {"id": rid, "name": name, "algorithm": algorithm, "domain_id": domain_id, "config": config, "created_at": ts}
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
async def list_reward_configs(user_id: str) -> List[Dict[str, Any]]:
|
| 26 |
+
rows = await fetch_all("SELECT * FROM reward_configs WHERE user_id=?", (user_id,))
|
| 27 |
+
return [
|
| 28 |
+
{"id": r["id"], "name": r["name"], "algorithm": r["algorithm"],
|
| 29 |
+
"domain_id": r.get("domain_id"), "config": json.loads(r["config"]), "created_at": r["created_at"]}
|
| 30 |
+
for r in rows
|
| 31 |
+
]
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
async def get_reward_config(user_id: str, config_id: str) -> Optional[Dict[str, Any]]:
|
| 35 |
+
r = await fetch_one("SELECT * FROM reward_configs WHERE id=? AND user_id=?", (config_id, user_id))
|
| 36 |
+
if not r:
|
| 37 |
+
return None
|
| 38 |
+
return {"id": r["id"], "name": r["name"], "algorithm": r["algorithm"],
|
| 39 |
+
"domain_id": r.get("domain_id"), "config": json.loads(r["config"]), "created_at": r["created_at"]}
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
async def delete_reward_config(user_id: str, config_id: str) -> bool:
|
| 43 |
+
r = await fetch_one("SELECT id FROM reward_configs WHERE id=? AND user_id=?", (config_id, user_id))
|
| 44 |
+
if not r:
|
| 45 |
+
return False
|
| 46 |
+
await execute("DELETE FROM reward_jobs WHERE reward_config_id=?", (config_id,))
|
| 47 |
+
await execute("DELETE FROM reward_configs WHERE id=?", (config_id,))
|
| 48 |
+
return True
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
# --- Reward Computation Engine ---
|
| 52 |
+
|
| 53 |
+
def compute_reward(
|
| 54 |
+
algorithm: str,
|
| 55 |
+
query: str = "",
|
| 56 |
+
answer: str = "",
|
| 57 |
+
retrieved_docs: List[str] = [],
|
| 58 |
+
config: Dict[str, Any] = {},
|
| 59 |
+
) -> Dict[str, Any]:
|
| 60 |
+
"""Compute reward dynamically based on algorithm and config."""
|
| 61 |
+
|
| 62 |
+
weights = {
|
| 63 |
+
"retrieval_relevance_weight": config.get("retrieval_relevance_weight", 0.25),
|
| 64 |
+
"reasoning_quality_weight": config.get("reasoning_quality_weight", 0.20),
|
| 65 |
+
"answer_completeness_weight": config.get("answer_completeness_weight", 0.30),
|
| 66 |
+
"efficiency_weight": config.get("efficiency_weight", 0.15),
|
| 67 |
+
"anti_hack_weight": config.get("anti_hack_weight", 0.10),
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
breakdown = {}
|
| 71 |
+
|
| 72 |
+
# 1. Retrieval relevance — overlap between query and docs
|
| 73 |
+
if retrieved_docs and query:
|
| 74 |
+
query_tokens = set(query.lower().split())
|
| 75 |
+
doc_scores = []
|
| 76 |
+
for doc in retrieved_docs:
|
| 77 |
+
doc_tokens = set(doc.lower().split())
|
| 78 |
+
overlap = len(query_tokens & doc_tokens) / max(len(query_tokens), 1)
|
| 79 |
+
doc_scores.append(min(overlap * 1.5, 1.0))
|
| 80 |
+
breakdown["retrieval_relevance"] = sum(doc_scores) / len(doc_scores) if doc_scores else 0.0
|
| 81 |
+
else:
|
| 82 |
+
breakdown["retrieval_relevance"] = 0.0
|
| 83 |
+
|
| 84 |
+
# 2. Reasoning quality — presence of reasoning markers in answer
|
| 85 |
+
reasoning_markers = ["because", "therefore", "however", "analysis", "evidence", "conclusion", "furthermore"]
|
| 86 |
+
if answer:
|
| 87 |
+
answer_lower = answer.lower()
|
| 88 |
+
marker_count = sum(1 for m in reasoning_markers if m in answer_lower)
|
| 89 |
+
breakdown["reasoning_quality"] = min(marker_count / 4.0, 1.0)
|
| 90 |
+
else:
|
| 91 |
+
breakdown["reasoning_quality"] = 0.0
|
| 92 |
+
|
| 93 |
+
# 3. Answer completeness — length and coverage
|
| 94 |
+
if answer:
|
| 95 |
+
word_count = len(answer.split())
|
| 96 |
+
length_score = min(word_count / 150.0, 1.0)
|
| 97 |
+
# Coverage of query terms in answer
|
| 98 |
+
if query:
|
| 99 |
+
q_tokens = set(query.lower().split())
|
| 100 |
+
a_tokens = set(answer.lower().split())
|
| 101 |
+
coverage = len(q_tokens & a_tokens) / max(len(q_tokens), 1)
|
| 102 |
+
else:
|
| 103 |
+
coverage = 0.5
|
| 104 |
+
breakdown["answer_completeness"] = 0.6 * length_score + 0.4 * coverage
|
| 105 |
+
else:
|
| 106 |
+
breakdown["answer_completeness"] = 0.0
|
| 107 |
+
|
| 108 |
+
# 4. Efficiency — penalize very long or very short answers
|
| 109 |
+
if answer:
|
| 110 |
+
wc = len(answer.split())
|
| 111 |
+
if wc < 20:
|
| 112 |
+
breakdown["efficiency"] = 0.3
|
| 113 |
+
elif wc > 500:
|
| 114 |
+
breakdown["efficiency"] = 0.6
|
| 115 |
+
else:
|
| 116 |
+
breakdown["efficiency"] = 0.9
|
| 117 |
+
else:
|
| 118 |
+
breakdown["efficiency"] = 0.0
|
| 119 |
+
|
| 120 |
+
# 5. Anti-hack penalty
|
| 121 |
+
penalty = 0.0
|
| 122 |
+
if answer:
|
| 123 |
+
words = answer.lower().split()
|
| 124 |
+
unique_ratio = len(set(words)) / max(len(words), 1)
|
| 125 |
+
if unique_ratio < 0.3:
|
| 126 |
+
penalty += 0.5
|
| 127 |
+
# Repetition check
|
| 128 |
+
if len(words) > 10:
|
| 129 |
+
bigrams = [f"{words[i]}_{words[i+1]}" for i in range(len(words) - 1)]
|
| 130 |
+
bigram_unique = len(set(bigrams)) / max(len(bigrams), 1)
|
| 131 |
+
if bigram_unique < 0.4:
|
| 132 |
+
penalty += 0.3
|
| 133 |
+
breakdown["anti_hack_penalty"] = min(penalty, 1.0)
|
| 134 |
+
|
| 135 |
+
# Compute total
|
| 136 |
+
total = (
|
| 137 |
+
weights["retrieval_relevance_weight"] * breakdown["retrieval_relevance"]
|
| 138 |
+
+ weights["reasoning_quality_weight"] * breakdown["reasoning_quality"]
|
| 139 |
+
+ weights["answer_completeness_weight"] * breakdown["answer_completeness"]
|
| 140 |
+
+ weights["efficiency_weight"] * breakdown["efficiency"]
|
| 141 |
+
- weights["anti_hack_weight"] * breakdown["anti_hack_penalty"]
|
| 142 |
+
)
|
| 143 |
+
total = max(0.01, min(0.99, total))
|
| 144 |
+
|
| 145 |
+
# Algorithm-specific adjustments
|
| 146 |
+
algo_meta = {}
|
| 147 |
+
if algorithm == "grpo":
|
| 148 |
+
algo_meta["group_relative"] = True
|
| 149 |
+
algo_meta["description"] = "Group Relative Policy Optimization — rewards are relative within groups"
|
| 150 |
+
elif algorithm == "ppo":
|
| 151 |
+
algo_meta["clipped"] = True
|
| 152 |
+
algo_meta["clip_range"] = config.get("clip_range", 0.2)
|
| 153 |
+
algo_meta["description"] = "Proximal Policy Optimization — clipped surrogate objective"
|
| 154 |
+
elif algorithm == "dpo":
|
| 155 |
+
algo_meta["preference_based"] = True
|
| 156 |
+
algo_meta["beta"] = config.get("beta", 0.1)
|
| 157 |
+
algo_meta["description"] = "Direct Preference Optimization — reward from preference pairs"
|
| 158 |
+
elif algorithm == "reinforce":
|
| 159 |
+
algo_meta["baseline"] = config.get("baseline", 0.5)
|
| 160 |
+
algo_meta["description"] = "REINFORCE — vanilla policy gradient with baseline"
|
| 161 |
+
total = total - algo_meta["baseline"]
|
| 162 |
+
total = max(0.01, min(0.99, (total + 1) / 2))
|
| 163 |
+
else:
|
| 164 |
+
algo_meta["description"] = "Custom reward function"
|
| 165 |
+
|
| 166 |
+
return {
|
| 167 |
+
"total_reward": round(total, 6),
|
| 168 |
+
"breakdown": {k: round(v, 4) for k, v in breakdown.items()},
|
| 169 |
+
"algorithm": algorithm,
|
| 170 |
+
"metadata": algo_meta,
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
# --- Reward Jobs ---
|
| 175 |
+
|
| 176 |
+
async def create_reward_job(user_id: str, reward_config_id: str, input_data: Dict[str, Any]) -> Dict[str, Any]:
|
| 177 |
+
config_row = await fetch_one("SELECT * FROM reward_configs WHERE id=? AND user_id=?", (reward_config_id, user_id))
|
| 178 |
+
if not config_row:
|
| 179 |
+
raise ValueError("Reward config not found")
|
| 180 |
+
|
| 181 |
+
jid = new_id()
|
| 182 |
+
ts = now_iso()
|
| 183 |
+
|
| 184 |
+
# Compute immediately (synchronous for now)
|
| 185 |
+
cfg = json.loads(config_row["config"])
|
| 186 |
+
result = compute_reward(
|
| 187 |
+
algorithm=config_row["algorithm"],
|
| 188 |
+
query=input_data.get("query", ""),
|
| 189 |
+
answer=input_data.get("answer", ""),
|
| 190 |
+
retrieved_docs=input_data.get("retrieved_docs", []),
|
| 191 |
+
config=cfg,
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
await execute(
|
| 195 |
+
"INSERT INTO reward_jobs (id, user_id, reward_config_id, status, result, created_at, completed_at) VALUES (?,?,?,?,?,?,?)",
|
| 196 |
+
(jid, user_id, reward_config_id, "completed", json.dumps(result), ts, ts),
|
| 197 |
+
)
|
| 198 |
+
return {"id": jid, "reward_config_id": reward_config_id, "status": "completed", "result": result, "created_at": ts, "completed_at": ts}
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
async def list_reward_jobs(user_id: str, config_id: Optional[str] = None) -> List[Dict[str, Any]]:
|
| 202 |
+
if config_id:
|
| 203 |
+
rows = await fetch_all(
|
| 204 |
+
"SELECT * FROM reward_jobs WHERE user_id=? AND reward_config_id=? ORDER BY created_at DESC LIMIT 50",
|
| 205 |
+
(user_id, config_id),
|
| 206 |
+
)
|
| 207 |
+
else:
|
| 208 |
+
rows = await fetch_all(
|
| 209 |
+
"SELECT * FROM reward_jobs WHERE user_id=? ORDER BY created_at DESC LIMIT 50",
|
| 210 |
+
(user_id,),
|
| 211 |
+
)
|
| 212 |
+
return [
|
| 213 |
+
{"id": r["id"], "reward_config_id": r["reward_config_id"], "status": r["status"],
|
| 214 |
+
"result": json.loads(r.get("result", "{}")), "created_at": r["created_at"], "completed_at": r.get("completed_at")}
|
| 215 |
+
for r in rows
|
| 216 |
+
]
|
documents/agentic_rag_os_architecture.md
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Agentic RAG OS — Architecture & Design Document
|
| 2 |
+
|
| 3 |
+
## 1. Overview
|
| 4 |
+
|
| 5 |
+
**Agentic RAG OS** is an RL B2B Rewards-as-a-Service (RaaS) platform — the operating system for agentic Retrieval-Augmented Generation. It provides a complete pipeline from user data ingestion → embedding → retrieval → reward signal generation, enabling teams to build, evaluate, and fine-tune LLMs using production-grade reward functions.
|
| 6 |
+
|
| 7 |
+
### 1.1 Mission Statement
|
| 8 |
+
|
| 9 |
+
Democratize access to RL-driven RAG improvement by providing infrastructure-as-a-service for reward signal generation, eliminating the need for teams to build custom evaluation pipelines from scratch.
|
| 10 |
+
|
| 11 |
+
### 1.2 Core Value Proposition
|
| 12 |
+
|
| 13 |
+
| Capability | Description |
|
| 14 |
+
|---|---|
|
| 15 |
+
| **Data → Embeddings** | Upload text documents, auto-embed with sentence-transformers, store in FAISS |
|
| 16 |
+
| **Rewards-as-a-Service** | Generate reward signals for GRPO, PPO, DPO, REINFORCE via API |
|
| 17 |
+
| **Multi-Algorithm Support** | Choose reward algorithm dynamically per request |
|
| 18 |
+
| **Anti-Reward Hacking** | Built-in guards against gaming, repetition, degenerate outputs |
|
| 19 |
+
| **API-First** | Every capability exposed as RESTful endpoint |
|
| 20 |
+
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
## 2. System Architecture
|
| 24 |
+
|
| 25 |
+
```
|
| 26 |
+
┌─────────────────────────────────────────────────────────────┐
|
| 27 |
+
│ Client Layer │
|
| 28 |
+
│ ┌──────────────┐ ┌─────────────┐ ┌───────────────────┐ │
|
| 29 |
+
│ │ Web App SPA │ │ API Client │ │ SDK / CLI │ │
|
| 30 |
+
│ │ (Dark Theme) │ │ (REST/JWT) │ │ (API Key Auth) │ │
|
| 31 |
+
│ └──────┬───────┘ └──────┬──────┘ └────────┬──────────┘ │
|
| 32 |
+
└─────────┼──────────────────┼──────────────────┼──────────────┘
|
| 33 |
+
│ │ │
|
| 34 |
+
▼ ▼ ▼
|
| 35 |
+
┌─────────────────────────────────────────────────────────────┐
|
| 36 |
+
│ FastAPI Application Layer │
|
| 37 |
+
│ │
|
| 38 |
+
│ ┌──────────────┐ ┌────────────┐ ┌──────────────────┐ │
|
| 39 |
+
│ │ Auth Routes │ │ RAG Routes │ │ Reward Routes │ │
|
| 40 |
+
│ │ /auth/* │ │ /rag/* │ │ /rewards/* │ │
|
| 41 |
+
│ └──────┬───────┘ └─────┬──────┘ └────────┬─────────┘ │
|
| 42 |
+
│ │ │ │ │
|
| 43 |
+
│ ┌──────▼───────┐ ┌─────▼──────┐ ┌────────▼─────────┐ │
|
| 44 |
+
│ │ User Routes │ │ Deps │ │ Health / Docs │ │
|
| 45 |
+
│ │ /user/* │ │ (Auth DI) │ │ /api/docs|redoc │ │
|
| 46 |
+
│ └──────────────┘ └────────────┘ └──────────────────┘ │
|
| 47 |
+
└──────────────────────────┬──────────────────────────────────┘
|
| 48 |
+
│
|
| 49 |
+
┌──────────────────────────▼──────────────────────────────────┐
|
| 50 |
+
│ Service Layer │
|
| 51 |
+
│ │
|
| 52 |
+
│ ┌──────────────────┐ ┌───────────────────────────────┐ │
|
| 53 |
+
│ │ Auth Service │ │ RAG Service │ │
|
| 54 |
+
│ │ - JWT tokens │ │ - Domain CRUD │ │
|
| 55 |
+
│ │ - Password hash │ │ - Document upload/embed │ │
|
| 56 |
+
│ │ - GitHub OAuth │ │ - FAISS indexing │ │
|
| 57 |
+
│ │ - API keys │ │ - Semantic retrieval │ │
|
| 58 |
+
│ └──────────────────┘ └───────────────────────────────┘ │
|
| 59 |
+
│ │
|
| 60 |
+
│ ┌──��─────────────────────────────────────────────────────┐ │
|
| 61 |
+
│ │ Reward Service │ │
|
| 62 |
+
│ │ - Multi-algorithm compute (GRPO, PPO, DPO, REINFORCE) │ │
|
| 63 |
+
│ │ - Config management │ │
|
| 64 |
+
│ │ - Job execution │ │
|
| 65 |
+
│ │ - Anti-hack detection │ │
|
| 66 |
+
│ └────────────────────────────────────────────────────────┘ │
|
| 67 |
+
└──────────────────────────┬──────────────────────────────────┘
|
| 68 |
+
│
|
| 69 |
+
┌──────────────────────────▼──────────────────────────────────┐
|
| 70 |
+
│ Data / Storage Layer │
|
| 71 |
+
│ │
|
| 72 |
+
│ ┌──────────────┐ ┌────────────────┐ ┌────────────────┐ │
|
| 73 |
+
│ │ SQLite DB │ │ FAISS Indices │ │ File Storage │ │
|
| 74 |
+
│ │ - Users │ │ - Per-domain │ │ - Uploads │ │
|
| 75 |
+
│ │ - API Keys │ │ - MiniLM-L6 │ │ - 2MB free │ │
|
| 76 |
+
│ │ - Domains │ │ - 384-dim │ │ - Premium │ │
|
| 77 |
+
│ │ - Documents │ │ - Inner product │ │ coming soon │ │
|
| 78 |
+
│ │ - Configs │ │ │ │ │ │
|
| 79 |
+
│ │ - Jobs │ │ │ │ │ │
|
| 80 |
+
│ └──────────────┘ └────────────────┘ └────────────────┘ │
|
| 81 |
+
└──────────────────────────┬──────────────────────────────────┘
|
| 82 |
+
│
|
| 83 |
+
┌──────────────────────────▼──────────────────────────────────┐
|
| 84 |
+
│ RAG Master Core │
|
| 85 |
+
│ (Shared library from agentic-rag-gym) │
|
| 86 |
+
│ │
|
| 87 |
+
│ ┌──────────────┐ ┌────────────────┐ ┌────────────────┐ │
|
| 88 |
+
│ │ Retriever │ │ Reward Funcs │ │ Models │ │
|
| 89 |
+
│ │ FAISSRetriever│ │ Composite │ │ Document │ │
|
| 90 |
+
│ │ SentenceTransf│ │ LLM Judge │ │ Trajectory │ │
|
| 91 |
+
│ └──────────────┘ └────────────────┘ └────────────────┘ │
|
| 92 |
+
│ │
|
| 93 |
+
│ ┌──────────────┐ ┌────────────────┐ ┌────────────────┐ │
|
| 94 |
+
│ │ Agents │ │ Orchestrator │ │ Adapters │ │
|
| 95 |
+
│ │ 5 specialized│ │ Multi-agent │ │ Base classes │ │
|
| 96 |
+
│ └──────────────┘ └────────────────┘ └────────────────┘ │
|
| 97 |
+
└─────────────────────────────────────────────────────────────┘
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
---
|
| 101 |
+
|
| 102 |
+
## 3. Authentication Architecture
|
| 103 |
+
|
| 104 |
+
### 3.1 Dual Auth Strategy
|
| 105 |
+
|
| 106 |
+
```
|
| 107 |
+
┌──────────────────────────────────────────┐
|
| 108 |
+
│ Authentication Flow │
|
| 109 |
+
│ │
|
| 110 |
+
│ Browser ─→ JWT Bearer Token │
|
| 111 |
+
│ 1. POST /auth/register (username/pw) │
|
| 112 |
+
│ 2. POST /auth/login (username/pw) │
|
| 113 |
+
│ 3. GET /auth/github/callback (OAuth) │
|
| 114 |
+
│ ↓ │
|
| 115 |
+
│ JWT in Authorization: Bearer <token> │
|
| 116 |
+
│ │
|
| 117 |
+
│ API Client ─→ API Key │
|
| 118 |
+
│ 1. POST /user/api-keys (generate) │
|
| 119 |
+
│ ↓ │
|
| 120 |
+
│ X-API-Key: ragos_<hex> │
|
| 121 |
+
└──────────────────────────────────────────┘
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
### 3.2 Security Measures
|
| 125 |
+
|
| 126 |
+
- **Password Hashing**: PBKDF2-HMAC-SHA256, 310,000 iterations, random salt
|
| 127 |
+
- **JWT**: HS256 signing, 24h expiry, rotating secret keys
|
| 128 |
+
- **API Keys**: SHA-256 hashed storage, prefix-only display, revocable
|
| 129 |
+
- **GitHub OAuth**: Standard OAuth 2.0 code exchange flow
|
| 130 |
+
|
| 131 |
+
---
|
| 132 |
+
|
| 133 |
+
## 4. Data Flow
|
| 134 |
+
|
| 135 |
+
### 4.1 Document Ingestion Pipeline
|
| 136 |
+
|
| 137 |
+
```
|
| 138 |
+
User Upload (text file)
|
| 139 |
+
│
|
| 140 |
+
▼
|
| 141 |
+
Storage Limit Check (2MB free / 100MB premium)
|
| 142 |
+
│
|
| 143 |
+
▼
|
| 144 |
+
SQLite Record (id, content, metadata)
|
| 145 |
+
│
|
| 146 |
+
▼
|
| 147 |
+
Sentence-Transformer Encoding (all-MiniLM-L6-v2, 384-dim)
|
| 148 |
+
│
|
| 149 |
+
▼
|
| 150 |
+
FAISS IndexFlatIP (inner product similarity)
|
| 151 |
+
│
|
| 152 |
+
▼
|
| 153 |
+
Domain-Isolated Index (per-user, per-domain)
|
| 154 |
+
```
|
| 155 |
+
|
| 156 |
+
### 4.2 RAG Query Pipeline
|
| 157 |
+
|
| 158 |
+
```
|
| 159 |
+
User Query (text)
|
| 160 |
+
│
|
| 161 |
+
▼
|
| 162 |
+
Embedding (same model as indexing)
|
| 163 |
+
│
|
| 164 |
+
▼
|
| 165 |
+
FAISS Similarity Search (top-k, cosine via normalized IP)
|
| 166 |
+
│
|
| 167 |
+
▼
|
| 168 |
+
Score Normalization [0, 1]
|
| 169 |
+
│
|
| 170 |
+
▼
|
| 171 |
+
Results + Metadata → Response
|
| 172 |
+
│
|
| 173 |
+
▼
|
| 174 |
+
Query Logged (for analytics)
|
| 175 |
+
```
|
| 176 |
+
|
| 177 |
+
### 4.3 Reward Computation Pipeline
|
| 178 |
+
|
| 179 |
+
```
|
| 180 |
+
Input: {algorithm, query, answer, retrieved_docs, config}
|
| 181 |
+
│
|
| 182 |
+
├──→ Retrieval Relevance (query-doc overlap)
|
| 183 |
+
├──→ Reasoning Quality (marker detection)
|
| 184 |
+
├──→ Answer Completeness (length + coverage)
|
| 185 |
+
├──→ Efficiency (length bounds)
|
| 186 |
+
└──→ Anti-Hack Detection (degenerate check)
|
| 187 |
+
│
|
| 188 |
+
▼
|
| 189 |
+
Weighted Sum → Clamp [0.01, 0.99]
|
| 190 |
+
│
|
| 191 |
+
▼
|
| 192 |
+
Algorithm-Specific Adjustments
|
| 193 |
+
│
|
| 194 |
+
▼
|
| 195 |
+
{total_reward, breakdown, algorithm, metadata}
|
| 196 |
+
```
|
| 197 |
+
|
| 198 |
+
---
|
| 199 |
+
|
| 200 |
+
## 5. API Design
|
| 201 |
+
|
| 202 |
+
### 5.1 Endpoint Map
|
| 203 |
+
|
| 204 |
+
| Method | Path | Auth | Description |
|
| 205 |
+
|--------|------|------|-------------|
|
| 206 |
+
| POST | `/api/v1/auth/register` | No | Create account |
|
| 207 |
+
| POST | `/api/v1/auth/login` | No | Login, get JWT |
|
| 208 |
+
| GET | `/api/v1/auth/github/callback` | No | GitHub OAuth |
|
| 209 |
+
| GET | `/api/v1/user/me` | Yes | Current user profile |
|
| 210 |
+
| GET | `/api/v1/user/dashboard` | Yes | Dashboard statistics |
|
| 211 |
+
| POST | `/api/v1/user/api-keys` | Yes | Create API key |
|
| 212 |
+
| GET | `/api/v1/user/api-keys` | Yes | List API keys |
|
| 213 |
+
| DELETE | `/api/v1/user/api-keys/:id` | Yes | Revoke API key |
|
| 214 |
+
| POST | `/api/v1/rag/domains` | Yes | Create domain |
|
| 215 |
+
| GET | `/api/v1/rag/domains` | Yes | List domains |
|
| 216 |
+
| DELETE | `/api/v1/rag/domains/:id` | Yes | Delete domain |
|
| 217 |
+
| POST | `/api/v1/rag/domains/:id/documents` | Yes | Upload document |
|
| 218 |
+
| GET | `/api/v1/rag/domains/:id/documents` | Yes | List documents |
|
| 219 |
+
| DELETE | `/api/v1/rag/domains/:id/documents/:docId` | Yes | Delete document |
|
| 220 |
+
| POST | `/api/v1/rag/domains/:id/query` | Yes | RAG query |
|
| 221 |
+
| POST | `/api/v1/rewards/compute` | Yes | Compute reward |
|
| 222 |
+
| POST | `/api/v1/rewards/configs` | Yes | Create config |
|
| 223 |
+
| GET | `/api/v1/rewards/configs` | Yes | List configs |
|
| 224 |
+
| DELETE | `/api/v1/rewards/configs/:id` | Yes | Delete config |
|
| 225 |
+
| POST | `/api/v1/rewards/jobs` | Yes | Create job |
|
| 226 |
+
| GET | `/api/v1/rewards/jobs` | Yes | List jobs |
|
| 227 |
+
| GET | `/api/v1/rewards/algorithms` | No | List algorithms |
|
| 228 |
+
| GET | `/api/v1/health` | No | Health check |
|
| 229 |
+
|
| 230 |
+
### 5.2 Error Responses
|
| 231 |
+
|
| 232 |
+
All errors follow a consistent format:
|
| 233 |
+
```json
|
| 234 |
+
{
|
| 235 |
+
"detail": "Human-readable error message"
|
| 236 |
+
}
|
| 237 |
+
```
|
| 238 |
+
|
| 239 |
+
Standard HTTP status codes: 400 (bad request), 401 (unauthorized), 404 (not found), 500 (server error).
|
| 240 |
+
|
| 241 |
+
---
|
| 242 |
+
|
| 243 |
+
## 6. Reward Algorithms
|
| 244 |
+
|
| 245 |
+
### 6.1 Supported Algorithms
|
| 246 |
+
|
| 247 |
+
| Algorithm | Description | Use Case |
|
| 248 |
+
|-----------|-------------|----------|
|
| 249 |
+
| **GRPO** | Group Relative Policy Optimization | Stable RL training with relative rewards |
|
| 250 |
+
| **PPO** | Proximal Policy Optimization | Clipped surrogate for monotonic improvement |
|
| 251 |
+
| **DPO** | Direct Preference Optimization | Preference-based, no reward model needed |
|
| 252 |
+
| **REINFORCE** | Policy gradient with baseline | Simple gradient estimation |
|
| 253 |
+
| **Custom** | User-defined weights | Domain-specific scoring |
|
| 254 |
+
|
| 255 |
+
### 6.2 Reward Signal Components
|
| 256 |
+
|
| 257 |
+
| Component | Weight | Signal Source |
|
| 258 |
+
|-----------|--------|---------------|
|
| 259 |
+
| Retrieval Relevance | 0.25 | Query-document token overlap |
|
| 260 |
+
| Reasoning Quality | 0.20 | Reasoning marker detection |
|
| 261 |
+
| Answer Completeness | 0.30 | Length + query coverage |
|
| 262 |
+
| Efficiency | 0.15 | Answer length bounds |
|
| 263 |
+
| Anti-Hack Penalty | 0.10 | Degenerate content detection |
|
| 264 |
+
|
| 265 |
+
All weights are configurable per reward config.
|
| 266 |
+
|
| 267 |
+
---
|
| 268 |
+
|
| 269 |
+
## 7. Deployment Architecture
|
| 270 |
+
|
| 271 |
+
### 7.1 Google Cloud Run
|
| 272 |
+
|
| 273 |
+
```
|
| 274 |
+
┌─────────────────────────────────────┐
|
| 275 |
+
│ Google Cloud Run │
|
| 276 |
+
│ │
|
| 277 |
+
│ ┌────────────────────────────────┐ │
|
| 278 |
+
│ │ Container: Agentic RAG OS │ │
|
| 279 |
+
│ │ │ │
|
| 280 |
+
│ │ FastAPI App (:8080) │ │
|
| 281 |
+
│ │ ├── Auth Service │ │
|
| 282 |
+
│ │ ├── RAG Service │ │
|
| 283 |
+
│ │ ├── Reward Service │ │
|
| 284 |
+
│ │ └── Static Frontend │ │
|
| 285 |
+
│ │ │ │
|
| 286 |
+
│ │ SQLite + FAISS (ephemeral) │ │
|
| 287 |
+
│ └────────────────────────────────┘ │
|
| 288 |
+
│ │
|
| 289 |
+
│ Auto-scaling: 0-10 instances │
|
| 290 |
+
│ Memory: 2Gi │
|
| 291 |
+
│ CPU: 2 │
|
| 292 |
+
└─────────────────────────────────────┘
|
| 293 |
+
```
|
| 294 |
+
|
| 295 |
+
### 7.2 Infrastructure
|
| 296 |
+
|
| 297 |
+
| Component | Service | Details |
|
| 298 |
+
|-----------|---------|---------|
|
| 299 |
+
| Compute | Cloud Run | Serverless containers, auto-scaling |
|
| 300 |
+
| Container Registry | Artifact Registry | Docker image storage |
|
| 301 |
+
| Networking | Cloud Run default | HTTPS with managed TLS |
|
| 302 |
+
| Storage | Ephemeral (container FS) | SQLite + FAISS per instance |
|
| 303 |
+
| Monitoring | Cloud Logging | Automatic log collection |
|
| 304 |
+
|
| 305 |
+
---
|
| 306 |
+
|
| 307 |
+
## 8. Security Architecture
|
| 308 |
+
|
| 309 |
+
### 8.1 OWASP Compliance
|
| 310 |
+
|
| 311 |
+
| Risk | Mitigation |
|
| 312 |
+
|------|------------|
|
| 313 |
+
| Injection | Parameterized SQL queries (aiosqlite), input sanitization |
|
| 314 |
+
| Auth Failure | PBKDF2 password hashing, JWT with expiry, API key rotation |
|
| 315 |
+
| Data Exposure | Hashed passwords, hashed API keys, prefix-only display |
|
| 316 |
+
| SSRF | No user-controlled URL fetching (except GitHub OAuth) |
|
| 317 |
+
| Security Logging | All auth events logged |
|
| 318 |
+
|
| 319 |
+
### 8.2 Data Isolation
|
| 320 |
+
|
| 321 |
+
- Each user has isolated domains
|
| 322 |
+
- Each domain has isolated FAISS index
|
| 323 |
+
- SQL queries filter by `user_id` at every layer
|
| 324 |
+
- File uploads restricted to text content only
|
| 325 |
+
|
| 326 |
+
---
|
| 327 |
+
|
| 328 |
+
## 9. Frontend Architecture
|
| 329 |
+
|
| 330 |
+
### 9.1 SPA Design
|
| 331 |
+
|
| 332 |
+
- **Framework**: Vanilla JavaScript (zero dependencies)
|
| 333 |
+
- **Theme**: Dark mode with purple/cyan accent gradient
|
| 334 |
+
- **Routing**: Hash-based client-side navigation
|
| 335 |
+
- **State**: In-memory with localStorage for JWT persistence
|
| 336 |
+
- **Animations**: CSS keyframes + IntersectionObserver
|
| 337 |
+
|
| 338 |
+
### 9.2 Views
|
| 339 |
+
|
| 340 |
+
| View | Path | Auth Required |
|
| 341 |
+
|------|------|---------------|
|
| 342 |
+
| Landing Page | `/` | No |
|
| 343 |
+
| Dashboard | `/dashboard` | Yes |
|
| 344 |
+
| Domains | `/domains` | Yes |
|
| 345 |
+
| Rewards | `/rewards` | Yes |
|
| 346 |
+
| API Keys | `/apikeys` | Yes |
|
| 347 |
+
|
| 348 |
+
---
|
| 349 |
+
|
| 350 |
+
## 10. Integration with RAG Master
|
| 351 |
+
|
| 352 |
+
Agentic RAG OS reuses core components from the `rag_master` library:
|
| 353 |
+
|
| 354 |
+
| Component | Usage |
|
| 355 |
+
|-----------|-------|
|
| 356 |
+
| `FAISSRetriever` | Document indexing and retrieval |
|
| 357 |
+
| `Document` model | Document data structure |
|
| 358 |
+
| `CompositeRewardFunction` | Reference reward implementation |
|
| 359 |
+
| `SentenceTransformer` | Embedding generation |
|
| 360 |
+
|
| 361 |
+
The RAG OS operates independently of the Gym server — it does **not** require the gym to be running.
|
| 362 |
+
|
| 363 |
+
---
|
| 364 |
+
|
| 365 |
+
## 11. Technology Stack
|
| 366 |
+
|
| 367 |
+
| Layer | Technology |
|
| 368 |
+
|-------|-----------|
|
| 369 |
+
| Language | Python 3.10+ |
|
| 370 |
+
| API Framework | FastAPI |
|
| 371 |
+
| Database | SQLite (aiosqlite) |
|
| 372 |
+
| Vector Store | FAISS (faiss-cpu) |
|
| 373 |
+
| Embeddings | sentence-transformers/all-MiniLM-L6-v2 |
|
| 374 |
+
| Auth | PyJWT + PBKDF2 |
|
| 375 |
+
| Frontend | HTML5 + CSS3 + Vanilla JS |
|
| 376 |
+
| Container | Docker |
|
| 377 |
+
| Cloud | Google Cloud Run |
|
| 378 |
+
| CI/CD | GitHub → Cloud Build → Cloud Run |
|
pyproject.toml
CHANGED
|
@@ -29,6 +29,11 @@ dependencies = [
|
|
| 29 |
]
|
| 30 |
|
| 31 |
[project.optional-dependencies]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
dev = [
|
| 33 |
"pytest>=8.3.0",
|
| 34 |
"pytest-cov>=5.0.0",
|
|
@@ -56,7 +61,7 @@ select = ["E", "F", "W", "I", "N", "UP", "S", "B"]
|
|
| 56 |
ignore = ["S101", "S104"]
|
| 57 |
|
| 58 |
[tool.hatch.build.targets.wheel]
|
| 59 |
-
packages = ["rag_master", "server", "domains"]
|
| 60 |
|
| 61 |
[tool.mypy]
|
| 62 |
python_version = "3.10"
|
|
|
|
| 29 |
]
|
| 30 |
|
| 31 |
[project.optional-dependencies]
|
| 32 |
+
ragos = [
|
| 33 |
+
"aiosqlite>=0.20.0",
|
| 34 |
+
"PyJWT>=2.8.0",
|
| 35 |
+
"python-multipart>=0.0.9",
|
| 36 |
+
]
|
| 37 |
dev = [
|
| 38 |
"pytest>=8.3.0",
|
| 39 |
"pytest-cov>=5.0.0",
|
|
|
|
| 61 |
ignore = ["S101", "S104"]
|
| 62 |
|
| 63 |
[tool.hatch.build.targets.wheel]
|
| 64 |
+
packages = ["rag_master", "server", "domains", "agentic_rag_os"]
|
| 65 |
|
| 66 |
[tool.mypy]
|
| 67 |
python_version = "3.10"
|