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()