AgentMask / src /web /app.py
b2230765034
docs: Update README and apply hacker terminal theme to UI
459127b
"""
FastAPI Web Application
========================
Developer console and API endpoints for the multi-agent system.
"""
import sys
import os
from pathlib import Path
# Add src to path
sys.path.insert(0, str(Path(__file__).parent.parent))
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from typing import Any, Optional
from orchestrator import Orchestrator
from ledger.merkle import compute_merkle_root
app = FastAPI(
title="AgentMask Developer Console",
description="Multi-agent system with audit trail",
version="0.1.0"
)
class TaskRequest(BaseModel):
"""Request model for running a task."""
query: str
options: Optional[dict[str, Any]] = None
class TaskResponse(BaseModel):
"""Response model for task execution."""
success: bool
task: dict[str, Any]
steps: list[dict[str, Any]]
final_output: dict[str, Any]
merkle_root: str
total_steps: int
# Global orchestrator instance
orchestrator = Orchestrator()
@app.get("/", response_class=HTMLResponse)
async def get_console():
"""
Serve the developer console HTML page.
"""
html_content = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AgentMask Developer Console</title>
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Share+Tech+Mono&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'JetBrains Mono', 'Consolas', monospace;
background: #0a0a0a;
background-image:
radial-gradient(ellipse at top, #0d1a0d 0%, transparent 50%),
radial-gradient(ellipse at bottom, #1a0d0d 0%, transparent 50%),
repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0, 255, 65, 0.03) 2px, rgba(0, 255, 65, 0.03) 4px);
min-height: 100vh;
color: #00ff41;
padding: 20px;
}
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.15),
rgba(0, 0, 0, 0.15) 1px,
transparent 1px,
transparent 2px
);
pointer-events: none;
z-index: 1000;
}
.container {
max-width: 1400px;
margin: 0 auto;
position: relative;
}
h1 {
text-align: center;
margin-bottom: 30px;
color: #00ff41;
text-shadow:
0 0 5px #00ff41,
0 0 10px #00ff41,
0 0 20px #00ff41,
0 0 40px #00cc33;
font-family: 'Share Tech Mono', monospace;
letter-spacing: 3px;
animation: flicker 3s infinite;
}
@keyframes flicker {
0%, 100% { opacity: 1; }
92% { opacity: 1; }
93% { opacity: 0.8; }
94% { opacity: 1; }
95% { opacity: 0.9; }
96% { opacity: 1; }
}
.input-section {
background: rgba(0, 20, 0, 0.8);
border: 1px solid #00ff41;
border-radius: 0;
padding: 20px;
margin-bottom: 20px;
box-shadow:
0 0 10px rgba(0, 255, 65, 0.3),
inset 0 0 30px rgba(0, 255, 65, 0.05);
}
.input-group {
display: flex;
gap: 10px;
}
input[type="text"] {
flex: 1;
padding: 12px 20px;
border: 1px solid #00ff41;
border-radius: 0;
background: rgba(0, 0, 0, 0.9);
color: #00ff41;
font-size: 16px;
font-family: 'JetBrains Mono', monospace;
text-shadow: 0 0 5px #00ff41;
}
input[type="text"]::placeholder {
color: #006622;
}
input[type="text"]:focus {
outline: none;
box-shadow:
0 0 10px rgba(0, 255, 65, 0.5),
0 0 20px rgba(0, 255, 65, 0.3);
border-color: #39ff14;
}
button {
padding: 12px 30px;
background: transparent;
border: 2px solid #ff0040;
border-radius: 0;
color: #ff0040;
font-size: 16px;
font-weight: bold;
font-family: 'Share Tech Mono', monospace;
cursor: pointer;
transition: all 0.3s;
text-transform: uppercase;
letter-spacing: 2px;
text-shadow: 0 0 5px #ff0040;
}
button:hover {
background: #ff0040;
color: #000;
box-shadow:
0 0 20px rgba(255, 0, 64, 0.6),
0 0 40px rgba(255, 0, 64, 0.4);
text-shadow: none;
}
button:disabled {
border-color: #333;
color: #333;
cursor: not-allowed;
text-shadow: none;
box-shadow: none;
}
.main-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
@media (max-width: 1000px) {
.main-content {
grid-template-columns: 1fr;
}
}
.results-section, .graph-section {
background: rgba(0, 20, 0, 0.8);
border: 1px solid #00ff41;
border-radius: 0;
padding: 20px;
box-shadow:
0 0 10px rgba(0, 255, 65, 0.2),
inset 0 0 50px rgba(0, 255, 65, 0.03);
}
.graph-section h2 {
color: #00ff41;
margin-bottom: 15px;
text-shadow: 0 0 10px #00ff41;
font-family: 'Share Tech Mono', monospace;
}
.mermaid {
background: #0d0d0d;
border: 1px solid #00ff41;
border-radius: 0;
padding: 20px;
min-height: 200px;
}
.merkle-root {
background: rgba(255, 0, 64, 0.1);
border: 1px solid #ff0040;
border-radius: 0;
padding: 15px;
margin-bottom: 20px;
font-family: monospace;
word-break: break-all;
box-shadow: 0 0 10px rgba(255, 0, 64, 0.2);
}
.merkle-root label {
color: #ff0040;
font-weight: bold;
text-shadow: 0 0 5px #ff0040;
}
.step {
background: rgba(0, 0, 0, 0.5);
border-radius: 0;
padding: 15px;
margin-bottom: 15px;
border-left: 3px solid #00ff41;
border-right: 1px solid #003311;
border-top: 1px solid #003311;
border-bottom: 1px solid #003311;
}
.step-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.step-number {
background: #ff0040;
color: #000;
padding: 5px 15px;
border-radius: 0;
font-weight: bold;
font-family: 'Share Tech Mono', monospace;
box-shadow: 0 0 10px rgba(255, 0, 64, 0.5);
}
.agent-name {
color: #39ff14;
font-weight: bold;
font-size: 18px;
text-shadow: 0 0 10px #39ff14;
text-transform: uppercase;
letter-spacing: 2px;
}
.step-hash {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: #006622;
word-break: break-all;
}
.step-content {
margin-top: 10px;
}
.step-content pre {
background: rgba(0, 0, 0, 0.8);
padding: 10px;
border-radius: 0;
border: 1px solid #003311;
overflow-x: auto;
font-size: 12px;
color: #00cc33;
}
.loading {
text-align: center;
padding: 40px;
color: #00ff41;
text-shadow: 0 0 10px #00ff41;
}
.loading::after {
content: '';
animation: dots 1.5s infinite;
}
@keyframes dots {
0%, 20% { content: '_'; }
40% { content: '__'; }
60%, 100% { content: '___'; }
}
.error {
background: rgba(255, 0, 0, 0.15);
border: 1px solid #ff0040;
border-radius: 0;
padding: 15px;
color: #ff3366;
text-shadow: 0 0 5px #ff0040;
}
.summary {
background: rgba(0, 255, 65, 0.1);
border: 1px solid #00ff41;
border-radius: 0;
padding: 15px;
margin-top: 20px;
box-shadow: 0 0 15px rgba(0, 255, 65, 0.2);
}
.summary h3 {
color: #39ff14;
margin-bottom: 10px;
text-shadow: 0 0 10px #39ff14;
}
.tab-container {
margin-bottom: 15px;
}
.tab-buttons {
display: flex;
gap: 5px;
margin-bottom: 10px;
}
.tab-btn {
padding: 8px 16px;
background: transparent;
border: 1px solid #00ff41;
border-radius: 0;
color: #00ff41;
cursor: pointer;
font-size: 14px;
font-family: 'Share Tech Mono', monospace;
transition: all 0.2s;
}
.tab-btn:hover {
background: rgba(0, 255, 65, 0.1);
box-shadow: 0 0 10px rgba(0, 255, 65, 0.3);
}
.tab-btn.active {
background: #00ff41;
color: #000;
box-shadow: 0 0 15px rgba(0, 255, 65, 0.5);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* Terminal cursor blink */
h1::after {
content: 'β–ˆ';
animation: blink 1s infinite;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #0a0a0a;
}
::-webkit-scrollbar-thumb {
background: #00ff41;
border-radius: 0;
}
::-webkit-scrollbar-thumb:hover {
background: #39ff14;
}
/* Selection color */
::selection {
background: #00ff41;
color: #000;
}
</style>
</head>
<body>
<div class="container">
<h1>⌘ AgentMask Terminal </h1>
<div class="input-section">
<div class="input-group">
<input type="text" id="queryInput" placeholder="[root@agentmask]$ Enter query..." value="AI in healthcare diagnosis">
<button id="runBtn" onclick="runTask()">β–Ί EXECUTE</button>
</div>
</div>
<div class="main-content">
<div class="results-section" id="results">
<p style="text-align: center; color: #006622;">&gt; Awaiting command input...<br>&gt; Type query and execute to initialize agent pipeline_</p>
</div>
<div class="graph-section">
<h2>&gt; AGENT PIPELINE GRAPH_</h2>
<div class="tab-container">
<div class="tab-buttons">
<button class="tab-btn active" onclick="showTab('flow')">Flow</button>
<button class="tab-btn" onclick="showTab('sequence')">Sequence</button>
<button class="tab-btn" onclick="showTab('merkle')">Merkle Tree</button>
</div>
<div id="flow-tab" class="tab-content active">
<div id="flowGraph" class="mermaid">
graph LR
A[πŸ” Input Query] --> B[πŸ“š Research Agent]
B --> C[πŸ“ Summarizer Agent]
C --> D[βœ… Output]
</div>
</div>
<div id="sequence-tab" class="tab-content">
<div id="sequenceGraph" class="mermaid">
sequenceDiagram
participant U as User
participant O as Orchestrator
participant R as Research
participant S as Summarizer
U->>O: Submit Query
O->>R: Execute Search
R-->>O: Search Results
O->>S: Summarize Results
S-->>O: Summary
O-->>U: Final Output
</div>
</div>
<div id="merkle-tab" class="tab-content">
<div id="merkleGraph" class="mermaid">
graph TB
Root[πŸ” Merkle Root]
Root --> H1[Hash 1-2]
Root --> H2[Hash 3-4]
H1 --> L1[Step 1 Hash]
H1 --> L2[Step 2 Hash]
</div>
</div>
</div>
</div>
</div>
</div>
<script>
mermaid.initialize({
startOnLoad: true,
theme: 'dark',
themeVariables: {
primaryColor: '#00ff41',
primaryTextColor: '#000',
primaryBorderColor: '#00cc33',
lineColor: '#00ff41',
secondaryColor: '#ff0040',
tertiaryColor: '#0a0a0a',
background: '#0a0a0a',
mainBkg: '#0d1a0d',
textColor: '#00ff41',
nodeTextColor: '#000',
nodeBorder: '#00ff41',
clusterBkg: '#0d1a0d',
clusterBorder: '#00ff41',
edgeLabelBackground: '#0a0a0a'
}
});
function showTab(tabName) {
// Hide all tabs
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.remove('active');
});
// Show selected tab
document.getElementById(tabName + '-tab').classList.add('active');
event.target.classList.add('active');
}
async function runTask() {
const query = document.getElementById('queryInput').value.trim();
const resultsDiv = document.getElementById('results');
const runBtn = document.getElementById('runBtn');
if (!query) {
resultsDiv.innerHTML = `<div class="error">&gt; ERROR: Please enter a query.</div>`;
return;
}
runBtn.disabled = true;
resultsDiv.innerHTML = '<div class="loading">&gt; Initializing agent pipeline</div>';
try {
const response = await fetch('/run', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ query: query })
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
displayResults(data);
updateGraphs(data);
} catch (error) {
resultsDiv.innerHTML = `<div class="error">&gt; FATAL: ${error.message}</div>`;
} finally {
runBtn.disabled = false;
}
}
function displayResults(data) {
const resultsDiv = document.getElementById('results');
let html = '';
// Merkle Root
html += `
<div class="merkle-root">
<label>πŸ” MERKLE ROOT HASH:</label>
<div style="margin-top: 5px; color: #ff0040;">${data.merkle_root}</div>
</div>
`;
// Steps
html += '<h2 style="margin-bottom: 15px; color: #00ff41; text-shadow: 0 0 10px #00ff41;">&gt; EXECUTION LOG_</h2>';
for (const step of data.steps) {
html += `
<div class="step">
<div class="step-header">
<span class="step-number">STEP ${step.step}</span>
<span class="agent-name">[${step.agent.toUpperCase()}]</span>
</div>
<div class="step-hash">SHA256: ${step.hash}</div>
<div class="step-content">
<details>
<summary style="cursor: pointer; color: #39ff14;">&gt; View I/O Data_</summary>
<h4 style="margin: 10px 0 5px; color: #ff0040;">INPUT:</h4>
<pre>${JSON.stringify(step.input, null, 2)}</pre>
<h4 style="margin: 10px 0 5px; color: #ff0040;">OUTPUT:</h4>
<pre>${JSON.stringify(step.output, null, 2)}</pre>
</details>
</div>
</div>
`;
}
// Final Summary
if (data.final_output && data.final_output.summary) {
html += `
<div class="summary">
<h3>&gt; FINAL OUTPUT_</h3>
<p style="color: #00ff41;">${data.final_output.summary}</p>
</div>
`;
}
resultsDiv.innerHTML = html;
}
function updateGraphs(data) {
// Update flow graph with actual steps
const steps = data.steps;
let flowDef = 'graph LR\\n';
flowDef += ' A[πŸ” Input Query]';
steps.forEach((step, i) => {
const prev = i === 0 ? 'A' : `S${i}`;
const curr = `S${i + 1}`;
const icon = step.agent === 'research' ? 'πŸ“š' : 'πŸ“';
flowDef += ` --> ${curr}[${icon} ${step.agent.charAt(0).toUpperCase() + step.agent.slice(1)}]`;
});
flowDef += ' --> Z[βœ… Output]';
// Update merkle tree visualization
let merkleDef = 'graph TB\\n';
merkleDef += ` Root[πŸ” ${data.merkle_root.substring(0, 12)}...]\\n`;
steps.forEach((step, i) => {
merkleDef += ` Root --> H${i + 1}[Step ${i + 1}: ${step.hash.substring(0, 8)}...]\\n`;
});
// Re-render mermaid graphs
const flowEl = document.getElementById('flowGraph');
const merkleEl = document.getElementById('merkleGraph');
flowEl.innerHTML = flowDef;
merkleEl.innerHTML = merkleDef;
mermaid.init(undefined, flowEl);
mermaid.init(undefined, merkleEl);
}
// Allow Enter key to submit
document.getElementById('queryInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
runTask();
}
});
</script>
</body>
</html>
"""
return HTMLResponse(content=html_content)
@app.post("/run", response_model=TaskResponse)
async def run_task(request: TaskRequest):
"""
Execute a task through the multi-agent pipeline.
Args:
request: TaskRequest with query and optional parameters
Returns:
TaskResponse with execution results and merkle root
"""
try:
# Run the orchestrator
result = await orchestrator.run_task({"query": request.query})
# Compute merkle root from all step hashes
step_hashes = [step["hash"] for step in result["steps"]]
merkle_root = compute_merkle_root(step_hashes) if step_hashes else ""
return TaskResponse(
success=True,
task=result["task"],
steps=result["steps"],
final_output=result["final_output"],
merkle_root=merkle_root,
total_steps=result["total_steps"]
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/health")
async def health_check():
"""Health check endpoint."""
return {"status": "healthy", "version": "0.1.0"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)