Spaces:
Sleeping
Sleeping
Ashish
#1
by Vector11187u - opened
This view is limited to 50 files because it contains too many changes. See the raw diff here.
- .build-trigger +0 -1
- .dockerignore +0 -10
- .gitignore +14 -37
- Dockerfile +39 -58
- README.md +239 -292
- backend/.gitignore +13 -0
- backend/api/routes/config_routes.py +42 -17
- backend/api/routes/model_routes.py +2 -76
- backend/api/routes/openenv.py +9 -37
- backend/api/routes/websocket.py +2 -2
- backend/api/schemas/state.py +2 -1
- backend/config.py +63 -117
- backend/core/agent_runner.py +141 -150
- backend/core/episode_manager.py +17 -19
- backend/core/state_manager.py +9 -12
- backend/main.py +1 -7
- backend/models/model_manager.py +115 -162
- backend/requirements.txt +15 -14
- backend/scenarios/data/easy/software-incident.json +0 -33
- backend/scenarios/data/hard/cascade-system-failure.json +0 -42
- backend/scenarios/data/medium/business-process-failure.json +0 -39
- backend/scenarios/graders/__init__.py +1 -1
- backend/scenarios/graders/base_grader.py +1 -11
- backend/scenarios/graders/easy_grader.py +1 -1
- backend/scenarios/graders/hard_grader.py +1 -1
- backend/scenarios/graders/medium_grader.py +1 -1
- backend/tests/__init__.py +0 -0
- backend/tests/conftest.py +0 -4
- backend/tools/tool_registry.py +0 -2
- backend/utils/embeddings.py +26 -33
- default.env +67 -54
- frontend/.gitignore +39 -0
- frontend/dist/assets/index-CpY48GhO.js +0 -0
- frontend/dist/assets/index-MUcnTDDz.css +0 -2
- frontend/dist/favicon.svg +0 -1
- frontend/dist/icons.svg +0 -24
- frontend/dist/index.html +0 -16
- frontend/package-lock.json +96 -98
- frontend/src/components/EpisodeEndOverlay.jsx +143 -313
- frontend/src/components/TopNavBar.jsx +11 -30
- frontend/src/hooks/useWebSocket.js +147 -211
- frontend/src/index.css +0 -16
- frontend/src/views/DashboardView.jsx +288 -345
- frontend/src/views/SettingsView.jsx +269 -294
- openenv.yaml +59 -59
- pyproject.toml +0 -27
- server/__init__.py +0 -1
- server/app.py +0 -13
- setup.bat +0 -66
- setup.sh +0 -42
.build-trigger
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
Final Release Sync (Definitive UI): 2026-04-07 23:38:07
|
|
|
|
|
|
.dockerignore
DELETED
|
@@ -1,10 +0,0 @@
|
|
| 1 |
-
.git/
|
| 2 |
-
.env
|
| 3 |
-
backend/venv/
|
| 4 |
-
backend/__pycache__/
|
| 5 |
-
frontend/node_modules/
|
| 6 |
-
frontend/dist/
|
| 7 |
-
.pytest_cache/
|
| 8 |
-
.coverage
|
| 9 |
-
brain/
|
| 10 |
-
.gemini/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.gitignore
CHANGED
|
@@ -1,37 +1,14 @@
|
|
| 1 |
-
#
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
.
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
.npm/
|
| 16 |
-
|
| 17 |
-
# Env & Secrets
|
| 18 |
-
.env
|
| 19 |
-
.env.*
|
| 20 |
-
!.env.example
|
| 21 |
-
# default.env is needed for HF Spaces
|
| 22 |
-
|
| 23 |
-
# OS
|
| 24 |
-
.DS_Store
|
| 25 |
-
Thumbs.db
|
| 26 |
-
|
| 27 |
-
# VS Code / IDE
|
| 28 |
-
.vscode/
|
| 29 |
-
.idea/
|
| 30 |
-
*.swp
|
| 31 |
-
*.swo
|
| 32 |
-
|
| 33 |
-
# Project specific
|
| 34 |
-
backend/logs/
|
| 35 |
-
*.log
|
| 36 |
-
.gemini/
|
| 37 |
-
brain/
|
|
|
|
| 1 |
+
# VS Code / IDE
|
| 2 |
+
.vscode/
|
| 3 |
+
.idea/
|
| 4 |
+
.DS_Store
|
| 5 |
+
|
| 6 |
+
# Brain / Artifacts (Optional: keep if you want them on GitHub)
|
| 7 |
+
# /home/habibi/.gemini/antigravity/brain/
|
| 8 |
+
|
| 9 |
+
# Local env
|
| 10 |
+
.env
|
| 11 |
+
*.local
|
| 12 |
+
node_modules/
|
| 13 |
+
venv/
|
| 14 |
+
__pycache__/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Dockerfile
CHANGED
|
@@ -1,58 +1,39 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
RUN
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
COPY --chown=user:user openenv.yaml .
|
| 41 |
-
COPY --chown=user:user inference.py .
|
| 42 |
-
COPY --chown=user:user pyproject.toml .
|
| 43 |
-
COPY --chown=user:user server ./server
|
| 44 |
-
|
| 45 |
-
# Copy built frontend from stage 1
|
| 46 |
-
COPY --chown=user:user --from=frontend-builder /app/frontend/dist ./frontend/dist
|
| 47 |
-
|
| 48 |
-
USER user
|
| 49 |
-
|
| 50 |
-
EXPOSE 7860
|
| 51 |
-
|
| 52 |
-
ENV HOST=0.0.0.0
|
| 53 |
-
ENV PORT=7860
|
| 54 |
-
ENV ENVIRONMENT=production
|
| 55 |
-
ENV PYTHONPATH=$HOME/app:$HOME/app/backend
|
| 56 |
-
ENV PYTHONUNBUFFERED=1
|
| 57 |
-
|
| 58 |
-
CMD ["python3", "server/app.py"]
|
|
|
|
| 1 |
+
FROM node:20 AS frontend-builder
|
| 2 |
+
WORKDIR /app/frontend
|
| 3 |
+
COPY frontend/package*.json ./
|
| 4 |
+
RUN npm install
|
| 5 |
+
COPY frontend .
|
| 6 |
+
RUN npm run build
|
| 7 |
+
|
| 8 |
+
FROM python:3.11-slim
|
| 9 |
+
RUN useradd -m -u 1000 user
|
| 10 |
+
ENV HOME=/home/user \
|
| 11 |
+
PATH=/home/user/.local/bin:$PATH
|
| 12 |
+
WORKDIR $HOME/app
|
| 13 |
+
|
| 14 |
+
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
| 15 |
+
|
| 16 |
+
COPY --chown=user:user backend/requirements.txt ./requirements.txt
|
| 17 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 18 |
+
|
| 19 |
+
# Copy FastAPI backend
|
| 20 |
+
COPY --chown=user:user backend ./backend
|
| 21 |
+
# Copy full repo bounds if necessary for local paths
|
| 22 |
+
COPY --chown=user:user default.env .
|
| 23 |
+
COPY --chown=user:user openenv.yaml .
|
| 24 |
+
COPY --chown=user:user inference.py .
|
| 25 |
+
|
| 26 |
+
# Copy pre-built React frontend
|
| 27 |
+
COPY --chown=user:user --from=frontend-builder /app/frontend/dist ./frontend/dist
|
| 28 |
+
|
| 29 |
+
USER user
|
| 30 |
+
|
| 31 |
+
EXPOSE 7860
|
| 32 |
+
EXPOSE 8001
|
| 33 |
+
|
| 34 |
+
ENV HOST=0.0.0.0
|
| 35 |
+
ENV PORT=7860
|
| 36 |
+
ENV ENVIRONMENT=production
|
| 37 |
+
ENV PYTHONPATH=$HOME/app
|
| 38 |
+
|
| 39 |
+
CMD ["python", "backend/main.py"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
|
@@ -1,292 +1,239 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: NEXON-AI
|
| 3 |
-
emoji: 🛡️
|
| 4 |
-
colorFrom: blue
|
| 5 |
-
colorTo: indigo
|
| 6 |
-
sdk: docker
|
| 7 |
-
app_port: 7860
|
| 8 |
-
pinned: false
|
| 9 |
-
---
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
.
|
| 262 |
-
|
| 263 |
-
---
|
| 264 |
-
|
| 265 |
-
### 3️⃣ Pulling Models
|
| 266 |
-
|
| 267 |
-
To run the simulation locally without cloud API keys, you must ensure you pull suitable reasoning models through Ollama:
|
| 268 |
-
|
| 269 |
-
```bash
|
| 270 |
-
ollama run qwen2.5:3b # Excellent validator logic footprint
|
| 271 |
-
ollama run dolphin-llama3 # Uncensored investigative assertions
|
| 272 |
-
ollama pull all-minilm # Mandatory for semantic similarity scoring
|
| 273 |
-
```
|
| 274 |
-
|
| 275 |
-
---
|
| 276 |
-
|
| 277 |
-
## 🧪 Automated Testing
|
| 278 |
-
NEXUS-AI includes a comprehensive test suite to ensure environment stability and specification compliance.
|
| 279 |
-
|
| 280 |
-
```bash
|
| 281 |
-
# Run the OpenEnv specification validator
|
| 282 |
-
python openenv_validator.py
|
| 283 |
-
|
| 284 |
-
# Run unit tests for core logic
|
| 285 |
-
pip install pytest
|
| 286 |
-
pytest tests/
|
| 287 |
-
```
|
| 288 |
-
|
| 289 |
-
---
|
| 290 |
-
|
| 291 |
-
## 🤝 Authors
|
| 292 |
-
**Developed by: Ashish Menon** & Vector
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: NEXON-AI
|
| 3 |
+
emoji: 🛡️
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# NEXUS-AI 🌐🛡️
|
| 12 |
+
### Autonomous Incident Investigation Dashboard
|
| 13 |
+
|
| 14 |
+
<div align="center">
|
| 15 |
+
|
| 16 |
+

|
| 17 |
+

|
| 18 |
+

|
| 19 |
+

|
| 20 |
+

|
| 21 |
+
|
| 22 |
+
**Status:** Active Simulation Pipeline
|
| 23 |
+
**Architecture:** Real-time WebSockets + Multi-Agent Consensus
|
| 24 |
+
|
| 25 |
+
</div>
|
| 26 |
+
|
| 27 |
+
---
|
| 28 |
+
|
| 29 |
+
## 📖 What is NEXUS-AI?
|
| 30 |
+
|
| 31 |
+
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.
|
| 32 |
+
|
| 33 |
+
Traditional manual debugging requires extensive context-switching and tool fatigue. NEXUS solves this through:
|
| 34 |
+
1. **Dual-Agent Autonomy**: Two specialized models communicating word-by-word via WebSockets.
|
| 35 |
+
2. **Dynamic Tool Execution**: Fully integrated system terminals allowing agents to run sandboxed validation scripts.
|
| 36 |
+
3. **Semantic Reward Engine**: Evaluates conversational drift mathematically (using native GPU embeddings).
|
| 37 |
+
|
| 38 |
+
The result: An AI "Incident Response Team" that navigates servers, traces logs, and fixes bugs identically to a human SRE.
|
| 39 |
+
|
| 40 |
+
---
|
| 41 |
+
|
| 42 |
+
## 🖼️ Application Screenshots
|
| 43 |
+
|
| 44 |
+
### 📊 Simulation Dashboard
|
| 45 |
+
|
| 46 |
+
> The core command center. Features live agent terminals, a dual-communication consensus log, and a mathematical performance reward graph plotting investigation confidence.
|
| 47 |
+
|
| 48 |
+
<div align="center">
|
| 49 |
+
<img src="./assets/screenshots/Dashboard.png" alt="Simulation Dashboard" width="90%"/>
|
| 50 |
+
</div>
|
| 51 |
+
|
| 52 |
+
---
|
| 53 |
+
|
| 54 |
+
## 🎛️ Scenario Registry & Core Settings
|
| 55 |
+
|
| 56 |
+
> The system is architected for instant adaptability — seamlessly switch LLM providers and inject custom threat models entirely through the frontend DOM.
|
| 57 |
+
|
| 58 |
+
<table>
|
| 59 |
+
<tr>
|
| 60 |
+
<td align="center" width="50%">
|
| 61 |
+
<img src="./assets/screenshots/Scenarios.png" alt="Scenario Browser"/>
|
| 62 |
+
<br/><b>Scenario Registry</b>
|
| 63 |
+
<br/><sub>A persistent LocalStorage-backed grid of tactical simulations. Users can dynamically inject custom infrastructure-specific incidents directly into the agent pipeline.</sub>
|
| 64 |
+
</td>
|
| 65 |
+
<td align="center" width="50%">
|
| 66 |
+
<img src="./assets/screenshots/Settings.png" alt="Hardware Configuration"/>
|
| 67 |
+
<br/><b>Runtime Configuration</b>
|
| 68 |
+
<br/><sub>Dynamically maps available locally-installed Ollama networks, allowing the user to pair models (e.g., Qwen vs Dolphin-Phi) with fully independent parameters.</sub>
|
| 69 |
+
</td>
|
| 70 |
+
</tr>
|
| 71 |
+
</table>
|
| 72 |
+
|
| 73 |
+
---
|
| 74 |
+
|
| 75 |
+
## 🏗️ System Architecture
|
| 76 |
+
|
| 77 |
+
```text
|
| 78 |
+
┌─────────────────────────────────────────────────────────────────┐
|
| 79 |
+
│ CLIENT BROWSER │
|
| 80 |
+
│ React SPA (Tailwind + Framer Motion) │
|
| 81 |
+
│ localhost:5173 │
|
| 82 |
+
└───────────┬─────────────────────────────────┬───────────────────┘
|
| 83 |
+
│ HTTP (REST) │ ws://
|
| 84 |
+
▼ ▼
|
| 85 |
+
┌─────────────────────────────────────────────────────────────────┐
|
| 86 |
+
│ FASTAPI BACKEND (localhost:7860) │
|
| 87 |
+
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
|
| 88 |
+
│ │ /config │ │/scenarios│ │ /reset │ │ ws:// Simulator │ │
|
| 89 |
+
│ │ Env Sync │ │ DB Cache │ │ Injection│ │ Live Stream Sync│ │
|
| 90 |
+
│ └──────────┘ └──────────┘ └──────────┘ └──────────────────┘ │
|
| 91 |
+
└───────────┬───────────────────────────────────┬─────────────────┘
|
| 92 |
+
│ │
|
| 93 |
+
▼ ▼
|
| 94 |
+
┌─────────────────────────────────────────────────────────────────┐
|
| 95 |
+
│ OLLAMA ENGINE / LLM PIPELINE │
|
| 96 |
+
│ Agent A (Investigator) ◄──────► Agent B (Validator) │
|
| 97 |
+
│ - Generates Hypotheses - Challenges Assertions │
|
| 98 |
+
│ - Runs System Tools - Requires Proof │
|
| 99 |
+
└─────────────────────────────────────────────────────────────────┘
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
---
|
| 103 |
+
|
| 104 |
+
## 🌐 Execution Environments
|
| 105 |
+
|
| 106 |
+
NEXUS-AI supports two distinct execution models for agent tools, toggleable via the **Settings** dashboard:
|
| 107 |
+
|
| 108 |
+
### 1. Simulated Mode (Safe Sandbox)
|
| 109 |
+
* **Default Mode**: Agents interact with a pre-defined `clue_map` within the scenario YAML.
|
| 110 |
+
* **No System Impact**: Commands like `read_logs` or `check_service` return mocked data.
|
| 111 |
+
* **Use Case**: Training, logic validation, and "what-if" analysis without infrastructure risk.
|
| 112 |
+
|
| 113 |
+
### 2. SSH Lab Node (Real-World Execution)
|
| 114 |
+
* **Live Connection**: Commands are executed in real-time on a remote Linux server via SSH.
|
| 115 |
+
* **Autonomous Terminal**: Agents use the `run_terminal_command` tool to browse logs, check systemd status, and inspect real configs.
|
| 116 |
+
* **Security**: Includes a command blocklist to prevent highly destructive operations (e.g., `rm -rf /`).
|
| 117 |
+
* **Use Case**: Actual incident response on isolated Lab/Staging nodes.
|
| 118 |
+
|
| 119 |
+
---
|
| 120 |
+
|
| 121 |
+
## 🧠 The AI Pipeline Deep-Dive
|
| 122 |
+
|
| 123 |
+
### Step 1: Scenario Injection & Bootstrapping
|
| 124 |
+
```python
|
| 125 |
+
# The EpisodeManager receives the frontend custom scenario JSON
|
| 126 |
+
# Broadcasts 'episode_start' natively over the WebSocket to synchronize the UI
|
| 127 |
+
await broadcast("episode_start", {
|
| 128 |
+
"scenario": active_scenario,
|
| 129 |
+
"agent_a_model": settings.AGENT_A_MODEL
|
| 130 |
+
})
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
### Step 2: Agent Consensus Loop
|
| 134 |
+
```python
|
| 135 |
+
# Agents interact sequentially. The Investigator attempts a solution
|
| 136 |
+
# while the Validator challenges it. Both agents have access to dynamic system execution.
|
| 137 |
+
client, model_name = model_manager.get_client(agent_id)
|
| 138 |
+
stream = await client.chat.completions.create(
|
| 139 |
+
model=model_name,
|
| 140 |
+
messages=injected_history,
|
| 141 |
+
tools=available_tools, # e.g. fix_proposer, run_terminal_command
|
| 142 |
+
stream=True
|
| 143 |
+
)
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
### Step 3: Fast GPU Embeddings (Similarity Evaluation)
|
| 147 |
+
```python
|
| 148 |
+
# Heavy CPU blocking is completely bypassed.
|
| 149 |
+
# Semantic embedding computations map strictly into the Ollama GPU pipeline.
|
| 150 |
+
@lru_cache(maxsize=256)
|
| 151 |
+
def get_embedding(text: str) -> List[float]:
|
| 152 |
+
response = httpx.post("http://localhost:11434/api/embeddings", json={
|
| 153 |
+
"model": "all-minilm",
|
| 154 |
+
"prompt": text
|
| 155 |
+
}, timeout=60.0)
|
| 156 |
+
return response.json().get("embedding", [])
|
| 157 |
+
```
|
| 158 |
+
|
| 159 |
+
---
|
| 160 |
+
|
| 161 |
+
## 🛠️ Full Technology Stack
|
| 162 |
+
|
| 163 |
+
| Layer | Technology | Why |
|
| 164 |
+
|---|---|---|
|
| 165 |
+
| Frontend Framework | React 18 (Vite) | Lightning fast HMR, component isolation |
|
| 166 |
+
| Frontend Styling | Tailwind CSS | Utility-first tactical glassmorphism |
|
| 167 |
+
| Backend Framework | FastAPI | Async Python, explicit endpoint mapping |
|
| 168 |
+
| Transport Layer | WebSockets | Word-by-word streaming across UI boundaries |
|
| 169 |
+
| Local AI Engine | Ollama | Native device acceleration, absolute privacy |
|
| 170 |
+
| Remote Provider | HuggingFace Inference API | Drop-in SaaS alternatives |
|
| 171 |
+
| SSH Connectivity | Paramiko | Secure remote shell execution for Lab Nodes |
|
| 172 |
+
| Data Persistence | LocalStorage & `.env` Injection | Avoids over-architected SQL constraints |
|
| 173 |
+
|
| 174 |
+
---
|
| 175 |
+
|
| 176 |
+
## 🚀 How to Run This Project (Full Step-by-Step Guide)
|
| 177 |
+
|
| 178 |
+
### 📋 Prerequisites
|
| 179 |
+
- Python 3.10+
|
| 180 |
+
- Node.js 18+
|
| 181 |
+
- [Ollama](https://ollama.com/) (installed locally for model hosting)
|
| 182 |
+
- **Optional**: A remote Linux VM (Ubuntu/Kali) with SSH enabled for Lab Node mode
|
| 183 |
+
|
| 184 |
+
---
|
| 185 |
+
|
| 186 |
+
### 1️⃣ Backend Setup (FastAPI / Python)
|
| 187 |
+
|
| 188 |
+
```bash
|
| 189 |
+
cd backend
|
| 190 |
+
|
| 191 |
+
# Create and activate virtual environment
|
| 192 |
+
python -m venv venv
|
| 193 |
+
# source venv/bin/activate # Linux/macOS
|
| 194 |
+
venv\Scripts\activate # Windows
|
| 195 |
+
|
| 196 |
+
# Install all dependencies
|
| 197 |
+
pip install -r requirements.txt
|
| 198 |
+
```
|
| 199 |
+
|
| 200 |
+
#### Start the Backend Engine
|
| 201 |
+
```bash
|
| 202 |
+
# This exposes the core REST API and the WebSocket simulation tunnel
|
| 203 |
+
python main.py
|
| 204 |
+
```
|
| 205 |
+
|
| 206 |
+
---
|
| 207 |
+
|
| 208 |
+
### 2️⃣ Frontend Setup (React)
|
| 209 |
+
|
| 210 |
+
Open a **new terminal tab**:
|
| 211 |
+
|
| 212 |
+
```bash
|
| 213 |
+
cd frontend
|
| 214 |
+
|
| 215 |
+
# Install Node.js dependencies
|
| 216 |
+
npm install
|
| 217 |
+
|
| 218 |
+
# Start the Vite development server
|
| 219 |
+
npm run dev
|
| 220 |
+
```
|
| 221 |
+
|
| 222 |
+
The application is now fully accessible at [http://localhost:5173](http://localhost:5173).
|
| 223 |
+
|
| 224 |
+
---
|
| 225 |
+
|
| 226 |
+
### 3️⃣ Pulling Models
|
| 227 |
+
|
| 228 |
+
To run the simulation locally without cloud API keys, you must ensure you pull suitable reasoning models through Ollama:
|
| 229 |
+
|
| 230 |
+
```bash
|
| 231 |
+
ollama run qwen2.5:3b # Excellent validator logic footprint
|
| 232 |
+
ollama run dolphin-llama3 # Uncensored investigative assertions
|
| 233 |
+
ollama pull all-minilm # Mandatory for semantic similarity scoring
|
| 234 |
+
```
|
| 235 |
+
|
| 236 |
+
---
|
| 237 |
+
|
| 238 |
+
## 🤝 Authors
|
| 239 |
+
**Developed by: Ashish Menon** & Vector
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/.gitignore
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python artifacts
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
venv/
|
| 6 |
+
.env
|
| 7 |
+
|
| 8 |
+
# Logs
|
| 9 |
+
*.log
|
| 10 |
+
|
| 11 |
+
# Scenarios data (if you want to keep it empty on GitHub)
|
| 12 |
+
scenarios/data/**/*.json
|
| 13 |
+
!scenarios/data/
|
backend/api/routes/config_routes.py
CHANGED
|
@@ -5,23 +5,23 @@ from utils.hardware import check_hardware
|
|
| 5 |
|
| 6 |
router = APIRouter()
|
| 7 |
|
| 8 |
-
from typing import List, Dict, Any
|
| 9 |
-
|
| 10 |
-
class AgentConfig(BaseModel):
|
| 11 |
-
id: str
|
| 12 |
-
model: str
|
| 13 |
-
provider: str
|
| 14 |
-
role: str = "INVESTIGATOR"
|
| 15 |
-
system_prompt: str = ""
|
| 16 |
-
temperature: float = 0.7
|
| 17 |
-
|
| 18 |
class ConfigUpdate(BaseModel):
|
| 19 |
MAX_STEPS: int
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
EXECUTION_MODE: str = "simulated"
|
| 22 |
SSH_HOST: str = ""
|
| 23 |
SSH_PORT: int = 22
|
| 24 |
SSH_USER: str = ""
|
|
|
|
| 25 |
SSH_PASSWORD: str = ""
|
| 26 |
OPENAI_API_KEY: str = ""
|
| 27 |
|
|
@@ -30,8 +30,17 @@ def get_config():
|
|
| 30 |
hw = check_hardware()
|
| 31 |
return {
|
| 32 |
"models": {
|
| 33 |
-
"
|
| 34 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
},
|
| 36 |
"episode": {
|
| 37 |
"max_steps": settings.MAX_STEPS,
|
|
@@ -50,8 +59,16 @@ def get_config():
|
|
| 50 |
@router.post("/config")
|
| 51 |
def update_config(req: ConfigUpdate):
|
| 52 |
settings.MAX_STEPS = req.MAX_STEPS
|
| 53 |
-
|
| 54 |
-
settings.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
settings.EXECUTION_MODE = req.EXECUTION_MODE
|
| 56 |
settings.SSH_HOST = req.SSH_HOST
|
| 57 |
settings.SSH_PORT = req.SSH_PORT
|
|
@@ -61,10 +78,18 @@ def update_config(req: ConfigUpdate):
|
|
| 61 |
|
| 62 |
# Persist to default.env
|
| 63 |
from models.model_manager import model_manager
|
| 64 |
-
import json
|
| 65 |
model_manager._update_env_file({
|
| 66 |
"MAX_STEPS": req.MAX_STEPS,
|
| 67 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
"EXECUTION_MODE": req.EXECUTION_MODE,
|
| 69 |
"SSH_HOST": req.SSH_HOST,
|
| 70 |
"SSH_PORT": req.SSH_PORT,
|
|
|
|
| 5 |
|
| 6 |
router = APIRouter()
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
class ConfigUpdate(BaseModel):
|
| 9 |
MAX_STEPS: int
|
| 10 |
+
AGENT_A_MODEL: str
|
| 11 |
+
AGENT_B_MODEL: str
|
| 12 |
+
AGENT_A_PROVIDER: str
|
| 13 |
+
AGENT_B_PROVIDER: str
|
| 14 |
+
AGENT_A_ROLE: str = "INVESTIGATOR"
|
| 15 |
+
AGENT_B_ROLE: str = "VALIDATOR"
|
| 16 |
+
AGENT_A_SYSTEM_PROMPT: str = ""
|
| 17 |
+
AGENT_B_SYSTEM_PROMPT: str = ""
|
| 18 |
+
AGENT_A_TEMPERATURE: float
|
| 19 |
+
AGENT_B_TEMPERATURE: float
|
| 20 |
EXECUTION_MODE: str = "simulated"
|
| 21 |
SSH_HOST: str = ""
|
| 22 |
SSH_PORT: int = 22
|
| 23 |
SSH_USER: str = ""
|
| 24 |
+
SSH_USER: str = ""
|
| 25 |
SSH_PASSWORD: str = ""
|
| 26 |
OPENAI_API_KEY: str = ""
|
| 27 |
|
|
|
|
| 30 |
hw = check_hardware()
|
| 31 |
return {
|
| 32 |
"models": {
|
| 33 |
+
"agent_a": settings.AGENT_A_MODEL,
|
| 34 |
+
"agent_b": settings.AGENT_B_MODEL,
|
| 35 |
+
"agent_a_provider": settings.AGENT_A_PROVIDER,
|
| 36 |
+
"agent_b_provider": settings.AGENT_B_PROVIDER,
|
| 37 |
+
"agent_a_role": settings.AGENT_A_ROLE,
|
| 38 |
+
"agent_b_role": settings.AGENT_B_ROLE,
|
| 39 |
+
"agent_a_system_prompt": settings.AGENT_A_SYSTEM_PROMPT,
|
| 40 |
+
"agent_b_system_prompt": settings.AGENT_B_SYSTEM_PROMPT,
|
| 41 |
+
"agent_a_temp": settings.AGENT_A_TEMPERATURE,
|
| 42 |
+
"agent_b_temp": settings.AGENT_B_TEMPERATURE,
|
| 43 |
+
"openai_api_key": settings.OPENAI_API_KEY
|
| 44 |
},
|
| 45 |
"episode": {
|
| 46 |
"max_steps": settings.MAX_STEPS,
|
|
|
|
| 59 |
@router.post("/config")
|
| 60 |
def update_config(req: ConfigUpdate):
|
| 61 |
settings.MAX_STEPS = req.MAX_STEPS
|
| 62 |
+
settings.AGENT_A_MODEL = req.AGENT_A_MODEL
|
| 63 |
+
settings.AGENT_B_MODEL = req.AGENT_B_MODEL
|
| 64 |
+
settings.AGENT_A_PROVIDER = req.AGENT_A_PROVIDER
|
| 65 |
+
settings.AGENT_B_PROVIDER = req.AGENT_B_PROVIDER
|
| 66 |
+
settings.AGENT_A_ROLE = req.AGENT_A_ROLE
|
| 67 |
+
settings.AGENT_B_ROLE = req.AGENT_B_ROLE
|
| 68 |
+
settings.AGENT_A_SYSTEM_PROMPT = req.AGENT_A_SYSTEM_PROMPT
|
| 69 |
+
settings.AGENT_B_SYSTEM_PROMPT = req.AGENT_B_SYSTEM_PROMPT
|
| 70 |
+
settings.AGENT_A_TEMPERATURE = req.AGENT_A_TEMPERATURE
|
| 71 |
+
settings.AGENT_B_TEMPERATURE = req.AGENT_B_TEMPERATURE
|
| 72 |
settings.EXECUTION_MODE = req.EXECUTION_MODE
|
| 73 |
settings.SSH_HOST = req.SSH_HOST
|
| 74 |
settings.SSH_PORT = req.SSH_PORT
|
|
|
|
| 78 |
|
| 79 |
# Persist to default.env
|
| 80 |
from models.model_manager import model_manager
|
|
|
|
| 81 |
model_manager._update_env_file({
|
| 82 |
"MAX_STEPS": req.MAX_STEPS,
|
| 83 |
+
"AGENT_A_MODEL": req.AGENT_A_MODEL,
|
| 84 |
+
"AGENT_B_MODEL": req.AGENT_B_MODEL,
|
| 85 |
+
"AGENT_A_PROVIDER": req.AGENT_A_PROVIDER,
|
| 86 |
+
"AGENT_B_PROVIDER": req.AGENT_B_PROVIDER,
|
| 87 |
+
"AGENT_A_ROLE": req.AGENT_A_ROLE,
|
| 88 |
+
"AGENT_B_ROLE": req.AGENT_B_ROLE,
|
| 89 |
+
"AGENT_A_SYSTEM_PROMPT": req.AGENT_A_SYSTEM_PROMPT,
|
| 90 |
+
"AGENT_B_SYSTEM_PROMPT": req.AGENT_B_SYSTEM_PROMPT,
|
| 91 |
+
"AGENT_A_TEMPERATURE": req.AGENT_A_TEMPERATURE,
|
| 92 |
+
"AGENT_B_TEMPERATURE": req.AGENT_B_TEMPERATURE,
|
| 93 |
"EXECUTION_MODE": req.EXECUTION_MODE,
|
| 94 |
"SSH_HOST": req.SSH_HOST,
|
| 95 |
"SSH_PORT": req.SSH_PORT,
|
backend/api/routes/model_routes.py
CHANGED
|
@@ -2,8 +2,6 @@ from fastapi import APIRouter, HTTPException
|
|
| 2 |
from pydantic import BaseModel
|
| 3 |
from models.model_manager import model_manager
|
| 4 |
from api.routes.websocket import broadcast
|
| 5 |
-
import httpx
|
| 6 |
-
import os
|
| 7 |
|
| 8 |
router = APIRouter()
|
| 9 |
|
|
@@ -19,90 +17,17 @@ class RemoveCustomModelReq(BaseModel):
|
|
| 19 |
class PullModelReq(BaseModel):
|
| 20 |
model_name: str
|
| 21 |
|
| 22 |
-
HF_MODELS = [
|
| 23 |
-
"meta-llama/Llama-3.1-8B-Instruct",
|
| 24 |
-
"meta-llama/Llama-3.2-1B-Instruct",
|
| 25 |
-
"meta-llama/Llama-3.3-70B-Instruct",
|
| 26 |
-
"meta-llama/Llama-4-Scout-17B-16E-Instruct",
|
| 27 |
-
"meta-llama/Llama-4-Maverick-17B-128E-Instruct",
|
| 28 |
-
"meta-llama/Meta-Llama-3-8B-Instruct",
|
| 29 |
-
"meta-llama/Meta-Llama-3-70B-Instruct",
|
| 30 |
-
"google/gemma-4-31B-it",
|
| 31 |
-
"google/gemma-4-26B-A4B-it",
|
| 32 |
-
"google/gemma-3-27b-it",
|
| 33 |
-
"google/gemma-3n-E4B-it",
|
| 34 |
-
"Qwen/Qwen3.5-9B",
|
| 35 |
-
"Qwen/Qwen2.5-7B-Instruct",
|
| 36 |
-
"Qwen/Qwen2.5-72B-Instruct",
|
| 37 |
-
"Qwen/Qwen2.5-Coder-7B-Instruct",
|
| 38 |
-
"Qwen/Qwen2.5-Coder-32B-Instruct",
|
| 39 |
-
"Qwen/Qwen3-8B",
|
| 40 |
-
"Qwen/Qwen3-32B",
|
| 41 |
-
"Qwen/Qwen3-4B-Instruct-2507",
|
| 42 |
-
"Qwen/Qwen3-14B",
|
| 43 |
-
"Qwen/Qwen3-VL-8B-Instruct",
|
| 44 |
-
"Qwen/Qwen3-VL-30B-A3B-Instruct",
|
| 45 |
-
"Qwen/QwQ-32B",
|
| 46 |
-
"deepseek-ai/DeepSeek-R1",
|
| 47 |
-
"deepseek-ai/DeepSeek-V3",
|
| 48 |
-
"deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B",
|
| 49 |
-
"deepseek-ai/DeepSeek-R1-Distill-Qwen-7B",
|
| 50 |
-
"deepseek-ai/DeepSeek-R1-Distill-Qwen-32B",
|
| 51 |
-
"deepseek-ai/DeepSeek-R1-Distill-Llama-70B",
|
| 52 |
-
"deepseek-ai/DeepSeek-R1-Distill-Llama-8B",
|
| 53 |
-
"deepseek-ai/DeepSeek-Prover-V2-671B",
|
| 54 |
-
"CohereLabs/c4ai-command-r-08-2024",
|
| 55 |
-
"CohereLabs/c4ai-command-r7b-arabic-02-2025",
|
| 56 |
-
"CohereLabs/command-a-vision-07-2025",
|
| 57 |
-
"CohereLabs/aya-expanse-32b",
|
| 58 |
-
"CohereLabs/aya-vision-32b",
|
| 59 |
-
"NousResearch/Hermes-2-Pro-Llama-3-8B",
|
| 60 |
-
"MiniMaxAI/MiniMax-M2.5",
|
| 61 |
-
"MiniMaxAI/MiniMax-M2",
|
| 62 |
-
"MiniMaxAI/MiniMax-M1-80k",
|
| 63 |
-
"moonshotai/Kimi-K2.5",
|
| 64 |
-
"moonshotai/Kimi-K2-Instruct",
|
| 65 |
-
"moonshotai/Kimi-K2-Thinking",
|
| 66 |
-
"xiaomiMiMo/MiMo-V2-Flash",
|
| 67 |
-
"zai-org/GLM-5",
|
| 68 |
-
"zai-org/GLM-4.7-Flash",
|
| 69 |
-
"zai-org/GLM-4.7",
|
| 70 |
-
"zai-org/GLM-4.6",
|
| 71 |
-
"zai-org/GLM-4.5",
|
| 72 |
-
]
|
| 73 |
-
|
| 74 |
@router.get("/models")
|
| 75 |
async def get_models():
|
| 76 |
local_models = await model_manager.list_available_models()
|
| 77 |
return {
|
| 78 |
"local_models": local_models,
|
| 79 |
-
"hf_models": HF_MODELS,
|
| 80 |
"custom_model": {
|
| 81 |
-
"enabled": True,
|
| 82 |
"agent": "agent_a"
|
| 83 |
}
|
| 84 |
}
|
| 85 |
|
| 86 |
-
@router.get("/models/hf")
|
| 87 |
-
async def get_hf_models():
|
| 88 |
-
hf_token = os.environ.get("HF_TOKEN", "") or "hf_demo"
|
| 89 |
-
|
| 90 |
-
try:
|
| 91 |
-
async with httpx.AsyncClient() as client:
|
| 92 |
-
resp = await client.get(
|
| 93 |
-
"https://router.huggingface.co/v1/models",
|
| 94 |
-
headers={"Authorization": f"Bearer {hf_token}"},
|
| 95 |
-
timeout=30.0
|
| 96 |
-
)
|
| 97 |
-
if resp.status_code == 200:
|
| 98 |
-
data = resp.json()
|
| 99 |
-
models = [m["id"] for m in data.get("data", [])]
|
| 100 |
-
return {"models": models, "source": "hf_router"}
|
| 101 |
-
except Exception as e:
|
| 102 |
-
pass
|
| 103 |
-
|
| 104 |
-
return {"models": HF_MODELS, "source": "fallback"}
|
| 105 |
-
|
| 106 |
@router.post("/models/add")
|
| 107 |
async def add_custom_model(req: AddCustomModelReq):
|
| 108 |
result = await model_manager.add_custom_model(
|
|
@@ -119,4 +44,5 @@ async def remove_custom_model(req: RemoveCustomModelReq):
|
|
| 119 |
|
| 120 |
@router.post("/models/pull")
|
| 121 |
async def pull_model(req: PullModelReq):
|
|
|
|
| 122 |
return {"message": "Streaming progress via WS not fully implemented but requested."}
|
|
|
|
| 2 |
from pydantic import BaseModel
|
| 3 |
from models.model_manager import model_manager
|
| 4 |
from api.routes.websocket import broadcast
|
|
|
|
|
|
|
| 5 |
|
| 6 |
router = APIRouter()
|
| 7 |
|
|
|
|
| 17 |
class PullModelReq(BaseModel):
|
| 18 |
model_name: str
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
@router.get("/models")
|
| 21 |
async def get_models():
|
| 22 |
local_models = await model_manager.list_available_models()
|
| 23 |
return {
|
| 24 |
"local_models": local_models,
|
|
|
|
| 25 |
"custom_model": {
|
| 26 |
+
"enabled": True, # Hardcode from settings in real app
|
| 27 |
"agent": "agent_a"
|
| 28 |
}
|
| 29 |
}
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
@router.post("/models/add")
|
| 32 |
async def add_custom_model(req: AddCustomModelReq):
|
| 33 |
result = await model_manager.add_custom_model(
|
|
|
|
| 44 |
|
| 45 |
@router.post("/models/pull")
|
| 46 |
async def pull_model(req: PullModelReq):
|
| 47 |
+
# Fire and forget streaming. Need a task runner ideally, but generator logic:
|
| 48 |
return {"message": "Streaming progress via WS not fully implemented but requested."}
|
backend/api/routes/openenv.py
CHANGED
|
@@ -22,9 +22,7 @@ async def simulation_loop():
|
|
| 22 |
|
| 23 |
step_num = 1
|
| 24 |
done = False
|
| 25 |
-
|
| 26 |
-
agent_list = getattr(settings, "AGENTS", [])
|
| 27 |
-
active_agent = agent_list[0]["id"] if agent_list else "agent_a"
|
| 28 |
|
| 29 |
while not done:
|
| 30 |
# Check if the episode was reset/cancelled
|
|
@@ -74,12 +72,7 @@ async def simulation_loop():
|
|
| 74 |
logger.error(f"Error in simulation loop at step {step_num}: {e}")
|
| 75 |
break
|
| 76 |
|
| 77 |
-
|
| 78 |
-
agent_list = settings.AGENTS if settings.AGENTS else [{"id": "agent_a"}]
|
| 79 |
-
current_idx = next((i for i, a in enumerate(agent_list) if a["id"] == active_agent), 0)
|
| 80 |
-
next_idx = (current_idx + 1) % len(agent_list)
|
| 81 |
-
active_agent = agent_list[next_idx]["id"]
|
| 82 |
-
|
| 83 |
step_num += 1
|
| 84 |
await asyncio.sleep(1)
|
| 85 |
|
|
@@ -88,7 +81,7 @@ async def simulation_loop():
|
|
| 88 |
from typing import Optional, Dict, Any
|
| 89 |
|
| 90 |
class ResetRequest(BaseModel):
|
| 91 |
-
task:
|
| 92 |
custom_scenario: Optional[Dict[str, Any]] = None
|
| 93 |
seed: Optional[int] = None
|
| 94 |
max_steps: Optional[int] = None
|
|
@@ -108,35 +101,16 @@ async def start_simulation():
|
|
| 108 |
if not episode_manager.env.active_episode:
|
| 109 |
logger.info("No active episode found for simulation. Performing auto-reset.")
|
| 110 |
await episode_manager.reset(task="software-incident")
|
| 111 |
-
else:
|
| 112 |
-
# Broadcast episode_start to notify frontend a new simulation is beginning
|
| 113 |
-
from api.routes.websocket import broadcast
|
| 114 |
-
sc_safe = episode_manager.env.active_scenario.copy()
|
| 115 |
-
if "root_cause" in sc_safe: del sc_safe["root_cause"]
|
| 116 |
-
if "correct_fix" in sc_safe: del sc_safe["correct_fix"]
|
| 117 |
-
if "clue_map" in sc_safe: del sc_safe["clue_map"]
|
| 118 |
-
from config import settings
|
| 119 |
-
await broadcast("episode_start", {
|
| 120 |
-
"episode_id": episode_manager.env.active_episode.episode_id,
|
| 121 |
-
"scenario": sc_safe,
|
| 122 |
-
"task": episode_manager.env.active_episode.task,
|
| 123 |
-
"difficulty": episode_manager.env.active_episode.difficulty,
|
| 124 |
-
"agents": settings.AGENTS
|
| 125 |
-
})
|
| 126 |
|
| 127 |
episode_manager.simulation_task = asyncio.create_task(simulation_loop())
|
| 128 |
from api.routes.websocket import broadcast
|
| 129 |
-
await broadcast("system_status", {"active": True, "paused": False
|
| 130 |
return {"status": "started"}
|
| 131 |
|
| 132 |
@router.post("/reset", response_model=NexusObservation)
|
| 133 |
-
async def reset_env(req:
|
| 134 |
try:
|
| 135 |
-
|
| 136 |
-
custom_scenario = req.custom_scenario if req else None
|
| 137 |
-
seed = req.seed if req else None
|
| 138 |
-
max_steps = req.max_steps if req else None
|
| 139 |
-
obs = await episode_manager.reset(task=task, custom_scenario=custom_scenario, seed=seed, max_steps=max_steps)
|
| 140 |
return obs
|
| 141 |
except Exception as e:
|
| 142 |
raise HTTPException(status_code=400, detail=str(e))
|
|
@@ -156,13 +130,11 @@ async def step_env(action: NexusAction):
|
|
| 156 |
except Exception as e:
|
| 157 |
raise HTTPException(status_code=500, detail=str(e))
|
| 158 |
|
| 159 |
-
@router.get("/state")
|
| 160 |
def get_state():
|
| 161 |
-
"""Returns the current episode state. Returns idle status if no episode is active."""
|
| 162 |
state = episode_manager.env.state()
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
return state.model_dump()
|
| 166 |
return state
|
| 167 |
|
| 168 |
@router.get("/telemetry")
|
|
|
|
| 22 |
|
| 23 |
step_num = 1
|
| 24 |
done = False
|
| 25 |
+
active_agent = "agent_a"
|
|
|
|
|
|
|
| 26 |
|
| 27 |
while not done:
|
| 28 |
# Check if the episode was reset/cancelled
|
|
|
|
| 72 |
logger.error(f"Error in simulation loop at step {step_num}: {e}")
|
| 73 |
break
|
| 74 |
|
| 75 |
+
active_agent = "agent_b" if active_agent == "agent_a" else "agent_a"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
step_num += 1
|
| 77 |
await asyncio.sleep(1)
|
| 78 |
|
|
|
|
| 81 |
from typing import Optional, Dict, Any
|
| 82 |
|
| 83 |
class ResetRequest(BaseModel):
|
| 84 |
+
task: str = "software-incident"
|
| 85 |
custom_scenario: Optional[Dict[str, Any]] = None
|
| 86 |
seed: Optional[int] = None
|
| 87 |
max_steps: Optional[int] = None
|
|
|
|
| 101 |
if not episode_manager.env.active_episode:
|
| 102 |
logger.info("No active episode found for simulation. Performing auto-reset.")
|
| 103 |
await episode_manager.reset(task="software-incident")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
episode_manager.simulation_task = asyncio.create_task(simulation_loop())
|
| 106 |
from api.routes.websocket import broadcast
|
| 107 |
+
await broadcast("system_status", {"active": True, "paused": False})
|
| 108 |
return {"status": "started"}
|
| 109 |
|
| 110 |
@router.post("/reset", response_model=NexusObservation)
|
| 111 |
+
async def reset_env(req: ResetRequest):
|
| 112 |
try:
|
| 113 |
+
obs = await episode_manager.reset(req.task, req.custom_scenario, seed=req.seed, max_steps=req.max_steps)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
return obs
|
| 115 |
except Exception as e:
|
| 116 |
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
| 130 |
except Exception as e:
|
| 131 |
raise HTTPException(status_code=500, detail=str(e))
|
| 132 |
|
| 133 |
+
@router.get("/state", response_model=NexusState)
|
| 134 |
def get_state():
|
|
|
|
| 135 |
state = episode_manager.env.state()
|
| 136 |
+
if not state:
|
| 137 |
+
raise HTTPException(status_code=400, detail="No active episode")
|
|
|
|
| 138 |
return state
|
| 139 |
|
| 140 |
@router.get("/telemetry")
|
backend/api/routes/websocket.py
CHANGED
|
@@ -60,8 +60,8 @@ async def websocket_endpoint(websocket: WebSocket):
|
|
| 60 |
})
|
| 61 |
elif action == "reset":
|
| 62 |
logger.info("UI Command: RESET")
|
| 63 |
-
await episode_manager.reset(task="software-incident"
|
| 64 |
-
await broadcast("system_status", {"paused": False, "status": "READY", "active":
|
| 65 |
elif action == "force_end":
|
| 66 |
logger.info("UI Command: FORCE_END")
|
| 67 |
if episode_manager.env and episode_manager.env.active_episode:
|
|
|
|
| 60 |
})
|
| 61 |
elif action == "reset":
|
| 62 |
logger.info("UI Command: RESET")
|
| 63 |
+
await episode_manager.reset(task="software-incident")
|
| 64 |
+
await broadcast("system_status", {"paused": False, "status": "READY", "active": false})
|
| 65 |
elif action == "force_end":
|
| 66 |
logger.info("UI Command: FORCE_END")
|
| 67 |
if episode_manager.env and episode_manager.env.active_episode:
|
backend/api/schemas/state.py
CHANGED
|
@@ -8,7 +8,8 @@ class NexusState(BaseModel):
|
|
| 8 |
difficulty: str
|
| 9 |
current_round: int
|
| 10 |
max_rounds: int
|
| 11 |
-
|
|
|
|
| 12 |
tool_calls_made: List[Dict]
|
| 13 |
clues_found: List[str]
|
| 14 |
root_cause_found: bool
|
|
|
|
| 8 |
difficulty: str
|
| 9 |
current_round: int
|
| 10 |
max_rounds: int
|
| 11 |
+
agent_a_messages: List[str]
|
| 12 |
+
agent_b_messages: List[str]
|
| 13 |
tool_calls_made: List[Dict]
|
| 14 |
clues_found: List[str]
|
| 15 |
root_cause_found: bool
|
backend/config.py
CHANGED
|
@@ -1,117 +1,63 @@
|
|
| 1 |
-
import
|
| 2 |
-
import
|
| 3 |
-
from
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
]
|
| 65 |
-
return agents
|
| 66 |
-
|
| 67 |
-
class Settings:
|
| 68 |
-
# OLLAMA
|
| 69 |
-
OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434/v1")
|
| 70 |
-
OLLAMA_API_KEY = os.getenv("OLLAMA_API_KEY", "ollama")
|
| 71 |
-
|
| 72 |
-
# AGENTS (Dynamic N-Agent Support)
|
| 73 |
-
try:
|
| 74 |
-
AGENTS_JSON = os.getenv("AGENTS_JSON")
|
| 75 |
-
AGENTS = json.loads(AGENTS_JSON) if AGENTS_JSON else _build_agents_from_env()
|
| 76 |
-
except:
|
| 77 |
-
AGENTS = _build_agents_from_env()
|
| 78 |
-
# EXECUTION ENVIRONMENT
|
| 79 |
-
EXECUTION_MODE = os.getenv("EXECUTION_MODE", "simulated")
|
| 80 |
-
SSH_HOST = os.getenv("SSH_HOST", "")
|
| 81 |
-
SSH_PORT = int(os.getenv("SSH_PORT", "22"))
|
| 82 |
-
SSH_USER = os.getenv("SSH_USER", "")
|
| 83 |
-
SSH_PASSWORD = os.getenv("SSH_PASSWORD", "")
|
| 84 |
-
|
| 85 |
-
# HUGGINGFACE
|
| 86 |
-
API_KEY = os.getenv("API_KEY", "ollama")
|
| 87 |
-
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
|
| 88 |
-
HF_TOKEN = os.getenv("HF_TOKEN", "")
|
| 89 |
-
HF_INFERENCE_URL = os.getenv("HF_INFERENCE_URL", "https://router.huggingface.co/v1")
|
| 90 |
-
|
| 91 |
-
# OPENROUTER
|
| 92 |
-
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "")
|
| 93 |
-
OPENROUTER_BASE_URL = os.getenv("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1")
|
| 94 |
-
|
| 95 |
-
# SERVER
|
| 96 |
-
HOST = os.getenv("HOST", "0.0.0.0")
|
| 97 |
-
PORT = int(os.getenv("PORT", "7860"))
|
| 98 |
-
DEBUG = os.getenv("DEBUG", "true").lower() in ("true", "1", "yes")
|
| 99 |
-
ENVIRONMENT = os.getenv("ENVIRONMENT", "local")
|
| 100 |
-
|
| 101 |
-
# EPISODE
|
| 102 |
-
MAX_STEPS = int(os.getenv("MAX_STEPS", "1000"))
|
| 103 |
-
MAX_EPISODE_TIME_SECONDS = int(os.getenv("MAX_EPISODE_TIME_SECONDS", "1200"))
|
| 104 |
-
SUCCESS_SCORE_THRESHOLD = float(os.getenv("SUCCESS_SCORE_THRESHOLD", "0.5"))
|
| 105 |
-
|
| 106 |
-
# MCP TOOL SERVER
|
| 107 |
-
MCP_SERVER_PORT = int(os.getenv("MCP_SERVER_PORT", "8001"))
|
| 108 |
-
MCP_SERVER_URL = os.getenv("MCP_SERVER_URL", "http://localhost:8001")
|
| 109 |
-
|
| 110 |
-
# CUSTOM MODEL
|
| 111 |
-
CUSTOM_MODEL_ENABLED = os.getenv("CUSTOM_MODEL_ENABLED", "false").lower() in ("true", "1", "yes")
|
| 112 |
-
CUSTOM_MODEL_BASE_URL = os.getenv("CUSTOM_MODEL_BASE_URL", "")
|
| 113 |
-
CUSTOM_MODEL_API_KEY = os.getenv("CUSTOM_MODEL_API_KEY", "")
|
| 114 |
-
CUSTOM_MODEL_NAME = os.getenv("CUSTOM_MODEL_NAME", "")
|
| 115 |
-
CUSTOM_MODEL_AGENT = os.getenv("CUSTOM_MODEL_AGENT", "")
|
| 116 |
-
|
| 117 |
-
settings = Settings()
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
|
| 5 |
+
BASE_DIR = Path(__file__).resolve().parent
|
| 6 |
+
|
| 7 |
+
# Load environment variables, overriding defaults if needed
|
| 8 |
+
load_dotenv(BASE_DIR / ".env")
|
| 9 |
+
|
| 10 |
+
class Settings:
|
| 11 |
+
# OLLAMA
|
| 12 |
+
OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434/v1")
|
| 13 |
+
OLLAMA_API_KEY = os.getenv("OLLAMA_API_KEY", "ollama")
|
| 14 |
+
|
| 15 |
+
# AGENTS
|
| 16 |
+
AGENT_A_MODEL = os.getenv("AGENT_A_MODEL", "")
|
| 17 |
+
AGENT_B_MODEL = os.getenv("AGENT_B_MODEL", "")
|
| 18 |
+
AGENT_A_PROVIDER = os.getenv("AGENT_A_PROVIDER", "ollama")
|
| 19 |
+
AGENT_B_PROVIDER = os.getenv("AGENT_B_PROVIDER", "ollama")
|
| 20 |
+
AGENT_A_ROLE = os.getenv("AGENT_A_ROLE", "INVESTIGATOR")
|
| 21 |
+
AGENT_B_ROLE = os.getenv("AGENT_B_ROLE", "VALIDATOR")
|
| 22 |
+
AGENT_A_SYSTEM_PROMPT = os.getenv("AGENT_A_SYSTEM_PROMPT", "")
|
| 23 |
+
AGENT_B_SYSTEM_PROMPT = os.getenv("AGENT_B_SYSTEM_PROMPT", "")
|
| 24 |
+
AGENT_A_TEMPERATURE = float(os.getenv("AGENT_A_TEMPERATURE", "0.8"))
|
| 25 |
+
AGENT_B_TEMPERATURE = float(os.getenv("AGENT_B_TEMPERATURE", "0.6"))
|
| 26 |
+
AGENT_A_MAX_TOKENS = int(os.getenv("AGENT_A_MAX_TOKENS", "300"))
|
| 27 |
+
AGENT_B_MAX_TOKENS = int(os.getenv("AGENT_B_MAX_TOKENS", "300"))
|
| 28 |
+
# EXECUTION ENVIRONMENT
|
| 29 |
+
EXECUTION_MODE = os.getenv("EXECUTION_MODE", "simulated")
|
| 30 |
+
SSH_HOST = os.getenv("SSH_HOST", "")
|
| 31 |
+
SSH_PORT = int(os.getenv("SSH_PORT", "22"))
|
| 32 |
+
SSH_USER = os.getenv("SSH_USER", "")
|
| 33 |
+
SSH_PASSWORD = os.getenv("SSH_PASSWORD", "")
|
| 34 |
+
|
| 35 |
+
# HUGGINGFACE
|
| 36 |
+
API_KEY = os.getenv("API_KEY", "ollama")
|
| 37 |
+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
|
| 38 |
+
HF_TOKEN = os.getenv("HF_TOKEN", "")
|
| 39 |
+
HF_INFERENCE_URL = os.getenv("HF_INFERENCE_URL", "https://api-inference.huggingface.co/v1")
|
| 40 |
+
|
| 41 |
+
# SERVER
|
| 42 |
+
HOST = os.getenv("HOST", "0.0.0.0")
|
| 43 |
+
PORT = int(os.getenv("PORT", "7860"))
|
| 44 |
+
DEBUG = os.getenv("DEBUG", "true").lower() in ("true", "1", "yes")
|
| 45 |
+
ENVIRONMENT = os.getenv("ENVIRONMENT", "local")
|
| 46 |
+
|
| 47 |
+
# EPISODE
|
| 48 |
+
MAX_STEPS = int(os.getenv("MAX_STEPS", "1000"))
|
| 49 |
+
MAX_EPISODE_TIME_SECONDS = int(os.getenv("MAX_EPISODE_TIME_SECONDS", "1200"))
|
| 50 |
+
SUCCESS_SCORE_THRESHOLD = float(os.getenv("SUCCESS_SCORE_THRESHOLD", "0.5"))
|
| 51 |
+
|
| 52 |
+
# MCP TOOL SERVER
|
| 53 |
+
MCP_SERVER_PORT = int(os.getenv("MCP_SERVER_PORT", "8001"))
|
| 54 |
+
MCP_SERVER_URL = os.getenv("MCP_SERVER_URL", "http://localhost:8001")
|
| 55 |
+
|
| 56 |
+
# CUSTOM MODEL
|
| 57 |
+
CUSTOM_MODEL_ENABLED = os.getenv("CUSTOM_MODEL_ENABLED", "false").lower() in ("true", "1", "yes")
|
| 58 |
+
CUSTOM_MODEL_BASE_URL = os.getenv("CUSTOM_MODEL_BASE_URL", "")
|
| 59 |
+
CUSTOM_MODEL_API_KEY = os.getenv("CUSTOM_MODEL_API_KEY", "")
|
| 60 |
+
CUSTOM_MODEL_NAME = os.getenv("CUSTOM_MODEL_NAME", "")
|
| 61 |
+
CUSTOM_MODEL_AGENT = os.getenv("CUSTOM_MODEL_AGENT", "")
|
| 62 |
+
|
| 63 |
+
settings = Settings()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/core/agent_runner.py
CHANGED
|
@@ -1,150 +1,141 @@
|
|
| 1 |
-
import re
|
| 2 |
-
import
|
| 3 |
-
from
|
| 4 |
-
from
|
| 5 |
-
from
|
| 6 |
-
from
|
| 7 |
-
from
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
"
|
| 12 |
-
"
|
| 13 |
-
"
|
| 14 |
-
"
|
| 15 |
-
"
|
| 16 |
-
"
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
You
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
You
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
k
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
#
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
"
|
| 73 |
-
"
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
sys_prompt =
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
return
|
| 143 |
-
except Exception as e:
|
| 144 |
-
last_error = e
|
| 145 |
-
logger.warning(f"Attempt {attempt + 1}/{max_retries + 1} failed for {model_name}: {e}")
|
| 146 |
-
if attempt < max_retries:
|
| 147 |
-
await asyncio.sleep(2 ** attempt)
|
| 148 |
-
|
| 149 |
-
logger.error(f"All retries exhausted for {model_name}: {last_error}")
|
| 150 |
-
yield f"I encountered an error: {last_error}. Please verify the model endpoint is accessible."
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
from typing import List
|
| 3 |
+
from api.schemas.action import ToolCall
|
| 4 |
+
from models.model_manager import model_manager
|
| 5 |
+
from tools.tool_registry import registry
|
| 6 |
+
from utils.logger import logger
|
| 7 |
+
from config import settings
|
| 8 |
+
|
| 9 |
+
ROLE_DEFINITIONS = {
|
| 10 |
+
"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.",
|
| 11 |
+
"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.",
|
| 12 |
+
"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.",
|
| 13 |
+
"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.",
|
| 14 |
+
"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.",
|
| 15 |
+
"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.",
|
| 16 |
+
"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."
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
TOOL_INSTRUCTIONS_SIMULATED = """
|
| 20 |
+
You have access to simulation tools. When calling a tool write exactly: TOOL: tool_name(param="value")
|
| 21 |
+
You can call multiple tools per message. You must use tools like update_config and restart_service to fix the system.
|
| 22 |
+
Once the fix is verified entirely, call TOOL: submit_resolution(root_cause_service="...", root_cause_description="...", fix_applied="...") to end the investigation.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
TOOL_INSTRUCTIONS_SSH = """
|
| 26 |
+
You are operating on a LIVE remote Linux server. You have a real bash terminal via the run_terminal_command tool. USE IT AGGRESSIVELY.
|
| 27 |
+
You have root access. Do NOT theorize without evidence — run actual commands to get facts.
|
| 28 |
+
When calling a tool write exactly: TOOL: run_terminal_command(command="your bash command here")
|
| 29 |
+
Examples: TOOL: run_terminal_command(command="journalctl -n 50 --no-pager")
|
| 30 |
+
TOOL: run_terminal_command(command="systemctl status nginx")
|
| 31 |
+
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.
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
class AgentRunner:
|
| 35 |
+
def parse_tool_calls(self, message: str) -> List[ToolCall]:
|
| 36 |
+
# Parse "TOOL: tool_name(param="value")"
|
| 37 |
+
tool_calls = []
|
| 38 |
+
pattern = r'TOOL:\s*([a-zA-Z0-9_]+)\((.*?)\)'
|
| 39 |
+
matches = re.finditer(pattern, message)
|
| 40 |
+
|
| 41 |
+
for match in matches:
|
| 42 |
+
tool_name = match.group(1)
|
| 43 |
+
params_str = match.group(2)
|
| 44 |
+
|
| 45 |
+
# Simple param parsing - expects key="value", key='value' or key=value
|
| 46 |
+
params = {}
|
| 47 |
+
if params_str.strip():
|
| 48 |
+
param_pairs = params_str.split(',')
|
| 49 |
+
for pair in param_pairs:
|
| 50 |
+
if '=' in pair:
|
| 51 |
+
k, v = pair.split('=', 1)
|
| 52 |
+
k = k.strip()
|
| 53 |
+
v = v.strip().strip('"').strip("'")
|
| 54 |
+
params[k] = v
|
| 55 |
+
|
| 56 |
+
tool_calls.append(ToolCall(tool_name=tool_name, params=params))
|
| 57 |
+
|
| 58 |
+
return tool_calls
|
| 59 |
+
|
| 60 |
+
async def execute_tool_calls(self, tool_calls: List[ToolCall], scenario: dict, round_num: int, episode_state) -> List[dict]:
|
| 61 |
+
results = []
|
| 62 |
+
for tc in tool_calls:
|
| 63 |
+
# We call the registry. In reality, MCP might be external but here it's in-process registry calls
|
| 64 |
+
# Episode state is passed for propose_fix and verify_fix
|
| 65 |
+
res_str = registry.call_tool(tc.tool_name, tc.params, scenario, round_num, episode_state)
|
| 66 |
+
|
| 67 |
+
# Record it in state
|
| 68 |
+
episode_state.add_tool_call(tc.tool_name, tc.params)
|
| 69 |
+
|
| 70 |
+
results.append({
|
| 71 |
+
"tool_name": tc.tool_name,
|
| 72 |
+
"result": res_str,
|
| 73 |
+
"success": not res_str.startswith("Error")
|
| 74 |
+
})
|
| 75 |
+
return results
|
| 76 |
+
|
| 77 |
+
async def run_step(self, agent_id: str, episode_state, scenario: dict):
|
| 78 |
+
client, model_name = model_manager.get_client(agent_id)
|
| 79 |
+
|
| 80 |
+
is_ssh = settings.EXECUTION_MODE == "ssh"
|
| 81 |
+
tool_rules = TOOL_INSTRUCTIONS_SSH if is_ssh else TOOL_INSTRUCTIONS_SIMULATED
|
| 82 |
+
|
| 83 |
+
# Build System Prompt based on mapping
|
| 84 |
+
if agent_id == "agent_a":
|
| 85 |
+
role = settings.AGENT_A_ROLE
|
| 86 |
+
custom_prompt = settings.AGENT_A_SYSTEM_PROMPT
|
| 87 |
+
else:
|
| 88 |
+
role = settings.AGENT_B_ROLE
|
| 89 |
+
custom_prompt = settings.AGENT_B_SYSTEM_PROMPT
|
| 90 |
+
|
| 91 |
+
if role.startswith("CUSTOM_") and custom_prompt:
|
| 92 |
+
sys_prompt = custom_prompt + "\n\n" + tool_rules
|
| 93 |
+
else:
|
| 94 |
+
behavior = ROLE_DEFINITIONS.get(role, ROLE_DEFINITIONS["INVESTIGATOR"])
|
| 95 |
+
sys_prompt = behavior + "\n\n" + tool_rules
|
| 96 |
+
|
| 97 |
+
# Build context
|
| 98 |
+
context = f"Current incident: {scenario.get('description', '')}\n"
|
| 99 |
+
if hasattr(episode_state, 'last_partner_message') and episode_state.last_partner_message:
|
| 100 |
+
context += f"Partner's last message: {episode_state.last_partner_message}\n"
|
| 101 |
+
if hasattr(episode_state, 'clues_found') and episode_state.clues_found:
|
| 102 |
+
context += f"Clues found: {episode_state.clues_found}\n"
|
| 103 |
+
# Note: don't mention rounds - let agents reason freely until consensus
|
| 104 |
+
|
| 105 |
+
# We append history
|
| 106 |
+
messages = [{"role": "system", "content": sys_prompt}]
|
| 107 |
+
|
| 108 |
+
# Add a summary of previous messages
|
| 109 |
+
if hasattr(episode_state, 'all_messages'):
|
| 110 |
+
all_msgs = episode_state.all_messages[-3:] # only last 3 to fit context
|
| 111 |
+
if all_msgs:
|
| 112 |
+
context += "\nRecent history:\n"
|
| 113 |
+
for m in all_msgs:
|
| 114 |
+
context += f"- {m}\n"
|
| 115 |
+
|
| 116 |
+
messages.append({"role": "user", "content": context})
|
| 117 |
+
|
| 118 |
+
# Call model with streaming
|
| 119 |
+
full_response = ""
|
| 120 |
+
try:
|
| 121 |
+
stream = await client.chat.completions.create(
|
| 122 |
+
model=model_name,
|
| 123 |
+
messages=messages,
|
| 124 |
+
max_tokens=2048,
|
| 125 |
+
timeout=60.0,
|
| 126 |
+
stream=True
|
| 127 |
+
)
|
| 128 |
+
async for chunk in stream:
|
| 129 |
+
content = chunk.choices[0].delta.content or ""
|
| 130 |
+
if content:
|
| 131 |
+
full_response += content
|
| 132 |
+
yield content # Yield partial chunk
|
| 133 |
+
except Exception as e:
|
| 134 |
+
logger.error(f"Error calling model {model_name} for {agent_id}: {e}")
|
| 135 |
+
full_response = "I encountered an error analyzing the situation. Let me try again next round."
|
| 136 |
+
yield full_response
|
| 137 |
+
|
| 138 |
+
# Final yielding of special end marker or just finish
|
| 139 |
+
# The caller (openenv.py) will collect all yielded values to build the full response
|
| 140 |
+
# and then call runner.parse_tool_calls(full_message) themselves.
|
| 141 |
+
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/core/episode_manager.py
CHANGED
|
@@ -9,7 +9,7 @@ class EpisodeManager:
|
|
| 9 |
self.is_paused = False
|
| 10 |
self.simulation_task = None
|
| 11 |
|
| 12 |
-
async def reset(self, task: str, custom_scenario: dict = None, seed: int = None, max_steps: int = None
|
| 13 |
# Cancel any active simulation loop
|
| 14 |
if hasattr(self, 'simulation_task') and self.simulation_task and not self.simulation_task.done():
|
| 15 |
self.simulation_task.cancel()
|
|
@@ -21,21 +21,21 @@ class EpisodeManager:
|
|
| 21 |
|
| 22 |
obs = await self.env.reset(task=task, custom_scenario=custom_scenario, seed=seed, max_steps=max_steps)
|
| 23 |
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
return obs
|
| 40 |
|
| 41 |
async def step(self, action):
|
|
@@ -81,11 +81,9 @@ class EpisodeManager:
|
|
| 81 |
"success": info.get("success", False),
|
| 82 |
"steps_taken": self.env.active_episode.steps_taken,
|
| 83 |
"final_score": info.get("final_score", getattr(self.env.active_episode, "cumulative_reward", 0)),
|
| 84 |
-
"final_breakdown": info.get("breakdown", {}),
|
| 85 |
-
"clues_found": self.env.active_episode.clues_found,
|
| 86 |
"root_cause_found": self.env.active_episode.fix_correct,
|
| 87 |
"fix_verified": self.env.active_episode.fix_verified,
|
| 88 |
-
"time_taken_seconds": 0,
|
| 89 |
"reward_history": self.env.active_episode.reward_history
|
| 90 |
})
|
| 91 |
|
|
|
|
| 9 |
self.is_paused = False
|
| 10 |
self.simulation_task = None
|
| 11 |
|
| 12 |
+
async def reset(self, task: str, custom_scenario: dict = None, seed: int = None, max_steps: int = None):
|
| 13 |
# Cancel any active simulation loop
|
| 14 |
if hasattr(self, 'simulation_task') and self.simulation_task and not self.simulation_task.done():
|
| 15 |
self.simulation_task.cancel()
|
|
|
|
| 21 |
|
| 22 |
obs = await self.env.reset(task=task, custom_scenario=custom_scenario, seed=seed, max_steps=max_steps)
|
| 23 |
|
| 24 |
+
# Broadcast episode_start
|
| 25 |
+
sc_safe = self.env.active_scenario.copy()
|
| 26 |
+
if "root_cause" in sc_safe: del sc_safe["root_cause"]
|
| 27 |
+
if "correct_fix" in sc_safe: del sc_safe["correct_fix"]
|
| 28 |
+
if "clue_map" in sc_safe: del sc_safe["clue_map"]
|
| 29 |
+
|
| 30 |
+
from config import settings
|
| 31 |
+
await broadcast("episode_start", {
|
| 32 |
+
"episode_id": self.env.active_episode.episode_id,
|
| 33 |
+
"scenario": sc_safe,
|
| 34 |
+
"task": task,
|
| 35 |
+
"difficulty": self.env.active_episode.difficulty,
|
| 36 |
+
"agent_a_model": settings.AGENT_A_MODEL,
|
| 37 |
+
"agent_b_model": settings.AGENT_B_MODEL
|
| 38 |
+
})
|
| 39 |
return obs
|
| 40 |
|
| 41 |
async def step(self, action):
|
|
|
|
| 81 |
"success": info.get("success", False),
|
| 82 |
"steps_taken": self.env.active_episode.steps_taken,
|
| 83 |
"final_score": info.get("final_score", getattr(self.env.active_episode, "cumulative_reward", 0)),
|
|
|
|
|
|
|
| 84 |
"root_cause_found": self.env.active_episode.fix_correct,
|
| 85 |
"fix_verified": self.env.active_episode.fix_verified,
|
| 86 |
+
"time_taken_seconds": 0, # could track
|
| 87 |
"reward_history": self.env.active_episode.reward_history
|
| 88 |
})
|
| 89 |
|
backend/core/state_manager.py
CHANGED
|
@@ -11,8 +11,8 @@ class EpisodeState:
|
|
| 11 |
self.current_round = 1
|
| 12 |
self.max_rounds = max_rounds
|
| 13 |
|
| 14 |
-
|
| 15 |
-
self.
|
| 16 |
self.all_messages: List[str] = []
|
| 17 |
|
| 18 |
self.tool_calls_made: List[Dict] = []
|
|
@@ -39,15 +39,11 @@ class EpisodeState:
|
|
| 39 |
def add_message(self, agent_id: str, message: str):
|
| 40 |
self.steps_taken += 1
|
| 41 |
self.all_messages.append(message)
|
| 42 |
-
if agent_id
|
| 43 |
-
self.
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
# A full round is defined as all agents having spoken at least once in the current sequence
|
| 48 |
-
# We can approximate this by incrementing round when the last agent in the list speaks
|
| 49 |
-
if settings.AGENTS and agent_id == settings.AGENTS[-1]["id"]:
|
| 50 |
-
self.current_round += 1
|
| 51 |
|
| 52 |
self.last_partner_message = message
|
| 53 |
|
|
@@ -68,7 +64,8 @@ class EpisodeState:
|
|
| 68 |
difficulty=self.difficulty,
|
| 69 |
current_round=self.current_round,
|
| 70 |
max_rounds=self.max_rounds,
|
| 71 |
-
|
|
|
|
| 72 |
tool_calls_made=self.tool_calls_made,
|
| 73 |
clues_found=self.clues_found,
|
| 74 |
root_cause_found=self.root_cause_found,
|
|
|
|
| 11 |
self.current_round = 1
|
| 12 |
self.max_rounds = max_rounds
|
| 13 |
|
| 14 |
+
self.agent_a_messages: List[str] = []
|
| 15 |
+
self.agent_b_messages: List[str] = []
|
| 16 |
self.all_messages: List[str] = []
|
| 17 |
|
| 18 |
self.tool_calls_made: List[Dict] = []
|
|
|
|
| 39 |
def add_message(self, agent_id: str, message: str):
|
| 40 |
self.steps_taken += 1
|
| 41 |
self.all_messages.append(message)
|
| 42 |
+
if agent_id == "agent_a":
|
| 43 |
+
self.agent_a_messages.append(message)
|
| 44 |
+
else:
|
| 45 |
+
self.agent_b_messages.append(message)
|
| 46 |
+
self.current_round += 1 # A full round is both agents speaking
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
self.last_partner_message = message
|
| 49 |
|
|
|
|
| 64 |
difficulty=self.difficulty,
|
| 65 |
current_round=self.current_round,
|
| 66 |
max_rounds=self.max_rounds,
|
| 67 |
+
agent_a_messages=self.agent_a_messages,
|
| 68 |
+
agent_b_messages=self.agent_b_messages,
|
| 69 |
tool_calls_made=self.tool_calls_made,
|
| 70 |
clues_found=self.clues_found,
|
| 71 |
root_cause_found=self.root_cause_found,
|
backend/main.py
CHANGED
|
@@ -94,10 +94,4 @@ async def startup_event():
|
|
| 94 |
run_mcp()
|
| 95 |
|
| 96 |
if __name__ == "__main__":
|
| 97 |
-
|
| 98 |
-
uvicorn.run(app, host=settings.HOST, port=settings.PORT)
|
| 99 |
-
except Exception as e:
|
| 100 |
-
import traceback
|
| 101 |
-
print(f"FATAL ERROR AT STARTUP: {str(e)}")
|
| 102 |
-
traceback.print_exc()
|
| 103 |
-
os._exit(1)
|
|
|
|
| 94 |
run_mcp()
|
| 95 |
|
| 96 |
if __name__ == "__main__":
|
| 97 |
+
uvicorn.run(app, host=settings.HOST, port=settings.PORT)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/models/model_manager.py
CHANGED
|
@@ -1,162 +1,115 @@
|
|
| 1 |
-
import os
|
| 2 |
-
from typing import Tuple, Dict, List
|
| 3 |
-
from openai import AsyncOpenAI
|
| 4 |
-
import httpx
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
from
|
| 8 |
-
from .
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
self.
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
if
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
except:
|
| 79 |
-
return False
|
| 80 |
-
|
| 81 |
-
async def
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
env_map = {"CUSTOM_MODEL_ENABLED": "false"}
|
| 117 |
-
self._update_env_file(env_map)
|
| 118 |
-
settings.CUSTOM_MODEL_ENABLED = False
|
| 119 |
-
|
| 120 |
-
async def list_available_models(self) -> List[str]:
|
| 121 |
-
hf_token = os.environ.get("HF_TOKEN", "") or settings.HF_TOKEN or ""
|
| 122 |
-
if hf_token and hf_token not in ("", "your_huggingface_token_here", "ollama", "hf_YourTokenHere"):
|
| 123 |
-
try:
|
| 124 |
-
async with httpx.AsyncClient() as client:
|
| 125 |
-
resp = await client.get(
|
| 126 |
-
"https://huggingface.co/api/models",
|
| 127 |
-
headers={"Authorization": f"Bearer {hf_token}"},
|
| 128 |
-
timeout=30.0
|
| 129 |
-
)
|
| 130 |
-
if resp.status_code == 200:
|
| 131 |
-
models = resp.json()
|
| 132 |
-
return [m["id"] for m in models[:50]]
|
| 133 |
-
except:
|
| 134 |
-
pass
|
| 135 |
-
return await self.ollama.list_models()
|
| 136 |
-
|
| 137 |
-
def pull_model(self, model_name: str):
|
| 138 |
-
return self.ollama.pull_model(model_name)
|
| 139 |
-
|
| 140 |
-
def _update_env_file(self, overrides: dict):
|
| 141 |
-
env_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "default.env")
|
| 142 |
-
if not os.path.exists(env_path):
|
| 143 |
-
return
|
| 144 |
-
|
| 145 |
-
with open(env_path, "r") as f:
|
| 146 |
-
lines = f.readlines()
|
| 147 |
-
|
| 148 |
-
new_lines = []
|
| 149 |
-
for line in lines:
|
| 150 |
-
updated = False
|
| 151 |
-
for k, v in overrides.items():
|
| 152 |
-
if line.startswith(f"{k}="):
|
| 153 |
-
new_lines.append(f"{k}={v}\n")
|
| 154 |
-
updated = True
|
| 155 |
-
break
|
| 156 |
-
if not updated:
|
| 157 |
-
new_lines.append(line)
|
| 158 |
-
|
| 159 |
-
with open(env_path, "w") as f:
|
| 160 |
-
f.writelines(new_lines)
|
| 161 |
-
|
| 162 |
-
model_manager = ModelManager()
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from typing import Tuple, Dict, List
|
| 3 |
+
from openai import AsyncOpenAI
|
| 4 |
+
import httpx
|
| 5 |
+
import json
|
| 6 |
+
|
| 7 |
+
from config import settings
|
| 8 |
+
from .ollama_client import OllamaClient
|
| 9 |
+
from .hf_client import HFClient
|
| 10 |
+
|
| 11 |
+
class ModelManager:
|
| 12 |
+
def __init__(self):
|
| 13 |
+
self.ollama = OllamaClient(settings.OLLAMA_BASE_URL, settings.OLLAMA_API_KEY)
|
| 14 |
+
self.hf = None
|
| 15 |
+
if settings.HF_TOKEN and settings.HF_TOKEN != "your_huggingface_token_here":
|
| 16 |
+
self.hf = HFClient(settings.HF_INFERENCE_URL, settings.HF_TOKEN)
|
| 17 |
+
|
| 18 |
+
def get_client(self, agent_id: str) -> Tuple[AsyncOpenAI, str]:
|
| 19 |
+
# Check if custom model set for this agent
|
| 20 |
+
if settings.CUSTOM_MODEL_ENABLED:
|
| 21 |
+
if settings.CUSTOM_MODEL_AGENT.lower() == agent_id.lower() or settings.CUSTOM_MODEL_AGENT.lower() == "both":
|
| 22 |
+
client = AsyncOpenAI(
|
| 23 |
+
base_url=settings.CUSTOM_MODEL_BASE_URL,
|
| 24 |
+
api_key=settings.CUSTOM_MODEL_API_KEY or "none"
|
| 25 |
+
)
|
| 26 |
+
return client, settings.CUSTOM_MODEL_NAME
|
| 27 |
+
|
| 28 |
+
# Determine provider and model
|
| 29 |
+
if agent_id == "agent_a":
|
| 30 |
+
provider = settings.AGENT_A_PROVIDER
|
| 31 |
+
model_name = settings.AGENT_A_MODEL
|
| 32 |
+
else:
|
| 33 |
+
provider = settings.AGENT_B_PROVIDER
|
| 34 |
+
model_name = settings.AGENT_B_MODEL
|
| 35 |
+
|
| 36 |
+
if provider == "hf" and self.hf:
|
| 37 |
+
return self.hf.get_client(), model_name
|
| 38 |
+
elif provider == "openai":
|
| 39 |
+
# We spin up OpenAI dynamically pulling the global OpenAI Key
|
| 40 |
+
client = AsyncOpenAI(api_key=settings.OPENAI_API_KEY)
|
| 41 |
+
return client, model_name
|
| 42 |
+
|
| 43 |
+
return self.ollama.get_client(), model_name
|
| 44 |
+
|
| 45 |
+
async def add_custom_model(self, agent_id: str, base_url: str, api_key: str, model_name: str) -> dict:
|
| 46 |
+
try:
|
| 47 |
+
# Validate endpoint and test with simple completion
|
| 48 |
+
client = AsyncOpenAI(base_url=base_url, api_key=api_key or "none")
|
| 49 |
+
response = await client.chat.completions.create(
|
| 50 |
+
model=model_name,
|
| 51 |
+
messages=[{"role": "user", "content": "Say 'hello' in exactly one word."}],
|
| 52 |
+
max_tokens=10,
|
| 53 |
+
timeout=10.0
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
if response and response.choices:
|
| 57 |
+
# Test passed, update .env file dynamically
|
| 58 |
+
env_map = {
|
| 59 |
+
"CUSTOM_MODEL_ENABLED": "true",
|
| 60 |
+
"CUSTOM_MODEL_BASE_URL": base_url,
|
| 61 |
+
"CUSTOM_MODEL_API_KEY": api_key,
|
| 62 |
+
"CUSTOM_MODEL_NAME": model_name,
|
| 63 |
+
"CUSTOM_MODEL_AGENT": agent_id
|
| 64 |
+
}
|
| 65 |
+
self._update_env_file(env_map)
|
| 66 |
+
|
| 67 |
+
# Update runtime config
|
| 68 |
+
settings.CUSTOM_MODEL_ENABLED = True
|
| 69 |
+
settings.CUSTOM_MODEL_BASE_URL = base_url
|
| 70 |
+
settings.CUSTOM_MODEL_API_KEY = api_key
|
| 71 |
+
settings.CUSTOM_MODEL_NAME = model_name
|
| 72 |
+
settings.CUSTOM_MODEL_AGENT = agent_id
|
| 73 |
+
|
| 74 |
+
return {"success": True, "message": "Custom model verified and activated."}
|
| 75 |
+
else:
|
| 76 |
+
return {"success": False, "message": "Model did not return a valid completion."}
|
| 77 |
+
|
| 78 |
+
except Exception as e:
|
| 79 |
+
return {"success": False, "message": f"Validation failed: {str(e)}"}
|
| 80 |
+
|
| 81 |
+
async def remove_custom_model(self, agent_id: str):
|
| 82 |
+
if settings.CUSTOM_MODEL_AGENT.lower() == agent_id.lower() or settings.CUSTOM_MODEL_AGENT.lower() == "both":
|
| 83 |
+
env_map = {"CUSTOM_MODEL_ENABLED": "false"}
|
| 84 |
+
self._update_env_file(env_map)
|
| 85 |
+
settings.CUSTOM_MODEL_ENABLED = False
|
| 86 |
+
|
| 87 |
+
async def list_available_models(self) -> List[str]:
|
| 88 |
+
return await self.ollama.list_models()
|
| 89 |
+
|
| 90 |
+
def pull_model(self, model_name: str):
|
| 91 |
+
return self.ollama.pull_model(model_name)
|
| 92 |
+
|
| 93 |
+
def _update_env_file(self, overrides: dict):
|
| 94 |
+
env_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "default.env")
|
| 95 |
+
if not os.path.exists(env_path):
|
| 96 |
+
return
|
| 97 |
+
|
| 98 |
+
with open(env_path, "r") as f:
|
| 99 |
+
lines = f.readlines()
|
| 100 |
+
|
| 101 |
+
new_lines = []
|
| 102 |
+
for line in lines:
|
| 103 |
+
updated = False
|
| 104 |
+
for k, v in overrides.items():
|
| 105 |
+
if line.startswith(f"{k}="):
|
| 106 |
+
new_lines.append(f"{k}={v}\n")
|
| 107 |
+
updated = True
|
| 108 |
+
break
|
| 109 |
+
if not updated:
|
| 110 |
+
new_lines.append(line)
|
| 111 |
+
|
| 112 |
+
with open(env_path, "w") as f:
|
| 113 |
+
f.writelines(new_lines)
|
| 114 |
+
|
| 115 |
+
model_manager = ModelManager()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/requirements.txt
CHANGED
|
@@ -1,14 +1,15 @@
|
|
| 1 |
-
fastapi>=0.110.0
|
| 2 |
-
uvicorn[standard]>=0.27.0
|
| 3 |
-
openai>=1.12.0
|
| 4 |
-
pydantic>=2.6.0
|
| 5 |
-
pydantic-settings>=2.2.0
|
| 6 |
-
python-dotenv>=1.0.0
|
| 7 |
-
websockets>=12.0
|
| 8 |
-
httpx>=0.27.0
|
| 9 |
-
numpy>=1.26.0
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
| 1 |
+
fastapi>=0.110.0
|
| 2 |
+
uvicorn[standard]>=0.27.0
|
| 3 |
+
openai>=1.12.0
|
| 4 |
+
pydantic>=2.6.0
|
| 5 |
+
pydantic-settings>=2.2.0
|
| 6 |
+
python-dotenv>=1.0.0
|
| 7 |
+
websockets>=12.0
|
| 8 |
+
httpx>=0.27.0
|
| 9 |
+
numpy>=1.26.0
|
| 10 |
+
sentence-transformers>=2.6.0
|
| 11 |
+
torch>=2.2.0
|
| 12 |
+
aiofiles>=23.2.1
|
| 13 |
+
python-multipart>=0.0.9
|
| 14 |
+
paramiko>=3.4.0
|
| 15 |
+
psutil>=5.9.0
|
backend/scenarios/data/easy/software-incident.json
DELETED
|
@@ -1,33 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"id": "software-incident",
|
| 3 |
-
"title": "Nginx Rate Limit Investigation",
|
| 4 |
-
"difficulty": "easy",
|
| 5 |
-
"domain": "DevOps",
|
| 6 |
-
"description": "Users are reporting 503 errors when accessing the main API. Initial reports suggest a misconfigured rate limit.",
|
| 7 |
-
"context": "The system uses Nginx as a reverse proxy. A recent change might have throttled legitimate traffic.",
|
| 8 |
-
"symptoms": [
|
| 9 |
-
"HTTP 503 errors",
|
| 10 |
-
"High latency for API calls"
|
| 11 |
-
],
|
| 12 |
-
"available_services": [
|
| 13 |
-
"nginx-proxy",
|
| 14 |
-
"api-gateway"
|
| 15 |
-
],
|
| 16 |
-
"initial_state": {
|
| 17 |
-
"nginx-proxy": {
|
| 18 |
-
"status": "running",
|
| 19 |
-
"rate_limit": "10",
|
| 20 |
-
"last_reload": "2 hours ago"
|
| 21 |
-
}
|
| 22 |
-
},
|
| 23 |
-
"root_cause": {
|
| 24 |
-
"service": "nginx-proxy",
|
| 25 |
-
"description": "Nginx rate limit was set too low (10 requests/sec) during a maintenance window."
|
| 26 |
-
},
|
| 27 |
-
"grading_criteria": {
|
| 28 |
-
"nginx_rate_limit_fixed": 0.49,
|
| 29 |
-
"nginx_restarted": 0.20,
|
| 30 |
-
"fix_verified": 0.20,
|
| 31 |
-
"efficiency_bonus": 0.09
|
| 32 |
-
}
|
| 33 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/scenarios/data/hard/cascade-system-failure.json
DELETED
|
@@ -1,42 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"id": "cascade-system-failure",
|
| 3 |
-
"title": "Postgres Connection Exhaustion",
|
| 4 |
-
"difficulty": "hard",
|
| 5 |
-
"domain": "Database",
|
| 6 |
-
"description": "A cascade failure is occurring across the cluster. Database connections are being exhausted by a long-running analytics query.",
|
| 7 |
-
"context": "The analytics service might be the culprit. A red herring points to the disk backup agent.",
|
| 8 |
-
"symptoms": [
|
| 9 |
-
"FATAL: too many connections",
|
| 10 |
-
"Application timeout",
|
| 11 |
-
"High I/O wait"
|
| 12 |
-
],
|
| 13 |
-
"available_services": [
|
| 14 |
-
"postgres-db",
|
| 15 |
-
"disk-backup-agent",
|
| 16 |
-
"analytics-service"
|
| 17 |
-
],
|
| 18 |
-
"initial_state": {
|
| 19 |
-
"postgres-db": {
|
| 20 |
-
"status": "running",
|
| 21 |
-
"max_connections": "20",
|
| 22 |
-
"long_running_query": "SELECT * FROM large_audit_table CROSS JOIN high_res_metrics",
|
| 23 |
-
"query_timeout_analytics": "0"
|
| 24 |
-
},
|
| 25 |
-
"disk-backup-agent": {
|
| 26 |
-
"status": "degraded",
|
| 27 |
-
"disk_scan_active": "true"
|
| 28 |
-
}
|
| 29 |
-
},
|
| 30 |
-
"root_cause": {
|
| 31 |
-
"service": "postgres-db",
|
| 32 |
-
"description": "A cross-join query in the analytics service is locking connections, coupled with a low max_connections limit."
|
| 33 |
-
},
|
| 34 |
-
"grading_criteria": {
|
| 35 |
-
"postgres_query_terminated": 0.25,
|
| 36 |
-
"postgres_max_connections_increased": 0.20,
|
| 37 |
-
"postgres_query_timeout_set": 0.20,
|
| 38 |
-
"penalty_disk_backup_agent_modified": -0.15,
|
| 39 |
-
"fix_verified": 0.10,
|
| 40 |
-
"efficiency_bonus": 0.05
|
| 41 |
-
}
|
| 42 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/scenarios/data/medium/business-process-failure.json
DELETED
|
@@ -1,39 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"id": "business-process-failure",
|
| 3 |
-
"title": "Inventory Stockout Loop",
|
| 4 |
-
"difficulty": "medium",
|
| 5 |
-
"domain": "E-Commerce",
|
| 6 |
-
"description": "The inventory service is failing to trigger restocking orders even when stock is zero.",
|
| 7 |
-
"context": "The inventory logic depends on a minimum stock threshold. A red herring might point to the CDN edge node.",
|
| 8 |
-
"symptoms": [
|
| 9 |
-
"Stockouts",
|
| 10 |
-
"Orders stuck in 'PENDING_STOCK'"
|
| 11 |
-
],
|
| 12 |
-
"available_services": [
|
| 13 |
-
"inventory-service",
|
| 14 |
-
"cdn-edge-node",
|
| 15 |
-
"order-processor"
|
| 16 |
-
],
|
| 17 |
-
"initial_state": {
|
| 18 |
-
"inventory-service": {
|
| 19 |
-
"status": "running",
|
| 20 |
-
"minimum_stock_threshold": "50",
|
| 21 |
-
"last_reload": "1 day ago"
|
| 22 |
-
},
|
| 23 |
-
"cdn-edge-node": {
|
| 24 |
-
"status": "running",
|
| 25 |
-
"cache_expiry": "3600s"
|
| 26 |
-
}
|
| 27 |
-
},
|
| 28 |
-
"root_cause": {
|
| 29 |
-
"service": "inventory-service",
|
| 30 |
-
"description": "Minimum stock threshold was accidentally hardcoded to a high value, preventing restocking."
|
| 31 |
-
},
|
| 32 |
-
"grading_criteria": {
|
| 33 |
-
"inventory_threshold_fixed": 0.45,
|
| 34 |
-
"inventory_restarted": 0.10,
|
| 35 |
-
"penalty_cdn_edge_node_modified": -0.15,
|
| 36 |
-
"fix_verified": 0.20,
|
| 37 |
-
"efficiency_bonus": 0.10
|
| 38 |
-
}
|
| 39 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/scenarios/graders/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
# Init for graders
|
|
|
|
| 1 |
+
# Init for graders
|
backend/scenarios/graders/base_grader.py
CHANGED
|
@@ -1,17 +1,7 @@
|
|
| 1 |
class BaseGrader:
|
| 2 |
-
def _clamp(self, score: float) -> float:
|
| 3 |
-
"""
|
| 4 |
-
Clamp score to be strictly between 0 and 1 (not exactly 0 or 1)
|
| 5 |
-
"""
|
| 6 |
-
if score <= 0.0:
|
| 7 |
-
return 0.001
|
| 8 |
-
elif score >= 1.0:
|
| 9 |
-
return 0.999
|
| 10 |
-
return round(score, 4)
|
| 11 |
-
|
| 12 |
def grade(self, episode_state, scenario: dict) -> float:
|
| 13 |
"""
|
| 14 |
-
Returns
|
| 15 |
Must be deterministic — same inputs always same output
|
| 16 |
"""
|
| 17 |
raise NotImplementedError("Subclasses must implement the grade method")
|
|
|
|
| 1 |
class BaseGrader:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
def grade(self, episode_state, scenario: dict) -> float:
|
| 3 |
"""
|
| 4 |
+
Returns 0.0 to 1.0
|
| 5 |
Must be deterministic — same inputs always same output
|
| 6 |
"""
|
| 7 |
raise NotImplementedError("Subclasses must implement the grade method")
|
backend/scenarios/graders/easy_grader.py
CHANGED
|
@@ -26,4 +26,4 @@ class EasyGrader(BaseGrader):
|
|
| 26 |
if steps_ratio <= 0.6 and episode_state.fix_verified and str(rate_limit) == "1000":
|
| 27 |
score += criteria.get('efficiency_bonus', 0.10)
|
| 28 |
|
| 29 |
-
return
|
|
|
|
| 26 |
if steps_ratio <= 0.6 and episode_state.fix_verified and str(rate_limit) == "1000":
|
| 27 |
score += criteria.get('efficiency_bonus', 0.10)
|
| 28 |
|
| 29 |
+
return max(0.0, min(1.0, round(score, 4)))
|
backend/scenarios/graders/hard_grader.py
CHANGED
|
@@ -51,4 +51,4 @@ class HardGrader(BaseGrader):
|
|
| 51 |
if steps_ratio <= 0.6 and episode_state.fix_verified and q_val in ["none", "null", ""]:
|
| 52 |
score += criteria.get('efficiency_bonus', 0.05)
|
| 53 |
|
| 54 |
-
return
|
|
|
|
| 51 |
if steps_ratio <= 0.6 and episode_state.fix_verified and q_val in ["none", "null", ""]:
|
| 52 |
score += criteria.get('efficiency_bonus', 0.05)
|
| 53 |
|
| 54 |
+
return max(0.0, min(1.0, round(score, 4)))
|
backend/scenarios/graders/medium_grader.py
CHANGED
|
@@ -42,4 +42,4 @@ class MediumGrader(BaseGrader):
|
|
| 42 |
if steps_ratio <= 0.6 and episode_state.fix_verified and str(threshold) == "0":
|
| 43 |
score += criteria.get('efficiency_bonus', 0.10)
|
| 44 |
|
| 45 |
-
return
|
|
|
|
| 42 |
if steps_ratio <= 0.6 and episode_state.fix_verified and str(threshold) == "0":
|
| 43 |
score += criteria.get('efficiency_bonus', 0.10)
|
| 44 |
|
| 45 |
+
return max(0.0, min(1.0, round(score, 4)))
|
backend/tests/__init__.py
DELETED
|
File without changes
|
backend/tests/conftest.py
DELETED
|
@@ -1,4 +0,0 @@
|
|
| 1 |
-
import sys
|
| 2 |
-
from pathlib import Path
|
| 3 |
-
|
| 4 |
-
sys.path.insert(0, str(Path(__file__).parent.parent / "backend"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/tools/tool_registry.py
CHANGED
|
@@ -11,7 +11,6 @@ from .tools.submit_resolution import tool_submit_resolution
|
|
| 11 |
from .tools.run_terminal import tool_run_terminal_command
|
| 12 |
from .tools.update_config import tool_update_config
|
| 13 |
from .tools.restart_service import tool_restart_service
|
| 14 |
-
from .tools.fix_verifier import tool_verify_fix
|
| 15 |
|
| 16 |
class ToolRegistry:
|
| 17 |
def __init__(self):
|
|
@@ -29,7 +28,6 @@ class ToolRegistry:
|
|
| 29 |
self.register_tool("run_terminal_command", tool_run_terminal_command)
|
| 30 |
self.register_tool("update_config", tool_update_config)
|
| 31 |
self.register_tool("restart_service", tool_restart_service)
|
| 32 |
-
self.register_tool("verify_fix", tool_verify_fix)
|
| 33 |
|
| 34 |
def register_tool(self, name: str, func: Callable):
|
| 35 |
self.tools[name] = func
|
|
|
|
| 11 |
from .tools.run_terminal import tool_run_terminal_command
|
| 12 |
from .tools.update_config import tool_update_config
|
| 13 |
from .tools.restart_service import tool_restart_service
|
|
|
|
| 14 |
|
| 15 |
class ToolRegistry:
|
| 16 |
def __init__(self):
|
|
|
|
| 28 |
self.register_tool("run_terminal_command", tool_run_terminal_command)
|
| 29 |
self.register_tool("update_config", tool_update_config)
|
| 30 |
self.register_tool("restart_service", tool_restart_service)
|
|
|
|
| 31 |
|
| 32 |
def register_tool(self, name: str, func: Callable):
|
| 33 |
self.tools[name] = func
|
backend/utils/embeddings.py
CHANGED
|
@@ -1,33 +1,26 @@
|
|
| 1 |
-
import httpx
|
| 2 |
-
from typing import List
|
| 3 |
-
from functools import lru_cache
|
| 4 |
-
|
| 5 |
-
@lru_cache(maxsize=256)
|
| 6 |
-
def get_embedding(text: str) -> List[float]:
|
| 7 |
-
"""Get embedding vector using Ollama directly (Synchronous)"""
|
| 8 |
-
try:
|
| 9 |
-
response = httpx.post("http://localhost:11434/api/embeddings", json={
|
| 10 |
-
"model": "all-minilm",
|
| 11 |
-
"prompt": text
|
| 12 |
-
}, timeout=60.0)
|
| 13 |
-
return response.json().get("embedding", [])
|
| 14 |
-
except Exception as e:
|
| 15 |
-
import logging
|
| 16 |
-
logging.error(f"Embedding failed: {e}
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
"""Cosine similarity without PyTorch/Numpy dependencies"""
|
| 28 |
-
if not a or not b: return 0.0
|
| 29 |
-
dot_product = sum(x * y for x, y in zip(a, b))
|
| 30 |
-
mag_a = sum(x * x for x in a) ** 0.5
|
| 31 |
-
mag_b = sum(x * x for x in b) ** 0.5
|
| 32 |
-
if mag_a == 0 or mag_b == 0: return 0.0
|
| 33 |
-
return dot_product / (mag_a * mag_b)
|
|
|
|
| 1 |
+
import httpx
|
| 2 |
+
from typing import List
|
| 3 |
+
from functools import lru_cache
|
| 4 |
+
|
| 5 |
+
@lru_cache(maxsize=256)
|
| 6 |
+
def get_embedding(text: str) -> List[float]:
|
| 7 |
+
"""Get embedding vector using Ollama directly (Synchronous)"""
|
| 8 |
+
try:
|
| 9 |
+
response = httpx.post("http://localhost:11434/api/embeddings", json={
|
| 10 |
+
"model": "all-minilm",
|
| 11 |
+
"prompt": text
|
| 12 |
+
}, timeout=60.0)
|
| 13 |
+
return response.json().get("embedding", [])
|
| 14 |
+
except Exception as e:
|
| 15 |
+
import logging
|
| 16 |
+
logging.error(f"Embedding failed: {e}")
|
| 17 |
+
return []
|
| 18 |
+
|
| 19 |
+
def cos_sim(a: List[float], b: List[float]) -> float:
|
| 20 |
+
"""Cosine similarity without PyTorch/Numpy dependencies"""
|
| 21 |
+
if not a or not b: return 0.0
|
| 22 |
+
dot_product = sum(x * y for x, y in zip(a, b))
|
| 23 |
+
mag_a = sum(x * x for x in a) ** 0.5
|
| 24 |
+
mag_b = sum(x * x for x in b) ** 0.5
|
| 25 |
+
if mag_a == 0 or mag_b == 0: return 0.0
|
| 26 |
+
return dot_product / (mag_a * mag_b)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
default.env
CHANGED
|
@@ -1,54 +1,67 @@
|
|
| 1 |
-
#
|
| 2 |
-
|
| 3 |
-
#
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
#
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
#
|
| 16 |
-
#
|
| 17 |
-
#
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
#
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 2 |
+
# NEXUS Backend — Environment Configuration
|
| 3 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 4 |
+
|
| 5 |
+
# ── COMPETITION REQUIRED VARIABLES ──────────────────────────────────────────
|
| 6 |
+
# These three vars are read by inference.py and evaluated by the competition.
|
| 7 |
+
# The OpenAI client is used for ALL LLM calls — it works with any compatible API.
|
| 8 |
+
|
| 9 |
+
# Option A — HuggingFace Inference (use this on HF Space / cloud deployment)
|
| 10 |
+
API_BASE_URL=https://router.huggingface.co/hf-inference/v1
|
| 11 |
+
MODEL_NAME=
|
| 12 |
+
HF_TOKEN=your_huggingface_token_here
|
| 13 |
+
|
| 14 |
+
# Option B — Local Ollama (use this for local dev and testing)
|
| 15 |
+
# API_BASE_URL=http://localhost:11434/v1
|
| 16 |
+
# MODEL_NAME=
|
| 17 |
+
# HF_TOKEN=ollama
|
| 18 |
+
|
| 19 |
+
# ── OLLAMA LOCAL SERVER ──────────────────────────────────────────────────────
|
| 20 |
+
OLLAMA_BASE_URL=http://localhost:11434/v1
|
| 21 |
+
OLLAMA_API_KEY=ollama
|
| 22 |
+
|
| 23 |
+
# ── AGENT DEFAULTS ───────────────────────────────────────────────────────────
|
| 24 |
+
AGENT_A_PROVIDER=ollama
|
| 25 |
+
AGENT_B_PROVIDER=ollama
|
| 26 |
+
AGENT_A_MODEL=
|
| 27 |
+
AGENT_B_MODEL=
|
| 28 |
+
AGENT_A_TEMPERATURE=0.8
|
| 29 |
+
AGENT_B_TEMPERATURE=0.6
|
| 30 |
+
AGENT_A_MAX_TOKENS=300
|
| 31 |
+
AGENT_B_MAX_TOKENS=300
|
| 32 |
+
|
| 33 |
+
# ── HUGGINGFACE (for HF Space deployment) ────────────────────────────────────
|
| 34 |
+
# When deploying to HF Space, uncomment and fill in:
|
| 35 |
+
# HF_INFERENCE_URL=https://api-inference.huggingface.co/v1
|
| 36 |
+
# HF_AGENT_A_MODEL=
|
| 37 |
+
# HF_AGENT_B_MODEL=
|
| 38 |
+
|
| 39 |
+
# ── SERVER ───────────────────────────────────────────────────────────────────
|
| 40 |
+
HOST=0.0.0.0
|
| 41 |
+
PORT=7860
|
| 42 |
+
DEBUG=true
|
| 43 |
+
ENVIRONMENT=local
|
| 44 |
+
|
| 45 |
+
# ── EPISODE SETTINGS ─────────────────────────────────────────────────────────
|
| 46 |
+
MAX_STEPS=8
|
| 47 |
+
MAX_EPISODE_TIME_SECONDS=1200
|
| 48 |
+
SUCCESS_SCORE_THRESHOLD=0.5
|
| 49 |
+
|
| 50 |
+
# ── MCP TOOL SERVER ──────────────────────────────────────────────────────────
|
| 51 |
+
MCP_SERVER_PORT=8001
|
| 52 |
+
MCP_SERVER_URL=http://localhost:8001
|
| 53 |
+
|
| 54 |
+
# ── CUSTOM MODEL (user-provided override) ────────────────────────────────────
|
| 55 |
+
CUSTOM_MODEL_ENABLED=false
|
| 56 |
+
CUSTOM_MODEL_BASE_URL=
|
| 57 |
+
CUSTOM_MODEL_API_KEY=
|
| 58 |
+
CUSTOM_MODEL_NAME=
|
| 59 |
+
CUSTOM_MODEL_AGENT=
|
| 60 |
+
|
| 61 |
+
# ── EXECUTION ENVIRONMENT ────────────────────────────────────────────────────
|
| 62 |
+
# Set to 'simulated' (default) or 'ssh' to use a real SSH Lab Node
|
| 63 |
+
EXECUTION_MODE=simulated
|
| 64 |
+
SSH_HOST=
|
| 65 |
+
SSH_PORT=22
|
| 66 |
+
SSH_USER=
|
| 67 |
+
SSH_PASSWORD=
|
frontend/.gitignore
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Logs
|
| 2 |
+
logs
|
| 3 |
+
*.log
|
| 4 |
+
npm-debug.log*
|
| 5 |
+
yarn-debug.log*
|
| 6 |
+
yarn-error.log*
|
| 7 |
+
pnpm-debug.log*
|
| 8 |
+
lerna-debug.log*
|
| 9 |
+
|
| 10 |
+
node_modules
|
| 11 |
+
dist
|
| 12 |
+
dist-ssr
|
| 13 |
+
*.local
|
| 14 |
+
|
| 15 |
+
# Editor directories and files
|
| 16 |
+
.vscode/*
|
| 17 |
+
!.vscode/extensions.json
|
| 18 |
+
.idea
|
| 19 |
+
.DS_Store
|
| 20 |
+
*.suo
|
| 21 |
+
*.ntvs*
|
| 22 |
+
*.njsproj
|
| 23 |
+
*.sln
|
| 24 |
+
*.sw?
|
| 25 |
+
|
| 26 |
+
# InsForge & AI agent skills
|
| 27 |
+
.insforge
|
| 28 |
+
.agent
|
| 29 |
+
.agents
|
| 30 |
+
.augment
|
| 31 |
+
.claude
|
| 32 |
+
.cline
|
| 33 |
+
.github/copilot*
|
| 34 |
+
.kilocode
|
| 35 |
+
.qoder
|
| 36 |
+
.qwen
|
| 37 |
+
.roo
|
| 38 |
+
.trae
|
| 39 |
+
.windsurf
|
frontend/dist/assets/index-CpY48GhO.js
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/dist/assets/index-MUcnTDDz.css
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */
|
| 2 |
-
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:"JetBrains Mono", monospace;--color-red-400:oklch(70.4% .191 22.216);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--container-4xl:56rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--text-8xl:6rem;--text-8xl--line-height:1;--font-weight-light:300;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-black:900;--tracking-tighter:-.05em;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--leading-relaxed:1.625;--radius-sm:.25rem;--radius-lg:.25rem;--radius-xl:.5rem;--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--blur-sm:8px;--blur-md:12px;--blur-xl:24px;--blur-2xl:40px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-primary:#a8e8ff;--color-primary-container:#00d4ff;--color-secondary:#ddb7ff;--color-tertiary:#66fa8c;--color-error:#ffb4ab;--color-background:#0a0a0f;--color-surface:#131318;--color-surface-container:#1f1f25;--color-surface-container-low:#1b1b20;--color-surface-container-high:#2a292f;--color-surface-container-highest:#35343a;--color-surface-container-lowest:#0e0e13;--color-outline:#859398;--color-outline-variant:#3c494e;--color-on-surface:#e4e1e9;--color-on-surface-variant:#bbc9cf;--font-headline:"Space Grotesk", sans-serif;--font-body:Inter, sans-serif}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}body{background-color:var(--color-background);font-family:var(--font-body);color:var(--color-on-surface);overflow-x:hidden}body ::selection{background-color:#00d4ff4d}@supports (color:color-mix(in lab, red, red)){body ::selection{background-color:color-mix(in oklab, var(--color-primary-container) 30%, transparent)}}body::selection{background-color:#00d4ff4d}@supports (color:color-mix(in lab, red, red)){body::selection{background-color:color-mix(in oklab, var(--color-primary-container) 30%, transparent)}}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.top-0{top:calc(var(--spacing) * 0)}.top-1\/2{top:50%}.top-3{top:calc(var(--spacing) * 3)}.top-16{top:calc(var(--spacing) * 16)}.right-0{right:calc(var(--spacing) * 0)}.right-3{right:calc(var(--spacing) * 3)}.bottom-0{bottom:calc(var(--spacing) * 0)}.bottom-12{bottom:calc(var(--spacing) * 12)}.-left-4{left:calc(var(--spacing) * -4)}.left-0{left:calc(var(--spacing) * 0)}.left-1\/2{left:50%}.left-3{left:calc(var(--spacing) * 3)}.left-20{left:calc(var(--spacing) * 20)}.-z-10{z-index:calc(10 * -1)}.z-10{z-index:10}.z-20{z-index:20}.z-40{z-index:40}.z-50{z-index:50}.z-\[100\]{z-index:100}.mx-auto{margin-inline:auto}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-12{margin-top:calc(var(--spacing) * 12)}.mt-auto{margin-top:auto}.mr-2{margin-right:calc(var(--spacing) * 2)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.ml-1{margin-left:calc(var(--spacing) * 1)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-3{margin-left:calc(var(--spacing) * 3)}.ml-4{margin-left:calc(var(--spacing) * 4)}.ml-20{margin-left:calc(var(--spacing) * 20)}.ml-auto{margin-left:auto}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.h-0{height:calc(var(--spacing) * 0)}.h-1{height:calc(var(--spacing) * 1)}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-2{height:calc(var(--spacing) * 2)}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-8{height:calc(var(--spacing) * 8)}.h-10{height:calc(var(--spacing) * 10)}.h-12{height:calc(var(--spacing) * 12)}.h-14{height:calc(var(--spacing) * 14)}.h-16{height:calc(var(--spacing) * 16)}.h-32{height:calc(var(--spacing) * 32)}.h-36{height:calc(var(--spacing) * 36)}.h-48{height:calc(var(--spacing) * 48)}.h-64{height:calc(var(--spacing) * 64)}.h-\[400px\]{height:400px}.h-\[500px\]{height:500px}.h-\[600px\]{height:600px}.h-full{height:100%}.max-h-32{max-height:calc(var(--spacing) * 32)}.max-h-48{max-height:calc(var(--spacing) * 48)}.max-h-\[80vh\]{max-height:80vh}.max-h-\[90vh\]{max-height:90vh}.min-h-\[80px\]{min-height:80px}.min-h-\[100px\]{min-height:100px}.min-h-screen{min-height:100vh}.custom-scrollbar::-webkit-scrollbar{width:6px}.custom-scrollbar::-webkit-scrollbar-track{background:0 0}.custom-scrollbar::-webkit-scrollbar-thumb{background:var(--color-outline-variant);border-radius:10px}.custom-scrollbar::-webkit-scrollbar-thumb:hover{background:var(--color-outline)}.w-1{width:calc(var(--spacing) * 1)}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-2{width:calc(var(--spacing) * 2)}.w-10{width:calc(var(--spacing) * 10)}.w-20{width:calc(var(--spacing) * 20)}.w-32{width:calc(var(--spacing) * 32)}.w-48{width:calc(var(--spacing) * 48)}.w-80{width:calc(var(--spacing) * 80)}.w-96{width:calc(var(--spacing) * 96)}.w-\[400px\]{width:400px}.w-\[500px\]{width:500px}.w-\[600px\]{width:600px}.w-full{width:100%}.w-px{width:1px}.max-w-2xl{max-width:var(--container-2xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-48{max-width:calc(var(--spacing) * 48)}.max-w-\[1600px\]{max-width:1600px}.max-w-lg{max-width:var(--container-lg)}.max-w-sm{max-width:var(--container-sm)}.max-w-xl{max-width:var(--container-xl)}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-\[40px\]{min-width:40px}.flex-1{flex:1}.flex-shrink-0,.shrink-0{flex-shrink:0}.-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-1\/2{--tw-translate-x:calc(1 / 2 * 100%);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-1\/2{--tw-translate-y:calc(1 / 2 * 100%);translate:var(--tw-translate-x) var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-\[ping_4s_infinite\]{animation:4s infinite ping}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-blink{animation:1s step-end infinite blink}.cursor-help{cursor:help}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.cursor-text{cursor:text}.resize{resize:both}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.appearance-none{appearance:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.content-start{align-content:flex-start}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-8{gap:calc(var(--spacing) * 8)}.gap-12{gap:calc(var(--spacing) * 12)}:where(.space-y-0\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * .5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * .5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-10>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 10) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 10) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-12>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 12) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 12) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-3{column-gap:calc(var(--spacing) * 3)}.gap-y-1{row-gap:calc(var(--spacing) * 1)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-r-lg{border-top-right-radius:var(--radius-lg);border-bottom-right-radius:var(--radius-lg)}.rounded-tr-xl{border-top-right-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-\[1px\]{border-style:var(--tw-border-style);border-width:1px}.border-x{border-inline-style:var(--tw-border-style);border-inline-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-t-2{border-top-style:var(--tw-border-style);border-top-width:2px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-\[\#f59e0b\]\/20{border-color:oklab(76.8591% .0560995 .154808/.2)}.border-error{border-color:var(--color-error)}.border-error\/20{border-color:#ffb4ab33}@supports (color:color-mix(in lab, red, red)){.border-error\/20{border-color:color-mix(in oklab, var(--color-error) 20%, transparent)}}.border-error\/30{border-color:#ffb4ab4d}@supports (color:color-mix(in lab, red, red)){.border-error\/30{border-color:color-mix(in oklab, var(--color-error) 30%, transparent)}}.border-error\/50{border-color:#ffb4ab80}@supports (color:color-mix(in lab, red, red)){.border-error\/50{border-color:color-mix(in oklab, var(--color-error) 50%, transparent)}}.border-green-500\/30{border-color:#00c7584d}@supports (color:color-mix(in lab, red, red)){.border-green-500\/30{border-color:color-mix(in oklab, var(--color-green-500) 30%, transparent)}}.border-outline-variant\/10{border-color:#3c494e1a}@supports (color:color-mix(in lab, red, red)){.border-outline-variant\/10{border-color:color-mix(in oklab, var(--color-outline-variant) 10%, transparent)}}.border-outline-variant\/20{border-color:#3c494e33}@supports (color:color-mix(in lab, red, red)){.border-outline-variant\/20{border-color:color-mix(in oklab, var(--color-outline-variant) 20%, transparent)}}.border-outline-variant\/30{border-color:#3c494e4d}@supports (color:color-mix(in lab, red, red)){.border-outline-variant\/30{border-color:color-mix(in oklab, var(--color-outline-variant) 30%, transparent)}}.border-primary{border-color:var(--color-primary)}.border-primary-container\/20{border-color:#00d4ff33}@supports (color:color-mix(in lab, red, red)){.border-primary-container\/20{border-color:color-mix(in oklab, var(--color-primary-container) 20%, transparent)}}.border-primary-container\/40{border-color:#00d4ff66}@supports (color:color-mix(in lab, red, red)){.border-primary-container\/40{border-color:color-mix(in oklab, var(--color-primary-container) 40%, transparent)}}.border-primary\/5{border-color:#a8e8ff0d}@supports (color:color-mix(in lab, red, red)){.border-primary\/5{border-color:color-mix(in oklab, var(--color-primary) 5%, transparent)}}.border-primary\/10{border-color:#a8e8ff1a}@supports (color:color-mix(in lab, red, red)){.border-primary\/10{border-color:color-mix(in oklab, var(--color-primary) 10%, transparent)}}.border-primary\/15{border-color:#a8e8ff26}@supports (color:color-mix(in lab, red, red)){.border-primary\/15{border-color:color-mix(in oklab, var(--color-primary) 15%, transparent)}}.border-primary\/20{border-color:#a8e8ff33}@supports (color:color-mix(in lab, red, red)){.border-primary\/20{border-color:color-mix(in oklab, var(--color-primary) 20%, transparent)}}.border-primary\/30{border-color:#a8e8ff4d}@supports (color:color-mix(in lab, red, red)){.border-primary\/30{border-color:color-mix(in oklab, var(--color-primary) 30%, transparent)}}.border-primary\/50{border-color:#a8e8ff80}@supports (color:color-mix(in lab, red, red)){.border-primary\/50{border-color:color-mix(in oklab, var(--color-primary) 50%, transparent)}}.border-secondary{border-color:var(--color-secondary)}.border-secondary\/20{border-color:#ddb7ff33}@supports (color:color-mix(in lab, red, red)){.border-secondary\/20{border-color:color-mix(in oklab, var(--color-secondary) 20%, transparent)}}.border-secondary\/30{border-color:#ddb7ff4d}@supports (color:color-mix(in lab, red, red)){.border-secondary\/30{border-color:color-mix(in oklab, var(--color-secondary) 30%, transparent)}}.border-slate-700{border-color:var(--color-slate-700)}.border-tertiary{border-color:var(--color-tertiary)}.border-tertiary\/10{border-color:#66fa8c1a}@supports (color:color-mix(in lab, red, red)){.border-tertiary\/10{border-color:color-mix(in oklab, var(--color-tertiary) 10%, transparent)}}.border-tertiary\/20{border-color:#66fa8c33}@supports (color:color-mix(in lab, red, red)){.border-tertiary\/20{border-color:color-mix(in oklab, var(--color-tertiary) 20%, transparent)}}.border-tertiary\/30{border-color:#66fa8c4d}@supports (color:color-mix(in lab, red, red)){.border-tertiary\/30{border-color:color-mix(in oklab, var(--color-tertiary) 30%, transparent)}}.border-tertiary\/40{border-color:#66fa8c66}@supports (color:color-mix(in lab, red, red)){.border-tertiary\/40{border-color:color-mix(in oklab, var(--color-tertiary) 40%, transparent)}}.border-transparent{border-color:#0000}.border-white\/5{border-color:#ffffff0d}@supports (color:color-mix(in lab, red, red)){.border-white\/5{border-color:color-mix(in oklab, var(--color-white) 5%, transparent)}}.border-white\/10{border-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.border-white\/10{border-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}.border-white\/40{border-color:#fff6}@supports (color:color-mix(in lab, red, red)){.border-white\/40{border-color:color-mix(in oklab, var(--color-white) 40%, transparent)}}.glass-panel{-webkit-backdrop-filter:blur(64px);backdrop-filter:blur(64px);background-color:#13131899;border:1px solid #ffffff0d}.bg-\[\#3b82f6\]{background-color:#3b82f6}.bg-\[\#10b981\]{background-color:#10b981}.bg-\[\#a855f7\]{background-color:#a855f7}.bg-\[\#f59e0b\]{background-color:#f59e0b}.bg-\[\#f59e0b\]\/10{background-color:oklab(76.8591% .0560995 .154808/.1)}.bg-background\/40{background-color:#0a0a0f66}@supports (color:color-mix(in lab, red, red)){.bg-background\/40{background-color:color-mix(in oklab, var(--color-background) 40%, transparent)}}.bg-background\/90{background-color:#0a0a0fe6}@supports (color:color-mix(in lab, red, red)){.bg-background\/90{background-color:color-mix(in oklab, var(--color-background) 90%, transparent)}}.bg-error{background-color:var(--color-error)}.bg-error\/5{background-color:#ffb4ab0d}@supports (color:color-mix(in lab, red, red)){.bg-error\/5{background-color:color-mix(in oklab, var(--color-error) 5%, transparent)}}.bg-error\/10{background-color:#ffb4ab1a}@supports (color:color-mix(in lab, red, red)){.bg-error\/10{background-color:color-mix(in oklab, var(--color-error) 10%, transparent)}}.bg-error\/20{background-color:#ffb4ab33}@supports (color:color-mix(in lab, red, red)){.bg-error\/20{background-color:color-mix(in oklab, var(--color-error) 20%, transparent)}}.bg-green-500\/20{background-color:#00c75833}@supports (color:color-mix(in lab, red, red)){.bg-green-500\/20{background-color:color-mix(in oklab, var(--color-green-500) 20%, transparent)}}.bg-outline-variant\/20{background-color:#3c494e33}@supports (color:color-mix(in lab, red, red)){.bg-outline-variant\/20{background-color:color-mix(in oklab, var(--color-outline-variant) 20%, transparent)}}.bg-primary{background-color:var(--color-primary)}.bg-primary-container\/20{background-color:#00d4ff33}@supports (color:color-mix(in lab, red, red)){.bg-primary-container\/20{background-color:color-mix(in oklab, var(--color-primary-container) 20%, transparent)}}.bg-primary\/5{background-color:#a8e8ff0d}@supports (color:color-mix(in lab, red, red)){.bg-primary\/5{background-color:color-mix(in oklab, var(--color-primary) 5%, transparent)}}.bg-primary\/10{background-color:#a8e8ff1a}@supports (color:color-mix(in lab, red, red)){.bg-primary\/10{background-color:color-mix(in oklab, var(--color-primary) 10%, transparent)}}.bg-primary\/20{background-color:#a8e8ff33}@supports (color:color-mix(in lab, red, red)){.bg-primary\/20{background-color:color-mix(in oklab, var(--color-primary) 20%, transparent)}}.bg-primary\/30{background-color:#a8e8ff4d}@supports (color:color-mix(in lab, red, red)){.bg-primary\/30{background-color:color-mix(in oklab, var(--color-primary) 30%, transparent)}}.bg-primary\/60{background-color:#a8e8ff99}@supports (color:color-mix(in lab, red, red)){.bg-primary\/60{background-color:color-mix(in oklab, var(--color-primary) 60%, transparent)}}.bg-secondary{background-color:var(--color-secondary)}.bg-secondary\/5{background-color:#ddb7ff0d}@supports (color:color-mix(in lab, red, red)){.bg-secondary\/5{background-color:color-mix(in oklab, var(--color-secondary) 5%, transparent)}}.bg-secondary\/10{background-color:#ddb7ff1a}@supports (color:color-mix(in lab, red, red)){.bg-secondary\/10{background-color:color-mix(in oklab, var(--color-secondary) 10%, transparent)}}.bg-surface{background-color:var(--color-surface)}.bg-surface-container{background-color:var(--color-surface-container)}.bg-surface-container-high{background-color:var(--color-surface-container-high)}.bg-surface-container-high\/30{background-color:#2a292f4d}@supports (color:color-mix(in lab, red, red)){.bg-surface-container-high\/30{background-color:color-mix(in oklab, var(--color-surface-container-high) 30%, transparent)}}.bg-surface-container-highest{background-color:var(--color-surface-container-highest)}.bg-surface-container-highest\/20{background-color:#35343a33}@supports (color:color-mix(in lab, red, red)){.bg-surface-container-highest\/20{background-color:color-mix(in oklab, var(--color-surface-container-highest) 20%, transparent)}}.bg-surface-container-low{background-color:var(--color-surface-container-low)}.bg-surface-container-low\/40{background-color:#1b1b2066}@supports (color:color-mix(in lab, red, red)){.bg-surface-container-low\/40{background-color:color-mix(in oklab, var(--color-surface-container-low) 40%, transparent)}}.bg-surface-container-low\/50{background-color:#1b1b2080}@supports (color:color-mix(in lab, red, red)){.bg-surface-container-low\/50{background-color:color-mix(in oklab, var(--color-surface-container-low) 50%, transparent)}}.bg-surface-container-lowest{background-color:var(--color-surface-container-lowest)}.bg-surface-container-lowest\/50{background-color:#0e0e1380}@supports (color:color-mix(in lab, red, red)){.bg-surface-container-lowest\/50{background-color:color-mix(in oklab, var(--color-surface-container-lowest) 50%, transparent)}}.bg-surface-container-lowest\/90{background-color:#0e0e13e6}@supports (color:color-mix(in lab, red, red)){.bg-surface-container-lowest\/90{background-color:color-mix(in oklab, var(--color-surface-container-lowest) 90%, transparent)}}.bg-surface\/60{background-color:#13131899}@supports (color:color-mix(in lab, red, red)){.bg-surface\/60{background-color:color-mix(in oklab, var(--color-surface) 60%, transparent)}}.bg-tertiary{background-color:var(--color-tertiary)}.bg-tertiary\/5{background-color:#66fa8c0d}@supports (color:color-mix(in lab, red, red)){.bg-tertiary\/5{background-color:color-mix(in oklab, var(--color-tertiary) 5%, transparent)}}.bg-tertiary\/10{background-color:#66fa8c1a}@supports (color:color-mix(in lab, red, red)){.bg-tertiary\/10{background-color:color-mix(in oklab, var(--color-tertiary) 10%, transparent)}}.bg-tertiary\/20{background-color:#66fa8c33}@supports (color:color-mix(in lab, red, red)){.bg-tertiary\/20{background-color:color-mix(in oklab, var(--color-tertiary) 20%, transparent)}}.bg-transparent{background-color:#0000}.bg-gradient-to-br{--tw-gradient-position:to bottom right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-r{--tw-gradient-position:to right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-t{--tw-gradient-position:to top in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-primary{--tw-gradient-from:var(--color-primary);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-primary\/5{--tw-gradient-from:#a8e8ff0d}@supports (color:color-mix(in lab, red, red)){.from-primary\/5{--tw-gradient-from:color-mix(in oklab, var(--color-primary) 5%, transparent)}}.from-primary\/5{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-primary\/20{--tw-gradient-from:#a8e8ff33}@supports (color:color-mix(in lab, red, red)){.from-primary\/20{--tw-gradient-from:color-mix(in oklab, var(--color-primary) 20%, transparent)}}.from-primary\/20{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-surface{--tw-gradient-from:var(--color-surface);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.via-transparent{--tw-gradient-via:transparent;--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.to-primary-container{--tw-gradient-to:var(--color-primary-container);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-secondary\/5{--tw-gradient-to:#ddb7ff0d}@supports (color:color-mix(in lab, red, red)){.to-secondary\/5{--tw-gradient-to:color-mix(in oklab, var(--color-secondary) 5%, transparent)}}.to-secondary\/5{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-transparent{--tw-gradient-to:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.bg-clip-text{-webkit-background-clip:text;background-clip:text}.p-1{padding:calc(var(--spacing) * 1)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-8{padding-inline:calc(var(--spacing) * 8)}.px-10{padding-inline:calc(var(--spacing) * 10)}.px-12{padding-inline:calc(var(--spacing) * 12)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-10{padding-block:calc(var(--spacing) * 10)}.py-20{padding-block:calc(var(--spacing) * 20)}.py-24{padding-block:calc(var(--spacing) * 24)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-16{padding-top:calc(var(--spacing) * 16)}.pr-2{padding-right:calc(var(--spacing) * 2)}.pb-1{padding-bottom:calc(var(--spacing) * 1)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pb-6{padding-bottom:calc(var(--spacing) * 6)}.pb-8{padding-bottom:calc(var(--spacing) * 8)}.pb-12{padding-bottom:calc(var(--spacing) * 12)}.pb-\[calc\(48px\+256px\)\]{padding-bottom:304px}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-middle{vertical-align:middle}.font-body{font-family:var(--font-body)}.font-headline{font-family:var(--font-headline)}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-8xl{font-size:var(--text-8xl);line-height:var(--tw-leading,var(--text-8xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[8px\]{font-size:8px}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[12px\]{font-size:12px}.text-\[14px\]{font-size:14px}.text-\[16px\]{font-size:16px}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-black{--tw-font-weight:var(--font-weight-black);font-weight:var(--font-weight-black)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.tracking-\[0\.2em\]{--tw-tracking:.2em;letter-spacing:.2em}.tracking-\[0\.3em\]{--tw-tracking:.3em;letter-spacing:.3em}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-tighter{--tw-tracking:var(--tracking-tighter);letter-spacing:var(--tracking-tighter)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-words{overflow-wrap:break-word}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-\[\#3b82f6\]{color:#3b82f6}.text-\[\#10b981\]{color:#10b981}.text-\[\#a855f7\]{color:#a855f7}.text-\[\#f59e0b\]{color:#f59e0b}.text-black{color:var(--color-black)}.text-error{color:var(--color-error)}.text-error\/80{color:#ffb4abcc}@supports (color:color-mix(in lab, red, red)){.text-error\/80{color:color-mix(in oklab, var(--color-error) 80%, transparent)}}.text-green-400{color:var(--color-green-400)}.text-green-500{color:var(--color-green-500)}.text-on-surface{color:var(--color-on-surface)}.text-on-surface-variant{color:var(--color-on-surface-variant)}.text-on-surface\/40{color:#e4e1e966}@supports (color:color-mix(in lab, red, red)){.text-on-surface\/40{color:color-mix(in oklab, var(--color-on-surface) 40%, transparent)}}.text-on-surface\/60{color:#e4e1e999}@supports (color:color-mix(in lab, red, red)){.text-on-surface\/60{color:color-mix(in oklab, var(--color-on-surface) 60%, transparent)}}.text-on-surface\/70{color:#e4e1e9b3}@supports (color:color-mix(in lab, red, red)){.text-on-surface\/70{color:color-mix(in oklab, var(--color-on-surface) 70%, transparent)}}.text-on-surface\/80{color:#e4e1e9cc}@supports (color:color-mix(in lab, red, red)){.text-on-surface\/80{color:color-mix(in oklab, var(--color-on-surface) 80%, transparent)}}.text-on-surface\/90{color:#e4e1e9e6}@supports (color:color-mix(in lab, red, red)){.text-on-surface\/90{color:color-mix(in oklab, var(--color-on-surface) 90%, transparent)}}.text-outline{color:var(--color-outline)}.text-outline-variant{color:var(--color-outline-variant)}.text-outline-variant\/40{color:#3c494e66}@supports (color:color-mix(in lab, red, red)){.text-outline-variant\/40{color:color-mix(in oklab, var(--color-outline-variant) 40%, transparent)}}.text-outline-variant\/50{color:#3c494e80}@supports (color:color-mix(in lab, red, red)){.text-outline-variant\/50{color:color-mix(in oklab, var(--color-outline-variant) 50%, transparent)}}.text-outline\/40{color:#85939866}@supports (color:color-mix(in lab, red, red)){.text-outline\/40{color:color-mix(in oklab, var(--color-outline) 40%, transparent)}}.text-primary{color:var(--color-primary)}.text-primary\/40{color:#a8e8ff66}@supports (color:color-mix(in lab, red, red)){.text-primary\/40{color:color-mix(in oklab, var(--color-primary) 40%, transparent)}}.text-primary\/60{color:#a8e8ff99}@supports (color:color-mix(in lab, red, red)){.text-primary\/60{color:color-mix(in oklab, var(--color-primary) 60%, transparent)}}.text-secondary{color:var(--color-secondary)}.text-secondary\/60{color:#ddb7ff99}@supports (color:color-mix(in lab, red, red)){.text-secondary\/60{color:color-mix(in oklab, var(--color-secondary) 60%, transparent)}}.text-slate-400{color:var(--color-slate-400)}.text-slate-500{color:var(--color-slate-500)}.text-slate-600{color:var(--color-slate-600)}.text-surface{color:var(--color-surface)}.text-tertiary{color:var(--color-tertiary)}.text-tertiary\/60{color:#66fa8c99}@supports (color:color-mix(in lab, red, red)){.text-tertiary\/60{color:color-mix(in oklab, var(--color-tertiary) 60%, transparent)}}.text-transparent{color:#0000}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.italic{font-style:italic}.accent-primary{accent-color:var(--color-primary)}.opacity-0{opacity:0}.opacity-10{opacity:.1}.opacity-20{opacity:.2}.opacity-30{opacity:.3}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-80{opacity:.8}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_-10px_30px_rgba\(0\,0\,0\,0\.5\)\]{--tw-shadow:0 -10px 30px var(--tw-shadow-color,#00000080);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_-10px_40px_rgba\(0\,0\,0\,0\.6\)\]{--tw-shadow:0 -10px 40px var(--tw-shadow-color,#0009);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_8px_\#66fa8c\]{--tw-shadow:0 0 8px var(--tw-shadow-color,#66fa8c);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_15px_rgba\(0\,212\,255\,0\.1\)\]{--tw-shadow:0 0 15px var(--tw-shadow-color,#00d4ff1a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_15px_rgba\(221\,183\,255\,0\.1\)\]{--tw-shadow:0 0 15px var(--tw-shadow-color,#ddb7ff1a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_15px_rgba\(221\,183\,255\,0\.05\)\]{--tw-shadow:0 0 15px var(--tw-shadow-color,#ddb7ff0d);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_20px_rgba\(0\,212\,255\,0\.1\)\]{--tw-shadow:0 0 20px var(--tw-shadow-color,#00d4ff1a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_40px_rgba\(0\,212\,255\,0\.04\)\]{--tw-shadow:0 0 40px var(--tw-shadow-color,#00d4ff0a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_80px_rgba\(0\,0\,0\,0\.8\)\]{--tw-shadow:0 0 80px var(--tw-shadow-color,#000c);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.refractive-edge{box-shadow:inset 0 1px #ffffff0d}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-\[100px\]{--tw-blur:blur(100px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-\[120px\]{--tw-blur:blur(120px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-\[0_0_15px_rgba\(0\,212\,255\,0\.3\)\]{--tw-drop-shadow-size:drop-shadow(0 0 15px var(--tw-drop-shadow-color,#00d4ff4d));--tw-drop-shadow:var(--tw-drop-shadow-size);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur-2xl{--tw-backdrop-blur:blur(var(--blur-2xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-md{--tw-backdrop-blur:blur(var(--blur-md));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-xl{--tw-backdrop-blur:blur(var(--blur-xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-500{--tw-duration:.5s;transition-duration:.5s}@media (hover:hover){.group-hover\:block:is(:where(.group):hover *){display:block}.group-hover\:flex-row:is(:where(.group):hover *){flex-direction:row}.group-hover\:items-start:is(:where(.group):hover *){align-items:flex-start}.group-hover\:gap-4:is(:where(.group):hover *){gap:calc(var(--spacing) * 4)}.group-hover\:px-6:is(:where(.group):hover *){padding-inline:calc(var(--spacing) * 6)}.group-hover\:opacity-40:is(:where(.group):hover *){opacity:.4}.group-hover\:opacity-60:is(:where(.group):hover *){opacity:.6}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}}.file\:mr-4::file-selector-button{margin-right:calc(var(--spacing) * 4)}.file\:rounded::file-selector-button{border-radius:.25rem}.file\:border-0::file-selector-button{border-style:var(--tw-border-style);border-width:0}.file\:bg-surface-container-highest::file-selector-button{background-color:var(--color-surface-container-highest)}.file\:px-3::file-selector-button{padding-inline:calc(var(--spacing) * 3)}.file\:py-1::file-selector-button{padding-block:calc(var(--spacing) * 1)}.file\:text-\[9px\]::file-selector-button{font-size:9px}.file\:font-semibold::file-selector-button{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.file\:text-on-surface::file-selector-button{color:var(--color-on-surface)}.placeholder\:text-slate-700::placeholder{color:var(--color-slate-700)}@media (hover:hover){.hover\:w-64:hover{width:calc(var(--spacing) * 64)}.hover\:bg-\[\#f59e0b\]\/20:hover{background-color:oklab(76.8591% .0560995 .154808/.2)}.hover\:bg-error:hover{background-color:var(--color-error)}.hover\:bg-error\/20:hover{background-color:#ffb4ab33}@supports (color:color-mix(in lab, red, red)){.hover\:bg-error\/20:hover{background-color:color-mix(in oklab, var(--color-error) 20%, transparent)}}.hover\:bg-error\/30:hover{background-color:#ffb4ab4d}@supports (color:color-mix(in lab, red, red)){.hover\:bg-error\/30:hover{background-color:color-mix(in oklab, var(--color-error) 30%, transparent)}}.hover\:bg-primary\/10:hover{background-color:#a8e8ff1a}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/10:hover{background-color:color-mix(in oklab, var(--color-primary) 10%, transparent)}}.hover\:bg-primary\/30:hover{background-color:#a8e8ff4d}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/30:hover{background-color:color-mix(in oklab, var(--color-primary) 30%, transparent)}}.hover\:bg-primary\/80:hover{background-color:#a8e8ffcc}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/80:hover{background-color:color-mix(in oklab, var(--color-primary) 80%, transparent)}}.hover\:bg-primary\/90:hover{background-color:#a8e8ffe6}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab, var(--color-primary) 90%, transparent)}}.hover\:bg-secondary\/20:hover{background-color:#ddb7ff33}@supports (color:color-mix(in lab, red, red)){.hover\:bg-secondary\/20:hover{background-color:color-mix(in oklab, var(--color-secondary) 20%, transparent)}}.hover\:bg-surface-container-high:hover{background-color:var(--color-surface-container-high)}.hover\:bg-surface-container-highest:hover{background-color:var(--color-surface-container-highest)}.hover\:bg-surface-container-low:hover{background-color:var(--color-surface-container-low)}.hover\:bg-tertiary\/10:hover{background-color:#66fa8c1a}@supports (color:color-mix(in lab, red, red)){.hover\:bg-tertiary\/10:hover{background-color:color-mix(in oklab, var(--color-tertiary) 10%, transparent)}}.hover\:bg-tertiary\/20:hover{background-color:#66fa8c33}@supports (color:color-mix(in lab, red, red)){.hover\:bg-tertiary\/20:hover{background-color:color-mix(in oklab, var(--color-tertiary) 20%, transparent)}}.hover\:bg-white\/5:hover{background-color:#ffffff0d}@supports (color:color-mix(in lab, red, red)){.hover\:bg-white\/5:hover{background-color:color-mix(in oklab, var(--color-white) 5%, transparent)}}.hover\:text-on-surface:hover{color:var(--color-on-surface)}.hover\:text-primary:hover{color:var(--color-primary)}.hover\:text-red-400:hover{color:var(--color-red-400)}.hover\:text-slate-400:hover{color:var(--color-slate-400)}.hover\:text-white:hover{color:var(--color-white)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-70:hover{opacity:.7}.hover\:opacity-100:hover{opacity:1}.hover\:ring-2:hover{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.hover\:ring-primary\/20:hover{--tw-ring-color:#a8e8ff33}@supports (color:color-mix(in lab, red, red)){.hover\:ring-primary\/20:hover{--tw-ring-color:color-mix(in oklab, var(--color-primary) 20%, transparent)}}.hover\:file\:bg-primary\/20:hover::file-selector-button{background-color:#a8e8ff33}@supports (color:color-mix(in lab, red, red)){.hover\:file\:bg-primary\/20:hover::file-selector-button{background-color:color-mix(in oklab, var(--color-primary) 20%, transparent)}}}.focus\:border-primary:focus{border-color:var(--color-primary)}.focus\:border-primary\/50:focus{border-color:#a8e8ff80}@supports (color:color-mix(in lab, red, red)){.focus\:border-primary\/50:focus{border-color:color-mix(in oklab, var(--color-primary) 50%, transparent)}}.focus\:border-secondary:focus{border-color:var(--color-secondary)}.focus\:border-tertiary:focus{border-color:var(--color-tertiary)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-primary\/20:focus{--tw-ring-color:#a8e8ff33}@supports (color:color-mix(in lab, red, red)){.focus\:ring-primary\/20:focus{--tw-ring-color:color-mix(in oklab, var(--color-primary) 20%, transparent)}}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.active\:scale-95:active{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.disabled\:opacity-50:disabled{opacity:.5}@media (width>=48rem){.md\:col-span-2{grid-column:span 2/span 2}.md\:col-span-4{grid-column:span 4/span 4}.md\:col-span-6{grid-column:span 6/span 6}.md\:col-span-8{grid-column:span 8/span 8}.md\:col-span-12{grid-column:span 12/span 12}.md\:block{display:block}.md\:flex{display:flex}.md\:w-auto{width:auto}.md\:flex-none{flex:none}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-8{grid-template-columns:repeat(8,minmax(0,1fr))}.md\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:items-end{align-items:flex-end}.md\:p-8{padding:calc(var(--spacing) * 8)}}@media (width>=64rem){.lg\:col-span-1{grid-column:span 1/span 1}.lg\:mx-0{margin-inline:calc(var(--spacing) * 0)}.lg\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}}@keyframes blink{0%,to{opacity:1}50%{opacity:0}}:root{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@keyframes spin{to{transform:rotate(360deg)}}@keyframes ping{75%,to{opacity:0;transform:scale(2)}}@keyframes pulse{50%{opacity:.5}}
|
|
|
|
|
|
|
|
|
frontend/dist/favicon.svg
DELETED
frontend/dist/icons.svg
DELETED
frontend/dist/index.html
DELETED
|
@@ -1,16 +0,0 @@
|
|
| 1 |
-
<!doctype html>
|
| 2 |
-
<html lang="en" class="dark">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="UTF-8" />
|
| 5 |
-
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 6 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
-
<title>NEXUS | Tactical Glass Command</title>
|
| 8 |
-
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@300;400;500;600&display=swap" rel="stylesheet"/>
|
| 9 |
-
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
| 10 |
-
<script type="module" crossorigin src="./assets/index-CpY48GhO.js"></script>
|
| 11 |
-
<link rel="stylesheet" crossorigin href="./assets/index-MUcnTDDz.css">
|
| 12 |
-
</head>
|
| 13 |
-
<body>
|
| 14 |
-
<div id="root"></div>
|
| 15 |
-
</body>
|
| 16 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/package-lock.json
CHANGED
|
@@ -282,21 +282,21 @@
|
|
| 282 |
}
|
| 283 |
},
|
| 284 |
"node_modules/@emnapi/core": {
|
| 285 |
-
"version": "1.9.
|
| 286 |
-
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.
|
| 287 |
-
"integrity": "sha512-
|
| 288 |
"dev": true,
|
| 289 |
"license": "MIT",
|
| 290 |
"optional": true,
|
| 291 |
"dependencies": {
|
| 292 |
-
"@emnapi/wasi-threads": "1.2.
|
| 293 |
"tslib": "^2.4.0"
|
| 294 |
}
|
| 295 |
},
|
| 296 |
"node_modules/@emnapi/runtime": {
|
| 297 |
-
"version": "1.9.
|
| 298 |
-
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.
|
| 299 |
-
"integrity": "sha512-
|
| 300 |
"dev": true,
|
| 301 |
"license": "MIT",
|
| 302 |
"optional": true,
|
|
@@ -305,9 +305,9 @@
|
|
| 305 |
}
|
| 306 |
},
|
| 307 |
"node_modules/@emnapi/wasi-threads": {
|
| 308 |
-
"version": "1.2.
|
| 309 |
-
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.
|
| 310 |
-
"integrity": "sha512-
|
| 311 |
"dev": true,
|
| 312 |
"license": "MIT",
|
| 313 |
"optional": true,
|
|
@@ -594,9 +594,9 @@
|
|
| 594 |
}
|
| 595 |
},
|
| 596 |
"node_modules/@oxc-project/types": {
|
| 597 |
-
"version": "0.
|
| 598 |
-
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.
|
| 599 |
-
"integrity": "sha512-
|
| 600 |
"dev": true,
|
| 601 |
"license": "MIT",
|
| 602 |
"funding": {
|
|
@@ -604,9 +604,9 @@
|
|
| 604 |
}
|
| 605 |
},
|
| 606 |
"node_modules/@rolldown/binding-android-arm64": {
|
| 607 |
-
"version": "1.0.0-rc.
|
| 608 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.
|
| 609 |
-
"integrity": "sha512-
|
| 610 |
"cpu": [
|
| 611 |
"arm64"
|
| 612 |
],
|
|
@@ -621,9 +621,9 @@
|
|
| 621 |
}
|
| 622 |
},
|
| 623 |
"node_modules/@rolldown/binding-darwin-arm64": {
|
| 624 |
-
"version": "1.0.0-rc.
|
| 625 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.
|
| 626 |
-
"integrity": "sha512-
|
| 627 |
"cpu": [
|
| 628 |
"arm64"
|
| 629 |
],
|
|
@@ -638,9 +638,9 @@
|
|
| 638 |
}
|
| 639 |
},
|
| 640 |
"node_modules/@rolldown/binding-darwin-x64": {
|
| 641 |
-
"version": "1.0.0-rc.
|
| 642 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.
|
| 643 |
-
"integrity": "sha512-
|
| 644 |
"cpu": [
|
| 645 |
"x64"
|
| 646 |
],
|
|
@@ -655,9 +655,9 @@
|
|
| 655 |
}
|
| 656 |
},
|
| 657 |
"node_modules/@rolldown/binding-freebsd-x64": {
|
| 658 |
-
"version": "1.0.0-rc.
|
| 659 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.
|
| 660 |
-
"integrity": "sha512-
|
| 661 |
"cpu": [
|
| 662 |
"x64"
|
| 663 |
],
|
|
@@ -672,9 +672,9 @@
|
|
| 672 |
}
|
| 673 |
},
|
| 674 |
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
| 675 |
-
"version": "1.0.0-rc.
|
| 676 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.
|
| 677 |
-
"integrity": "sha512-
|
| 678 |
"cpu": [
|
| 679 |
"arm"
|
| 680 |
],
|
|
@@ -689,9 +689,9 @@
|
|
| 689 |
}
|
| 690 |
},
|
| 691 |
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
| 692 |
-
"version": "1.0.0-rc.
|
| 693 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.
|
| 694 |
-
"integrity": "sha512-
|
| 695 |
"cpu": [
|
| 696 |
"arm64"
|
| 697 |
],
|
|
@@ -706,9 +706,9 @@
|
|
| 706 |
}
|
| 707 |
},
|
| 708 |
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
| 709 |
-
"version": "1.0.0-rc.
|
| 710 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.
|
| 711 |
-
"integrity": "sha512-
|
| 712 |
"cpu": [
|
| 713 |
"arm64"
|
| 714 |
],
|
|
@@ -723,9 +723,9 @@
|
|
| 723 |
}
|
| 724 |
},
|
| 725 |
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
| 726 |
-
"version": "1.0.0-rc.
|
| 727 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.
|
| 728 |
-
"integrity": "sha512-
|
| 729 |
"cpu": [
|
| 730 |
"ppc64"
|
| 731 |
],
|
|
@@ -740,9 +740,9 @@
|
|
| 740 |
}
|
| 741 |
},
|
| 742 |
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
| 743 |
-
"version": "1.0.0-rc.
|
| 744 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.
|
| 745 |
-
"integrity": "sha512-
|
| 746 |
"cpu": [
|
| 747 |
"s390x"
|
| 748 |
],
|
|
@@ -757,9 +757,9 @@
|
|
| 757 |
}
|
| 758 |
},
|
| 759 |
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
| 760 |
-
"version": "1.0.0-rc.
|
| 761 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.
|
| 762 |
-
"integrity": "sha512-
|
| 763 |
"cpu": [
|
| 764 |
"x64"
|
| 765 |
],
|
|
@@ -774,9 +774,9 @@
|
|
| 774 |
}
|
| 775 |
},
|
| 776 |
"node_modules/@rolldown/binding-linux-x64-musl": {
|
| 777 |
-
"version": "1.0.0-rc.
|
| 778 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.
|
| 779 |
-
"integrity": "sha512-
|
| 780 |
"cpu": [
|
| 781 |
"x64"
|
| 782 |
],
|
|
@@ -791,9 +791,9 @@
|
|
| 791 |
}
|
| 792 |
},
|
| 793 |
"node_modules/@rolldown/binding-openharmony-arm64": {
|
| 794 |
-
"version": "1.0.0-rc.
|
| 795 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.
|
| 796 |
-
"integrity": "sha512-
|
| 797 |
"cpu": [
|
| 798 |
"arm64"
|
| 799 |
],
|
|
@@ -808,9 +808,9 @@
|
|
| 808 |
}
|
| 809 |
},
|
| 810 |
"node_modules/@rolldown/binding-wasm32-wasi": {
|
| 811 |
-
"version": "1.0.0-rc.
|
| 812 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.
|
| 813 |
-
"integrity": "sha512-
|
| 814 |
"cpu": [
|
| 815 |
"wasm32"
|
| 816 |
],
|
|
@@ -818,18 +818,16 @@
|
|
| 818 |
"license": "MIT",
|
| 819 |
"optional": true,
|
| 820 |
"dependencies": {
|
| 821 |
-
"@
|
| 822 |
-
"@emnapi/runtime": "1.9.1",
|
| 823 |
-
"@napi-rs/wasm-runtime": "^1.1.2"
|
| 824 |
},
|
| 825 |
"engines": {
|
| 826 |
"node": ">=14.0.0"
|
| 827 |
}
|
| 828 |
},
|
| 829 |
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
| 830 |
-
"version": "1.0.0-rc.
|
| 831 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.
|
| 832 |
-
"integrity": "sha512-
|
| 833 |
"cpu": [
|
| 834 |
"arm64"
|
| 835 |
],
|
|
@@ -844,9 +842,9 @@
|
|
| 844 |
}
|
| 845 |
},
|
| 846 |
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
| 847 |
-
"version": "1.0.0-rc.
|
| 848 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.
|
| 849 |
-
"integrity": "sha512-
|
| 850 |
"cpu": [
|
| 851 |
"x64"
|
| 852 |
],
|
|
@@ -1317,9 +1315,9 @@
|
|
| 1317 |
"license": "MIT"
|
| 1318 |
},
|
| 1319 |
"node_modules/baseline-browser-mapping": {
|
| 1320 |
-
"version": "2.10.
|
| 1321 |
-
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.
|
| 1322 |
-
"integrity": "sha512-
|
| 1323 |
"dev": true,
|
| 1324 |
"license": "Apache-2.0",
|
| 1325 |
"bin": {
|
|
@@ -1385,9 +1383,9 @@
|
|
| 1385 |
}
|
| 1386 |
},
|
| 1387 |
"node_modules/caniuse-lite": {
|
| 1388 |
-
"version": "1.0.
|
| 1389 |
-
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.
|
| 1390 |
-
"integrity": "sha512-
|
| 1391 |
"dev": true,
|
| 1392 |
"funding": [
|
| 1393 |
{
|
|
@@ -1527,9 +1525,9 @@
|
|
| 1527 |
}
|
| 1528 |
},
|
| 1529 |
"node_modules/electron-to-chromium": {
|
| 1530 |
-
"version": "1.5.
|
| 1531 |
-
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.
|
| 1532 |
-
"integrity": "sha512-
|
| 1533 |
"dev": true,
|
| 1534 |
"license": "ISC"
|
| 1535 |
},
|
|
@@ -2697,14 +2695,14 @@
|
|
| 2697 |
}
|
| 2698 |
},
|
| 2699 |
"node_modules/rolldown": {
|
| 2700 |
-
"version": "1.0.0-rc.
|
| 2701 |
-
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.
|
| 2702 |
-
"integrity": "sha512-
|
| 2703 |
"dev": true,
|
| 2704 |
"license": "MIT",
|
| 2705 |
"dependencies": {
|
| 2706 |
-
"@oxc-project/types": "=0.
|
| 2707 |
-
"@rolldown/pluginutils": "1.0.0-rc.
|
| 2708 |
},
|
| 2709 |
"bin": {
|
| 2710 |
"rolldown": "bin/cli.mjs"
|
|
@@ -2713,27 +2711,27 @@
|
|
| 2713 |
"node": "^20.19.0 || >=22.12.0"
|
| 2714 |
},
|
| 2715 |
"optionalDependencies": {
|
| 2716 |
-
"@rolldown/binding-android-arm64": "1.0.0-rc.
|
| 2717 |
-
"@rolldown/binding-darwin-arm64": "1.0.0-rc.
|
| 2718 |
-
"@rolldown/binding-darwin-x64": "1.0.0-rc.
|
| 2719 |
-
"@rolldown/binding-freebsd-x64": "1.0.0-rc.
|
| 2720 |
-
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.
|
| 2721 |
-
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.
|
| 2722 |
-
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.
|
| 2723 |
-
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.
|
| 2724 |
-
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.
|
| 2725 |
-
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.
|
| 2726 |
-
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.
|
| 2727 |
-
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.
|
| 2728 |
-
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.
|
| 2729 |
-
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.
|
| 2730 |
-
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.
|
| 2731 |
}
|
| 2732 |
},
|
| 2733 |
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
|
| 2734 |
-
"version": "1.0.0-rc.
|
| 2735 |
-
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.
|
| 2736 |
-
"integrity": "sha512-
|
| 2737 |
"dev": true,
|
| 2738 |
"license": "MIT"
|
| 2739 |
},
|
|
@@ -2919,16 +2917,16 @@
|
|
| 2919 |
}
|
| 2920 |
},
|
| 2921 |
"node_modules/vite": {
|
| 2922 |
-
"version": "8.0.
|
| 2923 |
-
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.
|
| 2924 |
-
"integrity": "sha512-
|
| 2925 |
"dev": true,
|
| 2926 |
"license": "MIT",
|
| 2927 |
"dependencies": {
|
| 2928 |
"lightningcss": "^1.32.0",
|
| 2929 |
"picomatch": "^4.0.4",
|
| 2930 |
"postcss": "^8.5.8",
|
| 2931 |
-
"rolldown": "1.0.0-rc.
|
| 2932 |
"tinyglobby": "^0.2.15"
|
| 2933 |
},
|
| 2934 |
"bin": {
|
|
@@ -2946,7 +2944,7 @@
|
|
| 2946 |
"peerDependencies": {
|
| 2947 |
"@types/node": "^20.19.0 || >=22.12.0",
|
| 2948 |
"@vitejs/devtools": "^0.1.0",
|
| 2949 |
-
"esbuild": "^0.27.0
|
| 2950 |
"jiti": ">=1.21.0",
|
| 2951 |
"less": "^4.0.0",
|
| 2952 |
"sass": "^1.70.0",
|
|
|
|
| 282 |
}
|
| 283 |
},
|
| 284 |
"node_modules/@emnapi/core": {
|
| 285 |
+
"version": "1.9.2",
|
| 286 |
+
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
|
| 287 |
+
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
|
| 288 |
"dev": true,
|
| 289 |
"license": "MIT",
|
| 290 |
"optional": true,
|
| 291 |
"dependencies": {
|
| 292 |
+
"@emnapi/wasi-threads": "1.2.1",
|
| 293 |
"tslib": "^2.4.0"
|
| 294 |
}
|
| 295 |
},
|
| 296 |
"node_modules/@emnapi/runtime": {
|
| 297 |
+
"version": "1.9.2",
|
| 298 |
+
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
|
| 299 |
+
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
|
| 300 |
"dev": true,
|
| 301 |
"license": "MIT",
|
| 302 |
"optional": true,
|
|
|
|
| 305 |
}
|
| 306 |
},
|
| 307 |
"node_modules/@emnapi/wasi-threads": {
|
| 308 |
+
"version": "1.2.1",
|
| 309 |
+
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
| 310 |
+
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
| 311 |
"dev": true,
|
| 312 |
"license": "MIT",
|
| 313 |
"optional": true,
|
|
|
|
| 594 |
}
|
| 595 |
},
|
| 596 |
"node_modules/@oxc-project/types": {
|
| 597 |
+
"version": "0.122.0",
|
| 598 |
+
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz",
|
| 599 |
+
"integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==",
|
| 600 |
"dev": true,
|
| 601 |
"license": "MIT",
|
| 602 |
"funding": {
|
|
|
|
| 604 |
}
|
| 605 |
},
|
| 606 |
"node_modules/@rolldown/binding-android-arm64": {
|
| 607 |
+
"version": "1.0.0-rc.12",
|
| 608 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz",
|
| 609 |
+
"integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==",
|
| 610 |
"cpu": [
|
| 611 |
"arm64"
|
| 612 |
],
|
|
|
|
| 621 |
}
|
| 622 |
},
|
| 623 |
"node_modules/@rolldown/binding-darwin-arm64": {
|
| 624 |
+
"version": "1.0.0-rc.12",
|
| 625 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz",
|
| 626 |
+
"integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==",
|
| 627 |
"cpu": [
|
| 628 |
"arm64"
|
| 629 |
],
|
|
|
|
| 638 |
}
|
| 639 |
},
|
| 640 |
"node_modules/@rolldown/binding-darwin-x64": {
|
| 641 |
+
"version": "1.0.0-rc.12",
|
| 642 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz",
|
| 643 |
+
"integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==",
|
| 644 |
"cpu": [
|
| 645 |
"x64"
|
| 646 |
],
|
|
|
|
| 655 |
}
|
| 656 |
},
|
| 657 |
"node_modules/@rolldown/binding-freebsd-x64": {
|
| 658 |
+
"version": "1.0.0-rc.12",
|
| 659 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz",
|
| 660 |
+
"integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==",
|
| 661 |
"cpu": [
|
| 662 |
"x64"
|
| 663 |
],
|
|
|
|
| 672 |
}
|
| 673 |
},
|
| 674 |
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
| 675 |
+
"version": "1.0.0-rc.12",
|
| 676 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz",
|
| 677 |
+
"integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==",
|
| 678 |
"cpu": [
|
| 679 |
"arm"
|
| 680 |
],
|
|
|
|
| 689 |
}
|
| 690 |
},
|
| 691 |
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
| 692 |
+
"version": "1.0.0-rc.12",
|
| 693 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz",
|
| 694 |
+
"integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==",
|
| 695 |
"cpu": [
|
| 696 |
"arm64"
|
| 697 |
],
|
|
|
|
| 706 |
}
|
| 707 |
},
|
| 708 |
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
| 709 |
+
"version": "1.0.0-rc.12",
|
| 710 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz",
|
| 711 |
+
"integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==",
|
| 712 |
"cpu": [
|
| 713 |
"arm64"
|
| 714 |
],
|
|
|
|
| 723 |
}
|
| 724 |
},
|
| 725 |
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
| 726 |
+
"version": "1.0.0-rc.12",
|
| 727 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz",
|
| 728 |
+
"integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==",
|
| 729 |
"cpu": [
|
| 730 |
"ppc64"
|
| 731 |
],
|
|
|
|
| 740 |
}
|
| 741 |
},
|
| 742 |
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
| 743 |
+
"version": "1.0.0-rc.12",
|
| 744 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz",
|
| 745 |
+
"integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==",
|
| 746 |
"cpu": [
|
| 747 |
"s390x"
|
| 748 |
],
|
|
|
|
| 757 |
}
|
| 758 |
},
|
| 759 |
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
| 760 |
+
"version": "1.0.0-rc.12",
|
| 761 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz",
|
| 762 |
+
"integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==",
|
| 763 |
"cpu": [
|
| 764 |
"x64"
|
| 765 |
],
|
|
|
|
| 774 |
}
|
| 775 |
},
|
| 776 |
"node_modules/@rolldown/binding-linux-x64-musl": {
|
| 777 |
+
"version": "1.0.0-rc.12",
|
| 778 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz",
|
| 779 |
+
"integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==",
|
| 780 |
"cpu": [
|
| 781 |
"x64"
|
| 782 |
],
|
|
|
|
| 791 |
}
|
| 792 |
},
|
| 793 |
"node_modules/@rolldown/binding-openharmony-arm64": {
|
| 794 |
+
"version": "1.0.0-rc.12",
|
| 795 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz",
|
| 796 |
+
"integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==",
|
| 797 |
"cpu": [
|
| 798 |
"arm64"
|
| 799 |
],
|
|
|
|
| 808 |
}
|
| 809 |
},
|
| 810 |
"node_modules/@rolldown/binding-wasm32-wasi": {
|
| 811 |
+
"version": "1.0.0-rc.12",
|
| 812 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz",
|
| 813 |
+
"integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==",
|
| 814 |
"cpu": [
|
| 815 |
"wasm32"
|
| 816 |
],
|
|
|
|
| 818 |
"license": "MIT",
|
| 819 |
"optional": true,
|
| 820 |
"dependencies": {
|
| 821 |
+
"@napi-rs/wasm-runtime": "^1.1.1"
|
|
|
|
|
|
|
| 822 |
},
|
| 823 |
"engines": {
|
| 824 |
"node": ">=14.0.0"
|
| 825 |
}
|
| 826 |
},
|
| 827 |
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
| 828 |
+
"version": "1.0.0-rc.12",
|
| 829 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz",
|
| 830 |
+
"integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==",
|
| 831 |
"cpu": [
|
| 832 |
"arm64"
|
| 833 |
],
|
|
|
|
| 842 |
}
|
| 843 |
},
|
| 844 |
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
| 845 |
+
"version": "1.0.0-rc.12",
|
| 846 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz",
|
| 847 |
+
"integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==",
|
| 848 |
"cpu": [
|
| 849 |
"x64"
|
| 850 |
],
|
|
|
|
| 1315 |
"license": "MIT"
|
| 1316 |
},
|
| 1317 |
"node_modules/baseline-browser-mapping": {
|
| 1318 |
+
"version": "2.10.13",
|
| 1319 |
+
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.13.tgz",
|
| 1320 |
+
"integrity": "sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw==",
|
| 1321 |
"dev": true,
|
| 1322 |
"license": "Apache-2.0",
|
| 1323 |
"bin": {
|
|
|
|
| 1383 |
}
|
| 1384 |
},
|
| 1385 |
"node_modules/caniuse-lite": {
|
| 1386 |
+
"version": "1.0.30001784",
|
| 1387 |
+
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001784.tgz",
|
| 1388 |
+
"integrity": "sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==",
|
| 1389 |
"dev": true,
|
| 1390 |
"funding": [
|
| 1391 |
{
|
|
|
|
| 1525 |
}
|
| 1526 |
},
|
| 1527 |
"node_modules/electron-to-chromium": {
|
| 1528 |
+
"version": "1.5.331",
|
| 1529 |
+
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz",
|
| 1530 |
+
"integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==",
|
| 1531 |
"dev": true,
|
| 1532 |
"license": "ISC"
|
| 1533 |
},
|
|
|
|
| 2695 |
}
|
| 2696 |
},
|
| 2697 |
"node_modules/rolldown": {
|
| 2698 |
+
"version": "1.0.0-rc.12",
|
| 2699 |
+
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz",
|
| 2700 |
+
"integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==",
|
| 2701 |
"dev": true,
|
| 2702 |
"license": "MIT",
|
| 2703 |
"dependencies": {
|
| 2704 |
+
"@oxc-project/types": "=0.122.0",
|
| 2705 |
+
"@rolldown/pluginutils": "1.0.0-rc.12"
|
| 2706 |
},
|
| 2707 |
"bin": {
|
| 2708 |
"rolldown": "bin/cli.mjs"
|
|
|
|
| 2711 |
"node": "^20.19.0 || >=22.12.0"
|
| 2712 |
},
|
| 2713 |
"optionalDependencies": {
|
| 2714 |
+
"@rolldown/binding-android-arm64": "1.0.0-rc.12",
|
| 2715 |
+
"@rolldown/binding-darwin-arm64": "1.0.0-rc.12",
|
| 2716 |
+
"@rolldown/binding-darwin-x64": "1.0.0-rc.12",
|
| 2717 |
+
"@rolldown/binding-freebsd-x64": "1.0.0-rc.12",
|
| 2718 |
+
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12",
|
| 2719 |
+
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12",
|
| 2720 |
+
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12",
|
| 2721 |
+
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12",
|
| 2722 |
+
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12",
|
| 2723 |
+
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12",
|
| 2724 |
+
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.12",
|
| 2725 |
+
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.12",
|
| 2726 |
+
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.12",
|
| 2727 |
+
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12",
|
| 2728 |
+
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12"
|
| 2729 |
}
|
| 2730 |
},
|
| 2731 |
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
|
| 2732 |
+
"version": "1.0.0-rc.12",
|
| 2733 |
+
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz",
|
| 2734 |
+
"integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==",
|
| 2735 |
"dev": true,
|
| 2736 |
"license": "MIT"
|
| 2737 |
},
|
|
|
|
| 2917 |
}
|
| 2918 |
},
|
| 2919 |
"node_modules/vite": {
|
| 2920 |
+
"version": "8.0.3",
|
| 2921 |
+
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz",
|
| 2922 |
+
"integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==",
|
| 2923 |
"dev": true,
|
| 2924 |
"license": "MIT",
|
| 2925 |
"dependencies": {
|
| 2926 |
"lightningcss": "^1.32.0",
|
| 2927 |
"picomatch": "^4.0.4",
|
| 2928 |
"postcss": "^8.5.8",
|
| 2929 |
+
"rolldown": "1.0.0-rc.12",
|
| 2930 |
"tinyglobby": "^0.2.15"
|
| 2931 |
},
|
| 2932 |
"bin": {
|
|
|
|
| 2944 |
"peerDependencies": {
|
| 2945 |
"@types/node": "^20.19.0 || >=22.12.0",
|
| 2946 |
"@vitejs/devtools": "^0.1.0",
|
| 2947 |
+
"esbuild": "^0.27.0",
|
| 2948 |
"jiti": ">=1.21.0",
|
| 2949 |
"less": "^4.0.0",
|
| 2950 |
"sass": "^1.70.0",
|
frontend/src/components/EpisodeEndOverlay.jsx
CHANGED
|
@@ -6,83 +6,40 @@ const EpisodeEndOverlay = ({ isOpen, onClose, metrics, gameState }) => {
|
|
| 6 |
const handleDownload = () => {
|
| 7 |
if (!gameState) return;
|
| 8 |
|
|
|
|
| 9 |
const sc = gameState.scenario || {};
|
| 10 |
-
const
|
| 11 |
-
const
|
| 12 |
-
|
| 13 |
-
|
| 14 |
let report = `=================================================================\n`;
|
| 15 |
-
report += `
|
| 16 |
report += `=================================================================\n\n`;
|
| 17 |
-
|
| 18 |
report += `[ SCENARIO METADATA ]\n`;
|
| 19 |
report += `Title: ${sc.id || 'N/A'}\n`;
|
| 20 |
report += `Domain: ${sc.domain || 'N/A'}\n`;
|
| 21 |
report += `Difficulty: ${sc.difficulty || 'N/A'}\n`;
|
| 22 |
-
report += `Final Score:
|
| 23 |
-
report += `Total Steps: ${
|
| 24 |
-
report += `Active Agents: ${Object.keys(allAgents).length}\n`;
|
| 25 |
-
report += `Status: ${unifiedSummary?.isSuccess ? 'SUCCESS' : 'INCONCLUSIVE'}\n\n`;
|
| 26 |
-
|
| 27 |
-
report += `[ AGENTS DEPLOYED ]\n`;
|
| 28 |
-
Object.entries(allAgents).forEach(([agentId, agentData], idx) => {
|
| 29 |
-
const msgs = agentData?.messages || [];
|
| 30 |
-
const msgCount = msgs.filter(m => m.type === 'message').length;
|
| 31 |
-
const toolCount = msgs.filter(m => m.type === 'tool_call').length;
|
| 32 |
-
report += `${idx + 1}. ${agentId}: ${msgCount} messages, ${toolCount} tool calls\n`;
|
| 33 |
-
});
|
| 34 |
-
report += `\n`;
|
| 35 |
-
|
| 36 |
-
// UNIFIED SUMMARY SECTION
|
| 37 |
-
if (unifiedSummary) {
|
| 38 |
-
report += `=================================================================\n`;
|
| 39 |
-
report += `[ UNIFIED INVESTIGATION SUMMARY ]\n`;
|
| 40 |
-
report += `=================================================================\n\n`;
|
| 41 |
-
|
| 42 |
-
report += `## Combined Agent Conclusions\n`;
|
| 43 |
-
report += `${unifiedSummary.conclusionText || 'No conclusions recorded.'}\n\n`;
|
| 44 |
-
|
| 45 |
-
report += `## Key Findings & Clues\n`;
|
| 46 |
-
report += `${unifiedSummary.keyFindings || 'None recorded.'}\n\n`;
|
| 47 |
-
|
| 48 |
-
report += `## Key Tool Results\n`;
|
| 49 |
-
report += `${unifiedSummary.toolSummary}\n\n`;
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
report += `[ STEP REWARDS ]\n`;
|
| 53 |
-
if (gameState?.rewardHistory && gameState.rewardHistory.length > 0) {
|
| 54 |
-
gameState.rewardHistory.forEach((r, i) => {
|
| 55 |
-
report += `Step ${i + 1}: ${r.toFixed(4)}\n`;
|
| 56 |
-
});
|
| 57 |
-
report += `Average: ${(gameState.rewardHistory.reduce((a, b) => a + b, 0) / gameState.rewardHistory.length).toFixed(4)}\n`;
|
| 58 |
-
report += `Final Score: ${Number(gameState.cumulativeReward || 0).toFixed(4)}\n\n`;
|
| 59 |
-
} else {
|
| 60 |
-
report += `No step rewards recorded.\n\n`;
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
report += `[ REWARD BREAKDOWN ]\n`;
|
| 64 |
-
if (gameState?.rewardBreakdown && Object.keys(gameState.rewardBreakdown).length > 0) {
|
| 65 |
-
Object.entries(gameState.rewardBreakdown).forEach(([key, val]) => {
|
| 66 |
-
report += `${key}: ${typeof val === 'number' ? val.toFixed(4) : val}\n`;
|
| 67 |
-
});
|
| 68 |
-
report += `\n`;
|
| 69 |
-
}
|
| 70 |
|
| 71 |
report += `[ INCIDENT DESCRIPTION & PROBLEM ]\n`;
|
| 72 |
report += `${sc.description || 'No description provided.'}\n\n`;
|
| 73 |
-
|
| 74 |
report += `[ CONTEXT & ROOT CAUSE ]\n`;
|
| 75 |
report += `${sc.context || 'No context provided.'}\n`;
|
| 76 |
-
report += `Root Cause Validation: ${metrics?.rootCause || 'N/A'}\n\n`;
|
| 77 |
|
| 78 |
report += `=================================================================\n`;
|
| 79 |
report += `[ INVESTIGATION LOG & DETAILED TRACE ]\n`;
|
| 80 |
report += `=================================================================\n\n`;
|
| 81 |
|
|
|
|
|
|
|
|
|
|
| 82 |
const allErrors = [];
|
| 83 |
const allTools = [];
|
| 84 |
|
| 85 |
-
|
| 86 |
if (msg.type === 'tool_call') {
|
| 87 |
allTools.push(`- ${msg.tool_name}(${JSON.stringify(msg.params)})`);
|
| 88 |
}
|
|
@@ -90,6 +47,7 @@ const EpisodeEndOverlay = ({ isOpen, onClose, metrics, gameState }) => {
|
|
| 90 |
allErrors.push(`- Error from ${msg.tool_name}: ${msg.result}`);
|
| 91 |
}
|
| 92 |
if (msg.type === 'tool_result' && msg.result?.toLowerCase().includes('error')) {
|
|
|
|
| 93 |
allErrors.push(`- Log/Cmd Error: ${msg.result}`);
|
| 94 |
}
|
| 95 |
});
|
|
@@ -102,45 +60,40 @@ const EpisodeEndOverlay = ({ isOpen, onClose, metrics, gameState }) => {
|
|
| 102 |
}
|
| 103 |
report += `\n`;
|
| 104 |
|
| 105 |
-
report += `> SYSTEMS ERRORS DETECTED:\n`;
|
| 106 |
if (allErrors.length > 0) {
|
|
|
|
| 107 |
[...new Set(allErrors)].forEach(err => report += `${err}\n`);
|
| 108 |
} else {
|
| 109 |
-
report += `No significant system errors found.\n`;
|
| 110 |
}
|
| 111 |
report += `\n`;
|
| 112 |
|
| 113 |
report += `=================================================================\n`;
|
| 114 |
-
report += `[ SOLUTION & FIX VERIFICATION ]\n`;
|
| 115 |
report += `=================================================================\n\n`;
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
if (resCall?.params) {
|
| 119 |
-
report += `Root Cause Service: ${resCall.params.root_cause_service || 'UNKNOWN'}\n`;
|
| 120 |
-
report += `Root Cause Description: ${resCall.params.root_cause_description || 'None'}\n`;
|
| 121 |
-
report += `Fix Applied: ${resCall.params.fix_applied || 'None'}\n`;
|
| 122 |
-
} else {
|
| 123 |
-
report += `No resolution submitted.\n`;
|
| 124 |
-
}
|
| 125 |
-
report += `\n`;
|
| 126 |
|
| 127 |
report += `=================================================================\n`;
|
| 128 |
-
report += `[ RECOMMENDATIONS ]\n`;
|
| 129 |
report += `=================================================================\n\n`;
|
| 130 |
-
|
|
|
|
| 131 |
if (allTools.length > 15) {
|
| 132 |
-
report += `1. EFFICIENCY: ${allTools.length}
|
| 133 |
} else {
|
| 134 |
-
report += `1. EFFICIENCY: Tool
|
| 135 |
}
|
| 136 |
-
|
| 137 |
if (allErrors.length > 5) {
|
| 138 |
-
report += `2. ACCURACY: Multiple errors encountered.
|
| 139 |
}
|
| 140 |
|
| 141 |
-
report += `3. CAUSE-ANALYSIS:
|
| 142 |
-
report += `4. REMEDIATION:
|
| 143 |
|
|
|
|
| 144 |
const blob = new Blob([report], { type: 'text/plain;charset=utf-8' });
|
| 145 |
const url = URL.createObjectURL(blob);
|
| 146 |
const a = document.createElement('a');
|
|
@@ -152,59 +105,6 @@ const EpisodeEndOverlay = ({ isOpen, onClose, metrics, gameState }) => {
|
|
| 152 |
URL.revokeObjectURL(url);
|
| 153 |
};
|
| 154 |
|
| 155 |
-
const generateUnifiedSummary = () => {
|
| 156 |
-
const allAgents = gameState?.agents || {};
|
| 157 |
-
const agentEntries = Object.entries(allAgents);
|
| 158 |
-
|
| 159 |
-
if (agentEntries.length === 0) return null;
|
| 160 |
-
|
| 161 |
-
const allConclusions = [];
|
| 162 |
-
const allToolResults = [];
|
| 163 |
-
const allClues = gameState?.clues_found || [];
|
| 164 |
-
|
| 165 |
-
agentEntries.forEach(([agentId, agentData]) => {
|
| 166 |
-
const msgs = agentData?.messages || [];
|
| 167 |
-
const textMsgs = msgs.filter(m => m.type === 'message');
|
| 168 |
-
const lastMsg = textMsgs[textMsgs.length - 1];
|
| 169 |
-
if (lastMsg) {
|
| 170 |
-
allConclusions.push({
|
| 171 |
-
agentId,
|
| 172 |
-
content: lastMsg.content || lastMsg.text || lastMsg.message || '',
|
| 173 |
-
role: agentData.role || agentId
|
| 174 |
-
});
|
| 175 |
-
}
|
| 176 |
-
|
| 177 |
-
const toolResults = msgs.filter(m => m.type === 'tool_result');
|
| 178 |
-
toolResults.forEach(tr => {
|
| 179 |
-
if (tr.result && !tr.result.toLowerCase().includes('error')) {
|
| 180 |
-
allToolResults.push({
|
| 181 |
-
agentId,
|
| 182 |
-
tool: tr.tool_name || tr.tool,
|
| 183 |
-
result: tr.result
|
| 184 |
-
});
|
| 185 |
-
}
|
| 186 |
-
});
|
| 187 |
-
});
|
| 188 |
-
|
| 189 |
-
const conclusionText = allConclusions.map(c => c.content).join('\n\n');
|
| 190 |
-
const keyFindings = allClues.slice(0, 5).join('\n• ');
|
| 191 |
-
const toolSummary = allToolResults.length > 0
|
| 192 |
-
? allToolResults.slice(0, 3).map(t => `• ${t.tool}: ${t.result.substring(0, 100)}...`).join('\n')
|
| 193 |
-
: 'No tool results recorded.';
|
| 194 |
-
|
| 195 |
-
const isSuccess = Number(gameState?.cumulativeReward || metrics?.score || 0) >= 0.5;
|
| 196 |
-
|
| 197 |
-
return {
|
| 198 |
-
conclusionText,
|
| 199 |
-
keyFindings: keyFindings || 'No clues recorded.',
|
| 200 |
-
toolSummary,
|
| 201 |
-
agentCount: agentEntries.length,
|
| 202 |
-
isSuccess
|
| 203 |
-
};
|
| 204 |
-
};
|
| 205 |
-
|
| 206 |
-
const unifiedSummary = generateUnifiedSummary();
|
| 207 |
-
|
| 208 |
return (
|
| 209 |
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 md:p-8 animate-in fade-in duration-500">
|
| 210 |
{/* Particle/Pulse Background */}
|
|
@@ -215,7 +115,7 @@ const EpisodeEndOverlay = ({ isOpen, onClose, metrics, gameState }) => {
|
|
| 215 |
</div>
|
| 216 |
|
| 217 |
{/* Summary Modal */}
|
| 218 |
-
<div className="relative w-full max-w-
|
| 219 |
{/* Modal Header */}
|
| 220 |
<div className="flex items-center justify-between p-6 bg-surface-container-highest/20 border-b border-white/5">
|
| 221 |
<div className="flex items-center gap-3">
|
|
@@ -229,209 +129,139 @@ const EpisodeEndOverlay = ({ isOpen, onClose, metrics, gameState }) => {
|
|
| 229 |
</button>
|
| 230 |
</div>
|
| 231 |
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
<div className="
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
<div className="
|
| 238 |
-
<span className="font-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
</span>
|
| 243 |
-
<span className="font-headline text-2xl text-primary/40 font-light">/ 1.00</span>
|
| 244 |
-
</div>
|
| 245 |
</div>
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
<
|
| 250 |
-
|
| 251 |
-
<div className="grid grid-cols-4 gap-2">
|
| 252 |
-
{Object.entries(gameState.rewardBreakdown).map(([key, val]) => (
|
| 253 |
-
<div key={key} className="text-center bg-surface-container-high/30 rounded p-2">
|
| 254 |
-
<div className="text-[8px] text-slate-500 uppercase truncate">{key.replace(/_/g, ' ')}</div>
|
| 255 |
-
<div className={`font-mono text-sm font-bold ${val > 0 ? 'text-primary' : 'text-slate-600'}`}>
|
| 256 |
-
{typeof val === 'number' ? val.toFixed(3) : val}
|
| 257 |
-
</div>
|
| 258 |
-
</div>
|
| 259 |
-
))}
|
| 260 |
-
</div>
|
| 261 |
-
</div>
|
| 262 |
-
)}
|
| 263 |
-
|
| 264 |
-
{/* Reward History */}
|
| 265 |
-
{gameState?.rewardHistory && gameState.rewardHistory.length > 0 && (
|
| 266 |
-
<div className="p-4 bg-surface-container-lowest/50 border border-white/10 rounded-lg">
|
| 267 |
-
<span className="font-mono text-[10px] text-outline uppercase block mb-3">Step Rewards</span>
|
| 268 |
-
<div className="flex items-end gap-1 h-16">
|
| 269 |
-
{gameState.rewardHistory.map((r, i) => (
|
| 270 |
-
<div key={i} className="flex-1 bg-primary/60 rounded-t"
|
| 271 |
-
style={{ height: `${Math.max(5, (r / 1) * 100)}%` }}
|
| 272 |
-
title={`Step ${i + 1}: ${r.toFixed(3)}`}>
|
| 273 |
-
</div>
|
| 274 |
-
))}
|
| 275 |
-
</div>
|
| 276 |
-
<div className="flex justify-between mt-2 text-[9px] font-mono text-slate-500">
|
| 277 |
-
<span>Avg: {(gameState.rewardHistory.reduce((a, b) => a + b, 0) / gameState.rewardHistory.length).toFixed(3)}</span>
|
| 278 |
-
<span>Max: {Math.max(...gameState.rewardHistory).toFixed(3)}</span>
|
| 279 |
-
</div>
|
| 280 |
-
</div>
|
| 281 |
-
)}
|
| 282 |
-
<div className="grid grid-cols-2 gap-4">
|
| 283 |
-
<div className="bg-surface-container-lowest/50 p-4 border-l border-primary/20 refractive-edge">
|
| 284 |
-
<span className="font-mono text-[9px] text-outline uppercase block mb-1">Clues Found</span>
|
| 285 |
-
<span className="font-headline text-2xl font-medium">{gameState?.clues_found?.length || 0}</span>
|
| 286 |
-
</div>
|
| 287 |
-
<div className="bg-surface-container-lowest/50 p-4 border-l border-primary/20 refractive-edge">
|
| 288 |
-
<span className="font-mono text-[9px] text-outline uppercase block mb-1">Steps Executed</span>
|
| 289 |
-
<span className="font-headline text-2xl font-medium">{gameState?.step !== undefined ? gameState.step : (metrics?.steps !== undefined ? metrics.steps : '—')}</span>
|
| 290 |
-
</div>
|
| 291 |
</div>
|
| 292 |
-
<div className="
|
| 293 |
-
<
|
| 294 |
-
|
| 295 |
-
</div>
|
| 296 |
-
<div>
|
| 297 |
-
<span className="font-mono text-[10px] text-tertiary/60 uppercase block">State Validation</span>
|
| 298 |
-
<span className="text-sm font-medium tracking-wide">Status: <span className="font-mono text-tertiary">{metrics?.rootCause || '—'}</span></span>
|
| 299 |
-
</div>
|
| 300 |
</div>
|
| 301 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
<span className=
|
|
|
|
| 323 |
</div>
|
| 324 |
-
<div className="
|
| 325 |
-
<
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
<
|
| 330 |
-
|
| 331 |
-
<span className={`font-headline text-lg font-medium ${idx === 0 ? 'text-primary' : idx === 1 ? 'text-secondary' : idx === 2 ? 'text-tertiary' : 'text-error'}`}>{toolCount}</span>
|
| 332 |
-
</div>
|
| 333 |
-
<div>
|
| 334 |
-
<span className="font-mono text-[9px] text-outline flex flex-col items-center justify-center gap-1 uppercase"><span className="material-symbols-outlined text-[12px]">warning</span> ERRS</span>
|
| 335 |
-
<span className={`font-headline text-lg font-medium ${idx === 0 ? 'text-primary' : idx === 1 ? 'text-secondary' : idx === 2 ? 'text-tertiary' : 'text-error'}`}>{errCount}</span>
|
| 336 |
-
</div>
|
| 337 |
</div>
|
| 338 |
</div>
|
| 339 |
-
|
| 340 |
-
)
|
| 341 |
-
|
| 342 |
</div>
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
return (
|
| 351 |
-
<div className="px-8 pb-4">
|
| 352 |
-
<div className="p-6 bg-surface-container-low/40 border border-primary/20 rounded-lg">
|
| 353 |
-
<h3 className="font-headline font-bold text-primary tracking-widest uppercase mb-4 flex items-center gap-2">
|
| 354 |
-
<span className="material-symbols-outlined">description</span>
|
| 355 |
-
Incident Resolution Report
|
| 356 |
-
</h3>
|
| 357 |
-
<div className="space-y-4">
|
| 358 |
-
<div>
|
| 359 |
-
<span className="font-mono text-[10px] text-outline uppercase block mb-1">Root Cause Service</span>
|
| 360 |
-
<span className="font-mono text-sm text-on-surface bg-surface-container p-1 px-2 rounded border border-white/5">{p.root_cause_service || 'UNKNOWN'}</span>
|
| 361 |
-
</div>
|
| 362 |
-
<div>
|
| 363 |
-
<span className="font-mono text-[10px] text-outline uppercase block mb-1">Root Cause Description</span>
|
| 364 |
-
<p className="text-sm text-on-surface/80">{p.root_cause_description || 'No description provided.'}</p>
|
| 365 |
-
</div>
|
| 366 |
-
<div className="p-4 bg-tertiary/5 border-l-2 border-tertiary rounded-r">
|
| 367 |
-
<span className="font-mono text-[10px] text-tertiary uppercase block mb-1">Fix Applied</span>
|
| 368 |
-
<p className="text-sm text-on-surface">{p.fix_applied || 'No fix described.'}</p>
|
| 369 |
-
</div>
|
| 370 |
-
</div>
|
| 371 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
</div>
|
| 373 |
-
|
| 374 |
-
|
|
|
|
| 375 |
|
| 376 |
-
|
| 377 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
<div className="px-8 pb-8">
|
| 379 |
-
<div className="p-6 bg-
|
| 380 |
<h3 className="font-headline font-bold text-primary tracking-widest uppercase mb-4 flex items-center gap-2">
|
| 381 |
-
<span className="material-symbols-outlined">
|
| 382 |
-
|
| 383 |
</h3>
|
| 384 |
-
|
| 385 |
-
{/* Success/Failure Badge */}
|
| 386 |
-
<div className="mb-4">
|
| 387 |
-
<span className={`inline-flex items-center gap-2 px-3 py-1 rounded-full text-xs font-mono font-bold uppercase ${
|
| 388 |
-
unifiedSummary.isSuccess
|
| 389 |
-
? 'bg-tertiary/20 text-tertiary border border-tertiary/30'
|
| 390 |
-
: 'bg-error/20 text-error border border-error/30'
|
| 391 |
-
}`}>
|
| 392 |
-
<span className={`w-2 h-2 rounded-full ${unifiedSummary.isSuccess ? 'bg-tertiary' : 'bg-error'}`}></span>
|
| 393 |
-
{unifiedSummary.isSuccess ? 'Investigation Successful' : 'Investigation Inconclusive'}
|
| 394 |
-
</span>
|
| 395 |
-
<span className="ml-3 text-[10px] text-outline font-mono uppercase">
|
| 396 |
-
{unifiedSummary.agentCount} Agents Collaborated
|
| 397 |
-
</span>
|
| 398 |
-
</div>
|
| 399 |
-
|
| 400 |
-
{/* Combined Conclusions */}
|
| 401 |
-
<div className="mb-4">
|
| 402 |
-
<span className="font-mono text-[10px] text-primary/60 uppercase tracking-widest block mb-2">Combined Agent Conclusions</span>
|
| 403 |
-
<div className="p-4 bg-surface-container-low/50 rounded border border-white/5 max-h-48 overflow-y-auto custom-scrollbar">
|
| 404 |
-
<p className="text-sm text-on-surface/90 leading-relaxed whitespace-pre-wrap">
|
| 405 |
-
{unifiedSummary.conclusionText || 'No conclusions recorded.'}
|
| 406 |
-
</p>
|
| 407 |
-
</div>
|
| 408 |
-
</div>
|
| 409 |
-
|
| 410 |
-
{/* Key Findings */}
|
| 411 |
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
| 412 |
<div>
|
| 413 |
-
<span className="font-mono text-[10px] text-
|
| 414 |
-
<
|
| 415 |
-
<ul className="text-xs text-on-surface/80 space-y-1 list-disc list-inside">
|
| 416 |
-
{unifiedSummary.keyFindings.split('\n• ').map((finding, i) => (
|
| 417 |
-
finding && <li key={i}>{finding}</li>
|
| 418 |
-
))}
|
| 419 |
-
</ul>
|
| 420 |
-
</div>
|
| 421 |
</div>
|
| 422 |
<div>
|
| 423 |
-
<span className="font-mono text-[10px] text-
|
| 424 |
-
<
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
</
|
| 429 |
</div>
|
| 430 |
</div>
|
| 431 |
</div>
|
| 432 |
</div>
|
| 433 |
-
)
|
| 434 |
-
|
| 435 |
|
| 436 |
{/* Modal Footer */}
|
| 437 |
<div className="p-6 bg-surface-container-lowest/90 border-t border-white/5 flex flex-col md:flex-row justify-between items-center gap-4">
|
|
|
|
| 6 |
const handleDownload = () => {
|
| 7 |
if (!gameState) return;
|
| 8 |
|
| 9 |
+
// Assemble the detailed incident report
|
| 10 |
const sc = gameState.scenario || {};
|
| 11 |
+
const agentA = gameState.agents?.agent_a?.messages || [];
|
| 12 |
+
const agentB = gameState.agents?.agent_b?.messages || [];
|
| 13 |
+
|
|
|
|
| 14 |
let report = `=================================================================\n`;
|
| 15 |
+
report += ` NEXUS INCIDENT INVESTIGATION REPORT \n`;
|
| 16 |
report += `=================================================================\n\n`;
|
| 17 |
+
|
| 18 |
report += `[ SCENARIO METADATA ]\n`;
|
| 19 |
report += `Title: ${sc.id || 'N/A'}\n`;
|
| 20 |
report += `Domain: ${sc.domain || 'N/A'}\n`;
|
| 21 |
report += `Difficulty: ${sc.difficulty || 'N/A'}\n`;
|
| 22 |
+
report += `Final Score: ${metrics?.score || 'N/A'}\n`;
|
| 23 |
+
report += `Total Steps: ${metrics?.steps || 'N/A'}\n\n`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
report += `[ INCIDENT DESCRIPTION & PROBLEM ]\n`;
|
| 26 |
report += `${sc.description || 'No description provided.'}\n\n`;
|
| 27 |
+
|
| 28 |
report += `[ CONTEXT & ROOT CAUSE ]\n`;
|
| 29 |
report += `${sc.context || 'No context provided.'}\n`;
|
| 30 |
+
report += `Actual Root Cause Validation: ${metrics?.rootCause || 'N/A'}\n\n`;
|
| 31 |
|
| 32 |
report += `=================================================================\n`;
|
| 33 |
report += `[ INVESTIGATION LOG & DETAILED TRACE ]\n`;
|
| 34 |
report += `=================================================================\n\n`;
|
| 35 |
|
| 36 |
+
// Interweave the messages to show the timeline (roughly)
|
| 37 |
+
// Since we don't have exact timestamps, we'll just print Agent A then Agent B summary,
|
| 38 |
+
// or just print all tools called and errors encountered.
|
| 39 |
const allErrors = [];
|
| 40 |
const allTools = [];
|
| 41 |
|
| 42 |
+
[...agentA, ...agentB].forEach(msg => {
|
| 43 |
if (msg.type === 'tool_call') {
|
| 44 |
allTools.push(`- ${msg.tool_name}(${JSON.stringify(msg.params)})`);
|
| 45 |
}
|
|
|
|
| 47 |
allErrors.push(`- Error from ${msg.tool_name}: ${msg.result}`);
|
| 48 |
}
|
| 49 |
if (msg.type === 'tool_result' && msg.result?.toLowerCase().includes('error')) {
|
| 50 |
+
// Catch strings that say error but were marked success true somehow
|
| 51 |
allErrors.push(`- Log/Cmd Error: ${msg.result}`);
|
| 52 |
}
|
| 53 |
});
|
|
|
|
| 60 |
}
|
| 61 |
report += `\n`;
|
| 62 |
|
| 63 |
+
report += `> SYSTEMS ERRORS DETECTED DURING INVESTIGATION:\n`;
|
| 64 |
if (allErrors.length > 0) {
|
| 65 |
+
// deduplicate
|
| 66 |
[...new Set(allErrors)].forEach(err => report += `${err}\n`);
|
| 67 |
} else {
|
| 68 |
+
report += `No significant system errors found during tool execution.\n`;
|
| 69 |
}
|
| 70 |
report += `\n`;
|
| 71 |
|
| 72 |
report += `=================================================================\n`;
|
| 73 |
+
report += `[ SOLUTION IMPLEMENTED & FIX VERIFICATION ]\n`;
|
| 74 |
report += `=================================================================\n\n`;
|
| 75 |
+
report += `The Validator Agent verified the proposed fix successfully, leading to the resolution of the incident.\n`;
|
| 76 |
+
report += `End-state: ${metrics?.rootCause === 'VERIFIED' ? 'SUCCESS' : 'UNKNOWN'}\n\n`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
report += `=================================================================\n`;
|
| 79 |
+
report += `[ TIPS FOR IMPROVEMENT & RECOMMENDATIONS ]\n`;
|
| 80 |
report += `=================================================================\n\n`;
|
| 81 |
+
report += `Based on the automated evaluation of this scenario, consider the following:\n`;
|
| 82 |
+
|
| 83 |
if (allTools.length > 15) {
|
| 84 |
+
report += `1. EFFICIENCY: The agents called a large number of tools (${allTools.length}). Consider refining the initial hypothesis to reduce blind querying.\n`;
|
| 85 |
} else {
|
| 86 |
+
report += `1. EFFICIENCY: Tool execution was relatively concise (${allTools.length} calls).\n`;
|
| 87 |
}
|
| 88 |
+
|
| 89 |
if (allErrors.length > 5) {
|
| 90 |
+
report += `2. ACCURACY: Multiple tool execution errors were encountered. Ensure exact syntax and correct tool parameters are used to minimize invalid calls.\n`;
|
| 91 |
}
|
| 92 |
|
| 93 |
+
report += `3. CAUSE-ANALYSIS: Always grep application error logs before querying databases to save time tracking downstream symptoms.\n`;
|
| 94 |
+
report += `4. REMEDIATION: Post-incident reviews should establish better automated alerting for the specific failure domain (${sc.domain || 'general'}).\n`;
|
| 95 |
|
| 96 |
+
// Trigger Download
|
| 97 |
const blob = new Blob([report], { type: 'text/plain;charset=utf-8' });
|
| 98 |
const url = URL.createObjectURL(blob);
|
| 99 |
const a = document.createElement('a');
|
|
|
|
| 105 |
URL.revokeObjectURL(url);
|
| 106 |
};
|
| 107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
return (
|
| 109 |
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 md:p-8 animate-in fade-in duration-500">
|
| 110 |
{/* Particle/Pulse Background */}
|
|
|
|
| 115 |
</div>
|
| 116 |
|
| 117 |
{/* Summary Modal */}
|
| 118 |
+
<div className="relative w-full max-w-4xl glass-panel rounded-xl overflow-hidden shadow-[0_0_80px_rgba(0,0,0,0.8)] border border-white/10">
|
| 119 |
{/* Modal Header */}
|
| 120 |
<div className="flex items-center justify-between p-6 bg-surface-container-highest/20 border-b border-white/5">
|
| 121 |
<div className="flex items-center gap-3">
|
|
|
|
| 129 |
</button>
|
| 130 |
</div>
|
| 131 |
|
| 132 |
+
<div className="p-8 grid grid-cols-1 md:grid-cols-2 gap-8">
|
| 133 |
+
{/* Primary Metrics */}
|
| 134 |
+
<div className="space-y-6">
|
| 135 |
+
<div className="space-y-2">
|
| 136 |
+
<span className="font-mono text-[10px] text-outline tracking-widest uppercase">Cumulative Efficiency Score</span>
|
| 137 |
+
<div className="flex items-baseline gap-2">
|
| 138 |
+
<span className="font-headline text-8xl font-bold text-transparent bg-clip-text bg-gradient-to-br from-primary to-primary-container drop-shadow-[0_0_15px_rgba(0,212,255,0.3)]">
|
| 139 |
+
{metrics?.score || '—'}
|
| 140 |
+
</span>
|
| 141 |
+
<span className="font-headline text-2xl text-primary/40 font-light">pts</span>
|
|
|
|
|
|
|
|
|
|
| 142 |
</div>
|
| 143 |
+
</div>
|
| 144 |
+
<div className="grid grid-cols-2 gap-4">
|
| 145 |
+
<div className="bg-surface-container-lowest/50 p-4 border-l border-primary/20 refractive-edge">
|
| 146 |
+
<span className="font-mono text-[9px] text-outline uppercase block mb-1">Clues Found</span>
|
| 147 |
+
<span className="font-headline text-2xl font-medium">{gameState?.clues_found?.length || 0}</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
</div>
|
| 149 |
+
<div className="bg-surface-container-lowest/50 p-4 border-l border-primary/20 refractive-edge">
|
| 150 |
+
<span className="font-mono text-[9px] text-outline uppercase block mb-1">Steps Executed</span>
|
| 151 |
+
<span className="font-headline text-2xl font-medium">{gameState?.current_round || metrics?.steps || '—'}</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
</div>
|
| 153 |
</div>
|
| 154 |
+
<div className="flex items-center gap-4 p-5 bg-tertiary/5 border border-tertiary/10 rounded-lg">
|
| 155 |
+
<div className="p-3 rounded-full bg-tertiary/10 text-tertiary">
|
| 156 |
+
<span className="material-symbols-outlined">troubleshoot</span>
|
| 157 |
+
</div>
|
| 158 |
+
<div>
|
| 159 |
+
<span className="font-mono text-[10px] text-tertiary/60 uppercase block">State Validation</span>
|
| 160 |
+
<span className="text-sm font-medium tracking-wide">Status: <span className="font-mono text-tertiary">{metrics?.rootCause || '—'}</span></span>
|
| 161 |
+
</div>
|
| 162 |
+
</div>
|
| 163 |
+
</div>
|
| 164 |
|
| 165 |
+
{/* Right Column: Agent Metrics */}
|
| 166 |
+
<div className="space-y-6">
|
| 167 |
+
<h3 className="font-mono text-[10px] text-outline tracking-widest uppercase mb-4">Agent Performance Breakdown</h3>
|
| 168 |
+
{/* Agent A */}
|
| 169 |
+
<div className="relative group">
|
| 170 |
+
<div className="absolute -left-4 top-0 bottom-0 w-1 bg-primary shadow-[0_0_8px_rgba(0,212,255,0.4)]"></div>
|
| 171 |
+
<div className="bg-surface-container-low/40 p-5 space-y-4 border border-white/5 rounded-r-lg">
|
| 172 |
+
<div className="flex justify-between items-center">
|
| 173 |
+
<span className="font-headline font-bold text-primary tracking-tighter uppercase">Agent_Alpha</span>
|
| 174 |
+
<span className="font-mono text-[10px] text-primary/50">CYAN_PROTOCOL</span>
|
| 175 |
+
</div>
|
| 176 |
+
{(() => {
|
| 177 |
+
const msgs = gameState?.agents?.agent_a?.messages || [];
|
| 178 |
+
const msgCount = msgs.filter(m => m.type === 'message').length;
|
| 179 |
+
const toolCount = msgs.filter(m => m.type === 'tool_call').length;
|
| 180 |
+
const errCount = msgs.filter(m => m.type === 'tool_result' && m.result?.toLowerCase().includes('error')).length;
|
| 181 |
+
return (
|
| 182 |
+
<div className="grid grid-cols-3 gap-2 text-center">
|
| 183 |
+
<div>
|
| 184 |
+
<span className="font-mono text-[9px] text-outline flex flex-col items-center justify-center gap-1 uppercase"><span className="material-symbols-outlined text-[12px]">chat</span> MSGS</span>
|
| 185 |
+
<span className="font-headline text-lg font-medium text-primary">{msgCount}</span>
|
| 186 |
</div>
|
| 187 |
+
<div className="border-x border-white/5">
|
| 188 |
+
<span className="font-mono text-[9px] text-outline flex flex-col items-center justify-center gap-1 uppercase"><span className="material-symbols-outlined text-[12px]">build</span> TOOLS</span>
|
| 189 |
+
<span className="font-headline text-lg font-medium text-primary">{toolCount}</span>
|
| 190 |
+
</div>
|
| 191 |
+
<div>
|
| 192 |
+
<span className="font-mono text-[9px] text-outline flex flex-col items-center justify-center gap-1 uppercase"><span className="material-symbols-outlined text-[12px]">warning</span> ERRS</span>
|
| 193 |
+
<span className="font-headline text-lg font-medium text-primary">{errCount}</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
</div>
|
| 195 |
</div>
|
| 196 |
+
);
|
| 197 |
+
})()}
|
| 198 |
+
</div>
|
| 199 |
</div>
|
| 200 |
+
{/* Agent B */}
|
| 201 |
+
<div className="relative group">
|
| 202 |
+
<div className="absolute -left-4 top-0 bottom-0 w-1 bg-secondary shadow-[0_0_8px_rgba(221,183,255,0.4)]"></div>
|
| 203 |
+
<div className="bg-surface-container-low/40 p-5 space-y-4 border border-white/5 rounded-r-lg">
|
| 204 |
+
<div className="flex justify-between items-center">
|
| 205 |
+
<span className="font-headline font-bold text-secondary tracking-tighter uppercase">Agent_Bravo</span>
|
| 206 |
+
<span className="font-mono text-[10px] text-secondary/50">VIOLET_PROTOCOL</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
</div>
|
| 208 |
+
{(() => {
|
| 209 |
+
const msgs = gameState?.agents?.agent_b?.messages || [];
|
| 210 |
+
const msgCount = msgs.filter(m => m.type === 'message').length;
|
| 211 |
+
const toolCount = msgs.filter(m => m.type === 'tool_call').length;
|
| 212 |
+
const errCount = msgs.filter(m => m.type === 'tool_result' && m.result?.toLowerCase().includes('error')).length;
|
| 213 |
+
return (
|
| 214 |
+
<div className="grid grid-cols-3 gap-2 text-center">
|
| 215 |
+
<div>
|
| 216 |
+
<span className="font-mono text-[9px] text-outline flex flex-col items-center justify-center gap-1 uppercase"><span className="material-symbols-outlined text-[12px]">chat</span> MSGS</span>
|
| 217 |
+
<span className="font-headline text-lg font-medium text-secondary">{msgCount}</span>
|
| 218 |
+
</div>
|
| 219 |
+
<div className="border-x border-white/5">
|
| 220 |
+
<span className="font-mono text-[9px] text-outline flex flex-col items-center justify-center gap-1 uppercase"><span className="material-symbols-outlined text-[12px]">build</span> TOOLS</span>
|
| 221 |
+
<span className="font-headline text-lg font-medium text-secondary">{toolCount}</span>
|
| 222 |
+
</div>
|
| 223 |
+
<div>
|
| 224 |
+
<span className="font-mono text-[9px] text-outline flex flex-col items-center justify-center gap-1 uppercase"><span className="material-symbols-outlined text-[12px]">warning</span> ERRS</span>
|
| 225 |
+
<span className="font-headline text-lg font-medium text-secondary">{errCount}</span>
|
| 226 |
+
</div>
|
| 227 |
+
</div>
|
| 228 |
+
);
|
| 229 |
+
})()}
|
| 230 |
</div>
|
| 231 |
+
</div>
|
| 232 |
+
</div>
|
| 233 |
+
</div>
|
| 234 |
|
| 235 |
+
{/* Submit Resolution Report Panel */}
|
| 236 |
+
{(() => {
|
| 237 |
+
const resCall = gameState?.tool_calls_made?.find(c => c.tool_name === 'submit_resolution');
|
| 238 |
+
if (!resCall) return null;
|
| 239 |
+
const p = resCall.params || {};
|
| 240 |
+
return (
|
| 241 |
<div className="px-8 pb-8">
|
| 242 |
+
<div className="p-6 bg-surface-container-low/40 border border-primary/20 rounded-lg">
|
| 243 |
<h3 className="font-headline font-bold text-primary tracking-widest uppercase mb-4 flex items-center gap-2">
|
| 244 |
+
<span className="material-symbols-outlined">description</span>
|
| 245 |
+
Incident Resolution Report
|
| 246 |
</h3>
|
| 247 |
+
<div className="space-y-4">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
<div>
|
| 249 |
+
<span className="font-mono text-[10px] text-outline uppercase block mb-1">Root Cause Service</span>
|
| 250 |
+
<span className="font-mono text-sm text-on-surface bg-surface-container p-1 px-2 rounded border border-white/5">{p.root_cause_service || 'UNKNOWN'}</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
</div>
|
| 252 |
<div>
|
| 253 |
+
<span className="font-mono text-[10px] text-outline uppercase block mb-1">Root Cause Description</span>
|
| 254 |
+
<p className="text-sm text-on-surface/80">{p.root_cause_description || 'No description provided.'}</p>
|
| 255 |
+
</div>
|
| 256 |
+
<div className="p-4 bg-tertiary/5 border-l-2 border-tertiary rounded-r">
|
| 257 |
+
<span className="font-mono text-[10px] text-tertiary uppercase block mb-1">Fix Applied</span>
|
| 258 |
+
<p className="text-sm text-on-surface">{p.fix_applied || 'No fix described.'}</p>
|
| 259 |
</div>
|
| 260 |
</div>
|
| 261 |
</div>
|
| 262 |
</div>
|
| 263 |
+
);
|
| 264 |
+
})()}
|
| 265 |
|
| 266 |
{/* Modal Footer */}
|
| 267 |
<div className="p-6 bg-surface-container-lowest/90 border-t border-white/5 flex flex-col md:flex-row justify-between items-center gap-4">
|
frontend/src/components/TopNavBar.jsx
CHANGED
|
@@ -3,12 +3,6 @@ import { useApp } from '../context/AppContext';
|
|
| 3 |
|
| 4 |
const TopNavBar = () => {
|
| 5 |
const { sessionData, isConnected, sendCommand } = useApp();
|
| 6 |
-
|
| 7 |
-
const status = sessionData?.status || 'STANDBY';
|
| 8 |
-
const isRunning = sessionData?.active && status !== 'COMPLETED';
|
| 9 |
-
|
| 10 |
-
const isStandby = status === 'STANDBY' || status === 'READY';
|
| 11 |
-
|
| 12 |
return (
|
| 13 |
<header className="fixed top-0 w-full z-50 flex justify-between items-center px-6 h-16 bg-surface/60 backdrop-blur-xl border-b border-primary/10 shadow-[0_0_40px_rgba(0,212,255,0.04)]">
|
| 14 |
<div className="flex items-center gap-8">
|
|
@@ -16,49 +10,36 @@ const TopNavBar = () => {
|
|
| 16 |
<div className="h-8 w-px bg-outline-variant/20 hidden md:block"></div>
|
| 17 |
<div className="hidden md:flex flex-col">
|
| 18 |
<span className="text-[10px] font-mono text-outline-variant tracking-widest uppercase">System Status</span>
|
| 19 |
-
<div className="text-sm font-mono text-tertiary">{status}</div>
|
| 20 |
</div>
|
| 21 |
</div>
|
| 22 |
|
| 23 |
<div className="flex items-center gap-6">
|
|
|
|
|
|
|
| 24 |
<div className="flex gap-2">
|
| 25 |
-
{/* START - clickable when standby */}
|
| 26 |
<button
|
| 27 |
onClick={() => sendCommand({ action: 'start' })}
|
| 28 |
-
disabled={
|
| 29 |
-
className=
|
| 30 |
-
? 'bg-surface-container text-slate-600 border-slate-700 cursor-not-allowed'
|
| 31 |
-
: 'bg-tertiary/10 border-tertiary/20 text-tertiary hover:bg-tertiary/20 active:scale-95'}`}
|
| 32 |
>
|
| 33 |
<span className="material-symbols-outlined text-sm">play_arrow</span> START
|
| 34 |
</button>
|
| 35 |
-
|
| 36 |
-
{/* PAUSE/RESUME - clickable when running */}
|
| 37 |
<button
|
| 38 |
onClick={() => sendCommand({ action: 'pause' })}
|
| 39 |
-
disabled={!
|
| 40 |
-
className={`flex items-center gap-2 px-4 py-1.5 rounded-full border text-xs font-bold transition-all ${
|
| 41 |
-
? 'bg-surface-container text-slate-600 border-slate-700 cursor-not-allowed'
|
| 42 |
-
: status === 'PAUSED'
|
| 43 |
-
? 'bg-secondary text-surface border-secondary active:scale-95'
|
| 44 |
-
: 'bg-secondary/10 border-secondary/20 text-secondary hover:bg-secondary/20 active:scale-95'}`}
|
| 45 |
>
|
| 46 |
-
<span className="material-symbols-outlined text-sm">{status === 'PAUSED' ? 'play_arrow' : 'pause'}</span>
|
| 47 |
-
{status === 'PAUSED' ? 'RESUME' : 'PAUSE'}
|
| 48 |
</button>
|
| 49 |
-
|
| 50 |
-
{/* FORCE END - clickable when running */}
|
| 51 |
<button
|
| 52 |
onClick={() => sendCommand({ action: 'force_end' })}
|
| 53 |
-
disabled={!
|
| 54 |
-
className=
|
| 55 |
-
? 'bg-surface-container text-slate-600 border-slate-700 cursor-not-allowed'
|
| 56 |
-
: 'bg-[#f59e0b]/10 border-[#f59e0b]/20 text-[#f59e0b] hover:bg-[#f59e0b]/20 active:scale-95'}`}
|
| 57 |
>
|
| 58 |
<span className="material-symbols-outlined text-sm">stop_circle</span> FORCE END
|
| 59 |
</button>
|
| 60 |
-
|
| 61 |
-
{/* RESET - always clickable */}
|
| 62 |
<button
|
| 63 |
onClick={() => sendCommand({ action: 'reset' })}
|
| 64 |
className="flex items-center gap-2 px-4 py-1.5 rounded-full bg-error/10 border border-error/20 text-error text-xs font-bold hover:bg-error/20 transition-all active:scale-95"
|
|
|
|
| 3 |
|
| 4 |
const TopNavBar = () => {
|
| 5 |
const { sessionData, isConnected, sendCommand } = useApp();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
return (
|
| 7 |
<header className="fixed top-0 w-full z-50 flex justify-between items-center px-6 h-16 bg-surface/60 backdrop-blur-xl border-b border-primary/10 shadow-[0_0_40px_rgba(0,212,255,0.04)]">
|
| 8 |
<div className="flex items-center gap-8">
|
|
|
|
| 10 |
<div className="h-8 w-px bg-outline-variant/20 hidden md:block"></div>
|
| 11 |
<div className="hidden md:flex flex-col">
|
| 12 |
<span className="text-[10px] font-mono text-outline-variant tracking-widest uppercase">System Status</span>
|
| 13 |
+
<div className="text-sm font-mono text-tertiary">{sessionData?.status || 'INITIALIZING...'}</div>
|
| 14 |
</div>
|
| 15 |
</div>
|
| 16 |
|
| 17 |
<div className="flex items-center gap-6">
|
| 18 |
+
|
| 19 |
+
|
| 20 |
<div className="flex gap-2">
|
|
|
|
| 21 |
<button
|
| 22 |
onClick={() => sendCommand({ action: 'start' })}
|
| 23 |
+
disabled={sessionData?.active && sessionData?.status !== 'PAUSED'}
|
| 24 |
+
className="flex items-center gap-2 px-4 py-1.5 rounded-full bg-tertiary/10 border border-tertiary/20 text-tertiary text-xs font-bold hover:bg-tertiary/20 transition-all active:scale-95 disabled:opacity-30 disabled:pointer-events-none"
|
|
|
|
|
|
|
| 25 |
>
|
| 26 |
<span className="material-symbols-outlined text-sm">play_arrow</span> START
|
| 27 |
</button>
|
|
|
|
|
|
|
| 28 |
<button
|
| 29 |
onClick={() => sendCommand({ action: 'pause' })}
|
| 30 |
+
disabled={!sessionData?.active}
|
| 31 |
+
className={`flex items-center gap-2 px-4 py-1.5 rounded-full border text-xs font-bold transition-all active:scale-95 disabled:opacity-30 ${sessionData?.status === 'PAUSED' ? 'bg-secondary text-surface border-secondary' : 'bg-secondary/10 border-secondary/20 text-secondary hover:bg-secondary/20'}`}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
>
|
| 33 |
+
<span className="material-symbols-outlined text-sm">{sessionData?.status === 'PAUSED' ? 'play_arrow' : 'pause'}</span>
|
| 34 |
+
{sessionData?.status === 'PAUSED' ? 'RESUME' : 'PAUSE'}
|
| 35 |
</button>
|
|
|
|
|
|
|
| 36 |
<button
|
| 37 |
onClick={() => sendCommand({ action: 'force_end' })}
|
| 38 |
+
disabled={!sessionData?.active}
|
| 39 |
+
className="flex items-center gap-2 px-4 py-1.5 rounded-full bg-[#f59e0b]/10 border border-[#f59e0b]/20 text-[#f59e0b] text-xs font-bold hover:bg-[#f59e0b]/20 transition-all active:scale-95 disabled:opacity-30"
|
|
|
|
|
|
|
| 40 |
>
|
| 41 |
<span className="material-symbols-outlined text-sm">stop_circle</span> FORCE END
|
| 42 |
</button>
|
|
|
|
|
|
|
| 43 |
<button
|
| 44 |
onClick={() => sendCommand({ action: 'reset' })}
|
| 45 |
className="flex items-center gap-2 px-4 py-1.5 rounded-full bg-error/10 border border-error/20 text-error text-xs font-bold hover:bg-error/20 transition-all active:scale-95"
|
frontend/src/hooks/useWebSocket.js
CHANGED
|
@@ -1,211 +1,147 @@
|
|
| 1 |
-
import { useState, useEffect, useCallback, useRef } from 'react';
|
| 2 |
-
|
| 3 |
-
const useWebSocket = (url) => {
|
| 4 |
-
const [events, setEvents] = useState([]);
|
| 5 |
-
const [gameState, setGameState] = useState({
|
| 6 |
-
scenario: null,
|
| 7 |
-
active: false,
|
| 8 |
-
status: 'AWAITING_OBJECTIVE',
|
| 9 |
-
step: 0,
|
| 10 |
-
reward: 0,
|
| 11 |
-
cumulativeReward: 0,
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
const
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
socketRef.current
|
| 26 |
-
|
| 27 |
-
socketRef.current.
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
if (data.
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
result: data.result,
|
| 149 |
-
success: data.success
|
| 150 |
-
});
|
| 151 |
-
agents[activeId] = agentTarget;
|
| 152 |
-
newState.agents = agents;
|
| 153 |
-
}
|
| 154 |
-
}
|
| 155 |
-
|
| 156 |
-
// Simple heuristic for clues if not sent explicitly
|
| 157 |
-
const res = data.result?.toLowerCase() || '';
|
| 158 |
-
if (res.includes('error') || res.includes('anomaly') || res.includes('warning') || res.includes('degraded') || data.tool_name === 'propose_fix') {
|
| 159 |
-
const currentClues = newState.clues_found || [];
|
| 160 |
-
if (!currentClues.includes(data.result)) {
|
| 161 |
-
newState.clues_found = [...currentClues, data.result];
|
| 162 |
-
}
|
| 163 |
-
}
|
| 164 |
-
}
|
| 165 |
-
|
| 166 |
-
if (data.type === 'reward_update') {
|
| 167 |
-
newState.reward = data.reward;
|
| 168 |
-
newState.cumulativeReward = data.cumulative;
|
| 169 |
-
newState.rewardBreakdown = data.breakdown || {};
|
| 170 |
-
newState.rewardHistory = [...(newState.rewardHistory || []), data.reward];
|
| 171 |
-
}
|
| 172 |
-
|
| 173 |
-
if (data.type === 'episode_end') {
|
| 174 |
-
newState.active = false;
|
| 175 |
-
newState.status = 'COMPLETED';
|
| 176 |
-
newState.step = data.steps_taken || newState.step;
|
| 177 |
-
newState.cumulativeReward = data.final_score !== undefined ? data.final_score : newState.cumulativeReward;
|
| 178 |
-
newState.finalScore = data.final_score;
|
| 179 |
-
newState.success = data.success;
|
| 180 |
-
newState.fixVerified = data.fix_verified;
|
| 181 |
-
if (data.clues_found) newState.clues_found = data.clues_found;
|
| 182 |
-
if (data.reward_history) newState.rewardHistory = data.reward_history;
|
| 183 |
-
if (data.final_breakdown) newState.rewardBreakdown = data.final_breakdown;
|
| 184 |
-
|
| 185 |
-
const standbyAgents = {};
|
| 186 |
-
Object.keys(newState.agents).forEach(k => {
|
| 187 |
-
standbyAgents[k] = { ...newState.agents[k], status: 'STANDBY' };
|
| 188 |
-
});
|
| 189 |
-
newState.agents = standbyAgents;
|
| 190 |
-
}
|
| 191 |
-
|
| 192 |
-
return newState;
|
| 193 |
-
});
|
| 194 |
-
};
|
| 195 |
-
|
| 196 |
-
socketRef.current.onerror = (err) => setError(err);
|
| 197 |
-
socketRef.current.onclose = () => setIsConnected(false);
|
| 198 |
-
|
| 199 |
-
return () => socketRef.current.close();
|
| 200 |
-
}, [url]);
|
| 201 |
-
|
| 202 |
-
const sendCommand = useCallback((command) => {
|
| 203 |
-
if (socketRef.current && isConnected) {
|
| 204 |
-
socketRef.current.send(JSON.stringify(command));
|
| 205 |
-
}
|
| 206 |
-
}, [isConnected]);
|
| 207 |
-
|
| 208 |
-
return { events, gameState, isConnected, error, sendCommand };
|
| 209 |
-
};
|
| 210 |
-
|
| 211 |
-
export default useWebSocket;
|
|
|
|
| 1 |
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
| 2 |
+
|
| 3 |
+
const useWebSocket = (url) => {
|
| 4 |
+
const [events, setEvents] = useState([]);
|
| 5 |
+
const [gameState, setGameState] = useState({
|
| 6 |
+
scenario: null,
|
| 7 |
+
active: false,
|
| 8 |
+
status: 'AWAITING_OBJECTIVE',
|
| 9 |
+
step: 0,
|
| 10 |
+
reward: 0,
|
| 11 |
+
cumulativeReward: 0,
|
| 12 |
+
agent_a_model: '',
|
| 13 |
+
agent_b_model: '',
|
| 14 |
+
agents: {
|
| 15 |
+
agent_a: { status: 'STANDBY', messages: [] },
|
| 16 |
+
agent_b: { status: 'STANDBY', messages: [] }
|
| 17 |
+
}
|
| 18 |
+
});
|
| 19 |
+
|
| 20 |
+
const [isConnected, setIsConnected] = useState(false);
|
| 21 |
+
const [error, setError] = useState(null);
|
| 22 |
+
const socketRef = useRef(null);
|
| 23 |
+
|
| 24 |
+
useEffect(() => {
|
| 25 |
+
socketRef.current = new WebSocket(url);
|
| 26 |
+
|
| 27 |
+
socketRef.current.onopen = () => setIsConnected(true);
|
| 28 |
+
|
| 29 |
+
socketRef.current.onmessage = (event) => {
|
| 30 |
+
const data = JSON.parse(event.data);
|
| 31 |
+
setEvents(prev => [...prev, data]);
|
| 32 |
+
|
| 33 |
+
setGameState(prev => {
|
| 34 |
+
const draft = { ...prev };
|
| 35 |
+
|
| 36 |
+
if (data.type === 'episode_start') {
|
| 37 |
+
draft.scenario = data.scenario;
|
| 38 |
+
draft.active = true;
|
| 39 |
+
draft.status = 'INVESTIGATING';
|
| 40 |
+
draft.step = 0;
|
| 41 |
+
draft.reward = 0;
|
| 42 |
+
draft.cumulativeReward = 0;
|
| 43 |
+
draft.agent_a_model = data.agent_a_model || draft.agent_a_model;
|
| 44 |
+
draft.agent_b_model = data.agent_b_model || draft.agent_b_model;
|
| 45 |
+
draft.agents.agent_a = { status: 'ACTIVE', messages: [] };
|
| 46 |
+
draft.agents.agent_b = { status: 'ACTIVE', messages: [] };
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
if (data.type === 'agent_partial') {
|
| 50 |
+
const agent = draft.agents[data.agent_id];
|
| 51 |
+
if (agent) {
|
| 52 |
+
const lastMsg = agent.messages[agent.messages.length - 1];
|
| 53 |
+
if (lastMsg && lastMsg.type === 'message' && lastMsg.partial) {
|
| 54 |
+
lastMsg.content = data.full_message;
|
| 55 |
+
} else {
|
| 56 |
+
agent.messages.push({
|
| 57 |
+
type: 'message',
|
| 58 |
+
content: data.full_message,
|
| 59 |
+
partial: true
|
| 60 |
+
});
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
if (data.type === 'agent_message') {
|
| 66 |
+
const agent = draft.agents[data.agent_id];
|
| 67 |
+
if (agent) {
|
| 68 |
+
const lastMsg = agent.messages[agent.messages.length - 1];
|
| 69 |
+
if (lastMsg && lastMsg.partial) {
|
| 70 |
+
lastMsg.content = data.message;
|
| 71 |
+
delete lastMsg.partial;
|
| 72 |
+
} else {
|
| 73 |
+
agent.messages.push({
|
| 74 |
+
type: 'message',
|
| 75 |
+
content: data.message
|
| 76 |
+
});
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
if (data.status === 'READY') {
|
| 82 |
+
draft.status = 'READY_TO_INJECT';
|
| 83 |
+
draft.active = false;
|
| 84 |
+
draft.agents.agent_a.messages = [];
|
| 85 |
+
draft.agents.agent_b.messages = [];
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
if (data.type === 'system_status') {
|
| 89 |
+
if (data.paused !== undefined) {
|
| 90 |
+
draft.status = data.paused ? 'PAUSED' : 'INVESTIGATING';
|
| 91 |
+
}
|
| 92 |
+
if (data.status) {
|
| 93 |
+
draft.status = data.status;
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
if (data.type === 'tool_call') {
|
| 98 |
+
if (draft.agents[data.agent_id]) {
|
| 99 |
+
draft.agents[data.agent_id].messages.push({
|
| 100 |
+
type: 'tool_call',
|
| 101 |
+
tool_name: data.tool_name,
|
| 102 |
+
params: data.params
|
| 103 |
+
});
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
if (data.type === 'tool_result') {
|
| 108 |
+
draft.agents.agent_a.messages.push({
|
| 109 |
+
type: 'tool_result',
|
| 110 |
+
tool_name: data.tool_name,
|
| 111 |
+
result: data.result,
|
| 112 |
+
success: data.success
|
| 113 |
+
});
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
if (data.type === 'reward_update') {
|
| 117 |
+
draft.reward = data.reward;
|
| 118 |
+
draft.cumulativeReward = data.cumulative;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
if (data.type === 'episode_end') {
|
| 122 |
+
draft.active = false;
|
| 123 |
+
draft.status = 'COMPLETED';
|
| 124 |
+
draft.agents.agent_a.status = 'STANDBY';
|
| 125 |
+
draft.agents.agent_b.status = 'STANDBY';
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
return draft;
|
| 129 |
+
});
|
| 130 |
+
};
|
| 131 |
+
|
| 132 |
+
socketRef.current.onerror = (err) => setError(err);
|
| 133 |
+
socketRef.current.onclose = () => setIsConnected(false);
|
| 134 |
+
|
| 135 |
+
return () => socketRef.current.close();
|
| 136 |
+
}, [url]);
|
| 137 |
+
|
| 138 |
+
const sendCommand = useCallback((command) => {
|
| 139 |
+
if (socketRef.current && isConnected) {
|
| 140 |
+
socketRef.current.send(JSON.stringify(command));
|
| 141 |
+
}
|
| 142 |
+
}, [isConnected]);
|
| 143 |
+
|
| 144 |
+
return { events, gameState, isConnected, error, sendCommand };
|
| 145 |
+
};
|
| 146 |
+
|
| 147 |
+
export default useWebSocket;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/index.css
CHANGED
|
@@ -53,22 +53,6 @@
|
|
| 53 |
animation: blink 1s step-end infinite;
|
| 54 |
}
|
| 55 |
|
| 56 |
-
@utility custom-scrollbar {
|
| 57 |
-
&::-webkit-scrollbar {
|
| 58 |
-
width: 6px;
|
| 59 |
-
}
|
| 60 |
-
&::-webkit-scrollbar-track {
|
| 61 |
-
background: transparent;
|
| 62 |
-
}
|
| 63 |
-
&::-webkit-scrollbar-thumb {
|
| 64 |
-
background: var(--color-outline-variant);
|
| 65 |
-
border-radius: 10px;
|
| 66 |
-
}
|
| 67 |
-
&::-webkit-scrollbar-thumb:hover {
|
| 68 |
-
background: var(--color-outline);
|
| 69 |
-
}
|
| 70 |
-
}
|
| 71 |
-
|
| 72 |
@layer base {
|
| 73 |
body {
|
| 74 |
@apply bg-background text-on-surface font-body selection:bg-primary-container/30 overflow-x-hidden;
|
|
|
|
| 53 |
animation: blink 1s step-end infinite;
|
| 54 |
}
|
| 55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
@layer base {
|
| 57 |
body {
|
| 58 |
@apply bg-background text-on-surface font-body selection:bg-primary-container/30 overflow-x-hidden;
|
frontend/src/views/DashboardView.jsx
CHANGED
|
@@ -1,345 +1,288 @@
|
|
| 1 |
-
import React, { useState, useEffect } from 'react';
|
| 2 |
-
import { config } from '../config';
|
| 3 |
-
import { useApp } from '../context/AppContext';
|
| 4 |
-
import AgentTerminal from '../components/AgentTerminal';
|
| 5 |
-
import DynamicScenarioInjector from '../components/DynamicScenarioInjector';
|
| 6 |
-
import EpisodeEndOverlay from '../components/EpisodeEndOverlay';
|
| 7 |
-
|
| 8 |
-
const LiveTimer = () => {
|
| 9 |
-
const { simulationSeconds } = useApp();
|
| 10 |
-
|
| 11 |
-
const format = (secs) => {
|
| 12 |
-
const m = Math.floor(secs / 60).toString().padStart(2, '0');
|
| 13 |
-
const s = (secs % 60).toString().padStart(2, '0');
|
| 14 |
-
return `${m}:${s}`;
|
| 15 |
-
};
|
| 16 |
-
|
| 17 |
-
return <span>{format(simulationSeconds)}</span>;
|
| 18 |
-
};
|
| 19 |
-
|
| 20 |
-
const SystemTelemetryWidget = ({ status }) => {
|
| 21 |
-
// We will keep an array of 50 data points for 2 lines (CPU and RAM)
|
| 22 |
-
const maxPoints = 50;
|
| 23 |
-
const [dataPoints, setDataPoints] = useState(Array(maxPoints).fill({ cpu: 0, ram: 0, gpu: 0, vram: 0 }));
|
| 24 |
-
|
| 25 |
-
useEffect(() => {
|
| 26 |
-
let isActive = true;
|
| 27 |
-
|
| 28 |
-
const fetchTelemetry = async () => {
|
| 29 |
-
try {
|
| 30 |
-
const res = await fetch(`${config.API_BASE}/telemetry`);
|
| 31 |
-
if (!res.ok) return;
|
| 32 |
-
const data = await res.json();
|
| 33 |
-
|
| 34 |
-
if (isActive) {
|
| 35 |
-
setDataPoints(prev => {
|
| 36 |
-
const next = [...prev.slice(1)];
|
| 37 |
-
next.push({
|
| 38 |
-
cpu: data.cpu || 0,
|
| 39 |
-
ram: data.ram || 0,
|
| 40 |
-
gpu: data.gpu || 0,
|
| 41 |
-
vram: data.vram || 0
|
| 42 |
-
});
|
| 43 |
-
return next;
|
| 44 |
-
});
|
| 45 |
-
}
|
| 46 |
-
} catch (e) {
|
| 47 |
-
// Ignore errors gracefully
|
| 48 |
-
}
|
| 49 |
-
};
|
| 50 |
-
|
| 51 |
-
const interval = setInterval(fetchTelemetry, 1000);
|
| 52 |
-
|
| 53 |
-
return () => {
|
| 54 |
-
isActive = false;
|
| 55 |
-
clearInterval(interval);
|
| 56 |
-
};
|
| 57 |
-
}, []);
|
| 58 |
-
|
| 59 |
-
const latest = dataPoints[dataPoints.length - 1] || { cpu: 0, ram: 0, gpu: 0, vram: 0 };
|
| 60 |
-
|
| 61 |
-
// SVG coordinates computation
|
| 62 |
-
const toPoints = (key) => dataPoints.map((dp, i) => `${(i / (maxPoints - 1)) * 100},${100 - dp[key]}`).join(' ');
|
| 63 |
-
|
| 64 |
-
return (
|
| 65 |
-
<section className="bg-surface-container-low/40 backdrop-blur-md rounded-lg p-5 border border-white/5 refractive-edge flex flex-col">
|
| 66 |
-
<div className="flex items-center justify-between mb-4 shrink-0">
|
| 67 |
-
<div className="flex items-center gap-2">
|
| 68 |
-
<span className="material-symbols-outlined text-outline text-sm">memory</span>
|
| 69 |
-
<h3 className="text-xs font-bold font-headline tracking-widest uppercase text-outline">System Telemetry</h3>
|
| 70 |
-
</div>
|
| 71 |
-
<div className="flex flex-wrap gap-x-3 gap-y-1 text-[9px] font-mono tracking-widest uppercase">
|
| 72 |
-
<span className="flex items-center gap-1 text-[#3b82f6]"><span className="w-2 h-2 rounded-full bg-[#3b82f6]"></span> CPU {Number(latest.cpu).toFixed(0)}%</span>
|
| 73 |
-
<span className="flex items-center gap-1 text-[#10b981]"><span className="w-2 h-2 rounded-full bg-[#10b981]"></span> RAM {Number(latest.ram).toFixed(0)}%</span>
|
| 74 |
-
<span className="flex items-center gap-1 text-[#a855f7]"><span className="w-2 h-2 rounded-full bg-[#a855f7]"></span> GPU {Number(latest.gpu).toFixed(0)}%</span>
|
| 75 |
-
<span className="flex items-center gap-1 text-[#f59e0b]"><span className="w-2 h-2 rounded-full bg-[#f59e0b]"></span> VRAM {Number(latest.vram).toFixed(0)}%</span>
|
| 76 |
-
</div>
|
| 77 |
-
</div>
|
| 78 |
-
|
| 79 |
-
<div className="flex-1 min-h-[100px] border-b border-l border-white/10 relative">
|
| 80 |
-
{/* Y-axis grid lines */}
|
| 81 |
-
<div className="absolute inset-0 flex flex-col justify-between pointer-events-none opacity-20">
|
| 82 |
-
<div className="border-t border-dashed border-white/40 h-0"></div>
|
| 83 |
-
<div className="border-t border-dashed border-white/40 h-0"></div>
|
| 84 |
-
<div className="border-t border-dashed border-white/40 h-0"></div>
|
| 85 |
-
</div>
|
| 86 |
-
|
| 87 |
-
<svg viewBox="0 0 100 100" preserveAspectRatio="none" className="absolute inset-0 w-full h-full overflow-visible">
|
| 88 |
-
<defs>
|
| 89 |
-
<linearGradient id="gradCPU" x1="0" x2="0" y1="0" y2="1">
|
| 90 |
-
<stop offset="0%" stopColor="#3b82f6" stopOpacity="0.15" />
|
| 91 |
-
<stop offset="100%" stopColor="#3b82f6" stopOpacity="0" />
|
| 92 |
-
</linearGradient>
|
| 93 |
-
<linearGradient id="gradRAM" x1="0" x2="0" y1="0" y2="1">
|
| 94 |
-
<stop offset="0%" stopColor="#10b981" stopOpacity="0.15" />
|
| 95 |
-
<stop offset="100%" stopColor="#10b981" stopOpacity="0" />
|
| 96 |
-
</linearGradient>
|
| 97 |
-
<linearGradient id="gradGPU" x1="0" x2="0" y1="0" y2="1">
|
| 98 |
-
<stop offset="0%" stopColor="#a855f7" stopOpacity="0.15" />
|
| 99 |
-
<stop offset="100%" stopColor="#a855f7" stopOpacity="0" />
|
| 100 |
-
</linearGradient>
|
| 101 |
-
<linearGradient id="gradVRAM" x1="0" x2="0" y1="0" y2="1">
|
| 102 |
-
<stop offset="0%" stopColor="#f59e0b" stopOpacity="0.1" />
|
| 103 |
-
<stop offset="100%" stopColor="#f59e0b" stopOpacity="0" />
|
| 104 |
-
</linearGradient>
|
| 105 |
-
</defs>
|
| 106 |
-
|
| 107 |
-
{/* Area fills */}
|
| 108 |
-
<polygon points={`0,100 ${toPoints('cpu')} 100,100`} fill="url(#gradCPU)" />
|
| 109 |
-
<polygon points={`0,100 ${toPoints('ram')} 100,100`} fill="url(#gradRAM)" />
|
| 110 |
-
<polygon points={`0,100 ${toPoints('gpu')} 100,100`} fill="url(#gradGPU)" />
|
| 111 |
-
<polygon points={`0,100 ${toPoints('vram')} 100,100`} fill="url(#gradVRAM)" />
|
| 112 |
-
|
| 113 |
-
{/* Line strokes */}
|
| 114 |
-
<polyline points={toPoints('cpu')} fill="none" stroke="#3b82f6" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
|
| 115 |
-
<polyline points={toPoints('ram')} fill="none" stroke="#10b981" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
|
| 116 |
-
<polyline points={toPoints('gpu')} fill="none" stroke="#a855f7" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
|
| 117 |
-
<polyline points={toPoints('vram')} fill="none" stroke="#f59e0b" strokeWidth="0.75" strokeDasharray="2,2" strokeLinecap="round" strokeLinejoin="round" />
|
| 118 |
-
</svg>
|
| 119 |
-
</div>
|
| 120 |
-
</section>
|
| 121 |
-
);
|
| 122 |
-
};
|
| 123 |
-
|
| 124 |
-
const DashboardView = () => {
|
| 125 |
-
const { sessionData, isConnected } = useApp();
|
| 126 |
-
// Since AppContext maps the useWebSocket return `gameState` or `data` to `sessionData`
|
| 127 |
-
// And I rewritten useWebSocket to return { events, gameState, isConnected }, AppContext
|
| 128 |
-
// might still just say sessionData: data. Wait! AppContext maps `data` literally.
|
| 129 |
-
// useWebSocket exports { gameState }. If AppContext just takes data, it will be undefined!
|
| 130 |
-
// I need to read gameState directly from useApp(). Wait, I'll export gameState from AppContext later or just assume `sessionData` mapped to `data`.
|
| 131 |
-
// Actually in useWebSocket I returned { events, gameState, isConnected, sendCommand } instead of data!
|
| 132 |
-
// So AppContext should map `sessionData` to `gameState`. But wait, AppContext has `data`.
|
| 133 |
-
// Let me check my useWebSocket.js... I returned { events, gameState, isConnected, error, sendCommand }
|
| 134 |
-
// If I didn't change AppContext, then `const { data } = useWebSocket(...)` means `data` is undefined!
|
| 135 |
-
// Let's fix AppContext inside this file write temporarily? No I can't.
|
| 136 |
-
// I should destructure both, but `data` will be undefined.
|
| 137 |
-
// I will fix AppContext in a moment, but let's assume `sessionData` has the gameState object.
|
| 138 |
-
|
| 139 |
-
const state = sessionData || {
|
| 140 |
-
scenario: null,
|
| 141 |
-
active: false,
|
| 142 |
-
step: 0,
|
| 143 |
-
cumulativeReward: 0,
|
| 144 |
-
agents: {
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
const
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
<
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
<div
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
</p>
|
| 200 |
-
</div>
|
| 201 |
-
<div className="h-10 w-px bg-outline-variant/20"></div>
|
| 202 |
-
<div className="text-right">
|
| 203 |
-
<p className="text-[10px] font-mono text-slate-500 uppercase tracking-widest">
|
| 204 |
-
<p className="font-mono text-lg text-
|
| 205 |
-
</div>
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
<
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
{/* Incident Brief Mini */}
|
| 290 |
-
<section className="bg-surface-container-low/40 backdrop-blur-md rounded-lg p-5 border border-white/5 refractive-edge">
|
| 291 |
-
<div className="flex items-center gap-2 mb-6">
|
| 292 |
-
<span className="material-symbols-outlined text-outline text-sm">info</span>
|
| 293 |
-
<h3 className="text-xs font-bold font-headline tracking-widest uppercase text-outline">Incident Brief</h3>
|
| 294 |
-
</div>
|
| 295 |
-
<div className="space-y-6">
|
| 296 |
-
<div className="space-y-1">
|
| 297 |
-
<label className="text-[9px] font-mono text-outline-variant uppercase">Scenario Title</label>
|
| 298 |
-
<div className="h-6 w-full border-b border-outline-variant/20 text-sm text-on-surface/80">{sc.id || '—'}</div>
|
| 299 |
-
</div>
|
| 300 |
-
<div className="flex gap-4">
|
| 301 |
-
<div className="flex-1 space-y-1">
|
| 302 |
-
<label className="text-[9px] font-mono text-outline-variant uppercase">Difficulty</label>
|
| 303 |
-
<div className="h-6 w-full border-b border-outline-variant/20 text-sm text-on-surface/80">{sc.difficulty || '—'}</div>
|
| 304 |
-
</div>
|
| 305 |
-
<div className="flex-1 space-y-1">
|
| 306 |
-
<label className="text-[9px] font-mono text-outline-variant uppercase">Domain</label>
|
| 307 |
-
<div className="h-6 w-full border-b border-outline-variant/20 text-sm text-on-surface/80">{sc.domain || 'N/A'}</div>
|
| 308 |
-
</div>
|
| 309 |
-
</div>
|
| 310 |
-
</div>
|
| 311 |
-
</section>
|
| 312 |
-
|
| 313 |
-
{/* Scenario Injector */}
|
| 314 |
-
<div className="lg:col-span-1">
|
| 315 |
-
<DynamicScenarioInjector scenario={sc} />
|
| 316 |
-
</div>
|
| 317 |
-
|
| 318 |
-
{/* Live Task Manager Graph */}
|
| 319 |
-
<SystemTelemetryWidget
|
| 320 |
-
status={state.status || 'STANDBY'}
|
| 321 |
-
/>
|
| 322 |
-
</div>
|
| 323 |
-
|
| 324 |
-
{/* Background glows */}
|
| 325 |
-
<div className="fixed top-0 right-0 w-[500px] h-[500px] bg-primary/5 rounded-full blur-[120px] pointer-events-none -z-10 translate-x-1/2 -translate-y-1/2"></div>
|
| 326 |
-
<div className="fixed bottom-0 left-0 w-[400px] h-[400px] bg-secondary/5 rounded-full blur-[100px] pointer-events-none -z-10 -translate-x-1/2 translate-y-1/2"></div>
|
| 327 |
-
|
| 328 |
-
{/* Episode End Overlay */}
|
| 329 |
-
<EpisodeEndOverlay
|
| 330 |
-
isOpen={state.status === 'COMPLETED' && !isOverlayDismissed}
|
| 331 |
-
onClose={() => setIsOverlayDismissed(true)}
|
| 332 |
-
metrics={{
|
| 333 |
-
score: Number(state.cumulativeReward || 0).toFixed(2),
|
| 334 |
-
runtime: '00:00:00',
|
| 335 |
-
steps: state.step || 0,
|
| 336 |
-
rootCause: 'VERIFIED',
|
| 337 |
-
agentCount: Object.keys(state.agents || {}).length
|
| 338 |
-
}}
|
| 339 |
-
gameState={state}
|
| 340 |
-
/>
|
| 341 |
-
</div>
|
| 342 |
-
);
|
| 343 |
-
};
|
| 344 |
-
|
| 345 |
-
export default DashboardView;
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from 'react';
|
| 2 |
+
import { config } from '../config';
|
| 3 |
+
import { useApp } from '../context/AppContext';
|
| 4 |
+
import AgentTerminal from '../components/AgentTerminal';
|
| 5 |
+
import DynamicScenarioInjector from '../components/DynamicScenarioInjector';
|
| 6 |
+
import EpisodeEndOverlay from '../components/EpisodeEndOverlay';
|
| 7 |
+
|
| 8 |
+
const LiveTimer = () => {
|
| 9 |
+
const { simulationSeconds } = useApp();
|
| 10 |
+
|
| 11 |
+
const format = (secs) => {
|
| 12 |
+
const m = Math.floor(secs / 60).toString().padStart(2, '0');
|
| 13 |
+
const s = (secs % 60).toString().padStart(2, '0');
|
| 14 |
+
return `${m}:${s}`;
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
return <span>{format(simulationSeconds)}</span>;
|
| 18 |
+
};
|
| 19 |
+
|
| 20 |
+
const SystemTelemetryWidget = ({ status }) => {
|
| 21 |
+
// We will keep an array of 50 data points for 2 lines (CPU and RAM)
|
| 22 |
+
const maxPoints = 50;
|
| 23 |
+
const [dataPoints, setDataPoints] = useState(Array(maxPoints).fill({ cpu: 0, ram: 0, gpu: 0, vram: 0 }));
|
| 24 |
+
|
| 25 |
+
useEffect(() => {
|
| 26 |
+
let isActive = true;
|
| 27 |
+
|
| 28 |
+
const fetchTelemetry = async () => {
|
| 29 |
+
try {
|
| 30 |
+
const res = await fetch(`${config.API_BASE}/telemetry`);
|
| 31 |
+
if (!res.ok) return;
|
| 32 |
+
const data = await res.json();
|
| 33 |
+
|
| 34 |
+
if (isActive) {
|
| 35 |
+
setDataPoints(prev => {
|
| 36 |
+
const next = [...prev.slice(1)];
|
| 37 |
+
next.push({
|
| 38 |
+
cpu: data.cpu || 0,
|
| 39 |
+
ram: data.ram || 0,
|
| 40 |
+
gpu: data.gpu || 0,
|
| 41 |
+
vram: data.vram || 0
|
| 42 |
+
});
|
| 43 |
+
return next;
|
| 44 |
+
});
|
| 45 |
+
}
|
| 46 |
+
} catch (e) {
|
| 47 |
+
// Ignore errors gracefully
|
| 48 |
+
}
|
| 49 |
+
};
|
| 50 |
+
|
| 51 |
+
const interval = setInterval(fetchTelemetry, 1000);
|
| 52 |
+
|
| 53 |
+
return () => {
|
| 54 |
+
isActive = false;
|
| 55 |
+
clearInterval(interval);
|
| 56 |
+
};
|
| 57 |
+
}, []);
|
| 58 |
+
|
| 59 |
+
const latest = dataPoints[dataPoints.length - 1] || { cpu: 0, ram: 0, gpu: 0, vram: 0 };
|
| 60 |
+
|
| 61 |
+
// SVG coordinates computation
|
| 62 |
+
const toPoints = (key) => dataPoints.map((dp, i) => `${(i / (maxPoints - 1)) * 100},${100 - dp[key]}`).join(' ');
|
| 63 |
+
|
| 64 |
+
return (
|
| 65 |
+
<section className="bg-surface-container-low/40 backdrop-blur-md rounded-lg p-5 border border-white/5 refractive-edge flex flex-col">
|
| 66 |
+
<div className="flex items-center justify-between mb-4 shrink-0">
|
| 67 |
+
<div className="flex items-center gap-2">
|
| 68 |
+
<span className="material-symbols-outlined text-outline text-sm">memory</span>
|
| 69 |
+
<h3 className="text-xs font-bold font-headline tracking-widest uppercase text-outline">System Telemetry</h3>
|
| 70 |
+
</div>
|
| 71 |
+
<div className="flex flex-wrap gap-x-3 gap-y-1 text-[9px] font-mono tracking-widest uppercase">
|
| 72 |
+
<span className="flex items-center gap-1 text-[#3b82f6]"><span className="w-2 h-2 rounded-full bg-[#3b82f6]"></span> CPU {Number(latest.cpu).toFixed(0)}%</span>
|
| 73 |
+
<span className="flex items-center gap-1 text-[#10b981]"><span className="w-2 h-2 rounded-full bg-[#10b981]"></span> RAM {Number(latest.ram).toFixed(0)}%</span>
|
| 74 |
+
<span className="flex items-center gap-1 text-[#a855f7]"><span className="w-2 h-2 rounded-full bg-[#a855f7]"></span> GPU {Number(latest.gpu).toFixed(0)}%</span>
|
| 75 |
+
<span className="flex items-center gap-1 text-[#f59e0b]"><span className="w-2 h-2 rounded-full bg-[#f59e0b]"></span> VRAM {Number(latest.vram).toFixed(0)}%</span>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
|
| 79 |
+
<div className="flex-1 min-h-[100px] border-b border-l border-white/10 relative">
|
| 80 |
+
{/* Y-axis grid lines */}
|
| 81 |
+
<div className="absolute inset-0 flex flex-col justify-between pointer-events-none opacity-20">
|
| 82 |
+
<div className="border-t border-dashed border-white/40 h-0"></div>
|
| 83 |
+
<div className="border-t border-dashed border-white/40 h-0"></div>
|
| 84 |
+
<div className="border-t border-dashed border-white/40 h-0"></div>
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
<svg viewBox="0 0 100 100" preserveAspectRatio="none" className="absolute inset-0 w-full h-full overflow-visible">
|
| 88 |
+
<defs>
|
| 89 |
+
<linearGradient id="gradCPU" x1="0" x2="0" y1="0" y2="1">
|
| 90 |
+
<stop offset="0%" stopColor="#3b82f6" stopOpacity="0.15" />
|
| 91 |
+
<stop offset="100%" stopColor="#3b82f6" stopOpacity="0" />
|
| 92 |
+
</linearGradient>
|
| 93 |
+
<linearGradient id="gradRAM" x1="0" x2="0" y1="0" y2="1">
|
| 94 |
+
<stop offset="0%" stopColor="#10b981" stopOpacity="0.15" />
|
| 95 |
+
<stop offset="100%" stopColor="#10b981" stopOpacity="0" />
|
| 96 |
+
</linearGradient>
|
| 97 |
+
<linearGradient id="gradGPU" x1="0" x2="0" y1="0" y2="1">
|
| 98 |
+
<stop offset="0%" stopColor="#a855f7" stopOpacity="0.15" />
|
| 99 |
+
<stop offset="100%" stopColor="#a855f7" stopOpacity="0" />
|
| 100 |
+
</linearGradient>
|
| 101 |
+
<linearGradient id="gradVRAM" x1="0" x2="0" y1="0" y2="1">
|
| 102 |
+
<stop offset="0%" stopColor="#f59e0b" stopOpacity="0.1" />
|
| 103 |
+
<stop offset="100%" stopColor="#f59e0b" stopOpacity="0" />
|
| 104 |
+
</linearGradient>
|
| 105 |
+
</defs>
|
| 106 |
+
|
| 107 |
+
{/* Area fills */}
|
| 108 |
+
<polygon points={`0,100 ${toPoints('cpu')} 100,100`} fill="url(#gradCPU)" />
|
| 109 |
+
<polygon points={`0,100 ${toPoints('ram')} 100,100`} fill="url(#gradRAM)" />
|
| 110 |
+
<polygon points={`0,100 ${toPoints('gpu')} 100,100`} fill="url(#gradGPU)" />
|
| 111 |
+
<polygon points={`0,100 ${toPoints('vram')} 100,100`} fill="url(#gradVRAM)" />
|
| 112 |
+
|
| 113 |
+
{/* Line strokes */}
|
| 114 |
+
<polyline points={toPoints('cpu')} fill="none" stroke="#3b82f6" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
|
| 115 |
+
<polyline points={toPoints('ram')} fill="none" stroke="#10b981" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
|
| 116 |
+
<polyline points={toPoints('gpu')} fill="none" stroke="#a855f7" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
|
| 117 |
+
<polyline points={toPoints('vram')} fill="none" stroke="#f59e0b" strokeWidth="0.75" strokeDasharray="2,2" strokeLinecap="round" strokeLinejoin="round" />
|
| 118 |
+
</svg>
|
| 119 |
+
</div>
|
| 120 |
+
</section>
|
| 121 |
+
);
|
| 122 |
+
};
|
| 123 |
+
|
| 124 |
+
const DashboardView = () => {
|
| 125 |
+
const { sessionData, isConnected } = useApp();
|
| 126 |
+
// Since AppContext maps the useWebSocket return `gameState` or `data` to `sessionData`
|
| 127 |
+
// And I rewritten useWebSocket to return { events, gameState, isConnected }, AppContext
|
| 128 |
+
// might still just say sessionData: data. Wait! AppContext maps `data` literally.
|
| 129 |
+
// useWebSocket exports { gameState }. If AppContext just takes data, it will be undefined!
|
| 130 |
+
// I need to read gameState directly from useApp(). Wait, I'll export gameState from AppContext later or just assume `sessionData` mapped to `data`.
|
| 131 |
+
// Actually in useWebSocket I returned { events, gameState, isConnected, sendCommand } instead of data!
|
| 132 |
+
// So AppContext should map `sessionData` to `gameState`. But wait, AppContext has `data`.
|
| 133 |
+
// Let me check my useWebSocket.js... I returned { events, gameState, isConnected, error, sendCommand }
|
| 134 |
+
// If I didn't change AppContext, then `const { data } = useWebSocket(...)` means `data` is undefined!
|
| 135 |
+
// Let's fix AppContext inside this file write temporarily? No I can't.
|
| 136 |
+
// I should destructure both, but `data` will be undefined.
|
| 137 |
+
// I will fix AppContext in a moment, but let's assume `sessionData` has the gameState object.
|
| 138 |
+
|
| 139 |
+
const state = sessionData || {
|
| 140 |
+
scenario: null,
|
| 141 |
+
active: false,
|
| 142 |
+
step: 0,
|
| 143 |
+
cumulativeReward: 0,
|
| 144 |
+
agents: {
|
| 145 |
+
agent_a: { status: 'STANDBY', messages: [] },
|
| 146 |
+
agent_b: { status: 'STANDBY', messages: [] }
|
| 147 |
+
}
|
| 148 |
+
};
|
| 149 |
+
|
| 150 |
+
const sc = state.scenario || {};
|
| 151 |
+
|
| 152 |
+
const [isOverlayDismissed, setIsOverlayDismissed] = useState(false);
|
| 153 |
+
const [configModels, setConfigModels] = useState({ agent_a: 'Loading...', agent_b: 'Loading...' });
|
| 154 |
+
|
| 155 |
+
useEffect(() => {
|
| 156 |
+
const fetchConfig = async () => {
|
| 157 |
+
try {
|
| 158 |
+
const res = await fetch(`${config.API_BASE}/config`);
|
| 159 |
+
const data = await res.json();
|
| 160 |
+
setConfigModels({
|
| 161 |
+
agent_a: data.models.agent_a || 'Unconfigured',
|
| 162 |
+
agent_b: data.models.agent_b || 'Unconfigured',
|
| 163 |
+
agent_a_role: data.models.agent_a_role,
|
| 164 |
+
agent_b_role: data.models.agent_b_role
|
| 165 |
+
});
|
| 166 |
+
} catch (e) {
|
| 167 |
+
console.error("Failed to fetch config models for dashboard", e);
|
| 168 |
+
}
|
| 169 |
+
};
|
| 170 |
+
fetchConfig();
|
| 171 |
+
}, []);
|
| 172 |
+
|
| 173 |
+
useEffect(() => {
|
| 174 |
+
if (state.status !== 'COMPLETED') {
|
| 175 |
+
setIsOverlayDismissed(false);
|
| 176 |
+
}
|
| 177 |
+
}, [state.status]);
|
| 178 |
+
|
| 179 |
+
return (
|
| 180 |
+
<div className="space-y-8 animate-in fade-in duration-500">
|
| 181 |
+
{/* Header */}
|
| 182 |
+
<div className="flex flex-col md:flex-row justify-between items-end gap-6 border-b border-outline-variant/10 pb-6">
|
| 183 |
+
<div>
|
| 184 |
+
<h1 className="text-4xl font-headline font-bold tracking-tight text-on-surface uppercase">Operational_Dashboard</h1>
|
| 185 |
+
<p className={`font-mono text-sm mt-2 opacity-80 ${isConnected ? 'text-primary' : 'text-error'}`}>
|
| 186 |
+
{isConnected ? `CONNECTED_TO: ${config.WS_URL}` : 'DISCONNECTED: Backend server offline or starting...'}
|
| 187 |
+
</p>
|
| 188 |
+
</div>
|
| 189 |
+
<div className="flex gap-6">
|
| 190 |
+
<div className="text-right">
|
| 191 |
+
<p className="text-[10px] font-mono text-slate-500 uppercase tracking-widest">Runtime</p>
|
| 192 |
+
<p className="font-mono text-lg text-white">
|
| 193 |
+
<LiveTimer />
|
| 194 |
+
</p>
|
| 195 |
+
</div>
|
| 196 |
+
<div className="h-10 w-px bg-outline-variant/20"></div>
|
| 197 |
+
<div className="text-right">
|
| 198 |
+
<p className="text-[10px] font-mono text-slate-500 uppercase tracking-widest">Episode Step</p>
|
| 199 |
+
<p className="font-mono text-lg text-white">{String(state.step).padStart(2, '0')}</p>
|
| 200 |
+
</div>
|
| 201 |
+
<div className="h-10 w-px bg-outline-variant/20"></div>
|
| 202 |
+
<div className="text-right">
|
| 203 |
+
<p className="text-[10px] font-mono text-slate-500 uppercase tracking-widest">Current Reward</p>
|
| 204 |
+
<p className="font-mono text-lg text-tertiary">{Number(state.cumulativeReward).toFixed(2)}</p>
|
| 205 |
+
</div>
|
| 206 |
+
</div>
|
| 207 |
+
</div>
|
| 208 |
+
|
| 209 |
+
{/* Twin Terminals */}
|
| 210 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
| 211 |
+
<AgentTerminal
|
| 212 |
+
agentName={`Agent A: ${configModels.agent_a_role ? configModels.agent_a_role.replace(/_/g, ' ') : 'Investigator'}`}
|
| 213 |
+
model={state.agent_a_model || configModels.agent_a}
|
| 214 |
+
status={state.agents.agent_a.status}
|
| 215 |
+
accentColor="cyan"
|
| 216 |
+
icon="search"
|
| 217 |
+
messages={state.agents.agent_a.messages}
|
| 218 |
+
/>
|
| 219 |
+
<AgentTerminal
|
| 220 |
+
agentName={`Agent B: ${configModels.agent_b_role ? configModels.agent_b_role.replace(/_/g, ' ') : 'Validator'}`}
|
| 221 |
+
model={state.agent_b_model || configModels.agent_b}
|
| 222 |
+
status={state.agents.agent_b.status}
|
| 223 |
+
accentColor="purple"
|
| 224 |
+
icon="verified_user"
|
| 225 |
+
messages={state.agents.agent_b.messages}
|
| 226 |
+
/>
|
| 227 |
+
</div>
|
| 228 |
+
|
| 229 |
+
{/* Bottom Row */}
|
| 230 |
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
| 231 |
+
{/* Incident Brief Mini */}
|
| 232 |
+
<section className="bg-surface-container-low/40 backdrop-blur-md rounded-lg p-5 border border-white/5 refractive-edge">
|
| 233 |
+
<div className="flex items-center gap-2 mb-6">
|
| 234 |
+
<span className="material-symbols-outlined text-outline text-sm">info</span>
|
| 235 |
+
<h3 className="text-xs font-bold font-headline tracking-widest uppercase text-outline">Incident Brief</h3>
|
| 236 |
+
</div>
|
| 237 |
+
<div className="space-y-6">
|
| 238 |
+
<div className="space-y-1">
|
| 239 |
+
<label className="text-[9px] font-mono text-outline-variant uppercase">Scenario Title</label>
|
| 240 |
+
<div className="h-6 w-full border-b border-outline-variant/20 text-sm text-on-surface/80">{sc.id || '—'}</div>
|
| 241 |
+
</div>
|
| 242 |
+
<div className="flex gap-4">
|
| 243 |
+
<div className="flex-1 space-y-1">
|
| 244 |
+
<label className="text-[9px] font-mono text-outline-variant uppercase">Difficulty</label>
|
| 245 |
+
<div className="h-6 w-full border-b border-outline-variant/20 text-sm text-on-surface/80">{sc.difficulty || '—'}</div>
|
| 246 |
+
</div>
|
| 247 |
+
<div className="flex-1 space-y-1">
|
| 248 |
+
<label className="text-[9px] font-mono text-outline-variant uppercase">Domain</label>
|
| 249 |
+
<div className="h-6 w-full border-b border-outline-variant/20 text-sm text-on-surface/80">{sc.domain || 'N/A'}</div>
|
| 250 |
+
</div>
|
| 251 |
+
</div>
|
| 252 |
+
</div>
|
| 253 |
+
</section>
|
| 254 |
+
|
| 255 |
+
{/* Scenario Injector */}
|
| 256 |
+
<div className="lg:col-span-1">
|
| 257 |
+
<DynamicScenarioInjector scenario={sc} />
|
| 258 |
+
</div>
|
| 259 |
+
|
| 260 |
+
{/* Live Task Manager Graph */}
|
| 261 |
+
<SystemTelemetryWidget
|
| 262 |
+
status={state.status || 'STANDBY'}
|
| 263 |
+
/>
|
| 264 |
+
</div>
|
| 265 |
+
|
| 266 |
+
{/* Background glows */}
|
| 267 |
+
<div className="fixed top-0 right-0 w-[500px] h-[500px] bg-primary/5 rounded-full blur-[120px] pointer-events-none -z-10 translate-x-1/2 -translate-y-1/2"></div>
|
| 268 |
+
<div className="fixed bottom-0 left-0 w-[400px] h-[400px] bg-secondary/5 rounded-full blur-[100px] pointer-events-none -z-10 -translate-x-1/2 translate-y-1/2"></div>
|
| 269 |
+
|
| 270 |
+
{/* Episode End Overlay */}
|
| 271 |
+
<EpisodeEndOverlay
|
| 272 |
+
isOpen={state.status === 'COMPLETED' && !isOverlayDismissed}
|
| 273 |
+
onClose={() => setIsOverlayDismissed(true)}
|
| 274 |
+
metrics={{
|
| 275 |
+
score: Number(state.cumulativeReward || 0).toFixed(2),
|
| 276 |
+
runtime: '00:00:00', // could calculate if we tracked start/end time
|
| 277 |
+
steps: state.step || 0,
|
| 278 |
+
rootCause: 'VERIFIED',
|
| 279 |
+
agentA: { accuracy: 'High', latency: '42ms', iops: '9' },
|
| 280 |
+
agentB: { accuracy: 'High', latency: '38ms', iops: '7' }
|
| 281 |
+
}}
|
| 282 |
+
gameState={state}
|
| 283 |
+
/>
|
| 284 |
+
</div>
|
| 285 |
+
);
|
| 286 |
+
};
|
| 287 |
+
|
| 288 |
+
export default DashboardView;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/views/SettingsView.jsx
CHANGED
|
@@ -31,7 +31,8 @@ const OllamaModelPicker = ({ value, onChange, accentColor }) => {
|
|
| 31 |
onChange(models[0].name);
|
| 32 |
}
|
| 33 |
}
|
| 34 |
-
|
|
|
|
| 35 |
|
| 36 |
const borderClass = accentColor === 'primary' ? 'border-primary/30 focus:border-primary' : 'border-secondary/30 focus:border-secondary';
|
| 37 |
const textClass = accentColor === 'primary' ? 'text-primary' : 'text-secondary';
|
|
@@ -74,127 +75,44 @@ const OllamaModelPicker = ({ value, onChange, accentColor }) => {
|
|
| 74 |
);
|
| 75 |
};
|
| 76 |
|
| 77 |
-
const HFModelPicker = ({ value, onChange, accentColor }) => {
|
| 78 |
-
const [models, setModels] = useState([]);
|
| 79 |
-
const [loading, setLoading] = useState(true);
|
| 80 |
-
const [expanded, setExpanded] = useState(false);
|
| 81 |
-
|
| 82 |
-
useEffect(() => {
|
| 83 |
-
const fetchModels = async () => {
|
| 84 |
-
try {
|
| 85 |
-
const res = await fetch(`${config.API_BASE}/models/hf`);
|
| 86 |
-
if (res.ok) {
|
| 87 |
-
const data = await res.json();
|
| 88 |
-
setModels(data.models || []);
|
| 89 |
-
} else {
|
| 90 |
-
setModels([]);
|
| 91 |
-
}
|
| 92 |
-
} catch (e) {
|
| 93 |
-
setModels([]);
|
| 94 |
-
} finally {
|
| 95 |
-
setLoading(false);
|
| 96 |
-
}
|
| 97 |
-
};
|
| 98 |
-
fetchModels();
|
| 99 |
-
}, []);
|
| 100 |
-
|
| 101 |
-
const borderClass = accentColor === 'primary' ? 'border-primary/30 focus:border-primary' : 'border-secondary/30 focus:border-secondary';
|
| 102 |
-
const textClass = accentColor === 'primary' ? 'text-primary' : 'text-secondary';
|
| 103 |
-
|
| 104 |
-
const groupedModels = models.reduce((acc, model) => {
|
| 105 |
-
const org = model.split('/')[0];
|
| 106 |
-
if (!acc[org]) acc[org] = [];
|
| 107 |
-
acc[org].push(model);
|
| 108 |
-
return acc;
|
| 109 |
-
}, {});
|
| 110 |
-
|
| 111 |
-
return (
|
| 112 |
-
<div className="space-y-2">
|
| 113 |
-
<div className="flex justify-between items-center">
|
| 114 |
-
<label className="font-mono text-[10px] tracking-widest text-slate-400 uppercase">HuggingFace Models</label>
|
| 115 |
-
<button
|
| 116 |
-
onClick={() => setExpanded(!expanded)}
|
| 117 |
-
className={`text-[9px] font-mono ${textClass} hover:opacity-70 transition-opacity`}
|
| 118 |
-
>
|
| 119 |
-
{expanded ? '▲ Collapse' : '▼ Expand'}
|
| 120 |
-
</button>
|
| 121 |
-
</div>
|
| 122 |
-
{loading ? (
|
| 123 |
-
<div className="flex items-center gap-2 py-2">
|
| 124 |
-
<span className="material-symbols-outlined text-sm animate-spin">sync</span>
|
| 125 |
-
<span className="text-[10px] font-mono text-slate-500">Loading models...</span>
|
| 126 |
-
</div>
|
| 127 |
-
) : expanded ? (
|
| 128 |
-
<div className="max-h-48 overflow-y-auto bg-surface-container-lowest rounded border border-white/5">
|
| 129 |
-
{Object.entries(groupedModels).map(([org, orgModels]) => (
|
| 130 |
-
<div key={org}>
|
| 131 |
-
<div className="px-3 py-1 bg-surface-container-highest text-[9px] font-mono text-slate-400 uppercase sticky top-0">
|
| 132 |
-
{org}
|
| 133 |
-
</div>
|
| 134 |
-
{orgModels.map(model => (
|
| 135 |
-
<button
|
| 136 |
-
key={model}
|
| 137 |
-
onClick={() => onChange(model)}
|
| 138 |
-
className={`w-full text-left px-3 py-1.5 text-[10px] font-mono transition-all hover:bg-surface-container-high ${
|
| 139 |
-
value === model ? `${textClass} bg-primary/10` : 'text-on-surface'
|
| 140 |
-
}`}
|
| 141 |
-
>
|
| 142 |
-
{model.split('/')[1]}
|
| 143 |
-
</button>
|
| 144 |
-
))}
|
| 145 |
-
</div>
|
| 146 |
-
))}
|
| 147 |
-
</div>
|
| 148 |
-
) : (
|
| 149 |
-
<select
|
| 150 |
-
value={value}
|
| 151 |
-
onChange={e => onChange(e.target.value)}
|
| 152 |
-
className={`w-full bg-surface-container-lowest border-b ${borderClass} py-2 font-mono text-xs text-on-surface cursor-pointer focus:outline-none transition-all`}
|
| 153 |
-
>
|
| 154 |
-
{models.map(m => (
|
| 155 |
-
<option key={m} value={m}>{m}</option>
|
| 156 |
-
))}
|
| 157 |
-
</select>
|
| 158 |
-
)}
|
| 159 |
-
<p className={`text-[9px] font-mono ${textClass} opacity-50`}>{models.length} models available</p>
|
| 160 |
-
</div>
|
| 161 |
-
);
|
| 162 |
-
};
|
| 163 |
-
|
| 164 |
const ROLES = ["INVESTIGATOR", "VALIDATOR", "FORENSIC_ANALYST", "NETWORK_ENGINEER", "SYSTEM_ADMIN", "SECURITY_ARCHITECT", "COMPLIANCE_OFFICER", "CUSTOM_ROLE"];
|
| 165 |
|
| 166 |
const SettingsView = () => {
|
| 167 |
-
const [
|
|
|
|
| 168 |
const [openaiKey, setOpenaiKey] = useState('');
|
| 169 |
const [maxSteps, setMaxSteps] = useState(12);
|
| 170 |
const [complexity, setComplexity] = useState('LEVEL_02: ADVERSARIAL');
|
| 171 |
const [saved, setSaved] = useState(false);
|
| 172 |
const [executionMode, setExecutionMode] = useState('simulated');
|
| 173 |
const [sshConfig, setSshConfig] = useState({ host: '', port: 22, user: '', password: '' });
|
| 174 |
-
const [sshTestStatus, setSshTestStatus] = useState(null);
|
| 175 |
|
| 176 |
useEffect(() => {
|
| 177 |
const fetchConfig = async () => {
|
| 178 |
try {
|
| 179 |
const res = await fetch(`${config.API_BASE}/config`);
|
| 180 |
const data = await res.json();
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
|
|
|
|
|
|
|
|
|
| 198 |
if (data.models.openai_api_key) setOpenaiKey(data.models.openai_api_key);
|
| 199 |
setMaxSteps(data.episode.max_steps);
|
| 200 |
if (data.execution) {
|
|
@@ -215,20 +133,21 @@ const SettingsView = () => {
|
|
| 215 |
|
| 216 |
const handleSave = async () => {
|
| 217 |
try {
|
| 218 |
-
const agentPayload = agents.map(a => ({
|
| 219 |
-
id: a.id,
|
| 220 |
-
model: a.provider === 'ollama' ? a.model : (a.provider === 'openai' ? a.openaiModel : a.hfModel),
|
| 221 |
-
provider: a.provider,
|
| 222 |
-
role: a.role === 'CUSTOM_ROLE' ? `CUSTOM_${a.customRoleName.replace(/ /g, '_').toUpperCase()}` : a.role,
|
| 223 |
-
system_prompt: a.customPrompt,
|
| 224 |
-
temperature: a.temp
|
| 225 |
-
}));
|
| 226 |
await fetch(`${config.API_BASE}/config`, {
|
| 227 |
method: 'POST',
|
| 228 |
headers: { 'Content-Type': 'application/json' },
|
| 229 |
body: JSON.stringify({
|
| 230 |
MAX_STEPS: maxSteps,
|
| 231 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
EXECUTION_MODE: executionMode,
|
| 233 |
SSH_HOST: sshConfig.host,
|
| 234 |
SSH_PORT: sshConfig.port,
|
|
@@ -244,22 +163,24 @@ const SettingsView = () => {
|
|
| 244 |
}
|
| 245 |
};
|
| 246 |
|
|
|
|
| 247 |
useEffect(() => {
|
| 248 |
-
if (
|
| 249 |
-
const agentPayload = agents.map(a => ({
|
| 250 |
-
id: a.id,
|
| 251 |
-
model: a.provider === 'ollama' ? a.model : (a.provider === 'openai' ? a.openaiModel : a.hfModel),
|
| 252 |
-
provider: a.provider,
|
| 253 |
-
role: a.role === 'CUSTOM_ROLE' ? `CUSTOM_${a.customRoleName.replace(/ /g, '_').toUpperCase()}` : a.role,
|
| 254 |
-
system_prompt: a.customPrompt,
|
| 255 |
-
temperature: a.temp
|
| 256 |
-
}));
|
| 257 |
fetch(`${config.API_BASE}/config`, {
|
| 258 |
method: 'POST',
|
| 259 |
headers: { 'Content-Type': 'application/json' },
|
| 260 |
body: JSON.stringify({
|
| 261 |
MAX_STEPS: maxSteps,
|
| 262 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
EXECUTION_MODE: executionMode,
|
| 264 |
SSH_HOST: sshConfig.host,
|
| 265 |
SSH_PORT: sshConfig.port,
|
|
@@ -268,56 +189,21 @@ const SettingsView = () => {
|
|
| 268 |
OPENAI_API_KEY: openaiKey
|
| 269 |
})
|
| 270 |
}).catch(e => { });
|
| 271 |
-
}, [
|
| 272 |
-
|
| 273 |
-
const handleUpdateAgent = (index, updater) => {
|
| 274 |
-
setAgents(prev => {
|
| 275 |
-
const next = [...prev];
|
| 276 |
-
next[index] = typeof updater === 'function' ? updater(next[index]) : updater;
|
| 277 |
-
return next;
|
| 278 |
-
});
|
| 279 |
-
};
|
| 280 |
|
| 281 |
-
const
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
const ProviderToggle = ({ agent, index }) => {
|
| 296 |
-
const getButtonClass = (p) => {
|
| 297 |
-
if (agent.provider === p) {
|
| 298 |
-
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';
|
| 299 |
-
}
|
| 300 |
-
return 'flex-1 py-1 px-3 rounded text-[10px] font-mono font-bold uppercase transition-all text-outline-variant hover:text-white';
|
| 301 |
-
};
|
| 302 |
-
const getProviderLabel = (p) => {
|
| 303 |
-
if (p === 'ollama') return 'Local Ollama';
|
| 304 |
-
if (p === 'hf') return 'Hugging Face';
|
| 305 |
-
return 'OpenAI';
|
| 306 |
-
};
|
| 307 |
-
return (
|
| 308 |
-
<div className="flex gap-2 p-1 bg-surface-container-highest rounded-lg border border-white/5">
|
| 309 |
-
{['ollama', 'hf', 'openai'].map(p => (
|
| 310 |
-
<button
|
| 311 |
-
key={p}
|
| 312 |
-
onClick={() => handleUpdateAgent(index, a => ({ ...a, provider: p }))}
|
| 313 |
-
className={getButtonClass(p)}
|
| 314 |
-
>
|
| 315 |
-
{getProviderLabel(p)}
|
| 316 |
-
</button>
|
| 317 |
-
))}
|
| 318 |
-
</div>
|
| 319 |
-
);
|
| 320 |
-
};
|
| 321 |
|
| 322 |
return (
|
| 323 |
<div className="space-y-12 animate-in fade-in duration-500">
|
|
@@ -337,134 +223,227 @@ const SettingsView = () => {
|
|
| 337 |
</section>
|
| 338 |
|
| 339 |
<div className="grid grid-cols-1 md:grid-cols-12 gap-6 items-stretch">
|
| 340 |
-
{/*
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
<div className={`w-10 h-10 rounded-lg ${bgColor} flex items-center justify-center border ${borderColor}`}>
|
| 352 |
-
<span className={`material-symbols-outlined ${titleColor}`}>smart_toy</span>
|
| 353 |
</div>
|
| 354 |
-
<
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
</div>
|
| 370 |
</div>
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
/>
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
/>
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
</div>
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
<
|
| 411 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 413 |
<input
|
| 414 |
-
className="w-full
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
|
|
|
| 418 |
/>
|
| 419 |
</div>
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
<div className="space-y-2">
|
| 443 |
-
<label className={`font-mono text-[9px] tracking-widest ${titleColor} uppercase flex justify-between`}>
|
| 444 |
-
<span>System Prompt Configuration</span>
|
| 445 |
-
</label>
|
| 446 |
-
<textarea
|
| 447 |
-
placeholder="You are an elite expert... Your objective is to..."
|
| 448 |
-
value={agent.customPrompt}
|
| 449 |
-
onChange={e => handleUpdateAgent(index, a => ({ ...a, customPrompt: e.target.value }))}
|
| 450 |
-
className={`w-full h-32 bg-surface-container-lowest ${titleColor} font-mono text-[10px] p-3 rounded border border-white/5 focus:outline-none leading-relaxed`}
|
| 451 |
-
/>
|
| 452 |
-
</div>
|
| 453 |
-
</div>
|
| 454 |
-
)}
|
| 455 |
</div>
|
| 456 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 457 |
</div>
|
| 458 |
-
);
|
| 459 |
-
})}
|
| 460 |
-
{(
|
| 461 |
-
<div className="md:col-span-12 flex justify-center mt-4">
|
| 462 |
-
<button onClick={addAgent} className="flex items-center gap-2 px-8 py-3 rounded-xl border border-dashed border-outline-variant/30 text-outline-variant font-mono text-xs uppercase hover:bg-surface-container-highest hover:text-white transition-all">
|
| 463 |
-
<span className="material-symbols-outlined text-[16px]">add</span>
|
| 464 |
-
<span>Add Agent Node</span>
|
| 465 |
-
</button>
|
| 466 |
</div>
|
| 467 |
-
|
| 468 |
|
| 469 |
{/* Execution Environment */}
|
| 470 |
<div className="md:col-span-12 glass-panel rounded-xl p-8 refractive-edge">
|
|
@@ -484,7 +463,6 @@ const SettingsView = () => {
|
|
| 484 |
key={m.id}
|
| 485 |
id={`exec-mode-${m.id}`}
|
| 486 |
onClick={() => setExecutionMode(m.id)}
|
| 487 |
-
title={m.id === 'ssh' ? 'Connects to a live Linux server via SSH to execute raw commands (Destructive)' : 'Uses Sandbox constraints'}
|
| 488 |
className={`flex-1 flex items-center justify-center gap-2 py-2 px-3 rounded text-[11px] font-mono font-bold uppercase transition-all ${executionMode === m.id ? 'bg-tertiary text-black' : 'text-outline-variant hover:text-white'
|
| 489 |
}`}
|
| 490 |
>
|
|
@@ -606,10 +584,7 @@ const SettingsView = () => {
|
|
| 606 |
</div>
|
| 607 |
<div className="flex items-center gap-4">
|
| 608 |
<button
|
| 609 |
-
onClick={() => {
|
| 610 |
-
setAgents([{ id: 'agent_a', provider: 'ollama', model: '', temp: 0.7, role: 'INVESTIGATOR' }]);
|
| 611 |
-
setMaxSteps(12);
|
| 612 |
-
}}
|
| 613 |
className="px-8 py-3 bg-surface-container-high text-on-surface-variant font-headline font-bold text-sm tracking-widest rounded hover:bg-surface-container-highest hover:text-white transition-all uppercase"
|
| 614 |
>
|
| 615 |
Reset
|
|
|
|
| 31 |
onChange(models[0].name);
|
| 32 |
}
|
| 33 |
}
|
| 34 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 35 |
+
}, [models, value]);
|
| 36 |
|
| 37 |
const borderClass = accentColor === 'primary' ? 'border-primary/30 focus:border-primary' : 'border-secondary/30 focus:border-secondary';
|
| 38 |
const textClass = accentColor === 'primary' ? 'text-primary' : 'text-secondary';
|
|
|
|
| 75 |
);
|
| 76 |
};
|
| 77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
const ROLES = ["INVESTIGATOR", "VALIDATOR", "FORENSIC_ANALYST", "NETWORK_ENGINEER", "SYSTEM_ADMIN", "SECURITY_ARCHITECT", "COMPLIANCE_OFFICER", "CUSTOM_ROLE"];
|
| 79 |
|
| 80 |
const SettingsView = () => {
|
| 81 |
+
const [agentA, setAgentA] = useState({ provider: 'ollama', model: '', hfModel: 'microsoft/Phi-3-mini-4k-instruct', openaiModel: 'gpt-4o', temp: 0.8, role: 'INVESTIGATOR', customRoleName: '', customPrompt: '' });
|
| 82 |
+
const [agentB, setAgentB] = useState({ provider: 'ollama', model: '', hfModel: 'Qwen/Qwen2.5-3B-Instruct', openaiModel: 'gpt-4o-mini', temp: 0.6, role: 'VALIDATOR', customRoleName: '', customPrompt: '' });
|
| 83 |
const [openaiKey, setOpenaiKey] = useState('');
|
| 84 |
const [maxSteps, setMaxSteps] = useState(12);
|
| 85 |
const [complexity, setComplexity] = useState('LEVEL_02: ADVERSARIAL');
|
| 86 |
const [saved, setSaved] = useState(false);
|
| 87 |
const [executionMode, setExecutionMode] = useState('simulated');
|
| 88 |
const [sshConfig, setSshConfig] = useState({ host: '', port: 22, user: '', password: '' });
|
| 89 |
+
const [sshTestStatus, setSshTestStatus] = useState(null); // null | 'testing' | 'ok' | 'fail'
|
| 90 |
|
| 91 |
useEffect(() => {
|
| 92 |
const fetchConfig = async () => {
|
| 93 |
try {
|
| 94 |
const res = await fetch(`${config.API_BASE}/config`);
|
| 95 |
const data = await res.json();
|
| 96 |
+
const roleA = data.models.agent_a_role || 'INVESTIGATOR';
|
| 97 |
+
const roleB = data.models.agent_b_role || 'VALIDATOR';
|
| 98 |
+
setAgentA({
|
| 99 |
+
provider: data.models.agent_a_provider || 'ollama',
|
| 100 |
+
model: data.models.agent_a,
|
| 101 |
+
hfModel: 'microsoft/Phi-3-mini-4k-instruct',
|
| 102 |
+
temp: data.models.agent_a_temp,
|
| 103 |
+
role: roleA.startsWith('CUSTOM_') ? 'CUSTOM_ROLE' : roleA,
|
| 104 |
+
customRoleName: roleA.startsWith('CUSTOM_') ? roleA.replace('CUSTOM_', '').replace(/_/g, ' ') : '',
|
| 105 |
+
customPrompt: data.models.agent_a_system_prompt || ''
|
| 106 |
+
});
|
| 107 |
+
setAgentB({
|
| 108 |
+
provider: data.models.agent_b_provider || 'ollama',
|
| 109 |
+
model: data.models.agent_b,
|
| 110 |
+
hfModel: 'Qwen/Qwen2.5-3B-Instruct',
|
| 111 |
+
temp: data.models.agent_b_temp,
|
| 112 |
+
role: roleB.startsWith('CUSTOM_') ? 'CUSTOM_ROLE' : roleB,
|
| 113 |
+
customRoleName: roleB.startsWith('CUSTOM_') ? roleB.replace('CUSTOM_', '').replace(/_/g, ' ') : '',
|
| 114 |
+
customPrompt: data.models.agent_b_system_prompt || ''
|
| 115 |
+
});
|
| 116 |
if (data.models.openai_api_key) setOpenaiKey(data.models.openai_api_key);
|
| 117 |
setMaxSteps(data.episode.max_steps);
|
| 118 |
if (data.execution) {
|
|
|
|
| 133 |
|
| 134 |
const handleSave = async () => {
|
| 135 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
await fetch(`${config.API_BASE}/config`, {
|
| 137 |
method: 'POST',
|
| 138 |
headers: { 'Content-Type': 'application/json' },
|
| 139 |
body: JSON.stringify({
|
| 140 |
MAX_STEPS: maxSteps,
|
| 141 |
+
AGENT_A_MODEL: agentA.provider === 'ollama' ? agentA.model : (agentA.provider === 'openai' ? agentA.openaiModel : agentA.hfModel),
|
| 142 |
+
AGENT_B_MODEL: agentB.provider === 'ollama' ? agentB.model : (agentB.provider === 'openai' ? agentB.openaiModel : agentB.hfModel),
|
| 143 |
+
AGENT_A_PROVIDER: agentA.provider,
|
| 144 |
+
AGENT_B_PROVIDER: agentB.provider,
|
| 145 |
+
AGENT_A_ROLE: agentA.role === 'CUSTOM_ROLE' ? `CUSTOM_${agentA.customRoleName.replace(/ /g, '_').toUpperCase()}` : agentA.role,
|
| 146 |
+
AGENT_B_ROLE: agentB.role === 'CUSTOM_ROLE' ? `CUSTOM_${agentB.customRoleName.replace(/ /g, '_').toUpperCase()}` : agentB.role,
|
| 147 |
+
AGENT_A_SYSTEM_PROMPT: agentA.customPrompt,
|
| 148 |
+
AGENT_B_SYSTEM_PROMPT: agentB.customPrompt,
|
| 149 |
+
AGENT_A_TEMPERATURE: agentA.temp,
|
| 150 |
+
AGENT_B_TEMPERATURE: agentB.temp,
|
| 151 |
EXECUTION_MODE: executionMode,
|
| 152 |
SSH_HOST: sshConfig.host,
|
| 153 |
SSH_PORT: sshConfig.port,
|
|
|
|
| 163 |
}
|
| 164 |
};
|
| 165 |
|
| 166 |
+
// Auto-sync active settings so navigation doesn't wipe them
|
| 167 |
useEffect(() => {
|
| 168 |
+
if (!agentA.model && !agentB.model) return; // Wait for initial load or valid models
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
fetch(`${config.API_BASE}/config`, {
|
| 170 |
method: 'POST',
|
| 171 |
headers: { 'Content-Type': 'application/json' },
|
| 172 |
body: JSON.stringify({
|
| 173 |
MAX_STEPS: maxSteps,
|
| 174 |
+
AGENT_A_MODEL: agentA.provider === 'ollama' ? agentA.model : (agentA.provider === 'openai' ? agentA.openaiModel : agentA.hfModel),
|
| 175 |
+
AGENT_B_MODEL: agentB.provider === 'ollama' ? agentB.model : (agentB.provider === 'openai' ? agentB.openaiModel : agentB.hfModel),
|
| 176 |
+
AGENT_A_PROVIDER: agentA.provider,
|
| 177 |
+
AGENT_B_PROVIDER: agentB.provider,
|
| 178 |
+
AGENT_A_ROLE: agentA.role === 'CUSTOM_ROLE' ? `CUSTOM_${agentA.customRoleName.replace(/ /g, '_').toUpperCase()}` : agentA.role,
|
| 179 |
+
AGENT_B_ROLE: agentB.role === 'CUSTOM_ROLE' ? `CUSTOM_${agentB.customRoleName.replace(/ /g, '_').toUpperCase()}` : agentB.role,
|
| 180 |
+
AGENT_A_SYSTEM_PROMPT: agentA.customPrompt,
|
| 181 |
+
AGENT_B_SYSTEM_PROMPT: agentB.customPrompt,
|
| 182 |
+
AGENT_A_TEMPERATURE: agentA.temp,
|
| 183 |
+
AGENT_B_TEMPERATURE: agentB.temp,
|
| 184 |
EXECUTION_MODE: executionMode,
|
| 185 |
SSH_HOST: sshConfig.host,
|
| 186 |
SSH_PORT: sshConfig.port,
|
|
|
|
| 189 |
OPENAI_API_KEY: openaiKey
|
| 190 |
})
|
| 191 |
}).catch(e => { });
|
| 192 |
+
}, [agentA, agentB, maxSteps, executionMode, sshConfig, openaiKey]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
|
| 194 |
+
const ProviderToggle = ({ agent, agentId, onSetAgent }) => (
|
| 195 |
+
<div className="flex gap-2 p-1 bg-surface-container-highest rounded-lg border border-white/5">
|
| 196 |
+
{['ollama', 'hf', 'openai'].map(p => (
|
| 197 |
+
<button
|
| 198 |
+
key={p}
|
| 199 |
+
onClick={() => onSetAgent(a => ({ ...a, provider: p }))}
|
| 200 |
+
className={`flex-1 py-1 px-3 rounded text-[10px] font-mono font-bold uppercase transition-all ${agent.provider === p ? (agentId === 'A' ? 'bg-primary text-black' : 'bg-secondary text-black') : 'text-outline-variant hover:text-white'}`}
|
| 201 |
+
>
|
| 202 |
+
{p === 'ollama' ? 'Local Ollama' : (p === 'hf' ? 'Hugging Face' : 'OpenAI')}
|
| 203 |
+
</button>
|
| 204 |
+
))}
|
| 205 |
+
</div>
|
| 206 |
+
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
|
| 208 |
return (
|
| 209 |
<div className="space-y-12 animate-in fade-in duration-500">
|
|
|
|
| 223 |
</section>
|
| 224 |
|
| 225 |
<div className="grid grid-cols-1 md:grid-cols-12 gap-6 items-stretch">
|
| 226 |
+
{/* Agent A Config */}
|
| 227 |
+
<div className="md:col-span-6 glass-panel rounded-xl p-8 relative overflow-hidden group refractive-edge h-full">
|
| 228 |
+
<div className="flex items-center gap-4 mb-8">
|
| 229 |
+
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center border border-primary/20">
|
| 230 |
+
<span className="material-symbols-outlined text-primary">smart_toy</span>
|
| 231 |
+
</div>
|
| 232 |
+
<div className="flex-1">
|
| 233 |
+
<div className="flex justify-between items-start">
|
| 234 |
+
<div>
|
| 235 |
+
<h3 className="font-headline text-xl font-bold uppercase">Agent A <span className="text-primary text-sm ml-2 tracking-tighter">[PRIMARY]</span></h3>
|
| 236 |
+
<p className="font-mono text-[10px] text-slate-500 uppercase">Neural Processing Unit 01</p>
|
|
|
|
|
|
|
| 237 |
</div>
|
| 238 |
+
<ProviderToggle agent={agentA} agentId="A" onSetAgent={setAgentA} />
|
| 239 |
+
</div>
|
| 240 |
+
</div>
|
| 241 |
+
</div>
|
| 242 |
+
<div className="space-y-8">
|
| 243 |
+
{agentA.provider === 'ollama' ? (
|
| 244 |
+
<OllamaModelPicker
|
| 245 |
+
value={agentA.model}
|
| 246 |
+
onChange={v => setAgentA(a => ({ ...a, model: v }))}
|
| 247 |
+
accentColor="primary"
|
| 248 |
+
/>
|
| 249 |
+
) : agentA.provider === 'hf' ? (
|
| 250 |
+
<div className="space-y-4">
|
| 251 |
+
<div className="space-y-2">
|
| 252 |
+
<label className="font-mono text-[10px] tracking-widest text-slate-400 uppercase">HF Model Repo ID</label>
|
| 253 |
+
<input
|
| 254 |
+
className="w-full bg-transparent border-0 border-b border-primary/30 py-2 font-mono text-on-surface focus:outline-none focus:border-primary transition-all placeholder:text-slate-700"
|
| 255 |
+
placeholder="e.g. microsoft/Phi-3-mini-4k-instruct"
|
| 256 |
+
type="text"
|
| 257 |
+
value={agentA.hfModel}
|
| 258 |
+
onChange={e => setAgentA(a => ({ ...a, hfModel: e.target.value }))}
|
| 259 |
+
/>
|
| 260 |
</div>
|
| 261 |
</div>
|
| 262 |
+
) : (
|
| 263 |
+
<div className="space-y-4">
|
| 264 |
+
<div className="space-y-2">
|
| 265 |
+
<label className="font-mono text-[10px] tracking-widest text-slate-400 uppercase">OpenAI API Key</label>
|
| 266 |
+
<input
|
| 267 |
+
className="w-full bg-transparent border-0 border-b border-primary/30 py-2 font-mono text-on-surface focus:outline-none focus:border-primary transition-all placeholder:text-slate-700"
|
| 268 |
+
placeholder="sk-..."
|
| 269 |
+
type="password"
|
| 270 |
+
value={openaiKey}
|
| 271 |
+
onChange={e => setOpenaiKey(e.target.value)}
|
| 272 |
/>
|
| 273 |
+
</div>
|
| 274 |
+
<div className="space-y-2">
|
| 275 |
+
<label className="font-mono text-[10px] tracking-widest text-slate-400 uppercase">OpenAI Model Name</label>
|
| 276 |
+
<input
|
| 277 |
+
className="w-full bg-transparent border-0 border-b border-primary/30 py-2 font-mono text-on-surface focus:outline-none focus:border-primary transition-all placeholder:text-slate-700"
|
| 278 |
+
placeholder="gpt-4o"
|
| 279 |
+
type="text"
|
| 280 |
+
value={agentA.openaiModel}
|
| 281 |
+
onChange={e => setAgentA(a => ({ ...a, openaiModel: e.target.value }))}
|
| 282 |
/>
|
| 283 |
+
</div>
|
| 284 |
+
</div>
|
| 285 |
+
)}
|
| 286 |
+
<div className="space-y-4">
|
| 287 |
+
<div className="flex justify-between items-center">
|
| 288 |
+
<label className="font-mono text-[10px] tracking-widest text-slate-400 uppercase">Neural Temperature</label>
|
| 289 |
+
<span className="font-mono text-xs text-primary font-bold">{agentA.temp.toFixed(1)}</span>
|
| 290 |
+
</div>
|
| 291 |
+
<input
|
| 292 |
+
className="w-full h-1.5 rounded-lg appearance-none cursor-pointer accent-primary bg-surface-container-highest"
|
| 293 |
+
max="1" min="0" step="0.1" type="range"
|
| 294 |
+
value={agentA.temp}
|
| 295 |
+
onChange={e => setAgentA(a => ({ ...a, temp: parseFloat(e.target.value) }))}
|
| 296 |
+
/>
|
| 297 |
+
</div>
|
| 298 |
+
<div className="space-y-4 pt-4 border-t border-white/5">
|
| 299 |
+
<label className="font-mono text-[10px] tracking-widest text-slate-400 uppercase">Operational Role</label>
|
| 300 |
+
<select
|
| 301 |
+
className="w-full bg-surface-container-lowest border-b border-primary/30 py-2 font-mono text-sm text-on-surface focus:outline-none focus:border-primary transition-all cursor-pointer"
|
| 302 |
+
value={agentA.role}
|
| 303 |
+
onChange={e => setAgentA(a => ({ ...a, role: e.target.value }))}
|
| 304 |
+
>
|
| 305 |
+
{ROLES.map(r => <option key={r} value={r}>{r.replace(/_/g, ' ')}</option>)}
|
| 306 |
+
</select>
|
| 307 |
+
|
| 308 |
+
{agentA.role === 'CUSTOM_ROLE' && (
|
| 309 |
+
<div className="space-y-4 pt-2 animate-in fade-in slide-in-from-top-2">
|
| 310 |
+
<div className="space-y-2">
|
| 311 |
+
<label className="font-mono text-[9px] tracking-widest text-primary uppercase">Custom Role Title</label>
|
| 312 |
+
<input
|
| 313 |
+
type="text"
|
| 314 |
+
placeholder="e.g. DATABASE NINJA"
|
| 315 |
+
value={agentA.customRoleName}
|
| 316 |
+
onChange={e => setAgentA(a => ({ ...a, customRoleName: e.target.value }))}
|
| 317 |
+
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"
|
| 318 |
+
/>
|
| 319 |
</div>
|
| 320 |
+
<div className="space-y-2">
|
| 321 |
+
<label className="font-mono text-[9px] tracking-widest text-primary uppercase flex justify-between">
|
| 322 |
+
<span>System Prompt Configuration</span>
|
| 323 |
+
</label>
|
| 324 |
+
<textarea
|
| 325 |
+
placeholder="You are an elite expert... Your objective is to..."
|
| 326 |
+
value={agentA.customPrompt}
|
| 327 |
+
onChange={e => setAgentA(a => ({ ...a, customPrompt: e.target.value }))}
|
| 328 |
+
className="w-full h-32 bg-surface-container-lowest text-primary font-mono text-[10px] p-3 rounded border border-white/5 focus:border-primary/50 focus:outline-none leading-relaxed"
|
| 329 |
+
/>
|
| 330 |
</div>
|
| 331 |
+
</div>
|
| 332 |
+
)}
|
| 333 |
+
</div>
|
| 334 |
+
</div>
|
| 335 |
+
</div>
|
| 336 |
+
|
| 337 |
+
{/* Agent B Config */}
|
| 338 |
+
<div className="md:col-span-6 glass-panel rounded-xl p-8 relative overflow-hidden group refractive-edge h-full">
|
| 339 |
+
<div className="flex items-center gap-4 mb-8">
|
| 340 |
+
<div className="w-10 h-10 rounded-lg bg-secondary/10 flex items-center justify-center border border-secondary/20">
|
| 341 |
+
<span className="material-symbols-outlined text-secondary">memory</span>
|
| 342 |
+
</div>
|
| 343 |
+
<div className="flex-1">
|
| 344 |
+
<div className="flex justify-between items-start">
|
| 345 |
+
<div>
|
| 346 |
+
<h3 className="font-headline text-xl font-bold uppercase">Agent B <span className="text-secondary text-sm ml-2 tracking-tighter">[SECONDARY]</span></h3>
|
| 347 |
+
<p className="font-mono text-[10px] text-slate-500 uppercase">Logical Validation Unit 02</p>
|
| 348 |
+
</div>
|
| 349 |
+
<ProviderToggle agent={agentB} agentId="B" onSetAgent={setAgentB} />
|
| 350 |
+
</div>
|
| 351 |
+
</div>
|
| 352 |
+
</div>
|
| 353 |
+
<div className="space-y-8">
|
| 354 |
+
{agentB.provider === 'ollama' ? (
|
| 355 |
+
<OllamaModelPicker
|
| 356 |
+
value={agentB.model}
|
| 357 |
+
onChange={v => setAgentB(b => ({ ...b, model: v }))}
|
| 358 |
+
accentColor="secondary"
|
| 359 |
+
/>
|
| 360 |
+
) : agentB.provider === 'hf' ? (
|
| 361 |
+
<div className="space-y-4">
|
| 362 |
+
<div className="space-y-2">
|
| 363 |
+
<label className="font-mono text-[10px] tracking-widest text-slate-400 uppercase">HF Model Repo ID</label>
|
| 364 |
<input
|
| 365 |
+
className="w-full bg-transparent border-0 border-b border-secondary/30 py-2 font-mono text-on-surface focus:outline-none focus:border-secondary transition-all placeholder:text-slate-700"
|
| 366 |
+
placeholder="e.g. Qwen/Qwen2.5-3B-Instruct"
|
| 367 |
+
type="text"
|
| 368 |
+
value={agentB.hfModel}
|
| 369 |
+
onChange={e => setAgentB(b => ({ ...b, hfModel: e.target.value }))}
|
| 370 |
/>
|
| 371 |
</div>
|
| 372 |
+
</div>
|
| 373 |
+
) : (
|
| 374 |
+
<div className="space-y-4">
|
| 375 |
+
<div className="space-y-2">
|
| 376 |
+
<label className="font-mono text-[10px] tracking-widest text-slate-400 uppercase">Global OpenAI API Key</label>
|
| 377 |
+
<input
|
| 378 |
+
className="w-full bg-transparent border-0 border-b border-secondary/30 py-2 font-mono text-on-surface focus:outline-none focus:border-secondary transition-all placeholder:text-slate-700"
|
| 379 |
+
placeholder="sk-..."
|
| 380 |
+
type="password"
|
| 381 |
+
value={openaiKey}
|
| 382 |
+
onChange={e => setOpenaiKey(e.target.value)}
|
| 383 |
+
/>
|
| 384 |
+
</div>
|
| 385 |
+
<div className="space-y-2">
|
| 386 |
+
<label className="font-mono text-[10px] tracking-widest text-slate-400 uppercase">OpenAI Model Name</label>
|
| 387 |
+
<input
|
| 388 |
+
className="w-full bg-transparent border-0 border-b border-secondary/30 py-2 font-mono text-on-surface focus:outline-none focus:border-secondary transition-all placeholder:text-slate-700"
|
| 389 |
+
placeholder="gpt-4o-mini"
|
| 390 |
+
type="text"
|
| 391 |
+
value={agentB.openaiModel}
|
| 392 |
+
onChange={e => setAgentB(b => ({ ...b, openaiModel: e.target.value }))}
|
| 393 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
</div>
|
| 395 |
</div>
|
| 396 |
+
)}
|
| 397 |
+
<div className="space-y-4">
|
| 398 |
+
<div className="flex justify-between items-center">
|
| 399 |
+
<label className="font-mono text-[10px] tracking-widest text-slate-400 uppercase">Neural Temperature</label>
|
| 400 |
+
<span className="font-mono text-xs text-secondary font-bold">{agentB.temp.toFixed(1)}</span>
|
| 401 |
+
</div>
|
| 402 |
+
<input
|
| 403 |
+
className="w-full h-1.5 rounded-lg appearance-none cursor-pointer accent-secondary bg-surface-container-highest"
|
| 404 |
+
max="1" min="0" step="0.1" type="range"
|
| 405 |
+
value={agentB.temp}
|
| 406 |
+
onChange={e => setAgentB(b => ({ ...b, temp: parseFloat(e.target.value) }))}
|
| 407 |
+
/>
|
| 408 |
+
</div>
|
| 409 |
+
<div className="space-y-4 pt-4 border-t border-white/5">
|
| 410 |
+
<label className="font-mono text-[10px] tracking-widest text-slate-400 uppercase">Operational Role</label>
|
| 411 |
+
<select
|
| 412 |
+
className="w-full bg-surface-container-lowest border-b border-secondary/30 py-2 font-mono text-sm text-on-surface focus:outline-none focus:border-secondary transition-all cursor-pointer"
|
| 413 |
+
value={agentB.role}
|
| 414 |
+
onChange={e => setAgentB(b => ({ ...b, role: e.target.value }))}
|
| 415 |
+
>
|
| 416 |
+
{ROLES.map(r => <option key={r} value={r}>{r.replace(/_/g, ' ')}</option>)}
|
| 417 |
+
</select>
|
| 418 |
+
|
| 419 |
+
{agentB.role === 'CUSTOM_ROLE' && (
|
| 420 |
+
<div className="space-y-4 pt-2 animate-in fade-in slide-in-from-top-2">
|
| 421 |
+
<div className="space-y-2">
|
| 422 |
+
<label className="font-mono text-[9px] tracking-widest text-secondary uppercase">Custom Role Title</label>
|
| 423 |
+
<input
|
| 424 |
+
type="text"
|
| 425 |
+
placeholder="e.g. LOGICAL SKEPTIC"
|
| 426 |
+
value={agentB.customRoleName}
|
| 427 |
+
onChange={e => setAgentB(b => ({ ...b, customRoleName: e.target.value }))}
|
| 428 |
+
className="w-full bg-transparent border-0 border-b border-secondary/30 py-2 font-mono text-sm text-on-surface focus:outline-none focus:border-secondary transition-all placeholder:text-slate-700"
|
| 429 |
+
/>
|
| 430 |
+
</div>
|
| 431 |
+
<div className="space-y-2">
|
| 432 |
+
<label className="font-mono text-[9px] tracking-widest text-secondary uppercase flex justify-between">
|
| 433 |
+
<span>System Prompt Configuration</span>
|
| 434 |
+
</label>
|
| 435 |
+
<textarea
|
| 436 |
+
placeholder="You are an expert... Challenge your partner..."
|
| 437 |
+
value={agentB.customPrompt}
|
| 438 |
+
onChange={e => setAgentB(b => ({ ...b, customPrompt: e.target.value }))}
|
| 439 |
+
className="w-full h-32 bg-surface-container-lowest text-secondary font-mono text-[10px] p-3 rounded border border-white/5 focus:border-secondary/50 focus:outline-none leading-relaxed"
|
| 440 |
+
/>
|
| 441 |
+
</div>
|
| 442 |
+
</div>
|
| 443 |
+
)}
|
| 444 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 445 |
</div>
|
| 446 |
+
</div>
|
| 447 |
|
| 448 |
{/* Execution Environment */}
|
| 449 |
<div className="md:col-span-12 glass-panel rounded-xl p-8 refractive-edge">
|
|
|
|
| 463 |
key={m.id}
|
| 464 |
id={`exec-mode-${m.id}`}
|
| 465 |
onClick={() => setExecutionMode(m.id)}
|
|
|
|
| 466 |
className={`flex-1 flex items-center justify-center gap-2 py-2 px-3 rounded text-[11px] font-mono font-bold uppercase transition-all ${executionMode === m.id ? 'bg-tertiary text-black' : 'text-outline-variant hover:text-white'
|
| 467 |
}`}
|
| 468 |
>
|
|
|
|
| 584 |
</div>
|
| 585 |
<div className="flex items-center gap-4">
|
| 586 |
<button
|
| 587 |
+
onClick={() => { setAgentA(a => ({ ...a, model: '', provider: 'ollama' })); setAgentB(b => ({ ...b, model: '', provider: 'ollama' })); setMaxSteps(12); }}
|
|
|
|
|
|
|
|
|
|
| 588 |
className="px-8 py-3 bg-surface-container-high text-on-surface-variant font-headline font-bold text-sm tracking-widest rounded hover:bg-surface-container-highest hover:text-white transition-all uppercase"
|
| 589 |
>
|
| 590 |
Reset
|
openenv.yaml
CHANGED
|
@@ -1,59 +1,59 @@
|
|
| 1 |
-
name: nexus-incident-investigation
|
| 2 |
-
version: "1.0.0"
|
| 3 |
-
tags: ["openenv"
|
| 4 |
-
description: >
|
| 5 |
-
NEXUS —
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
Together they identify root causes across software, business-process,
|
| 10 |
-
and cascade-system failure scenarios.
|
| 11 |
-
|
| 12 |
-
tasks:
|
| 13 |
-
- name: software-incident
|
| 14 |
-
description: Single-service software bug causing user-facing errors
|
| 15 |
-
difficulty: easy
|
| 16 |
-
max_steps: 8
|
| 17 |
-
grader: scenarios/graders/easy_grader.py
|
| 18 |
-
|
| 19 |
-
- name: business-process-failure
|
| 20 |
-
description: Multi-team process breakdown with misleading red-herrings
|
| 21 |
-
difficulty: medium
|
| 22 |
-
max_steps: 8
|
| 23 |
-
grader: scenarios/graders/medium_grader.py
|
| 24 |
-
|
| 25 |
-
- name: cascade-system-failure
|
| 26 |
-
description: Multi-system cascade failure with misleading logs
|
| 27 |
-
difficulty: hard
|
| 28 |
-
max_steps: 8
|
| 29 |
-
grader: scenarios/graders/hard_grader.py
|
| 30 |
-
|
| 31 |
-
action_space:
|
| 32 |
-
type: text
|
| 33 |
-
description:
|
| 34 |
-
|
| 35 |
-
observation_space:
|
| 36 |
-
type: structured
|
| 37 |
-
fields:
|
| 38 |
-
scenario_description: string
|
| 39 |
-
scenario_context: string
|
| 40 |
-
partner_message: string
|
| 41 |
-
tool_results: list
|
| 42 |
-
clues_found: list
|
| 43 |
-
investigation_stage: string
|
| 44 |
-
round: integer
|
| 45 |
-
available_tools: list
|
| 46 |
-
|
| 47 |
-
reward_range: [0.0, 1.0]
|
| 48 |
-
reward_description: >
|
| 49 |
-
Dynamically computed from semantic similarity of hypothesis to root-cause,
|
| 50 |
-
tool quality, fix correctness, and investigation efficiency.
|
| 51 |
-
|
| 52 |
-
inference_script: inference.py
|
| 53 |
-
entry_point: backend/main.py
|
| 54 |
-
docker_port: 7860
|
| 55 |
-
|
| 56 |
-
baseline_scores:
|
| 57 |
-
software-incident: 0.
|
| 58 |
-
business-process-failure: 0.
|
| 59 |
-
cascade-system-failure: 0.
|
|
|
|
| 1 |
+
name: nexus-incident-investigation
|
| 2 |
+
version: "1.0.0"
|
| 3 |
+
tags: ["openenv"]
|
| 4 |
+
description: >
|
| 5 |
+
NEXUS — Dual Agent Incident Investigation Environment.
|
| 6 |
+
Two AI agents collaborate to investigate real-world system incidents.
|
| 7 |
+
Agent A (Investigator) proposes hypotheses and calls tools.
|
| 8 |
+
Agent B (Validator) challenges claims and verifies fixes.
|
| 9 |
+
Together they identify root causes across software, business-process,
|
| 10 |
+
and cascade-system failure scenarios.
|
| 11 |
+
|
| 12 |
+
tasks:
|
| 13 |
+
- name: software-incident
|
| 14 |
+
description: Single-service software bug causing user-facing errors
|
| 15 |
+
difficulty: easy
|
| 16 |
+
max_steps: 8
|
| 17 |
+
grader: scenarios/graders/easy_grader.py
|
| 18 |
+
|
| 19 |
+
- name: business-process-failure
|
| 20 |
+
description: Multi-team process breakdown with misleading red-herrings
|
| 21 |
+
difficulty: medium
|
| 22 |
+
max_steps: 8
|
| 23 |
+
grader: scenarios/graders/medium_grader.py
|
| 24 |
+
|
| 25 |
+
- name: cascade-system-failure
|
| 26 |
+
description: Multi-system cascade failure with misleading logs
|
| 27 |
+
difficulty: hard
|
| 28 |
+
max_steps: 8
|
| 29 |
+
grader: scenarios/graders/hard_grader.py
|
| 30 |
+
|
| 31 |
+
action_space:
|
| 32 |
+
type: text
|
| 33 |
+
description: Free-form natural language message with optional TOOL: calls
|
| 34 |
+
|
| 35 |
+
observation_space:
|
| 36 |
+
type: structured
|
| 37 |
+
fields:
|
| 38 |
+
scenario_description: string
|
| 39 |
+
scenario_context: string
|
| 40 |
+
partner_message: string
|
| 41 |
+
tool_results: list
|
| 42 |
+
clues_found: list
|
| 43 |
+
investigation_stage: string
|
| 44 |
+
round: integer
|
| 45 |
+
available_tools: list
|
| 46 |
+
|
| 47 |
+
reward_range: [0.0, 1.0]
|
| 48 |
+
reward_description: >
|
| 49 |
+
Dynamically computed from semantic similarity of hypothesis to root-cause,
|
| 50 |
+
tool quality, fix correctness, and investigation efficiency.
|
| 51 |
+
|
| 52 |
+
inference_script: inference.py
|
| 53 |
+
entry_point: backend/main.py
|
| 54 |
+
docker_port: 7860
|
| 55 |
+
|
| 56 |
+
baseline_scores:
|
| 57 |
+
software-incident: 0.45
|
| 58 |
+
business-process-failure: 0.35
|
| 59 |
+
cascade-system-failure: 0.25
|
pyproject.toml
DELETED
|
@@ -1,27 +0,0 @@
|
|
| 1 |
-
[build-system]
|
| 2 |
-
requires = ["setuptools>=45", "wheel"]
|
| 3 |
-
build-backend = "setuptools.build_meta"
|
| 4 |
-
|
| 5 |
-
[project]
|
| 6 |
-
name = "nexus-ai"
|
| 7 |
-
version = "1.0.0"
|
| 8 |
-
description = "NEXUS - Dual Agent Incident Investigation Environment"
|
| 9 |
-
requires-python = ">=3.10"
|
| 10 |
-
dependencies = [
|
| 11 |
-
"openenv-core>=0.2.0",
|
| 12 |
-
"fastapi>=0.100.0",
|
| 13 |
-
"uvicorn[standard]>=0.23.0",
|
| 14 |
-
"pydantic>=2.0.0",
|
| 15 |
-
"python-dotenv>=1.0.0",
|
| 16 |
-
"httpx>=0.24.0",
|
| 17 |
-
"openai>=1.0.0",
|
| 18 |
-
"psutil>=5.9.0",
|
| 19 |
-
]
|
| 20 |
-
|
| 21 |
-
[project.scripts]
|
| 22 |
-
server = "server.app:main"
|
| 23 |
-
|
| 24 |
-
[tool.setuptools]
|
| 25 |
-
include-package-data = true
|
| 26 |
-
packages = ["server"]
|
| 27 |
-
package-dir = { "server" = "server" }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
# Server package
|
|
|
|
|
|
server/app.py
DELETED
|
@@ -1,13 +0,0 @@
|
|
| 1 |
-
import sys
|
| 2 |
-
import os
|
| 3 |
-
|
| 4 |
-
# Add backend to path
|
| 5 |
-
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'backend'))
|
| 6 |
-
|
| 7 |
-
def main():
|
| 8 |
-
import uvicorn
|
| 9 |
-
from backend.main import app
|
| 10 |
-
uvicorn.run(app, host="0.0.0.0", port=7860)
|
| 11 |
-
|
| 12 |
-
if __name__ == "__main__":
|
| 13 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setup.bat
DELETED
|
@@ -1,66 +0,0 @@
|
|
| 1 |
-
@echo off
|
| 2 |
-
echo ==============================================================
|
| 3 |
-
echo NEXUS Incident Investigation Environment Setup
|
| 4 |
-
echo ==============================================================
|
| 5 |
-
echo.
|
| 6 |
-
|
| 7 |
-
REM Check Python
|
| 8 |
-
python --version >nul 2>&1
|
| 9 |
-
if %errorlevel% neq 0 (
|
| 10 |
-
echo [ERROR] Python is not installed or not in PATH!
|
| 11 |
-
pause
|
| 12 |
-
exit /b
|
| 13 |
-
)
|
| 14 |
-
|
| 15 |
-
REM Check npm
|
| 16 |
-
npm --version >nul 2>&1
|
| 17 |
-
if %errorlevel% neq 0 (
|
| 18 |
-
echo [ERROR] Node.js/npm is not installed or not in PATH!
|
| 19 |
-
pause
|
| 20 |
-
exit /b
|
| 21 |
-
)
|
| 22 |
-
|
| 23 |
-
echo [1/3] Setting up Backend Virtual Environment...
|
| 24 |
-
python -m venv backend\venv
|
| 25 |
-
call backend\venv\Scripts\activate.bat
|
| 26 |
-
pip install -r backend\requirements.txt
|
| 27 |
-
|
| 28 |
-
echo.
|
| 29 |
-
echo [2/3] Setting up Frontend Dependencies...
|
| 30 |
-
cd frontend
|
| 31 |
-
call npm install
|
| 32 |
-
cd ..
|
| 33 |
-
|
| 34 |
-
echo.
|
| 35 |
-
echo [3/4] Pulling Required LLM Models (Ollama)...
|
| 36 |
-
echo --------------------------------------------------------------
|
| 37 |
-
echo This will ensure you have the correct models for the simulation.
|
| 38 |
-
echo 1. microsoft/Phi-3-mini-4k-instruct (Investigator)
|
| 39 |
-
echo 2. Qwen/Qwen2.5-1.5B-Instruct (Validator)
|
| 40 |
-
echo 3. all-minilm (Reward Engine)
|
| 41 |
-
echo.
|
| 42 |
-
set /p PULL_MODELS="Do you want to pull these models now? (y/n): "
|
| 43 |
-
if /i "%PULL_MODELS%"=="y" (
|
| 44 |
-
echo [Pulling Phi-3...]
|
| 45 |
-
ollama pull phi3:mini
|
| 46 |
-
echo [Pulling Qwen-1.5B...]
|
| 47 |
-
ollama pull qwen2.5:1.5b
|
| 48 |
-
echo [Pulling all-minilm...]
|
| 49 |
-
ollama pull all-minilm
|
| 50 |
-
) else (
|
| 51 |
-
echo Skipping model pull. Ensure you pull them manually later.
|
| 52 |
-
)
|
| 53 |
-
|
| 54 |
-
echo.
|
| 55 |
-
echo [4/4] Validating OpenEnv Compliance...
|
| 56 |
-
call backend\venv\Scripts\python.exe openenv_validator.py
|
| 57 |
-
|
| 58 |
-
echo.
|
| 59 |
-
echo ==============================================================
|
| 60 |
-
echo SETUP COMPLETE!
|
| 61 |
-
echo.
|
| 62 |
-
echo To run locally:
|
| 63 |
-
echo 1. Start UI: cd frontend ^& npm run dev
|
| 64 |
-
echo 2. Start API: cd backend ^& venv\Scripts\python main.py
|
| 65 |
-
echo ==============================================================
|
| 66 |
-
pause
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setup.sh
DELETED
|
@@ -1,42 +0,0 @@
|
|
| 1 |
-
#!/bin/bash
|
| 2 |
-
|
| 3 |
-
echo "=============================================================="
|
| 4 |
-
echo "NEXUS Incident Investigation Environment Setup"
|
| 5 |
-
echo "=============================================================="
|
| 6 |
-
echo ""
|
| 7 |
-
|
| 8 |
-
# Check Python
|
| 9 |
-
if ! command -v python3 &> /dev/null; then
|
| 10 |
-
echo "[ERROR] python3 is not installed or not in PATH!"
|
| 11 |
-
exit 1
|
| 12 |
-
fi
|
| 13 |
-
|
| 14 |
-
# Check npm
|
| 15 |
-
if ! command -v npm &> /dev/null; then
|
| 16 |
-
echo "[ERROR] npm is not installed or not in PATH!"
|
| 17 |
-
exit 1
|
| 18 |
-
fi
|
| 19 |
-
|
| 20 |
-
echo "[1/3] Setting up Backend Virtual Environment..."
|
| 21 |
-
python3 -m venv backend/venv
|
| 22 |
-
source backend/venv/bin/activate
|
| 23 |
-
pip install -r backend/requirements.txt
|
| 24 |
-
|
| 25 |
-
echo ""
|
| 26 |
-
echo "[2/3] Setting up Frontend Dependencies..."
|
| 27 |
-
cd frontend
|
| 28 |
-
npm install
|
| 29 |
-
cd ..
|
| 30 |
-
|
| 31 |
-
echo ""
|
| 32 |
-
echo "[3/3] Validating OpenEnv Compliance..."
|
| 33 |
-
backend/venv/bin/python openenv_validator.py
|
| 34 |
-
|
| 35 |
-
echo ""
|
| 36 |
-
echo "=============================================================="
|
| 37 |
-
echo "SETUP COMPLETE!"
|
| 38 |
-
echo ""
|
| 39 |
-
echo "To run locally without Docker:"
|
| 40 |
-
echo "1. Start UI: cd frontend && npm run dev"
|
| 41 |
-
echo "2. Start API: cd backend && venv/bin/uvicorn main:app --reload"
|
| 42 |
-
echo "=============================================================="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|