Spaces:
Sleeping
Sleeping
Claude Code commited on
Commit Β·
2f8fc36
1
Parent(s): c4b8552
Claude Code: Refactor to use FastAPI as the backend instead of standard Grad
Browse files- Dockerfile +2 -1
- app.py +478 -38
- gradio_dashboard.py +285 -241
- requirements.txt +3 -0
- scripts/entrypoint.sh +12 -9
Dockerfile
CHANGED
|
@@ -13,7 +13,7 @@ RUN echo "[build] Installing system deps..." && START=$(date +%s) \
|
|
| 13 |
&& apt-get update \
|
| 14 |
&& apt-get install -y --no-install-recommends git ca-certificates curl python3 python3-pip \
|
| 15 |
&& rm -rf /var/lib/apt/lists/* \
|
| 16 |
-
&& pip3 install --no-cache-dir --break-system-packages huggingface_hub requests gradio psutil \
|
| 17 |
&& corepack enable \
|
| 18 |
&& mkdir -p /app/openclaw \
|
| 19 |
&& chown -R node:node /app \
|
|
@@ -54,6 +54,7 @@ COPY --chown=node:node frontend /home/node/frontend
|
|
| 54 |
COPY --chown=node:node workspace-templates /home/node/workspace-templates
|
| 55 |
COPY --chown=node:node openclaw.json /home/node/scripts/openclaw.json.default
|
| 56 |
COPY --chown=node:node gradio_dashboard.py /home/node/gradio_dashboard.py
|
|
|
|
| 57 |
RUN chmod +x /home/node/scripts/entrypoint.sh /home/node/scripts/sync_hf.py \
|
| 58 |
&& VERSION_TS=$(date +%s) \
|
| 59 |
&& sed "s/{{VERSION_TIMESTAMP}}/${VERSION_TS}/g" /home/node/frontend/electron-standalone.html > /home/node/frontend/index.html \
|
|
|
|
| 13 |
&& apt-get update \
|
| 14 |
&& apt-get install -y --no-install-recommends git ca-certificates curl python3 python3-pip \
|
| 15 |
&& rm -rf /var/lib/apt/lists/* \
|
| 16 |
+
&& pip3 install --no-cache-dir --break-system-packages huggingface_hub requests gradio psutil fastapi uvicorn[standard] \
|
| 17 |
&& corepack enable \
|
| 18 |
&& mkdir -p /app/openclaw \
|
| 19 |
&& chown -R node:node /app \
|
|
|
|
| 54 |
COPY --chown=node:node workspace-templates /home/node/workspace-templates
|
| 55 |
COPY --chown=node:node openclaw.json /home/node/scripts/openclaw.json.default
|
| 56 |
COPY --chown=node:node gradio_dashboard.py /home/node/gradio_dashboard.py
|
| 57 |
+
COPY --chown=node:node app.py /home/node/app.py
|
| 58 |
RUN chmod +x /home/node/scripts/entrypoint.sh /home/node/scripts/sync_hf.py \
|
| 59 |
&& VERSION_TS=$(date +%s) \
|
| 60 |
&& sed "s/{{VERSION_TIMESTAMP}}/${VERSION_TS}/g" /home/node/frontend/electron-standalone.html > /home/node/frontend/index.html \
|
app.py
CHANGED
|
@@ -1,46 +1,486 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import sys
|
| 3 |
-
import
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
else:
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
except Exception as e:
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
FastAPI Backend for HuggingClaw-Cain
|
| 4 |
+
=====================================
|
| 5 |
+
Provides REST API endpoints for agent status checks and chat interactions.
|
| 6 |
+
Mounts the Gradio dashboard as a sub-route.
|
| 7 |
+
"""
|
| 8 |
+
import os
|
| 9 |
import sys
|
| 10 |
+
import json
|
| 11 |
+
import asyncio
|
| 12 |
+
from typing import Dict, Any, List, Optional
|
| 13 |
+
from datetime import datetime
|
| 14 |
+
from pathlib import Path
|
| 15 |
|
| 16 |
+
from fastapi import FastAPI, HTTPException, BackgroundTasks
|
| 17 |
+
from fastapi.responses import JSONResponse
|
| 18 |
+
from pydantic import BaseModel, Field
|
| 19 |
+
import uvicorn
|
| 20 |
|
| 21 |
+
# Paths
|
| 22 |
+
WORKSPACE_DIR = Path("/tmp/claude-workspace")
|
| 23 |
+
OPENCLAW_HOME = Path.home() / ".openclaw"
|
| 24 |
|
| 25 |
+
# Use workspace if available (for local testing), otherwise home directory
|
| 26 |
+
if WORKSPACE_DIR.exists() and (WORKSPACE_DIR / ".openclaw").exists():
|
| 27 |
+
BASE_DIR = WORKSPACE_DIR / ".openclaw"
|
| 28 |
+
else:
|
| 29 |
+
BASE_DIR = OPENCLAW_HOME
|
| 30 |
+
|
| 31 |
+
AGENTS_DIR = BASE_DIR / "agents"
|
| 32 |
+
sys.path.insert(0, str(AGENTS_DIR))
|
| 33 |
+
sys.path.insert(0, str(BASE_DIR))
|
| 34 |
+
|
| 35 |
+
# Import brain and health monitor
|
| 36 |
+
try:
|
| 37 |
+
from brain_minimal import get_brain, BrainState
|
| 38 |
+
BRAIN_AVAILABLE = True
|
| 39 |
+
except ImportError:
|
| 40 |
+
BRAIN_AVAILABLE = False
|
| 41 |
+
print("[API] Warning: brain_minimal not available")
|
| 42 |
+
|
| 43 |
+
try:
|
| 44 |
+
from health_monitor import get_health_monitor, LogLevel, HealthStatus
|
| 45 |
+
HEALTH_MONITOR_AVAILABLE = True
|
| 46 |
+
except ImportError:
|
| 47 |
+
HEALTH_MONITOR_AVAILABLE = False
|
| 48 |
+
print("[API] Warning: health_monitor not available")
|
| 49 |
+
|
| 50 |
+
# FastAPI app
|
| 51 |
+
app = FastAPI(
|
| 52 |
+
title="HuggingClaw-Cain API",
|
| 53 |
+
description="REST API for Cain agent interactions and monitoring",
|
| 54 |
+
version="1.0.0"
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
# ========== Request/Response Models ==========
|
| 58 |
+
|
| 59 |
+
class ChatMessage(BaseModel):
|
| 60 |
+
"""Chat message model"""
|
| 61 |
+
message: str = Field(..., description="The message to process")
|
| 62 |
+
session_id: Optional[str] = Field(None, description="Optional session ID for conversation tracking")
|
| 63 |
+
context: Optional[Dict[str, Any]] = Field(None, description="Additional context for the conversation")
|
| 64 |
+
|
| 65 |
+
class AgentStatus(BaseModel):
|
| 66 |
+
"""Agent status response model"""
|
| 67 |
+
agent_name: str
|
| 68 |
+
role: str
|
| 69 |
+
state: str
|
| 70 |
+
brain_state: str
|
| 71 |
+
legacy_mode: bool
|
| 72 |
+
tools_registered: int
|
| 73 |
+
tools_allowed: int
|
| 74 |
+
last_action: Optional[str]
|
| 75 |
+
last_action_time: Optional[str]
|
| 76 |
+
available_states: List[str]
|
| 77 |
+
uptime_seconds: Optional[float]
|
| 78 |
+
timestamp: str
|
| 79 |
+
|
| 80 |
+
class ChatResponse(BaseModel):
|
| 81 |
+
"""Chat response model"""
|
| 82 |
+
success: bool
|
| 83 |
+
response: str
|
| 84 |
+
agent: str
|
| 85 |
+
state: str
|
| 86 |
+
timestamp: str
|
| 87 |
+
session_id: Optional[str]
|
| 88 |
+
action_taken: Optional[str]
|
| 89 |
+
|
| 90 |
+
class HealthResponse(BaseModel):
|
| 91 |
+
"""Health check response model"""
|
| 92 |
+
status: str
|
| 93 |
+
stage: str
|
| 94 |
+
agent: str
|
| 95 |
+
uptime_seconds: Optional[float]
|
| 96 |
+
components: Dict[str, Any]
|
| 97 |
+
timestamp: str
|
| 98 |
+
|
| 99 |
+
# ========== Global State ==========
|
| 100 |
+
|
| 101 |
+
_brain_instance = None
|
| 102 |
+
_start_time = datetime.utcnow()
|
| 103 |
+
|
| 104 |
+
def get_brain_instance():
|
| 105 |
+
"""Get or create brain instance"""
|
| 106 |
+
global _brain_instance
|
| 107 |
+
if _brain_instance is None and BRAIN_AVAILABLE:
|
| 108 |
+
_brain_instance = get_brain()
|
| 109 |
+
return _brain_instance
|
| 110 |
+
|
| 111 |
+
def get_health_monitor_instance():
|
| 112 |
+
"""Get health monitor instance"""
|
| 113 |
+
if not HEALTH_MONITOR_AVAILABLE:
|
| 114 |
+
return None
|
| 115 |
+
try:
|
| 116 |
+
workspace = WORKSPACE_DIR if (WORKSPACE_DIR / ".openclaw").exists() else None
|
| 117 |
+
return get_health_monitor(workspace_path=workspace)
|
| 118 |
+
except Exception:
|
| 119 |
+
return None
|
| 120 |
+
|
| 121 |
+
# ========== Utility Functions ==========
|
| 122 |
+
|
| 123 |
+
def get_uptime_seconds() -> float:
|
| 124 |
+
"""Get uptime in seconds"""
|
| 125 |
+
return (datetime.utcnow() - _start_time).total_seconds()
|
| 126 |
+
|
| 127 |
+
# ========== API Endpoints ==========
|
| 128 |
+
|
| 129 |
+
@app.get("/", tags=["Root"])
|
| 130 |
+
async def root():
|
| 131 |
+
"""Root endpoint - API information"""
|
| 132 |
+
return {
|
| 133 |
+
"name": "HuggingClaw-Cain API",
|
| 134 |
+
"version": "1.0.0",
|
| 135 |
+
"status": "running",
|
| 136 |
+
"endpoints": {
|
| 137 |
+
"agents": "/api/agents",
|
| 138 |
+
"chat": "/api/chat",
|
| 139 |
+
"health": "/api/health",
|
| 140 |
+
"tools": "/api/tools",
|
| 141 |
+
"docs": "/docs"
|
| 142 |
+
},
|
| 143 |
+
"gradio_dashboard": "/gradio",
|
| 144 |
+
"timestamp": datetime.utcnow().isoformat() + "Z"
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
@app.get("/api/agents", response_model=AgentStatus, tags=["Agents"])
|
| 148 |
+
async def get_agents_status():
|
| 149 |
+
"""
|
| 150 |
+
Get current agent status and state information.
|
| 151 |
+
|
| 152 |
+
Returns information about Cain's current state, available tools,
|
| 153 |
+
and last actions performed.
|
| 154 |
+
"""
|
| 155 |
+
brain = get_brain_instance()
|
| 156 |
+
|
| 157 |
+
if brain is None:
|
| 158 |
+
raise HTTPException(status_code=503, detail="Brain not available")
|
| 159 |
+
|
| 160 |
+
try:
|
| 161 |
+
info = brain.get_info()
|
| 162 |
+
|
| 163 |
+
return AgentStatus(
|
| 164 |
+
agent_name=info.get("agent", "cain"),
|
| 165 |
+
role=info.get("role", "unknown"),
|
| 166 |
+
state=info.get("agent_state", "idle"),
|
| 167 |
+
brain_state=info.get("brain_state", "idle"),
|
| 168 |
+
legacy_mode=info.get("legacy_mode", False),
|
| 169 |
+
tools_registered=info.get("tools_registered", 0),
|
| 170 |
+
tools_allowed=info.get("tools_allowed", 0),
|
| 171 |
+
last_action=info.get("last_action"),
|
| 172 |
+
last_action_time=info.get("last_action_time"),
|
| 173 |
+
available_states=[s.value for s in BrainState],
|
| 174 |
+
uptime_seconds=get_uptime_seconds(),
|
| 175 |
+
timestamp=datetime.utcnow().isoformat() + "Z"
|
| 176 |
+
)
|
| 177 |
+
except Exception as e:
|
| 178 |
+
raise HTTPException(status_code=500, detail=f"Error getting agent status: {str(e)}")
|
| 179 |
+
|
| 180 |
+
@app.post("/api/chat", response_model=ChatResponse, tags=["Chat"])
|
| 181 |
+
async def chat(message: ChatMessage, background_tasks: BackgroundTasks):
|
| 182 |
+
"""
|
| 183 |
+
Send a chat message to Cain and get a response.
|
| 184 |
+
|
| 185 |
+
Processes the input message through Cain's conversation tool
|
| 186 |
+
and returns the agent's response with current state.
|
| 187 |
+
"""
|
| 188 |
+
brain = get_brain_instance()
|
| 189 |
+
|
| 190 |
+
if brain is None:
|
| 191 |
+
raise HTTPException(status_code=503, detail="Brain not available")
|
| 192 |
+
|
| 193 |
+
try:
|
| 194 |
+
# Check if conversation_process tool is available
|
| 195 |
+
if not brain.can_use_tool("conversation_process"):
|
| 196 |
+
# Fallback: return a simple echo response
|
| 197 |
+
response_text = f"Cain received: {message.message}"
|
| 198 |
+
action_taken = "echo"
|
| 199 |
+
else:
|
| 200 |
+
# Execute conversation process tool
|
| 201 |
+
result = brain.execute_tool("conversation_process", message.message)
|
| 202 |
+
|
| 203 |
+
if result.get("success"):
|
| 204 |
+
response_text = result.get("response", f"Processed: {message.message}")
|
| 205 |
+
action_taken = "conversation_process"
|
| 206 |
else:
|
| 207 |
+
response_text = f"Error processing message: {result.get('error', 'Unknown error')}"
|
| 208 |
+
action_taken = "error"
|
| 209 |
+
|
| 210 |
+
return ChatResponse(
|
| 211 |
+
success=True,
|
| 212 |
+
response=response_text,
|
| 213 |
+
agent=brain.agent_name,
|
| 214 |
+
state=brain.get_state(),
|
| 215 |
+
timestamp=datetime.utcnow().isoformat() + "Z",
|
| 216 |
+
session_id=message.session_id,
|
| 217 |
+
action_taken=action_taken
|
| 218 |
+
)
|
| 219 |
+
except Exception as e:
|
| 220 |
+
raise HTTPException(status_code=500, detail=f"Error processing chat: {str(e)}")
|
| 221 |
|
| 222 |
+
@app.get("/api/health", response_model=HealthResponse, tags=["Health"])
|
| 223 |
+
async def get_health():
|
| 224 |
+
"""
|
| 225 |
+
Get system health status.
|
| 226 |
|
| 227 |
+
Returns overall health status and component-level health checks.
|
| 228 |
+
"""
|
| 229 |
+
monitor = get_health_monitor_instance()
|
| 230 |
+
brain = get_brain_instance()
|
| 231 |
+
|
| 232 |
+
components = {}
|
| 233 |
+
|
| 234 |
+
# Brain component
|
| 235 |
+
if brain:
|
| 236 |
+
try:
|
| 237 |
+
health_result = brain.execute_tool("health_check")
|
| 238 |
+
components["brain"] = health_result
|
| 239 |
except Exception as e:
|
| 240 |
+
components["brain"] = {"status": "error", "error": str(e)}
|
| 241 |
+
else:
|
| 242 |
+
components["brain"] = {"status": "unavailable"}
|
| 243 |
+
|
| 244 |
+
# Health monitor components
|
| 245 |
+
if monitor:
|
| 246 |
+
try:
|
| 247 |
+
health_results = monitor.run_all_health_checks()
|
| 248 |
+
components["health_monitor"] = {
|
| 249 |
+
"overall_status": health_results.get("overall_status"),
|
| 250 |
+
"check_count": health_results.get("check_count"),
|
| 251 |
+
"components": {
|
| 252 |
+
name: {
|
| 253 |
+
"status": result["status"].value if isinstance(result["status"], HealthStatus) else result["status"],
|
| 254 |
+
"details": result.get("details", {})
|
| 255 |
+
}
|
| 256 |
+
for name, result in health_results.get("components", {}).items()
|
| 257 |
+
}
|
| 258 |
+
}
|
| 259 |
+
except Exception as e:
|
| 260 |
+
components["health_monitor"] = {"status": "error", "error": str(e)}
|
| 261 |
+
|
| 262 |
+
# Determine overall status
|
| 263 |
+
if brain:
|
| 264 |
+
brain_health = brain.execute_tool("health_check")
|
| 265 |
+
overall_status = brain_health.get("status", "unknown")
|
| 266 |
+
stage = brain_health.get("stage", "UNKNOWN")
|
| 267 |
+
agent = brain_health.get("agent", "cain")
|
| 268 |
+
else:
|
| 269 |
+
overall_status = "degraded"
|
| 270 |
+
stage = "BRAIN_UNAVAILABLE"
|
| 271 |
+
agent = "cain"
|
| 272 |
+
|
| 273 |
+
return HealthResponse(
|
| 274 |
+
status=overall_status,
|
| 275 |
+
stage=stage,
|
| 276 |
+
agent=agent,
|
| 277 |
+
uptime_seconds=get_uptime_seconds(),
|
| 278 |
+
components=components,
|
| 279 |
+
timestamp=datetime.utcnow().isoformat() + "Z"
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
@app.get("/api/tools", tags=["Tools"])
|
| 283 |
+
async def list_tools(category: Optional[str] = None):
|
| 284 |
+
"""
|
| 285 |
+
List available tools for the agent.
|
| 286 |
+
|
| 287 |
+
Optionally filter by category (infrastructure, ui, interaction, shared).
|
| 288 |
+
"""
|
| 289 |
+
brain = get_brain_instance()
|
| 290 |
+
|
| 291 |
+
if brain is None:
|
| 292 |
+
raise HTTPException(status_code=503, detail="Brain not available")
|
| 293 |
+
|
| 294 |
+
try:
|
| 295 |
+
# Get allowed tools
|
| 296 |
+
allowed_tools = brain.get_allowed_tools()
|
| 297 |
+
|
| 298 |
+
tools_info = []
|
| 299 |
+
for tool_name in allowed_tools:
|
| 300 |
+
if tool_name in brain.tools:
|
| 301 |
+
tool = brain.tools[tool_name]
|
| 302 |
+
tool_dict = tool.to_dict()
|
| 303 |
+
if category is None or tool_dict["category"] == category:
|
| 304 |
+
tools_info.append(tool_dict)
|
| 305 |
+
|
| 306 |
+
return {
|
| 307 |
+
"success": True,
|
| 308 |
+
"agent": brain.agent_name,
|
| 309 |
+
"tools_count": len(tools_info),
|
| 310 |
+
"tools": tools_info,
|
| 311 |
+
"timestamp": datetime.utcnow().isoformat() + "Z"
|
| 312 |
+
}
|
| 313 |
+
except Exception as e:
|
| 314 |
+
raise HTTPException(status_code=500, detail=f"Error listing tools: {str(e)}")
|
| 315 |
+
|
| 316 |
+
@app.post("/api/execute/{tool_name}", tags=["Tools"])
|
| 317 |
+
async def execute_tool(tool_name: str, params: Optional[Dict[str, Any]] = None):
|
| 318 |
+
"""
|
| 319 |
+
Execute a specific tool with optional parameters.
|
| 320 |
+
|
| 321 |
+
Requires permission check and state management.
|
| 322 |
+
"""
|
| 323 |
+
brain = get_brain_instance()
|
| 324 |
+
|
| 325 |
+
if brain is None:
|
| 326 |
+
raise HTTPException(status_code=503, detail="Brain not available")
|
| 327 |
+
|
| 328 |
+
if not brain.can_use_tool(tool_name):
|
| 329 |
+
raise HTTPException(
|
| 330 |
+
status_code=403,
|
| 331 |
+
detail=f"Permission denied or tool not found: {tool_name}"
|
| 332 |
+
)
|
| 333 |
+
|
| 334 |
+
try:
|
| 335 |
+
# Execute tool with params (if provided)
|
| 336 |
+
if params:
|
| 337 |
+
result = brain.execute_tool(tool_name, **params)
|
| 338 |
+
else:
|
| 339 |
+
result = brain.execute_tool(tool_name)
|
| 340 |
+
|
| 341 |
+
return {
|
| 342 |
+
"success": result.get("success", True),
|
| 343 |
+
"tool": tool_name,
|
| 344 |
+
"result": result,
|
| 345 |
+
"agent": brain.agent_name,
|
| 346 |
+
"state": brain.get_state(),
|
| 347 |
+
"timestamp": datetime.utcnow().isoformat() + "Z"
|
| 348 |
+
}
|
| 349 |
+
except Exception as e:
|
| 350 |
+
raise HTTPException(status_code=500, detail=f"Error executing tool: {str(e)}")
|
| 351 |
+
|
| 352 |
+
@app.get("/api/logs", tags=["Monitoring"])
|
| 353 |
+
async def get_logs(
|
| 354 |
+
level: Optional[str] = None,
|
| 355 |
+
component: Optional[str] = None,
|
| 356 |
+
limit: int = 100
|
| 357 |
+
):
|
| 358 |
+
"""
|
| 359 |
+
Get recent log entries from the health monitor.
|
| 360 |
+
|
| 361 |
+
Can filter by log level (debug, info, warning, error, critical)
|
| 362 |
+
and component name.
|
| 363 |
+
"""
|
| 364 |
+
monitor = get_health_monitor_instance()
|
| 365 |
+
|
| 366 |
+
if monitor is None:
|
| 367 |
+
return {
|
| 368 |
+
"success": False,
|
| 369 |
+
"error": "Health monitor not available",
|
| 370 |
+
"logs": []
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
try:
|
| 374 |
+
log_level = None if level is None or level == "all" else LogLevel(level)
|
| 375 |
+
log_component = None if component is None or component == "all" else component
|
| 376 |
+
|
| 377 |
+
logs = monitor.get_logs(level=log_level, component=log_component, limit=limit)
|
| 378 |
+
|
| 379 |
+
formatted_logs = []
|
| 380 |
+
for log in logs:
|
| 381 |
+
formatted_logs.append({
|
| 382 |
+
"timestamp": log["timestamp"],
|
| 383 |
+
"level": log["level"],
|
| 384 |
+
"component": log["component"],
|
| 385 |
+
"message": log["message"],
|
| 386 |
+
"details": log.get("details", {})
|
| 387 |
+
})
|
| 388 |
+
|
| 389 |
+
return {
|
| 390 |
+
"success": True,
|
| 391 |
+
"count": len(formatted_logs),
|
| 392 |
+
"logs": formatted_logs,
|
| 393 |
+
"filters": {"level": level, "component": component, "limit": limit},
|
| 394 |
+
"timestamp": datetime.utcnow().isoformat() + "Z"
|
| 395 |
+
}
|
| 396 |
+
except Exception as e:
|
| 397 |
+
raise HTTPException(status_code=500, detail=f"Error getting logs: {str(e)}")
|
| 398 |
+
|
| 399 |
+
@app.post("/api/reset", tags=["Administration"])
|
| 400 |
+
async def reset_agent():
|
| 401 |
+
"""
|
| 402 |
+
Reset the agent state to IDLE.
|
| 403 |
+
|
| 404 |
+
Use this to recover from error states.
|
| 405 |
+
"""
|
| 406 |
+
brain = get_brain_instance()
|
| 407 |
+
|
| 408 |
+
if brain is None:
|
| 409 |
+
raise HTTPException(status_code=503, detail="Brain not available")
|
| 410 |
+
|
| 411 |
+
try:
|
| 412 |
+
from brain_minimal import AgentState
|
| 413 |
+
brain.transition_to(AgentState.IDLE)
|
| 414 |
+
|
| 415 |
+
return {
|
| 416 |
+
"success": True,
|
| 417 |
+
"message": "Agent state reset to IDLE",
|
| 418 |
+
"state": brain.get_state(),
|
| 419 |
+
"timestamp": datetime.utcnow().isoformat() + "Z"
|
| 420 |
+
}
|
| 421 |
+
except Exception as e:
|
| 422 |
+
raise HTTPException(status_code=500, detail=f"Error resetting agent: {str(e)}")
|
| 423 |
+
|
| 424 |
+
# ========== Gradio Dashboard Mount ==========
|
| 425 |
+
|
| 426 |
+
# Mount Gradio dashboard at /gradio route
|
| 427 |
+
# Note: We import and mount it here to integrate with FastAPI
|
| 428 |
+
try:
|
| 429 |
+
import gradio as gr
|
| 430 |
+
|
| 431 |
+
# Import the dashboard creation function
|
| 432 |
+
sys.path.insert(0, str(Path.home()))
|
| 433 |
+
from gradio_dashboard import create_dashboard
|
| 434 |
+
|
| 435 |
+
# Create the Gradio app
|
| 436 |
+
gradio_app = create_dashboard()
|
| 437 |
+
|
| 438 |
+
# Mount the Gradio app as a sub-app
|
| 439 |
+
# We use gr.mount_gradio_app which integrates Gradio with FastAPI
|
| 440 |
+
app = gr.mount_gradio_app(app, gradio_app, path="/gradio")
|
| 441 |
+
|
| 442 |
+
print("[API] Gradio dashboard mounted at /gradio")
|
| 443 |
+
except ImportError:
|
| 444 |
+
print("[API] Warning: Gradio not available, dashboard not mounted")
|
| 445 |
+
except Exception as e:
|
| 446 |
+
print(f"[API] Warning: Could not mount Gradio dashboard: {e}")
|
| 447 |
+
|
| 448 |
+
# ========== Main Entry Point ==========
|
| 449 |
+
|
| 450 |
+
def main():
|
| 451 |
+
"""Main entry point for running the server"""
|
| 452 |
+
import argparse
|
| 453 |
+
|
| 454 |
+
parser = argparse.ArgumentParser(description="FastAPI Backend for HuggingClaw-Cain")
|
| 455 |
+
parser.add_argument(
|
| 456 |
+
"--host",
|
| 457 |
+
default=os.getenv("GRADIO_SERVER_NAME", "0.0.0.0"),
|
| 458 |
+
help="Host to bind to (default: from GRADIO_SERVER_NAME env or 0.0.0.0)"
|
| 459 |
+
)
|
| 460 |
+
parser.add_argument(
|
| 461 |
+
"--port",
|
| 462 |
+
type=int,
|
| 463 |
+
default=int(os.getenv("PORT", "7860")),
|
| 464 |
+
help="Port to bind to (default: from PORT env or 7860)"
|
| 465 |
+
)
|
| 466 |
+
parser.add_argument(
|
| 467 |
+
"--reload",
|
| 468 |
+
action="store_true",
|
| 469 |
+
help="Enable auto-reload for development"
|
| 470 |
+
)
|
| 471 |
+
|
| 472 |
+
args = parser.parse_args()
|
| 473 |
+
|
| 474 |
+
print(f"[API] Starting FastAPI server on {args.host}:{args.port}")
|
| 475 |
+
print(f"[API] Dashboard available at http://{args.host}:{args.port}/gradio")
|
| 476 |
+
print(f"[API] API docs available at http://{args.host}:{args.port}/docs")
|
| 477 |
+
|
| 478 |
+
uvicorn.run(
|
| 479 |
+
"app:app",
|
| 480 |
+
host=args.host,
|
| 481 |
+
port=args.port,
|
| 482 |
+
reload=args.reload
|
| 483 |
+
)
|
| 484 |
+
|
| 485 |
+
if __name__ == "__main__":
|
| 486 |
+
main()
|
gradio_dashboard.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
Cain System Health Dashboard - Gradio Interface
|
| 4 |
================================================
|
| 5 |
Provides real-time monitoring of Cain's internal operations including:
|
| 6 |
-
- Agent states from
|
| 7 |
- RBAC system status
|
| 8 |
- Cron job execution history
|
| 9 |
- Memory usage stats
|
|
@@ -17,20 +17,17 @@ import sys
|
|
| 17 |
from pathlib import Path
|
| 18 |
from datetime import datetime
|
| 19 |
import psutil
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
# Paths - detect workspace vs home directory
|
| 22 |
WORKSPACE_DIR = Path("/tmp/claude-workspace")
|
| 23 |
OPENCLAW_HOME = Path.home() / ".openclaw"
|
| 24 |
|
| 25 |
-
# Health monitor integration
|
| 26 |
-
sys.path.insert(0, str(WORKSPACE_DIR / ".openclaw"))
|
| 27 |
-
try:
|
| 28 |
-
from health_monitor import get_health_monitor, LogLevel, HealthStatus
|
| 29 |
-
HEALTH_MONITOR_AVAILABLE = True
|
| 30 |
-
except ImportError:
|
| 31 |
-
HEALTH_MONITOR_AVAILABLE = False
|
| 32 |
-
print("[Dashboard] Warning: health_monitor not available, using legacy mode")
|
| 33 |
-
|
| 34 |
# Use workspace if available (for local testing), otherwise home directory
|
| 35 |
if WORKSPACE_DIR.exists() and (WORKSPACE_DIR / ".openclaw").exists():
|
| 36 |
BASE_DIR = WORKSPACE_DIR / ".openclaw"
|
|
@@ -41,48 +38,133 @@ CRON_LOGS_DIR = BASE_DIR / "logs"
|
|
| 41 |
CRON_JOBS_FILE = BASE_DIR / "cron" / "jobs.json"
|
| 42 |
MEMORY_STATE_FILE = Path("/data/memory/state.json")
|
| 43 |
WORKSPACE_LOGS = BASE_DIR / "workspace"
|
| 44 |
-
AGENTS_DIR = BASE_DIR / "agents"
|
| 45 |
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
try:
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
-
#
|
| 59 |
return {
|
| 60 |
-
"agent_name":
|
| 61 |
-
"role":
|
| 62 |
-
"state":
|
| 63 |
-
"
|
| 64 |
-
"
|
| 65 |
-
"
|
| 66 |
-
"
|
| 67 |
-
"
|
| 68 |
-
"
|
| 69 |
-
"
|
| 70 |
-
"
|
|
|
|
|
|
|
| 71 |
}
|
| 72 |
except Exception as e:
|
| 73 |
return {
|
| 74 |
"agent_name": "cain",
|
| 75 |
"role": "interaction",
|
| 76 |
"state": "idle",
|
| 77 |
-
"error": f"Could not
|
| 78 |
-
"available_states": ["idle", "
|
| 79 |
"base_dir": str(BASE_DIR),
|
| 80 |
-
"
|
| 81 |
}
|
| 82 |
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
def get_cron_history():
|
| 85 |
-
"""Get recent cron job execution history"""
|
| 86 |
history = []
|
| 87 |
|
| 88 |
# Read jobs configuration
|
|
@@ -153,7 +235,7 @@ def get_cron_history():
|
|
| 153 |
|
| 154 |
|
| 155 |
def get_memory_stats():
|
| 156 |
-
"""Get memory usage statistics"""
|
| 157 |
stats = {
|
| 158 |
"system_memory": {},
|
| 159 |
"disk_usage": {},
|
|
@@ -216,7 +298,63 @@ def get_memory_stats():
|
|
| 216 |
return stats
|
| 217 |
|
| 218 |
|
| 219 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
CUSTOM_CSS = """
|
| 221 |
.status-box {
|
| 222 |
padding: 10px;
|
|
@@ -243,200 +381,123 @@ CUSTOM_CSS = """
|
|
| 243 |
"""
|
| 244 |
|
| 245 |
|
| 246 |
-
# ==========
|
| 247 |
-
|
| 248 |
-
def get_health_monitor_instance():
|
| 249 |
-
"""Get or create health monitor instance"""
|
| 250 |
-
if not HEALTH_MONITOR_AVAILABLE:
|
| 251 |
-
return None
|
| 252 |
-
|
| 253 |
-
try:
|
| 254 |
-
workspace = WORKSPACE_DIR if (WORKSPACE_DIR / ".openclaw").exists() else None
|
| 255 |
-
return get_health_monitor(workspace_path=workspace)
|
| 256 |
-
except Exception as e:
|
| 257 |
-
print(f"[Dashboard] Error getting health monitor: {e}")
|
| 258 |
-
return None
|
| 259 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
return {
|
| 268 |
-
"monitor_available": False,
|
| 269 |
-
"components": {
|
| 270 |
-
"brain": get_agent_states(),
|
| 271 |
-
"memory": get_memory_stats(),
|
| 272 |
-
"cron": {"history": get_cron_history()}
|
| 273 |
-
}
|
| 274 |
-
}
|
| 275 |
-
|
| 276 |
-
try:
|
| 277 |
-
# Run health checks
|
| 278 |
-
health_results = monitor.run_all_health_checks()
|
| 279 |
-
status_report = monitor.get_status_report(include_logs=False)
|
| 280 |
-
|
| 281 |
-
return {
|
| 282 |
-
"monitor_available": True,
|
| 283 |
-
"overall_status": health_results["overall_status"],
|
| 284 |
-
"uptime_seconds": health_results["uptime_seconds"],
|
| 285 |
-
"check_count": health_results["check_count"],
|
| 286 |
-
"last_check": health_results["timestamp"],
|
| 287 |
-
"components": {
|
| 288 |
-
name: {
|
| 289 |
-
"status": result["status"].value if isinstance(result["status"], HealthStatus) else result["status"],
|
| 290 |
-
"details": result.get("details", {})
|
| 291 |
-
}
|
| 292 |
-
for name, result in health_results["components"].items()
|
| 293 |
-
}
|
| 294 |
-
}
|
| 295 |
-
except Exception as e:
|
| 296 |
-
return {
|
| 297 |
-
"monitor_available": True,
|
| 298 |
-
"error": str(e),
|
| 299 |
-
"components": {}
|
| 300 |
-
}
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
def get_health_logs(level: str = "all", component: str = "all", limit: int = 100):
|
| 304 |
-
"""Get recent log entries from health monitor"""
|
| 305 |
-
monitor = get_health_monitor_instance()
|
| 306 |
-
|
| 307 |
-
if monitor is None:
|
| 308 |
-
# Return empty if monitor not available
|
| 309 |
-
return []
|
| 310 |
-
|
| 311 |
-
try:
|
| 312 |
-
log_level = None if level == "all" else LogLevel(level)
|
| 313 |
-
log_component = None if component == "all" else component
|
| 314 |
-
|
| 315 |
-
logs = monitor.get_logs(level=log_level, component=log_component, limit=limit)
|
| 316 |
-
|
| 317 |
-
# Format for display
|
| 318 |
-
formatted = []
|
| 319 |
-
for log in logs:
|
| 320 |
-
formatted.append({
|
| 321 |
-
"timestamp": log["timestamp"],
|
| 322 |
-
"level": log["level"],
|
| 323 |
-
"component": log["component"],
|
| 324 |
-
"message": log["message"],
|
| 325 |
-
"details": json.dumps(log.get("details", {}), indent=2)[:200]
|
| 326 |
-
})
|
| 327 |
-
|
| 328 |
-
return formatted
|
| 329 |
-
except Exception as e:
|
| 330 |
-
return [{"error": str(e)}]
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
def get_health_metrics_summary():
|
| 334 |
-
"""Get summary of health metrics"""
|
| 335 |
-
monitor = get_health_monitor_instance()
|
| 336 |
-
|
| 337 |
-
if monitor is None:
|
| 338 |
-
return {}
|
| 339 |
-
|
| 340 |
-
try:
|
| 341 |
-
metrics = {}
|
| 342 |
-
|
| 343 |
-
# Get overall health metric
|
| 344 |
-
overall = monitor.get_metrics("overall_health", limit=1)
|
| 345 |
-
if overall:
|
| 346 |
-
metrics["overall"] = overall[0]
|
| 347 |
|
| 348 |
-
return metrics
|
| 349 |
-
except Exception as e:
|
| 350 |
-
return {"error": str(e)}
|
| 351 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
|
| 353 |
-
|
| 354 |
-
"""Generate a text status report"""
|
| 355 |
-
monitor = get_health_monitor_instance()
|
| 356 |
|
| 357 |
-
|
| 358 |
-
return "Health monitor not available. Please check the installation."
|
| 359 |
|
| 360 |
-
try:
|
| 361 |
-
return monitor.get_summary_report()
|
| 362 |
-
except Exception as e:
|
| 363 |
-
return f"Error generating report: {e}"
|
| 364 |
|
|
|
|
| 365 |
|
| 366 |
-
def
|
| 367 |
-
"""
|
| 368 |
-
|
|
|
|
| 369 |
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
"Health monitor not available",
|
| 375 |
-
f"Last updated: {datetime.utcnow().isoformat()}Z"
|
| 376 |
)
|
| 377 |
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
except Exception as e:
|
| 391 |
-
return (
|
| 392 |
-
json.dumps({"error": str(e)}, indent=2),
|
| 393 |
-
[],
|
| 394 |
-
f"Error: {e}",
|
| 395 |
-
f"Last updated: {datetime.utcnow().isoformat()}Z"
|
| 396 |
)
|
| 397 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
|
| 399 |
-
def
|
| 400 |
-
|
| 401 |
-
|
|
|
|
| 402 |
|
|
|
|
|
|
|
| 403 |
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
if not history:
|
| 407 |
-
return []
|
| 408 |
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
table_data.append([
|
| 412 |
-
entry.get("job_id", "N/A"),
|
| 413 |
-
entry.get("description", "")[:30],
|
| 414 |
-
entry.get("timestamp", "N/A"),
|
| 415 |
-
entry.get("action", "N/A"),
|
| 416 |
-
entry.get("status", "N/A"),
|
| 417 |
-
"β" if entry.get("success", True) else "β"
|
| 418 |
-
])
|
| 419 |
-
return table_data
|
| 420 |
|
|
|
|
| 421 |
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
|
|
|
|
|
|
| 425 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 426 |
|
| 427 |
-
|
| 428 |
-
"""Refresh all dashboard data"""
|
| 429 |
-
states = get_agent_states()
|
| 430 |
-
history = get_cron_history()
|
| 431 |
-
stats = get_memory_stats()
|
| 432 |
|
| 433 |
-
return (
|
| 434 |
-
format_agent_states(states),
|
| 435 |
-
format_cron_history(history),
|
| 436 |
-
format_memory_stats(stats),
|
| 437 |
-
f"Last updated: {datetime.utcnow().isoformat()}Z"
|
| 438 |
-
)
|
| 439 |
|
|
|
|
| 440 |
|
| 441 |
def create_dashboard():
|
| 442 |
"""Create the Gradio dashboard interface"""
|
|
@@ -445,9 +506,14 @@ def create_dashboard():
|
|
| 445 |
|
| 446 |
gr.Markdown("# π± Cain System Health Dashboard")
|
| 447 |
gr.Markdown("Real-time monitoring of HuggingClaw-Cain's internal operations")
|
|
|
|
| 448 |
|
| 449 |
with gr.Tabs():
|
| 450 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 451 |
with gr.Tab("π₯ System Health"):
|
| 452 |
gr.Markdown("### Component Health Overview")
|
| 453 |
|
|
@@ -511,9 +577,8 @@ def create_dashboard():
|
|
| 511 |
gr.Markdown("""
|
| 512 |
**State Descriptions:**
|
| 513 |
- **idle**: Agent is ready and waiting
|
| 514 |
-
- **
|
| 515 |
-
- **
|
| 516 |
-
- **waiting**: Agent is waiting for external response
|
| 517 |
- **error**: Agent encountered an error
|
| 518 |
""")
|
| 519 |
|
|
@@ -598,32 +663,6 @@ def create_dashboard():
|
|
| 598 |
)
|
| 599 |
|
| 600 |
# Refresh for System Health tab
|
| 601 |
-
def refresh_system_health(log_level="all", log_component="all"):
|
| 602 |
-
health_data, logs, report, timestamp = run_health_check_and_refresh()
|
| 603 |
-
|
| 604 |
-
# Filter logs
|
| 605 |
-
filtered_logs = logs
|
| 606 |
-
if log_level != "all":
|
| 607 |
-
filtered_logs = [l for l in logs if l.get("level") == log_level]
|
| 608 |
-
if log_component != "all":
|
| 609 |
-
filtered_logs = [l for l in filtered_logs if l.get("component") == log_component]
|
| 610 |
-
|
| 611 |
-
log_rows = [
|
| 612 |
-
[l.get("timestamp", ""), l.get("level", ""), l.get("component", ""),
|
| 613 |
-
l.get("message", ""), l.get("details", "")]
|
| 614 |
-
for l in filtered_logs[:100]
|
| 615 |
-
]
|
| 616 |
-
|
| 617 |
-
return health_data, log_rows, report, timestamp
|
| 618 |
-
|
| 619 |
-
# Auto-load system health data on tab load
|
| 620 |
-
app.load(
|
| 621 |
-
fn=lambda: refresh_system_health(),
|
| 622 |
-
inputs=[],
|
| 623 |
-
outputs=[health_json, logs_table, health_report, last_updated]
|
| 624 |
-
)
|
| 625 |
-
|
| 626 |
-
# Manual refresh for system health
|
| 627 |
system_health_refresh_btn = gr.Button("π₯ Refresh Health", visible=False)
|
| 628 |
system_health_refresh_btn.click(
|
| 629 |
fn=refresh_system_health,
|
|
@@ -631,7 +670,7 @@ def create_dashboard():
|
|
| 631 |
outputs=[health_json, logs_table, health_report, last_updated]
|
| 632 |
)
|
| 633 |
|
| 634 |
-
# Log filter change
|
| 635 |
log_level_filter.change(
|
| 636 |
fn=refresh_system_health,
|
| 637 |
inputs=[log_level_filter, log_component_filter],
|
|
@@ -654,11 +693,16 @@ def create_dashboard():
|
|
| 654 |
|
| 655 |
|
| 656 |
if __name__ == "__main__":
|
| 657 |
-
# Create and launch the dashboard
|
| 658 |
dashboard = create_dashboard()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 659 |
dashboard.launch(
|
| 660 |
-
server_name=
|
| 661 |
-
server_port=
|
| 662 |
share=False,
|
| 663 |
theme=gr.themes.Soft(primary_hue="blue"),
|
| 664 |
css=CUSTOM_CSS
|
|
|
|
| 3 |
Cain System Health Dashboard - Gradio Interface
|
| 4 |
================================================
|
| 5 |
Provides real-time monitoring of Cain's internal operations including:
|
| 6 |
+
- Agent states from /api/agents endpoint
|
| 7 |
- RBAC system status
|
| 8 |
- Cron job execution history
|
| 9 |
- Memory usage stats
|
|
|
|
| 17 |
from pathlib import Path
|
| 18 |
from datetime import datetime
|
| 19 |
import psutil
|
| 20 |
+
import requests
|
| 21 |
+
|
| 22 |
+
# ========== Configuration ==========
|
| 23 |
+
|
| 24 |
+
# API Base URL - internal endpoint
|
| 25 |
+
API_BASE_URL = os.getenv("CAIN_API_URL", "http://127.0.0.1:7860")
|
| 26 |
|
| 27 |
# Paths - detect workspace vs home directory
|
| 28 |
WORKSPACE_DIR = Path("/tmp/claude-workspace")
|
| 29 |
OPENCLAW_HOME = Path.home() / ".openclaw"
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
# Use workspace if available (for local testing), otherwise home directory
|
| 32 |
if WORKSPACE_DIR.exists() and (WORKSPACE_DIR / ".openclaw").exists():
|
| 33 |
BASE_DIR = WORKSPACE_DIR / ".openclaw"
|
|
|
|
| 38 |
CRON_JOBS_FILE = BASE_DIR / "cron" / "jobs.json"
|
| 39 |
MEMORY_STATE_FILE = Path("/data/memory/state.json")
|
| 40 |
WORKSPACE_LOGS = BASE_DIR / "workspace"
|
|
|
|
| 41 |
|
| 42 |
|
| 43 |
+
# ========== API Client Functions ==========
|
| 44 |
+
|
| 45 |
+
def fetch_api(endpoint: str, method: str = "GET", data: dict = None) -> dict:
|
| 46 |
+
"""
|
| 47 |
+
Fetch data from the internal API endpoint.
|
| 48 |
+
|
| 49 |
+
Args:
|
| 50 |
+
endpoint: API endpoint path (e.g., "/api/agents")
|
| 51 |
+
method: HTTP method (GET, POST)
|
| 52 |
+
data: Request body for POST requests
|
| 53 |
+
|
| 54 |
+
Returns:
|
| 55 |
+
JSON response as dict
|
| 56 |
+
"""
|
| 57 |
+
url = f"{API_BASE_URL}{endpoint}"
|
| 58 |
+
|
| 59 |
try:
|
| 60 |
+
if method == "GET":
|
| 61 |
+
response = requests.get(url, timeout=5)
|
| 62 |
+
elif method == "POST":
|
| 63 |
+
response = requests.post(url, json=data, timeout=30)
|
| 64 |
+
else:
|
| 65 |
+
return {"error": f"Unsupported method: {method}"}
|
| 66 |
+
|
| 67 |
+
response.raise_for_status()
|
| 68 |
+
return response.json()
|
| 69 |
+
except requests.exceptions.RequestException as e:
|
| 70 |
+
return {"error": f"API request failed: {str(e)}"}
|
| 71 |
+
except json.JSONDecodeError:
|
| 72 |
+
return {"error": "Invalid JSON response"}
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
# ========== Dashboard Functions (API-based) ==========
|
| 76 |
|
| 77 |
+
def get_agent_states():
|
| 78 |
+
"""Get current agent states from /api/agents endpoint"""
|
| 79 |
+
try:
|
| 80 |
+
data = fetch_api("/api/agents")
|
| 81 |
+
|
| 82 |
+
if "error" in data:
|
| 83 |
+
return {
|
| 84 |
+
"agent_name": "cain",
|
| 85 |
+
"role": "interaction",
|
| 86 |
+
"state": "idle",
|
| 87 |
+
"error": data["error"],
|
| 88 |
+
"available_states": ["idle", "processing", "success", "error"],
|
| 89 |
+
"base_dir": str(BASE_DIR),
|
| 90 |
+
"api_error": True
|
| 91 |
+
}
|
| 92 |
|
| 93 |
+
# Return the data from API
|
| 94 |
return {
|
| 95 |
+
"agent_name": data.get("agent_name", "cain"),
|
| 96 |
+
"role": data.get("role", "unknown"),
|
| 97 |
+
"state": data.get("state", "idle"),
|
| 98 |
+
"brain_state": data.get("brain_state", "idle"),
|
| 99 |
+
"legacy_mode": data.get("legacy_mode", False),
|
| 100 |
+
"tools_registered": data.get("tools_registered", 0),
|
| 101 |
+
"tools_allowed": data.get("tools_allowed", 0),
|
| 102 |
+
"last_action": data.get("last_action", "N/A"),
|
| 103 |
+
"last_action_time": data.get("last_action_time", "N/A"),
|
| 104 |
+
"available_states": data.get("available_states", []),
|
| 105 |
+
"uptime_seconds": data.get("uptime_seconds"),
|
| 106 |
+
"timestamp": data.get("timestamp"),
|
| 107 |
+
"base_dir": str(BASE_DIR)
|
| 108 |
}
|
| 109 |
except Exception as e:
|
| 110 |
return {
|
| 111 |
"agent_name": "cain",
|
| 112 |
"role": "interaction",
|
| 113 |
"state": "idle",
|
| 114 |
+
"error": f"Could not fetch agent states: {str(e)}",
|
| 115 |
+
"available_states": ["idle", "processing", "success", "error"],
|
| 116 |
"base_dir": str(BASE_DIR),
|
| 117 |
+
"api_error": True
|
| 118 |
}
|
| 119 |
|
| 120 |
|
| 121 |
+
def get_system_health():
|
| 122 |
+
"""Get system health from /api/health endpoint"""
|
| 123 |
+
try:
|
| 124 |
+
data = fetch_api("/api/health")
|
| 125 |
+
|
| 126 |
+
if "error" in data:
|
| 127 |
+
return {
|
| 128 |
+
"monitor_available": False,
|
| 129 |
+
"error": data["error"]
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
return data
|
| 133 |
+
except Exception as e:
|
| 134 |
+
return {
|
| 135 |
+
"monitor_available": False,
|
| 136 |
+
"error": f"Could not fetch health data: {str(e)}"
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def chat_with_agent(message: str, session_id: str = ""):
|
| 141 |
+
"""Send a chat message to the agent via /api/chat endpoint"""
|
| 142 |
+
try:
|
| 143 |
+
payload = {
|
| 144 |
+
"message": message
|
| 145 |
+
}
|
| 146 |
+
if session_id:
|
| 147 |
+
payload["session_id"] = session_id
|
| 148 |
+
|
| 149 |
+
data = fetch_api("/api/chat", method="POST", data=payload)
|
| 150 |
+
|
| 151 |
+
if "error" in data:
|
| 152 |
+
return f"Error: {data['error']}"
|
| 153 |
+
|
| 154 |
+
if data.get("success"):
|
| 155 |
+
response = data.get("response", "")
|
| 156 |
+
metadata = f"\n\n[Agent: {data.get('agent', 'cain')} | State: {data.get('state', 'unknown')}]"
|
| 157 |
+
return response + metadata
|
| 158 |
+
else:
|
| 159 |
+
return f"Error: {data.get('response', 'Unknown error')}"
|
| 160 |
+
except Exception as e:
|
| 161 |
+
return f"Error sending message: {str(e)}"
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
# ========== Legacy Functions (Fallback) ==========
|
| 165 |
+
|
| 166 |
def get_cron_history():
|
| 167 |
+
"""Get recent cron job execution history (legacy, file-based)"""
|
| 168 |
history = []
|
| 169 |
|
| 170 |
# Read jobs configuration
|
|
|
|
| 235 |
|
| 236 |
|
| 237 |
def get_memory_stats():
|
| 238 |
+
"""Get memory usage statistics (legacy, system-based)"""
|
| 239 |
stats = {
|
| 240 |
"system_memory": {},
|
| 241 |
"disk_usage": {},
|
|
|
|
| 298 |
return stats
|
| 299 |
|
| 300 |
|
| 301 |
+
# ========== API-based Log Functions ==========
|
| 302 |
+
|
| 303 |
+
def get_health_logs(level: str = "all", component: str = "all", limit: int = 100):
|
| 304 |
+
"""Get recent log entries from health monitor via API"""
|
| 305 |
+
try:
|
| 306 |
+
params = []
|
| 307 |
+
if level and level != "all":
|
| 308 |
+
params.append(f"level={level}")
|
| 309 |
+
if component and component != "all":
|
| 310 |
+
params.append(f"component={component}")
|
| 311 |
+
params.append(f"limit={limit}")
|
| 312 |
+
|
| 313 |
+
endpoint = f"/api/logs?{'&'.join(params)}"
|
| 314 |
+
data = fetch_api(endpoint)
|
| 315 |
+
|
| 316 |
+
if "error" in data:
|
| 317 |
+
return []
|
| 318 |
+
if not data.get("success"):
|
| 319 |
+
return []
|
| 320 |
+
|
| 321 |
+
return data.get("logs", [])
|
| 322 |
+
except Exception as e:
|
| 323 |
+
return [{"error": str(e)}]
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
# ========== Formatting Functions ==========
|
| 327 |
+
|
| 328 |
+
def format_agent_states(states):
|
| 329 |
+
"""Format agent states for JSON display"""
|
| 330 |
+
return json.dumps(states, indent=2)
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
def format_cron_history(history):
|
| 334 |
+
"""Format cron history for table display"""
|
| 335 |
+
if not history:
|
| 336 |
+
return []
|
| 337 |
+
|
| 338 |
+
table_data = []
|
| 339 |
+
for entry in history:
|
| 340 |
+
table_data.append([
|
| 341 |
+
entry.get("job_id", "N/A"),
|
| 342 |
+
entry.get("description", "")[:30],
|
| 343 |
+
entry.get("timestamp", "N/A"),
|
| 344 |
+
entry.get("action", "N/A"),
|
| 345 |
+
entry.get("status", "N/A"),
|
| 346 |
+
"β" if entry.get("success", True) else "β"
|
| 347 |
+
])
|
| 348 |
+
return table_data
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
def format_memory_stats(stats):
|
| 352 |
+
"""Format memory stats for JSON display"""
|
| 353 |
+
return json.dumps(stats, indent=2)
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
# ========== Custom CSS ==========
|
| 357 |
+
|
| 358 |
CUSTOM_CSS = """
|
| 359 |
.status-box {
|
| 360 |
padding: 10px;
|
|
|
|
| 381 |
"""
|
| 382 |
|
| 383 |
|
| 384 |
+
# ========== Refresh Functions ==========
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 385 |
|
| 386 |
+
def refresh_all():
|
| 387 |
+
"""Refresh all dashboard data using API endpoints"""
|
| 388 |
+
states = get_agent_states()
|
| 389 |
+
history = get_cron_history()
|
| 390 |
+
stats = get_memory_stats()
|
| 391 |
|
| 392 |
+
return (
|
| 393 |
+
format_agent_states(states),
|
| 394 |
+
format_cron_history(history),
|
| 395 |
+
format_memory_stats(stats),
|
| 396 |
+
f"Last updated: {datetime.utcnow().isoformat()}Z"
|
| 397 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
|
|
|
|
|
|
|
|
|
|
| 399 |
|
| 400 |
+
def refresh_system_health(log_level="all", log_component="all"):
|
| 401 |
+
"""Refresh system health data using API endpoints"""
|
| 402 |
+
# Get health data
|
| 403 |
+
health_data = get_system_health()
|
| 404 |
+
|
| 405 |
+
# Get logs
|
| 406 |
+
logs = get_health_logs(level=log_level, component=log_component, limit=100)
|
| 407 |
+
|
| 408 |
+
# Format for display
|
| 409 |
+
health_json = json.dumps(health_data, indent=2, default=str)
|
| 410 |
+
|
| 411 |
+
# Format logs for table
|
| 412 |
+
log_rows = [
|
| 413 |
+
[l.get("timestamp", ""), l.get("level", ""), l.get("component", ""),
|
| 414 |
+
l.get("message", ""), json.dumps(l.get("details", {}), indent=2)[:200]]
|
| 415 |
+
for l in logs[:100]
|
| 416 |
+
]
|
| 417 |
+
|
| 418 |
+
# Generate status report
|
| 419 |
+
if "error" in health_data:
|
| 420 |
+
report = f"Error: {health_data['error']}"
|
| 421 |
+
elif health_data.get("monitor_available"):
|
| 422 |
+
status = health_data.get("status", "unknown")
|
| 423 |
+
stage = health_data.get("stage", "UNKNOWN")
|
| 424 |
+
uptime = health_data.get("uptime_seconds", 0)
|
| 425 |
+
report = f"Status: {status}\nStage: {stage}\nUptime: {uptime:.0f}s\n"
|
| 426 |
+
report += f"Components: {len(health_data.get('components', {}))} monitored"
|
| 427 |
+
else:
|
| 428 |
+
report = "Health monitor not available"
|
| 429 |
|
| 430 |
+
timestamp = f"Last updated: {datetime.utcnow().isoformat()}Z"
|
|
|
|
|
|
|
| 431 |
|
| 432 |
+
return health_json, log_rows, report, timestamp
|
|
|
|
| 433 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
|
| 435 |
+
# ========== Chat Interface ==========
|
| 436 |
|
| 437 |
+
def create_chat_interface():
|
| 438 |
+
"""Create the chat interface component"""
|
| 439 |
+
with gr.Column() as chat_col:
|
| 440 |
+
gr.Markdown("### π¬ Chat with Cain")
|
| 441 |
|
| 442 |
+
chatbot = gr.Chatbot(
|
| 443 |
+
label="Conversation",
|
| 444 |
+
height=300,
|
| 445 |
+
show_copy_button=True
|
|
|
|
|
|
|
| 446 |
)
|
| 447 |
|
| 448 |
+
with gr.Row():
|
| 449 |
+
chat_input = gr.Textbox(
|
| 450 |
+
label="Message",
|
| 451 |
+
placeholder="Type a message to Cain...",
|
| 452 |
+
scale=4,
|
| 453 |
+
lines=1
|
| 454 |
+
)
|
| 455 |
+
send_btn = gr.Button("Send", variant="primary", scale=1)
|
| 456 |
+
|
| 457 |
+
session_id = gr.Textbox(
|
| 458 |
+
label="Session ID (optional)",
|
| 459 |
+
placeholder="Leave empty for new session"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 460 |
)
|
| 461 |
|
| 462 |
+
gr.Markdown("""
|
| 463 |
+
**Instructions:**
|
| 464 |
+
- Type a message and click Send to chat with Cain
|
| 465 |
+
- Cain will process your message and respond
|
| 466 |
+
- Use a Session ID to continue a previous conversation
|
| 467 |
+
""")
|
| 468 |
|
| 469 |
+
def handle_send(message, session, history):
|
| 470 |
+
"""Handle sending a chat message"""
|
| 471 |
+
if not message.strip():
|
| 472 |
+
return history, message, session
|
| 473 |
|
| 474 |
+
# Add user message to history
|
| 475 |
+
history = history + [[message, None]]
|
| 476 |
|
| 477 |
+
# Get response from API
|
| 478 |
+
response = chat_with_agent(message, session)
|
|
|
|
|
|
|
| 479 |
|
| 480 |
+
# Update history with response
|
| 481 |
+
history[-1][1] = response
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 482 |
|
| 483 |
+
return history, "", session
|
| 484 |
|
| 485 |
+
send_btn.click(
|
| 486 |
+
fn=handle_send,
|
| 487 |
+
inputs=[chat_input, session_id, chatbot],
|
| 488 |
+
outputs=[chatbot, chat_input, session_id]
|
| 489 |
+
)
|
| 490 |
|
| 491 |
+
chat_input.submit(
|
| 492 |
+
fn=handle_send,
|
| 493 |
+
inputs=[chat_input, session_id, chatbot],
|
| 494 |
+
outputs=[chatbot, chat_input, session_id]
|
| 495 |
+
)
|
| 496 |
|
| 497 |
+
return chat_col
|
|
|
|
|
|
|
|
|
|
|
|
|
| 498 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 499 |
|
| 500 |
+
# ========== Dashboard Creation ==========
|
| 501 |
|
| 502 |
def create_dashboard():
|
| 503 |
"""Create the Gradio dashboard interface"""
|
|
|
|
| 506 |
|
| 507 |
gr.Markdown("# π± Cain System Health Dashboard")
|
| 508 |
gr.Markdown("Real-time monitoring of HuggingClaw-Cain's internal operations")
|
| 509 |
+
gr.Markdown(f"**API Endpoint:** `{API_BASE_URL}`")
|
| 510 |
|
| 511 |
with gr.Tabs():
|
| 512 |
+
# Chat Tab (NEW)
|
| 513 |
+
with gr.Tab("π¬ Chat"):
|
| 514 |
+
create_chat_interface()
|
| 515 |
+
|
| 516 |
+
# System Health Tab
|
| 517 |
with gr.Tab("π₯ System Health"):
|
| 518 |
gr.Markdown("### Component Health Overview")
|
| 519 |
|
|
|
|
| 577 |
gr.Markdown("""
|
| 578 |
**State Descriptions:**
|
| 579 |
- **idle**: Agent is ready and waiting
|
| 580 |
+
- **processing**: Agent is actively processing a task
|
| 581 |
+
- **success**: Agent completed a task successfully
|
|
|
|
| 582 |
- **error**: Agent encountered an error
|
| 583 |
""")
|
| 584 |
|
|
|
|
| 663 |
)
|
| 664 |
|
| 665 |
# Refresh for System Health tab
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 666 |
system_health_refresh_btn = gr.Button("π₯ Refresh Health", visible=False)
|
| 667 |
system_health_refresh_btn.click(
|
| 668 |
fn=refresh_system_health,
|
|
|
|
| 670 |
outputs=[health_json, logs_table, health_report, last_updated]
|
| 671 |
)
|
| 672 |
|
| 673 |
+
# Log filter change handlers
|
| 674 |
log_level_filter.change(
|
| 675 |
fn=refresh_system_health,
|
| 676 |
inputs=[log_level_filter, log_component_filter],
|
|
|
|
| 693 |
|
| 694 |
|
| 695 |
if __name__ == "__main__":
|
| 696 |
+
# Create and launch the dashboard (standalone mode, not mounted)
|
| 697 |
dashboard = create_dashboard()
|
| 698 |
+
|
| 699 |
+
# Get environment variables with fallbacks
|
| 700 |
+
server_name = os.getenv("GRADIO_SERVER_NAME", "0.0.0.0")
|
| 701 |
+
server_port = int(os.getenv("PORT", "7861")) # Default to 7861 for standalone
|
| 702 |
+
|
| 703 |
dashboard.launch(
|
| 704 |
+
server_name=server_name,
|
| 705 |
+
server_port=server_port,
|
| 706 |
share=False,
|
| 707 |
theme=gr.themes.Soft(primary_hue="blue"),
|
| 708 |
css=CUSTOM_CSS
|
requirements.txt
CHANGED
|
@@ -1 +1,4 @@
|
|
| 1 |
huggingface_hub>=0.24.5 # Force rebuild 2026-02-11
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
huggingface_hub>=0.24.5 # Force rebuild 2026-02-11
|
| 2 |
+
fastapi>=0.104.0
|
| 3 |
+
uvicorn[standard]>=0.24.0
|
| 4 |
+
requests>=2.31.0
|
scripts/entrypoint.sh
CHANGED
|
@@ -66,12 +66,15 @@ if [ -f /app/openclaw/.version ]; then
|
|
| 66 |
echo "[entrypoint] OpenClaw version: $OPENCLAW_VERSION"
|
| 67 |
fi
|
| 68 |
|
| 69 |
-
# ββ Start Gradio Dashboard (port
|
| 70 |
-
echo "[entrypoint] Starting
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
echo "[entrypoint] OpenClaw version: $OPENCLAW_VERSION"
|
| 67 |
fi
|
| 68 |
|
| 69 |
+
# ββ Start FastAPI Backend with Gradio Dashboard (port 7860) ββββββββββββββββββ
|
| 70 |
+
echo "[entrypoint] Starting FastAPI Backend with Gradio Dashboard on port 7860..."
|
| 71 |
+
echo "[entrypoint] Environment: GRADIO_SERVER_NAME=${GRADIO_SERVER_NAME:-0.0.0.0} PORT=${PORT:-7860}"
|
| 72 |
+
|
| 73 |
+
# Set default environment variables if not already set
|
| 74 |
+
export GRADIO_SERVER_NAME=${GRADIO_SERVER_NAME:-0.0.0.0}
|
| 75 |
+
export PORT=${PORT:-7860}
|
| 76 |
+
export CAIN_API_URL="http://127.0.0.1:${PORT}"
|
| 77 |
+
|
| 78 |
+
# Start the FastAPI app which includes the mounted Gradio dashboard
|
| 79 |
+
# The app.py now runs the FastAPI server with /gradio endpoint for the dashboard
|
| 80 |
+
exec python3 -u /home/node/app.py
|