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.
439 lines
18 KiB
Python
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()
|