feat: scaffold Brain of Reese — FastAPI RAG chat over Postgres 17 + pgvector
Foundation (phase 01, verified): - FastAPI app: /api/health, /api/suggestions, /api/chat (placeholder), static frontend served locally (no CDN) - Postgres 17 + pgvector via db/Containerfile + compose.yaml (podman compose up -d db), Alembic initial migration (documents, chunks with vector(768), query_log) - LLM client targeting https://aipi.reeseapps.com/v1 (turbo/embed); scripts/llm_probe.py verified models + 768-dim embeddings live - Conditional debugpy: imported only when DEBUGPY=1 (attach on demand, :5678); logging config for clean single-line logs - Frontend shell: mobile-first chat + Sources pages, tokens, a11y baselines - Tests: 24 unit+integration (99% coverage on app/), ruff + pyright clean, Playwright smoke E2E (3 tests) against a deterministic mock LLM - Planning: .agent/PLAN.md (architecture + LOCKED decisions), AGENTS.md, 6 user stories, 7 phase files (one story / one phase / one Playwright suite each)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Runnable scripts (import_docs, llm_probe) — invoked via `python -m scripts.<name>`."""
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
# Production entrypoint: apply migrations, then serve.
|
||||
set -eu
|
||||
cd /app
|
||||
echo "[entrypoint] applying database migrations..."
|
||||
alembic upgrade head
|
||||
echo "[entrypoint] starting uvicorn on :${PORT:-8000}"
|
||||
exec uvicorn app.main:app --host 0.0.0.0 --port "${PORT:-8000}"
|
||||
@@ -0,0 +1,63 @@
|
||||
"""Probe the self-hosted LLM endpoint (aipi).
|
||||
|
||||
Lists available models and verifies the embedding dimension of the
|
||||
configured ``embed`` model against ``BOR_EMBEDDING_DIM`` (default 768).
|
||||
Run this before the first import if the LLM backend ever changes:
|
||||
|
||||
uv run python -m scripts.llm_probe
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import httpx
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
base_url = os.environ.get("BOR_LLM_BASE_URL", "https://aipi.reeseapps.com/v1").rstrip("/")
|
||||
api_key = (
|
||||
os.environ.get("BOR_LLM_API_KEY")
|
||||
or os.environ.get("AIPI_KEY")
|
||||
or "not-needed"
|
||||
)
|
||||
embed_model = os.environ.get("BOR_LLM_EMBED_MODEL", "embed")
|
||||
chat_model = os.environ.get("BOR_LLM_CHAT_MODEL", "turbo")
|
||||
expected_dim = int(os.environ.get("BOR_EMBEDDING_DIM", "768"))
|
||||
|
||||
headers = {"Authorization": f"Bearer {api_key}"}
|
||||
with httpx.Client(base_url=base_url, headers=headers, timeout=30.0) as client:
|
||||
r = client.get("/models")
|
||||
r.raise_for_status()
|
||||
models = [m["id"] for m in r.json()["data"]]
|
||||
print(f"[probe] base_url : {base_url}")
|
||||
print(f"[probe] models : {', '.join(models)}")
|
||||
|
||||
for needed in (chat_model, embed_model):
|
||||
if needed not in models:
|
||||
print(f"[probe] ERROR: required model '{needed}' not available")
|
||||
return 1
|
||||
|
||||
r = client.post(
|
||||
"/embeddings",
|
||||
json={"model": embed_model, "input": "brain of reese dimension probe"},
|
||||
)
|
||||
r.raise_for_status()
|
||||
dims = sorted({len(d["embedding"]) for d in r.json()["data"]})
|
||||
print(f"[probe] dims({embed_model}): {dims}")
|
||||
|
||||
if dims != [expected_dim]:
|
||||
print(
|
||||
f"[probe] MISMATCH: expected {expected_dim}, got {dims}. "
|
||||
"Update BOR_EMBEDDING_DIM and recreate the chunks table (see README)."
|
||||
)
|
||||
return 1
|
||||
print("[probe] OK — models present, embedding dimension matches configuration.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user