mirror of
https://github.com/Jeuners/Logpy-AgentOne.git
synced 2026-09-09 15:02:33 +02:00
feat: Modell-Dropdown listet lokale Ollama-Modelle live statt fest im Code
Bisher stand nur config.OLLAMA_MODELL zur Auswahl - ein neu per "ollama pull" installiertes Modell (z.B. ornith-1.5:9b) tauchte nicht im Leitstand auf, ohne Code-Aenderung. modellwahl.auswahl() fragt jetzt GET /api/tags live ab und baut die Ollama-Optionen daraus; reine Embedding-Modelle (capabilities ohne "completion", z.B. nomic-embed-text) werden ausgefiltert, weil sie nicht kategorisieren koennen. Die festen OpenRouter-Testeintraege bleiben bestehen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EbdWYnDtyMgxsq4eN1WnrQ
This commit is contained in:
parent
2605710818
commit
d39b360b7b
2 changed files with 82 additions and 26 deletions
|
|
@ -5,49 +5,104 @@ Der Watcher (pipe.watch, Langzeit-Prozess) und der Leitstand (pipe.server,
|
|||
eigener Prozess) laufen getrennt - die Auswahl wird deshalb in einer kleinen
|
||||
JSON-Datei geteilt, die categorize.py bei jedem Anruf frisch liest (kein
|
||||
Caching), damit eine Umschaltung sofort beim naechsten Anruf greift.
|
||||
|
||||
Die lokalen Ollama-Modelle werden live von Ollama abgefragt (GET /api/tags)
|
||||
statt fest im Code zu stehen - jedes per "ollama pull" installierte Modell
|
||||
taucht automatisch im Dropdown auf, ohne Code-Aenderung. Eine feste
|
||||
OpenRouter-Testauswahl bleibt daneben bestehen: die Modell-IDs dort lassen
|
||||
sich nicht lokal verifizieren, ein Tippfehler wuerde sonst erst beim naechsten
|
||||
Anruf sichtbar - fuer freie Eingabe siehe OPENROUTER_MODELL in der .env.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
from . import config
|
||||
|
||||
DATEI = config.TELEFON / "modell.json"
|
||||
|
||||
# Feste, kuratierte Auswahl statt freier Eingabe - ein Tippfehler in einer
|
||||
# Modell-ID im Leitstand darf nicht den naechsten Anruf scheitern lassen.
|
||||
# "openrouter"-Eintraege sind zum Testen gedacht (Transkript verlaesst dabei
|
||||
# den Rechner) - nicht fuer den Praxisbetrieb.
|
||||
AUSWAHL = [
|
||||
{"id": "ollama", "label": f"{config.OLLAMA_MODELL} (lokal)",
|
||||
"backend": "ollama", "modell": config.OLLAMA_MODELL},
|
||||
{"id": "or-ultra", "label": "Nemotron Ultra 550B (OpenRouter, Test)",
|
||||
"backend": "openrouter", "modell": "nvidia/nemotron-3-ultra-550b-a55b:free"},
|
||||
{"id": "or-lightning", "label": "Nemotron 3.5 Lightning (OpenRouter, Test)",
|
||||
"backend": "openrouter", "modell": "nvidia/nemotron-3.5-lightning:free"},
|
||||
]
|
||||
# Optionaler vierter Eintrag zum schnellen Ausprobieren eines beliebigen
|
||||
# OpenRouter-Modells, ohne Code zu aendern - einfach OPENROUTER_MODELL in
|
||||
# .env setzen.
|
||||
if config.OPENROUTER_MODELL:
|
||||
AUSWAHL.append({"id": "or-custom", "label": f"{config.OPENROUTER_MODELL} (OpenRouter, Test)",
|
||||
"backend": "openrouter", "modell": config.OPENROUTER_MODELL})
|
||||
_STANDARD = AUSWAHL[0]["id"]
|
||||
_NACH_ID = {e["id"]: e for e in AUSWAHL}
|
||||
# Ollama-Abfrage ist ein Netzwerkaufruf - kurz zwischenspeichern, damit ein
|
||||
# offener Leitstand (Polling alle paar Sekunden) Ollama nicht dauerbelastet.
|
||||
_CACHE: dict[str, tuple[float, list[dict]]] = {}
|
||||
CACHE_S = 30.0
|
||||
|
||||
|
||||
def _ollama_modelle() -> list[str]:
|
||||
"""Namen der lokal installierten, textfähigen Ollama-Modelle (leer bei Fehler).
|
||||
|
||||
Reine Embedding-Modelle (z. B. nomic-embed-text) werden ausgefiltert - sie
|
||||
können nicht kategorisieren, ein Aufruf würde nur mit einem Fehler enden.
|
||||
Ältere Ollama-Versionen liefern kein "capabilities"-Feld - dann bleibt das
|
||||
Modell sicherheitshalber drin, statt es zu Unrecht zu verstecken.
|
||||
"""
|
||||
req = urllib.request.Request(f"{config.OLLAMA_URL}/api/tags",
|
||||
headers={"User-Agent": "praxis-telefon-agent/1.0"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=5) as antwort:
|
||||
daten = json.loads(antwort.read().decode("utf-8"))
|
||||
except Exception:
|
||||
return []
|
||||
namen = []
|
||||
for m in daten.get("models", []):
|
||||
name = m.get("name")
|
||||
faehigkeiten = m.get("capabilities")
|
||||
if name and (faehigkeiten is None or "completion" in faehigkeiten):
|
||||
namen.append(name)
|
||||
return sorted(namen)
|
||||
|
||||
|
||||
def auswahl() -> list[dict]:
|
||||
"""Aktuelle Dropdown-Optionen: lokale Ollama-Modelle live + feste OpenRouter-Testeintraege."""
|
||||
jetzt = time.time()
|
||||
gepuffert = _CACHE.get("auswahl")
|
||||
if gepuffert and jetzt - gepuffert[0] < CACHE_S:
|
||||
return gepuffert[1]
|
||||
|
||||
# Ist Ollama gerade nicht erreichbar, bleibt wenigstens die konfigurierte
|
||||
# Standardauswahl im Dropdown - ein leeres Dropdown waere schlechter.
|
||||
lokale = _ollama_modelle() or [config.OLLAMA_MODELL]
|
||||
liste = [{"id": f"ollama:{name}", "label": f"{name} (lokal)",
|
||||
"backend": "ollama", "modell": name} for name in lokale]
|
||||
liste.append({"id": "or-ultra", "label": "Nemotron Ultra 550B (OpenRouter, Test)",
|
||||
"backend": "openrouter", "modell": "nvidia/nemotron-3-ultra-550b-a55b:free"})
|
||||
liste.append({"id": "or-lightning", "label": "Nemotron 3.5 Lightning (OpenRouter, Test)",
|
||||
"backend": "openrouter", "modell": "nvidia/nemotron-3.5-lightning:free"})
|
||||
# Optionaler Eintrag zum schnellen Ausprobieren eines beliebigen
|
||||
# OpenRouter-Modells, ohne Code zu aendern - einfach OPENROUTER_MODELL in
|
||||
# .env setzen.
|
||||
if config.OPENROUTER_MODELL:
|
||||
liste.append({"id": "or-custom", "label": f"{config.OPENROUTER_MODELL} (OpenRouter, Test)",
|
||||
"backend": "openrouter", "modell": config.OPENROUTER_MODELL})
|
||||
|
||||
_CACHE["auswahl"] = (jetzt, liste)
|
||||
return liste
|
||||
|
||||
|
||||
def _standard_id(optionen: list[dict]) -> str:
|
||||
"""Bevorzugt das in der .env konfigurierte Modell, sonst die erste Option."""
|
||||
for eintrag in optionen:
|
||||
if eintrag["backend"] == "ollama" and eintrag["modell"] == config.OLLAMA_MODELL:
|
||||
return eintrag["id"]
|
||||
return optionen[0]["id"]
|
||||
|
||||
|
||||
def aktuelle_id() -> str:
|
||||
"""Liest die aktive Auswahl. Unbekanntes/Fehlendes gilt als Standard."""
|
||||
optionen = auswahl()
|
||||
standard = _standard_id(optionen)
|
||||
try:
|
||||
wert = json.loads(DATEI.read_text(encoding="utf-8")).get("id")
|
||||
except Exception:
|
||||
return _STANDARD
|
||||
return wert if wert in _NACH_ID else _STANDARD
|
||||
return standard
|
||||
return wert if wert in {e["id"] for e in optionen} else standard
|
||||
|
||||
|
||||
def setze(kennung: str) -> bool:
|
||||
"""Schreibt die Auswahl. False, wenn die ID nicht vorgesehen ist."""
|
||||
if kennung not in _NACH_ID:
|
||||
"""Schreibt die Auswahl. False, wenn die ID nicht (mehr) vorgesehen ist."""
|
||||
if kennung not in {e["id"] for e in auswahl()}:
|
||||
return False
|
||||
try:
|
||||
DATEI.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
|
@ -59,5 +114,6 @@ def setze(kennung: str) -> bool:
|
|||
|
||||
def backend_und_modell() -> tuple[str, str]:
|
||||
"""Backend + Modell-ID der aktiven Auswahl, fuer categorize.py."""
|
||||
eintrag = _NACH_ID[aktuelle_id()]
|
||||
kennung = aktuelle_id()
|
||||
eintrag = next(e for e in auswahl() if e["id"] == kennung)
|
||||
return eintrag["backend"], eintrag["modell"]
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||
eintraege = _maskiere_verlauf(eintraege)
|
||||
self._json(eintraege)
|
||||
elif pfad == "/api/modell":
|
||||
self._json({"aktuell": modellwahl.aktuelle_id(), "optionen": modellwahl.AUSWAHL})
|
||||
self._json({"aktuell": modellwahl.aktuelle_id(), "optionen": modellwahl.auswahl()})
|
||||
elif pfad.startswith("/audio/"):
|
||||
self._audio(pfad[len("/audio/"):])
|
||||
else:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue