feat(diarization-ui): per-prompt LLM settings with default/custom toggle

Each prompt can override model, thinking, num_ctx, num_predict,
repeat_penalty and repeat_last_n. Default switch uses global settings;
custom values are preserved when switching back to default.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 11:50:39 +02:00
parent 5288309971
commit 21712c972d

187
app.py
View File

@@ -130,6 +130,19 @@ def init_db():
) )
except Exception: except Exception:
pass pass
for col in [
"llm_use_default INTEGER NOT NULL DEFAULT 1",
"llm_model TEXT",
"llm_think TEXT",
"llm_num_ctx TEXT",
"llm_num_predict INTEGER",
"llm_repeat_penalty REAL",
"llm_repeat_last_n INTEGER",
]:
try:
c.execute(f"ALTER TABLE prompts ADD COLUMN {col}")
except Exception:
pass
# defaults # defaults
c.execute("INSERT OR IGNORE INTO projects(name, created_at) VALUES (?,?)", ("Default", now_iso())) c.execute("INSERT OR IGNORE INTO projects(name, created_at) VALUES (?,?)", ("Default", now_iso()))
@@ -401,17 +414,25 @@ def _process_analysis_job(job_id: int):
+ f"\\nTEXT:\\n{doc['content_md']}\\n" + f"\\nTEXT:\\n{doc['content_md']}\\n"
) )
num_ctx_setting = get_setting("ollama_num_ctx", "auto") 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"))
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) 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}") _job_set(job_id, llm_prompt=f"[num_ctx={num_ctx}]\n\n{llm_prompt}")
with _JOB_STREAM_LOCK: with _JOB_STREAM_LOCK:
_JOB_STREAMS[job_id] = {"thinking": "", "response": ""} _JOB_STREAMS[job_id] = {"thinking": "", "response": ""}
model = get_setting("ollama_model", OLLAMA_MODEL)
think = get_setting("ollama_think", "true" if OLLAMA_THINK else "false") == "true"
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"))
r = requests.post( r = requests.post(
f"{OLLAMA_BASE_URL}/api/generate", f"{OLLAMA_BASE_URL}/api/generate",
json={"model": model, "prompt": llm_prompt, "stream": True, "think": think, "options": { json={"model": model, "prompt": llm_prompt, "stream": True, "think": think, "options": {
@@ -1224,11 +1245,79 @@ 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):
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
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 ""
return (
f"<hr class='my-3'>"
f"<div class='d-flex align-items-center gap-3 mb-2'>"
f" <span class='text-secondary small fw-semibold'><i class='bi bi-robot me-1'></i>KI-Einstellungen</span>"
f" <div class='form-check form-switch mb-0'>"
f" <input class='form-check-input' type='checkbox' role='switch' id='llmdef_{pid}'"
f" onchange='toggleLlmDef({pid},this.checked)' {'checked' if use_default else ''}>"
f" <label class='form-check-label small' for='llmdef_{pid}'>Standard</label>"
f" </div>"
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" <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" <i class='bi bi-arrow-clockwise'></i>"
f" </button>"
f" </div>"
f" </div>"
f" <div class='col-6 col-md-3'>"
f" <label class='form-label small text-secondary mb-1'>Kontext</label>"
f" <select class='form-select form-select-sm' name='llm_num_ctx' {disabled}>{ctx_html}</select>"
f" </div>"
f" <div class='col-6 col-md-3'>"
f" <label class='form-label small text-secondary mb-1'>Max. Tokens</label>"
f" <input class='form-control form-control-sm' type='number' name='llm_num_predict' min='256' max='131072' step='256' value='{np_}' {disabled}>"
f" </div>"
f" <div class='col-6 col-md-3'>"
f" <label class='form-label small text-secondary mb-1'>Repeat Penalty</label>"
f" <input class='form-control form-control-sm' type='number' name='llm_repeat_penalty' min='1.0' max='2.0' step='0.01' value='{rp}' {disabled}>"
f" </div>"
f" <div class='col-6 col-md-3'>"
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>"
)
@app.get("/prompts", response_class=HTMLResponse) @app.get("/prompts", response_class=HTMLResponse)
def prompts_page(): 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)
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")
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])
@@ -1264,6 +1353,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) +
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>"
@@ -1387,6 +1477,58 @@ document.addEventListener('DOMContentLoaded', () => {{
const el = document.getElementById('promptEditorModal'); const el = document.getElementById('promptEditorModal');
if(el) el.addEventListener('hide.bs.modal', syncPromptEditorBack); if(el) el.addEventListener('hide.bs.modal', syncPromptEditorBack);
}}); }});
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)},
repeat_last_n: {json.dumps(def_repeat_last_n)},
}};
function toggleLlmDef(pid, isDefault) {{
document.getElementById('llm_use_default_' + pid).value = isDefault ? '1' : '0';
const wrap = document.getElementById('llmfields_' + pid);
const inputs = wrap.querySelectorAll('input,select,button');
if (isDefault) {{
wrap.style.opacity = '.45';
wrap.style.pointerEvents = 'none';
inputs.forEach(el => el.disabled = true);
}} else {{
wrap.style.opacity = '';
wrap.style.pointerEvents = '';
inputs.forEach(el => el.disabled = false);
// pre-fill from defaults if field is empty
const modelInp = document.getElementById('llmmodel_' + pid);
if (modelInp && !modelInp.value) modelInp.value = _llmDefaults.model;
}}
}}
async function loadModelsInto(inputId) {{
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 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 '
);
if (v) inp.value = v;
}} catch(e) {{
alert('Fehler: ' + e);
}} finally {{
if (btn) btn.disabled = false;
}}
}}
</script> </script>
""" """
return layout("Prompts & Projekte", body) return layout("Prompts & Projekte", body)
@@ -1412,9 +1554,36 @@ def prompt_add(name: str = Form(...), prompt: str = Form(...)):
@app.post("/prompts/update", response_class=HTMLResponse) @app.post("/prompts/update", response_class=HTMLResponse)
def prompt_update(id: int = Form(...), name: str = Form(...), prompt: str = Form(...)): def prompt_update(
id: int = Form(...),
name: str = Form(...),
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),
):
with db() as c: with db() as c:
c.execute("UPDATE prompts SET name=?, prompt=?, updated_at=? WHERE id=?", (name.strip(), prompt.strip(), now_iso(), id)) 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=?
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,
id,
),
)
return HTMLResponse("<meta http-equiv='refresh' content='0; url=/prompts'>") return HTMLResponse("<meta http-equiv='refresh' content='0; url=/prompts'>")