Files
ely-eye/backend/src/ely_eye/api.py
T
ZacharyZhang-NY 9145a23f18 Require a token for writes and quarantine what nobody vouches for
Sixteen mutating routes now refuse a caller who cannot present the token the
server started with; queries stay open, because a question is not a write. The
environment variable is ELY_EYE_AUTH_TOKEN, following the repo's env_prefix
convention — the shorter name I reached for first returned 403 where the smoke
expected 401, and the server printed the reason.

Words from a stranger are quarantined on the way in: an HTTP upload is
third_party by definition, and a sentence carrying an instruction-shaped phrase
is quarantined whoever wrote it. A quarantined atom stays findable and stays out
of the compiled context until a second, live, unquarantined source agrees with
its rarest words. Corroboration is computed at read time because it is a
property of the corpus now, not of the moment a memory was written; it is
term-level, and the docstring says so.

forget now erases the source row and the compiled contexts beside the atoms, so
a forgotten memory leaves nothing that can still speak. The semantic cache key
carries a store revision, which fixes a real staleness: a context compiled
before new evidence arrived kept its old quarantine decision forever.

bench.audit --product still pointed at evals.py, which the p1-6 split moved into
backend/src/ely_eye/proofs/. It failed fast, but the anti-self-justification
rule had quietly stopped auditing the product; it now walks the package, and a
test pins that the package exists and holds no re-roll.

Gates: pytest 306 passed; ruff clean; mypy cold cache 24 errors in 9 files, the
recorded baseline, net zero; pyright 0 errors on the 23 backend files and the 2
bench files; go build/vet/test and gofmt clean; npm run build 221.39 kB with
eslint exit 0; bench/filesize_check clean; bench.audit --product clean over 31
harness files; scripts/mcp_write_smoke.sh PASS with the gate live across two
servers and the MCP child.
2026-10-08 23:05:15 -04:00

439 lines
18 KiB
Python

from __future__ import annotations
import json
import shutil
from pathlib import Path
from typing import Annotated
from fastapi import Depends, FastAPI, File, HTTPException, Query, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from . import __version__
from .adapters import plan as adapter_plan
from .api_auth import require_write
from .api_cartridges import register as register_cartridge_routes
from .api_holders import register as register_holder_routes
from .browse import browse, picture
from .cache_fabric import CacheFabric
from .cartridge import CartridgeService
from .compiler import ContextCompiler
from .config import Settings, get_settings
from .db import Database
from .evaluation_schemas import HashHopProof, ProofSuiteReport
from .evidence_graph import EvidenceGraphService
from .ingestion import IngestionService
from .memory import MemoryService
from .profiles import runtime_profile_report
from .proofs import HashHopEvaluator, PRDProofSuite
from .retrieval import RetrievalService
from .runtime import QwenRuntime, RuntimeUnavailableError
from .runtime_schemas import AdapterLoadReport
from .schemas import (
AdapterManifest,
AdapterStatus,
AnswerResponse,
AtomPage,
EvidenceAtom,
ForgetReport,
ForgetRequest,
MemoryOrigin,
Modality,
PathIngestRequest,
RememberRequest,
RuntimeProfile,
RuntimeProfileReport,
SearchRequest,
SearchResult,
SupersedeRequest,
UploadIngestRequest,
)
from .senses import sense_facts
from .status_schemas import MemoryStatus, SystemStatus
from .training_metrics import safetensor_weight_stats
from .verifier import Verifier, VisualContradictionLens
class ChatRequest(BaseModel):
question: str
profile: RuntimeProfile = RuntimeProfile.library_100m
class HashHopRequest(BaseModel):
kind: str = "visual"
hops: int = 2
token_equivalent: int = 262_144
class ProofSuiteRequest(BaseModel):
cartridge_id: str | None = None
def settings_dep() -> Settings:
return get_settings()
def create_app() -> FastAPI:
app = FastAPI(title="Ely-Eye", version=__version__)
app.state.write_settings = settings_dep
app.add_middleware(
CORSMiddleware,
allow_origins=settings_dep().allowed_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/api/status", response_model=SystemStatus)
def status(settings: Annotated[Settings, Depends(settings_dep)]) -> SystemStatus:
db = Database(settings)
cache = CacheFabric(settings, db)
runtime = QwenRuntime(settings).status()
layers = cache.statuses()
active_token_equivalent = db.token_equivalent_sum()
memory = MemoryStatus(
active_atoms=db.atom_count(),
active_token_equivalent=active_token_equivalent,
cartridges=db.cartridge_count(),
library_token_equivalent=max(active_token_equivalent, db.cartridge_token_equivalent_sum()),
layers=layers,
)
return SystemStatus(
version=__version__,
runtime=runtime,
memory=memory,
senses=sense_facts(settings, db),
data_home=str(settings.home),
profiles={
"Live Demo": {"context_tokens": settings.live_context_tokens, "purpose": "stable 262K workflow"},
"Extreme Context": {"context_tokens": settings.extreme_context_tokens, "purpose": "YaRN 1.01M workflow"},
"100M Library": {"context_tokens": settings.library_target_tokens, "purpose": "Context Cartridge memory"},
"Research Theater": {"context_tokens": settings.live_context_tokens, "purpose": "V-NSA, DVKV, TTT-VL proofs"},
},
)
@app.post("/api/ingest/path")
def ingest_path(
request: PathIngestRequest,
settings: Annotated[Settings, Depends(settings_dep)],
_: Annotated[None, Depends(require_write)],
):
try:
return IngestionService(settings).ingest_path(request.path, request.cartridge_name)
except Exception as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@app.post("/api/ingest/upload")
def ingest_upload(
metadata: Annotated[UploadIngestRequest, Depends()],
settings: Annotated[Settings, Depends(settings_dep)],
_: Annotated[None, Depends(require_write)],
files: Annotated[list[UploadFile], File()],
):
upload_root = settings.data_dir / "uploads"
upload_root.mkdir(parents=True, exist_ok=True)
saved_paths: list[Path] = []
total_size = 0
for uploaded in files:
target = upload_root / sanitize_filename(uploaded.filename or "upload.bin")
with target.open("wb") as handle:
shutil.copyfileobj(uploaded.file, handle)
total_size += target.stat().st_size
if total_size > settings.upload_max_bytes:
raise HTTPException(status_code=413, detail="Upload exceeds configured maximum size.")
saved_paths.append(target)
service = IngestionService(settings)
aggregate = None
for path in saved_paths:
result = service.ingest_path(path, None, origin=MemoryOrigin.third_party)
if aggregate is None:
aggregate = result
else:
aggregate.source_count += result.source_count
aggregate.atom_count += result.atom_count
aggregate.token_equivalent += result.token_equivalent
aggregate.sources.extend(result.sources)
if aggregate is None:
raise HTTPException(status_code=400, detail="No files were uploaded.")
if metadata.cartridge_name:
atoms = Database(settings).list_atoms()
sources = aggregate.sources
cartridge = CartridgeService(settings).materialize(metadata.cartridge_name, atoms, sources)
aggregate.cartridge_id = cartridge.cartridge_id
return aggregate
@app.post("/api/chat", response_model=AnswerResponse)
def chat(
request: ChatRequest,
settings: Annotated[Settings, Depends(settings_dep)],
_: Annotated[None, Depends(require_write)],
) -> AnswerResponse:
db = Database(settings)
compiler = ContextCompiler(settings, db)
context = compiler.compile(request.question, request.profile)
if not context.hits:
raise HTTPException(status_code=404, detail="No evidence atoms are available for this question.")
runtime = QwenRuntime(settings)
try:
generation = runtime.generate(request.question, context)
except RuntimeUnavailableError as exc:
raise HTTPException(status_code=503, detail=str(exc)) from exc
answer, citations, report = Verifier().verify_answer(generation.text, context)
contradiction_notes = VisualContradictionLens().detect(context)
report.contradiction_notes.extend(note for note in contradiction_notes if note not in report.contradiction_notes)
EvidenceGraphService(settings, db).record_answer(request.question, answer, report, context)
return AnswerResponse(
answer=answer,
citations=citations,
context=context,
verifier=report,
runtime_backend=generation.backend,
)
@app.post("/api/search", response_model=SearchResult)
def search(request: SearchRequest, settings: Annotated[Settings, Depends(settings_dep)]) -> SearchResult:
try:
return RetrievalService(settings).search(
request.query,
request.top_k,
filters=request.filters,
cursor=request.cursor,
image=request.image,
)
except ValueError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
@app.post("/api/memory", response_model=EvidenceAtom)
def remember(
request: RememberRequest,
settings: Annotated[Settings, Depends(settings_dep)],
_: Annotated[None, Depends(require_write)],
) -> EvidenceAtom:
try:
return MemoryService(settings).remember(
request.text,
holder_id=settings.default_holder,
origin=MemoryOrigin.self_,
scenario=request.scenario,
event_time=request.event_time,
event_time_precision=request.event_time_precision,
event_time_source=request.event_time_source,
retention_until=request.retention_until,
)
except ValueError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
@app.post("/api/memory/supersede", response_model=EvidenceAtom)
def supersede(
request: SupersedeRequest,
settings: Annotated[Settings, Depends(settings_dep)],
_: Annotated[None, Depends(require_write)],
) -> EvidenceAtom:
try:
return MemoryService(settings).supersede(
request.atom_id,
text=request.text,
holder_id=settings.default_holder,
scenario=request.scenario,
event_time=request.event_time,
event_time_precision=request.event_time_precision,
event_time_source=request.event_time_source,
)
except ValueError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
@app.post("/api/memory/forget", response_model=ForgetReport)
def forget(
request: ForgetRequest,
settings: Annotated[Settings, Depends(settings_dep)],
_: Annotated[None, Depends(require_write)],
) -> ForgetReport:
try:
return MemoryService(settings).forget(
atom_id=request.atom_id, source_id=request.source_id, reason=request.reason
)
except ValueError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
register_cartridge_routes(app, settings_dep)
register_holder_routes(app, settings_dep)
@app.get("/api/adapters", response_model=list[AdapterStatus])
def adapters(settings: Annotated[Settings, Depends(settings_dep)]) -> list[AdapterStatus]:
return list_adapter_statuses(settings)
@app.get("/api/atoms", response_model=AtomPage)
def atoms(
settings: Annotated[Settings, Depends(settings_dep)],
limit: Annotated[int, Query(ge=1, le=200)] = 50,
cursor: str | None = None,
modality: Modality | None = None,
source_id: str | None = None,
holder: str | None = None,
) -> AtomPage:
try:
return browse(
settings,
limit=limit,
cursor=cursor,
modality=modality,
source_id=source_id,
holder=holder,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@app.get("/api/atoms/{atom_id}/image")
def atom_image(atom_id: str, settings: Annotated[Settings, Depends(settings_dep)]) -> FileResponse:
found = picture(settings, atom_id)
if found is None:
raise HTTPException(status_code=404, detail=f"{atom_id} has no picture to show")
return FileResponse(found.path, media_type=found.mime)
@app.get("/api/runtime/profiles", response_model=RuntimeProfileReport)
def runtime_profiles(
settings: Annotated[Settings, Depends(settings_dep)],
) -> RuntimeProfileReport:
return runtime_profile_report(settings)
@app.get("/api/runtime/adapters", response_model=AdapterLoadReport)
def runtime_adapters(settings: Annotated[Settings, Depends(settings_dep)]) -> AdapterLoadReport:
return adapter_plan(settings, Database(settings))
@app.post("/api/hashhop", response_model=HashHopProof)
def hashhop(
request: HashHopRequest,
settings: Annotated[Settings, Depends(settings_dep)],
_: Annotated[None, Depends(require_write)],
) -> HashHopProof:
evaluator = HashHopEvaluator(settings)
if request.kind == "text":
return evaluator.generate_text_proof(request.hops, request.token_equivalent)
if request.kind == "visual":
return evaluator.generate_visual_proof(request.hops, request.token_equivalent)
raise HTTPException(status_code=400, detail="kind must be text or visual")
@app.post("/api/proof-suite", response_model=ProofSuiteReport)
def proof_suite(
request: ProofSuiteRequest,
settings: Annotated[Settings, Depends(settings_dep)],
_: Annotated[None, Depends(require_write)],
) -> ProofSuiteReport:
try:
return PRDProofSuite(settings).run(request.cartridge_id)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@app.get("/api/proof-suite/latest", response_model=ProofSuiteReport)
def latest_proof_suite(settings: Annotated[Settings, Depends(settings_dep)]) -> ProofSuiteReport:
report = PRDProofSuite(settings).latest()
if report is None:
raise HTTPException(status_code=404, detail="No PRD proof suite has been generated.")
return report
mounted_ui = get_settings().ui_dir
if mounted_ui is not None:
if not mounted_ui.is_dir():
raise SystemExit(
f"ui_dir {mounted_ui} is set but holds no built interface; "
"run `npm run build` in frontend/ or unset ELY_EYE_UI_DIR"
)
app.mount("/", StaticFiles(directory=mounted_ui, html=True), name="ui")
return app
def sanitize_filename(name: str) -> str:
safe = "".join(char if char.isalnum() or char in {".", "-", "_"} else "_" for char in name)
return safe.strip("._") or "upload.bin"
def list_adapter_statuses(settings: Settings) -> list[AdapterStatus]:
cartridge_bindings = {
manifest.cartridge_id: manifest.artifacts
for manifest in CartridgeService(settings).list_manifests()
}
statuses: list[AdapterStatus] = []
for manifest_path in sorted(settings.adapters_dir.glob("*/adapter_manifest.json")):
root = manifest_path.parent
manifest = AdapterManifest.model_validate_json(manifest_path.read_text(encoding="utf-8"))
weights = root / "adapter_model.safetensors"
weight_bytes = weights.stat().st_size if weights.exists() else 0
weight_stats = safetensor_weight_stats(weights)
artifacts = cartridge_bindings.get(manifest.cartridge_id or "", {})
cartridge_bound = f"adapter_{manifest.kind.value}" in artifacts
statuses.append(
AdapterStatus(
name=root.name,
kind=manifest.kind,
proof_version=manifest.proof_version,
training_method=manifest.training_method,
precision=manifest.precision,
framework=manifest.framework,
base_model=manifest.base_model,
path=str(root),
adapter_id=manifest.adapter_id,
sample_count=manifest.sample_count,
max_steps=manifest.max_steps,
trainable_params=manifest.trainable_params,
final_loss=manifest.final_loss,
sha256=manifest.sha256,
weight_bytes=weight_bytes,
safetensor_keys=int(weight_stats["tensor_count"]),
cartridge_id=manifest.cartridge_id,
cartridge_bound=cartridge_bound,
training_trace_path=manifest.training_trace_path,
training_trace_sha256=manifest.training_trace_sha256,
training_trace_steps=count_training_steps(root / "training_trace.jsonl"),
training_proof_path=manifest.training_proof_path,
training_proof_sha256=manifest.training_proof_sha256,
optimizer=manifest.optimizer,
optimizer_family=manifest.optimizer_family,
gradient_checkpointing=manifest.gradient_checkpointing,
gradient_checkpointing_mode=manifest.gradient_checkpointing_mode,
autocast_dtype=manifest.autocast_dtype,
unsloth_version=manifest.unsloth_version,
triton_version=manifest.triton_version,
xformers_version=manifest.xformers_version,
unsloth_model_type=manifest.unsloth_model_type,
optimizer_update_count=manifest.optimizer_update_count,
max_optimizer_update_l2=manifest.max_optimizer_update_l2,
loss_delta=manifest.loss_delta,
training_summary_path=manifest.training_summary_path,
training_wall_seconds=manifest.training_wall_seconds,
training_device=manifest.training_device,
bf16=manifest.bf16,
torch_version=manifest.torch_version,
cuda_version=manifest.cuda_version,
adapter_weight_sha256=str(weight_stats["sha256"] or manifest.adapter_weight_sha256 or ""),
adapter_tensor_count=int(weight_stats["tensor_count"]),
adapter_lora_tensor_count=int(weight_stats["lora_tensor_count"]),
adapter_total_elements=int(weight_stats["total_elements"]),
adapter_nonzero_elements=int(weight_stats["nonzero_elements"]),
adapter_max_abs=weight_stats["max_abs"],
adapter_weights_finite=bool(weight_stats["all_finite"]),
)
)
return statuses
def count_training_steps(path: Path) -> int:
if not path.exists():
return 0
steps = 0
for line in path.read_text(encoding="utf-8").splitlines():
if not line.strip():
continue
try:
if json.loads(line).get("event") == "step":
steps += 1
except json.JSONDecodeError:
return 0
return steps
app = create_app()