352 lines
20 KiB
Twig
352 lines
20 KiB
Twig
{% extends 'admin/base.html.twig' %}
|
|
|
|
{% block title %}Eval-Cases verwalten{% endblock %}
|
|
|
|
{% block body %}
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
|
<div>
|
|
<h1 class="h3 mb-1">
|
|
<i class="bi bi-journal-plus"></i> Eval-Cases verwalten
|
|
</h1>
|
|
<div class="small text-secondary">
|
|
Neue Regression-Cases separat anlegen oder bestehende Cases entfernen, ohne die Eval-Suite-Übersicht aufzublähen.
|
|
</div>
|
|
</div>
|
|
|
|
<a href="{{ path('admin_evals_index', {type: case_draft.type|default('retrieval')}) }}"
|
|
class="btn btn-sm btn-outline-secondary">
|
|
Zurück zur Eval Suite
|
|
</a>
|
|
</div>
|
|
|
|
{% for label in ['success', 'danger', 'warning', 'info'] %}
|
|
{% for message in app.flashes(label) %}
|
|
<div class="alert alert-{{ label }} shadow-sm">
|
|
{{ message }}
|
|
</div>
|
|
{% endfor %}
|
|
{% endfor %}
|
|
|
|
{% if case_draft.source_label|default('') %}
|
|
<div class="alert alert-info border-info bg-black text-light shadow-sm">
|
|
<strong>Vorlage geladen:</strong> {{ case_draft.source_label }}<br>
|
|
<span class="small text-secondary">
|
|
Bitte Case-ID, Prompt und Assertions prüfen, bevor du den Case speicherst.
|
|
</span>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="alert alert-secondary border-secondary bg-black text-light shadow-sm mb-4">
|
|
<div class="fw-semibold text-warning mb-1">
|
|
<i class="bi bi-compass"></i> Kurz erklärt
|
|
</div>
|
|
<div class="small text-secondary">
|
|
Ein Eval-Case ist ein wiederholbarer Test. Du trägst ein, <strong class="text-light">was der Nutzer fragt</strong>
|
|
und <strong class="text-light">woran RetrieX gemessen werden soll</strong>. Der Test verändert keine Daten im Shop oder im RAG-Wissen,
|
|
sondern prüft nur, ob ein bekannter Fall weiterhin richtig läuft.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-4">
|
|
<div class="col-xl-8">
|
|
<div class="card bg-black border-secondary text-light shadow-sm">
|
|
<div class="card-body">
|
|
<h5 class="text-warning mb-3">
|
|
<i class="bi bi-pencil-square"></i> Neuer Eval-Case
|
|
</h5>
|
|
|
|
<form method="post" action="{{ path('admin_evals_case_create') }}">
|
|
<input type="hidden" name="_token" value="{{ csrf_token('admin_eval_case_create') }}">
|
|
|
|
<div class="mb-4">
|
|
<label class="form-label">Eval-Typ</label>
|
|
<select name="type" class="form-select bg-dark text-light border-secondary">
|
|
{% for type, label in types %}
|
|
<option value="{{ type }}" {% if type == case_draft.type|default('retrieval') %}selected{% endif %}>
|
|
{{ label }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
<div class="form-text text-secondary">
|
|
Wähle zuerst, <strong class="text-light">was genau geprüft werden soll</strong>. Der Typ entscheidet auch,
|
|
in welche Datei der Case geschrieben wird: <code>tests/evals/cases/<type>.ndjson</code>.
|
|
</div>
|
|
<div class="small text-secondary mt-2 border border-secondary rounded p-3 bg-dark">
|
|
<div class="mb-1"><strong class="text-light">retrieval</strong>: prüft, ob die richtige Wissensquelle oder das richtige Dokument gefunden wird.</div>
|
|
<div class="mb-1"><strong class="text-light">shop_query</strong>: prüft, welche Suchquery an den Shop geschickt würde.</div>
|
|
<div class="mb-1"><strong class="text-light">followup</strong>: prüft eine Folgefrage, die den vorherigen Chatverlauf braucht.</div>
|
|
<div><strong class="text-light">answer_guard</strong>: prüft, dass RetrieX bei Unsinn oder fehlender Evidenz nichts erfindet.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label class="form-label">Neue Case-ID</label>
|
|
<input type="text"
|
|
name="id"
|
|
value="{{ case_draft.id|default('') }}"
|
|
class="form-control bg-dark text-light border-secondary"
|
|
placeholder="followup_testomat808_device_price_001"
|
|
required>
|
|
<div class="form-text text-secondary">
|
|
Das ist der <strong class="text-light">interne Name des Tests</strong>. Er erscheint später in der Eval-Auswertung,
|
|
damit du den Fall wiedererkennst. Verwende keine Leerzeichen. Erlaubt sind Buchstaben, Zahlen, <code>_</code> und <code>-</code>.
|
|
</div>
|
|
<div class="small text-secondary mt-2 border border-secondary rounded p-3 bg-dark">
|
|
Gute Beispiele: <code>retrieval_lieferbedingungen_versand_001</code>,
|
|
<code>shop_query_testomat808_indikator300_001</code>,
|
|
<code>followup_testomat808_device_price_001</code>.<br>
|
|
Faustregel: <code>typ_thema_ziel_nummer</code>.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label class="form-label">Prompt</label>
|
|
<textarea name="prompt"
|
|
rows="3"
|
|
class="form-control bg-dark text-light border-secondary"
|
|
placeholder="und was kostet das gerät selber"
|
|
required>{{ case_draft.prompt|default('') }}</textarea>
|
|
<div class="form-text text-secondary">
|
|
Hier kommt <strong class="text-light">genau die Nutzerfrage</strong> hinein, die getestet werden soll.
|
|
Nicht die erwartete Antwort eintragen, sondern den Satz, den ein Nutzer in den Chat schreiben würde.
|
|
</div>
|
|
<div class="small text-secondary mt-2 border border-secondary rounded p-3 bg-dark">
|
|
Tippfehler dürfen bewusst drin bleiben, wenn genau dieser Tippfehler abgesichert werden soll.
|
|
Beispiel: <code>ich würde gern chlor im schwinnbad messen</code> prüft dann auch die Korrektur Richtung <code>schwimmbad</code>.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label class="form-label">Assert-JSON</label>
|
|
<textarea name="assert_json"
|
|
rows="9"
|
|
class="form-control bg-dark text-light border-secondary font-monospace"
|
|
spellcheck="false">{{ case_draft.assert_json|default('{}') }}</textarea>
|
|
<div class="form-text text-secondary">
|
|
Hier steht, <strong class="text-light">was der Test erwarten soll</strong>. Das Feld muss gültiges JSON sein,
|
|
also mit <code>{</code> anfangen und mit <code>}</code> enden. Keine Kommentare und kein Komma nach dem letzten Eintrag.
|
|
</div>
|
|
<div class="small text-secondary mt-2 border border-secondary rounded p-3 bg-dark">
|
|
<div class="mb-2"><strong class="text-light">Wenn eine Shopquery exakt stimmen soll:</strong></div>
|
|
<pre class="bg-black border border-secondary rounded p-2 small text-light mb-3"><code>{
|
|
"expected_query": "testomat 808"
|
|
}</code></pre>
|
|
<div class="mb-2"><strong class="text-light">Wenn bestimmte Wörter enthalten sein müssen:</strong></div>
|
|
<pre class="bg-black border border-secondary rounded p-2 small text-light mb-3"><code>{
|
|
"must_include_terms": [
|
|
"testomat",
|
|
"808"
|
|
]
|
|
}</code></pre>
|
|
<div class="mb-2"><strong class="text-light">Wenn ein Dokument gefunden werden muss:</strong></div>
|
|
<pre class="bg-black border border-secondary rounded p-2 small text-light mb-0"><code>{
|
|
"min_results": 1,
|
|
"must_include_one_of_document_ids": [
|
|
"DOKUMENT-ID"
|
|
]
|
|
}</code></pre>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label class="form-label">History-JSON <span class="text-secondary">optional</span></label>
|
|
<textarea name="history_json"
|
|
rows="8"
|
|
class="form-control bg-dark text-light border-secondary font-monospace"
|
|
spellcheck="false"
|
|
placeholder='[{"prompt":"vorherige Frage","answer":"vorherige Antwort"}]'>{{ case_draft.history_json|default('') }}</textarea>
|
|
<div class="form-text text-secondary">
|
|
Nur ausfüllen, wenn die aktuelle Frage den <strong class="text-light">vorherigen Chatverlauf</strong> braucht.
|
|
Für direkte Einzelprompts leer lassen. Das Feld muss eine JSON-Liste sein, also mit <code>[</code> anfangen und mit <code>]</code> enden.
|
|
</div>
|
|
<div class="small text-secondary mt-2 border border-secondary rounded p-3 bg-dark">
|
|
Typischer Einsatz: Der Nutzer fragt zuerst nach dem niedrigsten Grenzwert, danach nach dem Indikator
|
|
und anschließend <code>was kostet der indikator</code>. Dann braucht der Test die vorherigen Fragen und Antworten als History.
|
|
<pre class="bg-black border border-secondary rounded p-2 small text-light mt-2 mb-0"><code>[
|
|
{
|
|
"prompt": "mit welchem indikator",
|
|
"answer": "Der Wert 0,02 °dH wird beim Testomat 808 mit Indikatortyp 300 gemessen."
|
|
}
|
|
]</code></pre>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label class="form-label">Request Context Hint <span class="text-secondary">optional</span></label>
|
|
<textarea name="request_context_hint"
|
|
rows="3"
|
|
class="form-control bg-dark text-light border-secondary"
|
|
placeholder="Nur für Spezialfälle, wenn History nicht ausreicht.">{{ case_draft.request_context_hint|default('') }}</textarea>
|
|
<div class="form-text text-secondary">
|
|
Dieses Feld kannst du fast immer <strong class="text-light">leer lassen</strong>. Es ist nur für Sonderfälle gedacht,
|
|
wenn der Test Zusatzkontext braucht, der nicht sauber als History darstellbar ist.
|
|
</div>
|
|
<div class="small text-secondary mt-2 border border-secondary rounded p-3 bg-dark">
|
|
Beispiel für einen Sonderfall: <code>Im vorherigen Ergebnis waren mehrere Shop-Produkte sichtbar, aber keine normale Chatantwort.</code>
|
|
Für normale Regressionen ist <strong class="text-light">History-JSON die bessere Wahl</strong>.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex flex-wrap gap-2">
|
|
<button type="submit" class="btn btn-warning">
|
|
<i class="bi bi-save"></i> Eval-Case speichern
|
|
</button>
|
|
<a href="{{ path('admin_evals_index', {type: case_draft.type|default('retrieval')}) }}"
|
|
class="btn btn-outline-secondary">
|
|
Abbrechen
|
|
</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-xl-4">
|
|
<div class="card bg-black border-danger text-light shadow-sm mb-4">
|
|
<div class="card-body">
|
|
<h5 class="text-danger mb-3">
|
|
<i class="bi bi-trash3"></i> Bestehende Eval-Cases entfernen
|
|
</h5>
|
|
<p class="small text-secondary mb-3">
|
|
Hier kannst du falsch angelegte oder nicht mehr benötigte Cases aus den
|
|
<code>tests/evals/cases/*.ndjson</code>-Dateien entfernen. Das Löschen betrifft nur den Eval-Case,
|
|
nicht das RAG-Wissen, nicht den Shop und nicht die bestehenden Reports.
|
|
</p>
|
|
|
|
{% for type, label in types %}
|
|
{% set cases = cases_by_type[type]|default([]) %}
|
|
<details class="border border-secondary rounded p-3 mb-3" {% if type == case_draft.type|default('retrieval') %}open{% endif %}>
|
|
<summary class="text-info" style="cursor:pointer;">
|
|
{{ label }} <span class="text-secondary">({{ cases|length }} Cases)</span>
|
|
</summary>
|
|
|
|
{% if cases is empty %}
|
|
<div class="small text-secondary mt-3">
|
|
Für diesen Typ gibt es aktuell keine Cases.
|
|
</div>
|
|
{% else %}
|
|
<div class="mt-3">
|
|
{% for case in cases %}
|
|
<div class="border-top border-secondary pt-3 mt-3">
|
|
<div class="small mb-2">
|
|
<code>{{ case.id }}</code>
|
|
<div class="text-secondary mt-1">{{ case.prompt }}</div>
|
|
</div>
|
|
<form method="post"
|
|
action="{{ path('admin_evals_case_delete') }}"
|
|
onsubmit="return confirm('Eval-Case {{ case.id }} wirklich löschen? Diese Änderung entfernt die NDJSON-Zeile dauerhaft.');">
|
|
<input type="hidden" name="_token" value="{{ csrf_token('admin_eval_case_delete_' ~ type ~ '_' ~ case.id) }}">
|
|
<input type="hidden" name="type" value="{{ type }}">
|
|
<input type="hidden" name="case_id" value="{{ case.id }}">
|
|
<button type="submit" class="btn btn-sm btn-outline-danger">
|
|
<i class="bi bi-trash3"></i> Case löschen
|
|
</button>
|
|
</form>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</details>
|
|
{% endfor %}
|
|
|
|
<div class="small text-secondary">
|
|
Nach dem Löschen solltest du den betroffenen Eval-Typ einmal ausführen, damit der Report zum neuen Case-Bestand passt.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card bg-black border-secondary text-light shadow-sm mb-4">
|
|
<div class="card-body">
|
|
<h5 class="text-info mb-3">
|
|
<i class="bi bi-info-circle"></i> Welcher Typ ist richtig?
|
|
</h5>
|
|
<div class="small text-secondary">
|
|
<div class="mb-3">
|
|
<strong class="text-light">Du willst prüfen, ob das richtige Dokument gefunden wird?</strong><br>
|
|
Dann nimm <code>retrieval</code>.
|
|
</div>
|
|
<div class="mb-3">
|
|
<strong class="text-light">Du willst prüfen, welche Suchwörter an den Shop gehen?</strong><br>
|
|
Dann nimm <code>shop_query</code>.
|
|
</div>
|
|
<div class="mb-3">
|
|
<strong class="text-light">Die Frage bezieht sich auf die vorherige Antwort?</strong><br>
|
|
Dann nimm <code>followup</code> und fülle <code>History-JSON</code> aus.
|
|
</div>
|
|
<div>
|
|
<strong class="text-light">RetrieX soll bei Unsinn nichts erfinden?</strong><br>
|
|
Dann nimm <code>answer_guard</code>.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card bg-black border-secondary text-light shadow-sm mb-4">
|
|
<div class="card-body">
|
|
<h5 class="text-info mb-3">
|
|
<i class="bi bi-braces"></i> Häufige Assertions
|
|
</h5>
|
|
<div class="small text-secondary mb-2">Exakte Query:</div>
|
|
<pre class="bg-dark border border-secondary rounded p-2 small text-light"><code>{
|
|
"expected_query": "testomat 808"
|
|
}</code></pre>
|
|
|
|
<div class="small text-secondary mb-2">Begriffe müssen enthalten sein:</div>
|
|
<pre class="bg-dark border border-secondary rounded p-2 small text-light"><code>{
|
|
"must_include_terms": [
|
|
"testomat",
|
|
"808"
|
|
]
|
|
}</code></pre>
|
|
|
|
<div class="small text-secondary mb-2">Begriffe dürfen nicht enthalten sein:</div>
|
|
<pre class="bg-dark border border-secondary rounded p-2 small text-light"><code>{
|
|
"must_not_include_terms": [
|
|
"indikator",
|
|
"300"
|
|
]
|
|
}</code></pre>
|
|
|
|
<div class="small text-secondary mb-2">Dokument muss enthalten sein:</div>
|
|
<pre class="bg-dark border border-secondary rounded p-2 small text-light"><code>{
|
|
"min_results": 1,
|
|
"must_include_one_of_document_ids": [
|
|
"DOKUMENT-ID"
|
|
]
|
|
}</code></pre>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card bg-black border-secondary text-light shadow-sm mb-4">
|
|
<div class="card-body">
|
|
<h5 class="text-info mb-3">
|
|
<i class="bi bi-check2-square"></i> Vor dem Speichern prüfen
|
|
</h5>
|
|
<ul class="small text-secondary mb-0">
|
|
<li>Prüft der Case genau einen Zweck?</li>
|
|
<li>Ist die Case-ID eindeutig und ohne Leerzeichen?</li>
|
|
<li>Ist der Prompt eine echte Nutzerfrage?</li>
|
|
<li>Ist Assert-JSON gültiges JSON?</li>
|
|
<li>Ist History nur bei echten Folgefragen gefüllt?</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card bg-black border-secondary text-light shadow-sm">
|
|
<div class="card-body">
|
|
<h5 class="text-info mb-3">
|
|
<i class="bi bi-lightbulb"></i> Empfehlung
|
|
</h5>
|
|
<p class="small text-secondary mb-0">
|
|
Ein guter Eval-Case prüft genau einen Zweck. Lieber mehrere kleine Cases anlegen als einen großen, empfindlichen Case.
|
|
Wenn du unsicher bist, starte mit <code>expected_query</code> bei Shop-/Follow-up-Fällen oder mit
|
|
<code>must_include_one_of_document_ids</code> bei Retrieval-Fällen.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% endblock %}
|