File size: 3,699 Bytes
587a933
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#!/usr/bin/env python3
"""
Preflight β€” verify every Path B dependency before flipping KanBot to live.

Probes the real services (no mocks). Prints a checklist and exits non-zero if
anything the live cycle needs is missing, so you never flip DRY_RUN=0 into a
half-ready stack.

  βœ“ Postgres reachable on DATABASE_URL, schema present
  βœ“ Ollama reachable on OLLAMA_HOST
  βœ“ both configured models pulled
  βœ“ dry-run status reported (informational)

Run:  python cli/preflight.py
"""
from __future__ import annotations

import asyncio
import os
import sys

sys.path.insert(0, ".")

GREEN, RED, YEL, DIM, RST = "\033[32m", "\033[31m", "\033[33m", "\033[2m", "\033[0m"
OK, BAD, WARN = f"{GREEN}βœ“{RST}", f"{RED}βœ—{RST}", f"{YEL}!{RST}"

DSN = os.environ.get("DATABASE_URL", "postgresql://kanbot:kanbot@localhost:5432/kanbot")
OLLAMA = os.environ.get("OLLAMA_HOST", "http://localhost:11434")
MODEL_FAST = os.environ.get("KANBOT_MODEL_FAST", "qwen2.5:8b-instruct-q4_K_M")
MODEL_DAILY = os.environ.get("KANBOT_MODEL_DAILY", "qwen2.5:14b-instruct-q4_K_M")


async def check_postgres() -> tuple[bool, str]:
    try:
        import asyncpg
    except ImportError:
        return False, "asyncpg not installed (pip install -r requirements.txt)"
    try:
        conn = await asyncpg.connect(DSN, timeout=4)
    except Exception as e:
        return False, f"cannot connect to {DSN}: {e}"
    try:
        has = await conn.fetchval(
            "SELECT to_regclass('public.cards') IS NOT NULL")
        return (bool(has),
                "connected, schema present" if has
                else "connected but 'cards' table missing (apply sql/001_schema.sql)")
    finally:
        await conn.close()


async def check_ollama() -> tuple[bool, str, set[str]]:
    try:
        import httpx
    except ImportError:
        return False, "httpx not installed", set()
    try:
        async with httpx.AsyncClient(timeout=4) as c:
            r = await c.get(f"{OLLAMA}/api/tags")
            r.raise_for_status()
            names = {m["name"] for m in r.json().get("models", [])}
            return True, f"reachable at {OLLAMA}", names
    except Exception as e:
        return False, f"unreachable at {OLLAMA}: {e}", set()


async def main() -> int:
    print(f"\n  KanBot preflight β€” Path B readiness\n")
    fail = 0

    ok, msg = await check_postgres()
    print(f"  {OK if ok else BAD} postgres   {DIM}{msg}{RST}")
    fail += not ok

    ok, msg, models = await check_ollama()
    print(f"  {OK if ok else BAD} ollama     {DIM}{msg}{RST}")
    fail += not ok

    if ok:
        for label, m in (("daily/critic", MODEL_DAILY), ("fast head", MODEL_FAST)):
            have = any(m.split(":")[0] in n or m == n for n in models)
            print(f"  {OK if have else BAD} model      {DIM}{label}: {m} "
                  f"{'pulled' if have else 'MISSING β€” ollama pull ' + m}{RST}")
            fail += not have
    else:
        print(f"  {WARN} model      {DIM}skipped (ollama unreachable){RST}")

    dry = os.environ.get("KANBOT_DRY_RUN", "1") == "1"
    print(f"  {WARN} dry-run    {DIM}KANBOT_DRY_RUN={'1 (proposals only)' if dry else '0 (LIVE β€” commands execute)'}{RST}")

    print()
    if fail:
        print(f"  {RED}NOT READY: {fail} dependency check(s) failed. See GOLIVE.md.{RST}\n")
        return 1
    print(f"  {GREEN}READY: all Path B dependencies satisfied.{RST}")
    if dry:
        print(f"  {DIM}Still in dry-run. Flip KANBOT_DRY_RUN=0 when you want live execution.{RST}\n")
    else:
        print(f"  {YEL}LIVE MODE ON β€” approved cards will execute through the sandbox.{RST}\n")
    return 0


if __name__ == "__main__":
    raise SystemExit(asyncio.run(main()))