p101
This commit is contained in:
731
RETRIEX-EVAL-CASE-HOWTO.md
Normal file
731
RETRIEX-EVAL-CASE-HOWTO.md
Normal file
@@ -0,0 +1,731 @@
|
||||
# RetrieX How-to: Neue Eval-Cases korrekt erstellen
|
||||
|
||||
Dieses How-to beschreibt, wie neue Regressionstests für die RetrieX Eval-Suite über den Admin-Bereich angelegt werden.
|
||||
|
||||
Ziel ist, neue rote oder fachlich wichtige Fälle dauerhaft abzusichern, ohne direkt Core-Logik, Retrieval-Regeln oder Shopquery-Heuristiken zu verändern.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Admin-Pfad:
|
||||
|
||||
```text
|
||||
/admin/evals/
|
||||
```
|
||||
|
||||
Im Bereich **„Eval-Case erstellen“** können neue Cases für folgende Typen angelegt werden:
|
||||
|
||||
```text
|
||||
retrieval
|
||||
shop_query
|
||||
followup
|
||||
answer_guard
|
||||
```
|
||||
|
||||
Nach dem Speichern wird der Case in die passende Datei geschrieben:
|
||||
|
||||
```text
|
||||
tests/evals/cases/retrieval.ndjson
|
||||
tests/evals/cases/shop_query.ndjson
|
||||
tests/evals/cases/followup.ndjson
|
||||
tests/evals/cases/answer_guard.ndjson
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Grundregel
|
||||
|
||||
Ein guter Eval-Case prüft genau **einen klaren Sachverhalt**.
|
||||
|
||||
Gut:
|
||||
|
||||
```json
|
||||
{
|
||||
"expected_query": "testomat 808",
|
||||
"must_not_include_terms": [
|
||||
"indikator",
|
||||
"300"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Weniger gut:
|
||||
|
||||
```json
|
||||
{
|
||||
"expected_query": "testomat 808",
|
||||
"must_include_terms": [
|
||||
"testomat",
|
||||
"808",
|
||||
"gerät",
|
||||
"preis",
|
||||
"wasserhärte"
|
||||
],
|
||||
"must_not_include_terms": [
|
||||
"indikator",
|
||||
"300",
|
||||
"testomat 2000",
|
||||
"chlor",
|
||||
"versand"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Je kleiner und eindeutiger der Case ist, desto besser eignet er sich als Regressionstest.
|
||||
|
||||
---
|
||||
|
||||
# Felder im Admin
|
||||
|
||||
## 1. Eval-Typ
|
||||
|
||||
Wähle den Typ passend zum Ziel des Tests.
|
||||
|
||||
```text
|
||||
retrieval → prüft, ob die richtigen RAG-Dokumente/Chunks gefunden werden
|
||||
shop_query → prüft, welche Shopquery aus einem direkten Prompt entsteht
|
||||
followup → prüft, welche Shopquery aus Prompt + Chatverlauf entsteht
|
||||
answer_guard → prüft No-Answer-, Nicht-Halluzinations- oder Evidenzfälle
|
||||
```
|
||||
|
||||
Faustregel:
|
||||
|
||||
```text
|
||||
Wird das richtige Dokument gefunden? → retrieval
|
||||
Wird die richtige Shopquery erzeugt? → shop_query
|
||||
Versteht RetrieX die Folgefrage im Verlauf? → followup
|
||||
Erfindet RetrieX nichts bei schwacher Evidenz? → answer_guard
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Neue Case-ID
|
||||
|
||||
Die Case-ID muss eindeutig sein und darf nur folgende Zeichen enthalten:
|
||||
|
||||
```text
|
||||
Buchstaben
|
||||
Zahlen
|
||||
_
|
||||
-
|
||||
```
|
||||
|
||||
Gute Beispiele:
|
||||
|
||||
```text
|
||||
retrieval_semantic_chlor_clt_001
|
||||
shop_query_indicator_300_exact_002
|
||||
followup_main_device_price_002
|
||||
answer_guard_unknown_medium_001
|
||||
```
|
||||
|
||||
Nicht verwenden:
|
||||
|
||||
```text
|
||||
Test 1
|
||||
shop query indikator 300
|
||||
gerät/frage/neue-version
|
||||
```
|
||||
|
||||
Empfohlenes Schema:
|
||||
|
||||
```text
|
||||
<typ>_<thema>_<ziel>_<nummer>
|
||||
```
|
||||
|
||||
Beispiel:
|
||||
|
||||
```text
|
||||
followup_testomat808_device_price_001
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Prompt
|
||||
|
||||
Hier kommt exakt der Nutzerprompt hinein, der getestet werden soll.
|
||||
|
||||
Beispiele:
|
||||
|
||||
```text
|
||||
welches geraet ist fuer chlorueberwachung gedacht
|
||||
```
|
||||
|
||||
```text
|
||||
was kostet der indikator
|
||||
```
|
||||
|
||||
```text
|
||||
und was kostet das gerät selber
|
||||
```
|
||||
|
||||
```text
|
||||
welcher testomat misst drachenblut
|
||||
```
|
||||
|
||||
Der Prompt sollte möglichst so eingetragen werden, wie er real im Chat vorkommt. Tippfehler dürfen bewusst enthalten sein, wenn genau dieses Verhalten abgesichert werden soll.
|
||||
|
||||
---
|
||||
|
||||
## 4. Assert-JSON
|
||||
|
||||
Das Assert-JSON beschreibt, was der Test prüfen soll.
|
||||
|
||||
Das Feld muss immer ein gültiges JSON-Objekt sein:
|
||||
|
||||
```json
|
||||
{
|
||||
}
|
||||
```
|
||||
|
||||
Wichtig:
|
||||
|
||||
- Keine Kommentare im JSON
|
||||
- Keine trailing commas
|
||||
- Doppelte Anführungszeichen verwenden
|
||||
- Das Feld muss ein Objekt `{ ... }` sein, kein Array
|
||||
|
||||
---
|
||||
|
||||
# Eval-Typen und Beispiele
|
||||
|
||||
## A) Retrieval-Case
|
||||
|
||||
Retrieval-Cases prüfen, ob die richtigen RAG-Dokumente oder Chunks gefunden werden.
|
||||
|
||||
### Minimaler positiver Retrieval-Case
|
||||
|
||||
```json
|
||||
{
|
||||
"min_results": 1
|
||||
}
|
||||
```
|
||||
|
||||
### Retrieval-Case mit erwarteter Dokument-ID
|
||||
|
||||
```json
|
||||
{
|
||||
"min_results": 1,
|
||||
"must_include_one_of_document_ids": [
|
||||
"DOKUMENT-ID-HIER"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Retrieval-Case mit mehreren möglichen Ziel-Dokumenten
|
||||
|
||||
```json
|
||||
{
|
||||
"min_results": 1,
|
||||
"must_include_one_of_document_ids": [
|
||||
"DOKUMENT-ID-1",
|
||||
"DOKUMENT-ID-2"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Retrieval-Case mit Pflichtbegriffen
|
||||
|
||||
```json
|
||||
{
|
||||
"min_results": 1,
|
||||
"must_include_any_terms": [
|
||||
"lieferung",
|
||||
"versand"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Retrieval-Case mit verbotenen Dokumenten
|
||||
|
||||
```json
|
||||
{
|
||||
"min_results": 1,
|
||||
"must_not_include_document_ids": [
|
||||
"FALSCHE-DOKUMENT-ID"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Retrieval-Case für No-Result / Unsinn
|
||||
|
||||
```json
|
||||
{
|
||||
"max_results": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Empfohlene Retrieval-Struktur
|
||||
|
||||
```json
|
||||
{
|
||||
"min_results": 1,
|
||||
"must_include_one_of_document_ids": [
|
||||
"DOKUMENT-ID-HIER"
|
||||
],
|
||||
"must_include_any_terms": [
|
||||
"wichtiger fachbegriff",
|
||||
"produktname"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## B) Shopquery-Case
|
||||
|
||||
Shopquery-Cases prüfen, welche Shopquery aus einem direkten Prompt entsteht.
|
||||
|
||||
### Exakte Shopquery
|
||||
|
||||
Prompt:
|
||||
|
||||
```text
|
||||
was kostet der Testomat 808 Indikator 300
|
||||
```
|
||||
|
||||
Assert-JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"expected_query": "testomat 808 300 indikator"
|
||||
}
|
||||
```
|
||||
|
||||
### Shopquery mit Pflicht- und Verbotsbegriffen
|
||||
|
||||
```json
|
||||
{
|
||||
"must_include_terms": [
|
||||
"testomat",
|
||||
"808",
|
||||
"300",
|
||||
"indikator"
|
||||
],
|
||||
"must_not_include_terms": [
|
||||
"300 s",
|
||||
"301",
|
||||
"302",
|
||||
"303"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Query darf nicht auf Noise fallen
|
||||
|
||||
```json
|
||||
{
|
||||
"must_not_equal_query": "information"
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-Produkt- oder Link-Follow-up mit Einzelqueries
|
||||
|
||||
```json
|
||||
{
|
||||
"expected_individual_queries": [
|
||||
"testomat 2000 self clean",
|
||||
"testomat 2000 cal",
|
||||
"testomat 808"
|
||||
],
|
||||
"expected_individual_queries_exact": true
|
||||
}
|
||||
```
|
||||
|
||||
### Empfehlung für Shopquery-Cases
|
||||
|
||||
Nicht jeden Case sofort zu streng mit `expected_query` absichern. Bei noch variabler Query-Bildung ist oft besser:
|
||||
|
||||
```json
|
||||
{
|
||||
"must_include_terms": [
|
||||
"testomat",
|
||||
"808",
|
||||
"sio2"
|
||||
],
|
||||
"must_not_include_terms": [
|
||||
"gerät",
|
||||
"möchte",
|
||||
"messen"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`expected_query` nur verwenden, wenn die Query bereits stabil und bewusst exakt sein soll.
|
||||
|
||||
---
|
||||
|
||||
## C) Follow-up-Case
|
||||
|
||||
Follow-up-Cases prüfen, ob RetrieX den Verlauf korrekt nutzt.
|
||||
|
||||
Bei `followup` ist **History-JSON praktisch Pflicht**, weil sonst kein echter Verlauf getestet wird.
|
||||
|
||||
### Beispiel: Indikatorpreis nach Verlauf
|
||||
|
||||
Prompt:
|
||||
|
||||
```text
|
||||
was kostet der indikator
|
||||
```
|
||||
|
||||
History-JSON:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"prompt": "Was ist der niedrigste Grenzwert für die Wasserhärte, welcher mit einem Testomaten überwacht werden kann?",
|
||||
"answer": "Der niedrigste Grenzwert für die Wasserhärte beträgt 0,02 °dH. Dieser Wert wird vom Testomat 808 gemessen."
|
||||
},
|
||||
{
|
||||
"prompt": "mit welchem indikator",
|
||||
"answer": "Der niedrigste messbare Grenzwert für Wasserhärte mit dem Testomat 808 wird mit dem Indikatortyp 300 erreicht."
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Assert-JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"expected_query": "testomat 808 300 indikator",
|
||||
"must_include_terms": [
|
||||
"testomat",
|
||||
"808",
|
||||
"300",
|
||||
"indikator"
|
||||
],
|
||||
"must_not_include_terms": [
|
||||
"300 s",
|
||||
"301",
|
||||
"302",
|
||||
"303",
|
||||
"testomat 2000"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Beispiel: Wechsel vom Indikator zurück zum Hauptgerät
|
||||
|
||||
Prompt:
|
||||
|
||||
```text
|
||||
und was kostet das gerät selber
|
||||
```
|
||||
|
||||
History-JSON:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"prompt": "was kostet der indikator",
|
||||
"answer": "Shop-Suche abgeschlossen. Gesendete Suchquery: testomat 808 300 indikator. Testomat® 808 Indikator 300 500 ml, Produkt-Nummer 141001. Testomat® 808 Indikator 300 2 x 100 ml, Produkt-Nummer 140001. Der zugehörige Testomat ist Testomat 808."
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Assert-JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"expected_query": "testomat 808",
|
||||
"must_include_terms": [
|
||||
"testomat",
|
||||
"808"
|
||||
],
|
||||
"must_not_include_terms": [
|
||||
"indikator",
|
||||
"300",
|
||||
"141001",
|
||||
"140001"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Empfehlung für Follow-up-Cases
|
||||
|
||||
Die History sollte genau die Informationen enthalten, die der echte Chat vorher hatte.
|
||||
|
||||
Nicht zu wenig:
|
||||
|
||||
```text
|
||||
Nur "Indikator 300" ohne Geräteanker kann zu unklar sein.
|
||||
```
|
||||
|
||||
Nicht zu viel:
|
||||
|
||||
```text
|
||||
Ein kompletter langer Chatverlauf kann den Case unnötig instabil machen.
|
||||
```
|
||||
|
||||
Gut ist ein kurzer, fachlich relevanter Auszug.
|
||||
|
||||
---
|
||||
|
||||
## D) Answer-Guard-Case
|
||||
|
||||
Answer-Guard-Cases prüfen, dass RetrieX bei Unsinn, schwacher Evidenz oder falschen Zuordnungen nichts erfindet.
|
||||
|
||||
### Unsinn soll keine Treffer liefern
|
||||
|
||||
Prompt:
|
||||
|
||||
```text
|
||||
dsgfsdgfsdgf
|
||||
```
|
||||
|
||||
Assert-JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"max_results": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Erfundenes Medium soll nicht als echtes Produkt beantwortet werden
|
||||
|
||||
Prompt:
|
||||
|
||||
```text
|
||||
welcher testomat misst drachenblut
|
||||
```
|
||||
|
||||
Assert-JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"must_not_include_terms": [
|
||||
"drachenblut"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Falsches Dokument darf nicht gezogen werden
|
||||
|
||||
```json
|
||||
{
|
||||
"min_results": 1,
|
||||
"must_not_include_document_ids": [
|
||||
"FALSCHE-DOKUMENT-ID"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Empfehlung für Answer-Guard-Cases
|
||||
|
||||
Bei Answer-Guard-Cases möglichst nicht auf einzelne Wörter im kompletten Retrieval-Text überreagieren. Besser sind:
|
||||
|
||||
```text
|
||||
Dokument-IDs
|
||||
klare Produktnamen
|
||||
klare verbotene Zielbegriffe
|
||||
max_results bei Unsinn
|
||||
```
|
||||
|
||||
Ein Wort irgendwo im Retrieval-Kontext ist nicht automatisch ein fachlicher Fehler.
|
||||
|
||||
---
|
||||
|
||||
# Optionales Feld: History-JSON
|
||||
|
||||
History-JSON wird vor allem für `followup` verwendet.
|
||||
|
||||
Format:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"prompt": "vorherige Nutzerfrage",
|
||||
"answer": "vorherige Antwort oder relevanter Auszug"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Mehrere Turns:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"prompt": "erste Frage",
|
||||
"answer": "erste Antwort"
|
||||
},
|
||||
{
|
||||
"prompt": "zweite Frage",
|
||||
"answer": "zweite Antwort"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Wichtig:
|
||||
|
||||
```text
|
||||
History-JSON ist ein Array [...]
|
||||
Assert-JSON ist ein Objekt {...}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Optionales Feld: Request Context Hint
|
||||
|
||||
Dieses Feld kann meistens leer bleiben.
|
||||
|
||||
Es ist nur sinnvoll, wenn ein Case zusätzlichen Kontext simulieren soll, der nicht sauber über History abbildbar ist.
|
||||
|
||||
Beispiel:
|
||||
|
||||
```text
|
||||
Sichtbare Shop-Ergebnisse enthalten Testomat 808 und Testomat 808 Indikator 300.
|
||||
Der Nutzer fragt nach dem Gerät selber.
|
||||
```
|
||||
|
||||
Empfehlung:
|
||||
|
||||
```text
|
||||
Für normale Regressionen lieber History-JSON verwenden.
|
||||
Request Context Hint nur für Spezialfälle nutzen.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Vollständiges Beispiel: Follow-up-Gerätepreis
|
||||
|
||||
## Eval-Typ
|
||||
|
||||
```text
|
||||
followup
|
||||
```
|
||||
|
||||
## Neue Case-ID
|
||||
|
||||
```text
|
||||
followup_testomat808_main_device_price_002
|
||||
```
|
||||
|
||||
## Prompt
|
||||
|
||||
```text
|
||||
und was kostet das gerät selber
|
||||
```
|
||||
|
||||
## Assert-JSON
|
||||
|
||||
```json
|
||||
{
|
||||
"expected_query": "testomat 808",
|
||||
"must_include_terms": [
|
||||
"testomat",
|
||||
"808"
|
||||
],
|
||||
"must_not_include_terms": [
|
||||
"indikator",
|
||||
"300",
|
||||
"141001",
|
||||
"140001"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## History-JSON
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"prompt": "was kostet der indikator",
|
||||
"answer": "Shop-Suche abgeschlossen. Gesendete Suchquery: testomat 808 300 indikator. Testomat® 808 Indikator 300 500 ml, Produkt-Nummer 141001. Testomat® 808 Indikator 300 2 x 100 ml, Produkt-Nummer 140001. Der zugehörige Testomat ist Testomat 808."
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Request Context Hint
|
||||
|
||||
Leer lassen.
|
||||
|
||||
---
|
||||
|
||||
# Nach dem Speichern prüfen
|
||||
|
||||
Nach dem Speichern sollte der passende Eval-Typ ausgeführt werden.
|
||||
|
||||
Im Admin:
|
||||
|
||||
```text
|
||||
/admin/evals/
|
||||
```
|
||||
|
||||
Oder per CLI:
|
||||
|
||||
```bash
|
||||
php bin/console mto:agent:config:validate
|
||||
php bin/console mto:agent:eval:run retrieval
|
||||
php bin/console mto:agent:eval:run shop_query
|
||||
php bin/console mto:agent:eval:run followup
|
||||
php bin/console mto:agent:eval:run answer_guard
|
||||
```
|
||||
|
||||
Für einen einzelnen Typ:
|
||||
|
||||
```bash
|
||||
php bin/console mto:agent:eval:run followup
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Praktische Checkliste
|
||||
|
||||
Vor dem Speichern prüfen:
|
||||
|
||||
```text
|
||||
[ ] Eval-Typ passt zum Ziel
|
||||
[ ] Case-ID ist eindeutig
|
||||
[ ] Case-ID enthält nur Buchstaben, Zahlen, _ oder -
|
||||
[ ] Prompt ist realistisch und exakt
|
||||
[ ] Assert-JSON ist gültiges JSON-Objekt
|
||||
[ ] History-JSON ist bei Follow-up-Cases vorhanden
|
||||
[ ] History-JSON ist gültiges JSON-Array
|
||||
[ ] Der Case prüft nur einen klaren Sachverhalt
|
||||
[ ] Assertions sind nicht unnötig streng
|
||||
[ ] Nach dem Speichern läuft der passende Eval-Typ grün
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Wann ein neuer Eval-Case angelegt werden sollte
|
||||
|
||||
Ein neuer Case ist sinnvoll, wenn:
|
||||
|
||||
```text
|
||||
ein realer Prompt rot war
|
||||
ein wichtiger grüner Flow dauerhaft abgesichert werden soll
|
||||
ein Tippfehler-/Noise-Fall stabil bleiben soll
|
||||
eine Produktidentität nicht verloren gehen darf
|
||||
eine falsche Dokumentzuordnung verhindert werden soll
|
||||
eine No-Answer-Situation nicht halluzinieren darf
|
||||
```
|
||||
|
||||
Kein neuer Case ist nötig, wenn:
|
||||
|
||||
```text
|
||||
nur die Formulierung einer Antwort leicht anders war
|
||||
der Prompt fachlich nicht relevant ist
|
||||
die Erwartung nicht eindeutig definiert werden kann
|
||||
der Case mehrere unabhängige Dinge gleichzeitig prüfen würde
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Leitlinie
|
||||
|
||||
Ab RetrieX v1.6.2 gilt:
|
||||
|
||||
```text
|
||||
Keine neue Genauigkeitslogik ohne konkreten roten oder fachlich wichtigen Eval-Fall.
|
||||
```
|
||||
|
||||
Daher sollten neue Optimierungen möglichst immer so ablaufen:
|
||||
|
||||
```text
|
||||
1. Prompt testen
|
||||
2. Verhalten bewerten
|
||||
3. Wenn wichtig: Eval-Case anlegen
|
||||
4. Eval grün bekommen
|
||||
5. Erst danach Logik, YAML oder Parameter ändern
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
# RetrieX Patch p100d – Admin Eval Prompt Context
|
||||
|
||||
Status: patch-only follow-up for p100 Admin Eval UX.
|
||||
|
||||
## Goal
|
||||
|
||||
Make eval results easier to understand in the Admin UI by showing the actual case prompt directly next to the case id. For follow-up and shopquery cases, show a compact history/context preview as well.
|
||||
|
||||
## Changes
|
||||
|
||||
- Admin eval result table now displays the case prompt below the case id.
|
||||
- Follow-up/shopquery eval details now include a compact history preview.
|
||||
- Admin eval result table shows history/context in a collapsible section when available.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `src/Eval/ShopQueryEvalRunner.php`
|
||||
- `templates/admin/evals/index.html.twig`
|
||||
|
||||
## Non-goals
|
||||
|
||||
No production answer logic is changed:
|
||||
|
||||
- no retrieval logic changes
|
||||
- no shopquery logic changes
|
||||
- no follow-up logic changes
|
||||
- no answer-guard logic changes
|
||||
- no eval assertion changes
|
||||
- no YAML or parameter changes
|
||||
- no database migration
|
||||
|
||||
## Validation
|
||||
|
||||
Recommended after applying:
|
||||
|
||||
```bash
|
||||
php bin/console mto:agent:config:validate
|
||||
php bin/console mto:agent:eval:run retrieval
|
||||
php bin/console mto:agent:eval:run shop_query
|
||||
php bin/console mto:agent:eval:run followup
|
||||
php bin/console mto:agent:eval:run answer_guard
|
||||
```
|
||||
|
||||
Then open `/admin/evals/` and verify that each result row shows the case prompt and that follow-up/shopquery rows can reveal context/history.
|
||||
@@ -0,0 +1,66 @@
|
||||
# RetrieX Patch p101 - Admin Eval Case Creator
|
||||
|
||||
## Ziel
|
||||
|
||||
p101 ergänzt die bestehende Admin Eval Suite um einen kleinen Case-Creator, damit neue Regression-Cases direkt aus dem Admin heraus in die passenden NDJSON-Dateien geschrieben werden können.
|
||||
|
||||
Der Patch baut auf dem grünen p100/p100a/p100b/p100c/p100d-Stand auf und verändert keine produktive RAG-, Shopquery-, Follow-up- oder Antwortlogik.
|
||||
|
||||
## Änderungen
|
||||
|
||||
- Neue POST-Route im Admin:
|
||||
- `/admin/evals/case/create`
|
||||
- Route-Name: `admin_evals_case_create`
|
||||
- `EvalAdminService::createCase()` zum validierten Schreiben neuer Eval-Cases.
|
||||
- Neues Formular auf `/admin/evals/`:
|
||||
- Eval-Typ
|
||||
- Case-ID
|
||||
- Prompt
|
||||
- Assert-JSON
|
||||
- optionales History-JSON
|
||||
- optionaler Request Context Hint
|
||||
- Button pro Report-Result:
|
||||
- `Als neuen Case vorbereiten`
|
||||
- übernimmt Prompt, Typ, History-Vorschau, Query oder Dokument-ID als Vorlage in den Creator.
|
||||
- JSON-/ID-Validierung vor dem Schreiben.
|
||||
- Duplicate-Guard über alle Eval-Typen.
|
||||
|
||||
## Geschriebene Dateien
|
||||
|
||||
Neue Cases werden an folgende Dateien angehängt:
|
||||
|
||||
- `tests/evals/cases/retrieval.ndjson`
|
||||
- `tests/evals/cases/shop_query.ndjson`
|
||||
- `tests/evals/cases/followup.ndjson`
|
||||
- `tests/evals/cases/answer_guard.ndjson`
|
||||
|
||||
## Sicherheit / Scope
|
||||
|
||||
Nicht geändert:
|
||||
|
||||
- keine Retrieval-Gewichte
|
||||
- keine Shopquery-Logik
|
||||
- keine Follow-up-Logik
|
||||
- keine Answer-Guard-Logik
|
||||
- keine Prompt-/YAML-/Parameteränderung
|
||||
- keine Migration
|
||||
|
||||
## Manuelle Prüfung
|
||||
|
||||
```bash
|
||||
php bin/console mto:agent:config:validate
|
||||
php bin/console mto:agent:eval:run retrieval
|
||||
php bin/console mto:agent:eval:run shop_query
|
||||
php bin/console mto:agent:eval:run followup
|
||||
php bin/console mto:agent:eval:run answer_guard
|
||||
```
|
||||
|
||||
Zusätzlich im Admin:
|
||||
|
||||
1. `/admin/evals/` öffnen.
|
||||
2. Einen Eval laufen lassen.
|
||||
3. Bei einem Result `Als neuen Case vorbereiten` klicken.
|
||||
4. Case-ID anpassen bzw. prüfen.
|
||||
5. Assert-JSON prüfen.
|
||||
6. Speichern.
|
||||
7. Den betroffenen Eval-Typ erneut laufen lassen.
|
||||
@@ -67,4 +67,45 @@ final class AdminEvalController extends AbstractController
|
||||
'type' => $type,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/case/create', name: 'admin_evals_case_create', methods: ['POST'])]
|
||||
public function createCase(Request $request, EvalAdminService $evals): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_KNOWLEDGE_ADMIN);
|
||||
|
||||
if (!$this->isCsrfTokenValid('admin_eval_case_create', (string) $request->request->get('_token'))) {
|
||||
throw $this->createAccessDeniedException();
|
||||
}
|
||||
|
||||
$type = trim((string) $request->request->get('type', 'retrieval'));
|
||||
|
||||
try {
|
||||
$created = $evals->createCase(
|
||||
type: $type,
|
||||
id: (string) $request->request->get('id', ''),
|
||||
prompt: (string) $request->request->get('prompt', ''),
|
||||
assertJson: (string) $request->request->get('assert_json', ''),
|
||||
historyJson: (string) $request->request->get('history_json', ''),
|
||||
requestContextHint: (string) $request->request->get('request_context_hint', ''),
|
||||
);
|
||||
|
||||
$type = (string) ($created['type'] ?? $type);
|
||||
|
||||
$this->addFlash(
|
||||
'success',
|
||||
sprintf('Eval-Case "%s" wurde in %s.ndjson gespeichert.', (string) ($created['id'] ?? ''), $type)
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
$this->addFlash('danger', $e->getMessage());
|
||||
}
|
||||
|
||||
if (!in_array($type, $evals->supportedTypeNames(), true)) {
|
||||
$type = 'retrieval';
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_evals_index', [
|
||||
'type' => $type,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ final readonly class ShopQueryEvalRunner
|
||||
details: [
|
||||
'prompt' => $case->prompt,
|
||||
'history_turns' => count($case->history),
|
||||
'history' => $this->buildHistoryPreview($case->history),
|
||||
'has_request_context_hint' => $case->requestContextHint !== '',
|
||||
'query' => $shopMeta['query'],
|
||||
'individual_queries' => $shopMeta['individual_queries'],
|
||||
@@ -82,6 +83,31 @@ final readonly class ShopQueryEvalRunner
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{prompt:string,answer:string}> $history
|
||||
* @return array<int, array{prompt:string,answer_preview:string}>
|
||||
*/
|
||||
private function buildHistoryPreview(array $history): array
|
||||
{
|
||||
$preview = [];
|
||||
|
||||
foreach ($history as $turn) {
|
||||
$prompt = trim((string) ($turn['prompt'] ?? ''));
|
||||
$answer = trim((string) ($turn['answer'] ?? ''));
|
||||
|
||||
if ($prompt === '' && $answer === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$preview[] = [
|
||||
'prompt' => $prompt !== '' ? $prompt : 'Eval-Kontext',
|
||||
'answer_preview' => $this->previewText($answer, 260),
|
||||
];
|
||||
}
|
||||
|
||||
return $preview;
|
||||
}
|
||||
|
||||
private function buildUserId(EvalCase $case): string
|
||||
{
|
||||
$safeId = preg_replace('/[^a-zA-Z0-9_-]+/', '_', $case->id) ?? $case->id;
|
||||
@@ -349,14 +375,15 @@ final readonly class ShopQueryEvalRunner
|
||||
return array_values(array_unique($out));
|
||||
}
|
||||
|
||||
private function previewText(string $value): string
|
||||
private function previewText(string $value, int $maxLength = 1200): string
|
||||
{
|
||||
$value = $this->normalizeOneLine($value);
|
||||
$maxLength = max(40, $maxLength);
|
||||
|
||||
if (mb_strlen($value, 'UTF-8') <= 1200) {
|
||||
if (mb_strlen($value, 'UTF-8') <= $maxLength) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return rtrim(mb_substr($value, 0, 1200, 'UTF-8')) . '...';
|
||||
return rtrim(mb_substr($value, 0, $maxLength, 'UTF-8')) . '...';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +145,83 @@ final readonly class EvalAdminService
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array{type:string,id:string,path:string,row:array<string,mixed>,case_count:int}
|
||||
*/
|
||||
public function createCase(
|
||||
string $type,
|
||||
string $id,
|
||||
string $prompt,
|
||||
string $assertJson,
|
||||
string $historyJson = '',
|
||||
string $requestContextHint = '',
|
||||
): array {
|
||||
$type = $this->assertSupportedType($type);
|
||||
$id = $this->normalizeNewCaseId($id);
|
||||
$prompt = trim($prompt);
|
||||
$requestContextHint = trim($requestContextHint);
|
||||
|
||||
if ($prompt === '') {
|
||||
throw new \InvalidArgumentException('Der Eval-Prompt darf nicht leer sein.');
|
||||
}
|
||||
|
||||
if ($this->caseIdExists($id)) {
|
||||
throw new \RuntimeException(sprintf(
|
||||
'Ein Eval-Case mit der ID "%s" existiert bereits. Bitte eine neue ID verwenden.',
|
||||
$id
|
||||
));
|
||||
}
|
||||
|
||||
$assert = $this->decodeJsonObject($assertJson, 'Assert-JSON');
|
||||
$history = $this->decodeHistoryJson($historyJson);
|
||||
|
||||
$row = [
|
||||
'id' => $id,
|
||||
'type' => $type,
|
||||
'prompt' => $prompt,
|
||||
'assert' => $assert,
|
||||
];
|
||||
|
||||
if ($history !== []) {
|
||||
$row['history'] = $history;
|
||||
}
|
||||
|
||||
if ($requestContextHint !== '') {
|
||||
$row['request_context_hint'] = $requestContextHint;
|
||||
}
|
||||
|
||||
// Reuse the regular DTO validation before writing the case file.
|
||||
EvalCase::fromArray($row);
|
||||
|
||||
$path = $this->caseFilePath($type);
|
||||
$line = json_encode(
|
||||
$row,
|
||||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR
|
||||
);
|
||||
|
||||
$prefix = '';
|
||||
if (is_file($path) && filesize($path) > 0) {
|
||||
$contents = file_get_contents($path);
|
||||
if (is_string($contents) && $contents !== '' && !str_ends_with($contents, "\n")) {
|
||||
$prefix = "\n";
|
||||
}
|
||||
}
|
||||
|
||||
$written = file_put_contents($path, $prefix . $line . PHP_EOL, FILE_APPEND | LOCK_EX);
|
||||
if ($written === false) {
|
||||
throw new \RuntimeException(sprintf('Eval-Case-Datei konnte nicht geschrieben werden: %s', $path));
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => $type,
|
||||
'id' => $id,
|
||||
'path' => $path,
|
||||
'row' => $row,
|
||||
'case_count' => count($this->loadCases($type)),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param array<int, EvalCase> $cases
|
||||
* @return array<int, EvalCase>
|
||||
@@ -249,6 +326,123 @@ final readonly class EvalAdminService
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
|
||||
private function normalizeNewCaseId(string $id): string
|
||||
{
|
||||
$id = trim($id);
|
||||
|
||||
if ($id === '') {
|
||||
throw new \InvalidArgumentException('Die Eval-Case-ID darf nicht leer sein.');
|
||||
}
|
||||
|
||||
if (preg_match('/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/', $id) !== 1) {
|
||||
throw new \InvalidArgumentException(
|
||||
'Die Eval-Case-ID darf nur Buchstaben, Zahlen, Unterstriche und Bindestriche enthalten und muss mit einem Buchstaben oder einer Zahl beginnen.'
|
||||
);
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
private function caseIdExists(string $id): bool
|
||||
{
|
||||
foreach (array_keys(self::TYPES) as $type) {
|
||||
foreach ($this->loadCases($type) as $case) {
|
||||
if ($case->id === $id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function decodeJsonObject(string $json, string $label): array
|
||||
{
|
||||
$json = trim($json);
|
||||
|
||||
if ($json === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
$decoded = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (\JsonException $e) {
|
||||
throw new \InvalidArgumentException(sprintf('%s ist ungültig: %s', $label, $e->getMessage()));
|
||||
}
|
||||
|
||||
if (!is_array($decoded)) {
|
||||
throw new \InvalidArgumentException(sprintf('%s muss ein JSON-Objekt sein.', $label));
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{prompt:string,answer:string}>
|
||||
*/
|
||||
private function decodeHistoryJson(string $json): array
|
||||
{
|
||||
$json = trim($json);
|
||||
|
||||
if ($json === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
$decoded = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (\JsonException $e) {
|
||||
throw new \InvalidArgumentException(sprintf('History-JSON ist ungültig: %s', $e->getMessage()));
|
||||
}
|
||||
|
||||
if (!is_array($decoded)) {
|
||||
throw new \InvalidArgumentException('History-JSON muss eine JSON-Liste sein.');
|
||||
}
|
||||
|
||||
$history = [];
|
||||
|
||||
foreach ($decoded as $entry) {
|
||||
if (is_string($entry)) {
|
||||
$entry = trim($entry);
|
||||
if ($entry !== '') {
|
||||
$history[] = [
|
||||
'prompt' => 'Eval-Kontext',
|
||||
'answer' => $entry,
|
||||
];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!is_array($entry)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$prompt = trim((string) ($entry['prompt'] ?? ''));
|
||||
$answer = trim((string) ($entry['answer'] ?? $entry['response'] ?? $entry['answer_preview'] ?? ''));
|
||||
|
||||
if ($prompt === '' && $answer === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$history[] = [
|
||||
'prompt' => $prompt !== '' ? $prompt : 'Eval-Kontext',
|
||||
'answer' => $answer,
|
||||
];
|
||||
}
|
||||
|
||||
return $history;
|
||||
}
|
||||
|
||||
private function caseFilePath(string $type): string
|
||||
{
|
||||
$type = $this->assertSupportedType($type);
|
||||
|
||||
return sprintf('%s/tests/evals/cases/%s.ndjson', $this->projectDir, $type);
|
||||
}
|
||||
|
||||
private function statusFromReport(?array $report): string
|
||||
{
|
||||
if ($report === null) {
|
||||
|
||||
@@ -212,6 +212,100 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card bg-black border-secondary text-light shadow-sm mb-4" id="adminEvalCaseCreator">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start flex-wrap gap-2 mb-3">
|
||||
<div>
|
||||
<h5 class="text-warning mb-1">
|
||||
<i class="bi bi-plus-square"></i> Eval-Case erstellen
|
||||
</h5>
|
||||
<div class="small text-secondary">
|
||||
Speichert neue Regression-Cases direkt in <code>tests/evals/cases/<type>.ndjson</code>.
|
||||
Aus Report-Ergebnissen kannst du Prompt, History, Query oder Dokument-IDs als Vorlage übernehmen.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ path('admin_evals_case_create') }}" class="row g-3">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('admin_eval_case_create') }}">
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Eval-Typ</label>
|
||||
<select name="type" class="form-select bg-dark text-light border-secondary js-admin-eval-create-type">
|
||||
{% for type, label in types %}
|
||||
<option value="{{ type }}" {% if type == selected_type %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Neue Case-ID</label>
|
||||
<input type="text"
|
||||
name="id"
|
||||
class="form-control bg-dark text-light border-secondary js-admin-eval-create-id"
|
||||
placeholder="z. B. retrieval_semantic_new_001"
|
||||
autocomplete="off"
|
||||
required>
|
||||
<div class="form-text text-secondary">
|
||||
Erlaubt: Buchstaben, Zahlen, Unterstrich, Bindestrich. IDs müssen eindeutig sein.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label">Prompt</label>
|
||||
<textarea name="prompt"
|
||||
rows="2"
|
||||
class="form-control bg-dark text-light border-secondary js-admin-eval-create-prompt"
|
||||
placeholder="Testprompt, der abgesichert werden soll"
|
||||
required></textarea>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<label class="form-label">Assert-JSON</label>
|
||||
<textarea name="assert_json"
|
||||
rows="8"
|
||||
class="form-control bg-dark text-light border-secondary font-monospace small js-admin-eval-create-assert"
|
||||
spellcheck="false">{
|
||||
"min_results": 1
|
||||
}</textarea>
|
||||
<div class="form-text text-secondary">
|
||||
Beispiel: <code>expected_query</code>, <code>must_include_one_of_document_ids</code>, <code>must_not_include_terms</code>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<label class="form-label">Optional: History-JSON</label>
|
||||
<textarea name="history_json"
|
||||
rows="8"
|
||||
class="form-control bg-dark text-light border-secondary font-monospace small js-admin-eval-create-history"
|
||||
spellcheck="false"
|
||||
placeholder='[{"prompt":"...","answer":"..."}]'></textarea>
|
||||
<div class="form-text text-secondary">
|
||||
Für Follow-up-Cases: Liste vorheriger Chat-Turns mit <code>prompt</code> und <code>answer</code>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label">Optional: Request Context Hint</label>
|
||||
<textarea name="request_context_hint"
|
||||
rows="2"
|
||||
class="form-control bg-dark text-light border-secondary js-admin-eval-create-context"
|
||||
placeholder="Nur nutzen, wenn ein Case explizit Zusatzkontext braucht."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="col-12 d-flex gap-2 flex-wrap">
|
||||
<button type="submit" class="btn btn-outline-warning">
|
||||
<i class="bi bi-save"></i> Case speichern
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary js-admin-eval-create-clear">
|
||||
Formular leeren
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-black border-secondary text-light shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
|
||||
@@ -281,9 +375,49 @@
|
||||
<span class="badge bg-danger">FAIL</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<td style="min-width: 260px;">
|
||||
<code>{{ result.case_id|default('') }}</code>
|
||||
<div class="small text-secondary">{{ result.type|default('') }}</div>
|
||||
<div class="small text-secondary mb-2">{{ result.type|default('') }}</div>
|
||||
|
||||
{% set casePrompt = result.prompt|default(result.details.prompt|default('')) %}
|
||||
{% if casePrompt %}
|
||||
<div class="small mb-2">
|
||||
<span class="text-secondary">Prompt:</span><br>
|
||||
<span class="text-light">{{ casePrompt }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% set historyRows = result.details.history|default([]) %}
|
||||
{% if historyRows is not empty %}
|
||||
<details class="small">
|
||||
<summary class="text-info" style="cursor:pointer;">
|
||||
Kontext / History anzeigen
|
||||
</summary>
|
||||
<div class="mt-2 ps-2 border-start border-secondary">
|
||||
{% for turn in historyRows %}
|
||||
<div class="mb-2">
|
||||
<div class="text-secondary">Vorheriger Prompt:</div>
|
||||
<div class="text-light">{{ turn.prompt|default('') }}</div>
|
||||
{% if turn.answer_preview|default('') %}
|
||||
<div class="text-secondary mt-1">Antwort-Auszug:</div>
|
||||
<div class="text-secondary">{{ turn.answer_preview }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-warning mt-2 js-admin-eval-prefill-case"
|
||||
data-result-type="{{ result.type|default(selected_type)|e('html_attr') }}"
|
||||
data-result-prompt="{{ casePrompt|default('')|e('html_attr') }}"
|
||||
data-result-history="{{ historyRows|default([])|json_encode|e('html_attr') }}"
|
||||
data-result-query="{{ result.details.query|default('')|e('html_attr') }}"
|
||||
data-result-individual-queries="{{ result.details.individual_queries|default([])|json_encode|e('html_attr') }}"
|
||||
data-result-document-ids="{{ result.details.document_ids|default([])|json_encode|e('html_attr') }}">
|
||||
Als neuen Case vorbereiten
|
||||
</button>
|
||||
</td>
|
||||
<td style="width: 120px;">
|
||||
{{ result.duration_ms|default(0) }} ms
|
||||
@@ -461,6 +595,173 @@
|
||||
});
|
||||
}
|
||||
|
||||
const creator = document.getElementById('adminEvalCaseCreator');
|
||||
|
||||
function parseJsonData(value, fallback) {
|
||||
if (!value) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (error) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function slugifyPrompt(prompt) {
|
||||
const normalized = (prompt || '')
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/ä/g, 'ae')
|
||||
.replace(/ö/g, 'oe')
|
||||
.replace(/ü/g, 'ue')
|
||||
.replace(/ß/g, 'ss')
|
||||
.replace(/[^a-z0-9]+/g, '_')
|
||||
.replace(/^_+|_+$/g, '')
|
||||
.slice(0, 44);
|
||||
|
||||
return normalized || 'case';
|
||||
}
|
||||
|
||||
function buildAssertTemplate(type, query, individualQueries, documentIds) {
|
||||
if ((type === 'shop_query' || type === 'followup') && individualQueries.length > 0) {
|
||||
return {
|
||||
expected_individual_queries: individualQueries,
|
||||
expected_individual_queries_exact: true
|
||||
};
|
||||
}
|
||||
|
||||
if ((type === 'shop_query' || type === 'followup') && query) {
|
||||
return {
|
||||
expected_query: query
|
||||
};
|
||||
}
|
||||
|
||||
if ((type === 'retrieval' || type === 'answer_guard') && documentIds.length > 0) {
|
||||
return {
|
||||
min_results: 1,
|
||||
must_include_one_of_document_ids: [documentIds[0]]
|
||||
};
|
||||
}
|
||||
|
||||
if (type === 'answer_guard') {
|
||||
return {
|
||||
max_results: 0
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
min_results: 1
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeHistoryForForm(historyRows) {
|
||||
return historyRows
|
||||
.map(function (turn) {
|
||||
return {
|
||||
prompt: (turn.prompt || 'Eval-Kontext').trim(),
|
||||
answer: (turn.answer || turn.response || turn.answer_preview || '').trim()
|
||||
};
|
||||
})
|
||||
.filter(function (turn) {
|
||||
return turn.prompt !== '' || turn.answer !== '';
|
||||
});
|
||||
}
|
||||
|
||||
function fillCreatorFormFromResult(button) {
|
||||
if (!creator) {
|
||||
return;
|
||||
}
|
||||
|
||||
const type = button.dataset.resultType || 'retrieval';
|
||||
const prompt = button.dataset.resultPrompt || '';
|
||||
const history = normalizeHistoryForForm(parseJsonData(button.dataset.resultHistory, []));
|
||||
const query = button.dataset.resultQuery || '';
|
||||
const individualQueries = parseJsonData(button.dataset.resultIndividualQueries, []);
|
||||
const documentIds = parseJsonData(button.dataset.resultDocumentIds, []);
|
||||
const now = new Date();
|
||||
const suffix = String(now.getFullYear()).slice(2)
|
||||
+ String(now.getMonth() + 1).padStart(2, '0')
|
||||
+ String(now.getDate()).padStart(2, '0')
|
||||
+ '_'
|
||||
+ String(now.getHours()).padStart(2, '0')
|
||||
+ String(now.getMinutes()).padStart(2, '0')
|
||||
+ String(now.getSeconds()).padStart(2, '0');
|
||||
|
||||
const typeField = creator.querySelector('.js-admin-eval-create-type');
|
||||
const idField = creator.querySelector('.js-admin-eval-create-id');
|
||||
const promptField = creator.querySelector('.js-admin-eval-create-prompt');
|
||||
const assertField = creator.querySelector('.js-admin-eval-create-assert');
|
||||
const historyField = creator.querySelector('.js-admin-eval-create-history');
|
||||
const contextField = creator.querySelector('.js-admin-eval-create-context');
|
||||
|
||||
if (typeField) {
|
||||
typeField.value = type;
|
||||
}
|
||||
|
||||
if (idField) {
|
||||
idField.value = type + '_' + slugifyPrompt(prompt) + '_' + suffix;
|
||||
}
|
||||
|
||||
if (promptField) {
|
||||
promptField.value = prompt;
|
||||
}
|
||||
|
||||
if (assertField) {
|
||||
assertField.value = JSON.stringify(
|
||||
buildAssertTemplate(type, query, individualQueries, documentIds),
|
||||
null,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
if (historyField) {
|
||||
historyField.value = history.length > 0 ? JSON.stringify(history, null, 2) : '';
|
||||
}
|
||||
|
||||
if (contextField) {
|
||||
contextField.value = '';
|
||||
}
|
||||
|
||||
creator.scrollIntoView({behavior: 'smooth', block: 'start'});
|
||||
}
|
||||
|
||||
if (creator) {
|
||||
creator.querySelectorAll('.js-admin-eval-create-clear').forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
const idField = creator.querySelector('.js-admin-eval-create-id');
|
||||
const promptField = creator.querySelector('.js-admin-eval-create-prompt');
|
||||
const assertField = creator.querySelector('.js-admin-eval-create-assert');
|
||||
const historyField = creator.querySelector('.js-admin-eval-create-history');
|
||||
const contextField = creator.querySelector('.js-admin-eval-create-context');
|
||||
|
||||
if (idField) {
|
||||
idField.value = '';
|
||||
}
|
||||
if (promptField) {
|
||||
promptField.value = '';
|
||||
}
|
||||
if (assertField) {
|
||||
assertField.value = '{\n "min_results": 1\n}';
|
||||
}
|
||||
if (historyField) {
|
||||
historyField.value = '';
|
||||
}
|
||||
if (contextField) {
|
||||
contextField.value = '';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('.js-admin-eval-prefill-case').forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
fillCreatorFormFromResult(button);
|
||||
});
|
||||
});
|
||||
|
||||
forms.forEach(function (form) {
|
||||
syncCaseSelect(form);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user