williyam commited on
Commit
62d11c6
·
1 Parent(s): 0881e41

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 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** | **A new orchestrator/framework designed from scratch** — RAG Master represents a new revolution for agentic RAG. This RL gym helps the system make better decisions by improving the entire agentic RAG foundation with reinforcement learning. Implemented as a domain-agnostic framework extensible to any domain, with 2 production domains (Aerospace Research, Legal Research) and deterministic grading |
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 &nbsp;<span class="str">← upload</span></div>
1041
+ <div><span class="cmd">POST</span> /api/v1/rag/domains/my-app/query &nbsp;<span class="str">← retrieve</span></div>
1042
+ <div><span class="cmd">POST</span> /api/v1/rewards/compute &nbsp;<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>&nbsp;&nbsp;<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>&nbsp;&nbsp;-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>&nbsp;&nbsp;-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...&#10;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"