195 lines
6.7 KiB
Python
195 lines
6.7 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
Plugin-Adapter für „Einheiten-Umrechnungen“.
|
||
|
||
Alles Verändernde läuft über tool/conversions.py im Job-Runner. Die Auswahl
|
||
steht vorher in einer Plandatei. Angelegte Umrechnungen sind über die
|
||
Sicherungen wieder löschbar.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import sys
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
from fastapi import FastAPI, HTTPException
|
||
from fastapi.responses import FileResponse
|
||
from pydantic import BaseModel
|
||
|
||
from core.jobs import job_router
|
||
from core.backups import backup_router
|
||
|
||
TOOL = "conversions.py"
|
||
|
||
|
||
class EditRequest(BaseModel):
|
||
accepted: list[dict[str, Any]]
|
||
|
||
|
||
class PrefillRequest(BaseModel):
|
||
plan: str
|
||
model: str | None = None
|
||
|
||
|
||
class ApplyRequest(BaseModel):
|
||
plan: str
|
||
apply: bool = True
|
||
|
||
|
||
class ProbeRequest(BaseModel):
|
||
model: str | None = None
|
||
|
||
|
||
def _num(value: Any) -> float | None:
|
||
try:
|
||
f = float(value)
|
||
return f if f > 0 else None
|
||
except (TypeError, ValueError):
|
||
return None
|
||
|
||
|
||
def create_app(ctx):
|
||
tool = ctx.path("tool", TOOL)
|
||
plans_dir = ctx.data_dir / "plaene"
|
||
plans_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
def tool_env() -> dict:
|
||
env = ctx.settings.tool_env()
|
||
env["DATA_DIR"] = str(ctx.data_dir)
|
||
return env
|
||
|
||
def resolve_plan(name: str) -> Path:
|
||
candidate = plans_dir / Path(name).name
|
||
if candidate.suffix != ".json" or not candidate.is_file():
|
||
raise HTTPException(404, f"Plandatei „{name}“ gibt es nicht.")
|
||
return candidate
|
||
|
||
app = FastAPI(title=ctx.meta.name, docs_url="/api/docs", redoc_url=None)
|
||
|
||
@app.get("/", include_in_schema=False)
|
||
def index():
|
||
return FileResponse(ctx.path("static", "index.html"))
|
||
|
||
@app.get("/api/state")
|
||
def state() -> dict[str, Any]:
|
||
running = [j.info() for j in ctx.jobs.running(ctx.id)]
|
||
return {
|
||
"plans": sorted((p.name for p in plans_dir.glob("*.json")), reverse=True),
|
||
"tandoor": ctx.settings.status()["tandoor"],
|
||
"openai": ctx.settings.status()["openai"],
|
||
"running": running[0] if running else None,
|
||
}
|
||
|
||
@app.get("/api/plans/{name}")
|
||
def read_plan(name: str) -> dict[str, Any]:
|
||
return json.loads(resolve_plan(name).read_text(encoding="utf-8"))
|
||
|
||
@app.post("/api/plans/{name}")
|
||
def save_plan(name: str, request: EditRequest) -> dict[str, Any]:
|
||
"""
|
||
Übernimmt die Auswahl. Eng gehalten: je Kandidat lassen sich nur
|
||
`accept`, die Ausgangsmenge (`base_amount`) und das Gramm-Gewicht
|
||
(`grams`) ändern. Einheit und Zutat stehen fest (immer nach Gramm), die
|
||
Oberfläche kann daran nichts drehen.
|
||
"""
|
||
file = resolve_plan(name)
|
||
plan = json.loads(file.read_text(encoding="utf-8"))
|
||
by_aid = {k.get("aid"): k for k in plan.get("candidates", []) if k.get("aid")}
|
||
|
||
gesetzt = 0
|
||
for wish in request.accepted:
|
||
k = by_aid.get(wish.get("aid"))
|
||
if k is None:
|
||
continue
|
||
if "base_amount" in wish:
|
||
v = _num(wish["base_amount"])
|
||
if v is not None:
|
||
k["base_amount"] = v
|
||
if "grams" in wish:
|
||
v = _num(wish["grams"])
|
||
if v is not None:
|
||
k["grams"] = v
|
||
k["accept"] = bool(wish.get("accept"))
|
||
gesetzt += 1
|
||
|
||
file.write_text(json.dumps(plan, ensure_ascii=False, indent=2), encoding="utf-8")
|
||
offen = sum(1 for k in plan["candidates"]
|
||
if k.get("accept") and k.get("grams") and k.get("base_amount"))
|
||
return {"ok": True, "gesetzt": gesetzt, "umrechnungen": offen}
|
||
|
||
# ---------------------------------------------------------------- Läufe
|
||
|
||
@app.post("/api/run/pruefen")
|
||
async def run_scan() -> dict[str, Any]:
|
||
if ctx.jobs.running(ctx.id):
|
||
raise HTTPException(409, "Es läuft bereits etwas.")
|
||
if not ctx.settings.status()["tandoor"]:
|
||
raise HTTPException(400, "Tandoor-URL und Token fehlen.")
|
||
job = await ctx.jobs.start(
|
||
plugin=ctx.id, label="Umrechnungen prüfen",
|
||
argv=[sys.executable, str(tool), "pruefen"],
|
||
cwd=ctx.data_dir, env=tool_env(),
|
||
)
|
||
return job.info()
|
||
|
||
@app.post("/api/run/vorschlagen")
|
||
async def run_prefill(request: PrefillRequest) -> dict[str, Any]:
|
||
if ctx.jobs.running(ctx.id):
|
||
raise HTTPException(409, "Es läuft bereits etwas.")
|
||
if not ctx.settings.status()["openai"]:
|
||
raise HTTPException(400, "OpenAI-Key fehlt – siehe Einstellungen.")
|
||
file = resolve_plan(request.plan)
|
||
argv = [sys.executable, str(tool), "vorschlagen", "--plan", str(file)]
|
||
if request.model:
|
||
argv += ["--model", request.model]
|
||
job = await ctx.jobs.start(
|
||
plugin=ctx.id, label=f"KI schlägt vor: {file.stem}",
|
||
argv=argv, cwd=ctx.data_dir, env=tool_env(),
|
||
)
|
||
return job.info()
|
||
|
||
@app.post("/api/run/anwenden")
|
||
async def run_apply(request: ApplyRequest) -> dict[str, Any]:
|
||
if ctx.jobs.running(ctx.id):
|
||
raise HTTPException(409, "Es läuft bereits etwas.")
|
||
if not ctx.settings.status()["tandoor"]:
|
||
raise HTTPException(400, "Tandoor-URL und Token fehlen.")
|
||
file = resolve_plan(request.plan)
|
||
argv = [sys.executable, str(tool), "anwenden", "--plan", str(file),
|
||
"--continue-on-error"]
|
||
if request.apply:
|
||
argv.append("--apply")
|
||
job = await ctx.jobs.start(
|
||
plugin=ctx.id,
|
||
label=("Umrechnungen anlegen" if request.apply else "Trockenübung")
|
||
+ f": {file.stem}",
|
||
argv=argv, cwd=ctx.data_dir, env=tool_env(),
|
||
)
|
||
return job.info()
|
||
|
||
@app.post("/api/run/probe")
|
||
async def run_probe(request: ProbeRequest) -> dict[str, Any]:
|
||
if ctx.jobs.running(ctx.id):
|
||
raise HTTPException(409, "Es läuft bereits etwas.")
|
||
if not ctx.settings.status()["openai"]:
|
||
raise HTTPException(400, "OpenAI-Key fehlt – siehe Einstellungen.")
|
||
argv = [sys.executable, str(tool), "probe"]
|
||
if request.model:
|
||
argv += ["--model", request.model]
|
||
job = await ctx.jobs.start(
|
||
plugin=ctx.id, label="OpenAI-Verbindung testen",
|
||
argv=argv, cwd=ctx.data_dir, env=tool_env(),
|
||
)
|
||
return job.info()
|
||
|
||
def restore_argv(run: str, apply: bool, force: bool) -> list[str]:
|
||
argv = [sys.executable, str(tool), "zurueck", "--lauf", run]
|
||
if apply:
|
||
argv.append("--apply")
|
||
return argv
|
||
|
||
app.include_router(backup_router(ctx, "laeufe", restore_argv))
|
||
app.include_router(job_router(ctx.jobs, ctx.id))
|
||
return app
|