# -*- 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