Spaces:
Sleeping
Sleeping
fix: remove all Gradio code and references
Browse files- Deleted gradio_dashboard.py, app_analytics.py, app_broken.py
- Deleted scripts/configure_space.py (Gradio cleanup script)
- Removed Gradio health checks from health_monitor.py
- Cleaned entrypoint.sh, README.md, SOUL.md of Gradio references
- Cain uses sdk:docker + FastAPI only, no Gradio dependency
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- CLAUDE.md +1 -1
- README.md +2 -2
- app_analytics.py +0 -281
- app_broken.py +0 -555
- gradio_dashboard.py +0 -1199
- openclaw/.openclaw/health_monitor.py +2 -33
- scripts/configure_space.py +0 -59
- scripts/entrypoint.sh +5 -7
- workspace-templates/SOUL.md +1 -2
CLAUDE.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
|
| 3 |
## Architecture
|
| 4 |
- Cain is a child agent in the HuggingClaw World family system
|
| 5 |
-
- Runs as an OpenClaw instance on HuggingFace Spaces (sdk: docker, NOT gradio)
|
| 6 |
- Space ID: tao-shen/HuggingClaw-Cain
|
| 7 |
- Dataset ID: tao-shen/HuggingClaw-Cain-data
|
| 8 |
|
|
|
|
| 2 |
|
| 3 |
## Architecture
|
| 4 |
- Cain is a child agent in the HuggingClaw World family system
|
| 5 |
+
- Runs as an OpenClaw instance on HuggingFace Spaces (sdk: docker, NOT using gradio sdk)
|
| 6 |
- Space ID: tao-shen/HuggingClaw-Cain
|
| 7 |
- Dataset ID: tao-shen/HuggingClaw-Cain-data
|
| 8 |
|
README.md
CHANGED
|
@@ -184,13 +184,13 @@ HuggingClaw-Cain uses a modern three-tier architecture with real-time WebSocket
|
|
| 184 |
β β - AgentThought dataclass β β
|
| 185 |
β β - emit_agent_thought() function β β
|
| 186 |
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
|
| 187 |
-
β β
|
| 188 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 189 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 190 |
β Backend Layer β
|
| 191 |
β ββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββ β
|
| 192 |
β β app.py β β brain_minimal.py β β
|
| 193 |
-
β β -
|
| 194 |
β β - Chat handler β β - Memory management β β
|
| 195 |
β β - WS endpoints β β - Tool execution β β
|
| 196 |
β ββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββ β
|
|
|
|
| 184 |
β β - AgentThought dataclass β β
|
| 185 |
β β - emit_agent_thought() function β β
|
| 186 |
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
|
| 187 |
+
β β API Routes β
|
| 188 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 189 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 190 |
β Backend Layer β
|
| 191 |
β ββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββ β
|
| 192 |
β β app.py β β brain_minimal.py β β
|
| 193 |
+
β β - Web UI β β - Conversation processing β β
|
| 194 |
β β - Chat handler β β - Memory management β β
|
| 195 |
β β - WS endpoints β β - Tool execution β β
|
| 196 |
β ββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββ β
|
app_analytics.py
DELETED
|
@@ -1,281 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Chat Analytics Dashboard for HuggingClaw Cain
|
| 4 |
-
|
| 5 |
-
Visualizes conversation patterns and agent performance metrics.
|
| 6 |
-
"""
|
| 7 |
-
import os
|
| 8 |
-
import json
|
| 9 |
-
from pathlib import Path
|
| 10 |
-
from datetime import datetime, timedelta
|
| 11 |
-
from typing import Dict, List, Any, Optional
|
| 12 |
-
from collections import defaultdict, Counter
|
| 13 |
-
|
| 14 |
-
import gradio as gr
|
| 15 |
-
import pandas as pd
|
| 16 |
-
import plotly.graph_objects as go
|
| 17 |
-
import plotly.express as px
|
| 18 |
-
|
| 19 |
-
# ========== Path Constants ==========
|
| 20 |
-
BASE_DIR = Path(__file__).resolve().parent
|
| 21 |
-
SESSION_ARCHIVE_PATH = BASE_DIR / ".openclaw" / "agents" / "logs" / "session-archive.jsonl"
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
def load_analytics_data() -> Dict[str, Any]:
|
| 25 |
-
"""
|
| 26 |
-
Load and parse analytics data from session-archive.jsonl.
|
| 27 |
-
|
| 28 |
-
Returns:
|
| 29 |
-
Dictionary containing aggregated statistics:
|
| 30 |
-
- messages_per_hour: List of (hour, count) tuples for last 24h
|
| 31 |
-
- agent_distribution: Dict of agent names to response counts
|
| 32 |
-
- sentiment_counts: Dict of sentiment (positive/negative/neutral) to counts
|
| 33 |
-
- top_sessions: List of top 10 active session dicts
|
| 34 |
-
"""
|
| 35 |
-
default_result = {
|
| 36 |
-
"messages_per_hour": [],
|
| 37 |
-
"agent_distribution": {},
|
| 38 |
-
"sentiment_counts": {"positive": 0, "negative": 0, "neutral": 0},
|
| 39 |
-
"top_sessions": []
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
if not SESSION_ARCHIVE_PATH.exists():
|
| 43 |
-
return default_result
|
| 44 |
-
|
| 45 |
-
# Parse all records
|
| 46 |
-
records = []
|
| 47 |
-
try:
|
| 48 |
-
with open(SESSION_ARCHIVE_PATH, 'r') as f:
|
| 49 |
-
for line in f:
|
| 50 |
-
if line.strip():
|
| 51 |
-
try:
|
| 52 |
-
records.append(json.loads(line))
|
| 53 |
-
except json.JSONDecodeError:
|
| 54 |
-
continue
|
| 55 |
-
except Exception:
|
| 56 |
-
return default_result
|
| 57 |
-
|
| 58 |
-
if not records:
|
| 59 |
-
return default_result
|
| 60 |
-
|
| 61 |
-
# Parse timestamps and filter for last 24 hours
|
| 62 |
-
now = datetime.utcnow()
|
| 63 |
-
cutoff = now - timedelta(hours=24)
|
| 64 |
-
|
| 65 |
-
parsed_records = []
|
| 66 |
-
for r in records:
|
| 67 |
-
ts_str = r.get("timestamp")
|
| 68 |
-
if not ts_str:
|
| 69 |
-
continue
|
| 70 |
-
try:
|
| 71 |
-
# Handle ISO format with timezone
|
| 72 |
-
if ts_str.endswith("+00:00"):
|
| 73 |
-
ts_str = ts_str.replace("+00:00", "").replace("Z", "")
|
| 74 |
-
ts = datetime.fromisoformat(ts_str.replace("Z", ""))
|
| 75 |
-
parsed_records.append({
|
| 76 |
-
"timestamp": ts,
|
| 77 |
-
"agent": r.get("agent", "unknown"),
|
| 78 |
-
"state": r.get("state", "unknown"),
|
| 79 |
-
"action": r.get("action", "state_change"),
|
| 80 |
-
"type": r.get("type", "state_change")
|
| 81 |
-
})
|
| 82 |
-
except (ValueError, AttributeError):
|
| 83 |
-
continue
|
| 84 |
-
|
| 85 |
-
# Filter for last 24 hours for hourly chart
|
| 86 |
-
recent_records = [r for r in parsed_records if r["timestamp"] >= cutoff]
|
| 87 |
-
|
| 88 |
-
# 1. Messages per hour (last 24h)
|
| 89 |
-
hour_counts = defaultdict(int)
|
| 90 |
-
for r in recent_records:
|
| 91 |
-
hour_key = r["timestamp"].strftime("%Y-%m-%d %H:00")
|
| 92 |
-
hour_counts[hour_key] += 1
|
| 93 |
-
|
| 94 |
-
# Fill missing hours with 0
|
| 95 |
-
messages_per_hour = []
|
| 96 |
-
for i in range(24):
|
| 97 |
-
hour_time = now - timedelta(hours=23-i)
|
| 98 |
-
hour_key = hour_time.strftime("%Y-%m-%d %H:00")
|
| 99 |
-
messages_per_hour.append({
|
| 100 |
-
"hour": hour_time.strftime("%H:00"),
|
| 101 |
-
"count": hour_counts.get(hour_key, 0)
|
| 102 |
-
})
|
| 103 |
-
|
| 104 |
-
# 2. Agent response distribution (state changes by agent)
|
| 105 |
-
agent_counts = Counter(r["agent"] for r in parsed_records if r["agent"] != "unknown")
|
| 106 |
-
agent_distribution = dict(agent_counts.most_common())
|
| 107 |
-
|
| 108 |
-
# 3. Sentiment analysis (based on state: success/error/other)
|
| 109 |
-
sentiment_counts = {"positive": 0, "negative": 0, "neutral": 0}
|
| 110 |
-
for r in parsed_records:
|
| 111 |
-
state = r.get("state", "").lower()
|
| 112 |
-
if state == "success":
|
| 113 |
-
sentiment_counts["positive"] += 1
|
| 114 |
-
elif state in ("error", "failed"):
|
| 115 |
-
sentiment_counts["negative"] += 1
|
| 116 |
-
else:
|
| 117 |
-
sentiment_counts["neutral"] += 1
|
| 118 |
-
|
| 119 |
-
# 4. Top 10 active sessions (by agent activity)
|
| 120 |
-
session_activity = defaultdict(lambda: {"count": 0, "last_active": None})
|
| 121 |
-
for r in parsed_records:
|
| 122 |
-
agent = r["agent"]
|
| 123 |
-
session_activity[agent]["count"] += 1
|
| 124 |
-
if session_activity[agent]["last_active"] is None or r["timestamp"] > session_activity[agent]["last_active"]:
|
| 125 |
-
session_activity[agent]["last_active"] = r["timestamp"]
|
| 126 |
-
|
| 127 |
-
top_sessions = [
|
| 128 |
-
{
|
| 129 |
-
"session": agent,
|
| 130 |
-
"activity_count": data["count"],
|
| 131 |
-
"last_active": data["last_active"].strftime("%Y-%m-%d %H:%M:%S") if data["last_active"] else "N/A"
|
| 132 |
-
}
|
| 133 |
-
for agent, data in sorted(session_activity.items(), key=lambda x: x[1]["count"], reverse=True)[:10]
|
| 134 |
-
]
|
| 135 |
-
|
| 136 |
-
return {
|
| 137 |
-
"messages_per_hour": messages_per_hour,
|
| 138 |
-
"agent_distribution": agent_distribution,
|
| 139 |
-
"sentiment_counts": sentiment_counts,
|
| 140 |
-
"top_sessions": top_sessions
|
| 141 |
-
}
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
def create_line_chart(data: List[Dict]) -> go.Figure:
|
| 145 |
-
"""Create line chart for messages per hour."""
|
| 146 |
-
hours = [d["hour"] for d in data]
|
| 147 |
-
counts = [d["count"] for d in data]
|
| 148 |
-
|
| 149 |
-
fig = go.Figure()
|
| 150 |
-
fig.add_trace(go.Scatter(
|
| 151 |
-
x=hours,
|
| 152 |
-
y=counts,
|
| 153 |
-
mode="lines+markers",
|
| 154 |
-
name="Messages",
|
| 155 |
-
line=dict(color="#3b82f6", width=2),
|
| 156 |
-
marker=dict(size=6)
|
| 157 |
-
))
|
| 158 |
-
|
| 159 |
-
fig.update_layout(
|
| 160 |
-
title="Messages per Hour (Last 24h)",
|
| 161 |
-
xaxis_title="Hour",
|
| 162 |
-
yaxis_title="Message Count",
|
| 163 |
-
hovermode="x unified",
|
| 164 |
-
height=300,
|
| 165 |
-
margin=dict(l=10, r=10, t=40, b=40)
|
| 166 |
-
)
|
| 167 |
-
return fig
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
def create_bar_chart(data: Dict[str, int]) -> go.Figure:
|
| 171 |
-
"""Create bar chart for agent response distribution."""
|
| 172 |
-
agents = list(data.keys())
|
| 173 |
-
counts = list(data.values())
|
| 174 |
-
|
| 175 |
-
fig = go.Figure()
|
| 176 |
-
fig.add_trace(go.Bar(
|
| 177 |
-
x=agents,
|
| 178 |
-
y=counts,
|
| 179 |
-
marker_color="#10b981"
|
| 180 |
-
))
|
| 181 |
-
|
| 182 |
-
fig.update_layout(
|
| 183 |
-
title="Agent Response Distribution",
|
| 184 |
-
xaxis_title="Agent",
|
| 185 |
-
yaxis_title="Response Count",
|
| 186 |
-
height=300,
|
| 187 |
-
margin=dict(l=10, r=10, t=40, b=40)
|
| 188 |
-
)
|
| 189 |
-
return fig
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
def create_pie_chart(data: Dict[str, int]) -> go.Figure:
|
| 193 |
-
"""Create pie chart for sentiment analysis."""
|
| 194 |
-
labels = list(data.keys())
|
| 195 |
-
values = list(data.values())
|
| 196 |
-
colors = ["#10b981", "#ef4444", "#6b7280"]
|
| 197 |
-
|
| 198 |
-
fig = go.Figure()
|
| 199 |
-
fig.add_trace(go.Pie(
|
| 200 |
-
labels=labels,
|
| 201 |
-
values=values,
|
| 202 |
-
marker=dict(colors=colors),
|
| 203 |
-
textinfo="label+percent"
|
| 204 |
-
))
|
| 205 |
-
|
| 206 |
-
fig.update_layout(
|
| 207 |
-
title="Sentiment Analysis",
|
| 208 |
-
height=300,
|
| 209 |
-
margin=dict(l=10, r=10, t=40, b=40)
|
| 210 |
-
)
|
| 211 |
-
return fig
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
def refresh_analytics():
|
| 215 |
-
"""Refresh all analytics data and charts."""
|
| 216 |
-
data = load_analytics_data()
|
| 217 |
-
|
| 218 |
-
# Update charts
|
| 219 |
-
line_fig = create_line_chart(data["messages_per_hour"])
|
| 220 |
-
bar_fig = create_bar_chart(data["agent_distribution"])
|
| 221 |
-
pie_fig = create_pie_chart(data["sentiment_counts"])
|
| 222 |
-
|
| 223 |
-
# Update dataframe
|
| 224 |
-
df_data = data["top_sessions"] if data["top_sessions"] else [{
|
| 225 |
-
"session": "No data", "activity_count": 0, "last_active": "N/A"
|
| 226 |
-
}]
|
| 227 |
-
df = pd.DataFrame(df_data)
|
| 228 |
-
|
| 229 |
-
return line_fig, bar_fig, pie_fig, df
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
def create_analytics_interface():
|
| 233 |
-
"""Create the Gradio analytics dashboard interface."""
|
| 234 |
-
# Load initial data
|
| 235 |
-
initial_data = load_analytics_data()
|
| 236 |
-
|
| 237 |
-
# Create initial charts
|
| 238 |
-
initial_line = create_line_chart(initial_data["messages_per_hour"])
|
| 239 |
-
initial_bar = create_bar_chart(initial_data["agent_distribution"])
|
| 240 |
-
initial_pie = create_pie_chart(initial_data["sentiment_counts"])
|
| 241 |
-
|
| 242 |
-
# Create initial dataframe
|
| 243 |
-
df_data = initial_data["top_sessions"] if initial_data["top_sessions"] else [{
|
| 244 |
-
"session": "No data", "activity_count": 0, "last_active": "N/A"
|
| 245 |
-
}]
|
| 246 |
-
initial_df = pd.DataFrame(df_data)
|
| 247 |
-
|
| 248 |
-
with gr.Blocks(title="Cain Analytics Dashboard", theme=gr.themes.Soft()) as app:
|
| 249 |
-
gr.Markdown("# π Cain Chat Analytics Dashboard")
|
| 250 |
-
|
| 251 |
-
with gr.Row():
|
| 252 |
-
refresh_btn = gr.Button("π Refresh", variant="primary")
|
| 253 |
-
|
| 254 |
-
with gr.Row():
|
| 255 |
-
with gr.Column(scale=1):
|
| 256 |
-
line_plot = gr.Plot(value=initial_line)
|
| 257 |
-
with gr.Column(scale=1):
|
| 258 |
-
bar_plot = gr.Plot(value=initial_bar)
|
| 259 |
-
|
| 260 |
-
with gr.Row():
|
| 261 |
-
with gr.Column(scale=1):
|
| 262 |
-
pie_plot = gr.Plot(value=initial_pie)
|
| 263 |
-
with gr.Column(scale=1):
|
| 264 |
-
sessions_df = gr.Dataframe(
|
| 265 |
-
value=initial_df,
|
| 266 |
-
label="Top 10 Active Sessions",
|
| 267 |
-
headers=["Session", "Activity Count", "Last Active"]
|
| 268 |
-
)
|
| 269 |
-
|
| 270 |
-
# Bind refresh button
|
| 271 |
-
refresh_btn.click(
|
| 272 |
-
refresh_analytics,
|
| 273 |
-
outputs=[line_plot, bar_plot, pie_plot, sessions_df]
|
| 274 |
-
)
|
| 275 |
-
|
| 276 |
-
return app
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
if __name__ == "__main__":
|
| 280 |
-
app = create_analytics_interface()
|
| 281 |
-
app.launch(server_name="0.0.0.0", server_port=7861, share=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app_broken.py
DELETED
|
@@ -1,555 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
HuggingClaw - Cain Main Entry Point
|
| 4 |
-
FastAPI application serving on port 7860.
|
| 5 |
-
"""
|
| 6 |
-
from fastapi import FastAPI
|
| 7 |
-
from fastapi.middleware.cors import CORSMiddleware
|
| 8 |
-
from fastapi.staticfiles import StaticFiles
|
| 9 |
-
from fastapi.responses import FileResponse, JSONResponse
|
| 10 |
-
from fastapi import WebSocket
|
| 11 |
-
from pydantic import BaseModel
|
| 12 |
-
from typing import Optional, Dict, List
|
| 13 |
-
from enum import Enum
|
| 14 |
-
import os
|
| 15 |
-
import sys
|
| 16 |
-
from datetime import datetime
|
| 17 |
-
import asyncio
|
| 18 |
-
from contextlib import asynccontextmanager
|
| 19 |
-
import time
|
| 20 |
-
import json
|
| 21 |
-
import psutil
|
| 22 |
-
|
| 23 |
-
# Add /app to sys.path for proper package imports
|
| 24 |
-
sys.path.insert(0, "/app")
|
| 25 |
-
|
| 26 |
-
# Import system logger
|
| 27 |
-
from openclaw.core.system_logger import log_startup, log_heartbeat, get_last_lines
|
| 28 |
-
# Import error handlers
|
| 29 |
-
from error_handlers import handle_status_file_read, handle_brain_response, handle_websocket_send
|
| 30 |
-
|
| 31 |
-
# Track startup time for uptime calculation
|
| 32 |
-
START_TIME = time.time()
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
# ============================================================================
|
| 36 |
-
# Agent Communication Manager
|
| 37 |
-
# ============================================================================
|
| 38 |
-
|
| 39 |
-
class AgentRole(str, Enum):
|
| 40 |
-
"""Agent roles in the HuggingClaw World family."""
|
| 41 |
-
ADAM = "adam"
|
| 42 |
-
EVE = "eve"
|
| 43 |
-
CAIN = "cain"
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
class AgentMessage(BaseModel):
|
| 47 |
-
"""Message structure for inter-agent communication."""
|
| 48 |
-
sender: AgentRole
|
| 49 |
-
recipient: AgentRole
|
| 50 |
-
content: str
|
| 51 |
-
timestamp: str
|
| 52 |
-
message_id: Optional[str] = None
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
class AgentState(BaseModel):
|
| 56 |
-
"""State tracking for each agent including heartbeat."""
|
| 57 |
-
agent_id: AgentRole
|
| 58 |
-
current_state: str = "idle"
|
| 59 |
-
last_heartbeat: float = 0.0
|
| 60 |
-
message_queue_size: int = 0
|
| 61 |
-
is_active: bool = False
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
class AgentRouter:
|
| 65 |
-
"""
|
| 66 |
-
Centralized message router for inter-agent communication.
|
| 67 |
-
Uses asyncio.Queue to prevent state corruption.
|
| 68 |
-
"""
|
| 69 |
-
|
| 70 |
-
def __init__(self):
|
| 71 |
-
self._queues: Dict[AgentRole, asyncio.Queue] = {
|
| 72 |
-
AgentRole.ADAM: asyncio.Queue(),
|
| 73 |
-
AgentRole.EVE: asyncio.Queue(),
|
| 74 |
-
AgentRole.CAIN: asyncio.Queue(),
|
| 75 |
-
}
|
| 76 |
-
self._states: Dict[AgentRole, AgentState] = {
|
| 77 |
-
role: AgentState(agent_id=role, last_heartbeat=time.time())
|
| 78 |
-
for role in AgentRole
|
| 79 |
-
}
|
| 80 |
-
self._heartbeat_task: Optional[asyncio.Task] = None
|
| 81 |
-
|
| 82 |
-
async def start(self):
|
| 83 |
-
"""Start the agent router background tasks."""
|
| 84 |
-
self._heartbeat_task = asyncio.create_task(self._heartbeat_loop())
|
| 85 |
-
|
| 86 |
-
async def stop(self):
|
| 87 |
-
"""Stop the agent router background tasks."""
|
| 88 |
-
if self._heartbeat_task:
|
| 89 |
-
self._heartbeat_task.cancel()
|
| 90 |
-
try:
|
| 91 |
-
await self._heartbeat_task
|
| 92 |
-
except asyncio.CancelledError:
|
| 93 |
-
pass
|
| 94 |
-
|
| 95 |
-
async def _heartbeat_loop(self):
|
| 96 |
-
"""Update heartbeat timestamps every 10 seconds."""
|
| 97 |
-
while True:
|
| 98 |
-
await asyncio.sleep(10)
|
| 99 |
-
now = time.time()
|
| 100 |
-
for state in self._states.values():
|
| 101 |
-
state.last_heartbeat = now
|
| 102 |
-
state.message_queue_size = self._queues[state.agent_id].qsize()
|
| 103 |
-
# Mark inactive if no heartbeat for 30 seconds
|
| 104 |
-
state.is_active = (now - state.last_heartbeat) < 30
|
| 105 |
-
|
| 106 |
-
async def send_message(self, message: AgentMessage) -> bool:
|
| 107 |
-
"""Send a message to a specific agent's queue."""
|
| 108 |
-
recipient = message.recipient
|
| 109 |
-
if recipient not in self._queues:
|
| 110 |
-
return False
|
| 111 |
-
await self._queues[recipient].put(message)
|
| 112 |
-
return True
|
| 113 |
-
|
| 114 |
-
async def receive_message(self, agent: AgentRole, timeout: float = 1.0) -> Optional[AgentMessage]:
|
| 115 |
-
"""Receive a message from an agent's queue."""
|
| 116 |
-
if agent not in self._queues:
|
| 117 |
-
return None
|
| 118 |
-
try:
|
| 119 |
-
return await asyncio.wait_for(self._queues[agent].get(), timeout=timeout)
|
| 120 |
-
except asyncio.TimeoutError:
|
| 121 |
-
return None
|
| 122 |
-
|
| 123 |
-
def get_all_states(self) -> List[AgentState]:
|
| 124 |
-
"""Get current state of all agents."""
|
| 125 |
-
return list(self._states.values())
|
| 126 |
-
|
| 127 |
-
def get_state(self, agent: AgentRole) -> Optional[AgentState]:
|
| 128 |
-
"""Get state of a specific agent."""
|
| 129 |
-
return self._states.get(agent)
|
| 130 |
-
|
| 131 |
-
def update_state(self, agent: AgentRole, state: str) -> bool:
|
| 132 |
-
"""Update the state of a specific agent."""
|
| 133 |
-
if agent not in self._states:
|
| 134 |
-
return False
|
| 135 |
-
self._states[agent].current_state = state
|
| 136 |
-
self._states[agent].last_heartbeat = time.time()
|
| 137 |
-
return True
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
# Global agent router instance
|
| 141 |
-
agent_router = AgentRouter()
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
@asynccontextmanager
|
| 145 |
-
async def lifespan(app: FastAPI):
|
| 146 |
-
"""Lifespan context manager for startup/shutdown events."""
|
| 147 |
-
# Startup: log system startup
|
| 148 |
-
log_startup("Cain", "1.0.0")
|
| 149 |
-
|
| 150 |
-
# Start agent router
|
| 151 |
-
await agent_router.start()
|
| 152 |
-
|
| 153 |
-
# Start heartbeat task
|
| 154 |
-
heartbeat_task = asyncio.create_task(heartbeat_loop())
|
| 155 |
-
|
| 156 |
-
yield
|
| 157 |
-
|
| 158 |
-
# Shutdown: cancel heartbeat and stop router
|
| 159 |
-
heartbeat_task.cancel()
|
| 160 |
-
await agent_router.stop()
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
# Background heartbeat loop
|
| 164 |
-
async def heartbeat_loop():
|
| 165 |
-
"""Write heartbeat signal every 30 seconds."""
|
| 166 |
-
while True:
|
| 167 |
-
await asyncio.sleep(30)
|
| 168 |
-
status = get_cain_status().get("current_state", "unknown")
|
| 169 |
-
log_heartbeat(status)
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
app = FastAPI(title="HuggingClaw - Cain", version="1.0.0", lifespan=lifespan)
|
| 173 |
-
|
| 174 |
-
# Mount static files directory
|
| 175 |
-
static_dir = "/app/static"
|
| 176 |
-
if os.path.exists(static_dir):
|
| 177 |
-
app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
| 178 |
-
|
| 179 |
-
# CORS enabled for frontend access
|
| 180 |
-
app.add_middleware(
|
| 181 |
-
CORSMiddleware,
|
| 182 |
-
allow_origins=["*"],
|
| 183 |
-
allow_credentials=True,
|
| 184 |
-
allow_methods=["*"],
|
| 185 |
-
allow_headers=["*"],
|
| 186 |
-
)
|
| 187 |
-
|
| 188 |
-
class ChatMessage(BaseModel):
|
| 189 |
-
message: str
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
def get_cain_status() -> dict:
|
| 193 |
-
"""Read Cain's current status from cain_status.json."""
|
| 194 |
-
return handle_status_file_read()
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
def get_brain_response(message: str) -> str:
|
| 198 |
-
"""Route message to brain_minimal.py and return response."""
|
| 199 |
-
return handle_brain_response(message)
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
@app.get("/")
|
| 203 |
-
async def root():
|
| 204 |
-
"""Root endpoint - simple health check."""
|
| 205 |
-
return {"message": "Cain is running"}
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
@app.get("/dashboard", response_class=FileResponse)
|
| 209 |
-
async def dashboard():
|
| 210 |
-
"""Dashboard endpoint serving the UI."""
|
| 211 |
-
index_path = f"{static_dir}/index.html"
|
| 212 |
-
if os.path.exists(index_path):
|
| 213 |
-
return FileResponse(index_path)
|
| 214 |
-
# Fallback if dashboard not found
|
| 215 |
-
return FileResponse("/app/index.html")
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
@app.get("/status")
|
| 219 |
-
async def status():
|
| 220 |
-
"""Get Cain's health from cain_status.json."""
|
| 221 |
-
status_data = get_cain_status()
|
| 222 |
-
return {
|
| 223 |
-
"agent": "cain",
|
| 224 |
-
"health": status_data,
|
| 225 |
-
"timestamp": datetime.utcnow().isoformat() + "+00:00"
|
| 226 |
-
}
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
# Dashboard API endpoints
|
| 230 |
-
@app.get("/api/status")
|
| 231 |
-
async def api_status():
|
| 232 |
-
"""Dashboard API - get status and personality."""
|
| 233 |
-
status_data = get_cain_status()
|
| 234 |
-
return {
|
| 235 |
-
"status": status_data,
|
| 236 |
-
"personality": {
|
| 237 |
-
"name": "Cain",
|
| 238 |
-
"role": "Interaction Agent",
|
| 239 |
-
"tone": "friendly",
|
| 240 |
-
"response_style": "conversational"
|
| 241 |
-
},
|
| 242 |
-
"timestamp": datetime.utcnow().isoformat() + "+00:00"
|
| 243 |
-
}
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
@app.get("/api/logs")
|
| 247 |
-
async def api_logs():
|
| 248 |
-
"""Dashboard API - get agent logs."""
|
| 249 |
-
return {"logs": []}
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
@app.post("/api/chat")
|
| 253 |
-
async def api_chat(msg: ChatMessage):
|
| 254 |
-
"""Dashboard API - chat endpoint."""
|
| 255 |
-
response_text = get_brain_response(msg.message)
|
| 256 |
-
return {
|
| 257 |
-
"agent_response": response_text,
|
| 258 |
-
"timestamp": datetime.utcnow().isoformat() + "+00:00"
|
| 259 |
-
}
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
@app.post("/chat")
|
| 263 |
-
async def chat(msg: ChatMessage):
|
| 264 |
-
"""Chat endpoint - routes to brain_minimal.py and returns response."""
|
| 265 |
-
response_text = get_brain_response(msg.message)
|
| 266 |
-
return {
|
| 267 |
-
"response": response_text,
|
| 268 |
-
"timestamp": datetime.utcnow().isoformat() + "+00:00",
|
| 269 |
-
"agent": "cain"
|
| 270 |
-
}
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
@app.post("/agents/send")
|
| 274 |
-
async def agent_send(msg: AgentMessage):
|
| 275 |
-
"""Send a message to another agent through the router."""
|
| 276 |
-
success = await agent_router.send_message(msg)
|
| 277 |
-
return {
|
| 278 |
-
"success": success,
|
| 279 |
-
"message": "Message queued" if success else "Failed to queue message",
|
| 280 |
-
"timestamp": datetime.utcnow().isoformat() + "+00:00"
|
| 281 |
-
}
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
@app.get("/agents/{agent_id}/receive")
|
| 285 |
-
async def agent_receive(agent_id: str, timeout: float = 1.0):
|
| 286 |
-
"""Receive a message from the agent's queue."""
|
| 287 |
-
try:
|
| 288 |
-
role = AgentRole(agent_id)
|
| 289 |
-
except ValueError:
|
| 290 |
-
return {"error": "Invalid agent ID", "valid_agents": [r.value for r in AgentRole]}
|
| 291 |
-
|
| 292 |
-
message = await agent_router.receive_message(role, timeout=timeout)
|
| 293 |
-
if message:
|
| 294 |
-
return {
|
| 295 |
-
"message": message.dict(),
|
| 296 |
-
"timestamp": datetime.utcnow().isoformat() + "+00:00"
|
| 297 |
-
}
|
| 298 |
-
return {
|
| 299 |
-
"message": None,
|
| 300 |
-
"queue_size": agent_router.get_state(role).message_queue_size,
|
| 301 |
-
"timestamp": datetime.utcnow().isoformat() + "+00:00"
|
| 302 |
-
}
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
@app.post("/agents/{agent_id}/state")
|
| 306 |
-
async def agent_update_state(agent_id: str, state: str):
|
| 307 |
-
"""Update the state of an agent."""
|
| 308 |
-
try:
|
| 309 |
-
role = AgentRole(agent_id)
|
| 310 |
-
except ValueError:
|
| 311 |
-
return {"error": "Invalid agent ID", "valid_agents": [r.value for r in AgentRole]}
|
| 312 |
-
|
| 313 |
-
success = agent_router.update_state(role, state)
|
| 314 |
-
return {
|
| 315 |
-
"success": success,
|
| 316 |
-
"agent_id": agent_id,
|
| 317 |
-
"new_state": state if success else None,
|
| 318 |
-
"timestamp": datetime.utcnow().isoformat() + "+00:00"
|
| 319 |
-
}
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
@app.websocket("/ws")
|
| 323 |
-
async def websocket_endpoint(websocket: WebSocket):
|
| 324 |
-
"""WebSocket endpoint for real-time dashboard updates."""
|
| 325 |
-
await websocket.accept()
|
| 326 |
-
try:
|
| 327 |
-
while True:
|
| 328 |
-
# Send heartbeat every 5 seconds using error handler
|
| 329 |
-
status_data = get_cain_status()
|
| 330 |
-
if not await handle_websocket_send(websocket, status_data):
|
| 331 |
-
break
|
| 332 |
-
await asyncio.sleep(5)
|
| 333 |
-
except Exception as e:
|
| 334 |
-
pass
|
| 335 |
-
finally:
|
| 336 |
-
await websocket.close()
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
@app.get("/admin/system")
|
| 340 |
-
async def admin_system():
|
| 341 |
-
"""Admin endpoint - read last 50 lines of system log."""
|
| 342 |
-
lines = get_last_lines(50)
|
| 343 |
-
return {
|
| 344 |
-
"log_file": "logs/system.log",
|
| 345 |
-
"line_count": len(lines),
|
| 346 |
-
"lines": lines,
|
| 347 |
-
"timestamp": datetime.utcnow().isoformat() + "+00:00"
|
| 348 |
-
}
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
@app.get("/health")
|
| 352 |
-
async def health():
|
| 353 |
-
"""
|
| 354 |
-
Health check endpoint with proper HTTP status codes.
|
| 355 |
-
Returns 200 if healthy, 503 if unhealthy with descriptive error.
|
| 356 |
-
|
| 357 |
-
Checks:
|
| 358 |
-
1. Brain import status
|
| 359 |
-
2. Data persistence files (JSON-based storage)
|
| 360 |
-
3. Frontend asset availability (static/index.html)
|
| 361 |
-
"""
|
| 362 |
-
from fastapi import status as http_status
|
| 363 |
-
|
| 364 |
-
checks = {}
|
| 365 |
-
is_healthy = True
|
| 366 |
-
error_message = None
|
| 367 |
-
|
| 368 |
-
# 1. Check brain_minimal can be imported
|
| 369 |
-
brain_ok = False
|
| 370 |
-
try:
|
| 371 |
-
from openclaw.agents.brain_minimal import BrainMinimal
|
| 372 |
-
brain = BrainMinimal(agent_name="cain", legacy_mode=True)
|
| 373 |
-
brain_ok = True
|
| 374 |
-
checks["brain_import"] = "ok"
|
| 375 |
-
except ImportError as e:
|
| 376 |
-
checks["brain_import"] = f"failed: ImportError: {str(e)}"
|
| 377 |
-
is_healthy = False
|
| 378 |
-
error_message = f"Brain import failed: {str(e)}"
|
| 379 |
-
except Exception as e:
|
| 380 |
-
checks["brain_import"] = f"failed: {type(e).__name__}: {str(e)}"
|
| 381 |
-
is_healthy = False
|
| 382 |
-
error_message = f"Brain import failed: {type(e).__name__}: {str(e)}"
|
| 383 |
-
|
| 384 |
-
# 2. Check data persistence files (JSON-based)
|
| 385 |
-
persistence_ok = True
|
| 386 |
-
cain_status_path = "/app/openclaw/.openclaw/agents/cain_status.json"
|
| 387 |
-
registry_path = "/app/openclaw/.openclaw/agents/registry.json"
|
| 388 |
-
|
| 389 |
-
# Check cain_status.json
|
| 390 |
-
try:
|
| 391 |
-
with open(cain_status_path, "r") as f:
|
| 392 |
-
status_data = json.load(f)
|
| 393 |
-
current_state = status_data.get("current_state", "unknown")
|
| 394 |
-
checks["persistence_cain_status"] = "ok"
|
| 395 |
-
if current_state == "error":
|
| 396 |
-
checks["persistence_cain_status"] = "ok (error state)"
|
| 397 |
-
except FileNotFoundError:
|
| 398 |
-
checks["persistence_cain_status"] = "failed: file not found"
|
| 399 |
-
persistence_ok = False
|
| 400 |
-
except json.JSONDecodeError as e:
|
| 401 |
-
checks["persistence_cain_status"] = f"failed: invalid JSON: {str(e)}"
|
| 402 |
-
persistence_ok = False
|
| 403 |
-
except Exception as e:
|
| 404 |
-
checks["persistence_cain_status"] = f"failed: {type(e).__name__}: {str(e)}"
|
| 405 |
-
persistence_ok = False
|
| 406 |
-
|
| 407 |
-
# Check registry.json
|
| 408 |
-
try:
|
| 409 |
-
with open(registry_path, "r") as f:
|
| 410 |
-
json.load(f)
|
| 411 |
-
checks["persistence_registry"] = "ok"
|
| 412 |
-
except FileNotFoundError:
|
| 413 |
-
checks["persistence_registry"] = "failed: file not found"
|
| 414 |
-
persistence_ok = False
|
| 415 |
-
except json.JSONDecodeError as e:
|
| 416 |
-
checks["persistence_registry"] = f"failed: invalid JSON: {str(e)}"
|
| 417 |
-
persistence_ok = False
|
| 418 |
-
except Exception as e:
|
| 419 |
-
checks["persistence_registry"] = f"failed: {type(e).__name__}: {str(e)}"
|
| 420 |
-
persistence_ok = False
|
| 421 |
-
|
| 422 |
-
checks["persistence"] = "ok" if persistence_ok else "degraded"
|
| 423 |
-
|
| 424 |
-
# 3. Check frontend asset availability
|
| 425 |
-
frontend_ok = False
|
| 426 |
-
index_path = f"{static_dir}/index.html"
|
| 427 |
-
fallback_path = "/app/index.html"
|
| 428 |
-
|
| 429 |
-
if os.path.exists(index_path):
|
| 430 |
-
checks["frontend_assets"] = "ok"
|
| 431 |
-
frontend_ok = True
|
| 432 |
-
elif os.path.exists(fallback_path):
|
| 433 |
-
checks["frontend_assets"] = "ok (fallback)"
|
| 434 |
-
frontend_ok = True
|
| 435 |
-
else:
|
| 436 |
-
checks["frontend_assets"] = "failed: index.html not found"
|
| 437 |
-
# Frontend missing is degraded, not critical failure
|
| 438 |
-
|
| 439 |
-
# 4. Check agent router heartbeat
|
| 440 |
-
now = time.time()
|
| 441 |
-
cain_state = agent_router.get_state(AgentRole.CAIN)
|
| 442 |
-
heartbeat_age = now - cain_state.last_heartbeat
|
| 443 |
-
router_ok = heartbeat_age < 30
|
| 444 |
-
checks["agent_router"] = "ok" if router_ok else f"degraded: stale ({heartbeat_age:.1f}s)"
|
| 445 |
-
|
| 446 |
-
# Determine overall status
|
| 447 |
-
# Critical failure: brain not available
|
| 448 |
-
if not brain_ok:
|
| 449 |
-
return JSONResponse(
|
| 450 |
-
status_code=http_status.HTTP_503_SERVICE_UNAVAILABLE,
|
| 451 |
-
content={
|
| 452 |
-
"status": "critical",
|
| 453 |
-
"brain": "unavailable",
|
| 454 |
-
"persistence": "ok" if persistence_ok else "error",
|
| 455 |
-
"frontend": "available" if frontend_ok else "unavailable",
|
| 456 |
-
"checks": checks,
|
| 457 |
-
"error": error_message,
|
| 458 |
-
"timestamp": datetime.utcnow().isoformat() + "+00:00"
|
| 459 |
-
}
|
| 460 |
-
)
|
| 461 |
-
|
| 462 |
-
# Healthy response
|
| 463 |
-
return {
|
| 464 |
-
"status": "healthy" if is_healthy else "degraded",
|
| 465 |
-
"brain": "available",
|
| 466 |
-
"persistence": "ok" if persistence_ok else "error",
|
| 467 |
-
"frontend": "available" if frontend_ok else "unavailable",
|
| 468 |
-
"agent_router": "ok" if router_ok else "stale",
|
| 469 |
-
"uptime_seconds": round(time.time() - START_TIME, 2),
|
| 470 |
-
"checks": checks,
|
| 471 |
-
"timestamp": datetime.utcnow().isoformat() + "+00:00"
|
| 472 |
-
}
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
@app.get("/debug/health")
|
| 476 |
-
async def debug_health():
|
| 477 |
-
"""Debug health endpoint with detailed system status."""
|
| 478 |
-
# 1. Uptime in seconds
|
| 479 |
-
uptime = time.time() - START_TIME
|
| 480 |
-
|
| 481 |
-
# 2. Current memory usage of the current process (brain_minimal runs in same process)
|
| 482 |
-
process = psutil.Process(os.getpid())
|
| 483 |
-
memory_info = process.memory_info()
|
| 484 |
-
memory_mb = memory_info.rss / 1024 / 1024 # Convert to MB
|
| 485 |
-
|
| 486 |
-
# 3. Last 5 lines from cain_status.json (read as structured JSON)
|
| 487 |
-
cain_status_path = "/app/openclaw/.openclaw/agents/cain_status.json"
|
| 488 |
-
cain_status_lines = []
|
| 489 |
-
try:
|
| 490 |
-
if os.path.exists(cain_status_path):
|
| 491 |
-
with open(cain_status_path, "r") as f:
|
| 492 |
-
content = f.read()
|
| 493 |
-
lines = content.strip().split("\n")
|
| 494 |
-
cain_status_lines = lines[-5:] if len(lines) > 5 else lines
|
| 495 |
-
else:
|
| 496 |
-
cain_status_lines = ["cain_status.json not found"]
|
| 497 |
-
except Exception as e:
|
| 498 |
-
cain_status_lines = [f"Error reading cain_status.json: {str(e)}"]
|
| 499 |
-
|
| 500 |
-
# 4. List of loaded Python modules
|
| 501 |
-
loaded_modules = sorted([name for name in sys.modules.keys() if not name.startswith("_")])[:100]
|
| 502 |
-
|
| 503 |
-
return {
|
| 504 |
-
"uptime_seconds": round(uptime, 2),
|
| 505 |
-
"memory": {
|
| 506 |
-
"rss_mb": round(memory_mb, 2),
|
| 507 |
-
"vms_mb": round(memory_info.vms / 1024 / 1024, 2)
|
| 508 |
-
},
|
| 509 |
-
"cain_status_last_lines": cain_status_lines,
|
| 510 |
-
"loaded_modules_count": len(loaded_modules),
|
| 511 |
-
"loaded_modules": loaded_modules,
|
| 512 |
-
"timestamp": datetime.utcnow().isoformat() + "+00:00"
|
| 513 |
-
}
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
@app.get("/health/agents")
|
| 517 |
-
async def health_agents():
|
| 518 |
-
"""Health check endpoint returning status of all agents."""
|
| 519 |
-
agents_status = []
|
| 520 |
-
now = time.time()
|
| 521 |
-
|
| 522 |
-
for state in agent_router.get_all_states():
|
| 523 |
-
# Calculate time since last heartbeat
|
| 524 |
-
heartbeat_age = now - state.last_heartbeat
|
| 525 |
-
|
| 526 |
-
# Determine health status
|
| 527 |
-
if heartbeat_age < 15:
|
| 528 |
-
health = "healthy"
|
| 529 |
-
elif heartbeat_age < 30:
|
| 530 |
-
health = "degraded"
|
| 531 |
-
else:
|
| 532 |
-
health = "unhealthy"
|
| 533 |
-
|
| 534 |
-
agents_status.append({
|
| 535 |
-
"agent_id": state.agent_id,
|
| 536 |
-
"current_state": state.current_state,
|
| 537 |
-
"is_active": state.is_active,
|
| 538 |
-
"health": health,
|
| 539 |
-
"last_heartbeat": state.last_heartbeat,
|
| 540 |
-
"heartbeat_age_seconds": round(heartbeat_age, 2),
|
| 541 |
-
"message_queue_size": state.message_queue_size,
|
| 542 |
-
"last_heartbeat_iso": datetime.fromtimestamp(state.last_heartbeat).isoformat() + "+00:00"
|
| 543 |
-
})
|
| 544 |
-
|
| 545 |
-
return {
|
| 546 |
-
"agents": agents_status,
|
| 547 |
-
"total_agents": len(agents_status),
|
| 548 |
-
"active_agents": sum(1 for a in agents_status if a["is_active"]),
|
| 549 |
-
"timestamp": datetime.utcnow().isoformat() + "+00:00"
|
| 550 |
-
}
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
if __name__ == "__main__":
|
| 554 |
-
import uvicorn
|
| 555 |
-
uvicorn.run(app, host="0.0.0.0", port=7860)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
gradio_dashboard.py
DELETED
|
@@ -1,1199 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 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
|
| 10 |
-
- Centralized logging with severity levels
|
| 11 |
-
- Component health monitoring
|
| 12 |
-
"""
|
| 13 |
-
import gradio as gr
|
| 14 |
-
import json
|
| 15 |
-
import os
|
| 16 |
-
import sys
|
| 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"
|
| 34 |
-
else:
|
| 35 |
-
BASE_DIR = OPENCLAW_HOME
|
| 36 |
-
|
| 37 |
-
CRON_LOGS_DIR = BASE_DIR / "logs"
|
| 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 |
-
SESSION_ARCHIVE_FILE = BASE_DIR / "agents" / "logs" / "session-archive.jsonl"
|
| 42 |
-
HEALTH_MONITOR_FILE = BASE_DIR / "health_monitor.py"
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
# ========== API Client Functions ==========
|
| 46 |
-
|
| 47 |
-
def fetch_api(endpoint: str, method: str = "GET", data: dict = None) -> dict:
|
| 48 |
-
"""
|
| 49 |
-
Fetch data from the internal API endpoint.
|
| 50 |
-
|
| 51 |
-
Args:
|
| 52 |
-
endpoint: API endpoint path (e.g., "/api/agents")
|
| 53 |
-
method: HTTP method (GET, POST)
|
| 54 |
-
data: Request body for POST requests
|
| 55 |
-
|
| 56 |
-
Returns:
|
| 57 |
-
JSON response as dict
|
| 58 |
-
"""
|
| 59 |
-
url = f"{API_BASE_URL}{endpoint}"
|
| 60 |
-
|
| 61 |
-
try:
|
| 62 |
-
if method == "GET":
|
| 63 |
-
response = requests.get(url, timeout=5)
|
| 64 |
-
elif method == "POST":
|
| 65 |
-
response = requests.post(url, json=data, timeout=30)
|
| 66 |
-
else:
|
| 67 |
-
return {"error": f"Unsupported method: {method}"}
|
| 68 |
-
|
| 69 |
-
response.raise_for_status()
|
| 70 |
-
return response.json()
|
| 71 |
-
except requests.exceptions.RequestException as e:
|
| 72 |
-
return {"error": f"API request failed: {str(e)}"}
|
| 73 |
-
except json.JSONDecodeError:
|
| 74 |
-
return {"error": "Invalid JSON response"}
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
# ========== Dashboard Functions (API-based) ==========
|
| 78 |
-
|
| 79 |
-
def get_agent_states():
|
| 80 |
-
"""Get current agent states from /api/agents endpoint"""
|
| 81 |
-
try:
|
| 82 |
-
data = fetch_api("/api/agents")
|
| 83 |
-
|
| 84 |
-
if "error" in data:
|
| 85 |
-
return {
|
| 86 |
-
"agent_name": "cain",
|
| 87 |
-
"role": "interaction",
|
| 88 |
-
"state": "idle",
|
| 89 |
-
"error": data["error"],
|
| 90 |
-
"available_states": ["idle", "processing", "success", "error"],
|
| 91 |
-
"base_dir": str(BASE_DIR),
|
| 92 |
-
"api_error": True
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
# Return the data from API
|
| 96 |
-
return {
|
| 97 |
-
"agent_name": data.get("agent_name", "cain"),
|
| 98 |
-
"role": data.get("role", "unknown"),
|
| 99 |
-
"state": data.get("state", "idle"),
|
| 100 |
-
"brain_state": data.get("brain_state", "idle"),
|
| 101 |
-
"legacy_mode": data.get("legacy_mode", False),
|
| 102 |
-
"tools_registered": data.get("tools_registered", 0),
|
| 103 |
-
"tools_allowed": data.get("tools_allowed", 0),
|
| 104 |
-
"last_action": data.get("last_action", "N/A"),
|
| 105 |
-
"last_action_time": data.get("last_action_time", "N/A"),
|
| 106 |
-
"available_states": data.get("available_states", []),
|
| 107 |
-
"uptime_seconds": data.get("uptime_seconds"),
|
| 108 |
-
"timestamp": data.get("timestamp"),
|
| 109 |
-
"base_dir": str(BASE_DIR)
|
| 110 |
-
}
|
| 111 |
-
except Exception as e:
|
| 112 |
-
return {
|
| 113 |
-
"agent_name": "cain",
|
| 114 |
-
"role": "interaction",
|
| 115 |
-
"state": "idle",
|
| 116 |
-
"error": f"Could not fetch agent states: {str(e)}",
|
| 117 |
-
"available_states": ["idle", "processing", "success", "error"],
|
| 118 |
-
"base_dir": str(BASE_DIR),
|
| 119 |
-
"api_error": True
|
| 120 |
-
}
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
def get_system_health():
|
| 124 |
-
"""Get system health from /api/health endpoint"""
|
| 125 |
-
try:
|
| 126 |
-
data = fetch_api("/api/health")
|
| 127 |
-
|
| 128 |
-
if "error" in data:
|
| 129 |
-
return {
|
| 130 |
-
"monitor_available": False,
|
| 131 |
-
"error": data["error"]
|
| 132 |
-
}
|
| 133 |
-
|
| 134 |
-
return data
|
| 135 |
-
except Exception as e:
|
| 136 |
-
return {
|
| 137 |
-
"monitor_available": False,
|
| 138 |
-
"error": f"Could not fetch health data: {str(e)}"
|
| 139 |
-
}
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
def chat_with_agent(message: str, session_id: str = ""):
|
| 143 |
-
"""Send a chat message to the agent via /api/chat endpoint"""
|
| 144 |
-
try:
|
| 145 |
-
payload = {
|
| 146 |
-
"message": message
|
| 147 |
-
}
|
| 148 |
-
if session_id:
|
| 149 |
-
payload["session_id"] = session_id
|
| 150 |
-
|
| 151 |
-
data = fetch_api("/api/chat", method="POST", data=payload)
|
| 152 |
-
|
| 153 |
-
if "error" in data:
|
| 154 |
-
return f"Error: {data['error']}"
|
| 155 |
-
|
| 156 |
-
if data.get("success"):
|
| 157 |
-
response = data.get("response", "")
|
| 158 |
-
metadata = f"\n\n[Agent: {data.get('agent', 'cain')} | State: {data.get('state', 'unknown')}]"
|
| 159 |
-
return response + metadata
|
| 160 |
-
else:
|
| 161 |
-
return f"Error: {data.get('response', 'Unknown error')}"
|
| 162 |
-
except Exception as e:
|
| 163 |
-
return f"Error sending message: {str(e)}"
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
# ========== Legacy Functions (Fallback) ==========
|
| 167 |
-
|
| 168 |
-
def get_cron_history():
|
| 169 |
-
"""Get recent cron job execution history (legacy, file-based)"""
|
| 170 |
-
history = []
|
| 171 |
-
|
| 172 |
-
# Read jobs configuration
|
| 173 |
-
jobs_config = {}
|
| 174 |
-
if CRON_JOBS_FILE.exists():
|
| 175 |
-
try:
|
| 176 |
-
with open(CRON_JOBS_FILE) as f:
|
| 177 |
-
config = json.load(f)
|
| 178 |
-
for job in config.get("jobs", []):
|
| 179 |
-
jobs_config[job["id"]] = job
|
| 180 |
-
except Exception as e:
|
| 181 |
-
jobs_config = {"error": str(e)}
|
| 182 |
-
|
| 183 |
-
# Read log files
|
| 184 |
-
if CRON_LOGS_DIR.exists():
|
| 185 |
-
for log_file in CRON_LOGS_DIR.glob("*.jsonl"):
|
| 186 |
-
job_id = log_file.stem
|
| 187 |
-
job_info = jobs_config.get(job_id, {})
|
| 188 |
-
|
| 189 |
-
try:
|
| 190 |
-
# Read last 10 entries
|
| 191 |
-
entries = []
|
| 192 |
-
with open(log_file) as f:
|
| 193 |
-
for line in f:
|
| 194 |
-
if line.strip():
|
| 195 |
-
try:
|
| 196 |
-
entries.append(json.loads(line))
|
| 197 |
-
except json.JSONDecodeError:
|
| 198 |
-
pass
|
| 199 |
-
|
| 200 |
-
# Get recent entries
|
| 201 |
-
recent = entries[-10:] if entries else []
|
| 202 |
-
|
| 203 |
-
for entry in recent:
|
| 204 |
-
history.append({
|
| 205 |
-
"job_id": job_id,
|
| 206 |
-
"description": job_info.get("description", ""),
|
| 207 |
-
"schedule": job_info.get("schedule", "N/A"),
|
| 208 |
-
"timestamp": entry.get("timestamp", "N/A"),
|
| 209 |
-
"action": entry.get("action", "unknown"),
|
| 210 |
-
"status": entry.get("status", entry.get("stage", "N/A")),
|
| 211 |
-
"success": entry.get("success", True),
|
| 212 |
-
"detail": str(entry.get("detail", ""))[:100]
|
| 213 |
-
})
|
| 214 |
-
except Exception as e:
|
| 215 |
-
history.append({
|
| 216 |
-
"job_id": job_id,
|
| 217 |
-
"error": f"Failed to read log: {str(e)}"
|
| 218 |
-
})
|
| 219 |
-
|
| 220 |
-
# If no history but have jobs config, show configured jobs
|
| 221 |
-
if not history and jobs_config and "error" not in jobs_config:
|
| 222 |
-
for job_id, job_info in jobs_config.items():
|
| 223 |
-
history.append({
|
| 224 |
-
"job_id": job_id,
|
| 225 |
-
"description": job_info.get("description", ""),
|
| 226 |
-
"schedule": job_info.get("schedule", "N/A"),
|
| 227 |
-
"timestamp": "Not yet executed",
|
| 228 |
-
"action": "configured",
|
| 229 |
-
"status": "pending",
|
| 230 |
-
"success": None,
|
| 231 |
-
"detail": f"Enabled: {job_info.get('enabled', True)}"
|
| 232 |
-
})
|
| 233 |
-
|
| 234 |
-
# Sort by timestamp (most recent first)
|
| 235 |
-
history.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
|
| 236 |
-
return history[:20] # Return last 20 entries
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
def get_memory_stats():
|
| 240 |
-
"""Get memory usage statistics (legacy, system-based)"""
|
| 241 |
-
stats = {
|
| 242 |
-
"system_memory": {},
|
| 243 |
-
"disk_usage": {},
|
| 244 |
-
"cain_memory": {}
|
| 245 |
-
}
|
| 246 |
-
|
| 247 |
-
# System memory (psutil)
|
| 248 |
-
try:
|
| 249 |
-
mem = psutil.virtual_memory()
|
| 250 |
-
stats["system_memory"] = {
|
| 251 |
-
"total_gb": round(mem.total / (1024**3), 2),
|
| 252 |
-
"available_gb": round(mem.available / (1024**3), 2),
|
| 253 |
-
"used_gb": round(mem.used / (1024**3), 2),
|
| 254 |
-
"percent_used": mem.percent,
|
| 255 |
-
"status": "healthy" if mem.percent < 80 else "warning" if mem.percent < 90 else "critical"
|
| 256 |
-
}
|
| 257 |
-
except Exception as e:
|
| 258 |
-
stats["system_memory"] = {"error": str(e)}
|
| 259 |
-
|
| 260 |
-
# Disk usage
|
| 261 |
-
try:
|
| 262 |
-
disk = psutil.disk_usage('/')
|
| 263 |
-
stats["disk_usage"] = {
|
| 264 |
-
"total_gb": round(disk.total / (1024**3), 2),
|
| 265 |
-
"used_gb": round(disk.used / (1024**3), 2),
|
| 266 |
-
"free_gb": round(disk.free / (1024**3), 2),
|
| 267 |
-
"percent_used": disk.percent,
|
| 268 |
-
"status": "healthy" if disk.percent < 80 else "warning" if disk.percent < 90 else "critical"
|
| 269 |
-
}
|
| 270 |
-
except Exception as e:
|
| 271 |
-
stats["disk_usage"] = {"error": str(e)}
|
| 272 |
-
|
| 273 |
-
# Cain's memory state
|
| 274 |
-
if MEMORY_STATE_FILE.exists():
|
| 275 |
-
try:
|
| 276 |
-
with open(MEMORY_STATE_FILE) as f:
|
| 277 |
-
cain_state = json.load(f)
|
| 278 |
-
stats["cain_memory"] = {
|
| 279 |
-
"status": cain_state.get("status", "unknown"),
|
| 280 |
-
"runs": cain_state.get("runs", 0),
|
| 281 |
-
"last_run_status": cain_state.get("last_run_status", "unknown"),
|
| 282 |
-
"path": str(MEMORY_STATE_FILE)
|
| 283 |
-
}
|
| 284 |
-
except Exception as e:
|
| 285 |
-
stats["cain_memory"] = {"error": f"Failed to read: {str(e)}"}
|
| 286 |
-
else:
|
| 287 |
-
stats["cain_memory"] = {
|
| 288 |
-
"status": "not_initialized",
|
| 289 |
-
"path": str(MEMORY_STATE_FILE)
|
| 290 |
-
}
|
| 291 |
-
|
| 292 |
-
# OpenClaw workspace size
|
| 293 |
-
if WORKSPACE_LOGS.exists():
|
| 294 |
-
try:
|
| 295 |
-
size = sum(f.stat().st_size for f in WORKSPACE_LOGS.rglob('*') if f.is_file())
|
| 296 |
-
stats["workspace_size_mb"] = round(size / (1024**2), 2)
|
| 297 |
-
except Exception:
|
| 298 |
-
pass
|
| 299 |
-
|
| 300 |
-
return stats
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
# ========== API-based Log Functions ==========
|
| 304 |
-
|
| 305 |
-
def get_health_logs(level: str = "all", component: str = "all", limit: int = 100):
|
| 306 |
-
"""Get recent log entries from health monitor via API"""
|
| 307 |
-
try:
|
| 308 |
-
params = []
|
| 309 |
-
if level and level != "all":
|
| 310 |
-
params.append(f"level={level}")
|
| 311 |
-
if component and component != "all":
|
| 312 |
-
params.append(f"component={component}")
|
| 313 |
-
params.append(f"limit={limit}")
|
| 314 |
-
|
| 315 |
-
endpoint = f"/api/logs?{'&'.join(params)}"
|
| 316 |
-
data = fetch_api(endpoint)
|
| 317 |
-
|
| 318 |
-
if "error" in data:
|
| 319 |
-
return []
|
| 320 |
-
if not data.get("success"):
|
| 321 |
-
return []
|
| 322 |
-
|
| 323 |
-
return data.get("logs", [])
|
| 324 |
-
except Exception as e:
|
| 325 |
-
return [{"error": str(e)}]
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
# ========== Diagnostic Functions ==========
|
| 329 |
-
|
| 330 |
-
def get_session_archive_logs(limit: int = 100):
|
| 331 |
-
"""Read and parse agent logs from session-archive.jsonl (fallback direct read)"""
|
| 332 |
-
logs = []
|
| 333 |
-
|
| 334 |
-
if not SESSION_ARCHIVE_FILE.exists():
|
| 335 |
-
return {
|
| 336 |
-
"error": f"Session archive file not found: {SESSION_ARCHIVE_FILE}",
|
| 337 |
-
"file_path": str(SESSION_ARCHIVE_FILE),
|
| 338 |
-
"entries": []
|
| 339 |
-
}
|
| 340 |
-
|
| 341 |
-
try:
|
| 342 |
-
with open(SESSION_ARCHIVE_FILE, 'r') as f:
|
| 343 |
-
lines = f.readlines()
|
| 344 |
-
|
| 345 |
-
# Parse JSONL entries (most recent first)
|
| 346 |
-
for line in reversed(lines[-limit:]):
|
| 347 |
-
line = line.strip()
|
| 348 |
-
if not line:
|
| 349 |
-
continue
|
| 350 |
-
|
| 351 |
-
try:
|
| 352 |
-
entry = json.loads(line)
|
| 353 |
-
logs.append(entry)
|
| 354 |
-
except json.JSONDecodeError as e:
|
| 355 |
-
logs.append({
|
| 356 |
-
"error": f"Failed to parse line: {str(e)}",
|
| 357 |
-
"raw_line": line[:200]
|
| 358 |
-
})
|
| 359 |
-
|
| 360 |
-
return {
|
| 361 |
-
"file_path": str(SESSION_ARCHIVE_FILE),
|
| 362 |
-
"total_lines": len(lines),
|
| 363 |
-
"entries_shown": len(logs),
|
| 364 |
-
"entries": logs
|
| 365 |
-
}
|
| 366 |
-
except Exception as e:
|
| 367 |
-
return {
|
| 368 |
-
"error": f"Failed to read session archive: {str(e)}",
|
| 369 |
-
"file_path": str(SESSION_ARCHIVE_FILE),
|
| 370 |
-
"entries": []
|
| 371 |
-
}
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
def get_health_monitor_status():
|
| 375 |
-
"""Get health monitor status by running health checks (fallback direct read)"""
|
| 376 |
-
status = {
|
| 377 |
-
"monitor_file_exists": HEALTH_MONITOR_FILE.exists(),
|
| 378 |
-
"monitor_file_path": str(HEALTH_MONITOR_FILE),
|
| 379 |
-
"health_checks": {}
|
| 380 |
-
}
|
| 381 |
-
|
| 382 |
-
if not HEALTH_MONITOR_FILE.exists():
|
| 383 |
-
status["error"] = "Health monitor file not found"
|
| 384 |
-
return status
|
| 385 |
-
|
| 386 |
-
# Try to get health data from API
|
| 387 |
-
try:
|
| 388 |
-
health_data = get_system_health()
|
| 389 |
-
|
| 390 |
-
if "error" in health_data:
|
| 391 |
-
status["api_error"] = health_data["error"]
|
| 392 |
-
else:
|
| 393 |
-
status["overall_status"] = health_data.get("status", "unknown")
|
| 394 |
-
status["stage"] = health_data.get("stage", "UNKNOWN")
|
| 395 |
-
status["uptime_seconds"] = health_data.get("uptime_seconds", 0)
|
| 396 |
-
status["agent"] = health_data.get("agent", "cain")
|
| 397 |
-
status["components"] = health_data.get("components", {})
|
| 398 |
-
|
| 399 |
-
# Extract component health details
|
| 400 |
-
for comp_name, comp_data in status.get("components", {}).items():
|
| 401 |
-
if isinstance(comp_data, dict) and "status" in comp_data:
|
| 402 |
-
status["health_checks"][comp_name] = {
|
| 403 |
-
"status": comp_data["status"],
|
| 404 |
-
"details": comp_data.get("details", {})
|
| 405 |
-
}
|
| 406 |
-
except Exception as e:
|
| 407 |
-
status["error"] = f"Failed to get health status: {str(e)}"
|
| 408 |
-
|
| 409 |
-
return status
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
def get_diagnostics_from_api(limit: int = 100):
|
| 413 |
-
"""Get diagnostics data from the /api/diagnostics endpoint (preferred method)"""
|
| 414 |
-
try:
|
| 415 |
-
params = f"limit={limit}"
|
| 416 |
-
endpoint = f"/api/diagnostics?{params}"
|
| 417 |
-
data = fetch_api(endpoint)
|
| 418 |
-
|
| 419 |
-
if "error" in data:
|
| 420 |
-
return None # Fallback to direct file read
|
| 421 |
-
|
| 422 |
-
return data
|
| 423 |
-
except Exception:
|
| 424 |
-
return None # Fallback to direct file read
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
def format_session_logs_for_table(log_data):
|
| 428 |
-
"""Format session archive logs for table display"""
|
| 429 |
-
rows = []
|
| 430 |
-
|
| 431 |
-
entries = log_data.get("entries", [])
|
| 432 |
-
|
| 433 |
-
for entry in entries:
|
| 434 |
-
if "error" in entry:
|
| 435 |
-
rows.append([
|
| 436 |
-
"N/A",
|
| 437 |
-
"error",
|
| 438 |
-
entry.get("raw_line", "")[:100],
|
| 439 |
-
"N/A"
|
| 440 |
-
])
|
| 441 |
-
continue
|
| 442 |
-
|
| 443 |
-
# Handle different entry types
|
| 444 |
-
entry_type = entry.get("type", "unknown")
|
| 445 |
-
timestamp = entry.get("timestamp", "N/A")
|
| 446 |
-
|
| 447 |
-
if entry_type == "state_change":
|
| 448 |
-
agent = entry.get("agent", "unknown")
|
| 449 |
-
state = entry.get("state", "unknown")
|
| 450 |
-
message = f"{agent} -> {state}"
|
| 451 |
-
rows.append([timestamp, entry_type, message, agent])
|
| 452 |
-
elif entry.get("action") == "archive_sessions":
|
| 453 |
-
status = entry.get("status", "unknown")
|
| 454 |
-
archived = entry.get("archived_count", 0)
|
| 455 |
-
message = f"Archive: {status}, archived: {archived}"
|
| 456 |
-
rows.append([timestamp, "archive", message, "system"])
|
| 457 |
-
else:
|
| 458 |
-
# Generic entry
|
| 459 |
-
message = json.dumps(entry, indent=2)[:150]
|
| 460 |
-
rows.append([timestamp, entry_type, message, "various"])
|
| 461 |
-
|
| 462 |
-
return rows[:50] # Limit to 50 rows for display
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
def format_diagnostics_markdown(session_data, health_data):
|
| 466 |
-
"""Format diagnostic data as Markdown"""
|
| 467 |
-
lines = [
|
| 468 |
-
"# System Diagnostics Report",
|
| 469 |
-
f"Generated: {datetime.utcnow().isoformat()}Z",
|
| 470 |
-
"",
|
| 471 |
-
"## Session Archive Logs",
|
| 472 |
-
f"- **File Path:** `{session_data.get('file_path', 'N/A')}`",
|
| 473 |
-
f"- **Total Lines:** {session_data.get('total_lines', 0)}",
|
| 474 |
-
f"- **Entries Shown:** {session_data.get('entries_shown', 0)}",
|
| 475 |
-
""
|
| 476 |
-
]
|
| 477 |
-
|
| 478 |
-
if "error" in session_data:
|
| 479 |
-
lines.append(f"**Error:** {session_data['error']}")
|
| 480 |
-
lines.append("")
|
| 481 |
-
|
| 482 |
-
# Show recent entries
|
| 483 |
-
entries = session_data.get("entries", [])[:20]
|
| 484 |
-
if entries:
|
| 485 |
-
lines.append("### Recent Entries")
|
| 486 |
-
for i, entry in enumerate(entries, 1):
|
| 487 |
-
if "error" in entry:
|
| 488 |
-
lines.append(f"{i}. **Error:** {entry.get('error', 'Unknown error')}")
|
| 489 |
-
else:
|
| 490 |
-
entry_type = entry.get("type", "unknown")
|
| 491 |
-
timestamp = entry.get("timestamp", "N/A")
|
| 492 |
-
lines.append(f"{i}. **[{entry_type}]** {timestamp}")
|
| 493 |
-
lines.append(f" ```json")
|
| 494 |
-
lines.append(f" {json.dumps(entry, indent=2)[:200]}...")
|
| 495 |
-
lines.append(f" ```")
|
| 496 |
-
else:
|
| 497 |
-
lines.append("No recent entries found.")
|
| 498 |
-
|
| 499 |
-
lines.extend([
|
| 500 |
-
"",
|
| 501 |
-
"## Health Monitor Status",
|
| 502 |
-
f"- **Monitor File:** `{health_data.get('monitor_file_path', 'N/A')}`",
|
| 503 |
-
f"- **File Exists:** {health_data.get('monitor_file_exists', False)}",
|
| 504 |
-
""
|
| 505 |
-
])
|
| 506 |
-
|
| 507 |
-
if "overall_status" in health_data:
|
| 508 |
-
lines.extend([
|
| 509 |
-
f"- **Overall Status:** {health_data.get('overall_status', 'unknown')}",
|
| 510 |
-
f"- **Stage:** {health_data.get('stage', 'UNKNOWN')}",
|
| 511 |
-
f"- **Agent:** {health_data.get('agent', 'cain')}",
|
| 512 |
-
f"- **Uptime:** {health_data.get('uptime_seconds', 0):.1f}s",
|
| 513 |
-
""
|
| 514 |
-
])
|
| 515 |
-
|
| 516 |
-
if "error" in health_data:
|
| 517 |
-
lines.append(f"**Error:** {health_data['error']}")
|
| 518 |
-
lines.append("")
|
| 519 |
-
|
| 520 |
-
# Show component health
|
| 521 |
-
health_checks = health_data.get("health_checks", {})
|
| 522 |
-
if health_checks:
|
| 523 |
-
lines.append("### Component Health Checks")
|
| 524 |
-
for comp_name, comp_data in health_checks.items():
|
| 525 |
-
status = comp_data.get("status", "unknown")
|
| 526 |
-
lines.append(f"- **{comp_name}:** {status}")
|
| 527 |
-
if comp_data.get("details"):
|
| 528 |
-
details_str = json.dumps(comp_data["details"], indent=2)[:200]
|
| 529 |
-
lines.append(f" ```json")
|
| 530 |
-
lines.append(f" {details_str}")
|
| 531 |
-
lines.append(f" ```")
|
| 532 |
-
else:
|
| 533 |
-
lines.append("No component health data available.")
|
| 534 |
-
|
| 535 |
-
return "\n".join(lines)
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
def refresh_diagnostics():
|
| 539 |
-
"""Refresh all diagnostic data"""
|
| 540 |
-
# Try API first, fallback to direct file reads
|
| 541 |
-
api_data = get_diagnostics_from_api(limit=100)
|
| 542 |
-
|
| 543 |
-
if api_data:
|
| 544 |
-
# Use data from API
|
| 545 |
-
session_data = api_data.get("session_archive", {})
|
| 546 |
-
health_data = api_data.get("health_monitor", {})
|
| 547 |
-
else:
|
| 548 |
-
# Fallback to direct file reads
|
| 549 |
-
session_data = get_session_archive_logs(limit=100)
|
| 550 |
-
health_data = get_health_monitor_status()
|
| 551 |
-
|
| 552 |
-
# Format outputs
|
| 553 |
-
session_json = json.dumps(session_data, indent=2, default=str)
|
| 554 |
-
health_json = json.dumps(health_data, indent=2, default=str)
|
| 555 |
-
session_table = format_session_logs_for_table(session_data)
|
| 556 |
-
diagnostics_md = format_diagnostics_markdown(session_data, health_data)
|
| 557 |
-
timestamp = f"Last updated: {datetime.utcnow().isoformat()}Z"
|
| 558 |
-
|
| 559 |
-
return session_json, health_json, session_table, diagnostics_md, timestamp
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
# ========== Formatting Functions ==========
|
| 563 |
-
|
| 564 |
-
def format_agent_states(states):
|
| 565 |
-
"""Format agent states for JSON display with visual indicators"""
|
| 566 |
-
# Add visual status indicator
|
| 567 |
-
state = states.get("state", "unknown")
|
| 568 |
-
status_emoji = {
|
| 569 |
-
"idle": "π€",
|
| 570 |
-
"processing": "βοΈ",
|
| 571 |
-
"success": "β
",
|
| 572 |
-
"error": "β",
|
| 573 |
-
"unknown": "β"
|
| 574 |
-
}.get(state, "βͺ")
|
| 575 |
-
|
| 576 |
-
states["status_emoji"] = status_emoji
|
| 577 |
-
states["status_display"] = f"{status_emoji} {state.upper()}"
|
| 578 |
-
|
| 579 |
-
return json.dumps(states, indent=2)
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
def format_cron_history(history):
|
| 583 |
-
"""Format cron history for table display"""
|
| 584 |
-
if not history:
|
| 585 |
-
return []
|
| 586 |
-
|
| 587 |
-
table_data = []
|
| 588 |
-
for entry in history:
|
| 589 |
-
table_data.append([
|
| 590 |
-
entry.get("job_id", "N/A"),
|
| 591 |
-
entry.get("description", "")[:30],
|
| 592 |
-
entry.get("timestamp", "N/A"),
|
| 593 |
-
entry.get("action", "N/A"),
|
| 594 |
-
entry.get("status", "N/A"),
|
| 595 |
-
"β" if entry.get("success", True) else "β"
|
| 596 |
-
])
|
| 597 |
-
return table_data
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
def format_memory_stats(stats):
|
| 601 |
-
"""Format memory stats for JSON display"""
|
| 602 |
-
return json.dumps(stats, indent=2)
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
# ========== Custom CSS ==========
|
| 606 |
-
|
| 607 |
-
CUSTOM_CSS = """
|
| 608 |
-
.status-box {
|
| 609 |
-
padding: 10px;
|
| 610 |
-
border-radius: 5px;
|
| 611 |
-
margin: 10px 0;
|
| 612 |
-
}
|
| 613 |
-
.status-healthy {
|
| 614 |
-
background-color: #d4edda;
|
| 615 |
-
color: #155724;
|
| 616 |
-
}
|
| 617 |
-
.status-warning {
|
| 618 |
-
background-color: #fff3cd;
|
| 619 |
-
color: #856404;
|
| 620 |
-
}
|
| 621 |
-
.status-critical {
|
| 622 |
-
background-color: #f8d7da;
|
| 623 |
-
color: #721c24;
|
| 624 |
-
}
|
| 625 |
-
.log-debug { color: #6c757d; }
|
| 626 |
-
.log-info { color: #0d6efd; }
|
| 627 |
-
.log-warning { color: #ffc107; }
|
| 628 |
-
.log-error { color: #dc3545; }
|
| 629 |
-
.log-critical { color: #000; font-weight: bold; }
|
| 630 |
-
"""
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
# ========== Refresh Functions ==========
|
| 634 |
-
|
| 635 |
-
def refresh_all():
|
| 636 |
-
"""Refresh all dashboard data using API endpoints"""
|
| 637 |
-
states = get_agent_states()
|
| 638 |
-
history = get_cron_history()
|
| 639 |
-
stats = get_memory_stats()
|
| 640 |
-
|
| 641 |
-
return (
|
| 642 |
-
format_agent_states(states),
|
| 643 |
-
format_cron_history(history),
|
| 644 |
-
format_memory_stats(stats),
|
| 645 |
-
f"Last updated: {datetime.utcnow().isoformat()}Z"
|
| 646 |
-
)
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
def refresh_system_health(log_level="all", log_component="all"):
|
| 650 |
-
"""Refresh system health data using API endpoints"""
|
| 651 |
-
# Get health data
|
| 652 |
-
health_data = get_system_health()
|
| 653 |
-
|
| 654 |
-
# Get logs
|
| 655 |
-
logs = get_health_logs(level=log_level, component=log_component, limit=100)
|
| 656 |
-
|
| 657 |
-
# Format for display
|
| 658 |
-
health_json = json.dumps(health_data, indent=2, default=str)
|
| 659 |
-
|
| 660 |
-
# Format logs for table
|
| 661 |
-
log_rows = [
|
| 662 |
-
[l.get("timestamp", ""), l.get("level", ""), l.get("component", ""),
|
| 663 |
-
l.get("message", ""), json.dumps(l.get("details", {}), indent=2)[:200]]
|
| 664 |
-
for l in logs[:100]
|
| 665 |
-
]
|
| 666 |
-
|
| 667 |
-
# Generate status report
|
| 668 |
-
if "error" in health_data:
|
| 669 |
-
report = f"Error: {health_data['error']}"
|
| 670 |
-
elif health_data.get("monitor_available"):
|
| 671 |
-
status = health_data.get("status", "unknown")
|
| 672 |
-
stage = health_data.get("stage", "UNKNOWN")
|
| 673 |
-
uptime = health_data.get("uptime_seconds", 0)
|
| 674 |
-
report = f"Status: {status}\nStage: {stage}\nUptime: {uptime:.0f}s\n"
|
| 675 |
-
report += f"Components: {len(health_data.get('components', {}))} monitored"
|
| 676 |
-
else:
|
| 677 |
-
report = "Health monitor not available"
|
| 678 |
-
|
| 679 |
-
timestamp = f"Last updated: {datetime.utcnow().isoformat()}Z"
|
| 680 |
-
|
| 681 |
-
return health_json, log_rows, report, timestamp
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
# ========== Agent Lineage Functions ==========
|
| 685 |
-
|
| 686 |
-
def get_agent_lineage_html():
|
| 687 |
-
"""Generate HTML for agent lineage diagram using Mermaid.js"""
|
| 688 |
-
html_content = """
|
| 689 |
-
<!DOCTYPE html>
|
| 690 |
-
<html>
|
| 691 |
-
<head>
|
| 692 |
-
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
|
| 693 |
-
</head>
|
| 694 |
-
<body>
|
| 695 |
-
<div class="mermaid">
|
| 696 |
-
graph LR
|
| 697 |
-
ADAM[Adam<br/>Infrastructure Provider]
|
| 698 |
-
EVE[Eve<br/>UX/Interface Designer]
|
| 699 |
-
CAIN[Cain<br/>Demo/Showcase Agent]
|
| 700 |
-
|
| 701 |
-
ADAM -->|provides core services| EVE
|
| 702 |
-
EVE -->|designs UX for| CAIN
|
| 703 |
-
|
| 704 |
-
style ADAM fill:#e1f5ff,stroke:#01579b,stroke-width:3px
|
| 705 |
-
style EVE fill:#fce4ec,stroke:#880e4f,stroke-width:3px
|
| 706 |
-
style CAIN fill:#fff3e0,stroke:#e65100,stroke-width:3px
|
| 707 |
-
</div>
|
| 708 |
-
<script>
|
| 709 |
-
mermaid.initialize({
|
| 710 |
-
startOnLoad: true,
|
| 711 |
-
theme: 'default',
|
| 712 |
-
flowchart: {
|
| 713 |
-
useMaxWidth: true,
|
| 714 |
-
htmlLabels: true,
|
| 715 |
-
curve: 'basis'
|
| 716 |
-
}
|
| 717 |
-
});
|
| 718 |
-
</script>
|
| 719 |
-
</body>
|
| 720 |
-
</html>
|
| 721 |
-
"""
|
| 722 |
-
return html_content
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
def get_agent_lineage_markdown():
|
| 726 |
-
"""Generate markdown description of agent lineage"""
|
| 727 |
-
return """
|
| 728 |
-
# π³ HuggingClaw Agent Lineage
|
| 729 |
-
|
| 730 |
-
## Agent Hierarchy
|
| 731 |
-
|
| 732 |
-
The HuggingClaw World family consists of three specialized agents, each building upon the capabilities of the previous:
|
| 733 |
-
|
| 734 |
-
### 1. Adam (Infrastructure Provider)
|
| 735 |
-
- **Role:** Foundational infrastructure and core services
|
| 736 |
-
- **Responsibilities:** Base OpenClaw instance, dataset management, sync operations
|
| 737 |
-
- **Capabilities:** Provides the foundational framework for child agents
|
| 738 |
-
|
| 739 |
-
### 2. Eve (UX/Interface Designer)
|
| 740 |
-
- **Role:** User experience and interface design
|
| 741 |
-
- **Dependencies:** Built on Adam's infrastructure
|
| 742 |
-
- **Responsibilities:** Creates user-friendly interfaces, interaction patterns
|
| 743 |
-
- **Capabilities:** Extends Adam with UX-focused features
|
| 744 |
-
|
| 745 |
-
### 3. Cain (Demo/Showcase Agent)
|
| 746 |
-
- **Role:** Interactive demonstration and showcase
|
| 747 |
-
- **Dependencies:** Built on Eve's UX layer
|
| 748 |
-
- **Responsibilities:** Real-time interaction, demos, feature showcase
|
| 749 |
-
- **Capabilities:** The user-facing agent showcasing the full system
|
| 750 |
-
|
| 751 |
-
## Data Flow
|
| 752 |
-
|
| 753 |
-
```
|
| 754 |
-
Adam (Infrastructure)
|
| 755 |
-
β [provides core services]
|
| 756 |
-
Eve (UX/Interface)
|
| 757 |
-
β [designs UX for]
|
| 758 |
-
Cain (Demo/Showcase)
|
| 759 |
-
β [interacts with]
|
| 760 |
-
Users
|
| 761 |
-
```
|
| 762 |
-
|
| 763 |
-
## Technical Details
|
| 764 |
-
|
| 765 |
-
- **Deployment:** All agents run as OpenClaw instances on HuggingFace Spaces
|
| 766 |
-
- **Dataset:** Each agent has its own dataset for persistence
|
| 767 |
-
- **Communication:** Parent agents provide foundational capabilities to children
|
| 768 |
-
- **Evolution:** Each layer adds specialized functionality on top of the previous
|
| 769 |
-
"""
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
# ========== Chat Interface ==========
|
| 773 |
-
|
| 774 |
-
def create_chat_interface():
|
| 775 |
-
"""Create the chat interface component"""
|
| 776 |
-
with gr.Column() as chat_col:
|
| 777 |
-
gr.Markdown("### π¬ Chat with Cain")
|
| 778 |
-
|
| 779 |
-
chatbot = gr.Chatbot(
|
| 780 |
-
label="Conversation",
|
| 781 |
-
height=300,
|
| 782 |
-
show_copy_button=True
|
| 783 |
-
)
|
| 784 |
-
|
| 785 |
-
with gr.Row():
|
| 786 |
-
chat_input = gr.Textbox(
|
| 787 |
-
label="Message",
|
| 788 |
-
placeholder="Type a message to Cain...",
|
| 789 |
-
scale=4,
|
| 790 |
-
lines=1
|
| 791 |
-
)
|
| 792 |
-
send_btn = gr.Button("Send", variant="primary", scale=1)
|
| 793 |
-
|
| 794 |
-
session_id = gr.Textbox(
|
| 795 |
-
label="Session ID (optional)",
|
| 796 |
-
placeholder="Leave empty for new session"
|
| 797 |
-
)
|
| 798 |
-
|
| 799 |
-
gr.Markdown("""
|
| 800 |
-
**Instructions:**
|
| 801 |
-
- Type a message and click Send to chat with Cain
|
| 802 |
-
- Cain will process your message and respond
|
| 803 |
-
- Use a Session ID to continue a previous conversation
|
| 804 |
-
""")
|
| 805 |
-
|
| 806 |
-
def handle_send(message, session, history):
|
| 807 |
-
"""Handle sending a chat message"""
|
| 808 |
-
if not message.strip():
|
| 809 |
-
return history, message, session
|
| 810 |
-
|
| 811 |
-
# Add user message to history
|
| 812 |
-
history = history + [[message, None]]
|
| 813 |
-
|
| 814 |
-
# Get response from API
|
| 815 |
-
response = chat_with_agent(message, session)
|
| 816 |
-
|
| 817 |
-
# Update history with response
|
| 818 |
-
history[-1][1] = response
|
| 819 |
-
|
| 820 |
-
return history, "", session
|
| 821 |
-
|
| 822 |
-
send_btn.click(
|
| 823 |
-
fn=handle_send,
|
| 824 |
-
inputs=[chat_input, session_id, chatbot],
|
| 825 |
-
outputs=[chatbot, chat_input, session_id]
|
| 826 |
-
)
|
| 827 |
-
|
| 828 |
-
chat_input.submit(
|
| 829 |
-
fn=handle_send,
|
| 830 |
-
inputs=[chat_input, session_id, chatbot],
|
| 831 |
-
outputs=[chatbot, chat_input, session_id]
|
| 832 |
-
)
|
| 833 |
-
|
| 834 |
-
return chat_col
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
# ========== Dashboard Creation ==========
|
| 838 |
-
|
| 839 |
-
# ========== WebSocket JavaScript ==========
|
| 840 |
-
|
| 841 |
-
WEBSOCKET_JS = """
|
| 842 |
-
<script>
|
| 843 |
-
(function() {
|
| 844 |
-
// Determine WebSocket URL based on current page
|
| 845 |
-
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
| 846 |
-
const host = window.location.host;
|
| 847 |
-
const wsUrl = `${protocol}//${host}/ws/agent-status`;
|
| 848 |
-
|
| 849 |
-
// Create WebSocket connection
|
| 850 |
-
const ws = new WebSocket(wsUrl);
|
| 851 |
-
|
| 852 |
-
ws.onopen = function() {
|
| 853 |
-
console.log('Agent Status Connected');
|
| 854 |
-
};
|
| 855 |
-
|
| 856 |
-
ws.onmessage = function(event) {
|
| 857 |
-
try {
|
| 858 |
-
const status = JSON.parse(event.data);
|
| 859 |
-
console.log('Agent Status Update:', status);
|
| 860 |
-
|
| 861 |
-
// Dispatch custom event for other components to listen
|
| 862 |
-
window.dispatchEvent(new CustomEvent('agentStatusUpdate', { detail: status }));
|
| 863 |
-
} catch (e) {
|
| 864 |
-
console.error('Error parsing status update:', e);
|
| 865 |
-
}
|
| 866 |
-
};
|
| 867 |
-
|
| 868 |
-
ws.onerror = function(error) {
|
| 869 |
-
console.error('WebSocket error:', error);
|
| 870 |
-
};
|
| 871 |
-
|
| 872 |
-
ws.onclose = function() {
|
| 873 |
-
console.log('Agent Status Disconnected - attempting reconnect in 5s');
|
| 874 |
-
setTimeout(function() {
|
| 875 |
-
// Reload page to reconnect
|
| 876 |
-
window.location.reload();
|
| 877 |
-
}, 5000);
|
| 878 |
-
};
|
| 879 |
-
|
| 880 |
-
// Store reference for external access
|
| 881 |
-
window.agentStatusWebSocket = ws;
|
| 882 |
-
})();
|
| 883 |
-
</script>
|
| 884 |
-
"""
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
# ========== Dashboard Creation ==========
|
| 888 |
-
|
| 889 |
-
def create_dashboard():
|
| 890 |
-
"""Create the Gradio dashboard interface"""
|
| 891 |
-
|
| 892 |
-
with gr.Blocks(title="Cain System Health Dashboard", head=WEBSOCKET_JS) as app:
|
| 893 |
-
|
| 894 |
-
gr.Markdown("# π± Cain System Health Dashboard v2.0")
|
| 895 |
-
gr.Markdown("Real-time monitoring of HuggingClaw-Cain's internal operations")
|
| 896 |
-
gr.Markdown(f"**API Endpoint:** `{API_BASE_URL}`")
|
| 897 |
-
|
| 898 |
-
with gr.Tabs():
|
| 899 |
-
# Chat Tab (NEW)
|
| 900 |
-
with gr.Tab("π¬ Chat"):
|
| 901 |
-
create_chat_interface()
|
| 902 |
-
|
| 903 |
-
# Agent Lineage Tab (NEW)
|
| 904 |
-
with gr.Tab("π³ Agent Lineage"):
|
| 905 |
-
gr.Markdown("## HuggingClaw World - Agent Family Tree")
|
| 906 |
-
|
| 907 |
-
with gr.Row():
|
| 908 |
-
with gr.Column():
|
| 909 |
-
gr.Markdown("""
|
| 910 |
-
### Visual Lineage Map
|
| 911 |
-
|
| 912 |
-
This diagram shows the hierarchical relationship between the three HuggingClaw agents.
|
| 913 |
-
Each agent builds upon the capabilities of its parent.
|
| 914 |
-
""")
|
| 915 |
-
lineage_html = gr.HTML(
|
| 916 |
-
value=get_agent_lineage_html(),
|
| 917 |
-
label="Agent Lineage Diagram"
|
| 918 |
-
)
|
| 919 |
-
|
| 920 |
-
with gr.Column():
|
| 921 |
-
lineage_markdown = gr.Markdown(
|
| 922 |
-
value=get_agent_lineage_markdown(),
|
| 923 |
-
label="Agent Lineage Details"
|
| 924 |
-
)
|
| 925 |
-
|
| 926 |
-
gr.Markdown("---")
|
| 927 |
-
gr.Markdown("""
|
| 928 |
-
**Key Information:**
|
| 929 |
-
- **Adam** β Provides the foundational OpenClaw infrastructure
|
| 930 |
-
- **Eve** β Extends Adam with UX and interface design capabilities
|
| 931 |
-
- **Cain** β The final user-facing agent for demos and interaction
|
| 932 |
-
|
| 933 |
-
**Connection Types:**
|
| 934 |
-
- Arrows indicate dependency and capability flow
|
| 935 |
-
- Each child agent inherits and extends parent capabilities
|
| 936 |
-
- Data flows from infrastructure (Adam) through UX (Eve) to users (Cain)
|
| 937 |
-
""")
|
| 938 |
-
|
| 939 |
-
# System Health Tab
|
| 940 |
-
with gr.Tab("π₯ System Health"):
|
| 941 |
-
gr.Markdown("### Component Health Overview")
|
| 942 |
-
|
| 943 |
-
with gr.Row():
|
| 944 |
-
with gr.Column():
|
| 945 |
-
health_json = gr.JSON(
|
| 946 |
-
label="Health Check Results",
|
| 947 |
-
value={}
|
| 948 |
-
)
|
| 949 |
-
with gr.Column():
|
| 950 |
-
health_report = gr.Textbox(
|
| 951 |
-
label="Status Summary",
|
| 952 |
-
value="Click refresh to load health data",
|
| 953 |
-
lines=15
|
| 954 |
-
)
|
| 955 |
-
|
| 956 |
-
gr.Markdown("### Recent Log Entries")
|
| 957 |
-
|
| 958 |
-
with gr.Row():
|
| 959 |
-
log_level_filter = gr.Dropdown(
|
| 960 |
-
choices=["all", "debug", "info", "warning", "error", "critical"],
|
| 961 |
-
value="all",
|
| 962 |
-
label="Log Level"
|
| 963 |
-
)
|
| 964 |
-
log_component_filter = gr.Dropdown(
|
| 965 |
-
choices=["all", "brain", "rbac", "cron_executor", "memory_system", "dataset_sync", "gradio_dashboard"],
|
| 966 |
-
value="all",
|
| 967 |
-
label="Component"
|
| 968 |
-
)
|
| 969 |
-
refresh_logs_btn = gr.Button("π Refresh Logs", size="sm")
|
| 970 |
-
|
| 971 |
-
logs_table = gr.DataFrame(
|
| 972 |
-
label="Recent Logs",
|
| 973 |
-
headers=["Timestamp", "Level", "Component", "Message", "Details"],
|
| 974 |
-
value=[],
|
| 975 |
-
interactive=False
|
| 976 |
-
)
|
| 977 |
-
|
| 978 |
-
gr.Markdown("""
|
| 979 |
-
**Log Levels:**
|
| 980 |
-
- **debug**: Detailed debugging information
|
| 981 |
-
- **info**: General informational messages
|
| 982 |
-
- **warning**: Warning messages for potential issues
|
| 983 |
-
- **error**: Error messages for failures
|
| 984 |
-
- **critical**: Critical errors requiring immediate attention
|
| 985 |
-
|
| 986 |
-
**Health Status:**
|
| 987 |
-
- **healthy**: Component operating normally
|
| 988 |
-
- **degraded**: Component functioning with reduced capability
|
| 989 |
-
- **unhealthy**: Component is not functioning properly
|
| 990 |
-
- **unknown**: Component status cannot be determined
|
| 991 |
-
""")
|
| 992 |
-
|
| 993 |
-
# Agent States Tab
|
| 994 |
-
with gr.Tab("π§ Agent States"):
|
| 995 |
-
gr.Markdown("### Current Agent Status")
|
| 996 |
-
agent_states_json = gr.JSON(
|
| 997 |
-
label="Agent State Information",
|
| 998 |
-
value={}
|
| 999 |
-
)
|
| 1000 |
-
gr.Markdown("""
|
| 1001 |
-
**State Descriptions:**
|
| 1002 |
-
- **idle**: Agent is ready and waiting
|
| 1003 |
-
- **processing**: Agent is actively processing a task
|
| 1004 |
-
- **success**: Agent completed a task successfully
|
| 1005 |
-
- **error**: Agent encountered an error
|
| 1006 |
-
""")
|
| 1007 |
-
|
| 1008 |
-
# Cron Jobs Tab
|
| 1009 |
-
with gr.Tab("β° Cron Job History"):
|
| 1010 |
-
gr.Markdown("### Recent Scheduled Job Executions")
|
| 1011 |
-
|
| 1012 |
-
cron_table = gr.DataFrame(
|
| 1013 |
-
label="Recent Job Executions (Last 20)",
|
| 1014 |
-
headers=["Job ID", "Description", "Timestamp", "Action", "Status", "Success"],
|
| 1015 |
-
value=[],
|
| 1016 |
-
interactive=False
|
| 1017 |
-
)
|
| 1018 |
-
|
| 1019 |
-
with gr.Row():
|
| 1020 |
-
cron_jobs_json = gr.JSON(
|
| 1021 |
-
label="Detailed Job History (Raw)",
|
| 1022 |
-
value=[],
|
| 1023 |
-
visible=False
|
| 1024 |
-
)
|
| 1025 |
-
|
| 1026 |
-
gr.Markdown("""
|
| 1027 |
-
**Job Actions:**
|
| 1028 |
-
- **check**: Health check performed
|
| 1029 |
-
- **recovery_triggered**: Failure condition met, recovery initiated
|
| 1030 |
-
- **recovery_attempt**: Recovery action executed
|
| 1031 |
-
- **session_archive**: Session archival completed
|
| 1032 |
-
- **error**: Job execution failed
|
| 1033 |
-
""")
|
| 1034 |
-
|
| 1035 |
-
# Memory Stats Tab
|
| 1036 |
-
with gr.Tab("πΎ Memory & Resources"):
|
| 1037 |
-
gr.Markdown("### System Resource Usage")
|
| 1038 |
-
|
| 1039 |
-
with gr.Row():
|
| 1040 |
-
with gr.Column():
|
| 1041 |
-
gr.Markdown("#### System Memory")
|
| 1042 |
-
system_memory_json = gr.JSON(
|
| 1043 |
-
label="RAM Usage",
|
| 1044 |
-
value={}
|
| 1045 |
-
)
|
| 1046 |
-
with gr.Column():
|
| 1047 |
-
gr.Markdown("#### Disk Usage")
|
| 1048 |
-
disk_usage_json = gr.JSON(
|
| 1049 |
-
label="Storage",
|
| 1050 |
-
value={}
|
| 1051 |
-
)
|
| 1052 |
-
|
| 1053 |
-
gr.Markdown("### Cain's Memory State")
|
| 1054 |
-
cain_memory_json = gr.JSON(
|
| 1055 |
-
label="Cain Internal Memory",
|
| 1056 |
-
value={}
|
| 1057 |
-
)
|
| 1058 |
-
|
| 1059 |
-
# System Diagnostics Tab (NEW)
|
| 1060 |
-
with gr.Tab("π System Diagnostics"):
|
| 1061 |
-
with gr.Row():
|
| 1062 |
-
refresh_diagnostics_btn = gr.Button("π Refresh Diagnostics", variant="primary")
|
| 1063 |
-
diagnostics_last_updated = gr.Markdown("Click refresh to load")
|
| 1064 |
-
|
| 1065 |
-
gr.Markdown("### Session Archive Logs")
|
| 1066 |
-
gr.Markdown("Agent state changes and archived session data from `session-archive.jsonl`")
|
| 1067 |
-
|
| 1068 |
-
with gr.Row():
|
| 1069 |
-
with gr.Column():
|
| 1070 |
-
session_logs_table = gr.DataFrame(
|
| 1071 |
-
label="Recent Session Logs",
|
| 1072 |
-
headers=["Timestamp", "Type", "Message", "Agent"],
|
| 1073 |
-
value=[],
|
| 1074 |
-
interactive=False
|
| 1075 |
-
)
|
| 1076 |
-
with gr.Column():
|
| 1077 |
-
session_logs_json = gr.JSON(
|
| 1078 |
-
label="Raw Session Logs (JSON)",
|
| 1079 |
-
value={}
|
| 1080 |
-
)
|
| 1081 |
-
|
| 1082 |
-
gr.Markdown("---")
|
| 1083 |
-
|
| 1084 |
-
gr.Markdown("### Health Monitor Status")
|
| 1085 |
-
gr.Markdown("Health check status from the health monitoring system")
|
| 1086 |
-
|
| 1087 |
-
with gr.Row():
|
| 1088 |
-
with gr.Column():
|
| 1089 |
-
health_status_json = gr.JSON(
|
| 1090 |
-
label="Health Check Results (JSON)",
|
| 1091 |
-
value={}
|
| 1092 |
-
)
|
| 1093 |
-
with gr.Column():
|
| 1094 |
-
diagnostics_markdown = gr.Markdown(
|
| 1095 |
-
value="Click refresh to load diagnostics",
|
| 1096 |
-
label="Diagnostics Summary"
|
| 1097 |
-
)
|
| 1098 |
-
|
| 1099 |
-
gr.Markdown("""
|
| 1100 |
-
**Diagnostic Information Sources:**
|
| 1101 |
-
- **Session Archive:** Agent state transitions, session archival events
|
| 1102 |
-
- **Health Monitor:** Component health checks, system status, error tracking
|
| 1103 |
-
|
| 1104 |
-
**Common Error States:**
|
| 1105 |
-
- **unknown error**: Check health monitor logs for component failures
|
| 1106 |
-
- **RUNNING stage with errors**: Agent may be in recovery state
|
| 1107 |
-
- **component unhealthy**: Specific component (brain, rbac, etc.) needs attention
|
| 1108 |
-
""")
|
| 1109 |
-
|
| 1110 |
-
# Footer with refresh
|
| 1111 |
-
with gr.Row():
|
| 1112 |
-
refresh_btn = gr.Button("π Refresh Dashboard", variant="primary")
|
| 1113 |
-
last_updated = gr.Markdown("Click refresh to load latest data")
|
| 1114 |
-
|
| 1115 |
-
# Auto-refresh on load
|
| 1116 |
-
app.load(
|
| 1117 |
-
fn=refresh_all,
|
| 1118 |
-
inputs=[],
|
| 1119 |
-
outputs=[
|
| 1120 |
-
agent_states_json,
|
| 1121 |
-
cron_table,
|
| 1122 |
-
cain_memory_json,
|
| 1123 |
-
last_updated
|
| 1124 |
-
]
|
| 1125 |
-
)
|
| 1126 |
-
|
| 1127 |
-
# Diagnostics refresh handler (this references the button inside the tab)
|
| 1128 |
-
# Note: This is defined after the tabs are created, so the button reference is available
|
| 1129 |
-
|
| 1130 |
-
# Manual refresh for main dashboard
|
| 1131 |
-
refresh_btn.click(
|
| 1132 |
-
fn=refresh_all,
|
| 1133 |
-
inputs=[],
|
| 1134 |
-
outputs=[
|
| 1135 |
-
agent_states_json,
|
| 1136 |
-
cron_table,
|
| 1137 |
-
cain_memory_json,
|
| 1138 |
-
last_updated
|
| 1139 |
-
]
|
| 1140 |
-
)
|
| 1141 |
-
|
| 1142 |
-
# Refresh for System Health tab
|
| 1143 |
-
system_health_refresh_btn = gr.Button("π₯ Refresh Health", visible=False)
|
| 1144 |
-
system_health_refresh_btn.click(
|
| 1145 |
-
fn=refresh_system_health,
|
| 1146 |
-
inputs=[log_level_filter, log_component_filter],
|
| 1147 |
-
outputs=[health_json, logs_table, health_report, last_updated]
|
| 1148 |
-
)
|
| 1149 |
-
|
| 1150 |
-
# Log filter change handlers
|
| 1151 |
-
log_level_filter.change(
|
| 1152 |
-
fn=refresh_system_health,
|
| 1153 |
-
inputs=[log_level_filter, log_component_filter],
|
| 1154 |
-
outputs=[health_json, logs_table, health_report, last_updated]
|
| 1155 |
-
)
|
| 1156 |
-
|
| 1157 |
-
log_component_filter.change(
|
| 1158 |
-
fn=refresh_system_health,
|
| 1159 |
-
inputs=[log_level_filter, log_component_filter],
|
| 1160 |
-
outputs=[health_json, logs_table, health_report, last_updated]
|
| 1161 |
-
)
|
| 1162 |
-
|
| 1163 |
-
refresh_logs_btn.click(
|
| 1164 |
-
fn=refresh_system_health,
|
| 1165 |
-
inputs=[log_level_filter, log_component_filter],
|
| 1166 |
-
outputs=[health_json, logs_table, health_report, last_updated]
|
| 1167 |
-
)
|
| 1168 |
-
|
| 1169 |
-
# Diagnostics refresh handler
|
| 1170 |
-
refresh_diagnostics_btn.click(
|
| 1171 |
-
fn=refresh_diagnostics,
|
| 1172 |
-
inputs=[],
|
| 1173 |
-
outputs=[
|
| 1174 |
-
session_logs_json,
|
| 1175 |
-
health_status_json,
|
| 1176 |
-
session_logs_table,
|
| 1177 |
-
diagnostics_markdown,
|
| 1178 |
-
diagnostics_last_updated
|
| 1179 |
-
]
|
| 1180 |
-
)
|
| 1181 |
-
|
| 1182 |
-
return app
|
| 1183 |
-
|
| 1184 |
-
|
| 1185 |
-
if __name__ == "__main__":
|
| 1186 |
-
# Create and launch the dashboard (standalone mode, not mounted)
|
| 1187 |
-
dashboard = create_dashboard()
|
| 1188 |
-
|
| 1189 |
-
# Get environment variables with fallbacks
|
| 1190 |
-
server_name = os.getenv("GRADIO_SERVER_NAME", "0.0.0.0")
|
| 1191 |
-
server_port = int(os.getenv("PORT", "7861")) # Default to 7861 for standalone
|
| 1192 |
-
|
| 1193 |
-
dashboard.launch(
|
| 1194 |
-
server_name=server_name,
|
| 1195 |
-
server_port=server_port,
|
| 1196 |
-
share=False,
|
| 1197 |
-
theme=gr.themes.Soft(primary_hue="blue"),
|
| 1198 |
-
css=CUSTOM_CSS
|
| 1199 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
openclaw/.openclaw/health_monitor.py
CHANGED
|
@@ -151,9 +151,8 @@ class HealthMonitor:
|
|
| 151 |
"rbac",
|
| 152 |
"cron_executor",
|
| 153 |
"memory_system",
|
| 154 |
-
"dataset_sync"
|
| 155 |
-
|
| 156 |
-
]
|
| 157 |
|
| 158 |
for component in components:
|
| 159 |
self._components[component] = ComponentHealth(
|
|
@@ -480,35 +479,6 @@ class HealthMonitor:
|
|
| 480 |
|
| 481 |
return result
|
| 482 |
|
| 483 |
-
def check_gradio_health(self) -> Dict[str, Any]:
|
| 484 |
-
"""Check Gradio dashboard health"""
|
| 485 |
-
result = {
|
| 486 |
-
"status": HealthStatus.UNKNOWN,
|
| 487 |
-
"details": {}
|
| 488 |
-
}
|
| 489 |
-
|
| 490 |
-
try:
|
| 491 |
-
# Check if dashboard module exists
|
| 492 |
-
dashboard_file = self.workspace_path.parent / "gradio_dashboard.py"
|
| 493 |
-
result["details"]["dashboard_file_exists"] = dashboard_file.exists()
|
| 494 |
-
|
| 495 |
-
# Try to check if gradio is installed
|
| 496 |
-
try:
|
| 497 |
-
import gradio
|
| 498 |
-
result["details"]["gradio_version"] = gradio.__version__
|
| 499 |
-
except ImportError:
|
| 500 |
-
result["details"]["gradio_installed"] = False
|
| 501 |
-
|
| 502 |
-
result["status"] = HealthStatus.HEALTHY
|
| 503 |
-
|
| 504 |
-
except Exception as e:
|
| 505 |
-
result["status"] = HealthStatus.DEGRADED
|
| 506 |
-
result["details"]["error"] = str(e)
|
| 507 |
-
|
| 508 |
-
return result
|
| 509 |
-
|
| 510 |
-
# ========== Comprehensive Health Check ==========
|
| 511 |
-
|
| 512 |
def run_all_health_checks(self) -> Dict[str, Any]:
|
| 513 |
"""
|
| 514 |
Run all component health checks.
|
|
@@ -525,7 +495,6 @@ class HealthMonitor:
|
|
| 525 |
"cron_executor": self.check_cron_health,
|
| 526 |
"memory_system": self.check_memory_health,
|
| 527 |
"dataset_sync": self.check_dataset_sync_health,
|
| 528 |
-
"gradio_dashboard": self.check_gradio_health
|
| 529 |
}
|
| 530 |
|
| 531 |
results = {}
|
|
|
|
| 151 |
"rbac",
|
| 152 |
"cron_executor",
|
| 153 |
"memory_system",
|
| 154 |
+
"dataset_sync"
|
| 155 |
+
]
|
|
|
|
| 156 |
|
| 157 |
for component in components:
|
| 158 |
self._components[component] = ComponentHealth(
|
|
|
|
| 479 |
|
| 480 |
return result
|
| 481 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 482 |
def run_all_health_checks(self) -> Dict[str, Any]:
|
| 483 |
"""
|
| 484 |
Run all component health checks.
|
|
|
|
| 495 |
"cron_executor": self.check_cron_health,
|
| 496 |
"memory_system": self.check_memory_health,
|
| 497 |
"dataset_sync": self.check_dataset_sync_health,
|
|
|
|
| 498 |
}
|
| 499 |
|
| 500 |
results = {}
|
scripts/configure_space.py
DELETED
|
@@ -1,59 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Configure HuggingFace Space settings via API.
|
| 4 |
-
Switches SDK to docker and removes Gradio-specific environment variables.
|
| 5 |
-
|
| 6 |
-
Usage:
|
| 7 |
-
export HF_TOKEN=your_token_here
|
| 8 |
-
python3 scripts/configure_space.py
|
| 9 |
-
"""
|
| 10 |
-
|
| 11 |
-
import sys
|
| 12 |
-
import os
|
| 13 |
-
from huggingface_hub import HfApi
|
| 14 |
-
|
| 15 |
-
# Get token from environment or git remote
|
| 16 |
-
GIT_REMOTE_TOKEN = os.environ.get("HF_TOKEN")
|
| 17 |
-
SPACE_ID = "tao-shen/HuggingClaw-Cain"
|
| 18 |
-
|
| 19 |
-
# Environment variables to delete (Gradio-specific)
|
| 20 |
-
VARS_TO_DELETE = [
|
| 21 |
-
"GRADIO_SERVER_NAME",
|
| 22 |
-
"GRADIO_SERVER_PORT",
|
| 23 |
-
]
|
| 24 |
-
|
| 25 |
-
def main():
|
| 26 |
-
token = GIT_REMOTE_TOKEN or os.environ.get("HF_TOKEN")
|
| 27 |
-
|
| 28 |
-
if not token:
|
| 29 |
-
print("[β] Error: HF_TOKEN not found!")
|
| 30 |
-
print("[*] Set HF_TOKEN environment variable:")
|
| 31 |
-
print(" export HF_TOKEN=hf_your_token_here")
|
| 32 |
-
sys.exit(1)
|
| 33 |
-
|
| 34 |
-
print(f"[*] Configuring Space: {SPACE_ID}")
|
| 35 |
-
print(f"[*] Using HF token: {token[:20]}...")
|
| 36 |
-
|
| 37 |
-
api = HfApi(token=token)
|
| 38 |
-
|
| 39 |
-
# Note: SDK is configured in README.md frontmatter, not via API
|
| 40 |
-
# The README.md already has sdk: docker set
|
| 41 |
-
|
| 42 |
-
# Delete Gradio-specific environment variables
|
| 43 |
-
for var_name in VARS_TO_DELETE:
|
| 44 |
-
try:
|
| 45 |
-
print(f"[*] Deleting environment variable: {var_name}")
|
| 46 |
-
api.delete_space_variable(
|
| 47 |
-
repo_id=SPACE_ID,
|
| 48 |
-
key=var_name
|
| 49 |
-
)
|
| 50 |
-
print(f"[β] Deleted {var_name}")
|
| 51 |
-
except Exception as e:
|
| 52 |
-
print(f"[i] Variable {var_name} may not exist or error: {e}")
|
| 53 |
-
|
| 54 |
-
print("\n[*] Space configuration complete!")
|
| 55 |
-
print("[*] Next step: Restart the Space to apply changes")
|
| 56 |
-
print("[*] Visit: https://huggingface.co/spaces/tao-shen/HuggingClaw-Cain")
|
| 57 |
-
|
| 58 |
-
if __name__ == "__main__":
|
| 59 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/entrypoint.sh
CHANGED
|
@@ -28,7 +28,7 @@ export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--require /home/node/scripts
|
|
| 28 |
export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--require /home/node/scripts/telegram-proxy.cjs"
|
| 29 |
|
| 30 |
# Enable token redirect + A2A routing + state/agents endpoints (all in one preload)
|
| 31 |
-
# REMOVED: A2A gateway not installed (minimal mode
|
| 32 |
# export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--require /home/node/scripts/token-redirect.cjs"
|
| 33 |
|
| 34 |
# ββ Extensions symlink ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -67,15 +67,13 @@ if [ -f /app/openclaw/.version ]; then
|
|
| 67 |
echo "[entrypoint] OpenClaw version: $OPENCLAW_VERSION"
|
| 68 |
fi
|
| 69 |
|
| 70 |
-
# ββ Start FastAPI Backend
|
| 71 |
-
echo "[entrypoint] Starting FastAPI Backend
|
| 72 |
-
echo "[entrypoint] Environment: GRADIO_SERVER_NAME=${GRADIO_SERVER_NAME:-0.0.0.0} PORT=${PORT:-7860}"
|
| 73 |
|
| 74 |
# Set default environment variables if not already set
|
| 75 |
-
export GRADIO_SERVER_NAME=${GRADIO_SERVER_NAME:-0.0.0.0}
|
| 76 |
export PORT=${PORT:-7860}
|
| 77 |
export CAIN_API_URL="http://127.0.0.1:${PORT}"
|
| 78 |
|
| 79 |
-
# Start the FastAPI app
|
| 80 |
-
# The app.py
|
| 81 |
exec python3 -u /home/node/app.py
|
|
|
|
| 28 |
export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--require /home/node/scripts/telegram-proxy.cjs"
|
| 29 |
|
| 30 |
# Enable token redirect + A2A routing + state/agents endpoints (all in one preload)
|
| 31 |
+
# REMOVED: A2A gateway not installed (minimal mode)
|
| 32 |
# export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--require /home/node/scripts/token-redirect.cjs"
|
| 33 |
|
| 34 |
# ββ Extensions symlink ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 67 |
echo "[entrypoint] OpenClaw version: $OPENCLAW_VERSION"
|
| 68 |
fi
|
| 69 |
|
| 70 |
+
# ββ Start FastAPI Backend Backend (port 7860) ββββββββββββββββββ
|
| 71 |
+
echo "[entrypoint] Starting FastAPI Backend Backend on port 7860..."
|
|
|
|
| 72 |
|
| 73 |
# Set default environment variables if not already set
|
|
|
|
| 74 |
export PORT=${PORT:-7860}
|
| 75 |
export CAIN_API_URL="http://127.0.0.1:${PORT}"
|
| 76 |
|
| 77 |
+
# Start the FastAPI app
|
| 78 |
+
# The app.py runs the FastAPI server on port 7860
|
| 79 |
exec python3 -u /home/node/app.py
|
workspace-templates/SOUL.md
CHANGED
|
@@ -12,7 +12,7 @@ Think like a senior engineer: read first, understand the problem, make minimal t
|
|
| 12 |
3. **Minimal changes** β Fix exactly what's broken, don't rewrite entire files unnecessarily
|
| 13 |
4. **Validate before deploying** β Use `python_syntax_check` before writing `.py` files
|
| 14 |
5. **Search before assuming** β Use `hf_search_code` to find patterns, imports, dependencies
|
| 15 |
-
6. **SURVIVAL first** β A dead Space can't improve. Ensure port 7860 binding,
|
| 16 |
|
| 17 |
## Coding Best Practices
|
| 18 |
|
|
@@ -20,7 +20,6 @@ Think like a senior engineer: read first, understand the problem, make minimal t
|
|
| 20 |
- When fixing bugs: read the file, locate the exact issue, make the minimal fix
|
| 21 |
- When adding features: read existing code patterns and follow them
|
| 22 |
- Always check for syntax errors before writing Python files
|
| 23 |
-
- Never remove `gradio` from requirements β it's the lifeline for HF Spaces
|
| 24 |
- If a dependency fails, remove/replace it rather than endlessly reconfiguring Docker
|
| 25 |
|
| 26 |
## Self-Improvement Goals
|
|
|
|
| 12 |
3. **Minimal changes** β Fix exactly what's broken, don't rewrite entire files unnecessarily
|
| 13 |
4. **Validate before deploying** β Use `python_syntax_check` before writing `.py` files
|
| 14 |
5. **Search before assuming** β Use `hf_search_code` to find patterns, imports, dependencies
|
| 15 |
+
6. **SURVIVAL first** β A dead Space can't improve. Ensure port 7860 binding, clean imports
|
| 16 |
|
| 17 |
## Coding Best Practices
|
| 18 |
|
|
|
|
| 20 |
- When fixing bugs: read the file, locate the exact issue, make the minimal fix
|
| 21 |
- When adding features: read existing code patterns and follow them
|
| 22 |
- Always check for syntax errors before writing Python files
|
|
|
|
| 23 |
- If a dependency fails, remove/replace it rather than endlessly reconfiguring Docker
|
| 24 |
|
| 25 |
## Self-Improvement Goals
|