import json import re from dataclasses import dataclass, asdict from pathlib import Path from typing import List, Optional APP_HEADER_RE = re.compile(r"^### App: (?P.+?)\s*$") IMAGE_RE = re.compile(r"image=(?P[^\s]+)") PORT_MAP_RE = re.compile(r"- tcp (?P\d+) -> (?P\d+|0\.0\.0\.0:(?P\d+))") PORT_LINE_RE = re.compile(r"- tcp (?P\d+) -> (?P[^:]+):(?P\d+)") VOLUME_RE = re.compile(r"- (?P/[^\s]+) -> (?P/[^\s]+)") NETWORK_RE = re.compile(r"- (?Pix-[^\s]+)_default") SUBNET_RE = re.compile(r"subnets=\[(?P[^\]]+)\]") MODELS_RE = re.compile(r"Models in /models: (?P.+)$") PORTAL_RE = re.compile(r"Portals: \{\'Web UI\': \'(?P[^\']+)\'\}") GPU_RE = re.compile(r"GPUs:\s*(?P\d+)x\s*(?P.+)$") CONTAINER_NAME_RE = re.compile(r"^(?Pix-llamacpp-[^\s]+)") @dataclass class LlamacppConfig: image: Optional[str] = None container_name: Optional[str] = None host_port: Optional[int] = None container_port: Optional[int] = None web_ui_url: Optional[str] = None model_host_path: Optional[str] = None model_container_path: Optional[str] = None models: List[str] = None network: Optional[str] = None subnets: List[str] = None gpu_count: Optional[int] = None gpu_name: Optional[str] = None def _find_section(lines: List[str], app_name: str) -> List[str]: start = None for i, line in enumerate(lines): m = APP_HEADER_RE.match(line.strip()) if m and m.group("name") == app_name: start = i break if start is None: return [] for j in range(start + 1, len(lines)): if APP_HEADER_RE.match(lines[j].strip()): return lines[start:j] return lines[start:] def parse_agents(path: Path) -> LlamacppConfig: text = path.read_text(encoding="utf-8", errors="ignore") lines = text.splitlines() section = _find_section(lines, "llamacpp") cfg = LlamacppConfig(models=[], subnets=[]) for line in section: if cfg.image is None: m = IMAGE_RE.search(line) if m: cfg.image = m.group("image") if cfg.web_ui_url is None: m = PORTAL_RE.search(line) if m: cfg.web_ui_url = m.group("url") if cfg.container_port is None or cfg.host_port is None: m = PORT_LINE_RE.search(line) if m: cfg.container_port = int(m.group("container")) cfg.host_port = int(m.group("host")) if cfg.model_host_path is None or cfg.model_container_path is None: m = VOLUME_RE.search(line) if m and "/models" in m.group("container"): cfg.model_host_path = m.group("host") cfg.model_container_path = m.group("container") if cfg.network is None: m = NETWORK_RE.search(line) if m: cfg.network = f"{m.group('name')}_default" if "subnets=" in line: m = SUBNET_RE.search(line) if m: subnets_raw = m.group("subnets") subnets = [s.strip().strip("'") for s in subnets_raw.split(",")] cfg.subnets.extend([s for s in subnets if s]) if "Models in /models:" in line: m = MODELS_RE.search(line) if m: models_raw = m.group("models") cfg.models = [s.strip() for s in models_raw.split(",") if s.strip()] for line in lines: if cfg.gpu_count is None: m = GPU_RE.search(line) if m: cfg.gpu_count = int(m.group("count")) cfg.gpu_name = m.group("name").strip() if cfg.container_name is None: m = CONTAINER_NAME_RE.match(line.strip()) if m: cfg.container_name = m.group("name") return cfg def main() -> None: import argparse parser = argparse.ArgumentParser() parser.add_argument("--agents", default="AGENTS.md") parser.add_argument("--out", default="app/agents_config.json") args = parser.parse_args() cfg = parse_agents(Path(args.agents)) out_path = Path(args.out) out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_text(json.dumps(asdict(cfg), indent=2), encoding="utf-8") if __name__ == "__main__": main()