579 lines
23 KiB
Python
579 lines
23 KiB
Python
import json
|
||
import os
|
||
import sqlite3
|
||
from datetime import datetime
|
||
from typing import Optional
|
||
|
||
import markdown as md
|
||
import requests
|
||
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
|
||
from fastapi.responses import HTMLResponse, PlainTextResponse, Response
|
||
|
||
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")
|
||
DB_PATH = os.getenv("DB_PATH", "/data/ui.db")
|
||
|
||
app = FastAPI(title="Diarization UI")
|
||
|
||
|
||
def db():
|
||
conn = sqlite3.connect(DB_PATH)
|
||
conn.row_factory = sqlite3.Row
|
||
return conn
|
||
|
||
|
||
def now_iso() -> str:
|
||
return datetime.utcnow().isoformat()
|
||
|
||
|
||
def init_db():
|
||
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
||
with db() as c:
|
||
c.execute(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS projects (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name TEXT UNIQUE NOT NULL,
|
||
created_at TEXT NOT NULL
|
||
)
|
||
"""
|
||
)
|
||
c.execute(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS prompts (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name TEXT UNIQUE NOT NULL,
|
||
prompt TEXT NOT NULL,
|
||
created_at TEXT NOT NULL,
|
||
updated_at TEXT NOT NULL
|
||
)
|
||
"""
|
||
)
|
||
c.execute(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS documents (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
project_id INTEGER NOT NULL,
|
||
kind TEXT NOT NULL, -- transcript|analysis
|
||
title TEXT NOT NULL,
|
||
content_md TEXT NOT NULL,
|
||
source_document_id INTEGER,
|
||
prompt_id INTEGER,
|
||
raw_json TEXT,
|
||
created_at TEXT NOT NULL,
|
||
FOREIGN KEY(project_id) REFERENCES projects(id),
|
||
FOREIGN KEY(source_document_id) REFERENCES documents(id),
|
||
FOREIGN KEY(prompt_id) REFERENCES prompts(id)
|
||
)
|
||
"""
|
||
)
|
||
|
||
# defaults
|
||
c.execute("INSERT OR IGNORE INTO projects(name, created_at) VALUES (?,?)", ("Default", now_iso()))
|
||
c.execute(
|
||
"INSERT OR IGNORE INTO prompts(name, prompt, created_at, updated_at) VALUES (?,?,?,?)",
|
||
(
|
||
"Zusammenfassung",
|
||
"Erstelle eine prägnante Zusammenfassung des Gesprächs in Stichpunkten.",
|
||
now_iso(),
|
||
now_iso(),
|
||
),
|
||
)
|
||
c.execute(
|
||
"INSERT OR IGNORE INTO prompts(name, prompt, created_at, updated_at) VALUES (?,?,?,?)",
|
||
(
|
||
"Aufgaben",
|
||
"Extrahiere alle Aufgaben. Gib pro Aufgabe: Verantwortlich, Aufgabe, Deadline (falls vorhanden), Priorität.",
|
||
now_iso(),
|
||
now_iso(),
|
||
),
|
||
)
|
||
|
||
|
||
def layout(title: str, body: str) -> str:
|
||
return f"""
|
||
<!doctype html>
|
||
<html>
|
||
<head>
|
||
<meta charset='utf-8'>
|
||
<meta name='viewport' content='width=device-width, initial-scale=1, viewport-fit=cover'>
|
||
<meta name='theme-color' content='#0f172a'>
|
||
<link rel='manifest' href='/manifest.webmanifest'>
|
||
<link rel='icon' href='/icon.svg' type='image/svg+xml'>
|
||
<title>{title}</title>
|
||
<style>
|
||
:root{{--bg:#0b1020;--bg2:#111827;--card:#0f172a;--txt:#e5e7eb;--muted:#94a3b8;--acc:#22d3ee;--acc2:#38bdf8;--ok:#34d399;--border:#1f2937}}
|
||
*{{box-sizing:border-box}}
|
||
body{{font-family:Inter,system-ui,Arial;margin:0;background:linear-gradient(180deg,var(--bg),#020617);color:var(--txt);display:flex;min-height:100vh}}
|
||
nav{{width:250px;background:rgba(15,23,42,.92);backdrop-filter:blur(8px);border-right:1px solid var(--border);padding:16px;position:sticky;top:0;height:100vh}}
|
||
.brand{{font-weight:700;letter-spacing:.3px;margin:0 0 12px 0}}
|
||
nav a{{display:flex;gap:10px;align-items:center;color:var(--txt);text-decoration:none;padding:10px 12px;border-radius:12px;margin:6px 0;border:1px solid transparent}}
|
||
nav a:hover{{background:#0b1222;border-color:#243244}}
|
||
main{{flex:1;padding:18px;max-width:1200px}}
|
||
.card{{background:rgba(15,23,42,.88);border:1px solid var(--border);border-radius:16px;padding:14px;margin:10px 0;box-shadow:0 8px 30px rgba(0,0,0,.25)}}
|
||
input,select,textarea,button{{padding:10px 12px;font-size:14px;border-radius:12px;border:1px solid #334155;background:#0b1222;color:var(--txt)}}
|
||
button{{background:linear-gradient(90deg,var(--acc),var(--acc2));color:#001018;border:none;font-weight:700}}
|
||
button:hover{{filter:brightness(1.05)}}
|
||
textarea{{width:100%;min-height:150px}}
|
||
pre{{white-space:pre-wrap;background:#020617;color:#86efac;padding:12px;border-radius:12px;border:1px solid #1e293b}}
|
||
.mdview{{line-height:1.55}}
|
||
.mdview h1,.mdview h2,.mdview h3{{margin:16px 0 8px}}
|
||
.mdview p{{margin:10px 0}}
|
||
.mdview ul,.mdview ol{{padding-left:22px}}
|
||
.mdview code{{background:#0b1222;padding:2px 5px;border-radius:6px}}
|
||
.mdview pre code{{display:block;padding:10px}}
|
||
.mdview blockquote{{border-left:3px solid #334155;padding-left:10px;color:#cbd5e1}}
|
||
.row{{display:flex;gap:8px;flex-wrap:wrap;align-items:center}}
|
||
small{{color:var(--muted)}}
|
||
.hint{{color:var(--muted);font-size:13px}}
|
||
@media (max-width:900px){{nav{{width:86px;padding:10px}} nav a span{{display:none}} .brand{{font-size:12px}} main{{padding:12px}}}}
|
||
</style>
|
||
<script>
|
||
if ('serviceWorker' in navigator) {{ window.addEventListener('load', () => navigator.serviceWorker.register('/sw.js').catch(()=>{{}})); }}
|
||
</script>
|
||
</head>
|
||
<body>
|
||
<nav>
|
||
<div class='brand'>🎙️ Audio Copilot</div>
|
||
<a href='/'><span>⤴️</span><span>Upload</span></a>
|
||
<a href='/library'><span>🗂️</span><span>Datenbank</span></a>
|
||
<a href='/prompts'><span>🧩</span><span>Prompts</span></a>
|
||
<a href='/run'><span>🤖</span><span>Prompt ausführen</span></a>
|
||
<a href='/healthz'><span>💚</span><span>Health</span></a>
|
||
</nav>
|
||
<main>{body}</main>
|
||
</body></html>
|
||
"""
|
||
|
||
|
||
def get_projects():
|
||
with db() as c:
|
||
return c.execute("SELECT id,name FROM projects ORDER BY name").fetchall()
|
||
|
||
|
||
def get_project_name(project_id: int) -> str:
|
||
with db() as c:
|
||
r = c.execute("SELECT name FROM projects WHERE id=?", (project_id,)).fetchone()
|
||
return r[0] if r else ""
|
||
|
||
|
||
def get_prompts():
|
||
with db() as c:
|
||
return c.execute("SELECT id,name,prompt FROM prompts ORDER BY name").fetchall()
|
||
|
||
|
||
@app.on_event("startup")
|
||
def startup():
|
||
init_db()
|
||
|
||
|
||
@app.get("/manifest.webmanifest")
|
||
def manifest():
|
||
return {
|
||
"name": "Audio Copilot",
|
||
"short_name": "Copilot",
|
||
"start_url": "/",
|
||
"display": "standalone",
|
||
"background_color": "#020617",
|
||
"theme_color": "#0f172a",
|
||
"icons": [
|
||
{"src": "/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any maskable"}
|
||
],
|
||
}
|
||
|
||
|
||
@app.get("/icon.svg")
|
||
def icon_svg():
|
||
svg = """<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 128 128'><rect rx='24' width='128' height='128' fill='#0f172a'/><text x='64' y='82' font-size='72' text-anchor='middle'>🎙️</text></svg>"""
|
||
return Response(content=svg, media_type="image/svg+xml")
|
||
|
||
|
||
@app.get("/sw.js")
|
||
def sw_js():
|
||
js = """
|
||
self.addEventListener('install', (event) => { self.skipWaiting(); });
|
||
self.addEventListener('activate', (event) => { event.waitUntil(clients.claim()); });
|
||
self.addEventListener('fetch', (event) => {
|
||
if (event.request.method !== 'GET') return;
|
||
event.respondWith(fetch(event.request).catch(() => new Response('offline', {status: 503})));
|
||
});
|
||
"""
|
||
return Response(content=js, media_type="application/javascript")
|
||
|
||
|
||
@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}
|
||
|
||
|
||
@app.get("/", response_class=HTMLResponse)
|
||
def upload_page(msg: str = ""):
|
||
projects = get_projects()
|
||
opts = "".join([f"<option value='{p['id']}'>{p['name']}</option>" for p in projects])
|
||
body = f"""
|
||
<h2>Audio Upload</h2>
|
||
<p>Audio wird transkribiert + mit Sprechern angereichert und als Dokument gespeichert.</p>
|
||
{f"<p><b>{msg}</b></p>" if msg else ""}
|
||
<form action='/upload' method='post' enctype='multipart/form-data' class='card'>
|
||
<div class='row'>
|
||
<label>Projekt:</label>
|
||
<select name='project_id'>{opts}</select>
|
||
<input name='title' placeholder='Titel (optional)'>
|
||
</div>
|
||
<div class='row' style='margin-top:8px'>
|
||
<input type='file' name='file' accept='audio/*' required>
|
||
<button type='submit'>Verarbeiten & speichern</button>
|
||
</div>
|
||
</form>
|
||
"""
|
||
return layout("Upload", body)
|
||
|
||
|
||
@app.post("/projects", response_class=HTMLResponse)
|
||
def add_project(name: str = Form(...)):
|
||
with db() as c:
|
||
c.execute("INSERT INTO projects(name, created_at) VALUES (?,?)", (name.strip(), now_iso()))
|
||
return HTMLResponse("<meta http-equiv='refresh' content='0; url=/prompts'>")
|
||
|
||
|
||
@app.post("/projects/update", response_class=HTMLResponse)
|
||
def rename_project(id: int = Form(...), name: str = Form(...)):
|
||
with db() as c:
|
||
c.execute("UPDATE projects SET name=? WHERE id=?", (name.strip(), id))
|
||
return HTMLResponse("<meta http-equiv='refresh' content='0; url=/prompts'>")
|
||
|
||
|
||
@app.post("/projects/{project_id}/delete", response_class=HTMLResponse)
|
||
def delete_project(project_id: int):
|
||
with db() as c:
|
||
default = c.execute("SELECT id FROM projects WHERE name='Default'").fetchone()
|
||
if not default:
|
||
c.execute("INSERT INTO projects(name, created_at) VALUES (?,?)", ("Default", now_iso()))
|
||
default_id = c.execute("SELECT id FROM projects WHERE name='Default'").fetchone()[0]
|
||
else:
|
||
default_id = default[0]
|
||
if project_id == default_id:
|
||
return HTMLResponse("<meta http-equiv='refresh' content='0; url=/prompts'>")
|
||
c.execute("UPDATE documents SET project_id=? WHERE project_id=?", (default_id, project_id))
|
||
c.execute("DELETE FROM projects WHERE id=?", (project_id,))
|
||
return HTMLResponse("<meta http-equiv='refresh' content='0; url=/prompts'>")
|
||
|
||
|
||
@app.post("/upload", response_class=HTMLResponse)
|
||
async def upload(project_id: int = Form(...), title: str = Form(""), file: UploadFile = File(...)):
|
||
data = await file.read()
|
||
if not data:
|
||
raise HTTPException(400, "Leere Datei")
|
||
|
||
files = {"file": (file.filename or "audio.bin", data, file.content_type or "application/octet-stream")}
|
||
r = requests.post(f"{API_BASE}/transcribe-diarize", files=files, timeout=1800)
|
||
if r.status_code >= 400:
|
||
raise HTTPException(r.status_code, r.text)
|
||
|
||
payload = r.json()
|
||
content_md = payload.get("formatted_text", "")
|
||
doc_title = (title or "").strip() or (file.filename or "Transkript")
|
||
|
||
with db() as c:
|
||
cur = c.execute(
|
||
"INSERT INTO documents(project_id, kind, title, content_md, raw_json, created_at) VALUES (?,?,?,?,?,?)",
|
||
(project_id, "transcript", doc_title, content_md, json.dumps(payload, ensure_ascii=False), now_iso()),
|
||
)
|
||
doc_id = cur.lastrowid
|
||
|
||
return HTMLResponse(f"<meta http-equiv='refresh' content='0; url=/document/{doc_id}'>")
|
||
|
||
|
||
@app.get("/library", response_class=HTMLResponse)
|
||
def library(project_id: Optional[int] = None):
|
||
with db() as c:
|
||
projects = c.execute("SELECT id,name FROM projects ORDER BY name").fetchall()
|
||
if project_id:
|
||
docs = c.execute(
|
||
"""
|
||
SELECT d.id,d.kind,d.title,d.created_at,p.name AS project
|
||
FROM documents d JOIN projects p ON p.id=d.project_id
|
||
WHERE d.project_id=? ORDER BY d.id DESC
|
||
""",
|
||
(project_id,),
|
||
).fetchall()
|
||
else:
|
||
docs = c.execute(
|
||
"""
|
||
SELECT d.id,d.kind,d.title,d.created_at,p.name AS project
|
||
FROM documents d JOIN projects p ON p.id=d.project_id
|
||
ORDER BY d.id DESC LIMIT 200
|
||
"""
|
||
).fetchall()
|
||
|
||
p_opts = "<option value=''>Alle</option>" + "".join(
|
||
[f"<option value='{p['id']}' {'selected' if project_id==p['id'] else ''}>{p['name']}</option>" for p in projects]
|
||
)
|
||
proj_opts = "".join([f"<option value='{p['id']}'>{p['name']}</option>" for p in projects])
|
||
items = "".join(
|
||
[
|
||
f"<div class='card'><b>#{d['id']}</b> [{d['kind']}] {d['title']}<br><small>{d['project']} · {d['created_at']}</small><br>"
|
||
f"<div class='row' style='margin-top:8px'>"
|
||
f"<a href='/document/{d['id']}' style='text-decoration:none'><button type='button'>👁️ Ansehen</button></a>"
|
||
f"<a href='/document/{d['id']}/download.md' style='text-decoration:none'><button type='button'>⬇️ .md</button></a>"
|
||
f"</div>"
|
||
f"<form method='post' action='/document/{d['id']}/rename' class='row' style='margin-top:8px'>"
|
||
f"<input name='title' value='{d['title']}'><button>✏️ Umbenennen</button></form>"
|
||
f"<form method='post' action='/document/{d['id']}/move' class='row' style='margin-top:6px'>"
|
||
f"<select name='project_id'>{proj_opts}</select><button>📁 Verschieben</button></form>"
|
||
f"<form method='post' action='/document/{d['id']}/delete' onsubmit=\"return confirm('Dokument löschen?')\" style='margin-top:6px'>"
|
||
f"<button>🗑️ Löschen</button></form></div>"
|
||
for d in docs
|
||
]
|
||
)
|
||
body = f"""
|
||
<h2>Datenbank / Dokumente</h2>
|
||
<form method='get' class='row card'>
|
||
<label>Projekt:</label>
|
||
<select name='project_id'>{p_opts}</select>
|
||
<button type='submit'>Filtern</button>
|
||
</form>
|
||
{items or '<p>Keine Einträge.</p>'}
|
||
"""
|
||
return layout("Library", body)
|
||
|
||
|
||
@app.get("/document/{doc_id}", response_class=HTMLResponse)
|
||
def view_document(doc_id: int):
|
||
with db() as c:
|
||
d = c.execute(
|
||
"""
|
||
SELECT d.*, p.name AS project, pr.name AS prompt_name
|
||
FROM documents d
|
||
JOIN projects p ON p.id=d.project_id
|
||
LEFT JOIN prompts pr ON pr.id=d.prompt_id
|
||
WHERE d.id=?
|
||
""",
|
||
(doc_id,),
|
||
).fetchone()
|
||
if not d:
|
||
raise HTTPException(404, "not found")
|
||
|
||
rendered = md.markdown(d["content_md"] or "", extensions=["fenced_code", "tables", "nl2br"])
|
||
projects = get_projects()
|
||
project_list = "\\n".join([f"{p['id']}: {p['name']}" for p in projects])
|
||
|
||
body = f"""
|
||
<h2>Dokument #{d['id']} – {d['title']}</h2>
|
||
<p>
|
||
<small>Projekt: {d['project']} · Typ: {d['kind']} · {d['created_at']}</small>
|
||
|
||
<a title='Download .md' href='/document/{doc_id}/download.md' style='text-decoration:none'>⬇️</a>
|
||
<a title='Umbenennen' href='#' onclick='renameDoc();return false;' style='text-decoration:none'>✏️</a>
|
||
<a title='Verschieben' href='#' onclick='moveDoc();return false;' style='text-decoration:none'>📁</a>
|
||
<a title='Löschen' href='#' onclick='deleteDoc();return false;' style='text-decoration:none'>🗑️</a>
|
||
</p>
|
||
<div class='card mdview'>{rendered}</div>
|
||
|
||
<script>
|
||
window.postForm = async function(url, data) {{
|
||
const body = new URLSearchParams(data);
|
||
const r = await fetch(url, {{method:'POST', headers:{{'Content-Type':'application/x-www-form-urlencoded'}}, body}});
|
||
if (!r.ok) {{ alert('Fehler: '+r.status); return; }}
|
||
location.href = '/library';
|
||
}};
|
||
window.renameDoc = function() {{
|
||
const v = prompt('Neuer Dokumentname:', {json.dumps(d['title'])});
|
||
if (v===null) return;
|
||
window.postForm('/document/{doc_id}/rename', {{title:v}});
|
||
}};
|
||
window.moveDoc = function() {{
|
||
const info = {json.dumps(project_list)};
|
||
const v = prompt('Ziel-Projekt-ID eingeben\\n'+info, '');
|
||
if (v===null) return;
|
||
window.postForm('/document/{doc_id}/move', {{project_id:v}});
|
||
}};
|
||
window.deleteDoc = function() {{
|
||
if (!confirm('Dokument wirklich löschen?')) return;
|
||
window.postForm('/document/{doc_id}/delete', {{}});
|
||
}};
|
||
</script>
|
||
"""
|
||
return layout("Dokument", body)
|
||
|
||
|
||
@app.get("/document/{doc_id}/download.md", response_class=PlainTextResponse)
|
||
def download_md(doc_id: int):
|
||
with db() as c:
|
||
d = c.execute("SELECT title,content_md FROM documents WHERE id=?", (doc_id,)).fetchone()
|
||
if not d:
|
||
raise HTTPException(404, "not found")
|
||
|
||
base = (d["title"] or f"document_{doc_id}").strip()
|
||
safe = "".join(ch if ch.isalnum() or ch in ("-", "_", " ") else "_" for ch in base).strip()
|
||
safe = safe.replace(" ", "_") or f"document_{doc_id}"
|
||
filename = f"{safe}.md"
|
||
|
||
return PlainTextResponse(
|
||
d["content_md"],
|
||
headers={"Content-Disposition": f"attachment; filename={filename}"},
|
||
)
|
||
|
||
|
||
@app.post("/document/{doc_id}/rename", response_class=HTMLResponse)
|
||
def rename_document(doc_id: int, title: str = Form(...)):
|
||
with db() as c:
|
||
c.execute("UPDATE documents SET title=? WHERE id=?", (title.strip(), doc_id))
|
||
return HTMLResponse("<meta http-equiv='refresh' content='0; url=/library'>")
|
||
|
||
|
||
@app.post("/document/{doc_id}/move", response_class=HTMLResponse)
|
||
def move_document(doc_id: int, project_id: int = Form(...)):
|
||
with db() as c:
|
||
c.execute("UPDATE documents SET project_id=? WHERE id=?", (project_id, doc_id))
|
||
return HTMLResponse("<meta http-equiv='refresh' content='0; url=/library'>")
|
||
|
||
|
||
@app.post("/document/{doc_id}/delete", response_class=HTMLResponse)
|
||
def delete_document(doc_id: int):
|
||
with db() as c:
|
||
c.execute("DELETE FROM documents WHERE id=?", (doc_id,))
|
||
return HTMLResponse("<meta http-equiv='refresh' content='0; url=/library'>")
|
||
|
||
|
||
@app.get("/prompts", response_class=HTMLResponse)
|
||
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()
|
||
|
||
p_list = "".join(
|
||
[
|
||
f"<div class='card'><b>{p['name']}</b><pre>{(p['prompt'] or '').replace('<','<')}</pre>"
|
||
f"<form method='post' action='/prompts/update'><input type='hidden' name='id' value='{p['id']}'><input name='name' value='{p['name']}'><br><textarea name='prompt'>{p['prompt']}</textarea><br><button>Speichern</button></form>"
|
||
f"<form method='post' action='/prompts/{p['id']}/delete' onsubmit=\"return confirm('Prompt löschen?')\" style='margin-top:6px'><button>Löschen</button></form>"
|
||
f"</div>"
|
||
for p in prompts
|
||
]
|
||
)
|
||
project_opts = "".join([f"<option value='{p['name']}'>{p['name']}</option>" for p in projects])
|
||
project_list = "".join([
|
||
f"<div class='card'><b>{p['name']}</b>"
|
||
f"<form method='post' action='/projects/update' class='row' style='margin-top:6px'>"
|
||
f"<input type='hidden' name='id' value='{p['id']}'><input name='name' value='{p['name']}'><button>Umbenennen</button></form>"
|
||
f"<form method='post' action='/projects/{p['id']}/delete' onsubmit=\"return confirm('Projekt löschen? Dokumente werden auf Default verschoben.')\" style='margin-top:6px'><button>Löschen</button></form>"
|
||
f"</div>" for p in projects
|
||
])
|
||
|
||
body = f"""
|
||
<h2>Prompt-Konfiguration</h2>
|
||
<div class='card'>
|
||
<form method='post' action='/prompts/add'>
|
||
<h4>Neuer Prompt</h4>
|
||
<input name='name' placeholder='Name' required>
|
||
<br><textarea name='prompt' placeholder='Prompttext' required></textarea>
|
||
<br><button type='submit'>Anlegen</button>
|
||
</form>
|
||
</div>
|
||
<div class='card'>
|
||
<form method='post' action='/projects'>
|
||
<h4>Neues Projekt</h4>
|
||
<input name='name' list='projectNames' placeholder='Projektname' required>
|
||
<datalist id='projectNames'>{project_opts}</datalist>
|
||
<button type='submit'>Anlegen</button>
|
||
</form>
|
||
</div>
|
||
<h3>Projekte</h3>
|
||
{project_list}
|
||
<h3>Prompts</h3>
|
||
{p_list}
|
||
"""
|
||
return layout("Prompts", body)
|
||
|
||
|
||
@app.post("/prompts/add", response_class=HTMLResponse)
|
||
def prompt_add(name: str = Form(...), prompt: str = Form(...)):
|
||
with db() as c:
|
||
c.execute(
|
||
"INSERT INTO prompts(name,prompt,created_at,updated_at) VALUES (?,?,?,?)",
|
||
(name.strip(), prompt.strip(), now_iso(), now_iso()),
|
||
)
|
||
return HTMLResponse("<meta http-equiv='refresh' content='0; url=/prompts'>")
|
||
|
||
|
||
@app.post("/prompts/update", response_class=HTMLResponse)
|
||
def prompt_update(id: int = Form(...), name: str = Form(...), prompt: str = Form(...)):
|
||
with db() as c:
|
||
c.execute("UPDATE prompts SET name=?, prompt=?, updated_at=? WHERE id=?", (name.strip(), prompt.strip(), now_iso(), id))
|
||
return HTMLResponse("<meta http-equiv='refresh' content='0; url=/prompts'>")
|
||
|
||
|
||
@app.post("/prompts/{prompt_id}/delete", response_class=HTMLResponse)
|
||
def prompt_delete(prompt_id: int):
|
||
with db() as c:
|
||
c.execute("DELETE FROM prompts WHERE id=?", (prompt_id,))
|
||
return HTMLResponse("<meta http-equiv='refresh' content='0; url=/prompts'>")
|
||
|
||
|
||
@app.get("/run", response_class=HTMLResponse)
|
||
def run_page():
|
||
with db() as c:
|
||
docs = c.execute("SELECT id,title,kind,created_at FROM documents ORDER BY id DESC LIMIT 200").fetchall()
|
||
prompts = c.execute("SELECT id,name FROM prompts ORDER BY name").fetchall()
|
||
|
||
d_opts = "".join([f"<option value='{d['id']}'>#{d['id']} [{d['kind']}] {d['title']}</option>" for d in docs])
|
||
p_opts = "".join([f"<option value='{p['id']}'>{p['name']}</option>" for p in prompts])
|
||
|
||
body = f"""
|
||
<h2>Prompt ausführen</h2>
|
||
<form method='post' action='/run' class='card'>
|
||
<label>Dokument:</label><br>
|
||
<select name='document_id' style='width:100%'>{d_opts}</select><br><br>
|
||
<label>Prompt:</label><br>
|
||
<select name='prompt_id' style='width:100%'>{p_opts}</select><br><br>
|
||
<button type='submit'>Ausführen (Qwen)</button>
|
||
</form>
|
||
"""
|
||
return layout("Run", body)
|
||
|
||
|
||
@app.post("/run", response_class=HTMLResponse)
|
||
def run_prompt(document_id: int = Form(...), prompt_id: int = Form(...)):
|
||
with db() as c:
|
||
doc = c.execute("SELECT * FROM documents WHERE id=?", (document_id,)).fetchone()
|
||
prm = c.execute("SELECT * FROM prompts WHERE id=?", (prompt_id,)).fetchone()
|
||
if not doc or not prm:
|
||
raise HTTPException(404, "Dokument oder Prompt nicht gefunden")
|
||
|
||
llm_prompt = (
|
||
"Du bist ein präziser Assistent. Antworte auf Deutsch.\n"
|
||
f"AUFTRAG:\n{prm['prompt']}\n\n"
|
||
f"TEXT:\n{doc['content_md']}\n"
|
||
)
|
||
|
||
r = requests.post(
|
||
f"{OLLAMA_BASE_URL}/api/generate",
|
||
json={"model": OLLAMA_MODEL, "prompt": llm_prompt, "stream": False},
|
||
timeout=1200,
|
||
)
|
||
if r.status_code >= 400:
|
||
raise HTTPException(r.status_code, r.text)
|
||
answer = r.json().get("response", "")
|
||
|
||
with db() as c:
|
||
cur = c.execute(
|
||
"""
|
||
INSERT INTO documents(project_id, kind, title, content_md, source_document_id, prompt_id, raw_json, created_at)
|
||
VALUES (?,?,?,?,?,?,?,?)
|
||
""",
|
||
(
|
||
doc["project_id"],
|
||
"analysis",
|
||
f"Analyse: {prm['name']} · {doc['title']}",
|
||
answer,
|
||
doc["id"],
|
||
prm["id"],
|
||
json.dumps({"ollama_response": r.json()}, ensure_ascii=False),
|
||
now_iso(),
|
||
),
|
||
)
|
||
new_id = cur.lastrowid
|
||
|
||
return HTMLResponse(f"<meta http-equiv='refresh' content='0; url=/document/{new_id}'>")
|