tao-shen Claude Opus 4.6 commited on
Commit
3d28748
Β·
1 Parent(s): de5ce91

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 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
- β”‚ ↕ Gradio API Routes β”‚
188
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
189
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
190
  β”‚ Backend Layer β”‚
191
  β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
192
  β”‚ β”‚ app.py β”‚ β”‚ brain_minimal.py β”‚ β”‚
193
- β”‚ β”‚ - Gradio UI β”‚ β”‚ - Conversation processing β”‚ β”‚
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
- "gradio_dashboard"
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 - Gradio only)
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 with Gradio Dashboard (port 7860) ──────────────────
71
- echo "[entrypoint] Starting FastAPI Backend with Gradio Dashboard on port 7860..."
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 which includes the mounted Gradio dashboard
80
- # The app.py now runs the FastAPI server with /gradio endpoint for the dashboard
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, gradio dependency, clean imports
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