⚜ AGENTIC RAG GYM ⚜
" "RL-Enhanced Agentic RAG Framework — Extensible to Any Domain
" """" Gradio UI for Agentic RAG Gym - Royal Glassmorphism Theme. Provides an interactive interface for: - Running episodes against the environment - Viewing task descriptions and difficulty - Real-time step-by-step feedback with rewards - Trajectory visualization - Benchmark results """ from __future__ import annotations import json import os from pathlib import Path from typing import Any, Dict, List, Optional import gradio as gr import httpx from rag_master.rewards import _SCORE_MIN, clamp_score _PROJECT_ROOT = Path(os.environ.get("APP_ROOT", Path(__file__).resolve().parent.parent)) _MEDIA_GH = "https://media.githubusercontent.com/media/williyam-m/agentic-rag-gym/main" def _resolve_images(text: str) -> str: """Replace relative image paths with absolute GitHub media URLs for Gradio. Uses media.githubusercontent.com instead of raw.githubusercontent.com because the repo uses Git LFS for images — raw URLs return LFS pointers, while media URLs serve the actual binary content. """ import re def _repl(m: re.Match) -> str: alt, path = m.group(1), m.group(2) if path.startswith(("http://", "https://")): return m.group(0) return f"" return re.sub(r"!\[([^\]]*)\]\(([^)]+)\)", _repl, text) def _load_markdown(filename: str) -> str: """Load a markdown file from the project root, stripping YAML frontmatter.""" path = _PROJECT_ROOT / filename if not path.exists(): return f"*{filename} not found.*" text = path.read_text(encoding="utf-8") # Strip YAML frontmatter if present if text.startswith("---"): end = text.find("---", 3) if end != -1: text = text[end + 3:].lstrip("\n") return _resolve_images(text) def _load_readme() -> str: """Load README.md, stripping frontmatter and badge lines.""" text = _load_markdown("README.md") # Remove badge image lines ([]) at the top lines = text.split("\n") cleaned: list[str] = [] for line in lines: if line.strip().startswith("[![") and line.strip().endswith(")"): continue cleaned.append(line) return "\n".join(cleaned) ROYAL_CSS = """ @import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Inter:wght@300;400;500;600&display=swap'); :root { --gold: #D4AF37; --gold-light: #F0D060; --gold-dark: #A08520; --royal-black: #0A0A0F; --royal-dark: #12121A; --royal-surface: #1A1A2E; --royal-card: rgba(26, 26, 46, 0.7); --text-primary: #FFFFFF; --text-secondary: #B0B0C0; --glass-border: rgba(212, 175, 55, 0.3); --glass-bg: rgba(26, 26, 46, 0.6); --success: #4CAF50; --warning: #FF9800; --error: #F44336; } .gradio-container { background: linear-gradient(135deg, var(--royal-black) 0%, var(--royal-dark) 50%, #16213E 100%) !important; font-family: 'Inter', sans-serif !important; min-height: 100vh; } .main-header { text-align: center; padding: 2rem 1rem; background: linear-gradient(180deg, rgba(212, 175, 55, 0.1) 0%, transparent 100%); border-bottom: 1px solid var(--glass-border); margin-bottom: 1.5rem; } .main-header h1 { font-family: 'Cinzel', serif !important; font-size: 2.4rem !important; font-weight: 700 !important; background: linear-gradient(135deg, var(--gold-light), var(--gold), var(--gold-dark)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 0.3rem; letter-spacing: 2px; } .main-header p { color: var(--text-secondary); font-size: 1rem; font-weight: 300; } .glass-card { background: var(--glass-bg) !important; backdrop-filter: blur(20px) !important; -webkit-backdrop-filter: blur(20px) !important; border: 1px solid var(--glass-border) !important; border-radius: 16px !important; padding: 1.5rem !important; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3) !important; } .gold-button { background: linear-gradient(135deg, var(--gold-dark), var(--gold), var(--gold-light)) !important; color: var(--royal-black) !important; font-weight: 600 !important; border: none !important; border-radius: 12px !important; padding: 12px 28px !important; font-size: 1rem !important; letter-spacing: 1px !important; transition: all 0.3s ease !important; box-shadow: 0 4px 15px rgba(212, 175, 55, 0.3) !important; } .gold-button:hover { box-shadow: 0 6px 25px rgba(212, 175, 55, 0.5) !important; transform: translateY(-2px) !important; } .status-badge { display: inline-block; padding: 4px 12px; border-radius: 20px; font-size: 0.85rem; font-weight: 500; } .badge-easy { background: rgba(76, 175, 80, 0.2); color: #4CAF50; border: 1px solid rgba(76, 175, 80, 0.4); } .badge-medium { background: rgba(255, 152, 0, 0.2); color: #FF9800; border: 1px solid rgba(255, 152, 0, 0.4); } .badge-hard { background: rgba(244, 67, 54, 0.2); color: #F44336; border: 1px solid rgba(244, 67, 54, 0.4); } .reward-display { font-family: 'Cinzel', serif; font-size: 2rem; text-align: center; padding: 1rem; } .step-log { font-family: 'JetBrains Mono', monospace; font-size: 0.85rem; line-height: 1.6; padding: 1rem; background: rgba(0, 0, 0, 0.3); border-radius: 8px; border: 1px solid rgba(212, 175, 55, 0.15); max-height: 400px; overflow-y: auto; } .metric-card { text-align: center; padding: 1rem; background: rgba(212, 175, 55, 0.05); border: 1px solid rgba(212, 175, 55, 0.2); border-radius: 12px; } .metric-card .value { font-family: 'Cinzel', serif; font-size: 1.8rem; color: var(--gold); } .metric-card .label { color: var(--text-secondary); font-size: 0.85rem; margin-top: 0.3rem; } /* Override Gradio defaults */ .dark .gr-box, .dark .gr-input, .dark .gr-panel { background: var(--glass-bg) !important; border-color: var(--glass-border) !important; } .dark label, .dark .gr-check-radio label { color: var(--text-secondary) !important; } textarea, input[type="text"], select { background: rgba(0, 0, 0, 0.3) !important; border: 1px solid var(--glass-border) !important; color: var(--text-primary) !important; border-radius: 8px !important; } .gr-button.primary { background: linear-gradient(135deg, var(--gold-dark), var(--gold)) !important; color: var(--royal-black) !important; border: none !important; } """ API_URL = "http://localhost:7860" # --- Task choices per domain --- DOMAIN_TASKS = { "aerospace": [ ("Compare Propulsion Technologies (Easy)", "aero_easy_propulsion_comparison"), ("Space Debris Mitigation (Easy)", "aero_easy_debris_mitigation"), ("Mars EDL Architecture (Medium)", "aero_medium_mars_edl"), ("Deep Space Life Support (Medium)", "aero_medium_life_support"), ("Hypersonic Vehicle Design (Hard)", "aero_hard_hypersonic_vehicle"), ], "legal_research": [ ("Contract Clause Analysis (Easy)", "legal_easy_contract_review"), ("Data Privacy Compliance (Easy)", "legal_easy_privacy_compliance"), ("IP Assessment (Medium)", "legal_medium_ip_analysis"), ("M&A Due Diligence (Medium)", "legal_medium_ma_due_diligence"), ("Cross-Border Dispute (Hard)", "legal_hard_cross_border_dispute"), ], } DOMAIN_LABELS = { "aerospace": "Aerospace Research", "legal_research": "Legal Research", } def _call_api(method: str, endpoint: str, data: Optional[Dict] = None) -> Dict[str, Any]: """Make an API call to the backend.""" url = f"{API_URL}{endpoint}" try: with httpx.Client(timeout=60.0) as client: if method == "GET": resp = client.get(url) else: resp = client.post(url, json=data or {}) if resp.status_code >= 400: try: body = resp.json() detail = body.get("detail", resp.text) except Exception: detail = resp.text return {"error": detail} return resp.json() except Exception as exc: return {"error": str(exc)} def switch_domain(domain: str): """Switch the active domain and return updated task choices for both dropdowns.""" result = _call_api("POST", "/domain/switch", {"domain": domain}) if "error" in result: fallback = DOMAIN_TASKS.get("aerospace", []) return ( gr.update(choices=fallback, value=None), gr.update(choices=fallback, value=None), f"Error: {result['error']}", ) tasks = DOMAIN_TASKS.get(domain, []) default_val = tasks[0][1] if tasks else None label = DOMAIN_LABELS.get(domain, domain) return ( gr.update(choices=tasks, value=default_val), gr.update(choices=tasks, value=default_val), f"Switched to {label} domain", ) def reset_environment(task_id: str) -> tuple: """Reset the environment with the selected task.""" data = {"task_id": task_id} if task_id else {} result = _call_api("POST", "/reset", data) if "error" in result: return ( f"**Error:** {result['error']}", "N/A", "", "0", "0.0", "Not started", ) obs = result.get("observation", {}) task = obs.get("task", {}) task_info = ( f"### {task.get('name', 'Unknown')}\n\n" f"**Difficulty:** {task.get('difficulty', 'N/A')}\n\n" f"**Max Steps:** {task.get('max_steps', 'N/A')}\n\n" f"**Description:**\n{task.get('description', 'N/A')}" ) difficulty = task.get("difficulty", "unknown") badge = f'{difficulty.upper()}' return ( task_info, badge, "", "0", "0.0", "Episode started - take your first action!", ) def take_step(action_type: str, query: str, answer: str) -> tuple: """Take a step in the environment.""" action_data: Dict[str, Any] = {"type": action_type} if query: action_data["query"] = query if answer: action_data["answer"] = answer result = _call_api("POST", "/step", action_data) # Auto-reset if environment was not initialized if "error" in result and "not initialized" in str(result["error"]).lower(): _call_api("POST", "/reset") result = _call_api("POST", "/step", action_data) if "error" in result: return ("", "0", "0.0", f"**Error:** {result['error']}", "") obs = result.get("observation", {}) reward = result.get("reward", 0.0) done = result.get("done", False) info = result.get("info", {}) # Build step log step_num = info.get("step", 0) step_log = ( f"**Step {step_num}** | Action: `{action_type}` | " f"Reward: `{reward:.4f}` | Done: `{done}`" ) # Retrieved docs display docs_display = "" if obs.get("retrieved_docs"): docs_display = "\n\n".join( f"**[{d['score']:.2f}]** {d['content'][:200]}..." for d in obs["retrieved_docs"] ) current_answer = obs.get("current_answer", "") status = "Episode Complete!" if done else f"Step {step_num} completed" return ( docs_display, str(step_num), f"{reward:.4f}", status, current_answer, ) def grade_episode() -> str: """Grade the current episode.""" result = _call_api("POST", "/grade") if "error" in result: return f"**Error:** {result['error']}" score = clamp_score(float(result.get("score", _SCORE_MIN))) task_id = result.get("task_id", "Unknown") return ( f'
RL-Enhanced Agentic RAG Framework — Extensible to Any Domain
" "Runs a complete episode automatically: Plan → Retrieve → Reason → Critique → Retrieve → Reason → Answer
Tasks for the currently active domain — switch domains above to see different tasks