feat(diarization-ui): multi-provider LLM support via OpenAI-compatible API

- Replace Ollama-native API (/api/generate) with OpenAI-compatible
  /v1/chat/completions — works with Ollama, OpenAI, Groq, LM Studio,
  LocalAI, and any other OpenAI-compatible endpoint
- New providers table: name, base_url, optional API key, default flag
- Settings page: add/delete/set-default providers; rename LLM params
  from ollama_* to llm_* settings keys
- Per-prompt: choose provider from dropdown, model load button fetches
  from that provider's /v1/models
- <think>…</think> tags extracted for live thinking display (works with
  Ollama thinking models and DeepSeek-R1 style responses)
- Existing OLLAMA_BASE_URL env var seeds the initial provider on first
  start for backward compat

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 14:42:36 +02:00
parent ce40a3376f
commit b26fff0d0f

401
app.py
View File

@@ -5,6 +5,7 @@ import email.mime.multipart
import email.mime.text
import json
import os
import re
import smtplib
import sqlite3
import threading
@@ -19,10 +20,10 @@ from fastapi import FastAPI, File, Form, HTTPException, UploadFile
from fastapi.responses import HTMLResponse, PlainTextResponse, Response, JSONResponse, RedirectResponse
API_BASE = os.getenv("API_BASE", "http://gx10.aquantico.lan:8093").rstrip("/")
OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://gx10.aquantico.lan:11434").rstrip("/")
OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "qwen3.5:9b")
OLLAMA_NUM_PREDICT = int(os.getenv("OLLAMA_NUM_PREDICT", "16384"))
OLLAMA_THINK = os.getenv("OLLAMA_THINK", "true").lower() in ("1", "true", "yes")
# Kept for backward-compat seeding of the initial provider on first start
_SEED_PROVIDER_URL = os.getenv("OLLAMA_BASE_URL", "http://gx10.aquantico.lan:11434").rstrip("/")
_SEED_MODEL = os.getenv("OLLAMA_MODEL", "qwen3.5:9b")
_SEED_NUM_PREDICT = int(os.getenv("OLLAMA_NUM_PREDICT", "16384"))
DB_PATH = os.getenv("DB_PATH", "/data/ui.db")
app = FastAPI(title="Diarization UI")
@@ -156,6 +157,35 @@ def init_db():
except Exception:
pass
try:
c.execute(
"""
CREATE TABLE IF NOT EXISTS providers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
base_url TEXT NOT NULL,
api_key TEXT,
is_default INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL
)
"""
)
except Exception:
pass
try:
c.execute("ALTER TABLE prompts ADD COLUMN provider_id INTEGER REFERENCES providers(id)")
except Exception:
pass
# Seed default provider from env if table is empty
count = c.execute("SELECT COUNT(*) FROM providers").fetchone()[0]
if count == 0:
c.execute(
"INSERT INTO providers(name, base_url, api_key, is_default, created_at) VALUES (?,?,?,1,?)",
("Ollama (Standard)", _SEED_PROVIDER_URL, None, now_iso()),
)
try:
c.execute(
"""
@@ -203,6 +233,32 @@ def set_setting(key: str, value: str):
c.execute("INSERT OR REPLACE INTO settings(key, value) VALUES (?,?)", (key, value))
def _get_default_provider() -> dict | None:
with db() as c:
row = c.execute("SELECT * FROM providers WHERE is_default=1 LIMIT 1").fetchone()
if not row:
row = c.execute("SELECT * FROM providers ORDER BY id LIMIT 1").fetchone()
return dict(row) if row else None
def _get_provider(provider_id: int | None) -> dict | None:
if not provider_id:
return _get_default_provider()
with db() as c:
row = c.execute("SELECT * FROM providers WHERE id=?", (provider_id,)).fetchone()
return dict(row) if row else _get_default_provider()
def _split_thinking(text: str) -> tuple[str, str]:
"""Split <think>…</think> prefix from the rest of the content."""
if text.startswith("<think>"):
end = text.find("</think>")
if end == -1:
return text[7:], ""
return text[7:end], text[end + 8:].lstrip("\n")
return "", text
def _send_email(to_addrs: list[str], subject: str, body_md: str, attachment_name: str):
host = get_setting("smtp_host", "")
if not host:
@@ -480,68 +536,98 @@ def _process_analysis_job(job_id: int):
if not (doc["content_md"] or "").strip():
raise RuntimeError("Dokument hat keinen Inhalt bitte zuerst das Transkript prüfen")
user_extra = (j.get("user_prompt") or "").strip()
llm_prompt = (
"Du bist ein präziser Assistent. Antworte auf Deutsch.\\n"
f"AUFTRAG:\\n{prm['prompt']}\\n"
+ (f"\\nZUSATZINFOS:\\n{user_extra}\\n" if user_extra else "")
+ f"\\nTEXT:\\n{doc['content_md']}\\n"
)
use_default = int(prm["llm_use_default"] if prm["llm_use_default"] is not None else 1)
if use_default:
model = get_setting("ollama_model", OLLAMA_MODEL)
think = get_setting("ollama_think", "true" if OLLAMA_THINK else "false") == "true"
num_ctx_setting = get_setting("ollama_num_ctx", "auto")
num_predict = int(get_setting("ollama_num_predict", str(OLLAMA_NUM_PREDICT)))
repeat_penalty = float(get_setting("ollama_repeat_penalty", "1.15"))
repeat_last_n = int(get_setting("ollama_repeat_last_n", "128"))
provider = _get_default_provider()
model = get_setting("llm_model", _SEED_MODEL)
num_ctx_setting = get_setting("llm_num_ctx", "auto")
num_predict = int(get_setting("llm_num_predict", str(_SEED_NUM_PREDICT)))
repeat_penalty = float(get_setting("llm_repeat_penalty", "1.15"))
repeat_last_n = int(get_setting("llm_repeat_last_n", "128"))
else:
model = prm["llm_model"] or get_setting("ollama_model", OLLAMA_MODEL)
think = (prm["llm_think"] or get_setting("ollama_think", "true" if OLLAMA_THINK else "false")) == "true"
num_ctx_setting = prm["llm_num_ctx"] or get_setting("ollama_num_ctx", "auto")
num_predict = int(prm["llm_num_predict"] or get_setting("ollama_num_predict", str(OLLAMA_NUM_PREDICT)))
repeat_penalty = float(prm["llm_repeat_penalty"] or get_setting("ollama_repeat_penalty", "1.15"))
repeat_last_n = int(prm["llm_repeat_last_n"] or get_setting("ollama_repeat_last_n", "128"))
num_ctx = _estimate_num_ctx(llm_prompt) if num_ctx_setting == "auto" else int(num_ctx_setting)
_job_set(job_id, llm_prompt=f"[num_ctx={num_ctx}]\n\n{llm_prompt}")
provider = _get_provider(prm.get("provider_id"))
model = prm["llm_model"] or get_setting("llm_model", _SEED_MODEL)
num_ctx_setting = prm["llm_num_ctx"] or get_setting("llm_num_ctx", "auto")
num_predict = int(prm["llm_num_predict"] or get_setting("llm_num_predict", str(_SEED_NUM_PREDICT)))
repeat_penalty = float(prm["llm_repeat_penalty"] or get_setting("llm_repeat_penalty", "1.15"))
repeat_last_n = int(prm["llm_repeat_last_n"] or get_setting("llm_repeat_last_n", "128"))
if not provider:
raise RuntimeError("Kein LLM-Anbieter konfiguriert")
user_extra = (j.get("user_prompt") or "").strip()
system_msg = "Du bist ein präziser Assistent. Antworte auf Deutsch."
user_msg = (
f"AUFTRAG:\n{prm['prompt']}\n"
+ (f"\nZUSATZINFOS:\n{user_extra}\n" if user_extra else "")
+ f"\nTEXT:\n{doc['content_md']}\n"
)
ctx_hint = f"[num_ctx=auto]" if num_ctx_setting == "auto" else f"[num_ctx={num_ctx_setting}]"
_job_set(job_id, llm_prompt=f"{ctx_hint} [Anbieter: {provider['name']} | Modell: {model}]\n\nSYSTEM:\n{system_msg}\n\nUSER:\n{user_msg}")
with _JOB_STREAM_LOCK:
_JOB_STREAMS[job_id] = {"thinking": "", "response": ""}
r = requests.post(
f"{OLLAMA_BASE_URL}/api/generate",
json={"model": model, "prompt": llm_prompt, "stream": True, "think": think, "options": {
"num_ctx": num_ctx,
headers = {"Content-Type": "application/json"}
if provider.get("api_key"):
headers["Authorization"] = f"Bearer {provider['api_key']}"
req_body: dict = {
"model": model,
"messages": [
{"role": "system", "content": system_msg},
{"role": "user", "content": user_msg},
],
"stream": True,
"max_tokens": num_predict,
}
if num_ctx_setting != "auto":
req_body["options"] = {
"num_ctx": int(num_ctx_setting),
"num_predict": num_predict,
"repeat_penalty": repeat_penalty,
"repeat_last_n": repeat_last_n,
}},
}
r = requests.post(
f"{provider['base_url'].rstrip('/')}/v1/chat/completions",
json=req_body,
headers=headers,
stream=True,
timeout=1200,
)
r.raise_for_status()
acc_thinking = ""
acc_response = ""
ollama_final = {}
acc = ""
chunk_count = 0
finish_reason = None
for line in r.iter_lines():
if not line:
continue
chunk = json.loads(line)
acc_thinking += chunk.get("thinking") or ""
acc_response += chunk.get("response") or ""
line_str = line.decode() if isinstance(line, bytes) else line
if not line_str.startswith("data: "):
continue
data_str = line_str[6:]
if data_str.strip() == "[DONE]":
break
try:
chunk = json.loads(data_str)
except Exception:
continue
choice = (chunk.get("choices") or [{}])[0]
acc += choice.get("delta", {}).get("content", "") or ""
finish_reason = choice.get("finish_reason") or finish_reason
t_part, r_part = _split_thinking(acc)
with _JOB_STREAM_LOCK:
_JOB_STREAMS[job_id] = {"thinking": acc_thinking, "response": acc_response}
_JOB_STREAMS[job_id] = {"thinking": t_part, "response": r_part}
chunk_count += 1
if chunk_count % 100 == 0:
j_check = _job_get(job_id)
if not j_check or j_check["status"] == "cancelled":
return
if chunk.get("done"):
ollama_final = chunk
break
answer = acc_response
acc_thinking, answer = _split_thinking(acc)
j = _job_get(job_id)
if not j or j["status"] == "cancelled":
@@ -560,7 +646,7 @@ def _process_analysis_job(job_id: int):
answer,
doc["id"],
prm["id"],
json.dumps({"ollama_response": ollama_final}, ensure_ascii=False),
json.dumps({"provider": provider["name"], "model": model, "finish_reason": finish_reason}, ensure_ascii=False),
now_iso(),
),
)
@@ -667,15 +753,22 @@ self.addEventListener('fetch', (event) => {
@app.get("/healthz")
def healthz():
return {"ok": True, "api_base": API_BASE, "ollama_base_url": OLLAMA_BASE_URL, "ollama_model": OLLAMA_MODEL, "db_path": DB_PATH}
return {"ok": True, "api_base": API_BASE, "db_path": DB_PATH}
@app.get("/settings/models")
def settings_models():
def settings_models(provider_id: Optional[int] = None):
provider = _get_provider(provider_id)
if not provider:
return JSONResponse({"ok": False, "error": "Kein Anbieter konfiguriert", "models": []})
headers = {}
if provider.get("api_key"):
headers["Authorization"] = f"Bearer {provider['api_key']}"
try:
r = requests.get(f"{OLLAMA_BASE_URL}/api/tags", timeout=5)
r = requests.get(f"{provider['base_url'].rstrip('/')}/v1/models", headers=headers, timeout=5)
r.raise_for_status()
names = sorted(m["name"] for m in r.json().get("models", []))
data = r.json()
names = sorted(m["id"] for m in data.get("data", []))
return JSONResponse({"ok": True, "models": names})
except Exception as e:
return JSONResponse({"ok": False, "error": str(e), "models": []})
@@ -683,12 +776,11 @@ def settings_models():
@app.get("/settings", response_class=HTMLResponse)
def settings_page(msg: str = ""):
current_model = get_setting("ollama_model", OLLAMA_MODEL)
current_think = get_setting("ollama_think", "true" if OLLAMA_THINK else "false") == "true"
current_num_ctx = get_setting("ollama_num_ctx", "auto")
current_num_predict = get_setting("ollama_num_predict", str(OLLAMA_NUM_PREDICT))
current_repeat_penalty = get_setting("ollama_repeat_penalty", "1.15")
current_repeat_last_n = get_setting("ollama_repeat_last_n", "128")
current_model = get_setting("llm_model", _SEED_MODEL)
current_num_ctx = get_setting("llm_num_ctx", "auto")
current_num_predict = get_setting("llm_num_predict", str(_SEED_NUM_PREDICT))
current_repeat_penalty = get_setting("llm_repeat_penalty", "1.15")
current_repeat_last_n = get_setting("llm_repeat_last_n", "128")
smtp_host = get_setting("smtp_host", "")
smtp_port = get_setting("smtp_port", "587")
@@ -699,8 +791,25 @@ def settings_page(msg: str = ""):
smtp_tls_checked = "checked" if smtp_tls else ""
with db() as c:
providers = c.execute("SELECT * FROM providers ORDER BY name").fetchall()
email_addrs = c.execute("SELECT id, label, email FROM email_addresses ORDER BY label").fetchall()
provider_rows = "".join(
f"""<tr>
<td>{p["name"]}</td>
<td><code class='small'>{p["base_url"]}</code></td>
<td class='text-center'><i class='bi bi-{'key-fill text-success' if p["api_key"] else 'key text-secondary'}'></i></td>
<td class='text-center'>{'<span class=\'badge bg-primary\'>Standard</span>' if p["is_default"] else f'<form method=\'post\' action=\'/providers/{p["id"]}/set-default\' class=\'d-inline\'><button class=\'btn btn-sm btn-outline-secondary py-0 px-2\'>Standard</button></form>'}</td>
<td class='text-end'>
<form method='post' action='/providers/{p["id"]}/delete' class='d-inline'
onsubmit='return confirm("Anbieter löschen?")'>
<button class='btn btn-sm btn-outline-danger py-0 px-2'><i class='bi bi-trash'></i></button>
</form>
</td>
</tr>"""
for p in providers
)
email_addr_rows = "".join(
f"""<tr>
<td>{a["label"]}</td>
@@ -715,7 +824,9 @@ def settings_page(msg: str = ""):
for a in email_addrs
)
think_checked = "checked" if current_think else ""
provider_id_opts = "".join(
f"<option value='{p['id']}'>{p['name']}</option>" for p in providers
)
ctx_options = [("auto", "Automatisch (empfohlen)"), ("4096", "4 096"), ("8192", "8 192"),
("16384", "16 384"), ("32768", "32 768"), ("65536", "65 536"), ("131072", "131 072")]
@@ -729,16 +840,48 @@ def settings_page(msg: str = ""):
<h2 class='h4 mb-0'>Einstellungen</h2>
</div>
{f"<div class='alert alert-success py-2'>{msg}</div>" if msg else ""}
<div class='card'>
<h5 class='h6 fw-semibold mb-3'><i class='bi bi-robot text-primary me-1'></i> Ollama / LLM</h5>
<div class='card' id='anbieter'>
<h5 class='h6 fw-semibold mb-3'><i class='bi bi-hdd-network text-primary me-1'></i> LLM-Anbieter</h5>
<p class='text-secondary small mb-2'>OpenAI-kompatible Endpunkte (Ollama, OpenAI, Groq, LM Studio, …)</p>
{"<table class='table table-sm mb-3'><thead><tr><th>Name</th><th>Base-URL</th><th class='text-center'>API-Key</th><th class='text-center'>Standard</th><th></th></tr></thead><tbody>" + provider_rows + "</tbody></table>" if providers else "<p class='text-secondary small'>Noch kein Anbieter konfiguriert.</p>"}
<form method='post' action='/providers/add' class='row g-2 align-items-end'>
<div class='col-12 col-md-3'>
<label class='form-label'>Name</label>
<input class='form-control' name='name' placeholder='z.B. Lokales Ollama' required>
</div>
<div class='col-12 col-md-4'>
<label class='form-label'>Base-URL</label>
<input class='form-control font-monospace' name='base_url' placeholder='http://localhost:11434' required>
</div>
<div class='col-12 col-md-3'>
<label class='form-label'>API-Key <span class='text-secondary fw-normal'>(optional)</span></label>
<input class='form-control' type='password' name='api_key' autocomplete='new-password'>
</div>
<div class='col-auto d-flex align-items-end gap-2'>
<div class='form-check mb-2'>
<input class='form-check-input' type='checkbox' name='is_default' value='1' id='provDefault'>
<label class='form-check-label' for='provDefault'>Standard</label>
</div>
<button class='btn btn-outline-primary mb-0' type='submit'><i class='bi bi-plus-lg'></i> Hinzufügen</button>
</div>
</form>
</div>
<div class='card mt-3'>
<h5 class='h6 fw-semibold mb-3'><i class='bi bi-sliders text-primary me-1'></i> Standard-LLM-Parameter</h5>
<p class='text-secondary small mb-2'>Gelten für alle Prompts ohne eigene Einstellungen. Anbieter = markierter Standard-Anbieter oben.</p>
<form method='post' action='/settings'>
<div class='row g-3'>
<div class='col-12'>
<label class='form-label'>Modell</label>
<label class='form-label'>Standard-Modell</label>
<div class='input-group'>
<input id='modelInput' class='form-control font-monospace' name='ollama_model'
value='{current_model}' placeholder='z.B. qwen3.5:9b' required>
<select id='providerSelect' class='form-select' style='max-width:220px' onchange='updateProviderForLoad()'>
{provider_id_opts}
</select>
<input id='modelInput' class='form-control font-monospace' name='llm_model'
value='{current_model}' placeholder='z.B. qwen3:8b' required>
<button type='button' class='btn btn-outline-secondary' id='loadModelsBtn' onclick='loadModels()'>
<i class='bi bi-arrow-clockwise' id='loadIcon'></i> Modelle laden
</button>
@@ -751,38 +894,28 @@ def settings_page(msg: str = ""):
<div class='col-12 col-md-4'>
<label class='form-label'>Kontextgröße (num_ctx)</label>
<select class='form-select' name='ollama_num_ctx'>{ctx_opts_html}</select>
<select class='form-select' name='llm_num_ctx'>{ctx_opts_html}</select>
<div class='form-text'>Automatisch wählt passend zur Promptgröße.</div>
</div>
<div class='col-12 col-md-4'>
<label class='form-label'>Max. Ausgabe-Tokens (num_predict)</label>
<input class='form-control' type='number' name='ollama_num_predict' min='256' max='131072' step='256'
<input class='form-control' type='number' name='llm_num_predict' min='256' max='131072' step='256'
value='{current_num_predict}'>
</div>
<div class='col-12 col-md-2'>
<label class='form-label'>Repeat Penalty</label>
<input class='form-control' type='number' name='ollama_repeat_penalty' min='1.0' max='2.0' step='0.01'
<input class='form-control' type='number' name='llm_repeat_penalty' min='1.0' max='2.0' step='0.01'
value='{current_repeat_penalty}'>
</div>
<div class='col-12 col-md-2'>
<label class='form-label'>Repeat Last N</label>
<input class='form-control' type='number' name='ollama_repeat_last_n' min='0' max='2048' step='1'
<input class='form-control' type='number' name='llm_repeat_last_n' min='0' max='2048' step='1'
value='{current_repeat_last_n}'>
</div>
<div class='col-12'>
<div class='form-check form-switch'>
<input class='form-check-input' type='checkbox' role='switch' id='thinkSwitch'
name='ollama_think' value='true' {think_checked}>
<label class='form-check-label' for='thinkSwitch'>
Thinking aktivieren <span class='text-secondary small'>(Extended Thinking / Chain-of-Thought)</span>
</label>
</div>
</div>
</div>
<div class='mt-3'>
<button class='btn btn-primary' type='submit'><i class='bi bi-check2-circle'></i> Speichern</button>
@@ -851,13 +984,18 @@ def settings_page(msg: str = ""):
<table class='table table-sm mb-0'>
<tbody>
<tr><td class='text-secondary' style='width:220px'>API_BASE</td><td><code>{API_BASE}</code></td></tr>
<tr><td class='text-secondary'>OLLAMA_BASE_URL</td><td><code>{OLLAMA_BASE_URL}</code></td></tr>
<tr><td class='text-secondary'>DB_PATH</td><td><code>{DB_PATH}</code></td></tr>
</tbody>
</table>
</div>
<script>
let _loadProviderId = null;
function updateProviderForLoad() {{
_loadProviderId = document.getElementById('providerSelect')?.value || null;
document.getElementById('modelSelectWrap').classList.add('d-none');
}}
async function loadModels() {{
const btn = document.getElementById('loadModelsBtn');
const icon = document.getElementById('loadIcon');
@@ -866,11 +1004,12 @@ async function loadModels() {{
const inp = document.getElementById('modelInput');
btn.disabled = true;
icon.className = 'bi bi-hourglass-split';
const pid = _loadProviderId || document.getElementById('providerSelect')?.value || '';
try {{
const res = await fetch('/settings/models');
const res = await fetch('/settings/models' + (pid ? '?provider_id=' + pid : ''));
const data = await res.json();
if (!data.ok || !data.models.length) {{
alert('Keine Modelle gefunden oder Ollama nicht erreichbar:\\n' + (data.error || ''));
alert('Keine Modelle gefunden:\\n' + (data.error || ''));
return;
}}
const current = inp.value.trim();
@@ -897,19 +1036,17 @@ async function loadModels() {{
@app.post("/settings", response_class=HTMLResponse)
def settings_save(
ollama_model: str = Form(...),
ollama_think: Optional[str] = Form(None),
ollama_num_ctx: str = Form("auto"),
ollama_num_predict: str = Form("16384"),
ollama_repeat_penalty: str = Form("1.15"),
ollama_repeat_last_n: str = Form("128"),
llm_model: str = Form(...),
llm_num_ctx: str = Form("auto"),
llm_num_predict: str = Form("16384"),
llm_repeat_penalty: str = Form("1.15"),
llm_repeat_last_n: str = Form("128"),
):
set_setting("ollama_model", ollama_model.strip())
set_setting("ollama_think", "true" if ollama_think == "true" else "false")
set_setting("ollama_num_ctx", ollama_num_ctx.strip())
set_setting("ollama_num_predict", str(int(float(ollama_num_predict))))
set_setting("ollama_repeat_penalty", str(float(ollama_repeat_penalty)))
set_setting("ollama_repeat_last_n", str(int(float(ollama_repeat_last_n))))
set_setting("llm_model", llm_model.strip())
set_setting("llm_num_ctx", llm_num_ctx.strip())
set_setting("llm_num_predict", str(int(float(llm_num_predict))))
set_setting("llm_repeat_penalty", str(float(llm_repeat_penalty)))
set_setting("llm_repeat_last_n", str(int(float(llm_repeat_last_n))))
return RedirectResponse("/settings?msg=Einstellungen+gespeichert.", status_code=303)
@@ -949,6 +1086,38 @@ def delete_email_address(addr_id: int):
return RedirectResponse("/settings?msg=E-Mail-Adresse+gelöscht.#email-adressen", status_code=303)
@app.post("/providers/add", response_class=HTMLResponse)
def add_provider(
name: str = Form(...),
base_url: str = Form(...),
api_key: str = Form(""),
is_default: Optional[str] = Form(None),
):
with db() as c:
if is_default == "1":
c.execute("UPDATE providers SET is_default=0")
c.execute(
"INSERT INTO providers(name, base_url, api_key, is_default, created_at) VALUES (?,?,?,?,?)",
(name.strip(), base_url.strip().rstrip("/"), api_key.strip() or None, 1 if is_default == "1" else 0, now_iso()),
)
return RedirectResponse("/settings?msg=Anbieter+hinzugefügt.#anbieter", status_code=303)
@app.post("/providers/{provider_id}/delete", response_class=HTMLResponse)
def delete_provider(provider_id: int):
with db() as c:
c.execute("DELETE FROM providers WHERE id=?", (provider_id,))
return RedirectResponse("/settings?msg=Anbieter+gelöscht.#anbieter", status_code=303)
@app.post("/providers/{provider_id}/set-default", response_class=HTMLResponse)
def set_default_provider(provider_id: int):
with db() as c:
c.execute("UPDATE providers SET is_default=0")
c.execute("UPDATE providers SET is_default=1 WHERE id=?", (provider_id,))
return RedirectResponse("/settings?msg=Standard-Anbieter+gesetzt.#anbieter", status_code=303)
@app.get("/", response_class=HTMLResponse)
def upload_page(msg: str = ""):
projects = get_projects()
@@ -1598,21 +1767,24 @@ def bulk_delete_documents(ids: List[int] = Form(...)):
return HTMLResponse("<meta http-equiv='refresh' content='0; url=/library'>")
def _prompt_llm_block(p, def_model, def_think, def_num_ctx, def_num_predict, def_repeat_penalty, def_repeat_last_n):
def _prompt_llm_block(p, providers, def_model, def_num_ctx, def_num_predict, def_repeat_penalty, def_repeat_last_n):
pid = p["id"]
use_default = int(p["llm_use_default"]) if p["llm_use_default"] is not None else 1
m = p["llm_model"] or def_model
th = p["llm_think"] or def_think
nc = p["llm_num_ctx"] or def_num_ctx
np_ = p["llm_num_predict"] or def_num_predict
rp = p["llm_repeat_penalty"] or def_repeat_penalty
rn = p["llm_repeat_last_n"] or def_repeat_last_n
cur_provider_id = p.get("provider_id")
disabled = "disabled" if use_default else ""
opacity = "opacity:.45;pointer-events:none" if use_default else ""
ctx_opts = [("auto","Automatisch"), ("4096","4 096"), ("8192","8 192"), ("16384","16 384"),
("32768","32 768"), ("65536","65 536"), ("131072","131 072")]
ctx_html = "".join(f"<option value='{v}'{' selected' if v==str(nc) else ''}>{l}</option>" for v,l in ctx_opts)
think_checked = "checked" if str(th) == "true" else ""
provider_opts = "".join(
f"<option value='{pv['id']}'{' selected' if pv['id'] == cur_provider_id else ''}>{pv['name']}</option>"
for pv in providers
)
return (
f"<hr class='my-3'>"
f"<div class='d-flex align-items-center gap-3 mb-2'>"
@@ -1625,11 +1797,18 @@ def _prompt_llm_block(p, def_model, def_think, def_num_ctx, def_num_predict, def
f"</div>"
f"<input type='hidden' id='llm_use_default_{pid}' name='llm_use_default' value='{use_default}'>"
f"<div id='llmfields_{pid}' class='row g-2' style='{opacity}'>"
f" <div class='col-12'>"
f" <div class='col-12 col-md-4'>"
f" <label class='form-label small text-secondary mb-1'>Anbieter</label>"
f" <select class='form-select form-select-sm' name='provider_id' id='llmprov_{pid}'"
f" onchange='loadModelsInto(\"llmmodel_{pid}\",this.value)' {disabled}>"
f" <option value=''> Standard </option>{provider_opts}"
f" </select>"
f" </div>"
f" <div class='col-12 col-md-8'>"
f" <label class='form-label small text-secondary mb-1'>Modell</label>"
f" <div class='input-group input-group-sm'>"
f" <input id='llmmodel_{pid}' class='form-control form-control-sm font-monospace' name='llm_model' value='{m}' {disabled}>"
f" <button type='button' class='btn btn-outline-secondary btn-sm' onclick='loadModelsInto(\"llmmodel_{pid}\")' {'disabled' if use_default else ''}>"
f" <button type='button' class='btn btn-outline-secondary btn-sm' onclick='loadModelsInto(\"llmmodel_{pid}\",document.getElementById(\"llmprov_{pid}\").value)' {'disabled' if use_default else ''}>"
f" <i class='bi bi-arrow-clockwise'></i>"
f" </button>"
f" </div>"
@@ -1650,12 +1829,6 @@ def _prompt_llm_block(p, def_model, def_think, def_num_ctx, def_num_predict, def
f" <label class='form-label small text-secondary mb-1'>Repeat Last N</label>"
f" <input class='form-control form-control-sm' type='number' name='llm_repeat_last_n' min='0' max='2048' step='1' value='{rn}' {disabled}>"
f" </div>"
f" <div class='col-12'>"
f" <div class='form-check form-switch'>"
f" <input class='form-check-input' type='checkbox' role='switch' id='llmthink_{pid}' name='llm_think' value='true' {think_checked} {disabled}>"
f" <label class='form-check-label small' for='llmthink_{pid}'>Thinking aktivieren</label>"
f" </div>"
f" </div>"
f"</div>"
)
@@ -1665,12 +1838,12 @@ def prompts_page():
with db() as c:
prompts = c.execute("SELECT * FROM prompts ORDER BY name").fetchall()
projects = c.execute("SELECT id,name FROM projects ORDER BY name").fetchall()
def_model = get_setting("ollama_model", OLLAMA_MODEL)
def_think = get_setting("ollama_think", "true" if OLLAMA_THINK else "false")
def_num_ctx = get_setting("ollama_num_ctx", "auto")
def_num_predict = get_setting("ollama_num_predict", str(OLLAMA_NUM_PREDICT))
def_repeat_penalty = get_setting("ollama_repeat_penalty", "1.15")
def_repeat_last_n = get_setting("ollama_repeat_last_n", "128")
providers = c.execute("SELECT * FROM providers ORDER BY name").fetchall()
def_model = get_setting("llm_model", _SEED_MODEL)
def_num_ctx = get_setting("llm_num_ctx", "auto")
def_num_predict = get_setting("llm_num_predict", str(_SEED_NUM_PREDICT))
def_repeat_penalty = get_setting("llm_repeat_penalty", "1.15")
def_repeat_last_n = get_setting("llm_repeat_last_n", "128")
project_opts = "".join([f"<option value='{p['name']}'>{p['name']}</option>" for p in projects])
@@ -1706,7 +1879,7 @@ def prompts_page():
f"<textarea id='prompt_text_{p['id']}' class='form-control' name='prompt' style='min-height:110px; resize:vertical'>{p['prompt']}</textarea>"
f"</div>"
f"</div>"
+ _prompt_llm_block(p, def_model, def_think, def_num_ctx, def_num_predict, def_repeat_penalty, def_repeat_last_n) +
+ _prompt_llm_block(p, providers, def_model, def_num_ctx, def_num_predict, def_repeat_penalty, def_repeat_last_n) +
f"<div class='mt-3 d-flex flex-wrap gap-2'>"
f"<button class='btn btn-primary btn-sm' type='submit'><i class='bi bi-check2-circle'></i> Speichern</button>"
f"<button class='btn btn-outline-secondary btn-sm' type='button' onclick='previewPrompt({p['id']})'><i class='bi bi-eye'></i> Anzeigen</button>"
@@ -1833,7 +2006,6 @@ document.addEventListener('DOMContentLoaded', () => {{
const _llmDefaults = {{
model: {json.dumps(def_model)},
think: {json.dumps(def_think)},
num_ctx: {json.dumps(def_num_ctx)},
num_predict: {json.dumps(def_num_predict)},
repeat_penalty: {json.dumps(def_repeat_penalty)},
@@ -1858,19 +2030,19 @@ function toggleLlmDef(pid, isDefault) {{
}}
}}
async function loadModelsInto(inputId) {{
async function loadModelsInto(inputId, providerId) {{
const inp = document.getElementById(inputId);
if (!inp) return;
const btn = inp.nextElementSibling;
if (btn) btn.disabled = true;
try {{
const res = await fetch('/settings/models');
const pid = providerId || '';
const res = await fetch('/settings/models' + (pid ? '?provider_id=' + pid : ''));
const data = await res.json();
if (!data.ok || !data.models.length) {{
alert('Keine Modelle gefunden: ' + (data.error || ''));
return;
}}
const current = inp.value.trim();
const v = await window.uiSelect('Modell wählen',
data.models.map(m => ({{value: m, label: m}})),
' Modell auswählen '
@@ -1913,27 +2085,28 @@ def prompt_update(
prompt: str = Form(...),
llm_use_default: int = Form(1),
llm_model: Optional[str] = Form(None),
llm_think: Optional[str] = Form(None),
llm_num_ctx: Optional[str] = Form(None),
llm_num_predict: Optional[str] = Form(None),
llm_repeat_penalty: Optional[str] = Form(None),
llm_repeat_last_n: Optional[str] = Form(None),
provider_id: Optional[int] = Form(None),
):
with db() as c:
c.execute(
"""UPDATE prompts SET name=?, prompt=?, updated_at=?,
llm_use_default=?, llm_model=?, llm_think=?,
llm_num_ctx=?, llm_num_predict=?, llm_repeat_penalty=?, llm_repeat_last_n=?
llm_use_default=?, llm_model=?,
llm_num_ctx=?, llm_num_predict=?, llm_repeat_penalty=?, llm_repeat_last_n=?,
provider_id=?
WHERE id=?""",
(
name.strip(), prompt.strip(), now_iso(),
llm_use_default,
(llm_model or "").strip() or None,
"true" if llm_think == "true" else "false",
(llm_num_ctx or "auto").strip(),
int(float(llm_num_predict)) if llm_num_predict else None,
float(llm_repeat_penalty) if llm_repeat_penalty else None,
int(float(llm_repeat_last_n)) if llm_repeat_last_n else None,
provider_id or None,
id,
),
)