Claude Code commited on
Commit
2f8fc36
Β·
1 Parent(s): c4b8552

Claude Code: Refactor to use FastAPI as the backend instead of standard Grad

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