Files
ely-eye/backend/src/ely_eye/cli.py
T
ZacharyZhang-NY 07825913fe Make the store movable and the CI gate actually runnable
CI was a workflow that could not run its own tests: pytest is not a
dependency of anything, so a fresh clone fails with "No module named
pytest". Test tooling now lives in the backend's dev extra, so CI and a
new contributor install the same tools, and ruff is pinned there instead
of drifting as a bare version in a workflow step.

backup, restore and doctor move the store. Backup uses SQLite's own
online API so a running server is safe, and the manifest records the
schema version, row counts, sha256 and size read back from the copy.
Restore verifies the digest, integrity_check, required tables and schema
age before it touches the live file, and keeps what it replaces as
.before-restore.

Doctor separates broken from degraded. On the 906 MB baseline store
(165,973 atoms) it reports no problems and one true warning: 21,362
picture atoms with no CLIP snapshot configured, so those atoms are
findable only by the text already on them. The first version called that
store healthy because it only complained about paths that were set and
missing, never about a sense the store needs and never had.

AGPL-3.0-only, because PyMuPDF is dual-licensed AGPL-or-commercial and
the repository is public. A permissive license needs the Artifex
commercial license bought first; the README says so rather than hiding it.

Gates: 253 tests pass; ruff clean repo-wide (17 errors at HEAD); mypy
unchanged from HEAD on touched files; pyright 0 errors on the new files;
backup 4.0s and restore 2.8s on real data with counts verified equal.
2026-10-08 02:32:52 -04:00

491 lines
16 KiB
Python

from __future__ import annotations
from datetime import datetime
from pathlib import Path
import typer
import uvicorn
from .adapters import plan as adapter_plan
from .api import create_app
from .benchmarks import (
benchmark_prediction_report,
benchmark_registry_report,
benchmark_sample_report,
benchmark_source_report,
run_prediction_benchmark,
)
from .cartridge import CartridgeService
from .cartridge_assets import CartridgeAssetService
from .cli_common import json_dump
from .cli_maintenance import register as register_maintenance_commands
from .cli_senses import register as register_sense_commands
from .cli_senses import senses_report
from .cli_transactive import register as register_transactive_commands
from .config import get_settings
from .db import Database
from .ingestion import IngestionService
from .memory import MemoryService
from .profiles import runtime_profile_report
from .proofs import HashHopEvaluator, PRDProofSuite
from .proofs.contradictions import CORPUS_NAME, build_contradiction_scenarios, write_corpus
from .proofs.table import render_table
from .research_modules import ResearchModuleService
from .retrieval import RetrievalService
from .runtime import QwenRuntime
from .schemas import MemoryOrigin, SearchFilters, TimePrecision, TrainingAdapterKind
from .training import TrainingService
app = typer.Typer(no_args_is_help=True)
register_transactive_commands(app, lambda: get_settings())
register_sense_commands(app, lambda: get_settings())
register_maintenance_commands(app, lambda: get_settings())
@app.command()
def serve(host: str = "127.0.0.1", port: int = 8765) -> None:
uvicorn.run(create_app(), host=host, port=port)
@app.command()
def status() -> None:
settings = get_settings()
runtime = QwenRuntime(settings).status()
db = Database(settings)
typer.echo(
json_dump(
{
"runtime": runtime.model_dump(mode="json"),
"atoms": db.atom_count(),
"token_equivalent": db.token_equivalent_sum(),
"library_token_equivalent": max(
db.token_equivalent_sum(),
db.cartridge_token_equivalent_sum(),
),
"cartridges": db.cartridge_count(),
"data_home": str(settings.home),
"senses": senses_report(settings, db),
}
)
)
@app.command()
def ingest(path: Path, cartridge_name: str | None = None) -> None:
result = IngestionService(get_settings()).ingest_path(path, cartridge_name)
typer.echo(result.model_dump_json(indent=2))
def _moment(value: str, option: str) -> datetime | None:
if not value:
return None
try:
return datetime.fromisoformat(value)
except ValueError as error:
raise ValueError(f"{option} {value!r} is not an ISO timestamp: {error}") from error
@app.command()
def search(
query: str,
top_k: int = typer.Option(0, "--top-k", help="hits to return; 0 uses the configured default"),
holder: str = typer.Option("", "--holder", help="only this holder's memories may answer"),
scenario: str = typer.Option("", "--scenario"),
source_id: str = typer.Option("", "--source-id"),
since: str = typer.Option("", "--since", help="event time at or after, ISO with offset"),
until: str = typer.Option("", "--until"),
min_trust: float = typer.Option(-1.0, "--min-trust", help="drop memories whose trust falls below this"),
cursor: str = typer.Option("", "--cursor", help="next_cursor from the previous page"),
image: str = typer.Option("", "--image", help="a picture to search with, instead of only words"),
) -> None:
"""Rank the memories that may answer, and say so when none may."""
settings = get_settings()
result = RetrievalService(settings).search(
query,
top_k or None,
filters=SearchFilters(
holder=holder or None,
scenario=scenario or None,
source_id=source_id or None,
since=_moment(since, "--since"),
until=_moment(until, "--until"),
min_trust=None if min_trust < 0 else min_trust,
),
cursor=cursor or None,
image=Path(image) if image else None,
)
typer.echo(result.model_dump_json(indent=2))
@app.command()
def remember(
text: str,
holder: str = typer.Option("", "--holder", help="who owns this memory"),
origin: MemoryOrigin = MemoryOrigin.self_,
scenario: str = "",
event_time: str = typer.Option("", "--event-time", help="when it was true, ISO with offset"),
precision: TimePrecision = TimePrecision.unknown,
retention_until: str = typer.Option("", "--retention-until"),
) -> None:
settings = get_settings()
atom = MemoryService(settings).remember(
text,
holder_id=holder or settings.default_holder,
origin=origin,
scenario=scenario,
event_time=_moment(event_time, "--event-time"),
event_time_precision=precision,
retention_until=_moment(retention_until, "--retention-until"),
)
typer.echo(atom.model_dump_json(indent=2))
@app.command()
def supersede(
atom_id: str,
text: str,
holder: str = typer.Option("", "--holder"),
scenario: str = "",
event_time: str = typer.Option("", "--event-time"),
precision: TimePrecision = TimePrecision.unknown,
) -> None:
settings = get_settings()
atom = MemoryService(settings).supersede(
atom_id,
text=text,
holder_id=holder or settings.default_holder,
scenario=scenario,
event_time=_moment(event_time, "--event-time"),
event_time_precision=precision,
)
typer.echo(atom.model_dump_json(indent=2))
@app.command()
def forget(atom_id: str = "", source_id: str = "", reason: str = "") -> None:
report = MemoryService(get_settings()).forget(
atom_id=atom_id or None,
source_id=source_id or None,
reason=reason,
)
typer.echo(report.model_dump_json(indent=2))
@app.command("list-cartridges")
def list_cartridges() -> None:
for manifest in CartridgeService(get_settings()).list_manifests():
typer.echo(manifest.model_dump_json(indent=2))
@app.command("mounts")
def mounts() -> None:
typer.echo(CartridgeService(get_settings()).mount_state().model_dump_json(indent=2))
@app.command()
def mount(cartridge_id: str) -> None:
"""Add a cartridge to the mounted set; a mounted search answers only from it."""
typer.echo(CartridgeService(get_settings()).mount(cartridge_id).model_dump_json(indent=2))
@app.command()
def switch(cartridge_id: str) -> None:
"""Mount one cartridge and nothing else."""
typer.echo(CartridgeService(get_settings()).switch(cartridge_id).model_dump_json(indent=2))
@app.command()
def unmount(cartridge_id: str) -> None:
typer.echo(CartridgeService(get_settings()).unmount(cartridge_id).model_dump_json(indent=2))
@app.command()
def append(
cartridge_id: str,
atom_id: list[str] = typer.Option(..., "--atom-id"),
) -> None:
"""Add memories to an existing cartridge; the bundle and its DNA follow."""
service = CartridgeService(get_settings())
typer.echo(service.append(cartridge_id, list(atom_id)).model_dump_json(indent=2))
@app.command("hashhop")
def hashhop(kind: str = "visual", hops: int = 2, token_equivalent: int = 262_144) -> None:
evaluator = HashHopEvaluator(get_settings())
if kind == "text":
proof = evaluator.generate_text_proof(hops, token_equivalent)
elif kind == "visual":
proof = evaluator.generate_visual_proof(hops, token_equivalent)
else:
raise typer.BadParameter("kind must be text or visual")
typer.echo(proof.model_dump_json(indent=2))
@app.command("proof-suite")
def proof_suite(cartridge_id: str | None = None) -> None:
report = PRDProofSuite(get_settings()).run(cartridge_id)
typer.echo(report.model_dump_json(indent=2))
@app.command("latest-proof-suite")
def latest_proof_suite(json_output: bool = typer.Option(False, "--json")) -> None:
"""Print what the last proof suite claimed and what it actually proved."""
report = PRDProofSuite(get_settings()).latest()
if report is None:
raise typer.BadParameter(
"no proof suite has been recorded yet; run: ely-eye proof-suite --cartridge-id <id>"
)
if json_output:
typer.echo(report.model_dump_json(indent=2))
else:
typer.echo(render_table(report))
if report.status == "failed":
raise typer.Exit(code=1)
@app.command("runtime-generation-proof")
def runtime_generation_proof() -> None:
check = PRDProofSuite(get_settings()).runtime_generation_proof()
typer.echo(check.model_dump_json(indent=2))
@app.command("visual-contradiction-proof")
def visual_contradiction_proof() -> None:
check = PRDProofSuite(get_settings()).visual_contradiction_proof()
typer.echo(check.model_dump_json(indent=2))
@app.command("contradiction-corpus")
def contradiction_corpus(data_dir: Path | None = None) -> None:
"""Write the corpus the contradiction lens is graded on, so it can be read and replaced."""
settings = get_settings()
target = write_corpus((data_dir or settings.data_dir) / CORPUS_NAME)
typer.echo(json_dump({"corpus": str(target), "scenarios": len(build_contradiction_scenarios())}))
@app.command("finalize-cartridge")
def finalize_cartridge(cartridge_id: str) -> None:
report = CartridgeAssetService(get_settings()).finalize(cartridge_id)
typer.echo(report.model_dump_json(indent=2))
@app.command("refresh-cartridge")
def refresh_cartridge(cartridge_id: str) -> None:
settings = get_settings()
db = Database(settings)
manifest = CartridgeService(settings, db).rebuild(
cartridge_id=cartridge_id,
atoms=db.list_atoms(),
sources=db.list_sources(),
)
typer.echo(manifest.model_dump_json(indent=2))
@app.command("benchmark-registry")
def benchmark_registry() -> None:
typer.echo(benchmark_registry_report().model_dump_json(indent=2))
@app.command("benchmark-sources")
def benchmark_sources() -> None:
typer.echo(benchmark_source_report().model_dump_json(indent=2))
@app.command("benchmark-samples")
def benchmark_samples() -> None:
settings = get_settings()
output_dir = settings.data_dir / "benchmark_samples"
typer.echo(benchmark_sample_report(output_dir).model_dump_json(indent=2))
@app.command("benchmark-predictions")
def benchmark_predictions() -> None:
settings = get_settings()
output_dir = settings.data_dir / "benchmark_predictions"
typer.echo(benchmark_prediction_report(settings, output_dir).model_dump_json(indent=2))
@app.command("run-benchmark")
def run_benchmark(
benchmark: str,
dataset_path: Path,
prediction_field: str = "prediction",
answer_field: str = "answer",
limit: int | None = None,
) -> None:
report = run_prediction_benchmark(
benchmark=benchmark,
dataset_path=dataset_path,
prediction_field=prediction_field,
answer_field=answer_field,
limit=limit,
)
typer.echo(report)
@app.command("runtime-profiles")
def runtime_profiles() -> None:
typer.echo(runtime_profile_report(get_settings()).model_dump_json(indent=2))
@app.command("research-modules")
def research_modules(query: str = "Ely-Eye V-NSA DVKV x-modal KV speculative prefetch evidence graph") -> None:
settings = get_settings()
db = Database(settings)
from .compiler import ContextCompiler
from .schemas import RuntimeProfile
context = ContextCompiler(settings, db).compile(query, RuntimeProfile.research_theater)
report = ResearchModuleService(settings, db).build_report(query, context.hits)
typer.echo(json_dump(report))
@app.command("validate-training-data")
def validate_training_data(path: Path) -> None:
samples = TrainingService(get_settings()).validate_dataset(path)
typer.echo({"samples": len(samples)})
@app.command("build-prd-training-data")
def build_prd_training_data(
prd_path: Path = Path("PRD.md"),
output_path: Path | None = None,
cartridge_id: str | None = None,
max_sections: int = 48,
) -> None:
path = TrainingService(get_settings()).build_prd_dataset(
prd_path=prd_path,
output_path=output_path,
cartridge_id=cartridge_id,
max_sections=max_sections,
)
samples = TrainingService(get_settings()).validate_dataset(path)
typer.echo({"dataset": str(path), "samples": len(samples)})
@app.command("build-visual-training-data")
def build_visual_training_data(
prd_path: Path = Path("PRD.md"),
output_path: Path | None = None,
cartridge_id: str | None = None,
image_path: list[Path] | None = typer.Option(None, "--image-path"),
) -> None:
path = TrainingService(get_settings()).build_visual_dataset(
prd_path=prd_path,
output_path=output_path,
image_paths=image_path,
cartridge_id=cartridge_id,
)
samples = TrainingService(get_settings()).validate_dataset(path)
typer.echo({"dataset": str(path), "samples": len(samples)})
@app.command("build-router-training-data")
def build_router_training_data(
prd_path: Path = Path("PRD.md"),
output_path: Path | None = None,
cartridge_id: str | None = None,
) -> None:
path = TrainingService(get_settings()).build_router_dataset(
prd_path=prd_path,
output_path=output_path,
cartridge_id=cartridge_id,
)
samples = TrainingService(get_settings()).validate_dataset(path)
typer.echo({"dataset": str(path), "samples": len(samples)})
@app.command("build-retrieval-training-data")
def build_retrieval_training_data(
prd_path: Path = Path("PRD.md"),
output_path: Path | None = None,
cartridge_id: str | None = None,
max_sections: int = 32,
) -> None:
path = TrainingService(get_settings()).build_retrieval_dataset(
prd_path=prd_path,
output_path=output_path,
cartridge_id=cartridge_id,
max_sections=max_sections,
)
samples = TrainingService(get_settings()).validate_dataset(path)
typer.echo({"dataset": str(path), "samples": len(samples)})
@app.command("train-lora")
def train_lora(
dataset_path: Path,
output_dir: Path,
max_steps: int = 8,
kind: TrainingAdapterKind = TrainingAdapterKind.hme_core_lora,
model_id: str | None = None,
rank: int = 16,
alpha: int = 32,
max_length: int | None = None,
gradient_accumulation_steps: int | None = None,
learning_rate: float | None = None,
cartridge_id: str | None = None,
) -> None:
manifest = TrainingService(get_settings()).train_lora(
dataset_path=dataset_path,
output_dir=output_dir,
max_steps=max_steps,
kind=kind,
model_id=model_id,
rank=rank,
alpha=alpha,
max_length=max_length,
gradient_accumulation_steps=gradient_accumulation_steps,
learning_rate=learning_rate,
cartridge_id=cartridge_id,
)
if cartridge_id:
CartridgeService(get_settings()).attach_adapter(cartridge_id, output_dir)
typer.echo(manifest.model_dump_json(indent=2))
@app.command("train-retrieval-lora")
def train_retrieval_lora(
dataset_path: Path,
output_dir: Path,
max_steps: int = 8,
rank: int = 8,
alpha: int = 16,
max_length: int | None = None,
learning_rate: float | None = None,
cartridge_id: str | None = None,
) -> None:
manifest = TrainingService(get_settings()).train_retrieval_lora(
dataset_path=dataset_path,
output_dir=output_dir,
max_steps=max_steps,
rank=rank,
alpha=alpha,
max_length=max_length,
learning_rate=learning_rate,
cartridge_id=cartridge_id,
)
if cartridge_id:
CartridgeService(get_settings()).attach_adapter(cartridge_id, output_dir)
typer.echo(manifest.model_dump_json(indent=2))
@app.command("attach-adapter")
def attach_adapter(cartridge_id: str, adapter_dir: Path) -> None:
manifest = CartridgeService(get_settings()).attach_adapter(cartridge_id, adapter_dir)
typer.echo(manifest.model_dump_json(indent=2))
@app.command("adapters")
def adapter_status() -> None:
"""Which adapter the runtime would load, and why each other kind would not."""
settings = get_settings()
report = adapter_plan(settings, Database(settings))
typer.echo(json_dump(report.model_dump(mode="json")))
if __name__ == "__main__":
app()