from datetime import datetime, timezone from fastapi import APIRouter from fastapi.responses import PlainTextResponse from app.core.config import get_parsed_servers from app.models.schemas import ( AiContextResponse, AiDuplicateSummary, AiInventoryStats, AiVmSummary, ScanResponse, ) from app.services.scanner import scan_servers router = APIRouter(prefix="/ai", tags=["ai"]) @router.get("/context", response_model=AiContextResponse) async def get_ai_context() -> AiContextResponse: scan = await scan_servers(get_parsed_servers()) return build_ai_context(scan) @router.get("/context.md", response_class=PlainTextResponse) async def get_ai_context_markdown() -> str: scan = await scan_servers(get_parsed_servers()) context = build_ai_context(scan) return render_markdown(context) def build_ai_context(scan: ScanResponse) -> AiContextResponse: running = sum(1 for item in scan.items if item.status == "running") stopped = sum(1 for item in scan.items if item.status == "stopped") qemu = sum(1 for item in scan.items if item.type == "qemu") lxc = sum(1 for item in scan.items if item.type == "lxc") return AiContextResponse( generated_at=datetime.now(timezone.utc).isoformat(), purpose=( "Contexte compact pour analyser l'inventaire Proxmox, les erreurs de scan " "et les doublons de VM/LXC." ), stats=AiInventoryStats( total=len(scan.items), running=running, stopped=stopped, qemu=qemu, lxc=lxc, duplicate_groups=len(scan.duplicates), ), servers=scan.servers, duplicates=[ AiDuplicateSummary( label=group.label, reason=group.reason, count=group.count, vmids=[ f"{item.server}/{item.node}/{item.type}/{item.vmid}" for item in group.items ], ) for group in scan.duplicates ], inventory=[ AiVmSummary( server=item.server, node=item.node, vmid=item.vmid, type=item.type, name=item.name, status=item.status, tags=item.tags, duplicate_id=item.duplicate_id, ) for item in scan.items ], notes=[ "Les secrets API Proxmox ne sont jamais exposes par cette route.", "Les champs duplicate_id dependent de la lecture des descriptions Proxmox.", "Une erreur de serveur indique souvent un probleme reseau, TLS, token ou ACL.", ], ) def render_markdown(context: AiContextResponse) -> str: lines = [ "# Prox Visualizer AI Context", "", f"Generated at: `{context.generated_at}`", "", "## Stats", "", f"- Total VM/LXC: {context.stats.total}", f"- Running: {context.stats.running}", f"- Stopped: {context.stats.stopped}", f"- QEMU VM: {context.stats.qemu}", f"- LXC: {context.stats.lxc}", f"- Duplicate groups: {context.stats.duplicate_groups}", "", "## Servers", "", ] for server in context.servers: state = "OK" if server.ok else "ERROR" lines.append(f"- {server.name}: {state}, {server.vm_count} VM/LXC, `{server.url}`") for error in server.errors: lines.append(f" - Error: {error}") lines.extend(["", "## Duplicates", ""]) if context.duplicates: for duplicate in context.duplicates: vmids = ", ".join(duplicate.vmids) lines.append( f"- {duplicate.label} ({duplicate.reason}, x{duplicate.count}): {vmids}" ) else: lines.append("- None") lines.extend(["", "## Inventory", ""]) for item in context.inventory: tags = f", tags={item.tags}" if item.tags else "" duplicate_id = ( f", duplicate_id={item.duplicate_id}" if item.duplicate_id is not None else "" ) lines.append( f"- {item.server}/{item.node}/{item.type}/{item.vmid}: " f"{item.name}, status={item.status or 'unknown'}{tags}{duplicate_id}" ) lines.extend(["", "## Notes", ""]) for note in context.notes: lines.append(f"- {note}") return "\n".join(lines) + "\n"