from __future__ import annotations from fastapi import FastAPI, HTTPException from pydantic import BaseModel, Field from typing import Any, Optional from uuid import uuid4 from datetime import datetime, timezone from pathlib import Path import hashlib app = FastAPI(title="JackAILocal SaaS Builder", version="0.1.0") BUILDS: dict[str, dict[str, Any]] = {} ROOT = Path(__file__).resolve().parents[2] class HardwareProfile(BaseModel): product: str = "JackAILocal" customer_email: str = "" os: str = "windows" cpu_name: Optional[str] = None cpu_threads: int = 4 ram_gb: float = 8 vram_gb: float = 0 gpus: list[Any] = Field(default_factory=list) whichllm: Optional[Any] = None MODEL_RULES = [ {"profile":"lite", "label":"Fast", "min_ram_gb":6, "min_vram_gb":0, "ollama":"qwen3.5:2b"}, {"profile":"balanced", "label":"Balanced", "min_ram_gb":8, "min_vram_gb":0, "ollama":"qwen3.5:4b"}, {"profile":"smart", "label":"Smart", "min_ram_gb":16, "min_vram_gb":6, "ollama":"qwen3.5:9b"}, {"profile":"vision", "label":"Vision", "min_ram_gb":12, "min_vram_gb":4, "ollama":"qwen3-vl:4b"}, ] def plan(profile: HardwareProfile) -> dict[str, Any]: # 3% slack: physical "32 GB" machines report ~31.7 GB usable. allowed = [r for r in MODEL_RULES if profile.ram_gb >= r["min_ram_gb"] * 0.97 and profile.vram_gb >= r["min_vram_gb"] * 0.97] if not allowed: allowed = [MODEL_RULES[0]] default = allowed[-1] return { "default_profile": default["profile"], "profiles": allowed, "ollama_models": sorted(set(r["ollama"] for r in allowed)), "backends": ["ollama"], "notes": ["Models are installed during target build or factory preload, not on every runtime launch."] } def asset_entry(relative: str) -> dict[str, Any] | None: path = ROOT / relative if not path.is_file(): return None return { "path": relative.replace("\\", "/"), "sha256": hashlib.sha256(path.read_bytes()).hexdigest(), "bytes": path.stat().st_size, } @app.post("/api/v1/hardware/submit") def submit(profile: HardwareProfile): build_id = str(uuid4()) build_plan = plan(profile) BUILDS[build_id] = { "build_id": build_id, "product": "JackAILocal", "created_at": datetime.now(timezone.utc).isoformat(), "hardware": profile.model_dump(), "plan": build_plan, } return {"build_id": build_id, "manifest_url": f"/api/v1/builds/{build_id}/manifest", "plan": build_plan} @app.get("/api/v1/builds/{build_id}/manifest") def manifest(build_id: str): b = BUILDS.get(build_id) if not b: raise HTTPException(404, "build not found") plan = b["plan"] assets = [ item for item in [ asset_entry("backends/ollama/windows/ollama.exe"), asset_entry("bin/jackailocald.exe"), asset_entry("config/voice-assets.json"), asset_entry("content/field-manual/cards.json"), ] if item is not None ] return { "product":"JackAILocal", "build_id":build_id, "created_at":b["created_at"], "default_profile":plan["default_profile"], "ollama_models":plan["ollama_models"], "assets": assets, "security":{"signed_updates":True,"local_only":True,"no_admin_normal_use":True}, "notes":"Use the platform builder package to install these assets and selected models into the target." }