mirror of
https://github.com/Jeuners/Logpy-AgentOne.git
synced 2026-09-09 15:02:33 +02:00
feat: Bewertungssystem (👍/👎) als Trainingsdaten-Export
Jede Karte im Leitstand bekommt Daumen hoch/runter fuer die Kategorisierung. Bei 👎 optional Korrektur (richtige Kategorie/Dringlichkeit). Gespeichert als bewertung.json pro Anruf-Ordner (gleiches Muster wie stapel.json, pipe/bewertung.py). Export als JSONL (pipe/export.py, neuer "📤 Bewertungen exportieren"-Knopf + automatisch beim "Tag archivieren"): ein Datensatz pro bewertetem Anruf mit Transkript, Modell-Ausgabe, Backend/Modell, Bewertung und Korrektur - fuer externe Auswertung (z.B. um Kategorisierungs-Fehler systematisch statt zufaellig zu finden, wie das Notfall-Sicherheitsnetz vom letzten Fund). Export-Knopf verlangt Bestaetigung ("Transkripte und Namen verlassen den Rechner") - bewusst kein automatischer Export ohne Hinweis, das steht sonst im Widerspruch zum On-Prem-Versprechen. training/ ist neu gitignored (volle Transkripte/Namen drin, siehe README-Hinweis zu Testdaten). Nebenbei: process_call.py legt jetzt auch backend/modell/dauer_s der Kategorisierung dauerhaft in meta.json ab (vorher nur im fluechtigen Log) - noetig, damit der Export nachvollziehbar ist, welches Modell lief. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015vtvcyNkapuDh583643hNs
This commit is contained in:
parent
7e4505d9b7
commit
90548944a2
5 changed files with 219 additions and 7 deletions
110
pipe/server.py
110
pipe/server.py
|
|
@ -23,7 +23,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|||
from pathlib import Path
|
||||
from urllib.parse import unquote, urlparse
|
||||
|
||||
from . import config, dashboard, modellwahl, protokoll, stapel, store
|
||||
from . import bewertung, config, dashboard, export, modellwahl, protokoll, stapel, store
|
||||
|
||||
# Statusabfragen sind teuer (Netz, Unterprozesse) - kurz zwischenspeichern,
|
||||
# damit haeufiges Nachfragen den Rechner nicht belastet.
|
||||
|
|
@ -147,14 +147,16 @@ def anrufe() -> list[dict]:
|
|||
"transkript": d.get("transkript") or "",
|
||||
"audio": audio,
|
||||
"kontakt": (d.get("bekannter_kontakt") or {}).get("name"),
|
||||
"bewertung": (bewertung.lies(config.ABLAGE_LOKAL / ordner) or {}).get("bewertung") if ordner else None,
|
||||
})
|
||||
return liste
|
||||
|
||||
|
||||
def archiviere_tag() -> int:
|
||||
"""Markiert alle 'Erledigt'-Anrufe als archiviert (simuliert - es wird
|
||||
nichts exportiert oder geloescht, nur vom Board geraeumt). Gibt die Anzahl
|
||||
zurueck."""
|
||||
nichts geloescht, nur vom Board geraeumt). Bewertete Anrufe werden dabei
|
||||
zusaetzlich als Trainingsdaten exportiert, siehe pipe.export. Gibt die
|
||||
Anzahl archivierter Anrufe zurueck."""
|
||||
letzter = stapel.erlaubt()[-1]
|
||||
anzahl = 0
|
||||
for a in anrufe():
|
||||
|
|
@ -293,7 +295,35 @@ class Handler(BaseHTTPRequestHandler):
|
|||
if pfad == "/api/archivieren":
|
||||
anzahl = archiviere_tag()
|
||||
protokoll.schreibe("archiv", f"{anzahl} Anruf(e) archiviert (simuliert)", anzahl=anzahl)
|
||||
self._json({"ok": True, "anzahl": anzahl, "simuliert": True})
|
||||
_, export_anzahl = export.exportiere()
|
||||
if export_anzahl:
|
||||
protokoll.schreibe("export", f"{export_anzahl} bewertete Anrufe exportiert")
|
||||
self._json({"ok": True, "anzahl": anzahl, "simuliert": True, "export_anzahl": export_anzahl})
|
||||
return
|
||||
if pfad == "/api/export":
|
||||
ziel, anzahl = export.exportiere()
|
||||
protokoll.schreibe("export", f"{anzahl} bewertete Anrufe exportiert nach {ziel.name}")
|
||||
self._json({"ok": True, "anzahl": anzahl, "datei": ziel.name})
|
||||
return
|
||||
if pfad == "/api/bewertung":
|
||||
try:
|
||||
laenge = int(self.headers.get("Content-Length") or 0)
|
||||
wunsch = json.loads(self.rfile.read(min(laenge, 10_000)).decode("utf-8"))
|
||||
kennung, wert = wunsch["id"], wunsch["bewertung"]
|
||||
korrektur = wunsch.get("korrektur")
|
||||
except Exception:
|
||||
self._json({"ok": False, "fehler": "ungültige Anfrage"})
|
||||
return
|
||||
ordner = stapel.ordner_zu(kennung)
|
||||
if ordner is None:
|
||||
self._json({"ok": False, "fehler": "Anruf nicht gefunden"})
|
||||
return
|
||||
if not bewertung.setze(ordner, wert, korrektur):
|
||||
self._json({"ok": False, "fehler": f"ungültige Bewertung: {wert}"})
|
||||
return
|
||||
protokoll.schreibe("bewertung", f"{ordner.name} → {wert}"
|
||||
+ (f" (Korrektur: {korrektur})" if korrektur else ""))
|
||||
self._json({"ok": True})
|
||||
return
|
||||
if pfad == "/api/modell":
|
||||
try:
|
||||
|
|
@ -433,6 +463,21 @@ animation:notfallBlinken 1.6s ease-in-out infinite}
|
|||
padding:6px 12px;font-size:13px;cursor:pointer;font-family:inherit}
|
||||
.archiv-btn:hover{border-color:var(--muted)}
|
||||
.archiv-btn:disabled{opacity:.5;cursor:default}
|
||||
.export-btn{background:var(--karte);border:1px solid var(--rand);color:var(--fg);border-radius:8px;
|
||||
padding:6px 12px;font-size:13px;cursor:pointer;font-family:inherit;margin-left:8px}
|
||||
.export-btn:hover{border-color:var(--muted)}
|
||||
.bewertung{display:flex;align-items:center;gap:6px;margin-top:8px}
|
||||
.bewertung button{font-size:15px;background:var(--bg);border:1px solid var(--rand);border-radius:7px;
|
||||
padding:3px 8px;cursor:pointer;line-height:1;opacity:.55}
|
||||
.bewertung button:hover{opacity:1;border-color:var(--muted)}
|
||||
.bw-gut.aktiv{opacity:1;background:color-mix(in srgb,var(--gut) 18%,var(--bg));border-color:var(--gut)}
|
||||
.bw-schlecht.aktiv{opacity:1;background:color-mix(in srgb,var(--schlecht) 18%,var(--bg));border-color:var(--schlecht)}
|
||||
.bw-korrektur{display:none;align-items:center;gap:5px}
|
||||
.bw-korrektur.zeigen{display:flex}
|
||||
.bw-korrektur select{font:inherit;font-size:12px;padding:2px 4px;border-radius:6px;
|
||||
border:1px solid var(--rand);background:var(--bg);color:var(--fg)}
|
||||
.bw-korrektur button{font-size:12px;padding:3px 8px;opacity:1;background:var(--gut);
|
||||
color:#fff;border-color:var(--gut)}
|
||||
.sicher-btn{background:var(--karte);border:1px solid var(--rand);color:var(--fg);border-radius:8px;
|
||||
padding:6px 12px;font-size:13px;cursor:pointer;font-family:inherit;margin-right:8px}
|
||||
.sicher-btn:hover{border-color:var(--muted)}
|
||||
|
|
@ -495,7 +540,8 @@ color:var(--gut);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:
|
|||
</style></head><body><div class="wrap">
|
||||
<header><h1>Anruf-Leitstand</h1>
|
||||
<button class="sicher-btn" id="sicherBtn" title="Namen, Nummern, Anliegen und Transkripte unscharf - fuer Bildschirmaufnahmen/Screenshots">🙈 Sicher-Modus</button>
|
||||
<button class="archiv-btn" id="archivBtn" title="Erledigte Anrufe vom Board raeumen (simuliert - nichts wird geloescht oder exportiert)">🗄️ Tag archivieren</button>
|
||||
<button class="archiv-btn" id="archivBtn" title="Erledigte Anrufe vom Board raeumen (simuliert, nichts wird geloescht) - bewertete Anrufe werden dabei als Trainingsdaten exportiert">🗄️ Tag archivieren</button>
|
||||
<button class="export-btn" id="exportBtn" title="Bewertete Anrufe (👍/👎) als JSONL-Trainingsdaten exportieren, unabhaengig vom Archivieren">📤 Bewertungen exportieren</button>
|
||||
<span class="stand" id="stand">…</span>
|
||||
<select class="modell-select" id="modellSelect" title="Kategorisierungs-Modell fuer neue Anrufe - Aenderung greift sofort, kein Neustart noetig"></select>
|
||||
</header>
|
||||
|
|
@ -539,6 +585,17 @@ async function verschiebe(id,ziel){
|
|||
if(!d||!d.ok){takt()} // hat nicht geklappt: echten Stand neu holen
|
||||
}
|
||||
|
||||
// Bewertung dient gleichzeitig als Trainingsdaten (siehe pipe.export) - 👍/👎
|
||||
// pro Anruf, bei 👎 optional die richtige Kategorie/Dringlichkeit dazu.
|
||||
async function bewerte(id,wert,korrektur){
|
||||
const a=ANRUFE.find(x=>x.id===id); if(a)a.bewertung=wert;
|
||||
const r=await fetch('/api/bewertung',{method:'POST',headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({id,bewertung:wert,korrektur:korrektur||null})});
|
||||
const d=await r.json().catch(()=>null);
|
||||
toast(d&&d.ok?`Bewertung gespeichert (${wert==='gut'?'👍':'👎'})`:'Bewertung fehlgeschlagen');
|
||||
if(a)zeichneBoard();
|
||||
}
|
||||
|
||||
// Nur Ziffern und ein fuehrendes Plus taugen fuer tel:
|
||||
const waehlbar=n=>String(n||'').replace(/[^\d+]/g,'').replace(/(?!^)\+/g,'');
|
||||
|
||||
|
|
@ -581,6 +638,15 @@ function karte(d){
|
|||
<div>${(d.stichworte||[]).map(s=>`<span class="tag">${esc(s)}</span>`).join('')}</div>
|
||||
${d.audio?`<audio controls preload="none" src="${d.audio}"></audio>`:''}
|
||||
<details data-id="${esc(d.id)}"${offen}><summary>Transkript</summary><p class="tr">${esc(d.transkript)}</p></details>
|
||||
<div class="bewertung" data-id="${esc(d.id)}">
|
||||
<button class="bw-gut${d.bewertung==='gut'?' aktiv':''}" data-wert="gut" title="Kategorisierung war korrekt - dient als Trainingsdaten">👍</button>
|
||||
<button class="bw-schlecht${d.bewertung==='schlecht'?' aktiv':''}" data-wert="schlecht" title="Kategorisierung war falsch - optional korrigieren">👎</button>
|
||||
<span class="bw-korrektur">
|
||||
<select class="bw-kat"></select>
|
||||
<select class="bw-dring"></select>
|
||||
<button class="bw-speichern">Speichern</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="schieber">${knoepfe}</div>
|
||||
</article>`;
|
||||
}
|
||||
|
|
@ -604,6 +670,23 @@ function zeichneBoard(){
|
|||
el.querySelectorAll('details').forEach(d=>{
|
||||
d.ontoggle=()=>{d.open?offeneTranskripte.add(d.dataset.id):offeneTranskripte.delete(d.dataset.id)};
|
||||
});
|
||||
el.querySelectorAll('.bewertung').forEach(bw=>{
|
||||
const id=bw.dataset.id, korrektur=bw.querySelector('.bw-korrektur');
|
||||
const selKat=bw.querySelector('.bw-kat'), selDring=bw.querySelector('.bw-dring');
|
||||
selKat.innerHTML='<option value="">Kategorie unverändert</option>'
|
||||
+Object.keys(KAT).map(k=>`<option value="${k}">${esc(KAT[k])}</option>`).join('');
|
||||
selDring.innerHTML='<option value="">Dringlichkeit unverändert</option>'
|
||||
+['niedrig','normal','hoch','notfall'].map(x=>`<option value="${x}">${x}</option>`).join('');
|
||||
bw.querySelector('.bw-gut').onclick=e=>{e.stopPropagation();bewerte(id,'gut')};
|
||||
bw.querySelector('.bw-schlecht').onclick=e=>{e.stopPropagation();korrektur.classList.toggle('zeigen')};
|
||||
bw.querySelector('.bw-speichern').onclick=e=>{
|
||||
e.stopPropagation();
|
||||
const k={};
|
||||
if(selKat.value)k.kategorie=selKat.value;
|
||||
if(selDring.value)k.dringlichkeit=selDring.value;
|
||||
bewerte(id,'schlecht',Object.keys(k).length?k:null);
|
||||
};
|
||||
});
|
||||
el.querySelectorAll('.karte').forEach(k=>{
|
||||
k.ondragstart=e=>{pausiert=true;k.classList.add('zieht');e.dataTransfer.setData('text/plain',k.dataset.id)};
|
||||
k.ondragend=()=>{pausiert=false;k.classList.remove('zieht')};
|
||||
|
|
@ -765,8 +848,21 @@ document.getElementById('archivBtn').onclick=async()=>{
|
|||
const r=await fetch('/api/archivieren',{method:'POST'});
|
||||
const d=await r.json().catch(()=>null);
|
||||
btn.disabled=false;
|
||||
if(d&&d.ok){toast(`✓ ${d.anzahl} Anruf(e) archiviert (simuliert)`);takt()}
|
||||
else{toast('Archivieren fehlgeschlagen.')}
|
||||
if(d&&d.ok){
|
||||
let text=`✓ ${d.anzahl} Anruf(e) archiviert (simuliert)`;
|
||||
if(d.export_anzahl)text+=` · ${d.export_anzahl} bewertete Anrufe exportiert`;
|
||||
toast(text);takt();
|
||||
} else{toast('Archivieren fehlgeschlagen.')}
|
||||
};
|
||||
|
||||
document.getElementById('exportBtn').onclick=async()=>{
|
||||
const bewertete=ANRUFE.filter(a=>a.bewertung).length;
|
||||
if(!confirm(`${bewertete} bewertete Anrufe auf dem aktuellen Board gefunden. `
|
||||
+'Export ist für externe Auswertung gedacht (z.B. Modell-Verbesserung) — '
|
||||
+'Transkripte und Namen der bewerteten Anrufe verlassen damit den Rechner. Fortfahren?'))return;
|
||||
const r=await fetch('/api/export',{method:'POST'});
|
||||
const d=await r.json().catch(()=>null);
|
||||
toast(d&&d.ok?`✓ ${d.anzahl} Anruf(e) exportiert → training/${d.datei}`:'Export fehlgeschlagen');
|
||||
};
|
||||
|
||||
takt();setInterval(takt,3000);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue