Claude Code commited on
Commit
9a0d91f
·
1 Parent(s): 2f8fc36

Claude Code: Create a "System Diagnostics" tab in that parses and displays t

Browse files
__pycache__/app.cpython-311.pyc ADDED
Binary file (25.2 kB). View file
 
__pycache__/gradio_dashboard.cpython-311.pyc CHANGED
Binary files a/__pycache__/gradio_dashboard.cpython-311.pyc and b/__pycache__/gradio_dashboard.cpython-311.pyc differ
 
app.py CHANGED
@@ -138,6 +138,9 @@ async def root():
138
  "chat": "/api/chat",
139
  "health": "/api/health",
140
  "tools": "/api/tools",
 
 
 
141
  "docs": "/docs"
142
  },
143
  "gradio_dashboard": "/gradio",
@@ -396,6 +399,80 @@ async def get_logs(
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
  """
 
138
  "chat": "/api/chat",
139
  "health": "/api/health",
140
  "tools": "/api/tools",
141
+ "logs": "/api/logs",
142
+ "diagnostics": "/api/diagnostics",
143
+ "reset": "/api/reset",
144
  "docs": "/docs"
145
  },
146
  "gradio_dashboard": "/gradio",
 
399
  except Exception as e:
400
  raise HTTPException(status_code=500, detail=f"Error getting logs: {str(e)}")
401
 
402
+ @app.get("/api/diagnostics", tags=["Monitoring"])
403
+ async def get_diagnostics(limit: int = 100):
404
+ """
405
+ Get system diagnostics data including session archive logs and health status.
406
+
407
+ Returns data from:
408
+ - session-archive.jsonl (agent state changes)
409
+ - Health monitor component status
410
+ """
411
+ result = {
412
+ "timestamp": datetime.utcnow().isoformat() + "Z",
413
+ "session_archive": {},
414
+ "health_monitor": {}
415
+ }
416
+
417
+ # Read session archive logs
418
+ session_archive_path = BASE_DIR / "agents" / "logs" / "session-archive.jsonl"
419
+ if session_archive_path.exists():
420
+ try:
421
+ entries = []
422
+ with open(session_archive_path, 'r') as f:
423
+ lines = f.readlines()
424
+
425
+ # Parse JSONL entries (most recent first)
426
+ for line in reversed(lines[-limit:]):
427
+ line = line.strip()
428
+ if not line:
429
+ continue
430
+ try:
431
+ entries.append(json.loads(line))
432
+ except json.JSONDecodeError:
433
+ pass
434
+
435
+ result["session_archive"] = {
436
+ "file_path": str(session_archive_path),
437
+ "total_lines": len(lines),
438
+ "entries_shown": len(entries),
439
+ "entries": entries
440
+ }
441
+ except Exception as e:
442
+ result["session_archive"] = {
443
+ "error": str(e),
444
+ "file_path": str(session_archive_path)
445
+ }
446
+ else:
447
+ result["session_archive"] = {
448
+ "error": "File not found",
449
+ "file_path": str(session_archive_path)
450
+ }
451
+
452
+ # Get health monitor status
453
+ monitor = get_health_monitor_instance()
454
+ if monitor:
455
+ try:
456
+ health_results = monitor.run_all_health_checks()
457
+ result["health_monitor"] = {
458
+ "overall_status": health_results.get("overall_status"),
459
+ "uptime_seconds": health_results.get("uptime_seconds"),
460
+ "check_count": health_results.get("check_count"),
461
+ "components": {
462
+ name: {
463
+ "status": result["status"].value if isinstance(result["status"], HealthStatus) else result["status"],
464
+ "details": result.get("details", {})
465
+ }
466
+ for name, result in health_results.get("components", {}).items()
467
+ }
468
+ }
469
+ except Exception as e:
470
+ result["health_monitor"] = {"error": str(e)}
471
+ else:
472
+ result["health_monitor"] = {"error": "Health monitor not available"}
473
+
474
+ return result
475
+
476
  @app.post("/api/reset", tags=["Administration"])
477
  async def reset_agent():
478
  """
gradio_dashboard.py CHANGED
@@ -38,6 +38,8 @@ 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
 
42
 
43
  # ========== API Client Functions ==========
@@ -323,6 +325,240 @@ def get_health_logs(level: str = "all", component: str = "all", limit: int = 100
323
  return [{"error": str(e)}]
324
 
325
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
  # ========== Formatting Functions ==========
327
 
328
  def format_agent_states(states):
@@ -633,6 +869,57 @@ def create_dashboard():
633
  value={}
634
  )
635
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
636
  # Footer with refresh
637
  with gr.Row():
638
  refresh_btn = gr.Button("🔄 Refresh Dashboard", variant="primary")
@@ -650,6 +937,9 @@ def create_dashboard():
650
  ]
651
  )
652
 
 
 
 
653
  # Manual refresh for main dashboard
654
  refresh_btn.click(
655
  fn=refresh_all,
@@ -689,6 +979,19 @@ def create_dashboard():
689
  outputs=[health_json, logs_table, health_report, last_updated]
690
  )
691
 
 
 
 
 
 
 
 
 
 
 
 
 
 
692
  return app
693
 
694
 
 
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 ==========
 
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):
 
869
  value={}
870
  )
871
 
872
+ # System Diagnostics Tab (NEW)
873
+ with gr.Tab("🔍 System Diagnostics"):
874
+ with gr.Row():
875
+ refresh_diagnostics_btn = gr.Button("🔍 Refresh Diagnostics", variant="primary")
876
+ diagnostics_last_updated = gr.Markdown("Click refresh to load")
877
+
878
+ gr.Markdown("### Session Archive Logs")
879
+ gr.Markdown("Agent state changes and archived session data from `session-archive.jsonl`")
880
+
881
+ with gr.Row():
882
+ with gr.Column():
883
+ session_logs_table = gr.DataFrame(
884
+ label="Recent Session Logs",
885
+ headers=["Timestamp", "Type", "Message", "Agent"],
886
+ value=[],
887
+ interactive=False
888
+ )
889
+ with gr.Column():
890
+ session_logs_json = gr.JSON(
891
+ label="Raw Session Logs (JSON)",
892
+ value={}
893
+ )
894
+
895
+ gr.Markdown("---")
896
+
897
+ gr.Markdown("### Health Monitor Status")
898
+ gr.Markdown("Health check status from the health monitoring system")
899
+
900
+ with gr.Row():
901
+ with gr.Column():
902
+ health_status_json = gr.JSON(
903
+ label="Health Check Results (JSON)",
904
+ value={}
905
+ )
906
+ with gr.Column():
907
+ diagnostics_markdown = gr.Markdown(
908
+ value="Click refresh to load diagnostics",
909
+ label="Diagnostics Summary"
910
+ )
911
+
912
+ gr.Markdown("""
913
+ **Diagnostic Information Sources:**
914
+ - **Session Archive:** Agent state transitions, session archival events
915
+ - **Health Monitor:** Component health checks, system status, error tracking
916
+
917
+ **Common Error States:**
918
+ - **unknown error**: Check health monitor logs for component failures
919
+ - **RUNNING stage with errors**: Agent may be in recovery state
920
+ - **component unhealthy**: Specific component (brain, rbac, etc.) needs attention
921
+ """)
922
+
923
  # Footer with refresh
924
  with gr.Row():
925
  refresh_btn = gr.Button("🔄 Refresh Dashboard", variant="primary")
 
937
  ]
938
  )
939
 
940
+ # Diagnostics refresh handler (this references the button inside the tab)
941
+ # Note: This is defined after the tabs are created, so the button reference is available
942
+
943
  # Manual refresh for main dashboard
944
  refresh_btn.click(
945
  fn=refresh_all,
 
979
  outputs=[health_json, logs_table, health_report, last_updated]
980
  )
981
 
982
+ # Diagnostics refresh handler
983
+ refresh_diagnostics_btn.click(
984
+ fn=refresh_diagnostics,
985
+ inputs=[],
986
+ outputs=[
987
+ session_logs_json,
988
+ health_status_json,
989
+ session_logs_table,
990
+ diagnostics_markdown,
991
+ diagnostics_last_updated
992
+ ]
993
+ )
994
+
995
  return app
996
 
997