Cyber Catalyst Team commited on
Commit
9621cee
·
1 Parent(s): 1e1e634

Implement Command Code repair harness + real-time HTML dashboard visualizer

Browse files
Files changed (1) hide show
  1. backend.py +303 -23
backend.py CHANGED
@@ -20,16 +20,31 @@ import uuid
20
  import subprocess
21
  import asyncio
22
  import time
 
 
23
  from pathlib import Path
24
  from typing import AsyncIterator, Optional
25
 
26
  from fastapi import FastAPI, Request, Header, HTTPException
27
- from fastapi.responses import StreamingResponse, JSONResponse
28
  from fastapi.middleware.cors import CORSMiddleware
29
  from openai import AsyncOpenAI
30
  import anyio
31
  import asyncpg
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  # ---------------------------------------------------------------------------
34
  # Configuration
35
  # ---------------------------------------------------------------------------
@@ -190,6 +205,47 @@ def _safe_path(rel_path: str) -> Path:
190
  return target
191
 
192
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  def execute_tool(name: str, arguments: dict) -> str:
194
  """Execute a tool and return its output as a string."""
195
  try:
@@ -428,7 +484,7 @@ async def check_models_health():
428
  best_model = None
429
  best_latency = 999.0
430
 
431
- print("[Health Check] Starting periodic model verification...")
432
  for model in models_to_test:
433
  start_time = time.time()
434
  try:
@@ -440,7 +496,8 @@ async def check_models_health():
440
  max_tokens=3,
441
  )
442
  latency = time.time() - start_time
443
- print(f"[Health Check] Model {model} is ONLINE. Latency: {latency:.2f}s")
 
444
 
445
  # We want the model that is within 15 seconds
446
  # and is the fastest (lowest latency)
@@ -449,33 +506,35 @@ async def check_models_health():
449
  best_model = model
450
 
451
  except Exception as e:
452
- print(f"[Health Check] Model {model} is OFFLINE or TIMEOUT: {e}")
 
453
 
454
  if best_model:
455
  RECOMMENDED_MODEL = best_model
456
- print(f"[Health Check] Best model found: {RECOMMENDED_MODEL} ({best_latency:.2f}s)")
457
  else:
458
- print("[Health Check] Warning: All checked models failed or timed out!")
459
 
460
  async def periodic_health_check_loop():
461
- # Wait 30 seconds after startup before the first check to let the space boot fully
462
- await asyncio.sleep(30)
463
  while True:
464
  try:
465
  await check_models_health()
466
  except Exception as e:
467
- print(f"[Health Check] Loop error: {e}")
468
  await asyncio.sleep(300) # every 5 minutes
469
 
470
  @app.on_event("startup")
471
  async def startup():
472
  await init_db()
473
  Path(WORKSPACE_DIR).mkdir(parents=True, exist_ok=True)
 
 
 
474
  # Start background health checking
475
  asyncio.create_task(periodic_health_check_loop())
476
- print(f"[Backend] Started. Workspace: {WORKSPACE_DIR}")
477
- print(f"[Backend] Tool-capable models: {list(TOOL_CAPABLE_MODELS.keys())}")
478
- print(f"[Backend] DB connected: {db_pool is not None}")
479
 
480
 
481
  # ---------------------------------------------------------------------------
@@ -512,6 +571,9 @@ async def chat_completions(request: Request, authorization: str = Header(None)):
512
  is_agentic = requested_model in TOOL_CAPABLE_MODELS
513
  request_id = str(uuid.uuid4())[:8]
514
 
 
 
 
515
  # Build message history
516
  final_messages = []
517
 
@@ -550,6 +612,8 @@ async def chat_completions(request: Request, authorization: str = Header(None)):
550
  response = await nim_client.chat.completions.create(**kwargs)
551
  content = response.choices[0].message.content or ""
552
  await save_message(session_id, "assistant", content)
 
 
553
  return JSONResponse({
554
  "id": f"chatcmpl-{request_id}",
555
  "object": "chat.completion",
@@ -558,6 +622,7 @@ async def chat_completions(request: Request, authorization: str = Header(None)):
558
  "choices": [{"index": 0, "message": {"role": "assistant", "content": content}, "finish_reason": "stop"}],
559
  })
560
  except Exception as e:
 
561
  return JSONResponse({"error": {"message": str(e), "type": "internal_error"}}, status_code=500)
562
 
563
  # Streaming + agentic loop
@@ -632,28 +697,55 @@ async def chat_completions(request: Request, authorization: str = Header(None)):
632
  # Execute each tool and add results
633
  for tc in tool_calls_list:
634
  func_name = tc["function"]["name"]
 
635
  try:
636
- func_args = json.loads(tc["function"]["arguments"])
637
  except json.JSONDecodeError:
638
- func_args = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
639
 
 
 
 
 
 
 
640
  # Show tool execution to user
641
  yield make_chunk(request_id, requested_model, f"\n\n🔧 **{func_name}**")
642
- if func_name == "run_bash" and "command" in func_args:
643
- yield make_chunk(request_id, requested_model, f": `{func_args['command']}`\n")
644
- elif func_name == "read_file" and "path" in func_args:
645
- yield make_chunk(request_id, requested_model, f": `{func_args['path']}`\n")
646
- elif func_name == "write_file" and "path" in func_args:
647
- yield make_chunk(request_id, requested_model, f": `{func_args['path']}`\n")
 
 
 
648
  elif func_name == "list_directory":
649
- yield make_chunk(request_id, requested_model, f": `{func_args.get('path', '.')}`\n")
650
  elif func_name == "grep_search":
651
- yield make_chunk(request_id, requested_model, f": `{func_args.get('pattern', '')}`\n")
652
  else:
653
  yield make_chunk(request_id, requested_model, "\n")
654
 
655
  # Execute the tool
656
- result = execute_tool(func_name, func_args)
 
 
 
 
657
 
658
  # Show truncated result to user
659
  preview = result[:500] + ("..." if len(result) > 500 else "")
@@ -717,6 +809,193 @@ async def list_models(authorization: str = Header(None)):
717
  # /health — Health check
718
  # ---------------------------------------------------------------------------
719
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
720
  @app.get("/health")
721
  async def health():
722
  return {
@@ -726,6 +1005,7 @@ async def health():
726
  "db_connected": db_pool is not None,
727
  "models_count": len(ALL_MODELS),
728
  "recommended_model": RECOMMENDED_MODEL,
 
729
  }
730
 
731
 
 
20
  import subprocess
21
  import asyncio
22
  import time
23
+ import re
24
+ import collections
25
  from pathlib import Path
26
  from typing import AsyncIterator, Optional
27
 
28
  from fastapi import FastAPI, Request, Header, HTTPException
29
+ from fastapi.responses import StreamingResponse, JSONResponse, HTMLResponse
30
  from fastapi.middleware.cors import CORSMiddleware
31
  from openai import AsyncOpenAI
32
  import anyio
33
  import asyncpg
34
 
35
+ # ---------------------------------------------------------------------------
36
+ # Globals & Activity Logs
37
+ # ---------------------------------------------------------------------------
38
+ activity_logs = collections.deque(maxlen=100)
39
+ MODEL_STATUSES = {}
40
+ ACTIVE_SESSIONS = set()
41
+
42
+ def log_activity(msg: str):
43
+ timestamp = time.strftime("%H:%M:%S")
44
+ log_line = f"[{timestamp}] {msg}"
45
+ activity_logs.append(log_line)
46
+ print(log_line)
47
+
48
  # ---------------------------------------------------------------------------
49
  # Configuration
50
  # ---------------------------------------------------------------------------
 
205
  return target
206
 
207
 
208
+ def repair_arguments(func_name: str, args: dict) -> tuple[dict, list[str]]:
209
+ notes = []
210
+ repaired_args = dict(args)
211
+
212
+ # 1. Nesting extraction (e.g. {"path": {"path": "file.txt"}})
213
+ for key in list(repaired_args.keys()):
214
+ val = repaired_args[key]
215
+ if isinstance(val, dict) and key in val:
216
+ repaired_args[key] = val[key]
217
+ notes.append(f"Flattened nested parameter '{key}'")
218
+
219
+ # 2. Markdown stripping from bash command
220
+ if func_name == "run_bash" and "command" in repaired_args:
221
+ cmd = repaired_args["command"]
222
+ if isinstance(cmd, str):
223
+ pattern = r"```(?:bash)?\s*(.*?)\s*```"
224
+ match = re.search(pattern, cmd, re.DOTALL)
225
+ if match:
226
+ repaired_args["command"] = match.group(1).strip()
227
+ notes.append("Stripped markdown code blocks from bash command")
228
+
229
+ # 3. Stringified array conversion
230
+ for key, val in repaired_args.items():
231
+ if isinstance(val, str) and val.strip().startswith("[") and val.strip().endswith("]"):
232
+ try:
233
+ parsed_arr = json.loads(val)
234
+ if isinstance(parsed_arr, list):
235
+ repaired_args[key] = parsed_arr
236
+ notes.append(f"Converted stringified array for parameter '{key}' to native array")
237
+ except:
238
+ pass
239
+
240
+ # 4. Optional empty objects replacing Null
241
+ for key in list(repaired_args.keys()):
242
+ if repaired_args[key] == {}:
243
+ repaired_args[key] = None
244
+ notes.append(f"Replaced empty object for parameter '{key}' with null")
245
+
246
+ return repaired_args, notes
247
+
248
+
249
  def execute_tool(name: str, arguments: dict) -> str:
250
  """Execute a tool and return its output as a string."""
251
  try:
 
484
  best_model = None
485
  best_latency = 999.0
486
 
487
+ log_activity("Periodic health check started: verifying 10 NIM models...")
488
  for model in models_to_test:
489
  start_time = time.time()
490
  try:
 
496
  max_tokens=3,
497
  )
498
  latency = time.time() - start_time
499
+ MODEL_STATUSES[model] = {"status": "ONLINE", "latency": f"{latency:.2f}s", "raw_latency": latency}
500
+ log_activity(f"Model checked: {model} is ONLINE ({latency:.2f}s)")
501
 
502
  # We want the model that is within 15 seconds
503
  # and is the fastest (lowest latency)
 
506
  best_model = model
507
 
508
  except Exception as e:
509
+ MODEL_STATUSES[model] = {"status": "OFFLINE", "latency": "N/A", "raw_latency": 999.0}
510
+ log_activity(f"Model checked: {model} is OFFLINE / TIMEOUT: {e}")
511
 
512
  if best_model:
513
  RECOMMENDED_MODEL = best_model
514
+ log_activity(f"Best model selected: {RECOMMENDED_MODEL} ({best_latency:.2f}s)")
515
  else:
516
+ log_activity("Warning: All checked models failed or timed out!")
517
 
518
  async def periodic_health_check_loop():
519
+ # Wait 10 seconds after startup before the first check to let the space boot fully
520
+ await asyncio.sleep(10)
521
  while True:
522
  try:
523
  await check_models_health()
524
  except Exception as e:
525
+ log_activity(f"Health check loop error: {e}")
526
  await asyncio.sleep(300) # every 5 minutes
527
 
528
  @app.on_event("startup")
529
  async def startup():
530
  await init_db()
531
  Path(WORKSPACE_DIR).mkdir(parents=True, exist_ok=True)
532
+ # Initialize statuses for all models
533
+ for model_id, display_name in ALL_MODELS.items():
534
+ MODEL_STATUSES[model_id] = {"status": "UNCHECKED", "latency": "N/A", "raw_latency": 999.0}
535
  # Start background health checking
536
  asyncio.create_task(periodic_health_check_loop())
537
+ log_activity(f"FastAPI backend started. Workspace: {WORKSPACE_DIR}")
 
 
538
 
539
 
540
  # ---------------------------------------------------------------------------
 
571
  is_agentic = requested_model in TOOL_CAPABLE_MODELS
572
  request_id = str(uuid.uuid4())[:8]
573
 
574
+ ACTIVE_SESSIONS.add(session_id)
575
+ log_activity(f"Session [{session_id[:6]}] connected. Model: {requested_model}")
576
+
577
  # Build message history
578
  final_messages = []
579
 
 
612
  response = await nim_client.chat.completions.create(**kwargs)
613
  content = response.choices[0].message.content or ""
614
  await save_message(session_id, "assistant", content)
615
+ ACTIVE_SESSIONS.discard(session_id)
616
+ log_activity(f"Session [{session_id[:6]}] finished (non-streaming)")
617
  return JSONResponse({
618
  "id": f"chatcmpl-{request_id}",
619
  "object": "chat.completion",
 
622
  "choices": [{"index": 0, "message": {"role": "assistant", "content": content}, "finish_reason": "stop"}],
623
  })
624
  except Exception as e:
625
+ ACTIVE_SESSIONS.discard(session_id)
626
  return JSONResponse({"error": {"message": str(e), "type": "internal_error"}}, status_code=500)
627
 
628
  # Streaming + agentic loop
 
697
  # Execute each tool and add results
698
  for tc in tool_calls_list:
699
  func_name = tc["function"]["name"]
700
+ raw_args_str = tc["function"]["arguments"]
701
  try:
702
+ func_args = json.loads(raw_args_str)
703
  except json.JSONDecodeError:
704
+ # Attempt raw JSON repair
705
+ repaired_str = raw_args_str.strip()
706
+ if not repaired_str.startswith("{"):
707
+ repaired_str = "{" + repaired_str
708
+ if not repaired_str.endswith("}"):
709
+ repaired_str = repaired_str + "}"
710
+ try:
711
+ func_args = json.loads(repaired_str)
712
+ log_activity(f"Auto-fixed invalid JSON string for tool: {func_name}")
713
+ except:
714
+ func_args = {}
715
+
716
+ # Perform semantic repairs
717
+ repaired_args, repair_notes = repair_arguments(func_name, func_args)
718
 
719
+ # Log activity
720
+ log_activity(f"Tool execution: {func_name} args={repaired_args}")
721
+ if repair_notes:
722
+ for note in repair_notes:
723
+ log_activity(f"[Tool Repair] {note}")
724
+
725
  # Show tool execution to user
726
  yield make_chunk(request_id, requested_model, f"\n\n🔧 **{func_name}**")
727
+ if repair_notes:
728
+ yield make_chunk(request_id, requested_model, " *(Auto-Repaired)*")
729
+
730
+ if func_name == "run_bash" and "command" in repaired_args:
731
+ yield make_chunk(request_id, requested_model, f": `{repaired_args['command']}`\n")
732
+ elif func_name == "read_file" and "path" in repaired_args:
733
+ yield make_chunk(request_id, requested_model, f": `{repaired_args['path']}`\n")
734
+ elif func_name == "write_file" and "path" in repaired_args:
735
+ yield make_chunk(request_id, requested_model, f": `{repaired_args['path']}`\n")
736
  elif func_name == "list_directory":
737
+ yield make_chunk(request_id, requested_model, f": `{repaired_args.get('path', '.')}`\n")
738
  elif func_name == "grep_search":
739
+ yield make_chunk(request_id, requested_model, f": `{repaired_args.get('pattern', '')}`\n")
740
  else:
741
  yield make_chunk(request_id, requested_model, "\n")
742
 
743
  # Execute the tool
744
+ result = execute_tool(func_name, repaired_args)
745
+
746
+ # Append teaching note if repaired
747
+ if repair_notes:
748
+ result += f"\n\n[SYSTEM REPAIR NOTE: The harness automatically fixed formatting issues: {', '.join(repair_notes)}. Please strictly follow the tool's JSON schema in subsequent calls without these wrapping/formatting errors.]"
749
 
750
  # Show truncated result to user
751
  preview = result[:500] + ("..." if len(result) > 500 else "")
 
809
  # /health — Health check
810
  # ---------------------------------------------------------------------------
811
 
812
+ # ---------------------------------------------------------------------------
813
+ # Dashboard and Status API
814
+ # ---------------------------------------------------------------------------
815
+
816
+ DASHBOARD_HTML = """
817
+ <!DOCTYPE html>
818
+ <html lang="en">
819
+ <head>
820
+ <meta charset="UTF-8">
821
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
822
+ <title>Claude Code Agent Console</title>
823
+ <script src="https://cdn.tailwindcss.com"></script>
824
+ <style>
825
+ @import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;700&family=Outfit:wght@400;600;800&display=swap');
826
+ body {
827
+ font-family: 'Outfit', sans-serif;
828
+ background-color: #0b0c10;
829
+ }
830
+ .code-font {
831
+ font-family: 'Fira Code', monospace;
832
+ }
833
+ .glow-amber {
834
+ box-shadow: 0 0 15px rgba(245, 158, 11, 0.2);
835
+ }
836
+ </style>
837
+ </head>
838
+ <body class="text-gray-100 min-h-screen flex flex-col pb-10">
839
+ <header class="border-b border-gray-800 bg-gray-950/80 backdrop-blur px-6 py-4 flex items-center justify-between sticky top-0 z-50">
840
+ <div class="flex items-center space-x-3">
841
+ <span class="text-2xl font-extrabold tracking-tight bg-gradient-to-r from-blue-400 via-indigo-400 to-purple-400 bg-clip-text text-transparent">
842
+ Claude Code Agent Console
843
+ </span>
844
+ <span class="px-2 py-0.5 text-xs rounded bg-blue-500/10 text-blue-400 border border-blue-500/20 font-semibold animate-pulse">
845
+ LIVE
846
+ </span>
847
+ </div>
848
+ <div class="flex items-center space-x-4 text-sm text-gray-400">
849
+ <div>Workspace: <span class="text-gray-200 code-font">/tmp/workspace</span></div>
850
+ <div class="h-4 w-px bg-gray-800"></div>
851
+ <div>Active Sessions: <span id="active-sessions-count" class="text-blue-400 font-bold code-font">0</span></div>
852
+ </div>
853
+ </header>
854
+
855
+ <main class="max-w-7xl w-full mx-auto px-6 mt-8 flex-1 grid grid-cols-1 lg:grid-cols-3 gap-8">
856
+ <!-- Left: Models Grid -->
857
+ <div class="lg:col-span-2 space-y-6">
858
+ <div class="flex items-center justify-between">
859
+ <h2 class="text-lg font-bold tracking-tight text-gray-300">Nvidia NIM Models & Health Status</h2>
860
+ <span class="text-xs text-gray-500">Checked every 5 mins</span>
861
+ </div>
862
+
863
+ <div id="models-container" class="grid grid-cols-1 md:grid-cols-2 gap-4">
864
+ <!-- Dynamically loaded models go here -->
865
+ </div>
866
+ </div>
867
+
868
+ <!-- Right: Log Viewer -->
869
+ <div class="space-y-6 flex flex-col h-full">
870
+ <h2 class="text-lg font-bold tracking-tight text-gray-300">System Activity Logs</h2>
871
+
872
+ <div class="border border-gray-800 rounded-lg overflow-hidden bg-gray-950 flex flex-col flex-1 min-h-[400px]">
873
+ <div class="bg-gray-900 px-4 py-2 border-b border-gray-800 flex items-center justify-between">
874
+ <span class="text-xs text-gray-400 font-semibold code-font">agent-stdout.log</span>
875
+ <div class="flex space-x-1.5">
876
+ <span class="w-2.5 h-2.5 rounded-full bg-red-500/30"></span>
877
+ <span class="w-2.5 h-2.5 rounded-full bg-yellow-500/30"></span>
878
+ <span class="w-2.5 h-2.5 rounded-full bg-green-500/30"></span>
879
+ </div>
880
+ </div>
881
+ <div id="terminal-content" class="p-4 flex-1 overflow-y-auto code-font text-xs text-green-400 bg-black/90 space-y-1 select-all">
882
+ <!-- Logs go here -->
883
+ </div>
884
+ </div>
885
+ </div>
886
+ </main>
887
+
888
+ <script>
889
+ async function fetchSystemData() {
890
+ try {
891
+ const res = await fetch('/health');
892
+ if (!res.ok) return;
893
+ const data = await res.json();
894
+ document.getElementById('active-sessions-count').innerText = data.active_sessions || 0;
895
+ } catch (e) {
896
+ console.error(e);
897
+ }
898
+ }
899
+
900
+ async function fetchModels() {
901
+ try {
902
+ const res = await fetch('/api/models-status');
903
+ if (!res.ok) return;
904
+ const models = await res.json();
905
+
906
+ const container = document.getElementById('models-container');
907
+ container.innerHTML = '';
908
+
909
+ models.forEach(model => {
910
+ const isRec = model.is_recommended;
911
+ const isOnline = model.status === 'ONLINE';
912
+
913
+ const card = document.createElement('div');
914
+ card.className = `p-4 border rounded-xl bg-gray-950 transition-all ${
915
+ isRec ? 'border-amber-500/50 glow-amber bg-amber-500/5' : 'border-gray-800 bg-gray-950'
916
+ }`;
917
+
918
+ card.innerHTML = `
919
+ <div class="flex items-center justify-between mb-3">
920
+ <span class="text-xs text-gray-500 code-font truncate max-w-[200px]" title="${model.id}">${model.id}</span>
921
+ <div class="flex items-center space-x-2">
922
+ ${isRec ? '<span class="text-[10px] px-1.5 py-0.5 rounded bg-amber-500/10 text-amber-400 border border-amber-500/20 font-bold">★ Recommended</span>' : ''}
923
+ <span class="h-2 w-2 rounded-full ${isOnline ? 'bg-green-500 animate-pulse' : 'bg-red-500'}"></span>
924
+ <span class="text-[10px] font-bold ${isOnline ? 'text-green-400' : 'text-red-400'}">${model.status}</span>
925
+ </div>
926
+ </div>
927
+ <h3 class="text-sm font-bold text-gray-200 mb-2 truncate">${model.name}</h3>
928
+ <div class="flex items-center justify-between text-xs text-gray-400 border-t border-gray-900 pt-2">
929
+ <span>Type: <strong class="text-gray-300 font-medium">${model.type}</strong></span>
930
+ <span>Latency: <strong class="text-blue-400 code-font">${model.latency}</strong></span>
931
+ </div>
932
+ `;
933
+ container.appendChild(card);
934
+ });
935
+ } catch (e) {
936
+ console.error(e);
937
+ }
938
+ }
939
+
940
+ async function fetchLogs() {
941
+ try {
942
+ const res = await fetch('/api/logs');
943
+ if (!res.ok) return;
944
+ const logs = await res.json();
945
+
946
+ const term = document.getElementById('terminal-content');
947
+ const shouldScroll = term.scrollHeight - term.clientHeight <= term.scrollTop + 50;
948
+ term.innerHTML = logs.map(line => `<div>${line}</div>`).join('');
949
+ if (shouldScroll) {
950
+ term.scrollTop = term.scrollHeight;
951
+ }
952
+ } catch (e) {
953
+ console.error(e);
954
+ }
955
+ }
956
+
957
+ setInterval(fetchSystemData, 3000);
958
+ setInterval(fetchModels, 3000);
959
+ setInterval(fetchLogs, 2000);
960
+
961
+ fetchSystemData();
962
+ fetchModels();
963
+ fetchLogs();
964
+ </script>
965
+ </body>
966
+ </html>
967
+ """
968
+
969
+ @app.get("/", response_class=HTMLResponse)
970
+ async def dashboard():
971
+ return HTMLResponse(content=DASHBOARD_HTML)
972
+
973
+
974
+ @app.get("/api/logs")
975
+ async def get_logs():
976
+ return list(activity_logs)
977
+
978
+
979
+ @app.get("/api/models-status")
980
+ async def get_models_status():
981
+ status_list = []
982
+ for model_id, display_name in ALL_MODELS.items():
983
+ status_info = MODEL_STATUSES.get(model_id, {"status": "ONLINE (Unchecked)", "latency": "N/A"})
984
+ is_rec = model_id == RECOMMENDED_MODEL
985
+ is_agentic = model_id in TOOL_CAPABLE_MODELS
986
+ status_list.append({
987
+ "id": model_id,
988
+ "name": display_name,
989
+ "status": status_info["status"],
990
+ "latency": status_info["latency"],
991
+ "is_recommended": is_rec,
992
+ "type": "Agentic (Tools)" if is_agentic else "Chat Only",
993
+ })
994
+ # Sort: Recommended first, then Agentic, then Chat
995
+ status_list.sort(key=lambda m: (not m["is_recommended"], m["type"] != "Agentic (Tools)", m["name"]))
996
+ return status_list
997
+
998
+
999
  @app.get("/health")
1000
  async def health():
1001
  return {
 
1005
  "db_connected": db_pool is not None,
1006
  "models_count": len(ALL_MODELS),
1007
  "recommended_model": RECOMMENDED_MODEL,
1008
+ "active_sessions": len(ACTIVE_SESSIONS),
1009
  }
1010
 
1011