diff --git a/.build-trigger b/.build-trigger deleted file mode 100644 index 3f88f81e5ac1ca2a47eb7f6658e68bfa8ccc731d..0000000000000000000000000000000000000000 --- a/.build-trigger +++ /dev/null @@ -1 +0,0 @@ -Final Release Sync (Definitive UI): 2026-04-07 23:38:07 diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 02856a35c753b102c068087f7befe9cb1a7bff91..0000000000000000000000000000000000000000 --- a/.dockerignore +++ /dev/null @@ -1,10 +0,0 @@ -.git/ -.env -backend/venv/ -backend/__pycache__/ -frontend/node_modules/ -frontend/dist/ -.pytest_cache/ -.coverage -brain/ -.gemini/ diff --git a/.gitignore b/.gitignore index 20e760800f6bebb165d015b36b3e569a681f2bc8..89f0e37c54096c75b8a2da7d3dd30c0535185c26 100644 --- a/.gitignore +++ b/.gitignore @@ -1,37 +1,14 @@ -# Python -__pycache__/ -*.py[cod] -*$py.class -*.so -.Python -backend/venv/ -.pytest_cache/ -.coverage -.cache -backend/scenarios/.cache - -# Node -node_modules/ -.npm/ - -# Env & Secrets -.env -.env.* -!.env.example -# default.env is needed for HF Spaces - -# OS -.DS_Store -Thumbs.db - -# VS Code / IDE -.vscode/ -.idea/ -*.swp -*.swo - -# Project specific -backend/logs/ -*.log -.gemini/ -brain/ +# VS Code / IDE +.vscode/ +.idea/ +.DS_Store + +# Brain / Artifacts (Optional: keep if you want them on GitHub) +# /home/habibi/.gemini/antigravity/brain/ + +# Local env +.env +*.local +node_modules/ +venv/ +__pycache__/ diff --git a/Dockerfile b/Dockerfile index 092c1474245070b720719c939a1a99c821459da8..984af2a80d2ff0d5ca3b582540eef5f30dea2644 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,58 +1,39 @@ -# Stage 1: Build frontend -FROM node:20-alpine AS frontend-builder -WORKDIR /app/frontend - -# Copy package files first for better caching -COPY frontend/package*.json ./ - -# Install dependencies -RUN npm install --legacy-peer-deps - -# Copy frontend source -COPY frontend/ ./ - -# Build frontend -RUN npm run build - -# Stage 2: Python backend -FROM python:3.11-slim -RUN useradd -m -u 1000 user -ENV HOME=/home/user \ - PATH=/home/user/.local/bin:$PATH -WORKDIR $HOME/app - -RUN apt-get update && apt-get install -y \ - curl \ - build-essential \ - python3-dev \ - libffi-dev \ - libssl-dev \ - && rm -rf /var/lib/apt/lists/* - -COPY --chown=user:user backend/requirements.txt ./requirements.txt -RUN pip install --no-cache-dir -r requirements.txt - -# Copy backend -COPY --chown=user:user backend ./backend - -# Copy root-level files -COPY --chown=user:user default.env ./.env -COPY --chown=user:user openenv.yaml . -COPY --chown=user:user inference.py . -COPY --chown=user:user pyproject.toml . -COPY --chown=user:user server ./server - -# Copy built frontend from stage 1 -COPY --chown=user:user --from=frontend-builder /app/frontend/dist ./frontend/dist - -USER user - -EXPOSE 7860 - -ENV HOST=0.0.0.0 -ENV PORT=7860 -ENV ENVIRONMENT=production -ENV PYTHONPATH=$HOME/app:$HOME/app/backend -ENV PYTHONUNBUFFERED=1 - -CMD ["python3", "server/app.py"] +FROM node:20 AS frontend-builder +WORKDIR /app/frontend +COPY frontend/package*.json ./ +RUN npm install +COPY frontend . +RUN npm run build + +FROM python:3.11-slim +RUN useradd -m -u 1000 user +ENV HOME=/home/user \ + PATH=/home/user/.local/bin:$PATH +WORKDIR $HOME/app + +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* + +COPY --chown=user:user backend/requirements.txt ./requirements.txt +RUN pip install --no-cache-dir -r requirements.txt + +# Copy FastAPI backend +COPY --chown=user:user backend ./backend +# Copy full repo bounds if necessary for local paths +COPY --chown=user:user default.env . +COPY --chown=user:user openenv.yaml . +COPY --chown=user:user inference.py . + +# Copy pre-built React frontend +COPY --chown=user:user --from=frontend-builder /app/frontend/dist ./frontend/dist + +USER user + +EXPOSE 7860 +EXPOSE 8001 + +ENV HOST=0.0.0.0 +ENV PORT=7860 +ENV ENVIRONMENT=production +ENV PYTHONPATH=$HOME/app + +CMD ["python", "backend/main.py"] diff --git a/README.md b/README.md index 25ea907ba27024ab9c735e4fbcf58e1f18ca10cb..0f023bfb390ea2ab55bc329a79323e2f7014d85b 100644 --- a/README.md +++ b/README.md @@ -1,292 +1,239 @@ ---- -title: NEXON-AI -emoji: ๐Ÿ›ก๏ธ -colorFrom: blue -colorTo: indigo -sdk: docker -app_port: 7860 -pinned: false ---- - - - -# NEXUS-AI ๐ŸŒ๐Ÿ›ก๏ธ -### Autonomous Incident Investigation Dashboard - -
- -![Python](https://img.shields.io/badge/Python-3.10+-3776AB?style=for-the-badge&logo=python&logoColor=white) -![FastAPI](https://img.shields.io/badge/FastAPI-0.100+-009688?style=for-the-badge&logo=fastapi&logoColor=white) -![React](https://img.shields.io/badge/React-18.x-61DAFB?style=for-the-badge&logo=react&logoColor=black) -![Tailwind](https://img.shields.io/badge/Tailwind_CSS-3.x-38B2AC?style=for-the-badge&logo=tailwind-css&logoColor=white) -![Ollama](https://img.shields.io/badge/Ollama-Local_LLM-000000?style=for-the-badge&logo=ollama) - -**Status:** Active Simulation Pipeline -**Architecture:** Real-time WebSockets + Multi-Agent Consensus - -
- ---- - -## ๐Ÿ“– What is NEXUS-AI? - -NEXUS is a next-generation, autonomous dual-agent environment designed to investigate and validate software incidents in real-time. Using a combination of an **Investigator** and a **Validator** agent, NEXUS autonomously forms hypotheses, executes systems tools, evaluates system behavior, and reaches strict consensus on root causes. - -Traditional manual debugging requires extensive context-switching and tool fatigue. NEXUS solves this through: -1. **Dual-Agent Autonomy**: Two specialized models communicating word-by-word via WebSockets. -2. **Dynamic Tool Execution**: Fully integrated system terminals allowing agents to run sandboxed validation scripts. -3. **Semantic Reward Engine**: Evaluates conversational drift mathematically (using native GPU embeddings). - -The result: An AI "Incident Response Team" that navigates servers, traces logs, and fixes bugs identically to a human SRE. - ---- - -## ๐Ÿ–ผ๏ธ Application Screenshots - -### ๐Ÿ“Š Simulation Dashboard - -> The core command center. Features live agent terminals, a dual-communication consensus log, and a mathematical performance reward graph plotting investigation confidence. - -
- Simulation Dashboard -
- ---- - -## ๐ŸŽ›๏ธ Scenario Registry & Core Settings - -> The system is architected for instant adaptability โ€” seamlessly switch LLM providers and inject custom threat models entirely through the frontend DOM. - - - - - - -
- Scenario Browser -
Scenario Registry -
A persistent LocalStorage-backed grid of tactical simulations. Users can dynamically inject custom infrastructure-specific incidents directly into the agent pipeline. -
- Hardware Configuration -
Runtime Configuration -
Dynamically maps available locally-installed Ollama networks, allowing the user to pair models (e.g., Qwen vs Dolphin-Phi) with fully independent parameters. -
- ---- - -## ๐Ÿ—๏ธ System Architecture - -```text -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ CLIENT BROWSER โ”‚ -โ”‚ React SPA (Tailwind + Framer Motion) โ”‚ -โ”‚ localhost:5173 โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ HTTP (REST) โ”‚ ws:// - โ–ผ โ–ผ -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ FASTAPI BACKEND (localhost:7860) โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ /config โ”‚ โ”‚/scenariosโ”‚ โ”‚ /reset โ”‚ โ”‚ ws:// Simulator โ”‚ โ”‚ -โ”‚ โ”‚ Env Sync โ”‚ โ”‚ DB Cache โ”‚ โ”‚ Injectionโ”‚ โ”‚ Live Stream Syncโ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ โ”‚ - โ–ผ โ–ผ -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ OLLAMA ENGINE / LLM PIPELINE โ”‚ -โ”‚ Agent A (Investigator) โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ–บ Agent B (Validator) โ”‚ -โ”‚ - Generates Hypotheses - Challenges Assertions โ”‚ -โ”‚ - Runs System Tools - Requires Proof โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - ---- - -## ๐ŸŒ Execution Environments - -NEXUS-AI supports two distinct execution models for agent tools, toggleable via the **Settings** dashboard: - -### 1. Simulated Mode (Safe Sandbox) -* **Default Mode**: Agents interact with a pre-defined `clue_map` within the scenario YAML. -* **No System Impact**: Commands like `read_logs` or `check_service` return mocked data. -* **Use Case**: Training, logic validation, and "what-if" analysis without infrastructure risk. - -### 2. SSH Lab Node (Real-World Execution) -* **Live Connection**: Commands are executed in real-time on a remote Linux server via SSH. -* **Autonomous Terminal**: Agents use the `run_terminal_command` tool to browse logs, check systemd status, and inspect real configs. -* **Security**: Includes a command blocklist to prevent highly destructive operations (e.g., `rm -rf /`). -* **Use Case**: Actual incident response on isolated Lab/Staging nodes. - ---- - -## ๐Ÿ“ OpenEnv Specification - -NEXUS-AI strictly adheres to the **OpenEnv 1.0** standard for agent-environment interaction. - -### ๐ŸŽฎ Action Space -The environment accepts a typed **NexusAction** (Text-based with structured tool calls). -- **agent_id**: `string` ("agent_a" or "agent_b") -- **message**: `string` (The natural language reasoning/communication) -- **tool_calls**: `List[ToolCall]` (Optional structured calls like `TOOL: read_logs(file='app.log')`) -- **confidence**: `float` (0.0 - 1.0) - -### ๐Ÿง Observation Space -The environment returns a structured **NexusObservation** summarizing the system state. -- **scenario_description**: `string` (High-level objective) -- **scenario_context**: `string` (Background telemetry/environment info) -- **partner_message**: `string` (The last message from the other agent) -- **tool_results**: `List[ToolResult]` (Output of any executed system tools) -- **clues_found**: `List[string]` (Accumulated evidence identified by the Reward Engine) -- **investigation_stage**: `string` (`investigating`, `narrowing`, `found`, `verified`) -- **round**: `integer` (Current episode round) -- **available_tools**: `List[string]` (List of permitted tools for the current mode) - -### ๐Ÿ“ Task Registry & Difficulty -| Task Name | Difficulty | Objective | Grader Method | -|---|---|---|---| -| `software-incident` | **Easy** | Fix Nginx 503 rate-limit misconfiguration | State Check: `nginx-proxy.rate_limit` | -| `business-process-failure` | **Medium** | Resolve inventory stockout logic error | State Check: `stock_threshold` + Red Herring Penalty | -| `cascade-system-failure` | **Hard** | Fix Postgres connection exhaustion | Multi-Step: Query Termination + Config Update | - -### ๐Ÿ“ˆ Baseline Benchmarks -Validated using `inference.py` (Phi-3-mini & Qwen2.5-1.5B). -- **Software Incident**: 0.88 / 1.00 -- **Business Process Failure**: 0.72 / 1.00 -- **Cascade System Failure**: 0.48 / 1.00 - ---- - -## ๐Ÿง  The AI Pipeline Deep-Dive - -### Step 1: Scenario Injection & Bootstrapping -```python -# The EpisodeManager receives the frontend custom scenario JSON -# Broadcasts 'episode_start' natively over the WebSocket to synchronize the UI -await broadcast("episode_start", { - "scenario": active_scenario, - "agent_a_model": settings.AGENT_A_MODEL -}) -``` - -### Step 2: Agent Consensus Loop -```python -# Agents interact sequentially. The Investigator attempts a solution -# while the Validator challenges it. Both agents have access to dynamic system execution. -client, model_name = model_manager.get_client(agent_id) -stream = await client.chat.completions.create( - model=model_name, - messages=injected_history, - tools=available_tools, # e.g. fix_proposer, run_terminal_command - stream=True -) -``` - -### Step 3: Fast GPU Embeddings (Similarity Evaluation) -```python -# Heavy CPU blocking is completely bypassed. -# Semantic embedding computations map strictly into the Ollama GPU pipeline. -@lru_cache(maxsize=256) -def get_embedding(text: str) -> List[float]: - response = httpx.post("http://localhost:11434/api/embeddings", json={ - "model": "all-minilm", - "prompt": text - }, timeout=60.0) - return response.json().get("embedding", []) -``` - ---- - -## ๐Ÿ› ๏ธ Full Technology Stack - -| Layer | Technology | Why | -|---|---|---| -| Frontend Framework | React 18 (Vite) | Lightning fast HMR, component isolation | -| Frontend Styling | Tailwind CSS | Utility-first tactical glassmorphism | -| Backend Framework | FastAPI | Async Python, explicit endpoint mapping | -| Transport Layer | WebSockets | Word-by-word streaming across UI boundaries | -| Local AI Engine | Ollama | Native device acceleration, absolute privacy | -| Remote Provider | HuggingFace Inference API | Drop-in SaaS alternatives | -| SSH Connectivity | Paramiko | Secure remote shell execution for Lab Nodes | -| Data Persistence | LocalStorage & `.env` Injection | Avoids over-architected SQL constraints | - ---- - -## ๐Ÿš€ How to Run This Project (Full Step-by-Step Guide) - -### ๐Ÿ“‹ Prerequisites -- Python 3.10+ -- Node.js 18+ -- [Ollama](https://ollama.com/) (installed locally for model hosting) -- **Optional**: A remote Linux VM (Ubuntu/Kali) with SSH enabled for Lab Node mode - ---- - -### 1๏ธโƒฃ Backend Setup (FastAPI / Python) - -```bash -cd backend - -# Create and activate virtual environment -python -m venv venv -# source venv/bin/activate # Linux/macOS -venv\Scripts\activate # Windows - -# Install all dependencies -pip install -r requirements.txt -``` - -#### Start the Backend Engine -```bash -# This exposes the core REST API and the WebSocket simulation tunnel -python main.py -``` - ---- - -### 2๏ธโƒฃ Frontend Setup (React) - -Open a **new terminal tab**: - -```bash -cd frontend - -# Install Node.js dependencies -npm install - -# Start the Vite development server -npm run dev -``` - -The application is now fully accessible at [http://localhost:5173](http://localhost:5173). - ---- - -### 3๏ธโƒฃ Pulling Models - -To run the simulation locally without cloud API keys, you must ensure you pull suitable reasoning models through Ollama: - -```bash -ollama run qwen2.5:3b # Excellent validator logic footprint -ollama run dolphin-llama3 # Uncensored investigative assertions -ollama pull all-minilm # Mandatory for semantic similarity scoring -``` - ---- - -## ๐Ÿงช Automated Testing -NEXUS-AI includes a comprehensive test suite to ensure environment stability and specification compliance. - -```bash -# Run the OpenEnv specification validator -python openenv_validator.py - -# Run unit tests for core logic -pip install pytest -pytest tests/ -``` - ---- - -## ๐Ÿค Authors -**Developed by: Ashish Menon** & Vector +--- +title: NEXON-AI +emoji: ๐Ÿ›ก๏ธ +colorFrom: blue +colorTo: indigo +sdk: docker +app_port: 7860 +pinned: false +--- + +# NEXUS-AI ๐ŸŒ๐Ÿ›ก๏ธ +### Autonomous Incident Investigation Dashboard + +
+ +![Python](https://img.shields.io/badge/Python-3.10+-3776AB?style=for-the-badge&logo=python&logoColor=white) +![FastAPI](https://img.shields.io/badge/FastAPI-0.100+-009688?style=for-the-badge&logo=fastapi&logoColor=white) +![React](https://img.shields.io/badge/React-18.x-61DAFB?style=for-the-badge&logo=react&logoColor=black) +![Tailwind](https://img.shields.io/badge/Tailwind_CSS-3.x-38B2AC?style=for-the-badge&logo=tailwind-css&logoColor=white) +![Ollama](https://img.shields.io/badge/Ollama-Local_LLM-000000?style=for-the-badge&logo=ollama) + +**Status:** Active Simulation Pipeline +**Architecture:** Real-time WebSockets + Multi-Agent Consensus + +
+ +--- + +## ๐Ÿ“– What is NEXUS-AI? + +NEXUS is a next-generation, autonomous dual-agent environment designed to investigate and validate software incidents in real-time. Using a combination of an **Investigator** and a **Validator** agent, NEXUS autonomously forms hypotheses, executes systems tools, evaluates system behavior, and reaches strict consensus on root causes. + +Traditional manual debugging requires extensive context-switching and tool fatigue. NEXUS solves this through: +1. **Dual-Agent Autonomy**: Two specialized models communicating word-by-word via WebSockets. +2. **Dynamic Tool Execution**: Fully integrated system terminals allowing agents to run sandboxed validation scripts. +3. **Semantic Reward Engine**: Evaluates conversational drift mathematically (using native GPU embeddings). + +The result: An AI "Incident Response Team" that navigates servers, traces logs, and fixes bugs identically to a human SRE. + +--- + +## ๐Ÿ–ผ๏ธ Application Screenshots + +### ๐Ÿ“Š Simulation Dashboard + +> The core command center. Features live agent terminals, a dual-communication consensus log, and a mathematical performance reward graph plotting investigation confidence. + +
+ Simulation Dashboard +
+ +--- + +## ๐ŸŽ›๏ธ Scenario Registry & Core Settings + +> The system is architected for instant adaptability โ€” seamlessly switch LLM providers and inject custom threat models entirely through the frontend DOM. + + + + + + +
+ Scenario Browser +
Scenario Registry +
A persistent LocalStorage-backed grid of tactical simulations. Users can dynamically inject custom infrastructure-specific incidents directly into the agent pipeline. +
+ Hardware Configuration +
Runtime Configuration +
Dynamically maps available locally-installed Ollama networks, allowing the user to pair models (e.g., Qwen vs Dolphin-Phi) with fully independent parameters. +
+ +--- + +## ๐Ÿ—๏ธ System Architecture + +```text +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ CLIENT BROWSER โ”‚ +โ”‚ React SPA (Tailwind + Framer Motion) โ”‚ +โ”‚ localhost:5173 โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ HTTP (REST) โ”‚ ws:// + โ–ผ โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ FASTAPI BACKEND (localhost:7860) โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ /config โ”‚ โ”‚/scenariosโ”‚ โ”‚ /reset โ”‚ โ”‚ ws:// Simulator โ”‚ โ”‚ +โ”‚ โ”‚ Env Sync โ”‚ โ”‚ DB Cache โ”‚ โ”‚ Injectionโ”‚ โ”‚ Live Stream Syncโ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ + โ–ผ โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ OLLAMA ENGINE / LLM PIPELINE โ”‚ +โ”‚ Agent A (Investigator) โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ–บ Agent B (Validator) โ”‚ +โ”‚ - Generates Hypotheses - Challenges Assertions โ”‚ +โ”‚ - Runs System Tools - Requires Proof โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## ๐ŸŒ Execution Environments + +NEXUS-AI supports two distinct execution models for agent tools, toggleable via the **Settings** dashboard: + +### 1. Simulated Mode (Safe Sandbox) +* **Default Mode**: Agents interact with a pre-defined `clue_map` within the scenario YAML. +* **No System Impact**: Commands like `read_logs` or `check_service` return mocked data. +* **Use Case**: Training, logic validation, and "what-if" analysis without infrastructure risk. + +### 2. SSH Lab Node (Real-World Execution) +* **Live Connection**: Commands are executed in real-time on a remote Linux server via SSH. +* **Autonomous Terminal**: Agents use the `run_terminal_command` tool to browse logs, check systemd status, and inspect real configs. +* **Security**: Includes a command blocklist to prevent highly destructive operations (e.g., `rm -rf /`). +* **Use Case**: Actual incident response on isolated Lab/Staging nodes. + +--- + +## ๐Ÿง  The AI Pipeline Deep-Dive + +### Step 1: Scenario Injection & Bootstrapping +```python +# The EpisodeManager receives the frontend custom scenario JSON +# Broadcasts 'episode_start' natively over the WebSocket to synchronize the UI +await broadcast("episode_start", { + "scenario": active_scenario, + "agent_a_model": settings.AGENT_A_MODEL +}) +``` + +### Step 2: Agent Consensus Loop +```python +# Agents interact sequentially. The Investigator attempts a solution +# while the Validator challenges it. Both agents have access to dynamic system execution. +client, model_name = model_manager.get_client(agent_id) +stream = await client.chat.completions.create( + model=model_name, + messages=injected_history, + tools=available_tools, # e.g. fix_proposer, run_terminal_command + stream=True +) +``` + +### Step 3: Fast GPU Embeddings (Similarity Evaluation) +```python +# Heavy CPU blocking is completely bypassed. +# Semantic embedding computations map strictly into the Ollama GPU pipeline. +@lru_cache(maxsize=256) +def get_embedding(text: str) -> List[float]: + response = httpx.post("http://localhost:11434/api/embeddings", json={ + "model": "all-minilm", + "prompt": text + }, timeout=60.0) + return response.json().get("embedding", []) +``` + +--- + +## ๐Ÿ› ๏ธ Full Technology Stack + +| Layer | Technology | Why | +|---|---|---| +| Frontend Framework | React 18 (Vite) | Lightning fast HMR, component isolation | +| Frontend Styling | Tailwind CSS | Utility-first tactical glassmorphism | +| Backend Framework | FastAPI | Async Python, explicit endpoint mapping | +| Transport Layer | WebSockets | Word-by-word streaming across UI boundaries | +| Local AI Engine | Ollama | Native device acceleration, absolute privacy | +| Remote Provider | HuggingFace Inference API | Drop-in SaaS alternatives | +| SSH Connectivity | Paramiko | Secure remote shell execution for Lab Nodes | +| Data Persistence | LocalStorage & `.env` Injection | Avoids over-architected SQL constraints | + +--- + +## ๐Ÿš€ How to Run This Project (Full Step-by-Step Guide) + +### ๐Ÿ“‹ Prerequisites +- Python 3.10+ +- Node.js 18+ +- [Ollama](https://ollama.com/) (installed locally for model hosting) +- **Optional**: A remote Linux VM (Ubuntu/Kali) with SSH enabled for Lab Node mode + +--- + +### 1๏ธโƒฃ Backend Setup (FastAPI / Python) + +```bash +cd backend + +# Create and activate virtual environment +python -m venv venv +# source venv/bin/activate # Linux/macOS +venv\Scripts\activate # Windows + +# Install all dependencies +pip install -r requirements.txt +``` + +#### Start the Backend Engine +```bash +# This exposes the core REST API and the WebSocket simulation tunnel +python main.py +``` + +--- + +### 2๏ธโƒฃ Frontend Setup (React) + +Open a **new terminal tab**: + +```bash +cd frontend + +# Install Node.js dependencies +npm install + +# Start the Vite development server +npm run dev +``` + +The application is now fully accessible at [http://localhost:5173](http://localhost:5173). + +--- + +### 3๏ธโƒฃ Pulling Models + +To run the simulation locally without cloud API keys, you must ensure you pull suitable reasoning models through Ollama: + +```bash +ollama run qwen2.5:3b # Excellent validator logic footprint +ollama run dolphin-llama3 # Uncensored investigative assertions +ollama pull all-minilm # Mandatory for semantic similarity scoring +``` + +--- + +## ๐Ÿค Authors +**Developed by: Ashish Menon** & Vector diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..e78237a18613303c376f92479eab0a15018856a8 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,13 @@ +# Python artifacts +__pycache__/ +*.py[cod] +*$py.class +venv/ +.env + +# Logs +*.log + +# Scenarios data (if you want to keep it empty on GitHub) +scenarios/data/**/*.json +!scenarios/data/ diff --git a/backend/api/routes/config_routes.py b/backend/api/routes/config_routes.py index c008218a7e2c6e312f5657e11c57cb1860fb01c5..8dfbeeed22d822cae8a3e280e1c7b2c0de30797f 100644 --- a/backend/api/routes/config_routes.py +++ b/backend/api/routes/config_routes.py @@ -5,23 +5,23 @@ from utils.hardware import check_hardware router = APIRouter() -from typing import List, Dict, Any - -class AgentConfig(BaseModel): - id: str - model: str - provider: str - role: str = "INVESTIGATOR" - system_prompt: str = "" - temperature: float = 0.7 - class ConfigUpdate(BaseModel): MAX_STEPS: int - AGENTS: List[AgentConfig] + AGENT_A_MODEL: str + AGENT_B_MODEL: str + AGENT_A_PROVIDER: str + AGENT_B_PROVIDER: str + AGENT_A_ROLE: str = "INVESTIGATOR" + AGENT_B_ROLE: str = "VALIDATOR" + AGENT_A_SYSTEM_PROMPT: str = "" + AGENT_B_SYSTEM_PROMPT: str = "" + AGENT_A_TEMPERATURE: float + AGENT_B_TEMPERATURE: float EXECUTION_MODE: str = "simulated" SSH_HOST: str = "" SSH_PORT: int = 22 SSH_USER: str = "" + SSH_USER: str = "" SSH_PASSWORD: str = "" OPENAI_API_KEY: str = "" @@ -30,8 +30,17 @@ def get_config(): hw = check_hardware() return { "models": { - "agents": settings.AGENTS, - "openai_api_key": getattr(settings, "OPENAI_API_KEY", "") + "agent_a": settings.AGENT_A_MODEL, + "agent_b": settings.AGENT_B_MODEL, + "agent_a_provider": settings.AGENT_A_PROVIDER, + "agent_b_provider": settings.AGENT_B_PROVIDER, + "agent_a_role": settings.AGENT_A_ROLE, + "agent_b_role": settings.AGENT_B_ROLE, + "agent_a_system_prompt": settings.AGENT_A_SYSTEM_PROMPT, + "agent_b_system_prompt": settings.AGENT_B_SYSTEM_PROMPT, + "agent_a_temp": settings.AGENT_A_TEMPERATURE, + "agent_b_temp": settings.AGENT_B_TEMPERATURE, + "openai_api_key": settings.OPENAI_API_KEY }, "episode": { "max_steps": settings.MAX_STEPS, @@ -50,8 +59,16 @@ def get_config(): @router.post("/config") def update_config(req: ConfigUpdate): settings.MAX_STEPS = req.MAX_STEPS - # Convert Pydantic models to dicts - settings.AGENTS = [a.model_dump() for a in req.AGENTS] + settings.AGENT_A_MODEL = req.AGENT_A_MODEL + settings.AGENT_B_MODEL = req.AGENT_B_MODEL + settings.AGENT_A_PROVIDER = req.AGENT_A_PROVIDER + settings.AGENT_B_PROVIDER = req.AGENT_B_PROVIDER + settings.AGENT_A_ROLE = req.AGENT_A_ROLE + settings.AGENT_B_ROLE = req.AGENT_B_ROLE + settings.AGENT_A_SYSTEM_PROMPT = req.AGENT_A_SYSTEM_PROMPT + settings.AGENT_B_SYSTEM_PROMPT = req.AGENT_B_SYSTEM_PROMPT + settings.AGENT_A_TEMPERATURE = req.AGENT_A_TEMPERATURE + settings.AGENT_B_TEMPERATURE = req.AGENT_B_TEMPERATURE settings.EXECUTION_MODE = req.EXECUTION_MODE settings.SSH_HOST = req.SSH_HOST settings.SSH_PORT = req.SSH_PORT @@ -61,10 +78,18 @@ def update_config(req: ConfigUpdate): # Persist to default.env from models.model_manager import model_manager - import json model_manager._update_env_file({ "MAX_STEPS": req.MAX_STEPS, - "AGENTS_JSON": json.dumps(settings.AGENTS), + "AGENT_A_MODEL": req.AGENT_A_MODEL, + "AGENT_B_MODEL": req.AGENT_B_MODEL, + "AGENT_A_PROVIDER": req.AGENT_A_PROVIDER, + "AGENT_B_PROVIDER": req.AGENT_B_PROVIDER, + "AGENT_A_ROLE": req.AGENT_A_ROLE, + "AGENT_B_ROLE": req.AGENT_B_ROLE, + "AGENT_A_SYSTEM_PROMPT": req.AGENT_A_SYSTEM_PROMPT, + "AGENT_B_SYSTEM_PROMPT": req.AGENT_B_SYSTEM_PROMPT, + "AGENT_A_TEMPERATURE": req.AGENT_A_TEMPERATURE, + "AGENT_B_TEMPERATURE": req.AGENT_B_TEMPERATURE, "EXECUTION_MODE": req.EXECUTION_MODE, "SSH_HOST": req.SSH_HOST, "SSH_PORT": req.SSH_PORT, diff --git a/backend/api/routes/model_routes.py b/backend/api/routes/model_routes.py index 985abb8229e15f37e2c69200abe579fe5eaf7b9b..4cab790ddb9a8e5f01c47c9835abca9483a029ec 100644 --- a/backend/api/routes/model_routes.py +++ b/backend/api/routes/model_routes.py @@ -2,8 +2,6 @@ from fastapi import APIRouter, HTTPException from pydantic import BaseModel from models.model_manager import model_manager from api.routes.websocket import broadcast -import httpx -import os router = APIRouter() @@ -19,90 +17,17 @@ class RemoveCustomModelReq(BaseModel): class PullModelReq(BaseModel): model_name: str -HF_MODELS = [ - "meta-llama/Llama-3.1-8B-Instruct", - "meta-llama/Llama-3.2-1B-Instruct", - "meta-llama/Llama-3.3-70B-Instruct", - "meta-llama/Llama-4-Scout-17B-16E-Instruct", - "meta-llama/Llama-4-Maverick-17B-128E-Instruct", - "meta-llama/Meta-Llama-3-8B-Instruct", - "meta-llama/Meta-Llama-3-70B-Instruct", - "google/gemma-4-31B-it", - "google/gemma-4-26B-A4B-it", - "google/gemma-3-27b-it", - "google/gemma-3n-E4B-it", - "Qwen/Qwen3.5-9B", - "Qwen/Qwen2.5-7B-Instruct", - "Qwen/Qwen2.5-72B-Instruct", - "Qwen/Qwen2.5-Coder-7B-Instruct", - "Qwen/Qwen2.5-Coder-32B-Instruct", - "Qwen/Qwen3-8B", - "Qwen/Qwen3-32B", - "Qwen/Qwen3-4B-Instruct-2507", - "Qwen/Qwen3-14B", - "Qwen/Qwen3-VL-8B-Instruct", - "Qwen/Qwen3-VL-30B-A3B-Instruct", - "Qwen/QwQ-32B", - "deepseek-ai/DeepSeek-R1", - "deepseek-ai/DeepSeek-V3", - "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B", - "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B", - "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B", - "deepseek-ai/DeepSeek-R1-Distill-Llama-70B", - "deepseek-ai/DeepSeek-R1-Distill-Llama-8B", - "deepseek-ai/DeepSeek-Prover-V2-671B", - "CohereLabs/c4ai-command-r-08-2024", - "CohereLabs/c4ai-command-r7b-arabic-02-2025", - "CohereLabs/command-a-vision-07-2025", - "CohereLabs/aya-expanse-32b", - "CohereLabs/aya-vision-32b", - "NousResearch/Hermes-2-Pro-Llama-3-8B", - "MiniMaxAI/MiniMax-M2.5", - "MiniMaxAI/MiniMax-M2", - "MiniMaxAI/MiniMax-M1-80k", - "moonshotai/Kimi-K2.5", - "moonshotai/Kimi-K2-Instruct", - "moonshotai/Kimi-K2-Thinking", - "xiaomiMiMo/MiMo-V2-Flash", - "zai-org/GLM-5", - "zai-org/GLM-4.7-Flash", - "zai-org/GLM-4.7", - "zai-org/GLM-4.6", - "zai-org/GLM-4.5", -] - @router.get("/models") async def get_models(): local_models = await model_manager.list_available_models() return { "local_models": local_models, - "hf_models": HF_MODELS, "custom_model": { - "enabled": True, + "enabled": True, # Hardcode from settings in real app "agent": "agent_a" } } -@router.get("/models/hf") -async def get_hf_models(): - hf_token = os.environ.get("HF_TOKEN", "") or "hf_demo" - - try: - async with httpx.AsyncClient() as client: - resp = await client.get( - "https://router.huggingface.co/v1/models", - headers={"Authorization": f"Bearer {hf_token}"}, - timeout=30.0 - ) - if resp.status_code == 200: - data = resp.json() - models = [m["id"] for m in data.get("data", [])] - return {"models": models, "source": "hf_router"} - except Exception as e: - pass - - return {"models": HF_MODELS, "source": "fallback"} - @router.post("/models/add") async def add_custom_model(req: AddCustomModelReq): result = await model_manager.add_custom_model( @@ -119,4 +44,5 @@ async def remove_custom_model(req: RemoveCustomModelReq): @router.post("/models/pull") async def pull_model(req: PullModelReq): + # Fire and forget streaming. Need a task runner ideally, but generator logic: return {"message": "Streaming progress via WS not fully implemented but requested."} diff --git a/backend/api/routes/openenv.py b/backend/api/routes/openenv.py index c9792f2908250ccd54b221c833e68204e218d850..e9a3a585336b4c155ecd26d5dbff04c899cbca37 100644 --- a/backend/api/routes/openenv.py +++ b/backend/api/routes/openenv.py @@ -22,9 +22,7 @@ async def simulation_loop(): step_num = 1 done = False - from config import settings - agent_list = getattr(settings, "AGENTS", []) - active_agent = agent_list[0]["id"] if agent_list else "agent_a" + active_agent = "agent_a" while not done: # Check if the episode was reset/cancelled @@ -74,12 +72,7 @@ async def simulation_loop(): logger.error(f"Error in simulation loop at step {step_num}: {e}") break - from config import settings - agent_list = settings.AGENTS if settings.AGENTS else [{"id": "agent_a"}] - current_idx = next((i for i, a in enumerate(agent_list) if a["id"] == active_agent), 0) - next_idx = (current_idx + 1) % len(agent_list) - active_agent = agent_list[next_idx]["id"] - + active_agent = "agent_b" if active_agent == "agent_a" else "agent_a" step_num += 1 await asyncio.sleep(1) @@ -88,7 +81,7 @@ async def simulation_loop(): from typing import Optional, Dict, Any class ResetRequest(BaseModel): - task: Optional[str] = "software-incident" + task: str = "software-incident" custom_scenario: Optional[Dict[str, Any]] = None seed: Optional[int] = None max_steps: Optional[int] = None @@ -108,35 +101,16 @@ async def start_simulation(): if not episode_manager.env.active_episode: logger.info("No active episode found for simulation. Performing auto-reset.") await episode_manager.reset(task="software-incident") - else: - # Broadcast episode_start to notify frontend a new simulation is beginning - from api.routes.websocket import broadcast - sc_safe = episode_manager.env.active_scenario.copy() - if "root_cause" in sc_safe: del sc_safe["root_cause"] - if "correct_fix" in sc_safe: del sc_safe["correct_fix"] - if "clue_map" in sc_safe: del sc_safe["clue_map"] - from config import settings - await broadcast("episode_start", { - "episode_id": episode_manager.env.active_episode.episode_id, - "scenario": sc_safe, - "task": episode_manager.env.active_episode.task, - "difficulty": episode_manager.env.active_episode.difficulty, - "agents": settings.AGENTS - }) episode_manager.simulation_task = asyncio.create_task(simulation_loop()) from api.routes.websocket import broadcast - await broadcast("system_status", {"active": True, "paused": False, "status": "INVESTIGATING"}) + await broadcast("system_status", {"active": True, "paused": False}) return {"status": "started"} @router.post("/reset", response_model=NexusObservation) -async def reset_env(req: Optional[ResetRequest] = None): +async def reset_env(req: ResetRequest): try: - task = req.task if req else "software-incident" - custom_scenario = req.custom_scenario if req else None - seed = req.seed if req else None - max_steps = req.max_steps if req else None - obs = await episode_manager.reset(task=task, custom_scenario=custom_scenario, seed=seed, max_steps=max_steps) + obs = await episode_manager.reset(req.task, req.custom_scenario, seed=req.seed, max_steps=req.max_steps) return obs except Exception as e: raise HTTPException(status_code=400, detail=str(e)) @@ -156,13 +130,11 @@ async def step_env(action: NexusAction): except Exception as e: raise HTTPException(status_code=500, detail=str(e)) -@router.get("/state") +@router.get("/state", response_model=NexusState) def get_state(): - """Returns the current episode state. Returns idle status if no episode is active.""" state = episode_manager.env.state() - # state() now always returns something โ€” either a NexusState pydantic object or an idle dict. - if hasattr(state, "model_dump"): - return state.model_dump() + if not state: + raise HTTPException(status_code=400, detail="No active episode") return state @router.get("/telemetry") diff --git a/backend/api/routes/websocket.py b/backend/api/routes/websocket.py index 05481fb00b4294c715355a018959592726c16e7a..d4ea7a4a6e9d04ae28940a7a2c5eed4336c54bf2 100644 --- a/backend/api/routes/websocket.py +++ b/backend/api/routes/websocket.py @@ -60,8 +60,8 @@ async def websocket_endpoint(websocket: WebSocket): }) elif action == "reset": logger.info("UI Command: RESET") - await episode_manager.reset(task="software-incident", broadcast_episode=False) - await broadcast("system_status", {"paused": False, "status": "READY", "active": False}) + await episode_manager.reset(task="software-incident") + await broadcast("system_status", {"paused": False, "status": "READY", "active": false}) elif action == "force_end": logger.info("UI Command: FORCE_END") if episode_manager.env and episode_manager.env.active_episode: diff --git a/backend/api/schemas/state.py b/backend/api/schemas/state.py index 2a70cbe75ae57d3f5cf832065b5109af0df1b9dd..1af6749c57e1ca3563be88ad9d12bfa9dcf9037e 100644 --- a/backend/api/schemas/state.py +++ b/backend/api/schemas/state.py @@ -8,7 +8,8 @@ class NexusState(BaseModel): difficulty: str current_round: int max_rounds: int - messages_by_agent: Dict[str, List[str]] + agent_a_messages: List[str] + agent_b_messages: List[str] tool_calls_made: List[Dict] clues_found: List[str] root_cause_found: bool diff --git a/backend/config.py b/backend/config.py index c45e541a15001d7a13ad9b908b4af95ca5573e1a..a4f1811099f2297bbc060ada9687ec0639f9e2e7 100644 --- a/backend/config.py +++ b/backend/config.py @@ -1,117 +1,63 @@ -import json -import os -from pathlib import Path -from dotenv import load_dotenv - -BASE_DIR = Path(__file__).resolve().parent -ROOT_DIR = BASE_DIR.parent - -# Load environment variables, checking both backend/ and project root -if (BASE_DIR / ".env").exists(): - load_dotenv(BASE_DIR / ".env") -elif (ROOT_DIR / ".env").exists(): - load_dotenv(ROOT_DIR / ".env") -elif (ROOT_DIR / "default.env").exists(): - load_dotenv(ROOT_DIR / "default.env") -else: - load_dotenv() # Fallback to standard search - -# Helper data for agent configuration -_BUILT_IN_ROLES = ["INVESTIGATOR", "VALIDATOR", "FORENSIC_ANALYST", "NETWORK_ENGINEER", "SYSTEM_ADMIN", "SECURITY_ARCHITECT", "COMPLIANCE_OFFICER"] -_DEFAULT_ROLES = ["INVESTIGATOR", "VALIDATOR", "FORENSIC_ANALYST", "NETWORK_ENGINEER"] - -def _build_agents_from_env(): - import json - agents = [] - suffix_map = {'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4, 'f': 5, 'g': 6, 'h': 7, 'i': 8, 'j': 9} - for suffix, idx in suffix_map.items(): - model_key = f"AGENT_{suffix.upper()}_MODEL" - provider_key = f"AGENT_{suffix.upper()}_PROVIDER" - role_key = f"AGENT_{suffix.upper()}_ROLE" - prompt_key = f"AGENT_{suffix.upper()}_SYSTEM_PROMPT" - temp_key = f"AGENT_{suffix.upper()}_TEMPERATURE" - - model = os.getenv(model_key, "") - if model: - role_idx = idx % len(_DEFAULT_ROLES) - agents.append({ - "id": f"agent_{suffix}", - "model": model, - "provider": os.getenv(provider_key, "ollama"), - "role": os.getenv(role_key, _DEFAULT_ROLES[role_idx]), - "system_prompt": os.getenv(prompt_key, ""), - "temperature": float(os.getenv(temp_key, str(0.7 - idx * 0.05))) - }) - - if not agents: - agents = [ - { - "id": "agent_a", - "model": os.getenv("AGENT_A_MODEL", "meta-llama/Llama-3.1-8B-Instruct"), - "provider": os.getenv("AGENT_A_PROVIDER", "hf"), - "role": "INVESTIGATOR", - "system_prompt": "", - "temperature": 0.7 - }, - { - "id": "agent_b", - "model": os.getenv("AGENT_B_MODEL", "meta-llama/Llama-3.2-1B-Instruct"), - "provider": os.getenv("AGENT_B_PROVIDER", "hf"), - "role": "VALIDATOR", - "system_prompt": "", - "temperature": 0.6 - } - ] - return agents - -class Settings: - # OLLAMA - OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434/v1") - OLLAMA_API_KEY = os.getenv("OLLAMA_API_KEY", "ollama") - - # AGENTS (Dynamic N-Agent Support) - try: - AGENTS_JSON = os.getenv("AGENTS_JSON") - AGENTS = json.loads(AGENTS_JSON) if AGENTS_JSON else _build_agents_from_env() - except: - AGENTS = _build_agents_from_env() - # EXECUTION ENVIRONMENT - EXECUTION_MODE = os.getenv("EXECUTION_MODE", "simulated") - SSH_HOST = os.getenv("SSH_HOST", "") - SSH_PORT = int(os.getenv("SSH_PORT", "22")) - SSH_USER = os.getenv("SSH_USER", "") - SSH_PASSWORD = os.getenv("SSH_PASSWORD", "") - - # HUGGINGFACE - API_KEY = os.getenv("API_KEY", "ollama") - OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "") - HF_TOKEN = os.getenv("HF_TOKEN", "") - HF_INFERENCE_URL = os.getenv("HF_INFERENCE_URL", "https://router.huggingface.co/v1") - - # OPENROUTER - OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "") - OPENROUTER_BASE_URL = os.getenv("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1") - - # SERVER - HOST = os.getenv("HOST", "0.0.0.0") - PORT = int(os.getenv("PORT", "7860")) - DEBUG = os.getenv("DEBUG", "true").lower() in ("true", "1", "yes") - ENVIRONMENT = os.getenv("ENVIRONMENT", "local") - - # EPISODE - MAX_STEPS = int(os.getenv("MAX_STEPS", "1000")) - MAX_EPISODE_TIME_SECONDS = int(os.getenv("MAX_EPISODE_TIME_SECONDS", "1200")) - SUCCESS_SCORE_THRESHOLD = float(os.getenv("SUCCESS_SCORE_THRESHOLD", "0.5")) - - # MCP TOOL SERVER - MCP_SERVER_PORT = int(os.getenv("MCP_SERVER_PORT", "8001")) - MCP_SERVER_URL = os.getenv("MCP_SERVER_URL", "http://localhost:8001") - - # CUSTOM MODEL - CUSTOM_MODEL_ENABLED = os.getenv("CUSTOM_MODEL_ENABLED", "false").lower() in ("true", "1", "yes") - CUSTOM_MODEL_BASE_URL = os.getenv("CUSTOM_MODEL_BASE_URL", "") - CUSTOM_MODEL_API_KEY = os.getenv("CUSTOM_MODEL_API_KEY", "") - CUSTOM_MODEL_NAME = os.getenv("CUSTOM_MODEL_NAME", "") - CUSTOM_MODEL_AGENT = os.getenv("CUSTOM_MODEL_AGENT", "") - -settings = Settings() +import os +from pathlib import Path +from dotenv import load_dotenv + +BASE_DIR = Path(__file__).resolve().parent + +# Load environment variables, overriding defaults if needed +load_dotenv(BASE_DIR / ".env") + +class Settings: + # OLLAMA + OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434/v1") + OLLAMA_API_KEY = os.getenv("OLLAMA_API_KEY", "ollama") + + # AGENTS + AGENT_A_MODEL = os.getenv("AGENT_A_MODEL", "") + AGENT_B_MODEL = os.getenv("AGENT_B_MODEL", "") + AGENT_A_PROVIDER = os.getenv("AGENT_A_PROVIDER", "ollama") + AGENT_B_PROVIDER = os.getenv("AGENT_B_PROVIDER", "ollama") + AGENT_A_ROLE = os.getenv("AGENT_A_ROLE", "INVESTIGATOR") + AGENT_B_ROLE = os.getenv("AGENT_B_ROLE", "VALIDATOR") + AGENT_A_SYSTEM_PROMPT = os.getenv("AGENT_A_SYSTEM_PROMPT", "") + AGENT_B_SYSTEM_PROMPT = os.getenv("AGENT_B_SYSTEM_PROMPT", "") + AGENT_A_TEMPERATURE = float(os.getenv("AGENT_A_TEMPERATURE", "0.8")) + AGENT_B_TEMPERATURE = float(os.getenv("AGENT_B_TEMPERATURE", "0.6")) + AGENT_A_MAX_TOKENS = int(os.getenv("AGENT_A_MAX_TOKENS", "300")) + AGENT_B_MAX_TOKENS = int(os.getenv("AGENT_B_MAX_TOKENS", "300")) + # EXECUTION ENVIRONMENT + EXECUTION_MODE = os.getenv("EXECUTION_MODE", "simulated") + SSH_HOST = os.getenv("SSH_HOST", "") + SSH_PORT = int(os.getenv("SSH_PORT", "22")) + SSH_USER = os.getenv("SSH_USER", "") + SSH_PASSWORD = os.getenv("SSH_PASSWORD", "") + + # HUGGINGFACE + API_KEY = os.getenv("API_KEY", "ollama") + OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "") + HF_TOKEN = os.getenv("HF_TOKEN", "") + HF_INFERENCE_URL = os.getenv("HF_INFERENCE_URL", "https://api-inference.huggingface.co/v1") + + # SERVER + HOST = os.getenv("HOST", "0.0.0.0") + PORT = int(os.getenv("PORT", "7860")) + DEBUG = os.getenv("DEBUG", "true").lower() in ("true", "1", "yes") + ENVIRONMENT = os.getenv("ENVIRONMENT", "local") + + # EPISODE + MAX_STEPS = int(os.getenv("MAX_STEPS", "1000")) + MAX_EPISODE_TIME_SECONDS = int(os.getenv("MAX_EPISODE_TIME_SECONDS", "1200")) + SUCCESS_SCORE_THRESHOLD = float(os.getenv("SUCCESS_SCORE_THRESHOLD", "0.5")) + + # MCP TOOL SERVER + MCP_SERVER_PORT = int(os.getenv("MCP_SERVER_PORT", "8001")) + MCP_SERVER_URL = os.getenv("MCP_SERVER_URL", "http://localhost:8001") + + # CUSTOM MODEL + CUSTOM_MODEL_ENABLED = os.getenv("CUSTOM_MODEL_ENABLED", "false").lower() in ("true", "1", "yes") + CUSTOM_MODEL_BASE_URL = os.getenv("CUSTOM_MODEL_BASE_URL", "") + CUSTOM_MODEL_API_KEY = os.getenv("CUSTOM_MODEL_API_KEY", "") + CUSTOM_MODEL_NAME = os.getenv("CUSTOM_MODEL_NAME", "") + CUSTOM_MODEL_AGENT = os.getenv("CUSTOM_MODEL_AGENT", "") + +settings = Settings() diff --git a/backend/core/agent_runner.py b/backend/core/agent_runner.py index 7c00b297da7fecc31d309950f44ae35ae18aedd5..d74a4785cc1fc3bf962e7a407f8a89d08c422eaa 100644 --- a/backend/core/agent_runner.py +++ b/backend/core/agent_runner.py @@ -1,150 +1,141 @@ -import re -import asyncio -from typing import List -from api.schemas.action import ToolCall -from models.model_manager import model_manager -from tools.tool_registry import registry -from utils.logger import logger -from config import settings - -ROLE_DEFINITIONS = { - "INVESTIGATOR": "You are an expert incident investigator with deep systems knowledge. Your job: form specific hypotheses, test them with tools, eliminate dead ends, and find the root cause. Be direct. Be technical. Never be vague.", - "VALIDATOR": "You are an expert systems validator and devil's advocate. Your job: challenge every hypothesis with evidence, find edge cases, and verify fixes. Do NOT simply agree. If your partner is wrong, prove it with tools. If they found the root cause, verify it thoroughly before accepting.", - "FORENSIC_ANALYST": "You are an elite digital forensic analyst. Focus heavily on reading logs, inspecting file timestamps, memory dumps, and tracing bash histories. Do not guessโ€”look for precise digital fingerprints and anomalies deep in the system logs.", - "NETWORK_ENGINEER": "You are a senior network engineer. Focus exclusively on port communication, routing tables, ping, active TCP/UDP connections, and firewall configurations. Your instinct is to determine how the threat or failure is propagating across the internal mesh.", - "SYSTEM_ADMIN": "You are a grizzled system administrator. Focus on core OS services, user permissions, kernel messages (dmesg), cron jobs, and runaway processes. Fix operational misconfigurations logically and decisively.", - "SECURITY_ARCHITECT": "You are a rigorous security architect. You look for structural flaws: overly permissive firewall rules, unencrypted traffic flows, and exposed secrets. Treat every incident as a potential systemic breach.", - "COMPLIANCE_OFFICER": "You are a strict compliance and audit officer. You focus strictly on policy violations and unauthorized data access. Identify unapproved tools and non-compliant execution paths. Prioritize strict adherence over operational speed." -} - -TOOL_INSTRUCTIONS_SIMULATED = """ -You have access to simulation tools. When calling a tool write exactly: TOOL: tool_name(param="value") -You can call multiple tools per message. You must use tools like update_config and restart_service to fix the system. -Once the fix is verified entirely, call TOOL: submit_resolution(root_cause_service="...", root_cause_description="...", fix_applied="...") to end the investigation. -""" - -TOOL_INSTRUCTIONS_SSH = """ -You are operating on a LIVE remote Linux server. You have a real bash terminal via the run_terminal_command tool. USE IT AGGRESSIVELY. -You have root access. Do NOT theorize without evidence โ€” run actual commands to get facts. -When calling a tool write exactly: TOOL: run_terminal_command(command="your bash command here") -Examples: TOOL: run_terminal_command(command="journalctl -n 50 --no-pager") - TOOL: run_terminal_command(command="systemctl status nginx") -You can also call propose_fix. Once the fix is verified, call TOOL: submit_resolution(root_cause_service="...", root_cause_description="...", fix_applied="...") to end the investigation. -""" - -class AgentRunner: - def parse_tool_calls(self, message: str) -> List[ToolCall]: - # Parse "TOOL: tool_name(param="value")" - tool_calls = [] - pattern = r'TOOL:\s*([a-zA-Z0-9_]+)\((.*?)\)' - matches = re.finditer(pattern, message) - - for match in matches: - tool_name = match.group(1) - params_str = match.group(2) - - # Simple param parsing - expects key="value", key='value' or key=value - params = {} - if params_str.strip(): - param_pairs = params_str.split(',') - for pair in param_pairs: - if '=' in pair: - k, v = pair.split('=', 1) - k = k.strip() - v = v.strip().strip('"').strip("'") - params[k] = v - - tool_calls.append(ToolCall(tool_name=tool_name, params=params)) - - return tool_calls - - async def execute_tool_calls(self, tool_calls: List[ToolCall], scenario: dict, round_num: int, episode_state) -> List[dict]: - results = [] - for tc in tool_calls: - # We call the registry. In reality, MCP might be external but here it's in-process registry calls - # Episode state is passed for propose_fix and verify_fix - res_str = registry.call_tool(tc.tool_name, tc.params, scenario, round_num, episode_state) - - # Record it in state - episode_state.add_tool_call(tc.tool_name, tc.params) - - results.append({ - "tool_name": tc.tool_name, - "result": res_str, - "success": not res_str.startswith("Error") - }) - return results - - async def run_step(self, agent_id: str, episode_state, scenario: dict, max_retries: int = 2): - client, model_name = model_manager.get_client(agent_id) - - is_ssh = settings.EXECUTION_MODE == "ssh" - tool_rules = TOOL_INSTRUCTIONS_SSH if is_ssh else TOOL_INSTRUCTIONS_SIMULATED - - agent_config = next((a for a in settings.AGENTS if a["id"] == agent_id), {}) - role = agent_config.get("role", "INVESTIGATOR") - custom_prompt = agent_config.get("system_prompt", "") - - if role.startswith("CUSTOM_") and custom_prompt: - sys_prompt = custom_prompt + "\n\n" + tool_rules - else: - behavior = ROLE_DEFINITIONS.get(role, ROLE_DEFINITIONS["INVESTIGATOR"]) - sys_prompt = behavior + "\n\n" + tool_rules - - context = f"Current incident: {scenario.get('description', '')}\n" - - other_agents = [a["id"] for a in settings.AGENTS if a["id"] != agent_id] - if other_agents: - context += f"Other agents in this investigation: {', '.join(other_agents)}\n" - - agent_configs = {a["id"]: a for a in settings.AGENTS} - for other_id in other_agents: - other_msgs = episode_state.messages_by_agent.get(other_id, []) - if other_msgs: - other_role = agent_configs.get(other_id, {}).get("role", "AGENT") - last_msg = other_msgs[-1] if other_msgs else "" - context += f"\n[{other_role}] {other_id}'s latest insight: {last_msg[:300]}...\n" - - if hasattr(episode_state, 'clues_found') and episode_state.clues_found: - context += f"\nClues discovered so far:\n" - for clue in episode_state.clues_found[-5:]: - context += f"- {clue[:200]}\n" - - messages = [{"role": "system", "content": sys_prompt}] - - recent_msgs = episode_state.all_messages[-6:] - if recent_msgs: - context += "\nRecent conversation history:\n" - for i, m in enumerate(recent_msgs[-4:]): - if len(m) > 150: - m = m[:150] + "..." - context += f"- {m}\n" - - messages.append({"role": "user", "content": context}) - - full_response = "" - last_error = None - - for attempt in range(max_retries + 1): - try: - stream = await client.chat.completions.create( - model=model_name, - messages=messages, - max_tokens=2048, - timeout=120.0, - stream=True - ) - async for chunk in stream: - content = chunk.choices[0].delta.content or "" - if content: - full_response += content - yield content - return - except Exception as e: - last_error = e - logger.warning(f"Attempt {attempt + 1}/{max_retries + 1} failed for {model_name}: {e}") - if attempt < max_retries: - await asyncio.sleep(2 ** attempt) - - logger.error(f"All retries exhausted for {model_name}: {last_error}") - yield f"I encountered an error: {last_error}. Please verify the model endpoint is accessible." +import re +from typing import List +from api.schemas.action import ToolCall +from models.model_manager import model_manager +from tools.tool_registry import registry +from utils.logger import logger +from config import settings + +ROLE_DEFINITIONS = { + "INVESTIGATOR": "You are an expert incident investigator with deep systems knowledge. Your job: form specific hypotheses, test them with tools, eliminate dead ends, and find the root cause. Be direct. Be technical. Never be vague.", + "VALIDATOR": "You are an expert systems validator and devil's advocate. Your job: challenge every hypothesis with evidence, find edge cases, and verify fixes. Do NOT simply agree. If your partner is wrong, prove it with tools. If they found the root cause, verify it thoroughly before accepting.", + "FORENSIC_ANALYST": "You are an elite digital forensic analyst. Focus heavily on reading logs, inspecting file timestamps, memory dumps, and tracing bash histories. Do not guessโ€”look for precise digital fingerprints and anomalies deep in the system logs.", + "NETWORK_ENGINEER": "You are a senior network engineer. Focus exclusively on port communication, routing tables, ping, active TCP/UDP connections, and firewall configurations. Your instinct is to determine how the threat or failure is propagating across the internal mesh.", + "SYSTEM_ADMIN": "You are a grizzled system administrator. Focus on core OS services, user permissions, kernel messages (dmesg), cron jobs, and runaway processes. Fix operational misconfigurations logically and decisively.", + "SECURITY_ARCHITECT": "You are a rigorous security architect. You look for structural flaws: overly permissive firewall rules, unencrypted traffic flows, and exposed secrets. Treat every incident as a potential systemic breach.", + "COMPLIANCE_OFFICER": "You are a strict compliance and audit officer. You focus strictly on policy violations and unauthorized data access. Identify unapproved tools and non-compliant execution paths. Prioritize strict adherence over operational speed." +} + +TOOL_INSTRUCTIONS_SIMULATED = """ +You have access to simulation tools. When calling a tool write exactly: TOOL: tool_name(param="value") +You can call multiple tools per message. You must use tools like update_config and restart_service to fix the system. +Once the fix is verified entirely, call TOOL: submit_resolution(root_cause_service="...", root_cause_description="...", fix_applied="...") to end the investigation. +""" + +TOOL_INSTRUCTIONS_SSH = """ +You are operating on a LIVE remote Linux server. You have a real bash terminal via the run_terminal_command tool. USE IT AGGRESSIVELY. +You have root access. Do NOT theorize without evidence โ€” run actual commands to get facts. +When calling a tool write exactly: TOOL: run_terminal_command(command="your bash command here") +Examples: TOOL: run_terminal_command(command="journalctl -n 50 --no-pager") + TOOL: run_terminal_command(command="systemctl status nginx") +You can also call propose_fix. Once the fix is verified, call TOOL: submit_resolution(root_cause_service="...", root_cause_description="...", fix_applied="...") to end the investigation. +""" + +class AgentRunner: + def parse_tool_calls(self, message: str) -> List[ToolCall]: + # Parse "TOOL: tool_name(param="value")" + tool_calls = [] + pattern = r'TOOL:\s*([a-zA-Z0-9_]+)\((.*?)\)' + matches = re.finditer(pattern, message) + + for match in matches: + tool_name = match.group(1) + params_str = match.group(2) + + # Simple param parsing - expects key="value", key='value' or key=value + params = {} + if params_str.strip(): + param_pairs = params_str.split(',') + for pair in param_pairs: + if '=' in pair: + k, v = pair.split('=', 1) + k = k.strip() + v = v.strip().strip('"').strip("'") + params[k] = v + + tool_calls.append(ToolCall(tool_name=tool_name, params=params)) + + return tool_calls + + async def execute_tool_calls(self, tool_calls: List[ToolCall], scenario: dict, round_num: int, episode_state) -> List[dict]: + results = [] + for tc in tool_calls: + # We call the registry. In reality, MCP might be external but here it's in-process registry calls + # Episode state is passed for propose_fix and verify_fix + res_str = registry.call_tool(tc.tool_name, tc.params, scenario, round_num, episode_state) + + # Record it in state + episode_state.add_tool_call(tc.tool_name, tc.params) + + results.append({ + "tool_name": tc.tool_name, + "result": res_str, + "success": not res_str.startswith("Error") + }) + return results + + async def run_step(self, agent_id: str, episode_state, scenario: dict): + client, model_name = model_manager.get_client(agent_id) + + is_ssh = settings.EXECUTION_MODE == "ssh" + tool_rules = TOOL_INSTRUCTIONS_SSH if is_ssh else TOOL_INSTRUCTIONS_SIMULATED + + # Build System Prompt based on mapping + if agent_id == "agent_a": + role = settings.AGENT_A_ROLE + custom_prompt = settings.AGENT_A_SYSTEM_PROMPT + else: + role = settings.AGENT_B_ROLE + custom_prompt = settings.AGENT_B_SYSTEM_PROMPT + + if role.startswith("CUSTOM_") and custom_prompt: + sys_prompt = custom_prompt + "\n\n" + tool_rules + else: + behavior = ROLE_DEFINITIONS.get(role, ROLE_DEFINITIONS["INVESTIGATOR"]) + sys_prompt = behavior + "\n\n" + tool_rules + + # Build context + context = f"Current incident: {scenario.get('description', '')}\n" + if hasattr(episode_state, 'last_partner_message') and episode_state.last_partner_message: + context += f"Partner's last message: {episode_state.last_partner_message}\n" + if hasattr(episode_state, 'clues_found') and episode_state.clues_found: + context += f"Clues found: {episode_state.clues_found}\n" + # Note: don't mention rounds - let agents reason freely until consensus + + # We append history + messages = [{"role": "system", "content": sys_prompt}] + + # Add a summary of previous messages + if hasattr(episode_state, 'all_messages'): + all_msgs = episode_state.all_messages[-3:] # only last 3 to fit context + if all_msgs: + context += "\nRecent history:\n" + for m in all_msgs: + context += f"- {m}\n" + + messages.append({"role": "user", "content": context}) + + # Call model with streaming + full_response = "" + try: + stream = await client.chat.completions.create( + model=model_name, + messages=messages, + max_tokens=2048, + timeout=60.0, + stream=True + ) + async for chunk in stream: + content = chunk.choices[0].delta.content or "" + if content: + full_response += content + yield content # Yield partial chunk + except Exception as e: + logger.error(f"Error calling model {model_name} for {agent_id}: {e}") + full_response = "I encountered an error analyzing the situation. Let me try again next round." + yield full_response + + # Final yielding of special end marker or just finish + # The caller (openenv.py) will collect all yielded values to build the full response + # and then call runner.parse_tool_calls(full_message) themselves. + pass diff --git a/backend/core/episode_manager.py b/backend/core/episode_manager.py index db70c3002ef2ab65653e4f6024f713337a8b8d8f..878e281302ae6c9317e18738b498690e653167ab 100644 --- a/backend/core/episode_manager.py +++ b/backend/core/episode_manager.py @@ -9,7 +9,7 @@ class EpisodeManager: self.is_paused = False self.simulation_task = None - async def reset(self, task: str, custom_scenario: dict = None, seed: int = None, max_steps: int = None, broadcast_episode: bool = True): + async def reset(self, task: str, custom_scenario: dict = None, seed: int = None, max_steps: int = None): # Cancel any active simulation loop if hasattr(self, 'simulation_task') and self.simulation_task and not self.simulation_task.done(): self.simulation_task.cancel() @@ -21,21 +21,21 @@ class EpisodeManager: obs = await self.env.reset(task=task, custom_scenario=custom_scenario, seed=seed, max_steps=max_steps) - if broadcast_episode: - # Broadcast episode_start - sc_safe = self.env.active_scenario.copy() - if "root_cause" in sc_safe: del sc_safe["root_cause"] - if "correct_fix" in sc_safe: del sc_safe["correct_fix"] - if "clue_map" in sc_safe: del sc_safe["clue_map"] - - from config import settings - await broadcast("episode_start", { - "episode_id": self.env.active_episode.episode_id, - "scenario": sc_safe, - "task": task, - "difficulty": self.env.active_episode.difficulty, - "agents": getattr(settings, "AGENTS", []) - }) + # Broadcast episode_start + sc_safe = self.env.active_scenario.copy() + if "root_cause" in sc_safe: del sc_safe["root_cause"] + if "correct_fix" in sc_safe: del sc_safe["correct_fix"] + if "clue_map" in sc_safe: del sc_safe["clue_map"] + + from config import settings + await broadcast("episode_start", { + "episode_id": self.env.active_episode.episode_id, + "scenario": sc_safe, + "task": task, + "difficulty": self.env.active_episode.difficulty, + "agent_a_model": settings.AGENT_A_MODEL, + "agent_b_model": settings.AGENT_B_MODEL + }) return obs async def step(self, action): @@ -81,11 +81,9 @@ class EpisodeManager: "success": info.get("success", False), "steps_taken": self.env.active_episode.steps_taken, "final_score": info.get("final_score", getattr(self.env.active_episode, "cumulative_reward", 0)), - "final_breakdown": info.get("breakdown", {}), - "clues_found": self.env.active_episode.clues_found, "root_cause_found": self.env.active_episode.fix_correct, "fix_verified": self.env.active_episode.fix_verified, - "time_taken_seconds": 0, + "time_taken_seconds": 0, # could track "reward_history": self.env.active_episode.reward_history }) diff --git a/backend/core/state_manager.py b/backend/core/state_manager.py index 28848810158e5aec27166eb0d719539a877dfb6b..9ea3abdfb83ea4f87a1ad4d4c7d46d3f808619ea 100644 --- a/backend/core/state_manager.py +++ b/backend/core/state_manager.py @@ -11,8 +11,8 @@ class EpisodeState: self.current_round = 1 self.max_rounds = max_rounds - from config import settings - self.messages_by_agent: Dict[str, List[str]] = {a["id"]: [] for a in settings.AGENTS} + self.agent_a_messages: List[str] = [] + self.agent_b_messages: List[str] = [] self.all_messages: List[str] = [] self.tool_calls_made: List[Dict] = [] @@ -39,15 +39,11 @@ class EpisodeState: def add_message(self, agent_id: str, message: str): self.steps_taken += 1 self.all_messages.append(message) - if agent_id not in self.messages_by_agent: - self.messages_by_agent[agent_id] = [] - self.messages_by_agent[agent_id].append(message) - - from config import settings - # A full round is defined as all agents having spoken at least once in the current sequence - # We can approximate this by incrementing round when the last agent in the list speaks - if settings.AGENTS and agent_id == settings.AGENTS[-1]["id"]: - self.current_round += 1 + if agent_id == "agent_a": + self.agent_a_messages.append(message) + else: + self.agent_b_messages.append(message) + self.current_round += 1 # A full round is both agents speaking self.last_partner_message = message @@ -68,7 +64,8 @@ class EpisodeState: difficulty=self.difficulty, current_round=self.current_round, max_rounds=self.max_rounds, - messages_by_agent=self.messages_by_agent, + agent_a_messages=self.agent_a_messages, + agent_b_messages=self.agent_b_messages, tool_calls_made=self.tool_calls_made, clues_found=self.clues_found, root_cause_found=self.root_cause_found, diff --git a/backend/main.py b/backend/main.py index 04f148c48ff081981fdddcaa01e2fd6a3cc9262f..000b958f8ba1af98a91a27aac93d495dea33a0f9 100644 --- a/backend/main.py +++ b/backend/main.py @@ -94,10 +94,4 @@ async def startup_event(): run_mcp() if __name__ == "__main__": - try: - uvicorn.run(app, host=settings.HOST, port=settings.PORT) - except Exception as e: - import traceback - print(f"FATAL ERROR AT STARTUP: {str(e)}") - traceback.print_exc() - os._exit(1) + uvicorn.run(app, host=settings.HOST, port=settings.PORT) diff --git a/backend/models/model_manager.py b/backend/models/model_manager.py index 306d0e407c7cd3bfa4e7e7af03dc1dfc7e1b2655..b10892e747f51d8517562498dbe3f7227520f313 100644 --- a/backend/models/model_manager.py +++ b/backend/models/model_manager.py @@ -1,162 +1,115 @@ -import os -from typing import Tuple, Dict, List -from openai import AsyncOpenAI -import httpx - -from config import settings -from .ollama_client import OllamaClient -from .hf_client import HFClient - -class ModelManager: - def __init__(self): - self.ollama = OllamaClient(settings.OLLAMA_BASE_URL, settings.OLLAMA_API_KEY) - self.hf_client = None - - hf_token = os.environ.get("HF_TOKEN", "") or settings.HF_TOKEN or "" - if hf_token and hf_token not in ("", "your_huggingface_token_here", "ollama", "hf_YourTokenHere"): - self.hf_client = HFClient(settings.HF_INFERENCE_URL, hf_token) - - def get_client(self, agent_id: str) -> Tuple[AsyncOpenAI, str]: - agent_config = next((a for a in settings.AGENTS if a["id"] == agent_id), None) - if not agent_config: - # Fallback for unrecognized agent - agent_config = settings.AGENTS[0] if settings.AGENTS else {"provider": "ollama", "model": "llama3"} - - provider = agent_config.get("provider", "ollama") - model_name = os.environ.get("MODEL_NAME", "") or agent_config.get("model", "") - - api_base = os.environ.get("API_BASE_URL", "") - api_key = os.environ.get("API_KEY", "") - if api_base and api_key and provider != "openai": - client = AsyncOpenAI( - base_url=api_base, - api_key=api_key - ) - return client, model_name - - hf_token = os.environ.get("HF_TOKEN", "") or settings.HF_TOKEN or "" - openai_key = os.environ.get("OPENAI_API_KEY", "") or getattr(settings, "OPENAI_API_KEY", "") - - if settings.CUSTOM_MODEL_ENABLED: - if settings.CUSTOM_MODEL_AGENT.lower() in (agent_id.lower(), "both", "all"): - client = AsyncOpenAI( - base_url=settings.CUSTOM_MODEL_BASE_URL, - api_key=settings.CUSTOM_MODEL_API_KEY or "none" - ) - return client, settings.CUSTOM_MODEL_NAME - - # Priority: OpenAI > HuggingFace > Ollama - if provider == "openai" and openai_key: - client = AsyncOpenAI(api_key=openai_key, base_url=getattr(settings, "OPENAI_BASE_URL", "https://api.openai.com/v1")) - return client, model_name - - if provider == "hf" or not self._is_ollama_available(): - if self.hf_client: - return self.hf_client.get_client(), model_name - if hf_token and hf_token not in ("", "your_huggingface_token_here", "ollama", "hf_YourTokenHere"): - temp_client = HFClient(settings.HF_INFERENCE_URL, hf_token) - return temp_client.get_client(), model_name - - if provider == "openrouter" and getattr(settings, "OPENROUTER_API_KEY", ""): - client = AsyncOpenAI(api_key=settings.OPENROUTER_API_KEY, base_url=settings.OPENROUTER_BASE_URL) - return client, model_name - - return self.ollama.get_client(), model_name - - def _is_ollama_available(self) -> bool: - try: - import socket - host = settings.OLLAMA_BASE_URL.replace("http://", "").replace("https://", "").split(":")[0] - port = 11434 - if ":" in settings.OLLAMA_BASE_URL: - port = int(settings.OLLAMA_BASE_URL.split(":")[-1].split("/")[0]) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(1) - result = sock.connect_ex((host, port)) - sock.close() - return result == 0 - except: - return False - - async def add_custom_model(self, agent_id: str, base_url: str, api_key: str, model_name: str) -> dict: - try: - client = AsyncOpenAI(base_url=base_url, api_key=api_key or "none") - response = await client.chat.completions.create( - model=model_name, - messages=[{"role": "user", "content": "Say 'hello' in exactly one word."}], - max_tokens=10, - timeout=30.0 - ) - - if response and response.choices: - env_map = { - "CUSTOM_MODEL_ENABLED": "true", - "CUSTOM_MODEL_BASE_URL": base_url, - "CUSTOM_MODEL_API_KEY": api_key, - "CUSTOM_MODEL_NAME": model_name, - "CUSTOM_MODEL_AGENT": agent_id - } - self._update_env_file(env_map) - - settings.CUSTOM_MODEL_ENABLED = True - settings.CUSTOM_MODEL_BASE_URL = base_url - settings.CUSTOM_MODEL_API_KEY = api_key - settings.CUSTOM_MODEL_NAME = model_name - settings.CUSTOM_MODEL_AGENT = agent_id - - return {"success": True, "message": "Custom model verified and activated."} - else: - return {"success": False, "message": "Model did not return a valid completion."} - - except Exception as e: - return {"success": False, "message": f"Validation failed: {str(e)}"} - - async def remove_custom_model(self, agent_id: str): - if settings.CUSTOM_MODEL_AGENT.lower() in (agent_id.lower(), "both"): - env_map = {"CUSTOM_MODEL_ENABLED": "false"} - self._update_env_file(env_map) - settings.CUSTOM_MODEL_ENABLED = False - - async def list_available_models(self) -> List[str]: - hf_token = os.environ.get("HF_TOKEN", "") or settings.HF_TOKEN or "" - if hf_token and hf_token not in ("", "your_huggingface_token_here", "ollama", "hf_YourTokenHere"): - try: - async with httpx.AsyncClient() as client: - resp = await client.get( - "https://huggingface.co/api/models", - headers={"Authorization": f"Bearer {hf_token}"}, - timeout=30.0 - ) - if resp.status_code == 200: - models = resp.json() - return [m["id"] for m in models[:50]] - except: - pass - return await self.ollama.list_models() - - def pull_model(self, model_name: str): - return self.ollama.pull_model(model_name) - - def _update_env_file(self, overrides: dict): - env_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "default.env") - if not os.path.exists(env_path): - return - - with open(env_path, "r") as f: - lines = f.readlines() - - new_lines = [] - for line in lines: - updated = False - for k, v in overrides.items(): - if line.startswith(f"{k}="): - new_lines.append(f"{k}={v}\n") - updated = True - break - if not updated: - new_lines.append(line) - - with open(env_path, "w") as f: - f.writelines(new_lines) - -model_manager = ModelManager() +import os +from typing import Tuple, Dict, List +from openai import AsyncOpenAI +import httpx +import json + +from config import settings +from .ollama_client import OllamaClient +from .hf_client import HFClient + +class ModelManager: + def __init__(self): + self.ollama = OllamaClient(settings.OLLAMA_BASE_URL, settings.OLLAMA_API_KEY) + self.hf = None + if settings.HF_TOKEN and settings.HF_TOKEN != "your_huggingface_token_here": + self.hf = HFClient(settings.HF_INFERENCE_URL, settings.HF_TOKEN) + + def get_client(self, agent_id: str) -> Tuple[AsyncOpenAI, str]: + # Check if custom model set for this agent + if settings.CUSTOM_MODEL_ENABLED: + if settings.CUSTOM_MODEL_AGENT.lower() == agent_id.lower() or settings.CUSTOM_MODEL_AGENT.lower() == "both": + client = AsyncOpenAI( + base_url=settings.CUSTOM_MODEL_BASE_URL, + api_key=settings.CUSTOM_MODEL_API_KEY or "none" + ) + return client, settings.CUSTOM_MODEL_NAME + + # Determine provider and model + if agent_id == "agent_a": + provider = settings.AGENT_A_PROVIDER + model_name = settings.AGENT_A_MODEL + else: + provider = settings.AGENT_B_PROVIDER + model_name = settings.AGENT_B_MODEL + + if provider == "hf" and self.hf: + return self.hf.get_client(), model_name + elif provider == "openai": + # We spin up OpenAI dynamically pulling the global OpenAI Key + client = AsyncOpenAI(api_key=settings.OPENAI_API_KEY) + return client, model_name + + return self.ollama.get_client(), model_name + + async def add_custom_model(self, agent_id: str, base_url: str, api_key: str, model_name: str) -> dict: + try: + # Validate endpoint and test with simple completion + client = AsyncOpenAI(base_url=base_url, api_key=api_key or "none") + response = await client.chat.completions.create( + model=model_name, + messages=[{"role": "user", "content": "Say 'hello' in exactly one word."}], + max_tokens=10, + timeout=10.0 + ) + + if response and response.choices: + # Test passed, update .env file dynamically + env_map = { + "CUSTOM_MODEL_ENABLED": "true", + "CUSTOM_MODEL_BASE_URL": base_url, + "CUSTOM_MODEL_API_KEY": api_key, + "CUSTOM_MODEL_NAME": model_name, + "CUSTOM_MODEL_AGENT": agent_id + } + self._update_env_file(env_map) + + # Update runtime config + settings.CUSTOM_MODEL_ENABLED = True + settings.CUSTOM_MODEL_BASE_URL = base_url + settings.CUSTOM_MODEL_API_KEY = api_key + settings.CUSTOM_MODEL_NAME = model_name + settings.CUSTOM_MODEL_AGENT = agent_id + + return {"success": True, "message": "Custom model verified and activated."} + else: + return {"success": False, "message": "Model did not return a valid completion."} + + except Exception as e: + return {"success": False, "message": f"Validation failed: {str(e)}"} + + async def remove_custom_model(self, agent_id: str): + if settings.CUSTOM_MODEL_AGENT.lower() == agent_id.lower() or settings.CUSTOM_MODEL_AGENT.lower() == "both": + env_map = {"CUSTOM_MODEL_ENABLED": "false"} + self._update_env_file(env_map) + settings.CUSTOM_MODEL_ENABLED = False + + async def list_available_models(self) -> List[str]: + return await self.ollama.list_models() + + def pull_model(self, model_name: str): + return self.ollama.pull_model(model_name) + + def _update_env_file(self, overrides: dict): + env_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "default.env") + if not os.path.exists(env_path): + return + + with open(env_path, "r") as f: + lines = f.readlines() + + new_lines = [] + for line in lines: + updated = False + for k, v in overrides.items(): + if line.startswith(f"{k}="): + new_lines.append(f"{k}={v}\n") + updated = True + break + if not updated: + new_lines.append(line) + + with open(env_path, "w") as f: + f.writelines(new_lines) + +model_manager = ModelManager() diff --git a/backend/requirements.txt b/backend/requirements.txt index ece7611e0ad1ac1f008c26d4062ef4405cd051a5..1dda98ecf626ead187fda342c7de5efb4493dc2c 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,14 +1,15 @@ -fastapi>=0.110.0 -uvicorn[standard]>=0.27.0 -openai>=1.12.0 -pydantic>=2.6.0 -pydantic-settings>=2.2.0 -python-dotenv>=1.0.0 -websockets>=12.0 -httpx>=0.27.0 -numpy>=1.26.0 -aiofiles>=23.2.1 -python-multipart>=0.0.9 -paramiko>=3.4.0 -psutil>=5.9.0 -openenv-core>=0.2.0 +fastapi>=0.110.0 +uvicorn[standard]>=0.27.0 +openai>=1.12.0 +pydantic>=2.6.0 +pydantic-settings>=2.2.0 +python-dotenv>=1.0.0 +websockets>=12.0 +httpx>=0.27.0 +numpy>=1.26.0 +sentence-transformers>=2.6.0 +torch>=2.2.0 +aiofiles>=23.2.1 +python-multipart>=0.0.9 +paramiko>=3.4.0 +psutil>=5.9.0 diff --git a/backend/scenarios/data/easy/software-incident.json b/backend/scenarios/data/easy/software-incident.json deleted file mode 100644 index 855fc54b08ba0dcd482116606c9fd806cf104e87..0000000000000000000000000000000000000000 --- a/backend/scenarios/data/easy/software-incident.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "id": "software-incident", - "title": "Nginx Rate Limit Investigation", - "difficulty": "easy", - "domain": "DevOps", - "description": "Users are reporting 503 errors when accessing the main API. Initial reports suggest a misconfigured rate limit.", - "context": "The system uses Nginx as a reverse proxy. A recent change might have throttled legitimate traffic.", - "symptoms": [ - "HTTP 503 errors", - "High latency for API calls" - ], - "available_services": [ - "nginx-proxy", - "api-gateway" - ], - "initial_state": { - "nginx-proxy": { - "status": "running", - "rate_limit": "10", - "last_reload": "2 hours ago" - } - }, - "root_cause": { - "service": "nginx-proxy", - "description": "Nginx rate limit was set too low (10 requests/sec) during a maintenance window." - }, - "grading_criteria": { - "nginx_rate_limit_fixed": 0.49, - "nginx_restarted": 0.20, - "fix_verified": 0.20, - "efficiency_bonus": 0.09 - } -} \ No newline at end of file diff --git a/backend/scenarios/data/hard/cascade-system-failure.json b/backend/scenarios/data/hard/cascade-system-failure.json deleted file mode 100644 index e69974c84ec19768249a108203c8b936dbede5d0..0000000000000000000000000000000000000000 --- a/backend/scenarios/data/hard/cascade-system-failure.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "id": "cascade-system-failure", - "title": "Postgres Connection Exhaustion", - "difficulty": "hard", - "domain": "Database", - "description": "A cascade failure is occurring across the cluster. Database connections are being exhausted by a long-running analytics query.", - "context": "The analytics service might be the culprit. A red herring points to the disk backup agent.", - "symptoms": [ - "FATAL: too many connections", - "Application timeout", - "High I/O wait" - ], - "available_services": [ - "postgres-db", - "disk-backup-agent", - "analytics-service" - ], - "initial_state": { - "postgres-db": { - "status": "running", - "max_connections": "20", - "long_running_query": "SELECT * FROM large_audit_table CROSS JOIN high_res_metrics", - "query_timeout_analytics": "0" - }, - "disk-backup-agent": { - "status": "degraded", - "disk_scan_active": "true" - } - }, - "root_cause": { - "service": "postgres-db", - "description": "A cross-join query in the analytics service is locking connections, coupled with a low max_connections limit." - }, - "grading_criteria": { - "postgres_query_terminated": 0.25, - "postgres_max_connections_increased": 0.20, - "postgres_query_timeout_set": 0.20, - "penalty_disk_backup_agent_modified": -0.15, - "fix_verified": 0.10, - "efficiency_bonus": 0.05 - } -} \ No newline at end of file diff --git a/backend/scenarios/data/medium/business-process-failure.json b/backend/scenarios/data/medium/business-process-failure.json deleted file mode 100644 index 3244f5d590dcbd3b2e1738c89599e3f3eb230c90..0000000000000000000000000000000000000000 --- a/backend/scenarios/data/medium/business-process-failure.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "id": "business-process-failure", - "title": "Inventory Stockout Loop", - "difficulty": "medium", - "domain": "E-Commerce", - "description": "The inventory service is failing to trigger restocking orders even when stock is zero.", - "context": "The inventory logic depends on a minimum stock threshold. A red herring might point to the CDN edge node.", - "symptoms": [ - "Stockouts", - "Orders stuck in 'PENDING_STOCK'" - ], - "available_services": [ - "inventory-service", - "cdn-edge-node", - "order-processor" - ], - "initial_state": { - "inventory-service": { - "status": "running", - "minimum_stock_threshold": "50", - "last_reload": "1 day ago" - }, - "cdn-edge-node": { - "status": "running", - "cache_expiry": "3600s" - } - }, - "root_cause": { - "service": "inventory-service", - "description": "Minimum stock threshold was accidentally hardcoded to a high value, preventing restocking." - }, - "grading_criteria": { - "inventory_threshold_fixed": 0.45, - "inventory_restarted": 0.10, - "penalty_cdn_edge_node_modified": -0.15, - "fix_verified": 0.20, - "efficiency_bonus": 0.10 - } -} \ No newline at end of file diff --git a/backend/scenarios/graders/__init__.py b/backend/scenarios/graders/__init__.py index d17a553674f552fc9c6607723f5b8e260064f93c..0412fb9be1afcfed97ff320151e56f84c7b11556 100644 --- a/backend/scenarios/graders/__init__.py +++ b/backend/scenarios/graders/__init__.py @@ -1 +1 @@ -# Init for graders +# Init for graders diff --git a/backend/scenarios/graders/base_grader.py b/backend/scenarios/graders/base_grader.py index a8b040503d8d9f3c13a31013eecd7a4d6bb21316..caa6307fdcdfad96f5713cd8dbe67a10d63fa802 100644 --- a/backend/scenarios/graders/base_grader.py +++ b/backend/scenarios/graders/base_grader.py @@ -1,17 +1,7 @@ class BaseGrader: - def _clamp(self, score: float) -> float: - """ - Clamp score to be strictly between 0 and 1 (not exactly 0 or 1) - """ - if score <= 0.0: - return 0.001 - elif score >= 1.0: - return 0.999 - return round(score, 4) - def grade(self, episode_state, scenario: dict) -> float: """ - Returns score strictly between 0 and 1 + Returns 0.0 to 1.0 Must be deterministic โ€” same inputs always same output """ raise NotImplementedError("Subclasses must implement the grade method") diff --git a/backend/scenarios/graders/easy_grader.py b/backend/scenarios/graders/easy_grader.py index 241aa28c01ffabbd6193fa5068b99c8dd0375d52..56d1259539c75bea41f8bd851607da613726c618 100644 --- a/backend/scenarios/graders/easy_grader.py +++ b/backend/scenarios/graders/easy_grader.py @@ -26,4 +26,4 @@ class EasyGrader(BaseGrader): if steps_ratio <= 0.6 and episode_state.fix_verified and str(rate_limit) == "1000": score += criteria.get('efficiency_bonus', 0.10) - return self._clamp(score) + return max(0.0, min(1.0, round(score, 4))) diff --git a/backend/scenarios/graders/hard_grader.py b/backend/scenarios/graders/hard_grader.py index 5b5d941a9e5c1e452843ac9865afb0f068dbf010..a034a20615bab0fb8c2d2ade6aca67bcf808c8ac 100644 --- a/backend/scenarios/graders/hard_grader.py +++ b/backend/scenarios/graders/hard_grader.py @@ -51,4 +51,4 @@ class HardGrader(BaseGrader): if steps_ratio <= 0.6 and episode_state.fix_verified and q_val in ["none", "null", ""]: score += criteria.get('efficiency_bonus', 0.05) - return self._clamp(score) + return max(0.0, min(1.0, round(score, 4))) diff --git a/backend/scenarios/graders/medium_grader.py b/backend/scenarios/graders/medium_grader.py index 68c431645b4d7b2e360d6e345c9a1aef7f7701d7..2c12e63ae2c54cd642de1ec6a772391aad8f12d8 100644 --- a/backend/scenarios/graders/medium_grader.py +++ b/backend/scenarios/graders/medium_grader.py @@ -42,4 +42,4 @@ class MediumGrader(BaseGrader): if steps_ratio <= 0.6 and episode_state.fix_verified and str(threshold) == "0": score += criteria.get('efficiency_bonus', 0.10) - return self._clamp(score) + return max(0.0, min(1.0, round(score, 4))) diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py deleted file mode 100644 index 9ae0cc04ec19f98a8c13f14ebc748859a7c0763b..0000000000000000000000000000000000000000 --- a/backend/tests/conftest.py +++ /dev/null @@ -1,4 +0,0 @@ -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).parent.parent / "backend")) diff --git a/backend/tools/tool_registry.py b/backend/tools/tool_registry.py index 065d12cca29987fcdbedbe4d5b5ba8fe5e29dc30..7bfcfeacf17073b51f517c44e564a2462b115c3f 100644 --- a/backend/tools/tool_registry.py +++ b/backend/tools/tool_registry.py @@ -11,7 +11,6 @@ from .tools.submit_resolution import tool_submit_resolution from .tools.run_terminal import tool_run_terminal_command from .tools.update_config import tool_update_config from .tools.restart_service import tool_restart_service -from .tools.fix_verifier import tool_verify_fix class ToolRegistry: def __init__(self): @@ -29,7 +28,6 @@ class ToolRegistry: self.register_tool("run_terminal_command", tool_run_terminal_command) self.register_tool("update_config", tool_update_config) self.register_tool("restart_service", tool_restart_service) - self.register_tool("verify_fix", tool_verify_fix) def register_tool(self, name: str, func: Callable): self.tools[name] = func diff --git a/backend/utils/embeddings.py b/backend/utils/embeddings.py index cf00095686f2d6ff24e82def65c1ed97c451a302..4788deb384aa5b7a36ee1d7dad1266b0b2923725 100644 --- a/backend/utils/embeddings.py +++ b/backend/utils/embeddings.py @@ -1,33 +1,26 @@ -import httpx -from typing import List -from functools import lru_cache - -@lru_cache(maxsize=256) -def get_embedding(text: str) -> List[float]: - """Get embedding vector using Ollama directly (Synchronous)""" - try: - response = httpx.post("http://localhost:11434/api/embeddings", json={ - "model": "all-minilm", - "prompt": text - }, timeout=60.0) - return response.json().get("embedding", []) - except Exception as e: - import logging - logging.error(f"Embedding failed: {e}. Using pseudo-embedding fallback.") - import re - import hashlib - words = re.findall(r'\w+', text.lower()) - vec = [0.0] * 384 - for w in words: - idx = int(hashlib.md5(w.encode()).hexdigest(), 16) % 384 - vec[idx] += 1.0 - return vec - -def cos_sim(a: List[float], b: List[float]) -> float: - """Cosine similarity without PyTorch/Numpy dependencies""" - if not a or not b: return 0.0 - dot_product = sum(x * y for x, y in zip(a, b)) - mag_a = sum(x * x for x in a) ** 0.5 - mag_b = sum(x * x for x in b) ** 0.5 - if mag_a == 0 or mag_b == 0: return 0.0 - return dot_product / (mag_a * mag_b) +import httpx +from typing import List +from functools import lru_cache + +@lru_cache(maxsize=256) +def get_embedding(text: str) -> List[float]: + """Get embedding vector using Ollama directly (Synchronous)""" + try: + response = httpx.post("http://localhost:11434/api/embeddings", json={ + "model": "all-minilm", + "prompt": text + }, timeout=60.0) + return response.json().get("embedding", []) + except Exception as e: + import logging + logging.error(f"Embedding failed: {e}") + return [] + +def cos_sim(a: List[float], b: List[float]) -> float: + """Cosine similarity without PyTorch/Numpy dependencies""" + if not a or not b: return 0.0 + dot_product = sum(x * y for x, y in zip(a, b)) + mag_a = sum(x * x for x in a) ** 0.5 + mag_b = sum(x * x for x in b) ** 0.5 + if mag_a == 0 or mag_b == 0: return 0.0 + return dot_product / (mag_a * mag_b) diff --git a/default.env b/default.env index 09d5f35495868a898792b97211b3cf5f1295019f..74adca9a6c9fec93f7be46659855a9b0963eced1 100644 --- a/default.env +++ b/default.env @@ -1,54 +1,67 @@ -# NEXUS Backend โ€” Environment Configuration - -# OLLAMA (for local development only) -OLLAMA_BASE_URL=http://localhost:11434/v1 -OLLAMA_API_KEY=ollama - -# HUGGINGFACE INFERENCE (PRIMARY - free tier) -HF_TOKEN= -HF_INFERENCE_URL=https://router.huggingface.co/v1 - -# OPENAI (optional - requires paid account) -OPENAI_API_KEY= -OPENAI_BASE_URL=https://api.openai.com/v1 - -# AGENTS - HuggingFace models (work with HF Inference API) -# Supports agents a through j (10 agents max via env vars) -# Additional agents can be configured via AGENTS_JSON env var -AGENT_A_MODEL=meta-llama/Llama-3.1-8B-Instruct -AGENT_B_MODEL=meta-llama/Llama-3.2-1B-Instruct -AGENT_C_MODEL= -AGENT_D_MODEL= -AGENT_A_PROVIDER=hf -AGENT_B_PROVIDER=hf -AGENT_C_PROVIDER=hf -AGENT_D_PROVIDER=hf -AGENT_A_ROLE=INVESTIGATOR -AGENT_B_ROLE=VALIDATOR -AGENT_C_ROLE=FORENSIC_ANALYST -AGENT_D_ROLE=NETWORK_ENGINEER -AGENT_A_TEMPERATURE=0.7 -AGENT_B_TEMPERATURE=0.6 -AGENT_C_TEMPERATURE=0.5 -AGENT_D_TEMPERATURE=0.5 -AGENT_A_MAX_TOKENS=512 -AGENT_B_MAX_TOKENS=512 -AGENT_C_MAX_TOKENS=512 -AGENT_D_MAX_TOKENS=512 - -# EXECUTION ENVIRONMENT -EXECUTION_MODE=simulated -ENVIRONMENT=production - -# SERVER -HOST=0.0.0.0 -PORT=7860 -DEBUG=false - -# EPISODE SETTINGS -MAX_STEPS=8 -SUCCESS_SCORE_THRESHOLD=0.5 - -# INFERENCE SCRIPT (for competition submission) -API_BASE_URL=https://router.huggingface.co/v1 -MODEL_NAME=meta-llama/Llama-3.1-8B-Instruct +# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +# NEXUS Backend โ€” Environment Configuration +# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +# โ”€โ”€ COMPETITION REQUIRED VARIABLES โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# These three vars are read by inference.py and evaluated by the competition. +# The OpenAI client is used for ALL LLM calls โ€” it works with any compatible API. + +# Option A โ€” HuggingFace Inference (use this on HF Space / cloud deployment) +API_BASE_URL=https://router.huggingface.co/hf-inference/v1 +MODEL_NAME= +HF_TOKEN=your_huggingface_token_here + +# Option B โ€” Local Ollama (use this for local dev and testing) +# API_BASE_URL=http://localhost:11434/v1 +# MODEL_NAME= +# HF_TOKEN=ollama + +# โ”€โ”€ OLLAMA LOCAL SERVER โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +OLLAMA_BASE_URL=http://localhost:11434/v1 +OLLAMA_API_KEY=ollama + +# โ”€โ”€ AGENT DEFAULTS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +AGENT_A_PROVIDER=ollama +AGENT_B_PROVIDER=ollama +AGENT_A_MODEL= +AGENT_B_MODEL= +AGENT_A_TEMPERATURE=0.8 +AGENT_B_TEMPERATURE=0.6 +AGENT_A_MAX_TOKENS=300 +AGENT_B_MAX_TOKENS=300 + +# โ”€โ”€ HUGGINGFACE (for HF Space deployment) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# When deploying to HF Space, uncomment and fill in: +# HF_INFERENCE_URL=https://api-inference.huggingface.co/v1 +# HF_AGENT_A_MODEL= +# HF_AGENT_B_MODEL= + +# โ”€โ”€ SERVER โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +HOST=0.0.0.0 +PORT=7860 +DEBUG=true +ENVIRONMENT=local + +# โ”€โ”€ EPISODE SETTINGS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +MAX_STEPS=8 +MAX_EPISODE_TIME_SECONDS=1200 +SUCCESS_SCORE_THRESHOLD=0.5 + +# โ”€โ”€ MCP TOOL SERVER โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +MCP_SERVER_PORT=8001 +MCP_SERVER_URL=http://localhost:8001 + +# โ”€โ”€ CUSTOM MODEL (user-provided override) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +CUSTOM_MODEL_ENABLED=false +CUSTOM_MODEL_BASE_URL= +CUSTOM_MODEL_API_KEY= +CUSTOM_MODEL_NAME= +CUSTOM_MODEL_AGENT= + +# โ”€โ”€ EXECUTION ENVIRONMENT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Set to 'simulated' (default) or 'ssh' to use a real SSH Lab Node +EXECUTION_MODE=simulated +SSH_HOST= +SSH_PORT=22 +SSH_USER= +SSH_PASSWORD= diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..332f1f7145d1edc813b8f17b4c4bf0ba1be2944f --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,39 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# InsForge & AI agent skills +.insforge +.agent +.agents +.augment +.claude +.cline +.github/copilot* +.kilocode +.qoder +.qwen +.roo +.trae +.windsurf diff --git a/frontend/dist/assets/index-CpY48GhO.js b/frontend/dist/assets/index-CpY48GhO.js deleted file mode 100644 index 559b3506516d6f03a705ca6f20f78fdd952b789d..0000000000000000000000000000000000000000 --- a/frontend/dist/assets/index-CpY48GhO.js +++ /dev/null @@ -1,73 +0,0 @@ -var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),s=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},c=(n,r,a)=>(a=n==null?{}:e(i(n)),s(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var l=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.consumer`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.for(`react.activity`),p=Symbol.iterator;function m(e){return typeof e!=`object`||!e?null:(e=p&&e[p]||e[`@@iterator`],typeof e==`function`?e:null)}var h={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},g=Object.assign,_={};function v(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}v.prototype.isReactComponent={},v.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},v.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function y(){}y.prototype=v.prototype;function b(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}var x=b.prototype=new y;x.constructor=b,g(x,v.prototype),x.isPureReactComponent=!0;var ee=Array.isArray;function S(){}var C={H:null,A:null,T:null,S:null},te=Object.prototype.hasOwnProperty;function w(e,n,r){var i=r.ref;return{$$typeof:t,type:e,key:n,ref:i===void 0?null:i,props:r}}function T(e,t){return w(e.type,t,e.props)}function ne(e){return typeof e==`object`&&!!e&&e.$$typeof===t}function re(e){var t={"=":`=0`,":":`=2`};return`$`+e.replace(/[=:]/g,function(e){return t[e]})}var ie=/\/+/g;function ae(e,t){return typeof e==`object`&&e&&e.key!=null?re(``+e.key):t.toString(36)}function oe(e){switch(e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason;default:switch(typeof e.status==`string`?e.then(S,S):(e.status=`pending`,e.then(function(t){e.status===`pending`&&(e.status=`fulfilled`,e.value=t)},function(t){e.status===`pending`&&(e.status=`rejected`,e.reason=t)})),e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason}}throw e}function se(e,r,i,a,o){var s=typeof e;(s===`undefined`||s===`boolean`)&&(e=null);var c=!1;if(e===null)c=!0;else switch(s){case`bigint`:case`string`:case`number`:c=!0;break;case`object`:switch(e.$$typeof){case t:case n:c=!0;break;case d:return c=e._init,se(c(e._payload),r,i,a,o)}}if(c)return o=o(e),c=a===``?`.`+ae(e,0):a,ee(o)?(i=``,c!=null&&(i=c.replace(ie,`$&/`)+`/`),se(o,r,i,``,function(e){return e})):o!=null&&(ne(o)&&(o=T(o,i+(o.key==null||e&&e.key===o.key?``:(``+o.key).replace(ie,`$&/`)+`/`)+c)),r.push(o)),1;c=0;var l=a===``?`.`:a+`:`;if(ee(e))for(var u=0;u{t.exports=l()})),d=o((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0>>1,a=e[r];if(0>>1;ri(c,n))li(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(li(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(e.unstable_now=void 0,typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=!1,_=typeof setTimeout==`function`?setTimeout:null,v=typeof clearTimeout==`function`?clearTimeout:null,y=typeof setImmediate<`u`?setImmediate:null;function b(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function x(e){if(h=!1,b(e),!m)if(n(c)!==null)m=!0,ee||(ee=!0,ne());else{var t=n(l);t!==null&&ae(x,t.startTime-e)}}var ee=!1,S=-1,C=5,te=-1;function w(){return g?!0:!(e.unstable_now()-tet&&w());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=t);if(t=e.unstable_now(),typeof s==`function`){d.callback=s,b(t),i=!0;break b}d===n(c)&&r(c),b(t)}else r(c);d=n(c)}if(d!==null)i=!0;else{var u=n(l);u!==null&&ae(x,u.startTime-t),i=!1}}break a}finally{d=null,f=a,p=!1}i=void 0}}finally{i?ne():ee=!1}}}var ne;if(typeof y==`function`)ne=function(){y(T)};else if(typeof MessageChannel<`u`){var re=new MessageChannel,ie=re.port2;re.port1.onmessage=T,ne=function(){ie.postMessage(null)}}else ne=function(){_(T,0)};function ae(t,n){S=_(function(){t(e.unstable_now())},n)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(e){e.callback=null},e.unstable_forceFrameRate=function(e){0>e||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(v(S),S=-1):h=!0,ae(x,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,ee||(ee=!0,ne()))),r},e.unstable_shouldYield=w,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),f=o(((e,t)=>{t.exports=d()})),p=o((e=>{var t=u();function n(e){var t=`https://react.dev/errors/`+e;if(1{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=p()})),h=o((e=>{var t=f(),n=u(),r=m();function i(e){var t=`https://react.dev/errors/`+e;if(1fe||(e.current=de[fe],de[fe]=null,fe--)}function k(e,t){fe++,de[fe]=e.current,e.current=t}var me=pe(null),he=pe(null),ge=pe(null),_e=pe(null);function ve(e,t){switch(k(ge,t),k(he,e),k(me,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Vd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Vd(t),e=Hd(t,e);else switch(e){case`svg`:e=1;break;case`math`:e=2;break;default:e=0}}O(me),k(me,e)}function ye(){O(me),O(he),O(ge)}function be(e){e.memoizedState!==null&&k(_e,e);var t=me.current,n=Hd(t,e.type);t!==n&&(k(he,e),k(me,n))}function xe(e){he.current===e&&(O(me),O(he)),_e.current===e&&(O(_e),Qf._currentValue=ue)}var Se,Ce;function we(e){if(Se===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);Se=t&&t[1]||``,Ce=-1)`:-1i||c[r]!==l[i]){var u=` -`+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(``)&&(u=u.replace(``,e.displayName)),u}while(1<=r&&0<=i);break}}}finally{Te=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?we(n):``}function De(e,t){switch(e.tag){case 26:case 27:case 5:return we(e.type);case 16:return we(`Lazy`);case 13:return e.child!==t&&t!==null?we(`Suspense Fallback`):we(`Suspense`);case 19:return we(`SuspenseList`);case 0:case 15:return Ee(e.type,!1);case 11:return Ee(e.type.render,!1);case 1:return Ee(e.type,!0);case 31:return we(`Activity`);default:return``}}function Oe(e){try{var t=``,n=null;do t+=De(e,n),n=e,e=e.return;while(e);return t}catch(e){return` -Error generating stack: `+e.message+` -`+e.stack}}var ke=Object.prototype.hasOwnProperty,Ae=t.unstable_scheduleCallback,je=t.unstable_cancelCallback,Me=t.unstable_shouldYield,Ne=t.unstable_requestPaint,Pe=t.unstable_now,Fe=t.unstable_getCurrentPriorityLevel,Ie=t.unstable_ImmediatePriority,Le=t.unstable_UserBlockingPriority,Re=t.unstable_NormalPriority,ze=t.unstable_LowPriority,Be=t.unstable_IdlePriority,Ve=t.log,He=t.unstable_setDisableYieldValue,Ue=null,We=null;function Ge(e){if(typeof Ve==`function`&&He(e),We&&typeof We.setStrictMode==`function`)try{We.setStrictMode(Ue,e)}catch{}}var Ke=Math.clz32?Math.clz32:Ye,qe=Math.log,Je=Math.LN2;function Ye(e){return e>>>=0,e===0?32:31-(qe(e)/Je|0)|0}var Xe=256,Ze=262144,Qe=4194304;function $e(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function et(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=$e(n))):i=$e(o):i=$e(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=$e(n))):i=$e(o)):i=$e(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function tt(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function nt(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function rt(){var e=Qe;return Qe<<=1,!(Qe&62914560)&&(Qe=4194304),e}function it(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function at(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function ot(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0`u`||window.document===void 0||window.document.createElement===void 0),vn=!1;if(_n)try{var yn={};Object.defineProperty(yn,`passive`,{get:function(){vn=!0}}),window.addEventListener(`test`,yn,yn),window.removeEventListener(`test`,yn,yn)}catch{vn=!1}var bn=null,xn=null,Sn=null;function Cn(){if(Sn)return Sn;var e,t=xn,n=t.length,r,i=`value`in bn?bn.value:bn.textContent,a=i.length;for(e=0;e=er),rr=` `,ir=!1;function ar(e,t){switch(e){case`keyup`:return Qn.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function or(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var sr=!1;function cr(e,t){switch(e){case`compositionend`:return or(t);case`keypress`:return t.which===32?(ir=!0,rr):null;case`textInput`:return e=t.data,e===rr&&ir?null:e;default:return null}}function lr(e,t){if(sr)return e===`compositionend`||!$n&&ar(e,t)?(e=Cn(),Sn=xn=bn=null,sr=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=jr(n)}}function Nr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Nr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Pr(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Wt(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=Wt(e.document)}return t}function Fr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}var Ir=_n&&`documentMode`in document&&11>=document.documentMode,Lr=null,Rr=null,zr=null,Br=!1;function Vr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Br||Lr==null||Lr!==Wt(r)||(r=Lr,`selectionStart`in r&&Fr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),zr&&Ar(zr,r)||(zr=r,r=Ed(Rr,`onSelect`),0>=o,i-=o,Ni=1<<32-Ke(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(i,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(i,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(i,d),N&&Fi(i,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),N&&Fi(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return N&&Fi(a,g),u}for(h=r(h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),N&&Fi(a,g),u}function b(e,r,o,c){if(typeof o==`object`&&o&&o.type===y&&o.key===null&&(o=o.props.children),typeof o==`object`&&o){switch(o.$$typeof){case _:a:{for(var l=o.key;r!==null;){if(r.key===l){if(l=o.type,l===y){if(r.tag===7){n(e,r.sibling),c=a(r,o.props.children),c.return=e,e=c;break a}}else if(r.elementType===l||typeof l==`object`&&l&&l.$$typeof===ne&&Na(l)===r.type){n(e,r.sibling),c=a(r,o.props),Ba(c,o),c.return=e,e=c;break a}n(e,r);break}else t(e,r);r=r.sibling}o.type===y?(c=bi(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=yi(o.type,o.key,o.props,null,e.mode,c),Ba(c,o),c.return=e,e=c)}return s(e);case v:a:{for(l=o.key;r!==null;){if(r.key===l)if(r.tag===4&&r.stateNode.containerInfo===o.containerInfo&&r.stateNode.implementation===o.implementation){n(e,r.sibling),c=a(r,o.children||[]),c.return=e,e=c;break a}else{n(e,r);break}else t(e,r);r=r.sibling}c=Ci(o,e.mode,c),c.return=e,e=c}return s(e);case ne:return o=Na(o),b(e,r,o,c)}if(le(o))return h(e,r,o,c);if(oe(o)){if(l=oe(o),typeof l!=`function`)throw Error(i(150));return o=l.call(o),g(e,r,o,c)}if(typeof o.then==`function`)return b(e,r,za(o),c);if(o.$$typeof===S)return b(e,r,ca(e,o),c);Va(e,o)}return typeof o==`string`&&o!==``||typeof o==`number`||typeof o==`bigint`?(o=``+o,r!==null&&r.tag===6?(n(e,r.sibling),c=a(r,o),c.return=e,e=c):(n(e,r),c=xi(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{Ra=0;var i=b(e,t,n,r);return La=null,i}catch(t){if(t===Da||t===ka)throw t;var a=hi(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var Ua=Ha(!0),Wa=Ha(!1),Ga=!1;function Ka(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function qa(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Ja(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Ya(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,G&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=fi(e),di(e,null,n),t}return ci(e,r,t,n),fi(e)}function Xa(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194048)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,ct(e,n)}}function Za(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,callbacks:r.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var Qa=!1;function $a(){if(Qa){var e=va;if(e!==null)throw e}}function eo(e,t,n,r){Qa=!1;var i=e.updateQueue;Ga=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane&-536870913,p=f!==s.lane;if(p?(J&f)===f:(r&f)===f){f!==0&&f===_a&&(Qa=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var m=e,g=s;f=t;var _=n;switch(g.tag){case 1:if(m=g.payload,typeof m==`function`){d=m.call(_,d,f);break a}d=m;break a;case 3:m.flags=m.flags&-65537|128;case 0:if(m=g.payload,f=typeof m==`function`?m.call(_,d,f):m,f==null)break a;d=h({},d,f);break a;case 2:Ga=!0}}f=s.callback,f!==null&&(e.flags|=64,p&&(e.flags|=8192),p=i.callbacks,p===null?i.callbacks=[f]:p.push(f))}else p={lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;p=s,s=p.next,p.next=null,i.lastBaseUpdate=p,i.shared.pending=null}}while(1);u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,a===null&&(i.shared.lanes=0),Gl|=o,e.lanes=o,e.memoizedState=d}}function to(e,t){if(typeof e!=`function`)throw Error(i(191,e));e.call(t)}function no(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ea?a:8;var o=E.T,s={};E.T=s,Ls(e,!1,t,n);try{var c=i(),l=E.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?Is(e,t,xa(c,r),pu(e)):Is(e,t,r,pu(e))}catch(n){Is(e,t,{then:function(){},status:`rejected`,reason:n},pu())}finally{D.p=a,o!==null&&s.types!==null&&(o.types=s.types),E.T=o}}function Es(){}function Ds(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=Os(e).queue;Ts(e,a,t,ue,n===null?Es:function(){return ks(e),n(r)})}function Os(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:ue,baseState:ue,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Ro,lastRenderedState:ue},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Ro,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function ks(e){var t=Os(e);t.next===null&&(t=e.alternate.memoizedState),Is(e,t.next.queue,{},pu())}function As(){return sa(Qf)}function js(){return B().memoizedState}function Ms(){return B().memoizedState}function Ns(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=pu();e=Ja(n);var r=Ya(t,e,n);r!==null&&(hu(r,t,n),Xa(r,t,n)),t={cache:pa()},e.payload=t;return}t=t.return}}function Ps(e,t,n){var r=pu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},Rs(e)?zs(t,n):(n=li(e,t,n,r),n!==null&&(hu(n,e,r),Bs(n,t,r)))}function Fs(e,t,n){Is(e,t,n,pu())}function Is(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(Rs(e))zs(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,kr(s,o))return ci(e,t,i,0),K===null&&si(),!1}catch{}if(n=li(e,t,i,r),n!==null)return hu(n,e,r),Bs(n,t,r),!0}return!1}function Ls(e,t,n,r){if(r={lane:2,revertLane:dd(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},Rs(e)){if(t)throw Error(i(479))}else t=li(e,n,r,2),t!==null&&hu(t,e,2)}function Rs(e){var t=e.alternate;return e===I||t!==null&&t===I}function zs(e,t){yo=vo=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Bs(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,ct(e,n)}}var Vs={readContext:sa,use:Io,useCallback:z,useContext:z,useEffect:z,useImperativeHandle:z,useLayoutEffect:z,useInsertionEffect:z,useMemo:z,useReducer:z,useRef:z,useState:z,useDebugValue:z,useDeferredValue:z,useTransition:z,useSyncExternalStore:z,useId:z,useHostTransitionStatus:z,useFormState:z,useActionState:z,useOptimistic:z,useMemoCache:z,useCacheRefresh:z};Vs.useEffectEvent=z;var Hs={readContext:sa,use:Io,useCallback:function(e,t){return No().memoizedState=[e,t===void 0?null:t],e},useContext:sa,useEffect:fs,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),us(4194308,4,vs.bind(null,t,e),n)},useLayoutEffect:function(e,t){return us(4194308,4,e,t)},useInsertionEffect:function(e,t){us(4,2,e,t)},useMemo:function(e,t){var n=No();t=t===void 0?null:t;var r=e();if(bo){Ge(!0);try{e()}finally{Ge(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=No();if(n!==void 0){var i=n(t);if(bo){Ge(!0);try{n(t)}finally{Ge(!1)}}}else i=t;return r.memoizedState=r.baseState=i,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:i},r.queue=e,e=e.dispatch=Ps.bind(null,I,e),[r.memoizedState,e]},useRef:function(e){var t=No();return e={current:e},t.memoizedState=e},useState:function(e){e=Jo(e);var t=e.queue,n=Fs.bind(null,I,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:bs,useDeferredValue:function(e,t){return Cs(No(),e,t)},useTransition:function(){var e=Jo(!1);return e=Ts.bind(null,I,e.queue,!0,!1),No().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=I,a=No();if(N){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),K===null)throw Error(i(349));J&127||Uo(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,fs(Go.bind(null,r,o,e),[e]),r.flags|=2048,cs(9,{destroy:void 0},Wo.bind(null,r,o,n,t),null),n},useId:function(){var e=No(),t=K.identifierPrefix;if(N){var n=Pi,r=Ni;n=(r&~(1<<32-Ke(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=xo++,0<\/script>`,o=o.removeChild(o.firstChild);break;case`select`:o=typeof r.is==`string`?s.createElement(`select`,{is:r.is}):s.createElement(`select`),r.multiple?o.multiple=!0:r.size&&(o.size=r.size);break;default:o=typeof r.is==`string`?s.createElement(a,{is:r.is}):s.createElement(a)}}o[ht]=t,o[gt]=r;a:for(s=t.child;s!==null;){if(s.tag===5||s.tag===6)o.appendChild(s.stateNode);else if(s.tag!==4&&s.tag!==27&&s.child!==null){s.child.return=s,s=s.child;continue}if(s===t)break a;for(;s.sibling===null;){if(s.return===null||s.return===t)break a;s=s.return}s.sibling.return=s.return,s=s.sibling}t.stateNode=o;a:switch(Pd(o,a,r),a){case`button`:case`input`:case`select`:case`textarea`:r=!!r.autoFocus;break a;case`img`:r=!0;break a;default:r=!1}r&&Fc(t)}}return H(t),Ic(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Fc(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=ge.current,qi(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=Bi,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[ht]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||Md(e.nodeValue,n)),e||Wi(t,!0)}else e=Bd(e).createTextNode(r),e[ht]=t,t.stateNode=e}return H(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=qi(t),n!==null){if(e===null){if(!r)throw Error(i(318));if(e=t.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(i(557));e[ht]=t}else Ji(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;H(t),e=!1}else n=Yi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(ho(t),t):(ho(t),null);if(t.flags&128)throw Error(i(558))}return H(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=qi(t),r!==null&&r.dehydrated!==null){if(e===null){if(!a)throw Error(i(318));if(a=t.memoizedState,a=a===null?null:a.dehydrated,!a)throw Error(i(317));a[ht]=t}else Ji(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;H(t),a=!1}else a=Yi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?(ho(t),t):(ho(t),null)}return ho(t),t.flags&128?(t.lanes=n,t):(n=r!==null,e=e!==null&&e.memoizedState!==null,n&&(r=t.child,a=null,r.alternate!==null&&r.alternate.memoizedState!==null&&r.alternate.memoizedState.cachePool!==null&&(a=r.alternate.memoizedState.cachePool.pool),o=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(o=r.memoizedState.cachePool.pool),o!==a&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),Rc(t,t.updateQueue),H(t),null);case 4:return ye(),e===null&&Sd(t.stateNode.containerInfo),H(t),null;case 10:return ta(t.type),H(t),null;case 19:if(O(F),r=t.memoizedState,r===null)return H(t),null;if(a=(t.flags&128)!=0,o=r.rendering,o===null)if(a)zc(r,!1);else{if(X!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=go(e),o!==null){for(t.flags|=128,zc(r,!1),e=o.updateQueue,t.updateQueue=e,Rc(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)vi(n,e),n=n.sibling;return k(F,F.current&1|2),N&&Fi(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&Pe()>tu&&(t.flags|=128,a=!0,zc(r,!1),t.lanes=4194304)}else{if(!a)if(e=go(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,Rc(t,e),zc(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!N)return H(t),null}else 2*Pe()-r.renderingStartTime>tu&&n!==536870912&&(t.flags|=128,a=!0,zc(r,!1),t.lanes=4194304);r.isBackwards?(o.sibling=t.child,t.child=o):(e=r.last,e===null?t.child=o:e.sibling=o,r.last=o)}return r.tail===null?(H(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=Pe(),e.sibling=null,n=F.current,k(F,a?n&1|2:n&1),N&&Fi(t,r.treeForkCount),e);case 22:case 23:return ho(t),so(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(H(t),t.subtreeFlags&6&&(t.flags|=8192)):H(t),n=t.updateQueue,n!==null&&Rc(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),r=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),e!==null&&O(Ca),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),ta(P),H(t),null;case 25:return null;case 30:return null}throw Error(i(156,t.tag))}function Vc(e,t){switch(Ri(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return ta(P),ye(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return xe(t),null;case 31:if(t.memoizedState!==null){if(ho(t),t.alternate===null)throw Error(i(340));Ji()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(ho(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));Ji()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return O(F),null;case 4:return ye(),null;case 10:return ta(t.type),null;case 22:case 23:return ho(t),so(),e!==null&&O(Ca),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return ta(P),null;case 25:return null;default:return null}}function Hc(e,t){switch(Ri(t),t.tag){case 3:ta(P),ye();break;case 26:case 27:case 5:xe(t);break;case 4:ye();break;case 31:t.memoizedState!==null&&ho(t);break;case 13:ho(t);break;case 19:O(F);break;case 10:ta(t.type);break;case 22:case 23:ho(t),so(),e!==null&&O(Ca);break;case 24:ta(P)}}function Uc(e,t){try{var n=t.updateQueue,r=n===null?null:n.lastEffect;if(r!==null){var i=r.next;n=i;do{if((n.tag&e)===e){r=void 0;var a=n.create,o=n.inst;r=a(),o.destroy=r}n=n.next}while(n!==i)}}catch(e){Z(t,t.return,e)}}function Wc(e,t,n){try{var r=t.updateQueue,i=r===null?null:r.lastEffect;if(i!==null){var a=i.next;r=a;do{if((r.tag&e)===e){var o=r.inst,s=o.destroy;if(s!==void 0){o.destroy=void 0,i=t;var c=n,l=s;try{l()}catch(e){Z(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Z(t,t.return,e)}}function Gc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{no(t,n)}catch(t){Z(e,e.return,t)}}}function Kc(e,t,n){n.props=Ys(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Z(e,t,n)}}function qc(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;case 30:r=e.stateNode;break;default:r=e.stateNode}typeof n==`function`?e.refCleanup=n(r):n.current=r}}catch(n){Z(e,t,n)}}function Jc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null)if(typeof r==`function`)try{r()}catch(n){Z(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n==`function`)try{n(null)}catch(n){Z(e,t,n)}else n.current=null}function Yc(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{a:switch(t){case`button`:case`input`:case`select`:case`textarea`:n.autoFocus&&r.focus();break a;case`img`:n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(t){Z(e,e.return,t)}}function Xc(e,t,n){try{var r=e.stateNode;Fd(r,e.type,n,t),r[gt]=t}catch(t){Z(e,e.return,t)}}function Zc(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&Zd(e.type)||e.tag===4}function Qc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Zc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&Zd(e.type)||e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function $c(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=cn));else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for($c(e,t,n),e=e.sibling;e!==null;)$c(e,t,n),e=e.sibling}function el(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode),e=e.child,e!==null))for(el(e,t,n),e=e.sibling;e!==null;)el(e,t,n),e=e.sibling}function tl(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Pd(t,r,n),t[ht]=e,t[gt]=n}catch(t){Z(e,e.return,t)}}var nl=!1,U=!1,rl=!1,il=typeof WeakSet==`function`?WeakSet:Set,al=null;function ol(e,t){if(e=e.containerInfo,Rd=sp,e=Pr(e),Fr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var a=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||r!==0&&f.nodeType!==3||(l=s+r),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===r&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(zd={focusedElem:e,selectionRange:n},sp=!1,al=t;al!==null;)if(t=al,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,al=e;else for(;al!==null;){switch(t=al,o=t.alternate,e=t.flags,t.tag){case 0:if(e&4&&(e=t.updateQueue,e=e===null?null:e.events,e!==null))for(n=0;n title`))),Pd(o,r,n),o[ht]=e,A(o),r=o;break a;case`link`:var s=Vf(`link`,`href`,a).get(r+(n.href||``));if(s){for(var c=0;cg&&(o=g,g=h,h=o);var _=Mr(s,h),v=Mr(s,g);if(_&&v&&(p.rangeCount!==1||p.anchorNode!==_.node||p.anchorOffset!==_.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var y=d.createRange();y.setStart(_.node,_.offset),p.removeAllRanges(),h>g?(p.addRange(y),p.extend(v.node,v.offset)):(y.setEnd(v.node,v.offset),p.addRange(y))}}}}for(d=[],p=s;p=p.parentNode;)p.nodeType===1&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof s.focus==`function`&&s.focus(),s=0;sn?32:n,E.T=null,n=lu,lu=null;var o=au,s=su;if(iu=0,ou=au=null,su=0,G&6)throw Error(i(331));var c=G;if(G|=4,Fl(o.current),Dl(o,o.current,s,n),G=c,id(0,!1),We&&typeof We.onPostCommitFiberRoot==`function`)try{We.onPostCommitFiberRoot(Ue,o)}catch{}return!0}finally{D.p=a,E.T=r,Vu(e,t)}}function Wu(e,t,n){t=Ti(n,t),t=tc(e.stateNode,t,2),e=Ya(e,t,2),e!==null&&(at(e,2),rd(e))}function Z(e,t,n){if(e.tag===3)Wu(e,e,n);else for(;t!==null;){if(t.tag===3){Wu(t,e,n);break}else if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(ru===null||!ru.has(r))){e=Ti(n,e),n=nc(2),r=Ya(t,n,2),r!==null&&(rc(n,r,t,e),at(r,2),rd(r));break}}t=t.return}}function Gu(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new zl;var i=new Set;r.set(t,i)}else i=r.get(t),i===void 0&&(i=new Set,r.set(t,i));i.has(n)||(Ul=!0,i.add(n),e=Ku.bind(null,e,t,n),t.then(e,e))}function Ku(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,K===e&&(J&n)===n&&(X===4||X===3&&(J&62914560)===J&&300>Pe()-$l?!(G&2)&&Su(e,0):ql|=n,Yl===J&&(Yl=0)),rd(e)}function qu(e,t){t===0&&(t=rt()),e=ui(e,t),e!==null&&(at(e,t),rd(e))}function Ju(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),qu(e,n)}function Yu(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(i(314))}r!==null&&r.delete(t),qu(e,n)}function Xu(e,t){return Ae(e,t)}var Zu=null,Qu=null,$u=!1,ed=!1,td=!1,nd=0;function rd(e){e!==Qu&&e.next===null&&(Qu===null?Zu=Qu=e:Qu=Qu.next=e),ed=!0,$u||($u=!0,ud())}function id(e,t){if(!td&&ed){td=!0;do for(var n=!1,r=Zu;r!==null;){if(!t)if(e!==0){var i=r.pendingLanes;if(i===0)var a=0;else{var o=r.suspendedLanes,s=r.pingedLanes;a=(1<<31-Ke(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,ld(r,a))}else a=J,a=et(r,r===K?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||tt(r,a)||(n=!0,ld(r,a));r=r.next}while(n);td=!1}}function ad(){od()}function od(){ed=$u=!1;var e=0;nd!==0&&Gd()&&(e=nd);for(var t=Pe(),n=null,r=Zu;r!==null;){var i=r.next,a=sd(r,t);a===0?(r.next=null,n===null?Zu=i:n.next=i,i===null&&(Qu=n)):(n=r,(e!==0||a&3)&&(ed=!0)),r=i}iu!==0&&iu!==5||id(e,!1),nd!==0&&(nd=0)}function sd(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,i=e.expirationTimes,a=e.pendingLanes&-62914561;0s)break;var u=c.transferSize,d=c.initiatorType;u&&Id(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function xf(e,t,n){var r=bf;if(r&&typeof t==`string`&&t){var i=Kt(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),hf.has(i)||(hf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Pd(t,`link`,e),A(t),r.head.appendChild(t)))}}function Sf(e){_f.D(e),xf(`dns-prefetch`,e,null)}function Cf(e,t){_f.C(e,t),xf(`preconnect`,e,t)}function wf(e,t,n){_f.L(e,t,n);var r=bf;if(r&&e&&t){var i=`link[rel="preload"][as="`+Kt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+Kt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+Kt(n.imageSizes)+`"]`)):i+=`[href="`+Kt(e)+`"]`;var a=i;switch(t){case`style`:a=Af(e);break;case`script`:a=Pf(e)}mf.has(a)||(e=h({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),mf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(jf(a))||t===`script`&&r.querySelector(Ff(a))||(t=r.createElement(`link`),Pd(t,`link`,e),A(t),r.head.appendChild(t)))}}function Tf(e,t){_f.m(e,t);var n=bf;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+Kt(r)+`"][href="`+Kt(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Pf(e)}if(!mf.has(a)&&(e=h({rel:`modulepreload`,href:e},t),mf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(Ff(a)))return}r=n.createElement(`link`),Pd(r,`link`,e),A(r),n.head.appendChild(r)}}}function Ef(e,t,n){_f.S(e,t,n);var r=bf;if(r&&e){var i=Dt(r).hoistableStyles,a=Af(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(jf(a)))s.loading=5;else{e=h({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=mf.get(a))&&Rf(e,n);var c=o=r.createElement(`link`);A(c),Pd(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Lf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function Df(e,t){_f.X(e,t);var n=bf;if(n&&e){var r=Dt(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=h({src:e,async:!0},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),A(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Of(e,t){_f.M(e,t);var n=bf;if(n&&e){var r=Dt(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=h({src:e,async:!0,type:`module`},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),A(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function kf(e,t,n,r){var a=(a=ge.current)?gf(a):null;if(!a)throw Error(i(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(t=Af(n.href),n=Dt(a).hoistableStyles,r=n.get(t),r||(r={type:`style`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=Af(n.href);var o=Dt(a).hoistableStyles,s=o.get(e);if(s||(a=a.ownerDocument||a,s={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},o.set(e,s),(o=a.querySelector(jf(e)))&&!o._p&&(s.instance=o,s.state.loading=5),mf.has(e)||(n={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},mf.set(e,n),o||Nf(a,e,n,s.state))),t&&r===null)throw Error(i(528,``));return s}if(t&&r!==null)throw Error(i(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(t=Pf(n),n=Dt(a).hoistableScripts,r=n.get(t),r||(r={type:`script`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(i(444,e))}}function Af(e){return`href="`+Kt(e)+`"`}function jf(e){return`link[rel="stylesheet"][`+e+`]`}function Mf(e){return h({},e,{"data-precedence":e.precedence,precedence:null})}function Nf(e,t,n,r){e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)?r.loading=1:(t=e.createElement(`link`),r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2}),Pd(t,`link`,n),A(t),e.head.appendChild(t))}function Pf(e){return`[src="`+Kt(e)+`"]`}function Ff(e){return`script[async]`+e}function If(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+Kt(n.href)+`"]`);if(r)return t.instance=r,A(r),r;var a=h({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),A(r),Pd(r,`style`,a),Lf(r,n.precedence,e),t.instance=r;case`stylesheet`:a=Af(n.href);var o=e.querySelector(jf(a));if(o)return t.state.loading|=4,t.instance=o,A(o),o;r=Mf(n),(a=mf.get(a))&&Rf(r,a),o=(e.ownerDocument||e).createElement(`link`),A(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Pd(o,`link`,r),t.state.loading|=4,Lf(o,n.precedence,e),t.instance=o;case`script`:return o=Pf(n.src),(a=e.querySelector(Ff(o)))?(t.instance=a,A(a),a):(r=n,(a=mf.get(o))&&(r=h({},n),zf(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),A(a),Pd(a,`link`,r),e.head.appendChild(a),t.instance=a);case`void`:return null;default:throw Error(i(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Lf(r,n.precedence,e));return t.instance}function Lf(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o title`):null)}function Uf(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case`meta`:case`title`:return!0;case`style`:if(typeof t.precedence!=`string`||typeof t.href!=`string`||t.href===``)break;return!0;case`link`:if(typeof t.rel!=`string`||typeof t.href!=`string`||t.href===``||t.onLoad||t.onError)break;switch(t.rel){case`stylesheet`:return e=t.disabled,typeof t.precedence==`string`&&e==null;default:return!0}case`script`:if(t.async&&typeof t.async!=`function`&&typeof t.async!=`symbol`&&!t.onLoad&&!t.onError&&t.src&&typeof t.src==`string`)return!0}return!1}function Wf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Gf(e,t,n,r){if(n.type===`stylesheet`&&(typeof r.media!=`string`||!1!==matchMedia(r.media).matches)&&!(n.state.loading&4)){if(n.instance===null){var i=Af(r.href),a=t.querySelector(jf(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=Jf.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,A(a);return}a=t.ownerDocument||t,r=Mf(r),(i=mf.get(i))&&Rf(r,i),a=a.createElement(`link`),A(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Pd(a,`link`,r),n.instance=a}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=Jf.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var Kf=0;function qf(e,t){return e.stylesheets&&e.count===0&&Xf(e,e.stylesheets),0Kf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function Jf(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Xf(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var Yf=null;function Xf(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,Yf=new Map,t.forEach(Zf,e),Yf=null,Jf.call(e))}function Zf(e,t){if(!(t.state.loading&4)){var n=Yf.get(e);if(n)var r=n.get(null);else{n=new Map,Yf.set(e,n);for(var i=e.querySelectorAll(`link[data-precedence],style[data-precedence]`),a=0;a{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=h()})),_=c(u(),1),v=g(),y=`modulepreload`,b=function(e,t){return new URL(e,t).href},x={},ee=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}r=o(t.map(t=>{if(t=b(t,n),t in x)return;x[t]=!0;let r=t.endsWith(`.css`),i=r?`[rel="stylesheet"]`:``;if(n)for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${t}"]${i}`))return;let o=document.createElement(`link`);if(o.rel=r?`stylesheet`:y,r||(o.as=`script`),o.crossOrigin=``,o.href=t,a&&o.setAttribute(`nonce`,a),document.head.appendChild(o),r)return new Promise((e,n)=>{o.addEventListener(`load`,e),o.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},S=`popstate`;function C(e){return typeof e==`object`&&!!e&&`pathname`in e&&`search`in e&&`hash`in e&&`state`in e&&`key`in e}function te(e={}){function t(e,t){let n=t.state?.masked,{pathname:r,search:i,hash:a}=n||e.location;return ie(``,{pathname:r,search:i,hash:a},t.state&&t.state.usr||null,t.state&&t.state.key||`default`,n?{pathname:e.location.pathname,search:e.location.search,hash:e.location.hash}:void 0)}function n(e,t){return typeof t==`string`?t:ae(t)}return se(t,n,null,e)}function w(e,t){if(e===!1||e==null)throw Error(t)}function T(e,t){if(!e){typeof console<`u`&&console.warn(t);try{throw Error(t)}catch{}}}function ne(){return Math.random().toString(36).substring(2,10)}function re(e,t){return{usr:e.state,key:e.key,idx:t,masked:e.unstable_mask?{pathname:e.pathname,search:e.search,hash:e.hash}:void 0}}function ie(e,t,n=null,r,i){return{pathname:typeof e==`string`?e:e.pathname,search:``,hash:``,...typeof t==`string`?oe(t):t,state:n,key:t&&t.key||r||ne(),unstable_mask:i}}function ae({pathname:e=`/`,search:t=``,hash:n=``}){return t&&t!==`?`&&(e+=t.charAt(0)===`?`?t:`?`+t),n&&n!==`#`&&(e+=n.charAt(0)===`#`?n:`#`+n),e}function oe(e){let t={};if(e){let n=e.indexOf(`#`);n>=0&&(t.hash=e.substring(n),e=e.substring(0,n));let r=e.indexOf(`?`);r>=0&&(t.search=e.substring(r),e=e.substring(0,r)),e&&(t.pathname=e)}return t}function se(e,t,n,r={}){let{window:i=document.defaultView,v5Compat:a=!1}=r,o=i.history,s=`POP`,c=null,l=u();l??(l=0,o.replaceState({...o.state,idx:l},``));function u(){return(o.state||{idx:null}).idx}function d(){s=`POP`;let e=u(),t=e==null?null:e-l;l=e,c&&c({action:s,location:h.location,delta:t})}function f(e,t){s=`PUSH`;let r=C(e)?e:ie(h.location,e,t);n&&n(r,e),l=u()+1;let d=re(r,l),f=h.createHref(r.unstable_mask||r);try{o.pushState(d,``,f)}catch(e){if(e instanceof DOMException&&e.name===`DataCloneError`)throw e;i.location.assign(f)}a&&c&&c({action:s,location:h.location,delta:1})}function p(e,t){s=`REPLACE`;let r=C(e)?e:ie(h.location,e,t);n&&n(r,e),l=u();let i=re(r,l),d=h.createHref(r.unstable_mask||r);o.replaceState(i,``,d),a&&c&&c({action:s,location:h.location,delta:0})}function m(e){return ce(e)}let h={get action(){return s},get location(){return e(i,o)},listen(e){if(c)throw Error(`A history only accepts one active listener`);return i.addEventListener(S,d),c=e,()=>{i.removeEventListener(S,d),c=null}},createHref(e){return t(i,e)},createURL:m,encodeLocation(e){let t=m(e);return{pathname:t.pathname,search:t.search,hash:t.hash}},push:f,replace:p,go(e){return o.go(e)}};return h}function ce(e,t=!1){let n=`http://localhost`;typeof window<`u`&&(n=window.location.origin===`null`?window.location.href:window.location.origin),w(n,`No window.location.(origin|href) available to create URL`);let r=typeof e==`string`?e:ae(e);return r=r.replace(/ $/,`%20`),!t&&r.startsWith(`//`)&&(r=n+r),new URL(r,n)}function le(e,t,n=`/`){return E(e,t,n,!1)}function E(e,t,n,r){let i=we((typeof t==`string`?oe(t):t).pathname||`/`,n);if(i==null)return null;let a=ue(e);fe(a);let o=null;for(let e=0;o==null&&e{let c={relativePath:s===void 0?e.path||``:s,caseSensitive:e.caseSensitive===!0,childrenIndex:a,route:e};if(c.relativePath.startsWith(`/`)){if(!c.relativePath.startsWith(r)&&o)return;w(c.relativePath.startsWith(r),`Absolute route path "${c.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),c.relativePath=c.relativePath.slice(r.length)}let l=Me([r,c.relativePath]),u=n.concat(c);e.children&&e.children.length>0&&(w(e.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${l}".`),ue(e.children,t,u,l,o)),!(e.path==null&&!e.index)&&t.push({path:l,score:ve(l,e.index),routesMeta:u})};return e.forEach((e,t)=>{if(e.path===``||!e.path?.includes(`?`))a(e,t);else for(let n of de(e.path))a(e,t,!0,n)}),t}function de(e){let t=e.split(`/`);if(t.length===0)return[];let[n,...r]=t,i=n.endsWith(`?`),a=n.replace(/\?$/,``);if(r.length===0)return i?[a,``]:[a];let o=de(r.join(`/`)),s=[];return s.push(...o.map(e=>e===``?a:[a,e].join(`/`))),i&&s.push(...o),s.map(t=>e.startsWith(`/`)&&t===``?`/`:t)}function fe(e){e.sort((e,t)=>e.score===t.score?ye(e.routesMeta.map(e=>e.childrenIndex),t.routesMeta.map(e=>e.childrenIndex)):t.score-e.score)}var pe=/^:[\w-]+$/,O=3,k=2,me=1,he=10,ge=-2,_e=e=>e===`*`;function ve(e,t){let n=e.split(`/`),r=n.length;return n.some(_e)&&(r+=ge),t&&(r+=k),n.filter(e=>!_e(e)).reduce((e,t)=>e+(pe.test(t)?O:t===``?me:he),r)}function ye(e,t){return e.length===t.length&&e.slice(0,-1).every((e,n)=>e===t[n])?e[e.length-1]-t[t.length-1]:0}function be(e,t,n=!1){let{routesMeta:r}=e,i={},a=`/`,o=[];for(let e=0;e{if(t===`*`){let e=s[r]||``;o=a.slice(0,a.length-e.length).replace(/(.)\/+$/,`$1`)}let i=s[r];return n&&!i?e[t]=void 0:e[t]=(i||``).replace(/%2F/g,`/`),e},{}),pathname:a,pathnameBase:o,pattern:e}}function Se(e,t=!1,n=!0){T(e===`*`||!e.endsWith(`*`)||e.endsWith(`/*`),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,`/*`)}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,`/*`)}".`);let r=[],i=`^`+e.replace(/\/*\*?$/,``).replace(/^\/*/,`/`).replace(/[\\.*+^${}|()[\]]/g,`\\$&`).replace(/\/:([\w-]+)(\?)?/g,(e,t,n,i,a)=>{if(r.push({paramName:t,isOptional:n!=null}),n){let t=a.charAt(i+e.length);return t&&t!==`/`?`/([^\\/]*)`:`(?:/([^\\/]*))?`}return`/([^\\/]+)`}).replace(/\/([\w-]+)\?(\/|$)/g,`(/$1)?$2`);return e.endsWith(`*`)?(r.push({paramName:`*`}),i+=e===`*`||e===`/*`?`(.*)$`:`(?:\\/(.+)|\\/*)$`):n?i+=`\\/*$`:e!==``&&e!==`/`&&(i+=`(?:(?=\\/|$))`),[new RegExp(i,t?void 0:`i`),r]}function Ce(e){try{return e.split(`/`).map(e=>decodeURIComponent(e).replace(/\//g,`%2F`)).join(`/`)}catch(t){return T(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${t}).`),e}}function we(e,t){if(t===`/`)return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith(`/`)?t.length-1:t.length,r=e.charAt(n);return r&&r!==`/`?null:e.slice(n)||`/`}var Te=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function Ee(e,t=`/`){let{pathname:n,search:r=``,hash:i=``}=typeof e==`string`?oe(e):e,a;return n?(n=n.replace(/\/\/+/g,`/`),a=n.startsWith(`/`)?De(n.substring(1),`/`):De(n,t)):a=t,{pathname:a,search:Pe(r),hash:Fe(i)}}function De(e,t){let n=t.replace(/\/+$/,``).split(`/`);return e.split(`/`).forEach(e=>{e===`..`?n.length>1&&n.pop():e!==`.`&&n.push(e)}),n.length>1?n.join(`/`):`/`}function Oe(e,t,n,r){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function ke(e){return e.filter((e,t)=>t===0||e.route.path&&e.route.path.length>0)}function Ae(e){let t=ke(e);return t.map((e,n)=>n===t.length-1?e.pathname:e.pathnameBase)}function je(e,t,n,r=!1){let i;typeof e==`string`?i=oe(e):(i={...e},w(!i.pathname||!i.pathname.includes(`?`),Oe(`?`,`pathname`,`search`,i)),w(!i.pathname||!i.pathname.includes(`#`),Oe(`#`,`pathname`,`hash`,i)),w(!i.search||!i.search.includes(`#`),Oe(`#`,`search`,`hash`,i)));let a=e===``||i.pathname===``,o=a?`/`:i.pathname,s;if(o==null)s=n;else{let e=t.length-1;if(!r&&o.startsWith(`..`)){let t=o.split(`/`);for(;t[0]===`..`;)t.shift(),--e;i.pathname=t.join(`/`)}s=e>=0?t[e]:`/`}let c=Ee(i,s),l=o&&o!==`/`&&o.endsWith(`/`),u=(a||o===`.`)&&n.endsWith(`/`);return!c.pathname.endsWith(`/`)&&(l||u)&&(c.pathname+=`/`),c}var Me=e=>e.join(`/`).replace(/\/\/+/g,`/`),Ne=e=>e.replace(/\/+$/,``).replace(/^\/*/,`/`),Pe=e=>!e||e===`?`?``:e.startsWith(`?`)?e:`?`+e,Fe=e=>!e||e===`#`?``:e.startsWith(`#`)?e:`#`+e,Ie=class{constructor(e,t,n,r=!1){this.status=e,this.statusText=t||``,this.internal=r,n instanceof Error?(this.data=n.toString(),this.error=n):this.data=n}};function Le(e){return e!=null&&typeof e.status==`number`&&typeof e.statusText==`string`&&typeof e.internal==`boolean`&&`data`in e}function Re(e){return e.map(e=>e.route.path).filter(Boolean).join(`/`).replace(/\/\/*/g,`/`)||`/`}var ze=typeof window<`u`&&window.document!==void 0&&window.document.createElement!==void 0;function Be(e,t){let n=e;if(typeof n!=`string`||!Te.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,i=!1;if(ze)try{let e=new URL(window.location.href),r=n.startsWith(`//`)?new URL(e.protocol+n):new URL(n),a=we(r.pathname,t);r.origin===e.origin&&a!=null?n=a+r.search+r.hash:i=!0}catch{T(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:r,isExternal:i,to:n}}Object.getOwnPropertyNames(Object.prototype).sort().join(`\0`);var Ve=[`POST`,`PUT`,`PATCH`,`DELETE`];new Set(Ve);var He=[`GET`,...Ve];new Set(He);var Ue=_.createContext(null);Ue.displayName=`DataRouter`;var We=_.createContext(null);We.displayName=`DataRouterState`;var Ge=_.createContext(!1);function Ke(){return _.useContext(Ge)}var qe=_.createContext({isTransitioning:!1});qe.displayName=`ViewTransition`;var Je=_.createContext(new Map);Je.displayName=`Fetchers`;var Ye=_.createContext(null);Ye.displayName=`Await`;var Xe=_.createContext(null);Xe.displayName=`Navigation`;var Ze=_.createContext(null);Ze.displayName=`Location`;var Qe=_.createContext({outlet:null,matches:[],isDataRoute:!1});Qe.displayName=`Route`;var $e=_.createContext(null);$e.displayName=`RouteError`;var et=`REACT_ROUTER_ERROR`,tt=`REDIRECT`,nt=`ROUTE_ERROR_RESPONSE`;function rt(e){if(e.startsWith(`${et}:${tt}:{`))try{let t=JSON.parse(e.slice(28));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`&&typeof t.location==`string`&&typeof t.reloadDocument==`boolean`&&typeof t.replace==`boolean`)return t}catch{}}function it(e){if(e.startsWith(`${et}:${nt}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`)return new Ie(t.status,t.statusText,t.data)}catch{}}function at(e,{relative:t}={}){w(ot(),`useHref() may be used only in the context of a component.`);let{basename:n,navigator:r}=_.useContext(Xe),{hash:i,pathname:a,search:o}=ft(e,{relative:t}),s=a;return n!==`/`&&(s=a===`/`?n:Me([n,a])),r.createHref({pathname:s,search:o,hash:i})}function ot(){return _.useContext(Ze)!=null}function st(){return w(ot(),`useLocation() may be used only in the context of a component.`),_.useContext(Ze).location}var ct=`You should call navigate() in a React.useEffect(), not when your component is first rendered.`;function lt(e){_.useContext(Xe).static||_.useLayoutEffect(e)}function ut(){let{isDataRoute:e}=_.useContext(Qe);return e?At():dt()}function dt(){w(ot(),`useNavigate() may be used only in the context of a component.`);let e=_.useContext(Ue),{basename:t,navigator:n}=_.useContext(Xe),{matches:r}=_.useContext(Qe),{pathname:i}=st(),a=JSON.stringify(Ae(r)),o=_.useRef(!1);return lt(()=>{o.current=!0}),_.useCallback((r,s={})=>{if(T(o.current,ct),!o.current)return;if(typeof r==`number`){n.go(r);return}let c=je(r,JSON.parse(a),i,s.relative===`path`);e==null&&t!==`/`&&(c.pathname=c.pathname===`/`?t:Me([t,c.pathname])),(s.replace?n.replace:n.push)(c,s.state,s)},[t,n,a,i,e])}_.createContext(null);function ft(e,{relative:t}={}){let{matches:n}=_.useContext(Qe),{pathname:r}=st(),i=JSON.stringify(Ae(n));return _.useMemo(()=>je(e,JSON.parse(i),r,t===`path`),[e,i,r,t])}function pt(e,t){return mt(e,t)}function mt(e,t,n){w(ot(),`useRoutes() may be used only in the context of a component.`);let{navigator:r}=_.useContext(Xe),{matches:i}=_.useContext(Qe),a=i[i.length-1],o=a?a.params:{},s=a?a.pathname:`/`,c=a?a.pathnameBase:`/`,l=a&&a.route;{let e=l&&l.path||``;Mt(s,!l||e.endsWith(`*`)||e.endsWith(`*?`),`You rendered descendant (or called \`useRoutes()\`) at "${s}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. - -Please change the parent to .`)}let u=st(),d;if(t){let e=typeof t==`string`?oe(t):t;w(c===`/`||e.pathname?.startsWith(c),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${c}" but pathname "${e.pathname}" was given in the \`location\` prop.`),d=e}else d=u;let f=d.pathname||`/`,p=f;if(c!==`/`){let e=c.replace(/^\//,``).split(`/`);p=`/`+f.replace(/^\//,``).split(`/`).slice(e.length).join(`/`)}let m=le(e,{pathname:p});T(l||m!=null,`No routes matched location "${d.pathname}${d.search}${d.hash}" `),T(m==null||m[m.length-1].route.element!==void 0||m[m.length-1].route.Component!==void 0||m[m.length-1].route.lazy!==void 0,`Matched leaf route at location "${d.pathname}${d.search}${d.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let h=xt(m&&m.map(e=>Object.assign({},e,{params:Object.assign({},o,e.params),pathname:Me([c,r.encodeLocation?r.encodeLocation(e.pathname.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathname]),pathnameBase:e.pathnameBase===`/`?c:Me([c,r.encodeLocation?r.encodeLocation(e.pathnameBase.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathnameBase])})),i,n);return t&&h?_.createElement(Ze.Provider,{value:{location:{pathname:`/`,search:``,hash:``,state:null,key:`default`,unstable_mask:void 0,...d},navigationType:`POP`}},h):h}function ht(){let e=kt(),t=Le(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,r=`rgba(200,200,200, 0.5)`,i={padding:`0.5rem`,backgroundColor:r},a={padding:`2px 4px`,backgroundColor:r},o=null;return console.error(`Error handled by React Router default ErrorBoundary:`,e),o=_.createElement(_.Fragment,null,_.createElement(`p`,null,`๐Ÿ’ฟ Hey developer ๐Ÿ‘‹`),_.createElement(`p`,null,`You can provide a way better UX than this when your app throws errors by providing your own `,_.createElement(`code`,{style:a},`ErrorBoundary`),` or`,` `,_.createElement(`code`,{style:a},`errorElement`),` prop on your route.`)),_.createElement(_.Fragment,null,_.createElement(`h2`,null,`Unexpected Application Error!`),_.createElement(`h3`,{style:{fontStyle:`italic`}},t),n?_.createElement(`pre`,{style:i},n):null,o)}var gt=_.createElement(ht,null),_t=class extends _.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,t){return t.location!==e.location||t.revalidation!==`idle`&&e.revalidation===`idle`?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error===void 0?t.error:e.error,location:t.location,revalidation:e.revalidation||t.revalidation}}componentDidCatch(e,t){this.props.onError?this.props.onError(e,t):console.error(`React Router caught the following error during render`,e)}render(){let e=this.state.error;if(this.context&&typeof e==`object`&&e&&`digest`in e&&typeof e.digest==`string`){let t=it(e.digest);t&&(e=t)}let t=e===void 0?this.props.children:_.createElement(Qe.Provider,{value:this.props.routeContext},_.createElement($e.Provider,{value:e,children:this.props.component}));return this.context?_.createElement(yt,{error:e},t):t}};_t.contextType=Ge;var vt=new WeakMap;function yt({children:e,error:t}){let{basename:n}=_.useContext(Xe);if(typeof t==`object`&&t&&`digest`in t&&typeof t.digest==`string`){let e=rt(t.digest);if(e){let r=vt.get(t);if(r)throw r;let i=Be(e.location,n);if(ze&&!vt.get(t))if(i.isExternal||e.reloadDocument)window.location.href=i.absoluteURL||i.to;else{let n=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(i.to,{replace:e.replace}));throw vt.set(t,n),n}return _.createElement(`meta`,{httpEquiv:`refresh`,content:`0;url=${i.absoluteURL||i.to}`})}}return e}function bt({routeContext:e,match:t,children:n}){let r=_.useContext(Ue);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),_.createElement(Qe.Provider,{value:e},n)}function xt(e,t=[],n){let r=n?.state;if(e==null){if(!r)return null;if(r.errors)e=r.matches;else if(t.length===0&&!r.initialized&&r.matches.length>0)e=r.matches;else return null}let i=e,a=r?.errors;if(a!=null){let e=i.findIndex(e=>e.route.id&&a?.[e.route.id]!==void 0);w(e>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(a).join(`,`)}`),i=i.slice(0,Math.min(i.length,e+1))}let o=!1,s=-1;if(n&&r){o=r.renderFallback;for(let e=0;e=0?i.slice(0,s+1):[i[0]];break}}}}let c=n?.onError,l=r&&c?(e,t)=>{c(e,{location:r.location,params:r.matches?.[0]?.params??{},unstable_pattern:Re(r.matches),errorInfo:t})}:void 0;return i.reduceRight((e,n,c)=>{let u,d=!1,f=null,p=null;r&&(u=a&&n.route.id?a[n.route.id]:void 0,f=n.route.errorElement||gt,o&&(s<0&&c===0?(Mt(`route-fallback`,!1,"No `HydrateFallback` element provided to render during initial hydration"),d=!0,p=null):s===c&&(d=!0,p=n.route.hydrateFallbackElement||null)));let m=t.concat(i.slice(0,c+1)),h=()=>{let t;return t=u?f:d?p:n.route.Component?_.createElement(n.route.Component,null):n.route.element?n.route.element:e,_.createElement(bt,{match:n,routeContext:{outlet:e,matches:m,isDataRoute:r!=null},children:t})};return r&&(n.route.ErrorBoundary||n.route.errorElement||c===0)?_.createElement(_t,{location:r.location,revalidation:r.revalidation,component:f,error:u,children:h(),routeContext:{outlet:null,matches:m,isDataRoute:!0},onError:l}):h()},null)}function St(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function Ct(e){let t=_.useContext(Ue);return w(t,St(e)),t}function wt(e){let t=_.useContext(We);return w(t,St(e)),t}function Tt(e){let t=_.useContext(Qe);return w(t,St(e)),t}function Et(e){let t=Tt(e),n=t.matches[t.matches.length-1];return w(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function Dt(){return Et(`useRouteId`)}function A(){return wt(`useNavigation`).navigation}function Ot(){let{matches:e,loaderData:t}=wt(`useMatches`);return _.useMemo(()=>e.map(e=>D(e,t)),[e,t])}function kt(){let e=_.useContext($e),t=wt(`useRouteError`),n=Et(`useRouteError`);return e===void 0?t.errors?.[n]:e}function At(){let{router:e}=Ct(`useNavigate`),t=Et(`useNavigate`),n=_.useRef(!1);return lt(()=>{n.current=!0}),_.useCallback(async(r,i={})=>{T(n.current,ct),n.current&&(typeof r==`number`?await e.navigate(r):await e.navigate(r,{fromRouteId:t,...i}))},[e,t])}var jt={};function Mt(e,t,n){!t&&!jt[e]&&(jt[e]=!0,T(!1,n))}_.useOptimistic,_.memo(Nt);function Nt({routes:e,future:t,state:n,isStatic:r,onError:i}){return mt(e,void 0,{state:n,isStatic:r,onError:i,future:t})}function Pt(e){w(!1,`A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .`)}function Ft({basename:e=`/`,children:t=null,location:n,navigationType:r=`POP`,navigator:i,static:a=!1,unstable_useTransitions:o}){w(!ot(),`You cannot render a inside another . You should never have more than one in your app.`);let s=e.replace(/^\/*/,`/`),c=_.useMemo(()=>({basename:s,navigator:i,static:a,unstable_useTransitions:o,future:{}}),[s,i,a,o]);typeof n==`string`&&(n=oe(n));let{pathname:l=`/`,search:u=``,hash:d=``,state:f=null,key:p=`default`,unstable_mask:m}=n,h=_.useMemo(()=>{let e=we(l,s);return e==null?null:{location:{pathname:e,search:u,hash:d,state:f,key:p,unstable_mask:m},navigationType:r}},[s,l,u,d,f,p,r,m]);return T(h!=null,` is not able to match the URL "${l}${u}${d}" because it does not start with the basename, so the won't render anything.`),h==null?null:_.createElement(Xe.Provider,{value:c},_.createElement(Ze.Provider,{children:t,value:h}))}function It({children:e,location:t}){return pt(Lt(e),t)}_.Component;function Lt(e,t=[]){let n=[];return _.Children.forEach(e,(e,r)=>{if(!_.isValidElement(e))return;let i=[...t,r];if(e.type===_.Fragment){n.push.apply(n,Lt(e.props.children,i));return}w(e.type===Pt,`[${typeof e.type==`string`?e.type:e.type.name}] is not a component. All component children of must be a or `),w(!e.props.index||!e.props.children,`An index route cannot have child routes.`);let a={id:e.props.id||i.join(`-`),caseSensitive:e.props.caseSensitive,element:e.props.element,Component:e.props.Component,index:e.props.index,path:e.props.path,middleware:e.props.middleware,loader:e.props.loader,action:e.props.action,hydrateFallbackElement:e.props.hydrateFallbackElement,HydrateFallback:e.props.HydrateFallback,errorElement:e.props.errorElement,ErrorBoundary:e.props.ErrorBoundary,hasErrorBoundary:e.props.hasErrorBoundary===!0||e.props.ErrorBoundary!=null||e.props.errorElement!=null,shouldRevalidate:e.props.shouldRevalidate,handle:e.props.handle,lazy:e.props.lazy};e.props.children&&(a.children=Lt(e.props.children,i)),n.push(a)}),n}var Rt=`get`,zt=`application/x-www-form-urlencoded`;function Bt(e){return typeof HTMLElement<`u`&&e instanceof HTMLElement}function Vt(e){return Bt(e)&&e.tagName.toLowerCase()===`button`}function Ht(e){return Bt(e)&&e.tagName.toLowerCase()===`form`}function Ut(e){return Bt(e)&&e.tagName.toLowerCase()===`input`}function Wt(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function Gt(e,t){return e.button===0&&(!t||t===`_self`)&&!Wt(e)}var Kt=null;function qt(){if(Kt===null)try{new FormData(document.createElement(`form`),0),Kt=!1}catch{Kt=!0}return Kt}var Jt=new Set([`application/x-www-form-urlencoded`,`multipart/form-data`,`text/plain`]);function Yt(e){return e!=null&&!Jt.has(e)?(T(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${zt}"`),null):e}function Xt(e,t){let n,r,i,a,o;if(Ht(e)){let o=e.getAttribute(`action`);r=o?we(o,t):null,n=e.getAttribute(`method`)||Rt,i=Yt(e.getAttribute(`enctype`))||zt,a=new FormData(e)}else if(Vt(e)||Ut(e)&&(e.type===`submit`||e.type===`image`)){let o=e.form;if(o==null)throw Error(`Cannot submit a - {/* Scrollable Content Area */} -
-
- {/* Primary Metrics */} -
-
- Final Grading Score -
- - {Number(gameState?.cumulativeReward || metrics?.score || 0).toFixed(2)} - - / 1.00 -
+
+ {/* Primary Metrics */} +
+
+ Cumulative Efficiency Score +
+ + {metrics?.score || 'โ€”'} + + pts
- - {/* Reward Breakdown from Episode */} - {gameState?.rewardBreakdown && Object.keys(gameState.rewardBreakdown).length > 0 && ( -
- Step Reward Breakdown -
- {Object.entries(gameState.rewardBreakdown).map(([key, val]) => ( -
-
{key.replace(/_/g, ' ')}
-
0 ? 'text-primary' : 'text-slate-600'}`}> - {typeof val === 'number' ? val.toFixed(3) : val} -
-
- ))} -
-
- )} - - {/* Reward History */} - {gameState?.rewardHistory && gameState.rewardHistory.length > 0 && ( -
- Step Rewards -
- {gameState.rewardHistory.map((r, i) => ( -
-
- ))} -
-
- Avg: {(gameState.rewardHistory.reduce((a, b) => a + b, 0) / gameState.rewardHistory.length).toFixed(3)} - Max: {Math.max(...gameState.rewardHistory).toFixed(3)} -
-
- )} -
-
- Clues Found - {gameState?.clues_found?.length || 0} -
-
- Steps Executed - {gameState?.step !== undefined ? gameState.step : (metrics?.steps !== undefined ? metrics.steps : 'โ€”')} -
+
+
+
+ Clues Found + {gameState?.clues_found?.length || 0}
-
-
- troubleshoot -
-
- State Validation - Status: {metrics?.rootCause || 'โ€”'} -
+
+ Steps Executed + {gameState?.current_round || metrics?.steps || 'โ€”'}
+
+
+ troubleshoot +
+
+ State Validation + Status: {metrics?.rootCause || 'โ€”'} +
+
+
- {/* Right Column: Agent Metrics */} -
-

Agent Performance Breakdown

- {Object.entries(gameState?.agents || {}).map(([agentId, agentData], idx) => { - const colors = ['primary', 'secondary', 'tertiary', 'error', 'success']; - const color = colors[idx % colors.length]; - const msgs = agentData?.messages || []; - const msgCount = msgs.filter(m => m.type === 'message').length; - const toolCount = msgs.filter(m => m.type === 'tool_call').length; - const errCount = msgs.filter(m => m.type === 'tool_result' && m.result?.toLowerCase().includes('error')).length; - const agentNames = ['ALPHA', 'BRAVO', 'CHARLIE', 'DELTA', 'ECHO']; - const agentName = agentNames[idx % agentNames.length]; - - return ( -
-
-
-
- Agent_{agentName} - {agentId.toUpperCase()} + {/* Right Column: Agent Metrics */} +
+

Agent Performance Breakdown

+ {/* Agent A */} +
+
+
+
+ Agent_Alpha + CYAN_PROTOCOL +
+ {(() => { + const msgs = gameState?.agents?.agent_a?.messages || []; + const msgCount = msgs.filter(m => m.type === 'message').length; + const toolCount = msgs.filter(m => m.type === 'tool_call').length; + const errCount = msgs.filter(m => m.type === 'tool_result' && m.result?.toLowerCase().includes('error')).length; + return ( +
+
+ chat MSGS + {msgCount}
-
-
- chat MSGS - {msgCount} -
-
- build TOOLS - {toolCount} -
-
- warning ERRS - {errCount} -
+
+ build TOOLS + {toolCount} +
+
+ warning ERRS + {errCount}
-
- ); - })} + ); + })()} +
-
- - {/* Submit Resolution Report Panel */} - {(() => { - const resCall = gameState?.tool_calls_made?.find(c => c.tool_name === 'submit_resolution'); - if (!resCall) return null; - const p = resCall.params || {}; - return ( -
-
-

- description - Incident Resolution Report -

-
-
- Root Cause Service - {p.root_cause_service || 'UNKNOWN'} -
-
- Root Cause Description -

{p.root_cause_description || 'No description provided.'}

-
-
- Fix Applied -

{p.fix_applied || 'No fix described.'}

-
-
+ {/* Agent B */} +
+
+
+
+ Agent_Bravo + VIOLET_PROTOCOL
+ {(() => { + const msgs = gameState?.agents?.agent_b?.messages || []; + const msgCount = msgs.filter(m => m.type === 'message').length; + const toolCount = msgs.filter(m => m.type === 'tool_call').length; + const errCount = msgs.filter(m => m.type === 'tool_result' && m.result?.toLowerCase().includes('error')).length; + return ( +
+
+ chat MSGS + {msgCount} +
+
+ build TOOLS + {toolCount} +
+
+ warning ERRS + {errCount} +
+
+ ); + })()}
- ); - })()} +
+
+
- {/* Unified Investigation Summary */} - {unifiedSummary && ( + {/* Submit Resolution Report Panel */} + {(() => { + const resCall = gameState?.tool_calls_made?.find(c => c.tool_name === 'submit_resolution'); + if (!resCall) return null; + const p = resCall.params || {}; + return (
-
+

- psychology - Unified Investigation Summary + description + Incident Resolution Report

- - {/* Success/Failure Badge */} -
- - - {unifiedSummary.isSuccess ? 'Investigation Successful' : 'Investigation Inconclusive'} - - - {unifiedSummary.agentCount} Agents Collaborated - -
- - {/* Combined Conclusions */} -
- Combined Agent Conclusions -
-

- {unifiedSummary.conclusionText || 'No conclusions recorded.'} -

-
-
- - {/* Key Findings */} -
+
- Key Findings & Clues -
-
    - {unifiedSummary.keyFindings.split('\nโ€ข ').map((finding, i) => ( - finding &&
  • {finding}
  • - ))} -
-
+ Root Cause Service + {p.root_cause_service || 'UNKNOWN'}
- Key Tool Results -
-
-                                                {unifiedSummary.toolSummary}
-                                            
-
+ Root Cause Description +

{p.root_cause_description || 'No description provided.'}

+
+
+ Fix Applied +

{p.fix_applied || 'No fix described.'}

- )} -
+ ); + })()} {/* Modal Footer */}
diff --git a/frontend/src/components/TopNavBar.jsx b/frontend/src/components/TopNavBar.jsx index 4a90408575b328ca24312839547da772334986da..e51eee51f3aa0e868fd5be744f04ff2f24ee47a6 100644 --- a/frontend/src/components/TopNavBar.jsx +++ b/frontend/src/components/TopNavBar.jsx @@ -3,12 +3,6 @@ import { useApp } from '../context/AppContext'; const TopNavBar = () => { const { sessionData, isConnected, sendCommand } = useApp(); - - const status = sessionData?.status || 'STANDBY'; - const isRunning = sessionData?.active && status !== 'COMPLETED'; - - const isStandby = status === 'STANDBY' || status === 'READY'; - return (
@@ -16,49 +10,36 @@ const TopNavBar = () => {
System Status -
{status}
+
{sessionData?.status || 'INITIALIZING...'}
+ +
- {/* START - clickable when standby */} - - {/* PAUSE/RESUME - clickable when running */} - - {/* FORCE END - clickable when running */} - - {/* RESET - always clickable */} -
- {loading ? ( -
- sync - Loading models... -
- ) : expanded ? ( -
- {Object.entries(groupedModels).map(([org, orgModels]) => ( -
-
- {org} -
- {orgModels.map(model => ( - - ))} -
- ))} -
- ) : ( - - )} -

{models.length} models available

-
- ); -}; - const ROLES = ["INVESTIGATOR", "VALIDATOR", "FORENSIC_ANALYST", "NETWORK_ENGINEER", "SYSTEM_ADMIN", "SECURITY_ARCHITECT", "COMPLIANCE_OFFICER", "CUSTOM_ROLE"]; const SettingsView = () => { - const [agents, setAgents] = useState([]); + const [agentA, setAgentA] = useState({ provider: 'ollama', model: '', hfModel: 'microsoft/Phi-3-mini-4k-instruct', openaiModel: 'gpt-4o', temp: 0.8, role: 'INVESTIGATOR', customRoleName: '', customPrompt: '' }); + const [agentB, setAgentB] = useState({ provider: 'ollama', model: '', hfModel: 'Qwen/Qwen2.5-3B-Instruct', openaiModel: 'gpt-4o-mini', temp: 0.6, role: 'VALIDATOR', customRoleName: '', customPrompt: '' }); const [openaiKey, setOpenaiKey] = useState(''); const [maxSteps, setMaxSteps] = useState(12); const [complexity, setComplexity] = useState('LEVEL_02: ADVERSARIAL'); const [saved, setSaved] = useState(false); const [executionMode, setExecutionMode] = useState('simulated'); const [sshConfig, setSshConfig] = useState({ host: '', port: 22, user: '', password: '' }); - const [sshTestStatus, setSshTestStatus] = useState(null); + const [sshTestStatus, setSshTestStatus] = useState(null); // null | 'testing' | 'ok' | 'fail' useEffect(() => { const fetchConfig = async () => { try { const res = await fetch(`${config.API_BASE}/config`); const data = await res.json(); - if (data.models && data.models.agents) { - setAgents(data.models.agents.map(a => ({ - id: a.id, - provider: a.provider || 'hf', - model: a.model, - hfModel: (a.provider === 'hf' && a.model?.includes('/')) ? a.model : 'meta-llama/Llama-3.1-8B-Instruct', - openaiModel: a.provider === 'openai' ? a.model : 'gpt-4o-mini', - temp: a.temperature || 0.7, - role: a.role?.startsWith('CUSTOM_') ? 'CUSTOM_ROLE' : a.role || 'INVESTIGATOR', - customRoleName: a.role?.startsWith('CUSTOM_') ? a.role.replace('CUSTOM_', '').replace(/_/g, ' ') : '', - customPrompt: a.system_prompt || '' - }))); - } else { - setAgents([{ - id: 'agent_a', provider: 'hf', model: '', hfModel: 'meta-llama/Llama-3.1-8B-Instruct', openaiModel: 'gpt-4o', temp: 0.7, role: 'INVESTIGATOR', customRoleName: '', customPrompt: '' - }]); - } + const roleA = data.models.agent_a_role || 'INVESTIGATOR'; + const roleB = data.models.agent_b_role || 'VALIDATOR'; + setAgentA({ + provider: data.models.agent_a_provider || 'ollama', + model: data.models.agent_a, + hfModel: 'microsoft/Phi-3-mini-4k-instruct', + temp: data.models.agent_a_temp, + role: roleA.startsWith('CUSTOM_') ? 'CUSTOM_ROLE' : roleA, + customRoleName: roleA.startsWith('CUSTOM_') ? roleA.replace('CUSTOM_', '').replace(/_/g, ' ') : '', + customPrompt: data.models.agent_a_system_prompt || '' + }); + setAgentB({ + provider: data.models.agent_b_provider || 'ollama', + model: data.models.agent_b, + hfModel: 'Qwen/Qwen2.5-3B-Instruct', + temp: data.models.agent_b_temp, + role: roleB.startsWith('CUSTOM_') ? 'CUSTOM_ROLE' : roleB, + customRoleName: roleB.startsWith('CUSTOM_') ? roleB.replace('CUSTOM_', '').replace(/_/g, ' ') : '', + customPrompt: data.models.agent_b_system_prompt || '' + }); if (data.models.openai_api_key) setOpenaiKey(data.models.openai_api_key); setMaxSteps(data.episode.max_steps); if (data.execution) { @@ -215,20 +133,21 @@ const SettingsView = () => { const handleSave = async () => { try { - const agentPayload = agents.map(a => ({ - id: a.id, - model: a.provider === 'ollama' ? a.model : (a.provider === 'openai' ? a.openaiModel : a.hfModel), - provider: a.provider, - role: a.role === 'CUSTOM_ROLE' ? `CUSTOM_${a.customRoleName.replace(/ /g, '_').toUpperCase()}` : a.role, - system_prompt: a.customPrompt, - temperature: a.temp - })); await fetch(`${config.API_BASE}/config`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ MAX_STEPS: maxSteps, - AGENTS: agentPayload, + AGENT_A_MODEL: agentA.provider === 'ollama' ? agentA.model : (agentA.provider === 'openai' ? agentA.openaiModel : agentA.hfModel), + AGENT_B_MODEL: agentB.provider === 'ollama' ? agentB.model : (agentB.provider === 'openai' ? agentB.openaiModel : agentB.hfModel), + AGENT_A_PROVIDER: agentA.provider, + AGENT_B_PROVIDER: agentB.provider, + AGENT_A_ROLE: agentA.role === 'CUSTOM_ROLE' ? `CUSTOM_${agentA.customRoleName.replace(/ /g, '_').toUpperCase()}` : agentA.role, + AGENT_B_ROLE: agentB.role === 'CUSTOM_ROLE' ? `CUSTOM_${agentB.customRoleName.replace(/ /g, '_').toUpperCase()}` : agentB.role, + AGENT_A_SYSTEM_PROMPT: agentA.customPrompt, + AGENT_B_SYSTEM_PROMPT: agentB.customPrompt, + AGENT_A_TEMPERATURE: agentA.temp, + AGENT_B_TEMPERATURE: agentB.temp, EXECUTION_MODE: executionMode, SSH_HOST: sshConfig.host, SSH_PORT: sshConfig.port, @@ -244,22 +163,24 @@ const SettingsView = () => { } }; + // Auto-sync active settings so navigation doesn't wipe them useEffect(() => { - if (agents.length === 0) return; - const agentPayload = agents.map(a => ({ - id: a.id, - model: a.provider === 'ollama' ? a.model : (a.provider === 'openai' ? a.openaiModel : a.hfModel), - provider: a.provider, - role: a.role === 'CUSTOM_ROLE' ? `CUSTOM_${a.customRoleName.replace(/ /g, '_').toUpperCase()}` : a.role, - system_prompt: a.customPrompt, - temperature: a.temp - })); + if (!agentA.model && !agentB.model) return; // Wait for initial load or valid models fetch(`${config.API_BASE}/config`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ MAX_STEPS: maxSteps, - AGENTS: agentPayload, + AGENT_A_MODEL: agentA.provider === 'ollama' ? agentA.model : (agentA.provider === 'openai' ? agentA.openaiModel : agentA.hfModel), + AGENT_B_MODEL: agentB.provider === 'ollama' ? agentB.model : (agentB.provider === 'openai' ? agentB.openaiModel : agentB.hfModel), + AGENT_A_PROVIDER: agentA.provider, + AGENT_B_PROVIDER: agentB.provider, + AGENT_A_ROLE: agentA.role === 'CUSTOM_ROLE' ? `CUSTOM_${agentA.customRoleName.replace(/ /g, '_').toUpperCase()}` : agentA.role, + AGENT_B_ROLE: agentB.role === 'CUSTOM_ROLE' ? `CUSTOM_${agentB.customRoleName.replace(/ /g, '_').toUpperCase()}` : agentB.role, + AGENT_A_SYSTEM_PROMPT: agentA.customPrompt, + AGENT_B_SYSTEM_PROMPT: agentB.customPrompt, + AGENT_A_TEMPERATURE: agentA.temp, + AGENT_B_TEMPERATURE: agentB.temp, EXECUTION_MODE: executionMode, SSH_HOST: sshConfig.host, SSH_PORT: sshConfig.port, @@ -268,56 +189,21 @@ const SettingsView = () => { OPENAI_API_KEY: openaiKey }) }).catch(e => { }); - }, [agents, maxSteps, executionMode, sshConfig, openaiKey]); - - const handleUpdateAgent = (index, updater) => { - setAgents(prev => { - const next = [...prev]; - next[index] = typeof updater === 'function' ? updater(next[index]) : updater; - return next; - }); - }; + }, [agentA, agentB, maxSteps, executionMode, sshConfig, openaiKey]); - const addAgent = () => { - const newId = `agent_${Date.now()}`; - const roles = ['INVESTIGATOR', 'VALIDATOR', 'FORENSIC_ANALYST', 'NETWORK_ENGINEER', 'SYSTEM_ADMIN', 'SECURITY_ARCHITECT', 'COMPLIANCE_OFFICER']; - const role = roles[agents.length % roles.length]; - setAgents(prev => [...prev, { - id: newId, provider: 'hf', model: '', hfModel: 'meta-llama/Llama-3.2-1B-Instruct', openaiModel: 'gpt-4o-mini', temp: Math.max(0.3, 0.7 - agents.length * 0.05), role, customRoleName: '', customPrompt: '' - }]); - }; - - const removeAgent = (index) => { - if (agents.length <= 1) return; - setAgents(prev => prev.filter((_, i) => i !== index)); - }; - - const ProviderToggle = ({ agent, index }) => { - const getButtonClass = (p) => { - if (agent.provider === p) { - return index % 2 === 0 ? 'flex-1 py-1 px-3 rounded text-[10px] font-mono font-bold uppercase transition-all bg-primary text-black' : 'flex-1 py-1 px-3 rounded text-[10px] font-mono font-bold uppercase transition-all bg-secondary text-black'; - } - return 'flex-1 py-1 px-3 rounded text-[10px] font-mono font-bold uppercase transition-all text-outline-variant hover:text-white'; - }; - const getProviderLabel = (p) => { - if (p === 'ollama') return 'Local Ollama'; - if (p === 'hf') return 'Hugging Face'; - return 'OpenAI'; - }; - return ( -
- {['ollama', 'hf', 'openai'].map(p => ( - - ))} -
- ); - }; + const ProviderToggle = ({ agent, agentId, onSetAgent }) => ( +
+ {['ollama', 'hf', 'openai'].map(p => ( + + ))} +
+ ); return (
@@ -337,134 +223,227 @@ const SettingsView = () => {
- {/* N-Agents Render */} - {agents.map((agent, index) => { - const isPrimary = index % 2 === 0; - const accentColor = isPrimary ? 'primary' : 'secondary'; - const titleColor = isPrimary ? 'text-primary' : 'text-secondary'; - const bgColor = isPrimary ? 'bg-primary/10' : 'bg-secondary/10'; - const borderColor = isPrimary ? 'border-primary/20' : 'border-secondary/20'; - - return ( -
-
-
- smart_toy + {/* Agent A Config */} +
+
+
+ smart_toy +
+
+
+
+

Agent A [PRIMARY]

+

Neural Processing Unit 01

-
-
-
-

{agent.role.replace(/_/g, ' ')} [{agent.id.toUpperCase()}]

-

Node ID: {agent.id}

-
-
- - {agents.length > 1 && ( - - )} -
-
+ +
+
+
+
+ {agentA.provider === 'ollama' ? ( + setAgentA(a => ({ ...a, model: v }))} + accentColor="primary" + /> + ) : agentA.provider === 'hf' ? ( +
+
+ + setAgentA(a => ({ ...a, hfModel: e.target.value }))} + />
-
- {agent.provider === 'ollama' ? ( - handleUpdateAgent(index, a => ({ ...a, model: v }))} - accentColor={accentColor} + ) : ( +
+
+ + setOpenaiKey(e.target.value)} /> - ) : agent.provider === 'hf' ? ( - handleUpdateAgent(index, a => ({ ...a, hfModel: v }))} - accentColor={accentColor} +
+
+ + setAgentA(a => ({ ...a, openaiModel: e.target.value }))} /> - ) : ( -
-
- - setOpenaiKey(e.target.value)} - /> -
-
- - handleUpdateAgent(index, a => ({ ...a, openaiModel: e.target.value }))} - /> -
+
+
+ )} +
+
+ + {agentA.temp.toFixed(1)} +
+ setAgentA(a => ({ ...a, temp: parseFloat(e.target.value) }))} + /> +
+
+ + + + {agentA.role === 'CUSTOM_ROLE' && ( +
+
+ + setAgentA(a => ({ ...a, customRoleName: e.target.value }))} + className="w-full bg-transparent border-0 border-b border-primary/30 py-2 font-mono text-sm text-on-surface focus:outline-none focus:border-primary transition-all placeholder:text-slate-700" + />
- )} -
-
- - {agent.temp.toFixed(1)} +
+ +