Spaces:
Sleeping
Sleeping
Cyber Catalyst Team commited on
Commit ·
9621cee
1
Parent(s): 1e1e634
Implement Command Code repair harness + real-time HTML dashboard visualizer
Browse files- 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 |
-
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 453 |
|
| 454 |
if best_model:
|
| 455 |
RECOMMENDED_MODEL = best_model
|
| 456 |
-
|
| 457 |
else:
|
| 458 |
-
|
| 459 |
|
| 460 |
async def periodic_health_check_loop():
|
| 461 |
-
# Wait
|
| 462 |
-
await asyncio.sleep(
|
| 463 |
while True:
|
| 464 |
try:
|
| 465 |
await check_models_health()
|
| 466 |
except Exception as e:
|
| 467 |
-
|
| 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 |
-
|
| 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(
|
| 637 |
except json.JSONDecodeError:
|
| 638 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 639 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 640 |
# Show tool execution to user
|
| 641 |
yield make_chunk(request_id, requested_model, f"\n\n🔧 **{func_name}**")
|
| 642 |
-
if
|
| 643 |
-
yield make_chunk(request_id, requested_model,
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
|
|
|
|
|
|
|
|
|
| 648 |
elif func_name == "list_directory":
|
| 649 |
-
yield make_chunk(request_id, requested_model, f": `{
|
| 650 |
elif func_name == "grep_search":
|
| 651 |
-
yield make_chunk(request_id, requested_model, f": `{
|
| 652 |
else:
|
| 653 |
yield make_chunk(request_id, requested_model, "\n")
|
| 654 |
|
| 655 |
# Execute the tool
|
| 656 |
-
result = execute_tool(func_name,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
|