#!/usr/bin/env python3
"""
DAARION Node State Discovery Script
This script collects and documents the current state of nodes in the DAARION network,
including Swapper models, DAGI Router agents, and database agents.
Usage:
python scripts/discover_node_state.py --node node-1-hetzner-gex44
python scripts/discover_node_state.py --node node-2-macbook-m4max
python scripts/discover_node_state.py --all
Output:
Creates markdown files in docs/users/nodes/NODE_STATE_{node_id}.md
"""
import argparse
import json
import os
import sys
from datetime import datetime
from pathlib import Path
try:
import httpx
except ImportError:
print("❌ httpx not installed. Run: pip install httpx")
sys.exit(1)
# Configuration
CITY_API_URL = os.environ.get("CITY_API_URL", "https://daarion.space/api")
DB_CONNECTION = os.environ.get("DATABASE_URL", None)
KNOWN_NODES = [
"node-1-hetzner-gex44",
"node-2-macbook-m4max",
]
OUTPUT_DIR = Path(__file__).parent.parent / "docs" / "users" / "nodes"
def fetch_swapper_state(node_id: str) -> dict:
"""Fetch Swapper state from city-service API."""
url = f"{CITY_API_URL}/node-internal/{node_id}/swapper"
try:
with httpx.Client(timeout=10.0) as client:
resp = client.get(url)
if resp.status_code == 200:
return resp.json()
else:
return {"error": f"HTTP {resp.status_code}", "healthy": False, "models": []}
except Exception as e:
return {"error": str(e), "healthy": False, "models": []}
def fetch_dagi_router_agents(node_id: str) -> dict:
"""Fetch DAGI Router agents from city-service API."""
url = f"{CITY_API_URL}/node-internal/{node_id}/dagi-router/agents"
try:
with httpx.Client(timeout=10.0) as client:
resp = client.get(url)
if resp.status_code == 200:
return resp.json()
else:
return {"error": f"HTTP {resp.status_code}", "agents": []}
except Exception as e:
return {"error": str(e), "agents": []}
def fetch_node_info(node_id: str) -> dict:
"""Fetch node info from city-service API."""
url = f"{CITY_API_URL}/nodes/{node_id}"
try:
with httpx.Client(timeout=10.0) as client:
resp = client.get(url)
if resp.status_code == 200:
return resp.json()
else:
return {"error": f"HTTP {resp.status_code}"}
except Exception as e:
return {"error": str(e)}
def generate_markdown(node_id: str, swapper: dict, router: dict, node_info: dict) -> str:
"""Generate markdown report for a node."""
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
md = f"""# Node {node_id} — State
> Generated: {now}
## Node Info
| Field | Value |
|-------|-------|
| Node ID | `{node_id}` |
| Name | {node_info.get('name', 'N/A')} |
| Status | {node_info.get('status', 'N/A')} |
| Environment | {node_info.get('environment', 'N/A')} |
| Hostname | {node_info.get('hostname', 'N/A')} |
## Swapper
"""
if swapper.get("error"):
md += f"**Error:** {swapper['error']}\n\n"
else:
healthy = "✅ Healthy" if swapper.get("healthy") else "❌ Unhealthy"
models_loaded = swapper.get("models_loaded", 0)
models_total = swapper.get("models_total", 0)
md += f"""- **Status:** {healthy}
- **Models Loaded:** {models_loaded} / {models_total}
### Models
| Name | Type | Loaded | VRAM (GB) |
|------|------|--------|-----------|
"""
models = swapper.get("models", [])
if models:
for m in models:
loaded = "✅" if m.get("loaded") else "❌"
vram = m.get("vram_gb", "N/A")
md += f"| {m.get('name', 'N/A')} | {m.get('type', 'N/A')} | {loaded} | {vram} |\n"
else:
md += "| (no models) | | | |\n"
md += "\n## DAGI Router Agents\n\n"
if router.get("error"):
md += f"**Error:** {router['error']}\n\n"
else:
total = router.get("total", 0)
active = router.get("active", 0)
phantom = router.get("phantom", 0)
stale = router.get("stale", 0)
md += f"""- **Total Agents:** {total}
- **Active:** {active}
- **Phantom:** {phantom}
- **Stale:** {stale}
### Agent List
| ID | Name | Kind | Status | Runtime | Has DB Record |
|----|------|------|--------|---------|---------------|
"""
agents = router.get("agents", [])
if agents:
for a in agents:
md += f"| {a.get('id', 'N/A')} | {a.get('name', 'N/A')} | {a.get('kind', 'N/A')} | {a.get('status', 'N/A')} | {a.get('runtime', 'N/A')} | {a.get('has_db_record', 'N/A')} |\n"
else:
md += "| (no agents) | | | | | |\n"
md += f"""
---
## Raw Data
Swapper JSON
```json
{json.dumps(swapper, indent=2, default=str)}
```
DAGI Router JSON
```json
{json.dumps(router, indent=2, default=str)}
```
"""
return md
def discover_node(node_id: str, output_dir: Path) -> None:
"""Discover and document state for a single node."""
print(f"\n🔍 Discovering node: {node_id}")
# Fetch data
print(f" 📡 Fetching Swapper state...")
swapper = fetch_swapper_state(node_id)
print(f" 📡 Fetching DAGI Router agents...")
router = fetch_dagi_router_agents(node_id)
print(f" 📡 Fetching node info...")
node_info = fetch_node_info(node_id)
# Generate markdown
md = generate_markdown(node_id, swapper, router, node_info)
# Write to file
output_dir.mkdir(parents=True, exist_ok=True)
output_file = output_dir / f"NODE_STATE_{node_id}.md"
output_file.write_text(md)
print(f" ✅ Saved to: {output_file}")
# Print summary
print(f"\n 📊 Summary:")
print(f" Swapper: {'healthy' if swapper.get('healthy') else 'unhealthy'}, {swapper.get('models_total', 0)} models")
print(f" Agents: {router.get('total', 0)} total, {router.get('active', 0)} active")
def main():
parser = argparse.ArgumentParser(description="Discover DAARION node state")
parser.add_argument("--node", type=str, help="Specific node ID to discover")
parser.add_argument("--all", action="store_true", help="Discover all known nodes")
parser.add_argument("--output", type=str, default=str(OUTPUT_DIR), help="Output directory")
parser.add_argument("--api-url", type=str, default=CITY_API_URL, help="City API URL")
args = parser.parse_args()
if not args.node and not args.all:
print("❌ Please specify --node or --all")
parser.print_help()
sys.exit(1)
global CITY_API_URL
CITY_API_URL = args.api_url
output_dir = Path(args.output)
print(f"🚀 DAARION Node State Discovery")
print(f" API URL: {CITY_API_URL}")
print(f" Output: {output_dir}")
nodes_to_discover = KNOWN_NODES if args.all else [args.node]
for node_id in nodes_to_discover:
discover_node(node_id, output_dir)
print(f"\n✨ Discovery complete!")
if __name__ == "__main__":
main()