jackailocal / saas /api /main.py
jackboy70's picture
Deploy: accurate lite-builder note
f25362a
Raw
History Blame Contribute Delete
3.48 kB
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."
}