Files
boehmitools-main/plugins/tandoor-categories/backend.py
T
2026-07-24 21:37:03 +02:00

195 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
Plugin-Adapter für „Zutaten-Kategorien“.
Alles Verändernde läuft über tool/categories.py im Job-Runner, nie direkt aus einer
Anfrage. Die Auswahl steht vorher in einer Plandatei, die sich ansehen, ändern
und im Zweifel einfach nicht anwenden lässt.
"""
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 = "categories.py"
class EditRequest(BaseModel):
accepted: list[dict[str, Any]]
class PlanRef(BaseModel):
plan: str
class ApplyRequest(BaseModel):
plan: str
apply: bool = True
class PrefillRequest(BaseModel):
plan: str
model: str | None = None
alle: bool = False
class ProbeRequest(BaseModel):
model: str | None = 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 aus der Oberfläche. Eng gehalten: je Zutat lassen
sich nur `accept` und `proposed_id` ändern, und `proposed_id` muss eine
der im Plan bekannten Kategorien sein (oder null). So kann die Oberfläche
keine fremde ID unterschieben.
"""
file = resolve_plan(name)
plan = json.loads(file.read_text(encoding="utf-8"))
valid = {c["id"]: c["name"] for c in plan.get("categories", [])}
by_aid = {f.get("aid"): f for f in plan.get("foods", []) if f.get("aid")}
gesetzt = 0
for wish in request.accepted:
food = by_aid.get(wish.get("aid"))
if food is None:
continue
if "proposed_id" in wish:
pid = wish.get("proposed_id")
if pid is None:
food["proposed_id"] = None
food["proposed_name"] = ""
elif pid in valid:
food["proposed_id"] = pid
food["proposed_name"] = valid[pid]
# unbekannte ID wird ignoriert
food["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 f in plan["foods"]
if f.get("accept") and f.get("proposed_id") != f.get("current_id"))
return {"ok": True, "gesetzt": gesetzt, "zuweisungen": 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="Kategorien 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]
if request.alle:
argv.append("--alle")
job = await ctx.jobs.start(
plugin=ctx.id, label=f"KI ordnet ein: {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=("Kategorien zuweisen" 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