homelab-codex-ws/services/planner-agent
Oskar Kapala bd7f955e4e fix+debug(planner-agent): use base_url (not api_base) for litellm.acompletion, add print [TEMP]
litellm.acompletion() has base_url as a named param; api_base only works
via **kwargs fallback path. Switching to base_url ensures the value lands
correctly in completion_kwargs and reaches the ollama provider.

Print() added (not logger) so base_url is always visible in docker logs
regardless of log level.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 13:07:58 +02:00
..
src fix+debug(planner-agent): use base_url (not api_base) for litellm.acompletion, add print [TEMP] 2026-05-28 13:07:58 +02:00
tests feat(planner-agent): main loop with LLM routing and HITL action proposals 2026-05-27 19:11:39 +02:00
docker-compose.yml fix(planner-agent): remove duplicate ANTHROPIC_API_KEY from environment 2026-05-28 10:57:08 +02:00
Dockerfile feat(planner-agent): main loop with LLM routing and HITL action proposals 2026-05-27 19:11:39 +02:00
healthcheck.sh feat(planner-agent): main loop with LLM routing and HITL action proposals 2026-05-27 19:11:39 +02:00
README.md docs: add planner-agent docs and session summary 2026-05-27 2026-05-27 22:35:59 +02:00
requirements.txt feat(planner-agent): main loop with LLM routing and HITL action proposals 2026-05-27 19:11:39 +02:00
service.yaml feat(planner-agent): main loop with LLM routing and HITL action proposals 2026-05-27 19:11:39 +02:00

planner-agent

Asynchroniczny agent diagnozujący zdarzenia zdrowotne w homelabowej infrastrukturze.
Nasłuchuje na kanałach Redis pub/sub, wysyła zdarzenie do LLM i zapisuje propozycję akcji do actions/pending/ — gdzie musi ją zaakceptować operator.


Co robi

Redis pub/sub                    LLM (Ollama → Haiku → Sonnet)
  health_events   ──────────────►  diagnoza zdarzenia
  world_updates   ──────────────►  propozycja JSON
                                        │
                            cooldown gate (5 min / svc_key)
                                        │
                       actions/pending/<action_id>.json
                                        │
                         events/<date>/<node>/evt-*.json
                          (typ: remediation_started)

HITL invariant: planner pisze wyłącznie do actions/pending/.
Executor wymaga pliku w actions/approved/ — planner nigdy tego katalogu nie dotyka.

Obsługiwane kanały

Kanał Źródło Opis
health_events node-agent, stability-agent Zdarzenia zdrowotne kontenerów i systemu
world_updates observer (control-plane) Zmiany w world state

Benign events (pomijane bez wywołania LLM)

service_healthy, service_recovered, node_online, deployment_completed, deployment_started, remediation_started, remediation_completed


Fallback chain LLM

1. ollama/<OLLAMA_MODEL>     timeout  8 s   (lokalny GPU — SOLARIA)
2. claude-haiku-4-5-20251001 timeout 30 s   (Anthropic cloud)
3. claude-sonnet-4-6         timeout 30 s   (Anthropic cloud)

Model jest odrzucany, gdy:

  • przekroczy timeout
  • zwróci błąd sieci / API
  • odpowie tekstem pasującym do wzorca odmowy ("I cannot", "nie wiem", …)
  • zwróci JSON niezgodny ze schematem propozycji

Metryki każdego wywołania są publikowane na kanał Redis llm_router_metrics.

Schemat propozycji (JSON Schema)

{
  "action":         "restart | redeploy | notify | ignore",
  "service":        "<nazwa serwisu>",
  "node":           "<nazwa noda>",
  "reason":         "<wyjaśnienie, min. 10 znaków>",
  "confidence":     <0.01.0>,
  "requires_human": <true|false>
}

Mapowanie na typ executora:

LLM action Executor type Risk level
restart container_restart low
redeploy redeploy guarded
notify notify low
ignore (nie zapisuje)

Zmienne środowiskowe

Wszystkie zmienne runtime żyją w /opt/homelab/config/planner-agent/.env na węźle. Nie commituj tego pliku — nie jest w repo.

# Ollama — lokalny GPU węzła (np. SOLARIA)
# Użyj host-gateway zamiast localhost (kontener nie może sięgnąć hosta przez localhost)
OLLAMA_HOST=http://host-gateway:11434
OLLAMA_MODEL=qwen2.5-coder:14b     # dowolny model dostępny w ollama list

# Redis na piha
REDIS_URL=redis://100.108.208.3:6379

# Tożsamość noda
NODE_NAME=solaria

# Cooldown między propozycjami dla tego samego serwisu
COOLDOWN_SECONDS=300

# Ścieżka do runtime state
RUNTIME_PATH=/opt/homelab

# Opcjonalnie — wymagane do cloud fallback (haiku/sonnet)
# ANTHROPIC_API_KEY=sk-ant-...

ANTHROPIC_API_KEY jest jedyną zmienną przekazywaną przez sekcję environment w docker-compose.yml (nie jest w env_file — sekret injektowany przez operatora).


Deployment na SOLARIA

# 1. Przygotuj .env na solaria
ssh oskar@100.100.231.104
mkdir -p /opt/homelab/config/planner-agent
cat > /opt/homelab/config/planner-agent/.env << 'EOF'
OLLAMA_HOST=http://host-gateway:11434
OLLAMA_MODEL=qwen2.5-coder:14b
REDIS_URL=redis://100.108.208.3:6379
NODE_NAME=solaria
COOLDOWN_SECONDS=300
RUNTIME_PATH=/opt/homelab
EOF

# 2. Deploy przez standardowy skrypt (z SATURN)
scripts/deploy/deploy.sh --service planner-agent

# 3. Weryfikacja
docker ps --filter name=planner-agent
docker logs planner-agent --tail 30
ls -la /opt/homelab/state/planner-agent.heartbeat

Uwaga: stage verify w deploy.sh może failować bezpośrednio po starcie (race condition — heartbeat jest pisany co ~5 s). Kontener jest zdrowy gdy docker ps pokazuje (healthy).


Uruchamianie lokalne (bez Dockera)

cd services/planner-agent

# Zainstaluj zależności
pip install -r requirements.txt

# Wyeksportuj zmienne (lub stwórz .env i użyj `export $(cat .env | xargs)`)
export REDIS_URL=redis://localhost:6379
export OLLAMA_HOST=http://localhost:11434
export OLLAMA_MODEL=qwen2.5:7b
export NODE_NAME=dev-local
export RUNTIME_PATH=/tmp/homelab-dev
export COOLDOWN_SECONDS=10

# Uruchom
python src/planner.py

Testowanie

Testy jednostkowe

cd services/planner-agent
pip install -r requirements.txt pytest pytest-asyncio
pytest tests/ -v
# 49 testów planner + 34 testy llm_router

Ręczny end-to-end test przez Redis

# Opublikuj sztuczne zdarzenie unhealthy na kanale health_events
redis-cli -h 100.108.208.3 PUBLISH health_events '{
  "type": "service_unhealthy",
  "node": "piha",
  "service": "mosquitto",
  "severity": "error",
  "payload": {"exit_code": 1, "reason": "OOMKilled"},
  "timestamp": "2026-05-27T20:00:00Z"
}'

# Obserwuj logi planera
docker logs planner-agent -f

# Sprawdź czy propozycja trafiła do pending
ls /opt/homelab/actions/pending/
cat /opt/homelab/actions/pending/plan-piha-mosquitto-*.json

Śledzenie metryk LLM

# Subskrybuj metryki routera w czasie rzeczywistym
redis-cli -h 100.108.208.3 SUBSCRIBE llm_router_metrics

Healthcheck

Skrypt healthcheck.sh sprawdza czy plik heartbeat /opt/homelab/state/planner-agent.heartbeat jest świeższy niż 300 s. Heartbeat jest pisany na górze każdej iteracji pętli (≤5 s interwał).

# Ręczny test healthchecku
docker exec planner-agent /bin/sh /app/healthcheck.sh

Struktura plików

services/planner-agent/
├── src/
│   ├── planner.py       # Główna pętla agenta
│   └── llm_router.py    # Routing LLM z fallback chain
├── tests/
│   ├── test_planner.py  # 49 testów
│   └── test_llm_router.py # 34 testy
├── docker-compose.yml
├── Dockerfile
├── requirements.txt
├── service.yaml         # Kontrakt operacyjny dla agentów
├── healthcheck.sh
└── README.md