checkpoint: matrix-agent-validated (2026-04-25)
Architectural snapshot of the lakehouse codebase at the point where the
full matrix-driven agent loop with Mem0 versioning + deletion was
validated end-to-end.
WHAT THIS REPO IS
A clean single-commit snapshot of the lakehouse code. Heavy test data
(.parquet datasets, vector indexes) excluded — see REPLICATION.md for
regen path. Full lakehouse history at git.agentview.dev/profit/lakehouse.
WHAT WAS PROVEN
- Vector retrieval across multi-corpora matrix (chicago_permits + entity
briefs + sec_tickers + distilled procedural + llm_team runs)
- Observer hand-review (cloud + heuristic fallback) gating each candidate
- Local-model agent loop (qwen3.5:latest) with tool use + scratchpad
- Playbook seal on success → next-iter retrieval surfaces it as preamble
- Mem0 versioning + deletion in pathway_memory:
* UPSERT: ADD on new workflow, UPDATE bumps replay_count on identical
* REVISE: chains versions, parent.superseded_at + superseded_by stamped
* RETIRE: marks specific trace retired with reason, excluded from retrieval
* HISTORY: walks chain root→tip, cycle-safe
KEY DIRECTORIES
- crates/vectord/src/pathway_memory.rs — Mem0 ops live here
- crates/vectord/src/playbook_memory.rs — original Mem0 reference
- tests/agent_test/ — local-model agent harness + PRD + session archives
- scripts/dump_raw_corpus.sh — MinIO bucket dump (raw test corpus)
- scripts/vectorize_raw_corpus.ts — corpus → vector indexes
- scripts/analyze_chicago_contracts.ts — real inference pipeline
- scripts/seal_agent_playbook.ts — Mem0 upsert from agent traces
Replication: see REPLICATION.md for Debian 13 clean install + cloud-only
adaptation (no local Ollama).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
FROM python:3.13-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY pyproject.toml .
|
||||
RUN pip install --no-cache-dir .
|
||||
|
||||
COPY sidecar/ sidecar/
|
||||
|
||||
ENV OLLAMA_URL=http://host.docker.internal:11434
|
||||
ENV EMBED_MODEL=nomic-embed-text
|
||||
ENV GEN_MODEL=qwen2.5
|
||||
ENV RERANK_MODEL=qwen2.5
|
||||
|
||||
EXPOSE 3200
|
||||
|
||||
CMD ["uvicorn", "sidecar.main:app", "--host", "0.0.0.0", "--port", "3200"]
|
||||
@@ -0,0 +1,14 @@
|
||||
[project]
|
||||
name = "lakehouse-sidecar"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"fastapi>=0.115",
|
||||
"uvicorn>=0.34",
|
||||
"httpx>=0.28",
|
||||
"pydantic>=2.0",
|
||||
]
|
||||
|
||||
[tool.uvicorn]
|
||||
host = "0.0.0.0"
|
||||
port = 3200
|
||||
@@ -0,0 +1,137 @@
|
||||
"""Admin endpoints: model lifecycle + VRAM introspection.
|
||||
|
||||
Phase 17 / Phase C — the VRAM-aware profile swap story. Ollama loads
|
||||
models lazily and unloads after a TTL (default 5min). For predictable
|
||||
swaps between model profiles, we need explicit control:
|
||||
|
||||
- unload: send keep_alive=0 to force immediate unload
|
||||
- preload: send keep_alive=5m with empty prompt to proactively load
|
||||
- ps: query Ollama's /api/ps to see what's currently loaded
|
||||
|
||||
All three are thin wrappers over Ollama's own API. No state held here.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import shutil
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .ollama import client
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class ModelRequest(BaseModel):
|
||||
model: str
|
||||
|
||||
|
||||
@router.post("/unload")
|
||||
async def unload(req: ModelRequest):
|
||||
"""Force Ollama to unload a model immediately (keep_alive=0 trick)."""
|
||||
async with client() as c:
|
||||
resp = await c.post(
|
||||
"/api/generate",
|
||||
json={"model": req.model, "prompt": "", "keep_alive": 0, "stream": False},
|
||||
)
|
||||
# Ollama returns 200 even on unload; anything else is abnormal.
|
||||
if resp.status_code not in (200,):
|
||||
raise HTTPException(502, f"Ollama unload error: {resp.text}")
|
||||
return {"unloaded": req.model}
|
||||
|
||||
|
||||
@router.post("/preload")
|
||||
async def preload(req: ModelRequest):
|
||||
"""Force Ollama to load a model into VRAM and keep it there."""
|
||||
async with client() as c:
|
||||
resp = await c.post(
|
||||
"/api/generate",
|
||||
json={
|
||||
"model": req.model,
|
||||
# "" can confuse some models; a single token is safer and still ~instant.
|
||||
"prompt": " ",
|
||||
"keep_alive": "5m",
|
||||
"stream": False,
|
||||
"options": {"num_predict": 1},
|
||||
},
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
raise HTTPException(502, f"Ollama preload error: {resp.text}")
|
||||
data = resp.json()
|
||||
return {
|
||||
"preloaded": req.model,
|
||||
"load_duration_ns": data.get("load_duration"),
|
||||
"total_duration_ns": data.get("total_duration"),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/ps")
|
||||
async def list_loaded():
|
||||
"""What models does Ollama currently have in VRAM?"""
|
||||
async with client() as c:
|
||||
resp = await c.get("/api/ps")
|
||||
if resp.status_code != 200:
|
||||
raise HTTPException(502, f"Ollama ps error: {resp.text}")
|
||||
data = resp.json()
|
||||
# Flatten Ollama's fields into something small + useful.
|
||||
models = [
|
||||
{
|
||||
"name": m.get("name"),
|
||||
"size_bytes": m.get("size"),
|
||||
"size_vram_bytes": m.get("size_vram"),
|
||||
"expires_at": m.get("expires_at"),
|
||||
}
|
||||
for m in data.get("models", [])
|
||||
]
|
||||
return {"models": models}
|
||||
|
||||
|
||||
@router.get("/vram")
|
||||
async def vram_summary():
|
||||
"""Combined: nvidia-smi VRAM + Ollama loaded models.
|
||||
|
||||
Shells out to nvidia-smi; if it's not on PATH, returns just the
|
||||
Ollama view. Intentionally async-via-to_thread so the blocking
|
||||
subprocess doesn't stall the event loop.
|
||||
"""
|
||||
gpu = None
|
||||
if shutil.which("nvidia-smi"):
|
||||
gpu = await asyncio.to_thread(_nvidia_smi_snapshot)
|
||||
|
||||
async with client() as c:
|
||||
resp = await c.get("/api/ps")
|
||||
loaded = resp.json().get("models", []) if resp.status_code == 200 else []
|
||||
|
||||
return {
|
||||
"gpu": gpu,
|
||||
"ollama_loaded": [
|
||||
{
|
||||
"name": m.get("name"),
|
||||
"size_vram_mib": (m.get("size_vram", 0) or 0) // (1024 * 1024),
|
||||
"expires_at": m.get("expires_at"),
|
||||
}
|
||||
for m in loaded
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _nvidia_smi_snapshot():
|
||||
"""One-shot nvidia-smi poll. Returns None on failure."""
|
||||
import subprocess
|
||||
try:
|
||||
out = subprocess.check_output(
|
||||
[
|
||||
"nvidia-smi",
|
||||
"--query-gpu=memory.used,memory.total,utilization.gpu,name",
|
||||
"--format=csv,noheader,nounits",
|
||||
],
|
||||
timeout=2,
|
||||
).decode().strip()
|
||||
used_mib, total_mib, util_pct, name = [s.strip() for s in out.split(",")]
|
||||
return {
|
||||
"name": name,
|
||||
"used_mib": int(used_mib),
|
||||
"total_mib": int(total_mib),
|
||||
"utilization_pct": int(util_pct),
|
||||
}
|
||||
except Exception:
|
||||
return None
|
||||
@@ -0,0 +1,44 @@
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .ollama import client
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
EMBED_MODEL = os.environ.get("EMBED_MODEL", "nomic-embed-text")
|
||||
|
||||
|
||||
class EmbedRequest(BaseModel):
|
||||
texts: list[str]
|
||||
model: str | None = None
|
||||
|
||||
|
||||
class EmbedResponse(BaseModel):
|
||||
embeddings: list[list[float]]
|
||||
model: str
|
||||
dimensions: int
|
||||
|
||||
|
||||
@router.post("", response_model=EmbedResponse)
|
||||
async def embed(req: EmbedRequest):
|
||||
model = req.model or EMBED_MODEL
|
||||
embeddings = []
|
||||
|
||||
async with client() as c:
|
||||
for text in req.texts:
|
||||
resp = await c.post("/api/embed", json={"model": model, "input": text})
|
||||
if resp.status_code != 200:
|
||||
raise HTTPException(502, f"Ollama error: {resp.text}")
|
||||
data = resp.json()
|
||||
embeddings.extend(data.get("embeddings", []))
|
||||
|
||||
if not embeddings:
|
||||
raise HTTPException(502, "No embeddings returned")
|
||||
|
||||
return EmbedResponse(
|
||||
embeddings=embeddings,
|
||||
model=model,
|
||||
dimensions=len(embeddings[0]),
|
||||
)
|
||||
@@ -0,0 +1,61 @@
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .ollama import client
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
GEN_MODEL = os.environ.get("GEN_MODEL", "qwen2.5")
|
||||
|
||||
|
||||
class GenerateRequest(BaseModel):
|
||||
prompt: str
|
||||
model: str | None = None
|
||||
system: str | None = None
|
||||
temperature: float = 0.7
|
||||
max_tokens: int = 2048
|
||||
# think=false disables hidden reasoning blocks on thinking models
|
||||
# (qwen3, qwen3.5, gpt-oss). Required for hot-path JSON emitters
|
||||
# that need the whole token budget for the visible response.
|
||||
think: bool | None = None
|
||||
|
||||
|
||||
class GenerateResponse(BaseModel):
|
||||
text: str
|
||||
model: str
|
||||
tokens_evaluated: int | None = None
|
||||
tokens_generated: int | None = None
|
||||
|
||||
|
||||
@router.post("", response_model=GenerateResponse)
|
||||
async def generate(req: GenerateRequest):
|
||||
model = req.model or GEN_MODEL
|
||||
|
||||
payload = {
|
||||
"model": model,
|
||||
"prompt": req.prompt,
|
||||
"stream": False,
|
||||
"options": {
|
||||
"temperature": req.temperature,
|
||||
"num_predict": req.max_tokens,
|
||||
},
|
||||
}
|
||||
if req.system:
|
||||
payload["system"] = req.system
|
||||
if req.think is not None:
|
||||
payload["think"] = req.think
|
||||
|
||||
async with client() as c:
|
||||
resp = await c.post("/api/generate", json=payload)
|
||||
if resp.status_code != 200:
|
||||
raise HTTPException(502, f"Ollama error: {resp.text}")
|
||||
data = resp.json()
|
||||
|
||||
return GenerateResponse(
|
||||
text=data.get("response", ""),
|
||||
model=model,
|
||||
tokens_evaluated=data.get("prompt_eval_count"),
|
||||
tokens_generated=data.get("eval_count"),
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
import os
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
from .admin import router as admin_router
|
||||
from .embed import router as embed_router
|
||||
from .generate import router as generate_router
|
||||
from .rerank import router as rerank_router
|
||||
|
||||
app = FastAPI(title="Lakehouse AI Sidecar")
|
||||
|
||||
app.include_router(embed_router, prefix="/embed", tags=["embed"])
|
||||
app.include_router(generate_router, prefix="/generate", tags=["generate"])
|
||||
app.include_router(rerank_router, prefix="/rerank", tags=["rerank"])
|
||||
app.include_router(admin_router, prefix="/admin", tags=["admin"])
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {
|
||||
"status": "ok",
|
||||
"ollama_url": os.environ.get("OLLAMA_URL", "http://localhost:11434"),
|
||||
"embed_model": os.environ.get("EMBED_MODEL", "nomic-embed-text"),
|
||||
"gen_model": os.environ.get("GEN_MODEL", "qwen2.5"),
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
"""Shared Ollama HTTP client."""
|
||||
|
||||
import os
|
||||
|
||||
import httpx
|
||||
|
||||
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434")
|
||||
TIMEOUT = float(os.environ.get("OLLAMA_TIMEOUT", "120"))
|
||||
|
||||
|
||||
def client() -> httpx.AsyncClient:
|
||||
return httpx.AsyncClient(base_url=OLLAMA_URL, timeout=TIMEOUT)
|
||||
@@ -0,0 +1,70 @@
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .ollama import client
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
RERANK_MODEL = os.environ.get("RERANK_MODEL", "qwen2.5")
|
||||
|
||||
|
||||
class RerankRequest(BaseModel):
|
||||
query: str
|
||||
documents: list[str]
|
||||
model: str | None = None
|
||||
top_k: int | None = None
|
||||
|
||||
|
||||
class ScoredDocument(BaseModel):
|
||||
index: int
|
||||
text: str
|
||||
score: float
|
||||
|
||||
|
||||
class RerankResponse(BaseModel):
|
||||
results: list[ScoredDocument]
|
||||
model: str
|
||||
|
||||
|
||||
@router.post("", response_model=RerankResponse)
|
||||
async def rerank(req: RerankRequest):
|
||||
"""Cross-encoder reranking via Ollama generate.
|
||||
|
||||
Scores each document against the query by asking the model to rate relevance 0-10,
|
||||
then sorts by score descending.
|
||||
"""
|
||||
model = req.model or RERANK_MODEL
|
||||
scored = []
|
||||
|
||||
async with client() as c:
|
||||
for i, doc in enumerate(req.documents):
|
||||
prompt = (
|
||||
f"Rate the relevance of the following document to the query on a scale of 0 to 10. "
|
||||
f"Respond with ONLY a number.\n\n"
|
||||
f"Query: {req.query}\n\n"
|
||||
f"Document: {doc}\n\n"
|
||||
f"Score:"
|
||||
)
|
||||
resp = await c.post(
|
||||
"/api/generate",
|
||||
json={"model": model, "prompt": prompt, "stream": False, "options": {"temperature": 0.0, "num_predict": 8}},
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
raise HTTPException(502, f"Ollama error: {resp.text}")
|
||||
|
||||
text = resp.json().get("response", "").strip()
|
||||
try:
|
||||
score = float(text.split()[0])
|
||||
score = max(0.0, min(10.0, score))
|
||||
except (ValueError, IndexError):
|
||||
score = 0.0
|
||||
|
||||
scored.append(ScoredDocument(index=i, text=doc, score=score))
|
||||
|
||||
scored.sort(key=lambda x: x.score, reverse=True)
|
||||
if req.top_k:
|
||||
scored = scored[: req.top_k]
|
||||
|
||||
return RerankResponse(results=scored, model=model)
|
||||
Reference in New Issue
Block a user