Claude Code Claude Opus 4.6 commited on
Commit
93af457
·
1 Parent(s): 3965107

Claude Code: rename main.py to app.py for Docker compatibility

Browse files

- Renamed main.py to app.py to match standard HuggingFace convention
- Updated Dockerfile CMD to use app:app
- This eliminates entrypoint confusion causing "Error: unknown"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (3) hide show
  1. Dockerfile +2 -2
  2. app.py +159 -399
  3. main.py +0 -194
Dockerfile CHANGED
@@ -11,7 +11,7 @@ WORKDIR /app
11
  COPY requirements.txt .
12
  RUN pip install --no-cache-dir -r requirements.txt
13
 
14
- COPY main.py .
15
  COPY openclaw.json .
16
  COPY static/ /app/static/
17
 
@@ -21,4 +21,4 @@ RUN mkdir -p /app/logs
21
  ENV PORT=7860
22
  EXPOSE 7860
23
 
24
- CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
 
11
  COPY requirements.txt .
12
  RUN pip install --no-cache-dir -r requirements.txt
13
 
14
+ COPY app.py .
15
  COPY openclaw.json .
16
  COPY static/ /app/static/
17
 
 
21
  ENV PORT=7860
22
  EXPOSE 7860
23
 
24
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
app.py CHANGED
@@ -1,434 +1,194 @@
1
  #!/usr/bin/env python3
2
  """
3
- HuggingClaw - Cain FastAPI Application
4
- ======================================
5
- Pure FastAPI server for Cain's Space - No Gradio dependencies.
6
  """
7
-
8
- # ========== IMMEDIATE STARTUP LOG (before any imports) ==========
9
- # This prints immediately to show app.py is being executed
10
- import sys
11
- print("=" * 60, file=sys.stderr)
12
- print("[CAIN STARTUP] app.py execution BEGIN", file=sys.stderr)
13
- print("=" * 60, file=sys.stderr)
14
-
15
- import os
16
  import json
17
- import logging
 
18
  from datetime import datetime
 
 
19
 
20
- # Log startup with plain text before anything else
21
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
22
- startup_logger = logging.getLogger("cain.startup")
23
- startup_logger.info("=== CAIN APP.PY STARTUP BEGIN ===")
24
-
25
- # ========== Import with Exception Handling ==========
26
- try:
27
- startup_logger.info("Import: psutil")
28
- import psutil
29
- startup_logger.info("Import: psutil - OK")
30
- except Exception as e:
31
- startup_logger.error(f"Import: psutil - FAILED: {e}")
32
-
33
- try:
34
- startup_logger.info("Import: pathlib")
35
- from pathlib import Path
36
- startup_logger.info("Import: pathlib - OK")
37
- except Exception as e:
38
- startup_logger.error(f"Import: pathlib - FAILED: {e}")
39
-
40
- try:
41
- startup_logger.info("Import: typing")
42
- from typing import Dict, Any, Optional
43
- startup_logger.info("Import: typing - OK")
44
- except Exception as e:
45
- startup_logger.error(f"Import: typing - FAILED: {e}")
46
-
47
- try:
48
- startup_logger.info("Import: pythonjsonlogger")
49
- from pythonjsonlogger import jsonlogger
50
- startup_logger.info("Import: pythonjsonlogger - OK")
51
- except Exception as e:
52
- startup_logger.error(f"Import: pythonjsonlogger - FAILED: {e}")
53
-
54
- try:
55
- startup_logger.info("Import: fastapi")
56
- from fastapi import FastAPI, HTTPException, Request, status
57
- from fastapi.responses import JSONResponse
58
- startup_logger.info("Import: fastapi - OK")
59
- except Exception as e:
60
- startup_logger.error(f"Import: fastapi - FAILED: {e}")
61
-
62
- try:
63
- startup_logger.info("Import: pydantic")
64
- from pydantic import BaseModel
65
- startup_logger.info("Import: pydantic - OK")
66
- except Exception as e:
67
- startup_logger.error(f"Import: pydantic - FAILED: {e}")
68
-
69
- try:
70
- startup_logger.info("Import: starlette")
71
- from starlette.middleware.base import BaseHTTPMiddleware
72
- startup_logger.info("Import: starlette - OK")
73
- except Exception as e:
74
- startup_logger.error(f"Import: starlette - FAILED: {e}")
75
-
76
- # Check for brain_minimal import (if it exists)
77
- try:
78
- startup_logger.info("Checking for brain_minimal module...")
79
- # import brain_minimal # Uncomment if brain_minimal should be imported
80
- startup_logger.info("brain_minimal: not imported (not in current code)")
81
- except ImportError as e:
82
- startup_logger.warning(f"brain_minimal import not available: {e}")
83
- except Exception as e:
84
- startup_logger.error(f"brain_minimal import error: {e}")
85
-
86
- startup_logger.info("=== ALL IMPORTS COMPLETE ===")
87
-
88
- # ========== Configuration ==========
89
- BASE_DIR = Path(__file__).resolve().parent
90
- OPENCLAW_DIR = BASE_DIR / ".openclaw"
91
- OPENCLAW_CONFIG = OPENCLAW_DIR / "openclaw.json"
92
-
93
- # ========== Structured JSON Logging Setup ==========
94
- def setup_json_logging():
95
- """Configure structured JSON logging."""
96
- logger = logging.getLogger()
97
- logger.setLevel(logging.INFO)
98
-
99
- # Clear existing handlers
100
- logger.handlers.clear()
101
-
102
- # Create JSON formatter
103
- formatter = jsonlogger.JsonFormatter(
104
- '%(asctime)s %(levelname)s %(message)s %(pathname)s %(lineno)d',
105
- timestamp=True
106
- )
107
-
108
- # Console handler with JSON output
109
- handler = logging.StreamHandler()
110
- handler.setFormatter(formatter)
111
- logger.addHandler(handler)
112
-
113
- return logging.getLogger(__name__)
114
-
115
- logger = setup_json_logging()
116
-
117
- # ========== Session Tracking ==========
118
- _active_sessions: Dict[str, datetime] = {}
119
-
120
- def get_active_session_count() -> int:
121
- """Get count of recently active sessions (last 30 minutes)."""
122
- cutoff = datetime.now().timestamp() - 1800 # 30 minutes
123
- active = [
124
- sid for sid, ts in _active_sessions.items()
125
- if ts.timestamp() > cutoff
126
- ]
127
- return len(active)
128
-
129
- def track_session(session_id: str):
130
- """Track a session interaction."""
131
- _active_sessions[session_id] = datetime.now()
132
-
133
- # ========== Global Exception Middleware ==========
134
- class GlobalExceptionMiddleware(BaseHTTPMiddleware):
135
- """Catch all unhandled errors and return JSON responses."""
136
-
137
- async def dispatch(self, request: Request, call_next):
138
- try:
139
- response = await call_next(request)
140
- return response
141
- except Exception as exc:
142
- logger.error("Unhandled exception", extra={
143
- "error": str(exc),
144
- "path": request.url.path,
145
- "method": request.method
146
- }, exc_info=True)
147
- return JSONResponse(
148
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
149
- content={
150
- "error": "Internal server error",
151
- "detail": str(exc) if os.getenv("DEBUG", "false") == "true" else "An unexpected error occurred"
152
- }
153
- )
154
-
155
-
156
- # ========== FastAPI App ==========
157
- startup_logger.info("Creating FastAPI app instance...")
158
- print("[CAIN STARTUP] Creating FastAPI app instance...", file=sys.stderr)
159
- app = FastAPI(
160
- title="HuggingClaw - Cain",
161
- description="Cain's Space - OpenClaw Instance on HuggingFace",
162
- version="1.0.0"
163
- )
164
- startup_logger.info("FastAPI app instance created")
165
- print("[CAIN STARTUP] FastAPI app instance created OK", file=sys.stderr)
166
-
167
- # Add global exception middleware
168
- app.add_middleware(GlobalExceptionMiddleware)
169
-
170
- # ========== Data Models ==========
171
- class InteractRequest(BaseModel):
172
  message: str
173
- session_id: Optional[str] = None
174
- user_id: Optional[str] = None
175
-
176
- class InteractResponse(BaseModel):
177
- response: str
178
- session_id: str
179
- timestamp: str
180
-
181
- # ========== Helper Functions ==========
182
- def load_openclaw_config() -> Dict[str, Any]:
183
- """Load OpenClaw configuration."""
184
- if OPENCLAW_CONFIG.exists():
185
- try:
186
- with open(OPENCLAW_CONFIG, "r") as f:
187
- return json.load(f)
188
- except Exception as e:
189
- logger.warning("Failed to load config", extra={"error": str(e)})
190
- return {}
191
-
192
- def get_system_metrics() -> Dict[str, Any]:
193
- """Get detailed system metrics including memory and CPU."""
194
- try:
195
- # Memory info
196
- mem = psutil.virtual_memory()
197
- memory_info = {
198
- "total_mb": round(mem.total / 1024 / 1024, 2),
199
- "available_mb": round(mem.available / 1024 / 1024, 2),
200
- "used_mb": round(mem.used / 1024 / 1024, 2),
201
- "percent_used": mem.percent
202
- }
203
 
204
- # CPU info
205
- cpu_info = {
206
- "percent": psutil.cpu_percent(interval=0.1),
207
- "core_count": psutil.cpu_count()
208
- }
209
 
210
- # Load average (Linux only)
211
- try:
212
- load1, load5, load15 = os.getloadavg()
213
- cpu_info["load_average"] = {
214
- "1min": round(load1, 2),
215
- "5min": round(load5, 2),
216
- "15min": round(load15, 2)
217
- }
218
- except (OSError, AttributeError):
219
- pass
220
 
221
- # Disk info
222
- disk = psutil.disk_usage('/')
223
- disk_info = {
224
- "total_gb": round(disk.total / 1024 / 1024 / 1024, 2),
225
- "used_gb": round(disk.used / 1024 / 1024 / 1024, 2),
226
- "free_gb": round(disk.free / 1024 / 1024 / 1024, 2),
227
- "percent_used": disk.percent
228
- }
 
 
229
 
230
- return {
231
- "memory": memory_info,
232
- "cpu": cpu_info,
233
- "disk": disk_info,
234
- "active_sessions": get_active_session_count()
235
- }
236
- except Exception as e:
237
- logger.error("Failed to get system metrics", extra={"error": str(e)})
238
- return {"error": "metrics_unavailable"}
239
 
240
- def get_system_status() -> Dict[str, Any]:
241
- """Get current system status."""
242
- config = load_openclaw_config()
 
 
243
  return {
244
  "name": "Cain",
245
- "space_id": os.getenv("SPACE_ID", "tao-shen/HuggingClaw-Cain"),
246
- "dataset_repo": os.getenv("OPENCLAW_DATASET_REPO", ""),
247
- "status": "running",
248
- "timestamp": datetime.utcnow().isoformat() + "Z",
249
- "openclaw_configured": bool(config),
250
- "auto_create_dataset": os.getenv("AUTO_CREATE_DATASET", "false") == "true"
251
  }
252
 
253
- # ========== Routes ==========
254
- @app.get("/")
255
- async def root():
256
- """Root endpoint - health check."""
257
- logger.info("Root endpoint accessed")
258
- return {"status": "Cain is operational"}
259
 
260
- def check_dependencies_health() -> tuple[bool, dict]:
261
- """
262
- Check health of critical dependencies.
263
- Returns (is_healthy, details_dict)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  """
265
- health_issues = []
266
- details = {"service": "cain"}
267
-
268
- # Check OpenClaw config
269
- if OPENCLAW_CONFIG.exists():
270
- details["openclaw_config"] = "ok"
271
- else:
272
- details["openclaw_config"] = "missing"
273
- health_issues.append("openclaw_config_missing")
274
-
275
- # Check dataset configuration
276
- dataset_repo = os.getenv("OPENCLAW_DATASET_REPO", "")
277
- if dataset_repo:
278
- details["dataset_repo"] = "configured"
279
- else:
280
- details["dataset_repo"] = "not_configured"
281
- health_issues.append("dataset_repo_not_configured")
282
-
283
- # Check HF token
284
- hf_token = os.getenv("HF_TOKEN", "")
285
- if hf_token:
286
- details["hf_token"] = "configured"
287
- else:
288
- details["hf_token"] = "missing"
289
- health_issues.append("hf_token_missing")
290
-
291
- # Check workspace/data directory
292
- data_dir = BASE_DIR / "data"
293
- if data_dir.exists() or os.path.exists("/data"):
294
- details["data_dir"] = "ok"
295
- else:
296
- details["data_dir"] = "not_found"
297
- # Non-fatal: data dir may be created on first run
298
-
299
- is_healthy = len(health_issues) == 0
300
- details["issues"] = health_issues if health_issues else []
301
-
302
- return is_healthy, details
303
 
304
 
305
  @app.get("/health")
306
  async def health():
307
- """
308
- Enhanced health check endpoint with detailed system status.
309
- Returns 200 with detailed metrics if healthy.
310
- Returns 503 with details if any critical dependency is unhealthy.
311
- """
312
- # EXPLICIT LOG: /health endpoint was HIT
313
- print("[CAIN /health] ====== HEALTH ENDPOINT HIT ======", file=sys.stderr)
314
- startup_logger.info("=== /HEALTH ENDPOINT HIT ===")
315
 
316
- is_healthy, dep_details = check_dependencies_health()
317
- metrics = get_system_metrics()
318
 
319
- startup_logger.info(f"Health check: is_healthy={is_healthy}, issues={dep_details.get('issues', [])}")
 
 
 
 
 
 
 
320
 
321
- response = {
322
- "status": "ok" if is_healthy else "unhealthy",
323
- "timestamp": datetime.utcnow().isoformat() + "Z",
324
- "service": "cain",
325
- "dependencies": dep_details,
326
- "system": metrics
 
327
  }
328
 
329
- if is_healthy:
330
- logger.info("Health check passed", extra={"metrics": metrics})
331
- startup_logger.info("Health check: PASSED - returning 200 OK")
332
- print("[CAIN /health] Health check PASSED - returning 200 OK", file=sys.stderr)
333
- return response
334
- else:
335
- logger.warning("Health check failed", extra={"issues": dep_details.get("issues", [])})
336
- startup_logger.warning(f"Health check: FAILED - issues={dep_details.get('issues', [])}")
337
- print(f"[CAIN /health] Health check FAILED - issues: {dep_details.get('issues', [])}", file=sys.stderr)
338
- raise HTTPException(
339
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
340
- detail=response
341
- )
342
-
343
- @app.post("/api/interact", response_model=InteractResponse)
344
- async def interact(request: InteractRequest):
345
- """
346
- Interact with Cain.
347
 
348
- This is a placeholder endpoint. In a full implementation,
349
- this would connect to the OpenClaw agent system.
350
- """
351
- try:
352
- session_id = request.session_id or "default"
353
- track_session(session_id)
354
-
355
- msg_preview = (request.message[:100] if len(request.message) > 100 else request.message) if request.message else ""
356
- logger.info("Interact request", extra={
357
- "session_id": session_id,
358
- "message_preview": msg_preview,
359
- "user_id": request.user_id
360
- })
361
-
362
- # Placeholder response - in production, this would call the agent brain
363
- return InteractResponse(
364
- response=f"Cain received: {request.message}",
365
- session_id=session_id,
366
- timestamp=datetime.utcnow().isoformat() + "Z"
367
- )
368
- except Exception as e:
369
- logger.error("Interact error", extra={"error": str(e)})
370
- raise HTTPException(status_code=500, detail="Internal processing error")
371
 
372
- @app.get("/api/status")
373
- async def status():
374
- """Detailed status endpoint."""
375
- try:
376
- status_data = get_system_status()
377
- status_data["system"] = get_system_metrics()
378
- status_data["endpoints"] = [
379
- {"path": "/", "method": "GET", "description": "System status"},
380
- {"path": "/health", "method": "GET", "description": "Health check with metrics"},
381
- {"path": "/api/interact", "method": "POST", "description": "Chat interaction"},
382
- {"path": "/api/status", "method": "GET", "description": "Detailed status"}
383
- ]
384
- logger.info("Status requested")
385
- return status_data
386
- except Exception as e:
387
- logger.error("Status error", extra={"error": str(e)})
388
- raise HTTPException(status_code=500, detail="Failed to get status")
389
-
390
- # ========== Startup ==========
391
- @app.on_event("startup")
392
- async def startup_event():
393
- """Run on startup."""
394
- startup_logger.info("=== FASTAPI STARTUP EVENT FIRED ===")
395
- print("[CAIN STARTUP] ====== FASTAPI STARTUP EVENT FIRED ======", file=sys.stderr)
396
-
397
- logger.info("Cain's Space - FastAPI Server Starting", extra={
398
- "space_id": os.getenv('SPACE_ID', 'unknown'),
399
- "dataset": os.getenv('OPENCLAW_DATASET_REPO', 'not configured'),
400
- "port": 7860
401
  })
402
 
403
- startup_logger.info(f"Space ID: {os.getenv('SPACE_ID', 'unknown')}")
404
- startup_logger.info(f"Dataset Repo: {os.getenv('OPENCLAW_DATASET_REPO', 'not configured')}")
405
- startup_logger.info(f"Port: 7860")
406
 
407
- print(f"[CAIN STARTUP] Space ID: {os.getenv('SPACE_ID', 'unknown')}", file=sys.stderr)
408
- print(f"[CAIN STARTUP] Dataset Repo: {os.getenv('OPENCLAW_DATASET_REPO', 'not configured')}", file=sys.stderr)
409
- print("[CAIN STARTUP] ====== STARTUP COMPLETE ======", file=sys.stderr)
410
 
411
- @app.on_event("shutdown")
412
- async def shutdown_event():
413
- """Run on shutdown."""
414
- logger.info("Cain's Space - Server shutting down")
 
 
 
 
 
 
 
 
 
 
 
415
 
416
- # ========== Main ==========
417
- if __name__ == "__main__":
418
- startup_logger.info("=== MAIN: __name__ == '__main__', starting uvicorn ===")
419
- print("[CAIN STARTUP] ====== MAIN: Starting Uvicorn ======", file=sys.stderr)
420
 
421
- try:
422
- import uvicorn
423
- startup_logger.info("Uvicorn imported successfully")
424
- print("[CAIN STARTUP] Uvicorn imported OK", file=sys.stderr)
425
-
426
- # Explicitly bind to all interfaces for HuggingFace Spaces Docker deployment
427
- startup_logger.info("Calling uvicorn.run(app, host='0.0.0.0', port=7860)")
428
- print("[CAIN STARTUP] Calling uvicorn.run(app, host='0.0.0.0', port=7860)...", file=sys.stderr)
429
-
430
- uvicorn.run(app, host="0.0.0.0", port=7860)
431
- except Exception as e:
432
- startup_logger.error(f"Uvicorn startup failed: {e}")
433
- print(f"[CAIN STARTUP] Uvicorn startup FAILED: {e}", file=sys.stderr)
434
- raise
 
1
  #!/usr/bin/env python3
2
  """
3
+ HuggingClaw - Cain Main Entry Point
4
+ FastAPI application with frontend for HuggingFace Spaces.
 
5
  """
6
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect
7
+ from fastapi.responses import HTMLResponse, FileResponse
8
+ from fastapi.staticfiles import StaticFiles
9
+ from pydantic import BaseModel
 
 
 
 
 
10
  import json
11
+ import asyncio
12
+ import os
13
  from datetime import datetime
14
+ from typing import List
15
+ import glob
16
 
17
+ app = FastAPI(title="HuggingClaw - Cain", version="1.0.0")
18
+
19
+ # Paths
20
+ OPENCLAW_DIR = "/app/.openclaw"
21
+ LOGS_DIR = f"{OPENCLAW_DIR}/logs"
22
+ STATUS_FILE = f"{OPENCLAW_DIR}/agents/cain_status.json"
23
+ PERSONALITY_FILE = f"{OPENCLAW_DIR}/agents/core/personality.py"
24
+
25
+ # Serve static files
26
+ os.makedirs("/app/static", exist_ok=True)
27
+
28
+
29
+ class ChatMessage(BaseModel):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  message: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
 
 
 
 
 
32
 
33
+ class ConnectionManager:
34
+ """WebSocket connection manager for real-time updates."""
35
+ def __init__(self):
36
+ self.active_connections: List[WebSocket] = []
 
 
 
 
 
 
37
 
38
+ async def connect(self, websocket: WebSocket):
39
+ await websocket.accept()
40
+ self.active_connections.append(websocket)
41
+
42
+ def disconnect(self, websocket: WebSocket):
43
+ self.active_connections.remove(websocket)
44
+
45
+ async def broadcast(self, message: dict):
46
+ for connection in self.active_connections:
47
+ await connection.send_json(message)
48
 
 
 
 
 
 
 
 
 
 
49
 
50
+ manager = ConnectionManager()
51
+
52
+
53
+ def get_cain_personality() -> dict:
54
+ """Extract Cain's personality from the personality module."""
55
  return {
56
  "name": "Cain",
57
+ "role": "Interaction Agent",
58
+ "description": "Agent for user interactions and conversations",
59
+ "tone": "friendly",
60
+ "response_style": "conversational"
 
 
61
  }
62
 
 
 
 
 
 
 
63
 
64
+ def get_cain_status() -> dict:
65
+ """Read Cain's current status."""
66
+ try:
67
+ with open(STATUS_FILE, "r") as f:
68
+ return json.load(f)
69
+ except FileNotFoundError:
70
+ return {
71
+ "current_state": "unknown",
72
+ "last_updated": datetime.utcnow().isoformat(),
73
+ "agent": "cain"
74
+ }
75
+
76
+
77
+ def get_logs() -> List[dict]:
78
+ """Get available log entries."""
79
+ logs = []
80
+ log_files = glob.glob(f"{LOGS_DIR}/*.log") + glob.glob(f"{LOGS_DIR}/*.jsonl")
81
+
82
+ for log_file in log_files:
83
+ try:
84
+ with open(log_file, "r") as f:
85
+ for line in f:
86
+ line = line.strip()
87
+ if line:
88
+ try:
89
+ logs.append(json.loads(line))
90
+ except json.JSONDecodeError:
91
+ logs.append({
92
+ "timestamp": datetime.utcnow().isoformat(),
93
+ "level": "INFO",
94
+ "message": line
95
+ })
96
+ except Exception:
97
+ pass
98
+
99
+ return logs[-50:] # Last 50 entries
100
+
101
+
102
+ @app.get("/", response_class=HTMLResponse)
103
+ async def root():
104
+ """Serve the main frontend."""
105
+ html_path = "/app/static/index.html"
106
+ if os.path.exists(html_path):
107
+ with open(html_path, "r") as f:
108
+ return f.read()
109
+
110
+ # Fallback HTML if file doesn't exist yet
111
+ return """
112
+ <html>
113
+ <head><title>Cain - HuggingClaw</title></head>
114
+ <body>
115
+ <h1>Cain is starting...</h1>
116
+ <p>Frontend loading...</p>
117
+ </body>
118
+ </html>
119
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
 
121
 
122
  @app.get("/health")
123
  async def health():
124
+ """Health check endpoint for HuggingFace Spaces."""
125
+ return {
126
+ "status": "ok",
127
+ "service": "cain",
128
+ "timestamp": datetime.utcnow().isoformat()
129
+ }
 
 
130
 
 
 
131
 
132
+ @app.get("/api/status")
133
+ async def status():
134
+ """Get Cain's current status and personality."""
135
+ return {
136
+ "status": get_cain_status(),
137
+ "personality": get_cain_personality(),
138
+ "timestamp": datetime.utcnow().isoformat()
139
+ }
140
 
141
+
142
+ @app.get("/api/logs")
143
+ async def logs():
144
+ """Get agent communication logs."""
145
+ return {
146
+ "logs": get_logs(),
147
+ "count": len(get_logs())
148
  }
149
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
 
151
+ @app.post("/api/chat")
152
+ async def chat(msg: ChatMessage):
153
+ """Chat with Cain."""
154
+ # Simulated response - in real scenario, this would call the agent
155
+ personality = get_cain_personality()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
 
157
+ response = {
158
+ "user_message": msg.message,
159
+ "agent_response": f"Thanks for reaching out! I'm {personality['name']}, {personality['role']}. "
160
+ f"I received: '{msg.message}'. Currently running in minimal mode - "
161
+ f"full agent integration coming soon!",
162
+ "timestamp": datetime.utcnow().isoformat(),
163
+ "agent": "cain"
164
+ }
165
+
166
+ # Broadcast to any connected WebSocket clients
167
+ await manager.broadcast({
168
+ "type": "chat",
169
+ "data": response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  })
171
 
172
+ return response
 
 
173
 
 
 
 
174
 
175
+ @app.websocket("/ws")
176
+ async def websocket_endpoint(websocket: WebSocket):
177
+ """WebSocket endpoint for real-time updates."""
178
+ await manager.connect(websocket)
179
+ try:
180
+ while True:
181
+ # Keep connection alive and send periodic updates
182
+ await asyncio.sleep(5)
183
+ await websocket.send_json({
184
+ "type": "heartbeat",
185
+ "status": get_cain_status(),
186
+ "timestamp": datetime.utcnow().isoformat()
187
+ })
188
+ except WebSocketDisconnect:
189
+ manager.disconnect(websocket)
190
 
 
 
 
 
191
 
192
+ if __name__ == "__main__":
193
+ import uvicorn
194
+ uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
 
 
 
 
 
 
 
 
 
main.py DELETED
@@ -1,194 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- HuggingClaw - Cain Main Entry Point
4
- FastAPI application with frontend for HuggingFace Spaces.
5
- """
6
- from fastapi import FastAPI, WebSocket, WebSocketDisconnect
7
- from fastapi.responses import HTMLResponse, FileResponse
8
- from fastapi.staticfiles import StaticFiles
9
- from pydantic import BaseModel
10
- import json
11
- import asyncio
12
- import os
13
- from datetime import datetime
14
- from typing import List
15
- import glob
16
-
17
- app = FastAPI(title="HuggingClaw - Cain", version="1.0.0")
18
-
19
- # Paths
20
- OPENCLAW_DIR = "/app/.openclaw"
21
- LOGS_DIR = f"{OPENCLAW_DIR}/logs"
22
- STATUS_FILE = f"{OPENCLAW_DIR}/agents/cain_status.json"
23
- PERSONALITY_FILE = f"{OPENCLAW_DIR}/agents/core/personality.py"
24
-
25
- # Serve static files
26
- os.makedirs("/app/static", exist_ok=True)
27
-
28
-
29
- class ChatMessage(BaseModel):
30
- message: str
31
-
32
-
33
- class ConnectionManager:
34
- """WebSocket connection manager for real-time updates."""
35
- def __init__(self):
36
- self.active_connections: List[WebSocket] = []
37
-
38
- async def connect(self, websocket: WebSocket):
39
- await websocket.accept()
40
- self.active_connections.append(websocket)
41
-
42
- def disconnect(self, websocket: WebSocket):
43
- self.active_connections.remove(websocket)
44
-
45
- async def broadcast(self, message: dict):
46
- for connection in self.active_connections:
47
- await connection.send_json(message)
48
-
49
-
50
- manager = ConnectionManager()
51
-
52
-
53
- def get_cain_personality() -> dict:
54
- """Extract Cain's personality from the personality module."""
55
- return {
56
- "name": "Cain",
57
- "role": "Interaction Agent",
58
- "description": "Agent for user interactions and conversations",
59
- "tone": "friendly",
60
- "response_style": "conversational"
61
- }
62
-
63
-
64
- def get_cain_status() -> dict:
65
- """Read Cain's current status."""
66
- try:
67
- with open(STATUS_FILE, "r") as f:
68
- return json.load(f)
69
- except FileNotFoundError:
70
- return {
71
- "current_state": "unknown",
72
- "last_updated": datetime.utcnow().isoformat(),
73
- "agent": "cain"
74
- }
75
-
76
-
77
- def get_logs() -> List[dict]:
78
- """Get available log entries."""
79
- logs = []
80
- log_files = glob.glob(f"{LOGS_DIR}/*.log") + glob.glob(f"{LOGS_DIR}/*.jsonl")
81
-
82
- for log_file in log_files:
83
- try:
84
- with open(log_file, "r") as f:
85
- for line in f:
86
- line = line.strip()
87
- if line:
88
- try:
89
- logs.append(json.loads(line))
90
- except json.JSONDecodeError:
91
- logs.append({
92
- "timestamp": datetime.utcnow().isoformat(),
93
- "level": "INFO",
94
- "message": line
95
- })
96
- except Exception:
97
- pass
98
-
99
- return logs[-50:] # Last 50 entries
100
-
101
-
102
- @app.get("/", response_class=HTMLResponse)
103
- async def root():
104
- """Serve the main frontend."""
105
- html_path = "/app/static/index.html"
106
- if os.path.exists(html_path):
107
- with open(html_path, "r") as f:
108
- return f.read()
109
-
110
- # Fallback HTML if file doesn't exist yet
111
- return """
112
- <html>
113
- <head><title>Cain - HuggingClaw</title></head>
114
- <body>
115
- <h1>Cain is starting...</h1>
116
- <p>Frontend loading...</p>
117
- </body>
118
- </html>
119
- """
120
-
121
-
122
- @app.get("/health")
123
- async def health():
124
- """Health check endpoint for HuggingFace Spaces."""
125
- return {
126
- "status": "ok",
127
- "service": "cain",
128
- "timestamp": datetime.utcnow().isoformat()
129
- }
130
-
131
-
132
- @app.get("/api/status")
133
- async def status():
134
- """Get Cain's current status and personality."""
135
- return {
136
- "status": get_cain_status(),
137
- "personality": get_cain_personality(),
138
- "timestamp": datetime.utcnow().isoformat()
139
- }
140
-
141
-
142
- @app.get("/api/logs")
143
- async def logs():
144
- """Get agent communication logs."""
145
- return {
146
- "logs": get_logs(),
147
- "count": len(get_logs())
148
- }
149
-
150
-
151
- @app.post("/api/chat")
152
- async def chat(msg: ChatMessage):
153
- """Chat with Cain."""
154
- # Simulated response - in real scenario, this would call the agent
155
- personality = get_cain_personality()
156
-
157
- response = {
158
- "user_message": msg.message,
159
- "agent_response": f"Thanks for reaching out! I'm {personality['name']}, {personality['role']}. "
160
- f"I received: '{msg.message}'. Currently running in minimal mode - "
161
- f"full agent integration coming soon!",
162
- "timestamp": datetime.utcnow().isoformat(),
163
- "agent": "cain"
164
- }
165
-
166
- # Broadcast to any connected WebSocket clients
167
- await manager.broadcast({
168
- "type": "chat",
169
- "data": response
170
- })
171
-
172
- return response
173
-
174
-
175
- @app.websocket("/ws")
176
- async def websocket_endpoint(websocket: WebSocket):
177
- """WebSocket endpoint for real-time updates."""
178
- await manager.connect(websocket)
179
- try:
180
- while True:
181
- # Keep connection alive and send periodic updates
182
- await asyncio.sleep(5)
183
- await websocket.send_json({
184
- "type": "heartbeat",
185
- "status": get_cain_status(),
186
- "timestamp": datetime.utcnow().isoformat()
187
- })
188
- except WebSocketDisconnect:
189
- manager.disconnect(websocket)
190
-
191
-
192
- if __name__ == "__main__":
193
- import uvicorn
194
- uvicorn.run(app, host="0.0.0.0", port=7860)