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:
401
app.py
401
app.py
@@ -5,6 +5,7 @@ import email.mime.multipart
|
|||||||
import email.mime.text
|
import email.mime.text
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import smtplib
|
import smtplib
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import threading
|
import threading
|
||||||
@@ -19,10 +20,10 @@ from fastapi import FastAPI, File, Form, HTTPException, UploadFile
|
|||||||
from fastapi.responses import HTMLResponse, PlainTextResponse, Response, JSONResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, PlainTextResponse, Response, JSONResponse, RedirectResponse
|
||||||
|
|
||||||
API_BASE = os.getenv("API_BASE", "http://gx10.aquantico.lan:8093").rstrip("/")
|
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("/")
|
# Kept for backward-compat seeding of the initial provider on first start
|
||||||
OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "qwen3.5:9b")
|
_SEED_PROVIDER_URL = os.getenv("OLLAMA_BASE_URL", "http://gx10.aquantico.lan:11434").rstrip("/")
|
||||||
OLLAMA_NUM_PREDICT = int(os.getenv("OLLAMA_NUM_PREDICT", "16384"))
|
_SEED_MODEL = os.getenv("OLLAMA_MODEL", "qwen3.5:9b")
|
||||||
OLLAMA_THINK = os.getenv("OLLAMA_THINK", "true").lower() in ("1", "true", "yes")
|
_SEED_NUM_PREDICT = int(os.getenv("OLLAMA_NUM_PREDICT", "16384"))
|
||||||
DB_PATH = os.getenv("DB_PATH", "/data/ui.db")
|
DB_PATH = os.getenv("DB_PATH", "/data/ui.db")
|
||||||
|
|
||||||
app = FastAPI(title="Diarization UI")
|
app = FastAPI(title="Diarization UI")
|
||||||
@@ -156,6 +157,35 @@ def init_db():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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:
|
try:
|
||||||
c.execute(
|
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))
|
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):
|
def _send_email(to_addrs: list[str], subject: str, body_md: str, attachment_name: str):
|
||||||
host = get_setting("smtp_host", "")
|
host = get_setting("smtp_host", "")
|
||||||
if not host:
|
if not host:
|
||||||
@@ -480,68 +536,98 @@ def _process_analysis_job(job_id: int):
|
|||||||
if not (doc["content_md"] or "").strip():
|
if not (doc["content_md"] or "").strip():
|
||||||
raise RuntimeError("Dokument hat keinen Inhalt – bitte zuerst das Transkript prüfen")
|
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)
|
use_default = int(prm["llm_use_default"] if prm["llm_use_default"] is not None else 1)
|
||||||
if use_default:
|
if use_default:
|
||||||
model = get_setting("ollama_model", OLLAMA_MODEL)
|
provider = _get_default_provider()
|
||||||
think = get_setting("ollama_think", "true" if OLLAMA_THINK else "false") == "true"
|
model = get_setting("llm_model", _SEED_MODEL)
|
||||||
num_ctx_setting = get_setting("ollama_num_ctx", "auto")
|
num_ctx_setting = get_setting("llm_num_ctx", "auto")
|
||||||
num_predict = int(get_setting("ollama_num_predict", str(OLLAMA_NUM_PREDICT)))
|
num_predict = int(get_setting("llm_num_predict", str(_SEED_NUM_PREDICT)))
|
||||||
repeat_penalty = float(get_setting("ollama_repeat_penalty", "1.15"))
|
repeat_penalty = float(get_setting("llm_repeat_penalty", "1.15"))
|
||||||
repeat_last_n = int(get_setting("ollama_repeat_last_n", "128"))
|
repeat_last_n = int(get_setting("llm_repeat_last_n", "128"))
|
||||||
else:
|
else:
|
||||||
model = prm["llm_model"] or get_setting("ollama_model", OLLAMA_MODEL)
|
provider = _get_provider(prm.get("provider_id"))
|
||||||
think = (prm["llm_think"] or get_setting("ollama_think", "true" if OLLAMA_THINK else "false")) == "true"
|
model = prm["llm_model"] or get_setting("llm_model", _SEED_MODEL)
|
||||||
num_ctx_setting = prm["llm_num_ctx"] or get_setting("ollama_num_ctx", "auto")
|
num_ctx_setting = prm["llm_num_ctx"] or get_setting("llm_num_ctx", "auto")
|
||||||
num_predict = int(prm["llm_num_predict"] or get_setting("ollama_num_predict", str(OLLAMA_NUM_PREDICT)))
|
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("ollama_repeat_penalty", "1.15"))
|
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("ollama_repeat_last_n", "128"))
|
repeat_last_n = int(prm["llm_repeat_last_n"] or get_setting("llm_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}")
|
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:
|
with _JOB_STREAM_LOCK:
|
||||||
_JOB_STREAMS[job_id] = {"thinking": "", "response": ""}
|
_JOB_STREAMS[job_id] = {"thinking": "", "response": ""}
|
||||||
r = requests.post(
|
|
||||||
f"{OLLAMA_BASE_URL}/api/generate",
|
headers = {"Content-Type": "application/json"}
|
||||||
json={"model": model, "prompt": llm_prompt, "stream": True, "think": think, "options": {
|
if provider.get("api_key"):
|
||||||
"num_ctx": num_ctx,
|
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,
|
"num_predict": num_predict,
|
||||||
"repeat_penalty": repeat_penalty,
|
"repeat_penalty": repeat_penalty,
|
||||||
"repeat_last_n": repeat_last_n,
|
"repeat_last_n": repeat_last_n,
|
||||||
}},
|
}
|
||||||
|
|
||||||
|
r = requests.post(
|
||||||
|
f"{provider['base_url'].rstrip('/')}/v1/chat/completions",
|
||||||
|
json=req_body,
|
||||||
|
headers=headers,
|
||||||
stream=True,
|
stream=True,
|
||||||
timeout=1200,
|
timeout=1200,
|
||||||
)
|
)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
acc_thinking = ""
|
acc = ""
|
||||||
acc_response = ""
|
|
||||||
ollama_final = {}
|
|
||||||
chunk_count = 0
|
chunk_count = 0
|
||||||
|
finish_reason = None
|
||||||
for line in r.iter_lines():
|
for line in r.iter_lines():
|
||||||
if not line:
|
if not line:
|
||||||
continue
|
continue
|
||||||
chunk = json.loads(line)
|
line_str = line.decode() if isinstance(line, bytes) else line
|
||||||
acc_thinking += chunk.get("thinking") or ""
|
if not line_str.startswith("data: "):
|
||||||
acc_response += chunk.get("response") or ""
|
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:
|
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
|
chunk_count += 1
|
||||||
if chunk_count % 100 == 0:
|
if chunk_count % 100 == 0:
|
||||||
j_check = _job_get(job_id)
|
j_check = _job_get(job_id)
|
||||||
if not j_check or j_check["status"] == "cancelled":
|
if not j_check or j_check["status"] == "cancelled":
|
||||||
return
|
return
|
||||||
if chunk.get("done"):
|
|
||||||
ollama_final = chunk
|
|
||||||
break
|
|
||||||
|
|
||||||
answer = acc_response
|
acc_thinking, answer = _split_thinking(acc)
|
||||||
|
|
||||||
j = _job_get(job_id)
|
j = _job_get(job_id)
|
||||||
if not j or j["status"] == "cancelled":
|
if not j or j["status"] == "cancelled":
|
||||||
@@ -560,7 +646,7 @@ def _process_analysis_job(job_id: int):
|
|||||||
answer,
|
answer,
|
||||||
doc["id"],
|
doc["id"],
|
||||||
prm["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(),
|
now_iso(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -667,15 +753,22 @@ self.addEventListener('fetch', (event) => {
|
|||||||
|
|
||||||
@app.get("/healthz")
|
@app.get("/healthz")
|
||||||
def 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")
|
@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:
|
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()
|
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})
|
return JSONResponse({"ok": True, "models": names})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JSONResponse({"ok": False, "error": str(e), "models": []})
|
return JSONResponse({"ok": False, "error": str(e), "models": []})
|
||||||
@@ -683,12 +776,11 @@ def settings_models():
|
|||||||
|
|
||||||
@app.get("/settings", response_class=HTMLResponse)
|
@app.get("/settings", response_class=HTMLResponse)
|
||||||
def settings_page(msg: str = ""):
|
def settings_page(msg: str = ""):
|
||||||
current_model = get_setting("ollama_model", OLLAMA_MODEL)
|
current_model = get_setting("llm_model", _SEED_MODEL)
|
||||||
current_think = get_setting("ollama_think", "true" if OLLAMA_THINK else "false") == "true"
|
current_num_ctx = get_setting("llm_num_ctx", "auto")
|
||||||
current_num_ctx = get_setting("ollama_num_ctx", "auto")
|
current_num_predict = get_setting("llm_num_predict", str(_SEED_NUM_PREDICT))
|
||||||
current_num_predict = get_setting("ollama_num_predict", str(OLLAMA_NUM_PREDICT))
|
current_repeat_penalty = get_setting("llm_repeat_penalty", "1.15")
|
||||||
current_repeat_penalty = get_setting("ollama_repeat_penalty", "1.15")
|
current_repeat_last_n = get_setting("llm_repeat_last_n", "128")
|
||||||
current_repeat_last_n = get_setting("ollama_repeat_last_n", "128")
|
|
||||||
|
|
||||||
smtp_host = get_setting("smtp_host", "")
|
smtp_host = get_setting("smtp_host", "")
|
||||||
smtp_port = get_setting("smtp_port", "587")
|
smtp_port = get_setting("smtp_port", "587")
|
||||||
@@ -699,8 +791,25 @@ def settings_page(msg: str = ""):
|
|||||||
smtp_tls_checked = "checked" if smtp_tls else ""
|
smtp_tls_checked = "checked" if smtp_tls else ""
|
||||||
|
|
||||||
with db() as c:
|
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()
|
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(
|
email_addr_rows = "".join(
|
||||||
f"""<tr>
|
f"""<tr>
|
||||||
<td>{a["label"]}</td>
|
<td>{a["label"]}</td>
|
||||||
@@ -715,7 +824,9 @@ def settings_page(msg: str = ""):
|
|||||||
for a in email_addrs
|
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"),
|
ctx_options = [("auto", "Automatisch (empfohlen)"), ("4096", "4 096"), ("8192", "8 192"),
|
||||||
("16384", "16 384"), ("32768", "32 768"), ("65536", "65 536"), ("131072", "131 072")]
|
("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>
|
<h2 class='h4 mb-0'>Einstellungen</h2>
|
||||||
</div>
|
</div>
|
||||||
{f"<div class='alert alert-success py-2'>{msg}</div>" if msg else ""}
|
{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'>
|
<form method='post' action='/settings'>
|
||||||
<div class='row g-3'>
|
<div class='row g-3'>
|
||||||
|
|
||||||
<div class='col-12'>
|
<div class='col-12'>
|
||||||
<label class='form-label'>Modell</label>
|
<label class='form-label'>Standard-Modell</label>
|
||||||
<div class='input-group'>
|
<div class='input-group'>
|
||||||
<input id='modelInput' class='form-control font-monospace' name='ollama_model'
|
<select id='providerSelect' class='form-select' style='max-width:220px' onchange='updateProviderForLoad()'>
|
||||||
value='{current_model}' placeholder='z.B. qwen3.5:9b' required>
|
{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()'>
|
<button type='button' class='btn btn-outline-secondary' id='loadModelsBtn' onclick='loadModels()'>
|
||||||
<i class='bi bi-arrow-clockwise' id='loadIcon'></i> Modelle laden
|
<i class='bi bi-arrow-clockwise' id='loadIcon'></i> Modelle laden
|
||||||
</button>
|
</button>
|
||||||
@@ -751,38 +894,28 @@ def settings_page(msg: str = ""):
|
|||||||
|
|
||||||
<div class='col-12 col-md-4'>
|
<div class='col-12 col-md-4'>
|
||||||
<label class='form-label'>Kontextgröße (num_ctx)</label>
|
<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 class='form-text'>Automatisch wählt passend zur Promptgröße.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class='col-12 col-md-4'>
|
<div class='col-12 col-md-4'>
|
||||||
<label class='form-label'>Max. Ausgabe-Tokens (num_predict)</label>
|
<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}'>
|
value='{current_num_predict}'>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class='col-12 col-md-2'>
|
<div class='col-12 col-md-2'>
|
||||||
<label class='form-label'>Repeat Penalty</label>
|
<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}'>
|
value='{current_repeat_penalty}'>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class='col-12 col-md-2'>
|
<div class='col-12 col-md-2'>
|
||||||
<label class='form-label'>Repeat Last N</label>
|
<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}'>
|
value='{current_repeat_last_n}'>
|
||||||
</div>
|
</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>
|
||||||
<div class='mt-3'>
|
<div class='mt-3'>
|
||||||
<button class='btn btn-primary' type='submit'><i class='bi bi-check2-circle'></i> Speichern</button>
|
<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'>
|
<table class='table table-sm mb-0'>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td class='text-secondary' style='width:220px'>API_BASE</td><td><code>{API_BASE}</code></td></tr>
|
<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>
|
<tr><td class='text-secondary'>DB_PATH</td><td><code>{DB_PATH}</code></td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
let _loadProviderId = null;
|
||||||
|
function updateProviderForLoad() {{
|
||||||
|
_loadProviderId = document.getElementById('providerSelect')?.value || null;
|
||||||
|
document.getElementById('modelSelectWrap').classList.add('d-none');
|
||||||
|
}}
|
||||||
|
|
||||||
async function loadModels() {{
|
async function loadModels() {{
|
||||||
const btn = document.getElementById('loadModelsBtn');
|
const btn = document.getElementById('loadModelsBtn');
|
||||||
const icon = document.getElementById('loadIcon');
|
const icon = document.getElementById('loadIcon');
|
||||||
@@ -866,11 +1004,12 @@ async function loadModels() {{
|
|||||||
const inp = document.getElementById('modelInput');
|
const inp = document.getElementById('modelInput');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
icon.className = 'bi bi-hourglass-split';
|
icon.className = 'bi bi-hourglass-split';
|
||||||
|
const pid = _loadProviderId || document.getElementById('providerSelect')?.value || '';
|
||||||
try {{
|
try {{
|
||||||
const res = await fetch('/settings/models');
|
const res = await fetch('/settings/models' + (pid ? '?provider_id=' + pid : ''));
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (!data.ok || !data.models.length) {{
|
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;
|
return;
|
||||||
}}
|
}}
|
||||||
const current = inp.value.trim();
|
const current = inp.value.trim();
|
||||||
@@ -897,19 +1036,17 @@ async function loadModels() {{
|
|||||||
|
|
||||||
@app.post("/settings", response_class=HTMLResponse)
|
@app.post("/settings", response_class=HTMLResponse)
|
||||||
def settings_save(
|
def settings_save(
|
||||||
ollama_model: str = Form(...),
|
llm_model: str = Form(...),
|
||||||
ollama_think: Optional[str] = Form(None),
|
llm_num_ctx: str = Form("auto"),
|
||||||
ollama_num_ctx: str = Form("auto"),
|
llm_num_predict: str = Form("16384"),
|
||||||
ollama_num_predict: str = Form("16384"),
|
llm_repeat_penalty: str = Form("1.15"),
|
||||||
ollama_repeat_penalty: str = Form("1.15"),
|
llm_repeat_last_n: str = Form("128"),
|
||||||
ollama_repeat_last_n: str = Form("128"),
|
|
||||||
):
|
):
|
||||||
set_setting("ollama_model", ollama_model.strip())
|
set_setting("llm_model", llm_model.strip())
|
||||||
set_setting("ollama_think", "true" if ollama_think == "true" else "false")
|
set_setting("llm_num_ctx", llm_num_ctx.strip())
|
||||||
set_setting("ollama_num_ctx", ollama_num_ctx.strip())
|
set_setting("llm_num_predict", str(int(float(llm_num_predict))))
|
||||||
set_setting("ollama_num_predict", str(int(float(ollama_num_predict))))
|
set_setting("llm_repeat_penalty", str(float(llm_repeat_penalty)))
|
||||||
set_setting("ollama_repeat_penalty", str(float(ollama_repeat_penalty)))
|
set_setting("llm_repeat_last_n", str(int(float(llm_repeat_last_n))))
|
||||||
set_setting("ollama_repeat_last_n", str(int(float(ollama_repeat_last_n))))
|
|
||||||
return RedirectResponse("/settings?msg=Einstellungen+gespeichert.", status_code=303)
|
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)
|
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)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
def upload_page(msg: str = ""):
|
def upload_page(msg: str = ""):
|
||||||
projects = get_projects()
|
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'>")
|
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"]
|
pid = p["id"]
|
||||||
use_default = int(p["llm_use_default"]) if p["llm_use_default"] is not None else 1
|
use_default = int(p["llm_use_default"]) if p["llm_use_default"] is not None else 1
|
||||||
m = p["llm_model"] or def_model
|
m = p["llm_model"] or def_model
|
||||||
th = p["llm_think"] or def_think
|
|
||||||
nc = p["llm_num_ctx"] or def_num_ctx
|
nc = p["llm_num_ctx"] or def_num_ctx
|
||||||
np_ = p["llm_num_predict"] or def_num_predict
|
np_ = p["llm_num_predict"] or def_num_predict
|
||||||
rp = p["llm_repeat_penalty"] or def_repeat_penalty
|
rp = p["llm_repeat_penalty"] or def_repeat_penalty
|
||||||
rn = p["llm_repeat_last_n"] or def_repeat_last_n
|
rn = p["llm_repeat_last_n"] or def_repeat_last_n
|
||||||
|
cur_provider_id = p.get("provider_id")
|
||||||
disabled = "disabled" if use_default else ""
|
disabled = "disabled" if use_default else ""
|
||||||
opacity = "opacity:.45;pointer-events:none" 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"),
|
ctx_opts = [("auto","Automatisch"), ("4096","4 096"), ("8192","8 192"), ("16384","16 384"),
|
||||||
("32768","32 768"), ("65536","65 536"), ("131072","131 072")]
|
("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)
|
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 (
|
return (
|
||||||
f"<hr class='my-3'>"
|
f"<hr class='my-3'>"
|
||||||
f"<div class='d-flex align-items-center gap-3 mb-2'>"
|
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"</div>"
|
||||||
f"<input type='hidden' id='llm_use_default_{pid}' name='llm_use_default' value='{use_default}'>"
|
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 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" <label class='form-label small text-secondary mb-1'>Modell</label>"
|
||||||
f" <div class='input-group input-group-sm'>"
|
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" <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" <i class='bi bi-arrow-clockwise'></i>"
|
||||||
f" </button>"
|
f" </button>"
|
||||||
f" </div>"
|
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" <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" <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>"
|
||||||
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>"
|
f"</div>"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1665,12 +1838,12 @@ def prompts_page():
|
|||||||
with db() as c:
|
with db() as c:
|
||||||
prompts = c.execute("SELECT * FROM prompts ORDER BY name").fetchall()
|
prompts = c.execute("SELECT * FROM prompts ORDER BY name").fetchall()
|
||||||
projects = c.execute("SELECT id,name FROM projects ORDER BY name").fetchall()
|
projects = c.execute("SELECT id,name FROM projects ORDER BY name").fetchall()
|
||||||
def_model = get_setting("ollama_model", OLLAMA_MODEL)
|
providers = c.execute("SELECT * FROM providers ORDER BY name").fetchall()
|
||||||
def_think = get_setting("ollama_think", "true" if OLLAMA_THINK else "false")
|
def_model = get_setting("llm_model", _SEED_MODEL)
|
||||||
def_num_ctx = get_setting("ollama_num_ctx", "auto")
|
def_num_ctx = get_setting("llm_num_ctx", "auto")
|
||||||
def_num_predict = get_setting("ollama_num_predict", str(OLLAMA_NUM_PREDICT))
|
def_num_predict = get_setting("llm_num_predict", str(_SEED_NUM_PREDICT))
|
||||||
def_repeat_penalty = get_setting("ollama_repeat_penalty", "1.15")
|
def_repeat_penalty = get_setting("llm_repeat_penalty", "1.15")
|
||||||
def_repeat_last_n = get_setting("ollama_repeat_last_n", "128")
|
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])
|
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"<textarea id='prompt_text_{p['id']}' class='form-control' name='prompt' style='min-height:110px; resize:vertical'>{p['prompt']}</textarea>"
|
||||||
f"</div>"
|
f"</div>"
|
||||||
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"<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-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>"
|
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 = {{
|
const _llmDefaults = {{
|
||||||
model: {json.dumps(def_model)},
|
model: {json.dumps(def_model)},
|
||||||
think: {json.dumps(def_think)},
|
|
||||||
num_ctx: {json.dumps(def_num_ctx)},
|
num_ctx: {json.dumps(def_num_ctx)},
|
||||||
num_predict: {json.dumps(def_num_predict)},
|
num_predict: {json.dumps(def_num_predict)},
|
||||||
repeat_penalty: {json.dumps(def_repeat_penalty)},
|
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);
|
const inp = document.getElementById(inputId);
|
||||||
if (!inp) return;
|
if (!inp) return;
|
||||||
const btn = inp.nextElementSibling;
|
const btn = inp.nextElementSibling;
|
||||||
if (btn) btn.disabled = true;
|
if (btn) btn.disabled = true;
|
||||||
try {{
|
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();
|
const data = await res.json();
|
||||||
if (!data.ok || !data.models.length) {{
|
if (!data.ok || !data.models.length) {{
|
||||||
alert('Keine Modelle gefunden: ' + (data.error || ''));
|
alert('Keine Modelle gefunden: ' + (data.error || ''));
|
||||||
return;
|
return;
|
||||||
}}
|
}}
|
||||||
const current = inp.value.trim();
|
|
||||||
const v = await window.uiSelect('Modell wählen',
|
const v = await window.uiSelect('Modell wählen',
|
||||||
data.models.map(m => ({{value: m, label: m}})),
|
data.models.map(m => ({{value: m, label: m}})),
|
||||||
'– Modell auswählen –'
|
'– Modell auswählen –'
|
||||||
@@ -1913,27 +2085,28 @@ def prompt_update(
|
|||||||
prompt: str = Form(...),
|
prompt: str = Form(...),
|
||||||
llm_use_default: int = Form(1),
|
llm_use_default: int = Form(1),
|
||||||
llm_model: Optional[str] = Form(None),
|
llm_model: Optional[str] = Form(None),
|
||||||
llm_think: Optional[str] = Form(None),
|
|
||||||
llm_num_ctx: Optional[str] = Form(None),
|
llm_num_ctx: Optional[str] = Form(None),
|
||||||
llm_num_predict: Optional[str] = Form(None),
|
llm_num_predict: Optional[str] = Form(None),
|
||||||
llm_repeat_penalty: Optional[str] = Form(None),
|
llm_repeat_penalty: Optional[str] = Form(None),
|
||||||
llm_repeat_last_n: Optional[str] = Form(None),
|
llm_repeat_last_n: Optional[str] = Form(None),
|
||||||
|
provider_id: Optional[int] = Form(None),
|
||||||
):
|
):
|
||||||
with db() as c:
|
with db() as c:
|
||||||
c.execute(
|
c.execute(
|
||||||
"""UPDATE prompts SET name=?, prompt=?, updated_at=?,
|
"""UPDATE prompts SET name=?, prompt=?, updated_at=?,
|
||||||
llm_use_default=?, llm_model=?, llm_think=?,
|
llm_use_default=?, llm_model=?,
|
||||||
llm_num_ctx=?, llm_num_predict=?, llm_repeat_penalty=?, llm_repeat_last_n=?
|
llm_num_ctx=?, llm_num_predict=?, llm_repeat_penalty=?, llm_repeat_last_n=?,
|
||||||
|
provider_id=?
|
||||||
WHERE id=?""",
|
WHERE id=?""",
|
||||||
(
|
(
|
||||||
name.strip(), prompt.strip(), now_iso(),
|
name.strip(), prompt.strip(), now_iso(),
|
||||||
llm_use_default,
|
llm_use_default,
|
||||||
(llm_model or "").strip() or None,
|
(llm_model or "").strip() or None,
|
||||||
"true" if llm_think == "true" else "false",
|
|
||||||
(llm_num_ctx or "auto").strip(),
|
(llm_num_ctx or "auto").strip(),
|
||||||
int(float(llm_num_predict)) if llm_num_predict else None,
|
int(float(llm_num_predict)) if llm_num_predict else None,
|
||||||
float(llm_repeat_penalty) if llm_repeat_penalty 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,
|
int(float(llm_repeat_last_n)) if llm_repeat_last_n else None,
|
||||||
|
provider_id or None,
|
||||||
id,
|
id,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user