p96 follow up
This commit is contained in:
@@ -73,6 +73,15 @@ security:
|
|||||||
- { path: ^/admin/login$, roles: PUBLIC_ACCESS }
|
- { path: ^/admin/login$, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/admin/logout$, roles: PUBLIC_ACCESS }
|
- { path: ^/admin/logout$, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/admin/users, roles: ROLE_SUPER_ADMIN }
|
- { path: ^/admin/users, roles: ROLE_SUPER_ADMIN }
|
||||||
|
- { path: ^/admin/system-logs, roles: ROLE_SUPER_ADMIN }
|
||||||
|
- { path: ^/admin/vector, roles: ROLE_SUPER_ADMIN }
|
||||||
|
- { path: '^/admin/system/(?:prompt|agent)', roles: ROLE_SUPER_ADMIN }
|
||||||
|
- { path: ^/admin/jobs/global-reindex, roles: ROLE_SUPER_ADMIN }
|
||||||
|
- { path: '^/admin/ingest-profiles/(?:create|activate|remove)', roles: ROLE_SUPER_ADMIN }
|
||||||
|
- { path: ^/admin/ingest-profiles, roles: ROLE_KNOWLEDGE_ADMIN }
|
||||||
|
- { path: ^/admin/model-config, roles: ROLE_KNOWLEDGE_ADMIN }
|
||||||
|
- { path: ^/admin/documents, roles: ROLE_EDITOR }
|
||||||
|
- { path: ^/admin/tags, roles: ROLE_EDITOR }
|
||||||
- { path: ^/admin, roles: ROLE_ADMIN_AREA }
|
- { path: ^/admin, roles: ROLE_ADMIN_AREA }
|
||||||
|
|
||||||
- { path: ^/chat/login$, roles: PUBLIC_ACCESS }
|
- { path: ^/chat/login$, roles: PUBLIC_ACCESS }
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ parameters:
|
|||||||
hide_when_material_query_matches_current: true
|
hide_when_material_query_matches_current: true
|
||||||
shop_results:
|
shop_results:
|
||||||
- label: Preis anzeigen
|
- label: Preis anzeigen
|
||||||
prompt: Zeige mir die Preise zu {shop_price_query}.
|
prompt: 'Zeige mir die Preise zu folgendem Produkt bzw. den Produkten: {shop_price_query}.'
|
||||||
action_type: price_details
|
action_type: price_details
|
||||||
hide_when_answer_matches_any:
|
hide_when_answer_matches_any:
|
||||||
- '/\b(?:preis(?:angabe|information|e)?|preise?)\b.{0,100}\b(?:nicht|kein(?:e|en)?|ohne)\b.{0,100}\b(?:angegeben|vorhanden|enthalten|ausgewiesen|gefunden|verfügbar|verfuegbar)\b/iu'
|
- '/\b(?:preis(?:angabe|information|e)?|preise?)\b.{0,100}\b(?:nicht|kein(?:e|en)?|ohne)\b.{0,100}\b(?:angegeben|vorhanden|enthalten|ausgewiesen|gefunden|verfügbar|verfuegbar)\b/iu'
|
||||||
|
|||||||
@@ -1208,6 +1208,14 @@ parameters:
|
|||||||
- kostet
|
- kostet
|
||||||
- shopsuche
|
- shopsuche
|
||||||
- shop-suche
|
- shop-suche
|
||||||
|
- information
|
||||||
|
- informationen
|
||||||
|
- info
|
||||||
|
- infos
|
||||||
|
- details
|
||||||
|
- detail
|
||||||
|
- daten
|
||||||
|
- angaben
|
||||||
context_fallback_filter_terms:
|
context_fallback_filter_terms:
|
||||||
- preis
|
- preis
|
||||||
- preise
|
- preise
|
||||||
@@ -1217,6 +1225,14 @@ parameters:
|
|||||||
- grenzwert
|
- grenzwert
|
||||||
- grenzwerte
|
- grenzwerte
|
||||||
- grenzwerten
|
- grenzwerten
|
||||||
|
- information
|
||||||
|
- informationen
|
||||||
|
- info
|
||||||
|
- infos
|
||||||
|
- details
|
||||||
|
- detail
|
||||||
|
- daten
|
||||||
|
- angaben
|
||||||
- welche
|
- welche
|
||||||
- gut
|
- gut
|
||||||
- geeignet
|
- geeignet
|
||||||
@@ -1286,6 +1302,7 @@ parameters:
|
|||||||
- wuerde
|
- wuerde
|
||||||
- will
|
- will
|
||||||
- brauche
|
- brauche
|
||||||
|
- explizit
|
||||||
- benötigen
|
- benötigen
|
||||||
- benoetigen
|
- benoetigen
|
||||||
- benötige
|
- benötige
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
# RetrieX Patch p95 - Admin Role Matrix Hardening
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Dieser Patch gleicht die im Admin sichtbare Rollenlogik mit der serverseitigen Zugriffskontrolle ab. Rollen sollen nicht nur in der Userverwaltung auswählbar sein, sondern an den relevanten Routen, Controllern und UI-Aktionen konsistent wirksam werden.
|
||||||
|
|
||||||
|
## Ausgangslage
|
||||||
|
|
||||||
|
Nach p93/p94 waren Userverwaltung, Aktiv/Inaktiv-Login-Blocker und Access-Denied-Seiten funktionsfähig. Bei der Rollenprüfung fielen aber mehrere Inkonsistenzen auf:
|
||||||
|
|
||||||
|
- `ROLE_EDITOR` existierte in der Hierarchie, wurde aber kaum als fachliche Serverberechtigung genutzt.
|
||||||
|
- Einige Aktionen waren im UI nur für Super-Admins sichtbar, serverseitig aber nur durch den allgemeinen `/admin`-Zugriff geschützt.
|
||||||
|
- Indexierungsprofile konnten serverseitig nicht streng genug zwischen Ansicht und kritischen Aktionen unterscheiden.
|
||||||
|
- `admin_ingest_profile_remove` erlaubte noch `GET` als Methode.
|
||||||
|
- Sidebar und Dashboard zeigten Links bzw. Aktionen, die für die jeweilige Rolle später nur in 403 endeten.
|
||||||
|
|
||||||
|
## Umgesetzte Zielmatrix
|
||||||
|
|
||||||
|
| Bereich | Effektive Rolle |
|
||||||
|
| --- | --- |
|
||||||
|
| Chat, Ask/SSE, History, Frontend-Messages | `ROLE_CHAT_USER` |
|
||||||
|
| Admin-Dashboard, Guides, allgemeine Adminbasis, Ingest-Job-Ansicht | `ROLE_ADMIN_AREA` |
|
||||||
|
| Dokumente, Dokumentversionen, Tags, Tag-Zuweisungen, Dokument-Ingest | `ROLE_EDITOR` |
|
||||||
|
| Modell-/Retrieval-Konfiguration lesen/testen, Ingest-Profile ansehen | `ROLE_KNOWLEDGE_ADMIN` |
|
||||||
|
| Userverwaltung, Logs, System Prompt, System Agent, Reset/Delete, Global Reindex, Profil-/Modell-Umschaltungen | `ROLE_SUPER_ADMIN` |
|
||||||
|
|
||||||
|
Die bestehende Hierarchie bleibt erhalten:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ROLE_SUPER_ADMIN: [ROLE_KNOWLEDGE_ADMIN, ROLE_EDITOR, ROLE_ADMIN_AREA, ROLE_CHAT_USER, ROLE_USER]
|
||||||
|
ROLE_KNOWLEDGE_ADMIN: [ROLE_EDITOR, ROLE_ADMIN_AREA, ROLE_CHAT_USER, ROLE_USER]
|
||||||
|
ROLE_EDITOR: [ROLE_ADMIN_AREA, ROLE_CHAT_USER, ROLE_USER]
|
||||||
|
ROLE_ADMIN_AREA: [ROLE_USER]
|
||||||
|
ROLE_CHAT_USER: [ROLE_USER]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Geänderte Dateien
|
||||||
|
|
||||||
|
### Controller / Security
|
||||||
|
|
||||||
|
- `config/packages/security.yaml`
|
||||||
|
- ergänzt konkrete `access_control`-Regeln vor dem generischen `/admin`-Fallback
|
||||||
|
- schützt Dokument-/Tag-Bereiche mit `ROLE_EDITOR`
|
||||||
|
- schützt Modell-/Ingest-Konfigbereiche mit `ROLE_KNOWLEDGE_ADMIN`
|
||||||
|
- schützt kritische Systembereiche mit `ROLE_SUPER_ADMIN`
|
||||||
|
|
||||||
|
- `src/Security/ApplicationRoles.php`
|
||||||
|
- Dokumentation der zentralen Rollenquelle erweitert
|
||||||
|
|
||||||
|
- `src/Controller/Admin/DocumentController.php`
|
||||||
|
- Dokumentliste/-details und alle Dokumentpflegeaktionen nun `ROLE_EDITOR`
|
||||||
|
- Reset/Delete bleiben `ROLE_SUPER_ADMIN`
|
||||||
|
- Rollenstrings auf `ApplicationRoles`-Konstanten umgestellt
|
||||||
|
|
||||||
|
- `src/Controller/Admin/DocumentTagController.php`
|
||||||
|
- Tagbearbeitung auf Dokumentebene nun `ROLE_EDITOR`
|
||||||
|
|
||||||
|
- `src/Controller/Admin/TagController.php`
|
||||||
|
- Tagliste, Taganlage, Löschung und Zuweisung nun `ROLE_EDITOR`
|
||||||
|
|
||||||
|
- `src/Controller/Admin/IngestProfileController.php`
|
||||||
|
- Profilansicht nun `ROLE_KNOWLEDGE_ADMIN`
|
||||||
|
- Profilanlage, Aktivierung und Löschung nun `ROLE_SUPER_ADMIN`
|
||||||
|
- `remove` nur noch per `POST`
|
||||||
|
- Aktivieren/Löschen prüfen jetzt serverseitig CSRF-Token
|
||||||
|
|
||||||
|
- `src/Controller/Admin/ModelGenerationConfigController.php`
|
||||||
|
- `src/Controller/Admin/IngestJobController.php`
|
||||||
|
- `src/Controller/Admin/AdminVectorLogController.php`
|
||||||
|
- `src/Controller/Admin/AdminSystemLogController.php`
|
||||||
|
- `src/Controller/Admin/SystemAgentController.php`
|
||||||
|
- `src/Controller/Admin/SystemPromptController.php`
|
||||||
|
- `src/Controller/Admin/UserController.php`
|
||||||
|
- Rollenstrings auf zentrale `ApplicationRoles`-Konstanten umgestellt
|
||||||
|
|
||||||
|
### Templates
|
||||||
|
|
||||||
|
- `templates/admin/base.html.twig`
|
||||||
|
- Sidebar blendet Rollenbereiche passend aus:
|
||||||
|
- User: Super-Admin
|
||||||
|
- Dokumente/Tags: Editor
|
||||||
|
- System Prompt/Agent/Logs: Super-Admin
|
||||||
|
- KI-/LLM-Setup und Ingest-Profile: Knowledge-Admin
|
||||||
|
|
||||||
|
- `templates/admin/dashboard/index.html.twig`
|
||||||
|
- Global-Reindex-Button nur noch für Super-Admins sichtbar
|
||||||
|
- Nicht-Super-Admins sehen einen Hinweis statt eines Buttons, der in 403 läuft
|
||||||
|
|
||||||
|
- `templates/admin/document/index.html.twig`
|
||||||
|
- `templates/admin/document/show.html.twig`
|
||||||
|
- `templates/admin/document/new_version.html.twig`
|
||||||
|
- Dokumentpflege- und Versionsaktionen auf `ROLE_EDITOR` ausgerichtet
|
||||||
|
- Dokumentlöschung bleibt `ROLE_SUPER_ADMIN`
|
||||||
|
|
||||||
|
- `templates/admin/ingest_profile/list.html.twig`
|
||||||
|
- Profilanlage nur noch für Super-Admins sichtbar
|
||||||
|
- Aktivieren/Löschen bleiben Super-Admin-Aktionen
|
||||||
|
|
||||||
|
- `templates/admin/tag/index.html.twig`
|
||||||
|
- Taganlage, Zuweisung und Löschung nur für Editor sichtbar
|
||||||
|
- Nicht-Editor sehen keine mutierenden Aktionen
|
||||||
|
|
||||||
|
## Nicht geändert
|
||||||
|
|
||||||
|
- RAG-/Retrieval-Logik
|
||||||
|
- Scoring
|
||||||
|
- Shop-Matching
|
||||||
|
- PromptBuilder
|
||||||
|
- AgentRunner
|
||||||
|
- Chat-Antwortlogik
|
||||||
|
- User-CRUD-Fachlogik aus p93
|
||||||
|
- Error-Page-/Access-Denied-Grundlogik aus p94
|
||||||
|
|
||||||
|
## Lokale Checks
|
||||||
|
|
||||||
|
Ausgeführt im ZIP-Stand ohne `vendor/`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
find src/Controller/Admin src/Security src/Repository src/Service/Admin -type f -name '*.php' -print0 | xargs -0 -n1 php -l
|
||||||
|
python3 - <<'PY'
|
||||||
|
from pathlib import Path
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
yaml.safe_load(Path('config/packages/security.yaml').read_text())
|
||||||
|
print('[OK] yaml parse')
|
||||||
|
PY
|
||||||
|
```
|
||||||
|
|
||||||
|
Zusätzlich wurde eine einfache Twig-Balance-Prüfung auf `{% if %}`, `{% for %}` und `{% block %}` für die geänderten Templates durchgeführt.
|
||||||
|
|
||||||
|
## Empfohlene Checks in der Zielumgebung
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php bin/console cache:clear
|
||||||
|
php bin/console lint:yaml config/packages/security.yaml
|
||||||
|
php bin/console lint:twig templates/admin
|
||||||
|
php bin/console debug:router | grep admin_
|
||||||
|
php bin/console mto:agent:config:validate
|
||||||
|
php bin/console mto:agent:regression:test
|
||||||
|
php bin/console mto:agent:config:audit-source --details
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manuelle Rollentests
|
||||||
|
|
||||||
|
### `ROLE_ADMIN_AREA`
|
||||||
|
|
||||||
|
Soll können:
|
||||||
|
|
||||||
|
- `/admin/dashboard`
|
||||||
|
- `/admin/guides`
|
||||||
|
- `/admin/jobs`
|
||||||
|
|
||||||
|
Soll verweigert bekommen:
|
||||||
|
|
||||||
|
- `/admin/documents`
|
||||||
|
- `/admin/tags`
|
||||||
|
- `/admin/model-config`
|
||||||
|
- `/admin/ingest-profiles`
|
||||||
|
- `/admin/users`
|
||||||
|
|
||||||
|
### `ROLE_EDITOR`
|
||||||
|
|
||||||
|
Soll können:
|
||||||
|
|
||||||
|
- Dokumente ansehen/anlegen
|
||||||
|
- Dokumentversionen hochladen
|
||||||
|
- Versionen aktivieren
|
||||||
|
- Ingest für Dokumentversionen starten
|
||||||
|
- Tags anlegen/löschen/zuweisen
|
||||||
|
|
||||||
|
Soll verweigert bekommen:
|
||||||
|
|
||||||
|
- Userverwaltung
|
||||||
|
- System Prompt
|
||||||
|
- System Agent
|
||||||
|
- Systemlogs / Vectorlog
|
||||||
|
- Global Reindex
|
||||||
|
- Dokumentlöschung / kompletter Reset
|
||||||
|
- Ingest-Profil aktivieren/löschen
|
||||||
|
|
||||||
|
### `ROLE_KNOWLEDGE_ADMIN`
|
||||||
|
|
||||||
|
Soll können:
|
||||||
|
|
||||||
|
- alles aus `ROLE_EDITOR`
|
||||||
|
- Modellkonfiguration ansehen/testen
|
||||||
|
- Ingest-Profile ansehen
|
||||||
|
|
||||||
|
Soll verweigert bekommen:
|
||||||
|
|
||||||
|
- Userverwaltung
|
||||||
|
- System Prompt/Agent
|
||||||
|
- Logs
|
||||||
|
- Profil-/Modell-Aktivierung und Löschung
|
||||||
|
- Global Reindex
|
||||||
|
- Reset/Delete
|
||||||
|
|
||||||
|
### `ROLE_SUPER_ADMIN`
|
||||||
|
|
||||||
|
Soll alles können.
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
# RetrieX Patch 96 - Follow-up Product Identity & Weak History Guard
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Dieser Patch haertet zwei beobachtete Folgefrage-Fehlerklassen, ohne Retrieval, Scoring, Shop-Ranking oder Shop-Matching fachlich umzubauen:
|
||||||
|
|
||||||
|
1. **Preis-Folgeaktionen nach mehreren sichtbaren Produkten** duerfen nicht mehr auf den ersten generischen Modellanker zurueckfallen.
|
||||||
|
- Fehlerfall: Antwort nennt `Testomat 2000 CLT` und `Testomat LAB CL`; der Button `Preis anzeigen` erzeugte nur `Testomat 2000`.
|
||||||
|
- Neu: Wenn mehrere in der Antwort sichtbare Produkte erkannt werden, wird die Preis-Folgeaktion aus den konkreten Produktidentitaeten mit Produktnummern gebaut.
|
||||||
|
|
||||||
|
2. **Referenzielle Shop-Nachfragen mit schwacher Query** wie `suche im Shop nach der Information` duerfen den Produktfokus aus der History nicht verlieren.
|
||||||
|
- Fehlerfall: Nach `Testomat 2000 THCL` wurde die Shopquery zu `information` statt zum Verlaufanker `testomat 2000 thcl`.
|
||||||
|
- Neu: Schwache referenzielle Shopqueries, die nur aus Meta-/Info-/Noise-Tokens bestehen, werden mit dem letzten Produktmodellanker aus der History ersetzt.
|
||||||
|
|
||||||
|
## Geaenderte Dateien
|
||||||
|
|
||||||
|
- `src/Agent/AgentRunner.php`
|
||||||
|
- `config/retriex/chat-messages.yaml`
|
||||||
|
- `config/retriex/genre.yaml`
|
||||||
|
|
||||||
|
## Technische Umsetzung
|
||||||
|
|
||||||
|
### 1. Preis-Folgeaktionen behalten mehrere konkrete Produktidentitaeten
|
||||||
|
|
||||||
|
`buildFollowUpActionPriceQuery()` faellt bei mehreren angezeigten Produkten nicht mehr auf `answer_anchor` zurueck. Stattdessen wird eine begrenzte Produktliste aus den sichtbaren Shopprodukten gebaut, inklusive Produktnummern.
|
||||||
|
|
||||||
|
Beispiel erwarteter Button-Prompt:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Zeige mir die Preise zu folgendem Produkt bzw. den Produkten: Testomat 2000® CLT 100137; Testomat® LAB CL 116106.
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Prompt enthaelt bewusst `Produkt/Produkten` und `Preise`, damit die bestehende Product-List-Follow-up-Logik fuer mehrere Produkte greifen kann und einzelne Produktanker separat suchen kann.
|
||||||
|
|
||||||
|
### 2. Schwache referenzielle Shopqueries nutzen History-Modellanker
|
||||||
|
|
||||||
|
Neue Guard-Funktion:
|
||||||
|
|
||||||
|
```php
|
||||||
|
guardWeakReferentialShopQueryWithHistoryModelAnchor()
|
||||||
|
```
|
||||||
|
|
||||||
|
Sie greift nur, wenn:
|
||||||
|
|
||||||
|
- die aktuelle Frage referenziell genug ist, um History fuer Shopqueries zu nutzen,
|
||||||
|
- die Shopquery keinen eigenen Produktmodellanker enthaelt,
|
||||||
|
- der Prompt selbst keinen neuen Produktmodellanker enthaelt,
|
||||||
|
- es kein Product-List-Follow-up ist,
|
||||||
|
- es kein Indikator-/Zubehoer-/Reagenz-Follow-up ist,
|
||||||
|
- die Query nur aus schwachen Meta-/Info-/Noise-Tokens besteht,
|
||||||
|
- in der History ein Produktmodellanker gefunden wird.
|
||||||
|
|
||||||
|
Die schwachen Info-Woerter werden in `genre.yaml` gepflegt:
|
||||||
|
|
||||||
|
- `information`
|
||||||
|
- `informationen`
|
||||||
|
- `info`
|
||||||
|
- `infos`
|
||||||
|
- `details`
|
||||||
|
- `detail`
|
||||||
|
- `daten`
|
||||||
|
- `angaben`
|
||||||
|
|
||||||
|
## Bewusst nicht geaendert
|
||||||
|
|
||||||
|
- Keine neue fachliche Produktliste im PHP-Core.
|
||||||
|
- Keine neue Testomat-/THCL-/CLT-/LAB-CL-Sonderlogik im Core.
|
||||||
|
- Keine Aenderung an Retrieval, Scoring, Ranking, Shopware-Suche oder PromptBuilder-Faktenregeln.
|
||||||
|
- Bestehende Indikator-/Zubehoer-Follow-up-Guards bleiben priorisiert.
|
||||||
|
|
||||||
|
## Lokale Checks
|
||||||
|
|
||||||
|
Im Patch-Arbeitsverzeichnis ausgefuehrt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php -l src/Agent/AgentRunner.php
|
||||||
|
python3 - <<'PY'
|
||||||
|
import yaml, pathlib
|
||||||
|
for path in pathlib.Path('config/retriex').glob('*.yaml'):
|
||||||
|
with path.open(encoding='utf-8') as f:
|
||||||
|
yaml.safe_load(f)
|
||||||
|
print('all retriex yaml OK')
|
||||||
|
PY
|
||||||
|
```
|
||||||
|
|
||||||
|
Ergebnis:
|
||||||
|
|
||||||
|
- `AgentRunner.php`: Syntax OK
|
||||||
|
- alle `config/retriex/*.yaml`: YAML OK
|
||||||
|
|
||||||
|
Nicht lokal ausfuehrbar, weil `vendor/` im ZIP nicht enthalten ist:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/console mto:agent:config:validate
|
||||||
|
bin/console mto:agent:regression:test
|
||||||
|
bin/console mto:agent:config:audit-source --details
|
||||||
|
bin/console mto:agent:config:audit-patterns --details
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testprompts nach Einspielen
|
||||||
|
|
||||||
|
### Fehlerfall 1: Preis-Folgeaktion nach zwei Chlor-Geraeten
|
||||||
|
|
||||||
|
```text
|
||||||
|
mit welchem testomat kann ich freies chlor messen
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartung in der Antwort:
|
||||||
|
|
||||||
|
- Hauptprodukte bleiben fokussiert auf die passenden Chlor-Geraete, insbesondere:
|
||||||
|
- `Testomat 2000® CLT` / `100137`
|
||||||
|
- `Testomat® LAB CL` / `116106`
|
||||||
|
- Folgeaktion `Preis anzeigen` darf nicht mehr nur `Testomat 2000` materialisieren.
|
||||||
|
|
||||||
|
Danach den angezeigten Preis-Button klicken oder sinngleich testen:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Zeige mir die Preise zu folgendem Produkt bzw. den Produkten: Testomat 2000® CLT 100137; Testomat® LAB CL 116106.
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartung:
|
||||||
|
|
||||||
|
- Shopquery/Split-Lookups fokussieren die konkreten Produktidentitaeten.
|
||||||
|
- Es duerfen nicht breit alle `Testomat 2000` Varianten wie Fe, Antox, Br2, ClO2 oder Wartungskoffer als Hauptantwort erscheinen.
|
||||||
|
|
||||||
|
### Fehlerfall 2: schwache Info-Shopnachfrage verliert THCL-Anker
|
||||||
|
|
||||||
|
```text
|
||||||
|
welche grenzwerte kann der testomat 2000 thcl messen
|
||||||
|
```
|
||||||
|
|
||||||
|
Dann:
|
||||||
|
|
||||||
|
```text
|
||||||
|
suche im shop nach der information
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional auch Tippfehler-Variante:
|
||||||
|
|
||||||
|
```text
|
||||||
|
such eim shop nach der information
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartung:
|
||||||
|
|
||||||
|
- Die finale Shopquery darf nicht `information` sein.
|
||||||
|
- Erwarteter Verlaufanker: `testomat 2000 thcl`.
|
||||||
|
- Shopantwort bleibt beim Geraet/Modellfokus und driftet nicht zu fremden Testomat-808-SiO2-Zubehoerteilen, Horiba-Testern oder allgemeinen Treffern ab.
|
||||||
|
|
||||||
|
### Gruener Gegenflow: Brauwasser bleibt stabil
|
||||||
|
|
||||||
|
```text
|
||||||
|
ich suche ein wasseranalyse messgerät für eine Brauerei, um das brauwasser messen zu können
|
||||||
|
```
|
||||||
|
|
||||||
|
Dann:
|
||||||
|
|
||||||
|
```text
|
||||||
|
begründe deine auswahl
|
||||||
|
```
|
||||||
|
|
||||||
|
Dann:
|
||||||
|
|
||||||
|
```text
|
||||||
|
was kostet das gerät denn
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartung:
|
||||||
|
|
||||||
|
- Auswahl bleibt auf `Testomat 2000® CAL` fokussiert, sofern die Begruendung CAL als Hauptempfehlung gesetzt hat.
|
||||||
|
- Preis-Follow-up bleibt `testomat 2000 cal gerät` bzw. entsprechend CAL-fokussiert.
|
||||||
|
- Keine Regression hin zu breiten `testomat 2000` Varianten.
|
||||||
|
|
||||||
|
### Bestehende Praezisionsregression: Indikator 300 bleibt stabil
|
||||||
|
|
||||||
|
```text
|
||||||
|
Was ist der niedrigste Grenzwert für die Wasserhärte, welcher mit einem Testomaten überwacht werden kann?
|
||||||
|
```
|
||||||
|
|
||||||
|
Dann:
|
||||||
|
|
||||||
|
```text
|
||||||
|
mit welchem indikator wird der wert gemessen
|
||||||
|
```
|
||||||
|
|
||||||
|
Dann:
|
||||||
|
|
||||||
|
```text
|
||||||
|
was kostet der indikator
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartung:
|
||||||
|
|
||||||
|
- `0,02 °dH` / `Testomat 808`
|
||||||
|
- `Indikatortyp 300`
|
||||||
|
- Shopquery weiterhin fokussiert, z. B. `testomat 808 300 indikator`
|
||||||
|
- Nur die zwei exakten Indikator-300-Produkte, keine `300 S`-Varianten und keine anderen Codes.
|
||||||
@@ -336,6 +336,26 @@ final readonly class AgentRunner
|
|||||||
$optimizedShopQuery = '';
|
$optimizedShopQuery = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$weakReferentialHistoryModelShopSearchQuery = $this->guardWeakReferentialShopQueryWithHistoryModelAnchor(
|
||||||
|
prompt: $originalPrompt,
|
||||||
|
shopSearchQuery: $shopSearchQuery,
|
||||||
|
commerceHistoryContext: $commerceHistoryContext
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($weakReferentialHistoryModelShopSearchQuery !== $shopSearchQuery) {
|
||||||
|
$this->agentLogger->info('Replaced weak referential shop query with history model anchor', [
|
||||||
|
'userId' => $userId,
|
||||||
|
'prompt' => $prompt,
|
||||||
|
'routingPrompt' => $routingPrompt,
|
||||||
|
'optimizedShopQuery' => $optimizedShopQuery,
|
||||||
|
'shopSearchQuery' => $shopSearchQuery,
|
||||||
|
'weakReferentialHistoryModelShopSearchQuery' => $weakReferentialHistoryModelShopSearchQuery,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$shopSearchQuery = $weakReferentialHistoryModelShopSearchQuery;
|
||||||
|
$optimizedShopQuery = '';
|
||||||
|
}
|
||||||
|
|
||||||
$productListAnchoredShopSearchQuery = $this->guardReferentialProductListShopQueryWithHistoryAnchors(
|
$productListAnchoredShopSearchQuery = $this->guardReferentialProductListShopQueryWithHistoryAnchors(
|
||||||
prompt: $originalPrompt,
|
prompt: $originalPrompt,
|
||||||
shopSearchQuery: $shopSearchQuery,
|
shopSearchQuery: $shopSearchQuery,
|
||||||
@@ -4148,6 +4168,84 @@ final readonly class AgentRunner
|
|||||||
: $modelAnchor;
|
: $modelAnchor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function guardWeakReferentialShopQueryWithHistoryModelAnchor(
|
||||||
|
string $prompt,
|
||||||
|
string $shopSearchQuery,
|
||||||
|
string $commerceHistoryContext
|
||||||
|
): string {
|
||||||
|
$shopSearchQuery = trim($shopSearchQuery);
|
||||||
|
|
||||||
|
if (
|
||||||
|
$shopSearchQuery === ''
|
||||||
|
|| trim($commerceHistoryContext) === ''
|
||||||
|
|| $this->referenceAnchorExtractor->extractFirstProductModelAnchor($prompt) !== ''
|
||||||
|
|| $this->referenceAnchorExtractor->extractFirstProductModelAnchor($shopSearchQuery) !== ''
|
||||||
|
|| $this->isReferentialProductListShopFollowUpPrompt($prompt)
|
||||||
|
) {
|
||||||
|
return $shopSearchQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->shouldUseCommerceHistoryForShopQuery($prompt)) {
|
||||||
|
return $shopSearchQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accessory and consumable follow-ups have their own detail-anchor guard.
|
||||||
|
if ($this->containsConfiguredShopQueryAnchorTrigger(trim($prompt . ' ' . $shopSearchQuery))) {
|
||||||
|
return $shopSearchQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isWeakReferentialHistoryModelShopQuery($shopSearchQuery)) {
|
||||||
|
return $shopSearchQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
$modelAnchor = $this->normalizeShopQueryAnchor(
|
||||||
|
$this->extractLatestHistoryProductModelAnchor($commerceHistoryContext)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($modelAnchor === '') {
|
||||||
|
return $shopSearchQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->queryAlreadyContainsAllAnchorTokens($shopSearchQuery, $modelAnchor)
|
||||||
|
? $shopSearchQuery
|
||||||
|
: $modelAnchor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isWeakReferentialHistoryModelShopQuery(string $shopSearchQuery): bool
|
||||||
|
{
|
||||||
|
if ($this->referenceAnchorExtractor->extractFirstProductModelAnchor($shopSearchQuery) !== '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tokens = $this->tokenizeShopQueryCandidate($shopSearchQuery);
|
||||||
|
if ($tokens === []) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$weakTokens = $this->buildShopQueryTokenSet($this->mergeUniqueStrings(
|
||||||
|
$this->mergeUniqueStrings(
|
||||||
|
$this->getShopQueryMetaGuardTerms(),
|
||||||
|
$this->getShopQueryContextFallbackFilterTerms()
|
||||||
|
),
|
||||||
|
$this->mergeUniqueStrings(
|
||||||
|
$this->agentRunnerConfig->getShopQueryProductListFollowUpNoiseTerms(),
|
||||||
|
$this->agentRunnerConfig->getShopQueryStopwordCleanupTerms()
|
||||||
|
)
|
||||||
|
));
|
||||||
|
|
||||||
|
if ($weakTokens === []) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($tokens as $token) {
|
||||||
|
if (!isset($weakTokens[$token])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private function isMainDeviceReferentialShopQueryPrompt(string $prompt): bool
|
private function isMainDeviceReferentialShopQueryPrompt(string $prompt): bool
|
||||||
{
|
{
|
||||||
$tokens = $this->tokenizeShopQueryCandidate($prompt);
|
$tokens = $this->tokenizeShopQueryCandidate($prompt);
|
||||||
@@ -6864,15 +6962,27 @@ final readonly class AgentRunner
|
|||||||
return $this->buildFollowUpActionProductQuery($numberedProducts[0]);
|
return $this->buildFollowUpActionProductQuery($numberedProducts[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (count($numberedProducts) > 1) {
|
||||||
|
return $this->buildFollowUpActionProductListQuery($numberedProducts);
|
||||||
|
}
|
||||||
|
|
||||||
$focusedProducts = $this->filterFollowUpActionPrimaryDisplayedProducts($displayedProducts);
|
$focusedProducts = $this->filterFollowUpActionPrimaryDisplayedProducts($displayedProducts);
|
||||||
if (count($focusedProducts) === 1) {
|
if (count($focusedProducts) === 1) {
|
||||||
return $this->buildFollowUpActionProductQuery($focusedProducts[0]);
|
return $this->buildFollowUpActionProductQuery($focusedProducts[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (count($focusedProducts) > 1) {
|
||||||
|
return $this->buildFollowUpActionProductListQuery($focusedProducts);
|
||||||
|
}
|
||||||
|
|
||||||
if (count($displayedProducts) === 1) {
|
if (count($displayedProducts) === 1) {
|
||||||
return $this->buildFollowUpActionProductQuery($displayedProducts[0]);
|
return $this->buildFollowUpActionProductQuery($displayedProducts[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (count($displayedProducts) > 1) {
|
||||||
|
return $this->buildFollowUpActionProductListQuery($displayedProducts);
|
||||||
|
}
|
||||||
|
|
||||||
$answerAnchor = $this->buildFollowUpActionAnswerAnchor($answerText);
|
$answerAnchor = $this->buildFollowUpActionAnswerAnchor($answerText);
|
||||||
if ($answerAnchor !== '') {
|
if ($answerAnchor !== '') {
|
||||||
return $answerAnchor;
|
return $answerAnchor;
|
||||||
@@ -6941,6 +7051,37 @@ final readonly class AgentRunner
|
|||||||
return $this->normalizeOneLine(implode(' ', array_filter($parts, static fn(string $part): bool => trim($part) !== '')));
|
return $this->normalizeOneLine(implode(' ', array_filter($parts, static fn(string $part): bool => trim($part) !== '')));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ShopProductResult[] $products
|
||||||
|
*/
|
||||||
|
private function buildFollowUpActionProductListQuery(array $products): string
|
||||||
|
{
|
||||||
|
$queries = [];
|
||||||
|
$seen = [];
|
||||||
|
$maxProducts = max(1, $this->agentRunnerConfig->getShopQueryProductListFollowUpMaxAnchors());
|
||||||
|
|
||||||
|
foreach ($products as $product) {
|
||||||
|
if (!$product instanceof ShopProductResult) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = $this->buildFollowUpActionProductQuery($product);
|
||||||
|
$key = mb_strtolower($query, 'UTF-8');
|
||||||
|
if ($query === '' || isset($seen[$key])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$seen[$key] = true;
|
||||||
|
$queries[] = $query;
|
||||||
|
|
||||||
|
if (count($queries) >= $maxProducts) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->normalizeOneLine(implode('; ', $queries));
|
||||||
|
}
|
||||||
|
|
||||||
private function buildFollowUpActionAnswerAnchor(string $answerText): string
|
private function buildFollowUpActionAnswerAnchor(string $answerText): string
|
||||||
{
|
{
|
||||||
$anchors = [];
|
$anchors = [];
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Controller\Admin;
|
namespace App\Controller\Admin;
|
||||||
|
|
||||||
|
use App\Security\ApplicationRoles;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
@@ -21,7 +22,7 @@ final class AdminSystemLogController extends AbstractController
|
|||||||
#[Route('', name: 'admin_system_logs_index')]
|
#[Route('', name: 'admin_system_logs_index')]
|
||||||
public function index(): Response
|
public function index(): Response
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
|
||||||
|
|
||||||
$files = [];
|
$files = [];
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ final class AdminSystemLogController extends AbstractController
|
|||||||
)]
|
)]
|
||||||
public function view(string $date): Response
|
public function view(string $date): Response
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
|
||||||
|
|
||||||
if ($date === 'current') {
|
if ($date === 'current') {
|
||||||
$safeFilename = 'system.log';
|
$safeFilename = 'system.log';
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Controller\Admin;
|
namespace App\Controller\Admin;
|
||||||
|
|
||||||
|
use App\Security\ApplicationRoles;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
@@ -14,7 +15,7 @@ final class AdminVectorLogController extends AbstractController
|
|||||||
#[Route('/log', name: 'admin_vector_log')]
|
#[Route('/log', name: 'admin_vector_log')]
|
||||||
public function view(): Response
|
public function view(): Response
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
|
||||||
|
|
||||||
// ⚠️ Pfad ggf. anpassen falls bei dir anders konfiguriert
|
// ⚠️ Pfad ggf. anpassen falls bei dir anders konfiguriert
|
||||||
$logFile = \dirname(__DIR__, 3) . '/var/log/vector_service.log';
|
$logFile = \dirname(__DIR__, 3) . '/var/log/vector_service.log';
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use App\Entity\Document;
|
|||||||
use App\Entity\DocumentVersion;
|
use App\Entity\DocumentVersion;
|
||||||
use App\Entity\IngestJob;
|
use App\Entity\IngestJob;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
use App\Security\ApplicationRoles;
|
||||||
use App\Service\DocumentService;
|
use App\Service\DocumentService;
|
||||||
use App\Service\FormatText;
|
use App\Service\FormatText;
|
||||||
use App\Service\IngestJobService;
|
use App\Service\IngestJobService;
|
||||||
@@ -33,6 +34,8 @@ final class DocumentController extends AbstractController
|
|||||||
#[Route('', name: 'admin_documents', methods: ['GET'])]
|
#[Route('', name: 'admin_documents', methods: ['GET'])]
|
||||||
public function index(EntityManagerInterface $em): Response
|
public function index(EntityManagerInterface $em): Response
|
||||||
{
|
{
|
||||||
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR);
|
||||||
|
|
||||||
$documents = $em->getRepository(Document::class)
|
$documents = $em->getRepository(Document::class)
|
||||||
->createQueryBuilder('d')
|
->createQueryBuilder('d')
|
||||||
->leftJoin('d.versions', 'v')
|
->leftJoin('d.versions', 'v')
|
||||||
@@ -56,6 +59,8 @@ final class DocumentController extends AbstractController
|
|||||||
)]
|
)]
|
||||||
public function show(string $id, EntityManagerInterface $em): Response
|
public function show(string $id, EntityManagerInterface $em): Response
|
||||||
{
|
{
|
||||||
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR);
|
||||||
|
|
||||||
return $this->render('admin/document/show.html.twig', [
|
return $this->render('admin/document/show.html.twig', [
|
||||||
'document' => $this->findDocument($id, $em),
|
'document' => $this->findDocument($id, $em),
|
||||||
]);
|
]);
|
||||||
@@ -70,6 +75,8 @@ final class DocumentController extends AbstractController
|
|||||||
ParameterBagInterface $params,
|
ParameterBagInterface $params,
|
||||||
EntityManagerInterface $em,
|
EntityManagerInterface $em,
|
||||||
): Response {
|
): Response {
|
||||||
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR);
|
||||||
|
|
||||||
if (!$request->isMethod('POST')) {
|
if (!$request->isMethod('POST')) {
|
||||||
return $this->render('admin/document/new.html.twig');
|
return $this->render('admin/document/new.html.twig');
|
||||||
}
|
}
|
||||||
@@ -150,6 +157,8 @@ final class DocumentController extends AbstractController
|
|||||||
ParameterBagInterface $params,
|
ParameterBagInterface $params,
|
||||||
FormatText $formatText,
|
FormatText $formatText,
|
||||||
): Response {
|
): Response {
|
||||||
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR);
|
||||||
|
|
||||||
$document = $this->findDocument($id, $em);
|
$document = $this->findDocument($id, $em);
|
||||||
|
|
||||||
if (!$request->isMethod('POST')) {
|
if (!$request->isMethod('POST')) {
|
||||||
@@ -202,6 +211,8 @@ final class DocumentController extends AbstractController
|
|||||||
DocumentService $documentService,
|
DocumentService $documentService,
|
||||||
IngestJobService $jobService,
|
IngestJobService $jobService,
|
||||||
): RedirectResponse {
|
): RedirectResponse {
|
||||||
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR);
|
||||||
|
|
||||||
if (!$this->isCsrfTokenValid('activate_version_' . $versionId, (string) $request->request->get('_token'))) {
|
if (!$this->isCsrfTokenValid('activate_version_' . $versionId, (string) $request->request->get('_token'))) {
|
||||||
throw $this->createAccessDeniedException();
|
throw $this->createAccessDeniedException();
|
||||||
}
|
}
|
||||||
@@ -260,6 +271,8 @@ final class DocumentController extends AbstractController
|
|||||||
EntityManagerInterface $em,
|
EntityManagerInterface $em,
|
||||||
IngestJobService $jobService,
|
IngestJobService $jobService,
|
||||||
): RedirectResponse {
|
): RedirectResponse {
|
||||||
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR);
|
||||||
|
|
||||||
if (!$this->isCsrfTokenValid('ingest_version_' . $versionId, (string) $request->request->get('_token'))) {
|
if (!$this->isCsrfTokenValid('ingest_version_' . $versionId, (string) $request->request->get('_token'))) {
|
||||||
throw $this->createAccessDeniedException();
|
throw $this->createAccessDeniedException();
|
||||||
}
|
}
|
||||||
@@ -329,7 +342,7 @@ final class DocumentController extends AbstractController
|
|||||||
ParameterBagInterface $params,
|
ParameterBagInterface $params,
|
||||||
Connection $connection,
|
Connection $connection,
|
||||||
): RedirectResponse {
|
): RedirectResponse {
|
||||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
|
||||||
|
|
||||||
if (!$this->isCsrfTokenValid('system_reset', (string) $request->request->get('_token'))) {
|
if (!$this->isCsrfTokenValid('system_reset', (string) $request->request->get('_token'))) {
|
||||||
$this->addFlash('danger', 'Ungültiges CSRF-Token.');
|
$this->addFlash('danger', 'Ungültiges CSRF-Token.');
|
||||||
@@ -400,7 +413,7 @@ SQL;
|
|||||||
IngestJobService $jobService,
|
IngestJobService $jobService,
|
||||||
LockService $lockService,
|
LockService $lockService,
|
||||||
): RedirectResponse {
|
): RedirectResponse {
|
||||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
|
||||||
|
|
||||||
if (!$this->isCsrfTokenValid('delete_document_' . $id, (string) $request->request->get('_token'))) {
|
if (!$this->isCsrfTokenValid('delete_document_' . $id, (string) $request->request->get('_token'))) {
|
||||||
throw $this->createAccessDeniedException();
|
throw $this->createAccessDeniedException();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Controller\Admin;
|
namespace App\Controller\Admin;
|
||||||
|
|
||||||
use App\Entity\TagRebuildJob;
|
use App\Entity\TagRebuildJob;
|
||||||
|
use App\Security\ApplicationRoles;
|
||||||
use App\Service\Admin\DocumentTagAdminService;
|
use App\Service\Admin\DocumentTagAdminService;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
@@ -19,6 +20,8 @@ final class DocumentTagController extends AbstractController
|
|||||||
#[Route('/{id}/tags', name: 'admin_document_tags_edit', methods: ['GET'])]
|
#[Route('/{id}/tags', name: 'admin_document_tags_edit', methods: ['GET'])]
|
||||||
public function edit(string $id, DocumentTagAdminService $svc): Response
|
public function edit(string $id, DocumentTagAdminService $svc): Response
|
||||||
{
|
{
|
||||||
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR);
|
||||||
|
|
||||||
$id = trim($id);
|
$id = trim($id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -38,6 +41,8 @@ final class DocumentTagController extends AbstractController
|
|||||||
#[Route('/{id}/tags/save', name: 'admin_document_tags_save', methods: ['POST'])]
|
#[Route('/{id}/tags/save', name: 'admin_document_tags_save', methods: ['POST'])]
|
||||||
public function save(string $id, Request $request, DocumentTagAdminService $svc): RedirectResponse
|
public function save(string $id, Request $request, DocumentTagAdminService $svc): RedirectResponse
|
||||||
{
|
{
|
||||||
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR);
|
||||||
|
|
||||||
$id = trim($id);
|
$id = trim($id);
|
||||||
|
|
||||||
if (!$this->isCsrfTokenValid('admin_document_tags_save_' . $id, (string) $request->request->get('_token'))) {
|
if (!$this->isCsrfTokenValid('admin_document_tags_save_' . $id, (string) $request->request->get('_token'))) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Controller\Admin;
|
namespace App\Controller\Admin;
|
||||||
|
|
||||||
|
use App\Security\ApplicationRoles;
|
||||||
use App\Entity\IngestJob;
|
use App\Entity\IngestJob;
|
||||||
use App\Service\IngestJobService;
|
use App\Service\IngestJobService;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
@@ -50,7 +51,7 @@ final class IngestJobController extends AbstractController
|
|||||||
)]
|
)]
|
||||||
public function status(string $id, EntityManagerInterface $em): JsonResponse
|
public function status(string $id, EntityManagerInterface $em): JsonResponse
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('ROLE_ADMIN_AREA');
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_ADMIN_AREA);
|
||||||
|
|
||||||
$job = $this->findJob($id, $em);
|
$job = $this->findJob($id, $em);
|
||||||
|
|
||||||
@@ -71,7 +72,7 @@ final class IngestJobController extends AbstractController
|
|||||||
IngestJobService $jobService,
|
IngestJobService $jobService,
|
||||||
EntityManagerInterface $em,
|
EntityManagerInterface $em,
|
||||||
): RedirectResponse {
|
): RedirectResponse {
|
||||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
|
||||||
|
|
||||||
if (!$this->isCsrfTokenValid('global_reindex', (string) $request->request->get('_token'))) {
|
if (!$this->isCsrfTokenValid('global_reindex', (string) $request->request->get('_token'))) {
|
||||||
$this->addFlash('danger', 'Ungültiges CSRF-Token.');
|
$this->addFlash('danger', 'Ungültiges CSRF-Token.');
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Controller\Admin;
|
namespace App\Controller\Admin;
|
||||||
|
|
||||||
use App\Entity\IngestProfile;
|
use App\Entity\IngestProfile;
|
||||||
|
use App\Security\ApplicationRoles;
|
||||||
use App\Service\Admin\IngestProfileAdminService;
|
use App\Service\Admin\IngestProfileAdminService;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
@@ -17,6 +18,8 @@ final class IngestProfileController extends AbstractController
|
|||||||
#[Route('/', name: 'admin_ingest_profile_list')]
|
#[Route('/', name: 'admin_ingest_profile_list')]
|
||||||
public function list(IngestProfileAdminService $svc): Response
|
public function list(IngestProfileAdminService $svc): Response
|
||||||
{
|
{
|
||||||
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_KNOWLEDGE_ADMIN);
|
||||||
|
|
||||||
$data = $svc->listData();
|
$data = $svc->listData();
|
||||||
|
|
||||||
return $this->render('admin/ingest_profile/list.html.twig', $data);
|
return $this->render('admin/ingest_profile/list.html.twig', $data);
|
||||||
@@ -25,6 +28,8 @@ final class IngestProfileController extends AbstractController
|
|||||||
#[Route('/create', name: 'admin_ingest_profile_create', methods: ['GET', 'POST'])]
|
#[Route('/create', name: 'admin_ingest_profile_create', methods: ['GET', 'POST'])]
|
||||||
public function create(Request $request, IngestProfileAdminService $svc): Response
|
public function create(Request $request, IngestProfileAdminService $svc): Response
|
||||||
{
|
{
|
||||||
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
|
||||||
|
|
||||||
if ($request->isMethod('POST')) {
|
if ($request->isMethod('POST')) {
|
||||||
try {
|
try {
|
||||||
$svc->create([
|
$svc->create([
|
||||||
@@ -49,8 +54,15 @@ final class IngestProfileController extends AbstractController
|
|||||||
#[Route('/activate/{id}', name: 'admin_ingest_profile_activate', methods: ['POST'])]
|
#[Route('/activate/{id}', name: 'admin_ingest_profile_activate', methods: ['POST'])]
|
||||||
public function activate(
|
public function activate(
|
||||||
IngestProfile $profile,
|
IngestProfile $profile,
|
||||||
IngestProfileAdminService $svc
|
IngestProfileAdminService $svc,
|
||||||
|
Request $request
|
||||||
): Response {
|
): Response {
|
||||||
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
|
||||||
|
|
||||||
|
if (!$this->isCsrfTokenValid('activate_ingest_profile_' . $profile->getId(), (string) $request->request->get('_token'))) {
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$svc->activate($profile);
|
$svc->activate($profile);
|
||||||
$this->addFlash('success', 'Ingest-Profil wurde aktiviert.');
|
$this->addFlash('success', 'Ingest-Profil wurde aktiviert.');
|
||||||
@@ -61,9 +73,15 @@ final class IngestProfileController extends AbstractController
|
|||||||
return $this->redirectToRoute('admin_ingest_profile_list');
|
return $this->redirectToRoute('admin_ingest_profile_list');
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/remove/{id}', name: 'admin_ingest_profile_remove', methods: ['POST', 'GET'])]
|
#[Route('/remove/{id}', name: 'admin_ingest_profile_remove', methods: ['POST'])]
|
||||||
public function remove(string $id, IngestProfileAdminService $svc): Response
|
public function remove(string $id, IngestProfileAdminService $svc, Request $request): Response
|
||||||
{
|
{
|
||||||
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
|
||||||
|
|
||||||
|
if (!$this->isCsrfTokenValid('delete_ingest_profile_' . $id, (string) $request->request->get('_token'))) {
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$svc->remove($id);
|
$svc->remove($id);
|
||||||
$this->addFlash('success', 'Ingest-Profil wurde entfernt.');
|
$this->addFlash('success', 'Ingest-Profil wurde entfernt.');
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Controller\Admin;
|
namespace App\Controller\Admin;
|
||||||
|
|
||||||
|
use App\Security\ApplicationRoles;
|
||||||
use App\Entity\ModelGenerationConfig;
|
use App\Entity\ModelGenerationConfig;
|
||||||
use App\Service\Admin\ModelGenerationConfigAdminService;
|
use App\Service\Admin\ModelGenerationConfigAdminService;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
@@ -17,7 +18,7 @@ final class ModelGenerationConfigController extends AbstractController
|
|||||||
#[Route('/', name: 'admin_model_config_list')]
|
#[Route('/', name: 'admin_model_config_list')]
|
||||||
public function list(ModelGenerationConfigAdminService $svc): Response
|
public function list(ModelGenerationConfigAdminService $svc): Response
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('ROLE_KNOWLEDGE_ADMIN');
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_KNOWLEDGE_ADMIN);
|
||||||
|
|
||||||
return $this->render('admin/model_config/list.html.twig', [
|
return $this->render('admin/model_config/list.html.twig', [
|
||||||
'configs' => $svc->list(),
|
'configs' => $svc->list(),
|
||||||
@@ -29,7 +30,7 @@ final class ModelGenerationConfigController extends AbstractController
|
|||||||
Request $request,
|
Request $request,
|
||||||
ModelGenerationConfigAdminService $svc
|
ModelGenerationConfigAdminService $svc
|
||||||
): Response {
|
): Response {
|
||||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
|
||||||
|
|
||||||
if ($request->isMethod('POST')) {
|
if ($request->isMethod('POST')) {
|
||||||
try {
|
try {
|
||||||
@@ -50,7 +51,7 @@ final class ModelGenerationConfigController extends AbstractController
|
|||||||
ModelGenerationConfig $config,
|
ModelGenerationConfig $config,
|
||||||
ModelGenerationConfigAdminService $svc
|
ModelGenerationConfigAdminService $svc
|
||||||
): Response {
|
): Response {
|
||||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
|
||||||
|
|
||||||
$svc->activate($config);
|
$svc->activate($config);
|
||||||
|
|
||||||
@@ -63,7 +64,7 @@ final class ModelGenerationConfigController extends AbstractController
|
|||||||
Request $request,
|
Request $request,
|
||||||
ModelGenerationConfigAdminService $svc
|
ModelGenerationConfigAdminService $svc
|
||||||
): Response {
|
): Response {
|
||||||
$this->denyAccessUnlessGranted('ROLE_KNOWLEDGE_ADMIN');
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_KNOWLEDGE_ADMIN);
|
||||||
|
|
||||||
$prompt = '';
|
$prompt = '';
|
||||||
$results = [];
|
$results = [];
|
||||||
@@ -86,7 +87,7 @@ final class ModelGenerationConfigController extends AbstractController
|
|||||||
Request $request,
|
Request $request,
|
||||||
ModelGenerationConfigAdminService $svc
|
ModelGenerationConfigAdminService $svc
|
||||||
): Response {
|
): Response {
|
||||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
|
||||||
|
|
||||||
if (!$this->isCsrfTokenValid(
|
if (!$this->isCsrfTokenValid(
|
||||||
'delete_model_config_'.$config->getId(),
|
'delete_model_config_'.$config->getId(),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Controller\Admin;
|
namespace App\Controller\Admin;
|
||||||
|
|
||||||
|
use App\Security\ApplicationRoles;
|
||||||
use App\Service\Admin\IndexNdjsonInspector;
|
use App\Service\Admin\IndexNdjsonInspector;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
@@ -12,7 +13,7 @@ use Symfony\Component\HttpFoundation\Response;
|
|||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
#[IsGranted('ROLE_SUPER_ADMIN')]
|
#[IsGranted(ApplicationRoles::ROLE_SUPER_ADMIN)]
|
||||||
final class SystemAgentController extends AbstractController
|
final class SystemAgentController extends AbstractController
|
||||||
{
|
{
|
||||||
#[Route('/admin/system/agent', name: 'admin_system_agent', methods: ['GET'])]
|
#[Route('/admin/system/agent', name: 'admin_system_agent', methods: ['GET'])]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Controller\Admin;
|
namespace App\Controller\Admin;
|
||||||
|
|
||||||
|
use App\Security\ApplicationRoles;
|
||||||
use App\Entity\SystemPrompt;
|
use App\Entity\SystemPrompt;
|
||||||
use App\Service\Admin\SystemPromptAdminService;
|
use App\Service\Admin\SystemPromptAdminService;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
@@ -12,7 +13,7 @@ use Symfony\Component\HttpFoundation\Response;
|
|||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
#[IsGranted('ROLE_SUPER_ADMIN')]
|
#[IsGranted(ApplicationRoles::ROLE_SUPER_ADMIN)]
|
||||||
final class SystemPromptController extends AbstractController
|
final class SystemPromptController extends AbstractController
|
||||||
{
|
{
|
||||||
#[Route('/admin/system/prompt', name: 'admin_system_prompt', methods: ['GET', 'POST'])]
|
#[Route('/admin/system/prompt', name: 'admin_system_prompt', methods: ['GET', 'POST'])]
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Controller\Admin;
|
namespace App\Controller\Admin;
|
||||||
|
|
||||||
use App\Entity\TagRebuildJob;
|
use App\Entity\TagRebuildJob;
|
||||||
|
use App\Security\ApplicationRoles;
|
||||||
use App\Service\Admin\TagAdminService;
|
use App\Service\Admin\TagAdminService;
|
||||||
use App\Tag\TagTypes;
|
use App\Tag\TagTypes;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
@@ -19,6 +20,8 @@ final class TagController extends AbstractController
|
|||||||
#[Route('', name: 'admin_tags_index', methods: ['GET'])]
|
#[Route('', name: 'admin_tags_index', methods: ['GET'])]
|
||||||
public function index(TagAdminService $svc): Response
|
public function index(TagAdminService $svc): Response
|
||||||
{
|
{
|
||||||
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR);
|
||||||
|
|
||||||
return $this->render('admin/tag/index.html.twig', [
|
return $this->render('admin/tag/index.html.twig', [
|
||||||
...$svc->getIndexData(),
|
...$svc->getIndexData(),
|
||||||
...$this->buildJobStatusViewData(),
|
...$this->buildJobStatusViewData(),
|
||||||
@@ -28,6 +31,8 @@ final class TagController extends AbstractController
|
|||||||
#[Route('/create', name: 'admin_tags_create', methods: ['POST'])]
|
#[Route('/create', name: 'admin_tags_create', methods: ['POST'])]
|
||||||
public function create(Request $request, TagAdminService $svc): RedirectResponse
|
public function create(Request $request, TagAdminService $svc): RedirectResponse
|
||||||
{
|
{
|
||||||
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR);
|
||||||
|
|
||||||
if (!$this->isCsrfTokenValid('admin_tag_create', (string) $request->request->get('_token'))) {
|
if (!$this->isCsrfTokenValid('admin_tag_create', (string) $request->request->get('_token'))) {
|
||||||
$this->addFlash('danger', 'Ungültiges CSRF-Token.');
|
$this->addFlash('danger', 'Ungültiges CSRF-Token.');
|
||||||
|
|
||||||
@@ -53,6 +58,8 @@ final class TagController extends AbstractController
|
|||||||
#[Route('/{id}/delete', name: 'admin_tags_delete', methods: ['POST'])]
|
#[Route('/{id}/delete', name: 'admin_tags_delete', methods: ['POST'])]
|
||||||
public function delete(string $id, Request $request, TagAdminService $svc): RedirectResponse
|
public function delete(string $id, Request $request, TagAdminService $svc): RedirectResponse
|
||||||
{
|
{
|
||||||
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR);
|
||||||
|
|
||||||
if (!$this->isCsrfTokenValid('admin_tag_delete_' . $id, (string) $request->request->get('_token'))) {
|
if (!$this->isCsrfTokenValid('admin_tag_delete_' . $id, (string) $request->request->get('_token'))) {
|
||||||
$this->addFlash('danger', 'Ungültiges CSRF-Token.');
|
$this->addFlash('danger', 'Ungültiges CSRF-Token.');
|
||||||
|
|
||||||
@@ -72,6 +79,8 @@ final class TagController extends AbstractController
|
|||||||
#[Route('/{id}/assign', name: 'admin_tags_assign', methods: ['GET', 'POST'])]
|
#[Route('/{id}/assign', name: 'admin_tags_assign', methods: ['GET', 'POST'])]
|
||||||
public function assign(string $id, Request $request, TagAdminService $svc): Response
|
public function assign(string $id, Request $request, TagAdminService $svc): Response
|
||||||
{
|
{
|
||||||
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR);
|
||||||
|
|
||||||
$id = trim($id);
|
$id = trim($id);
|
||||||
|
|
||||||
if ($request->isMethod('POST')) {
|
if ($request->isMethod('POST')) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Controller\Admin;
|
namespace App\Controller\Admin;
|
||||||
|
|
||||||
|
use App\Security\ApplicationRoles;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Service\Admin\UserAdminService;
|
use App\Service\Admin\UserAdminService;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
@@ -19,7 +20,7 @@ final class UserController extends AbstractController
|
|||||||
#[Route('', name: 'admin_users_index', methods: ['GET'])]
|
#[Route('', name: 'admin_users_index', methods: ['GET'])]
|
||||||
public function index(Request $request, UserAdminService $users): Response
|
public function index(Request $request, UserAdminService $users): Response
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
|
||||||
|
|
||||||
$query = trim((string) $request->query->get('q', ''));
|
$query = trim((string) $request->query->get('q', ''));
|
||||||
$status = (string) $request->query->get('status', 'all');
|
$status = (string) $request->query->get('status', 'all');
|
||||||
@@ -44,7 +45,7 @@ final class UserController extends AbstractController
|
|||||||
#[Route('/create', name: 'admin_users_create', methods: ['GET', 'POST'])]
|
#[Route('/create', name: 'admin_users_create', methods: ['GET', 'POST'])]
|
||||||
public function create(Request $request, UserAdminService $users): Response
|
public function create(Request $request, UserAdminService $users): Response
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
|
||||||
|
|
||||||
if (!$request->isMethod('POST')) {
|
if (!$request->isMethod('POST')) {
|
||||||
return $this->render('admin/user/create.html.twig', [
|
return $this->render('admin/user/create.html.twig', [
|
||||||
@@ -80,7 +81,7 @@ final class UserController extends AbstractController
|
|||||||
#[Route('/{id}/edit', name: 'admin_users_edit', requirements: ['id' => '[0-9a-fA-F\-]{36}'], methods: ['GET', 'POST'])]
|
#[Route('/{id}/edit', name: 'admin_users_edit', requirements: ['id' => '[0-9a-fA-F\-]{36}'], methods: ['GET', 'POST'])]
|
||||||
public function edit(string $id, Request $request, UserAdminService $users): Response
|
public function edit(string $id, Request $request, UserAdminService $users): Response
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$user = $users->requireUser($id);
|
$user = $users->requireUser($id);
|
||||||
@@ -127,7 +128,7 @@ final class UserController extends AbstractController
|
|||||||
#[Route('/{id}/toggle-active', name: 'admin_users_toggle_active', requirements: ['id' => '[0-9a-fA-F\-]{36}'], methods: ['POST'])]
|
#[Route('/{id}/toggle-active', name: 'admin_users_toggle_active', requirements: ['id' => '[0-9a-fA-F\-]{36}'], methods: ['POST'])]
|
||||||
public function toggleActive(string $id, Request $request, UserAdminService $users): RedirectResponse
|
public function toggleActive(string $id, Request $request, UserAdminService $users): RedirectResponse
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
|
||||||
|
|
||||||
if (!$this->isCsrfTokenValid('admin_user_toggle_active_' . $id, (string) $request->request->get('_token'))) {
|
if (!$this->isCsrfTokenValid('admin_user_toggle_active_' . $id, (string) $request->request->get('_token'))) {
|
||||||
$this->addFlash('danger', 'Ungültiges CSRF-Token.');
|
$this->addFlash('danger', 'Ungültiges CSRF-Token.');
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ final class ApplicationRoles
|
|||||||
public const ROLE_USER = 'ROLE_USER';
|
public const ROLE_USER = 'ROLE_USER';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Roles that can be assigned from the Admin UI or console.
|
||||||
|
*
|
||||||
|
* Effective access is backed by config/packages/security.yaml and by
|
||||||
|
* controller-level guards for action-specific permissions.
|
||||||
|
*
|
||||||
* @return array<string, string>
|
* @return array<string, string>
|
||||||
*/
|
*/
|
||||||
public static function assignableChoices(): array
|
public static function assignableChoices(): array
|
||||||
|
|||||||
@@ -77,6 +77,7 @@
|
|||||||
RAG Dokumente & Wissen
|
RAG Dokumente & Wissen
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if is_granted('ROLE_EDITOR') %}
|
||||||
<a class="nav-link text-light {% if route starts with 'admin_document' %}active fw-bold{% endif %}"
|
<a class="nav-link text-light {% if route starts with 'admin_document' %}active fw-bold{% endif %}"
|
||||||
href="{{ path('admin_documents') }}">
|
href="{{ path('admin_documents') }}">
|
||||||
<i class="bi bi-card-list"></i> Dokumente
|
<i class="bi bi-card-list"></i> Dokumente
|
||||||
@@ -89,11 +90,14 @@
|
|||||||
href="{{ path('admin_tags_index') }}">
|
href="{{ path('admin_tags_index') }}">
|
||||||
<i class="bi bi-tag-fill"></i> Tags
|
<i class="bi bi-tag-fill"></i> Tags
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||||
<a class="nav-link text-light {% if route starts with 'admin_system_agent' %}active fw-bold{% endif %}"
|
<a class="nav-link text-light {% if route starts with 'admin_system_agent' %}active fw-bold{% endif %}"
|
||||||
href="{{ path('admin_system_agent') }}">
|
href="{{ path('admin_system_agent') }}">
|
||||||
<i class="bi bi-robot"></i> Wissensbasis (Chunk-Index)
|
<i class="bi bi-robot"></i> Wissensbasis (Chunk-Index)
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<hr class="border-secondary">
|
<hr class="border-secondary">
|
||||||
|
|
||||||
@@ -101,15 +105,19 @@
|
|||||||
RAG System-Profile
|
RAG System-Profile
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||||
<a class="nav-link text-light {% if route starts with 'admin_system_prompt' %}active fw-bold{% endif %}"
|
<a class="nav-link text-light {% if route starts with 'admin_system_prompt' %}active fw-bold{% endif %}"
|
||||||
href="{{ path('admin_system_prompt') }}">
|
href="{{ path('admin_system_prompt') }}">
|
||||||
<i class="bi bi-chat-right-dots-fill"></i> System Prompt
|
<i class="bi bi-chat-right-dots-fill"></i> System Prompt
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if is_granted('ROLE_KNOWLEDGE_ADMIN') %}
|
||||||
<a class="nav-link text-light {% if route starts with 'admin_ingest_profile' %}active fw-bold{% endif %}"
|
<a class="nav-link text-light {% if route starts with 'admin_ingest_profile' %}active fw-bold{% endif %}"
|
||||||
href="{{ path('admin_ingest_profile_list') }}">
|
href="{{ path('admin_ingest_profile_list') }}">
|
||||||
<i class="bi bi-search"></i> Indexierungsprofile (Ingest)
|
<i class="bi bi-search"></i> Indexierungsprofile (Ingest)
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<hr class="border-secondary">
|
<hr class="border-secondary">
|
||||||
|
|
||||||
@@ -117,6 +125,7 @@
|
|||||||
KI-Endpunkte
|
KI-Endpunkte
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if is_granted('ROLE_KNOWLEDGE_ADMIN') %}
|
||||||
<a class="nav-link text-light {% if route starts with 'admin_model_config' %}active fw-bold{% endif %}"
|
<a class="nav-link text-light {% if route starts with 'admin_model_config' %}active fw-bold{% endif %}"
|
||||||
href="{{ path('admin_model_config_list') }}">
|
href="{{ path('admin_model_config_list') }}">
|
||||||
<i class="bi bi-rocket-takeoff-fill"></i> KI-/LLM-Setup
|
<i class="bi bi-rocket-takeoff-fill"></i> KI-/LLM-Setup
|
||||||
@@ -125,6 +134,7 @@
|
|||||||
href="{{ path('admin_model_config_list') }}#agentLiveTest">
|
href="{{ path('admin_model_config_list') }}#agentLiveTest">
|
||||||
<i class="bi bi-rocket-takeoff-fill"></i> KI-Agent Live-Test
|
<i class="bi bi-rocket-takeoff-fill"></i> KI-Agent Live-Test
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
<hr class="border-secondary">
|
<hr class="border-secondary">
|
||||||
<div class="text-info text-uppercase small mb-2">
|
<div class="text-info text-uppercase small mb-2">
|
||||||
System-Guiide
|
System-Guiide
|
||||||
@@ -142,14 +152,16 @@
|
|||||||
href="{{ path('admin_jobs') }}">
|
href="{{ path('admin_jobs') }}">
|
||||||
<i class="bi bi-terminal"></i> Indexierungs-Log (Ingest Jobs)
|
<i class="bi bi-terminal"></i> Indexierungs-Log (Ingest Jobs)
|
||||||
</a>
|
</a>
|
||||||
<a class="nav-link text-light {% if route starts with 'admin_job' %}active fw-bold{% endif %}"
|
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||||
|
<a class="nav-link text-light {% if route starts with 'admin_vector_log' %}active fw-bold{% endif %}"
|
||||||
href="{{ path('admin_vector_log') }}">
|
href="{{ path('admin_vector_log') }}">
|
||||||
<i class="bi bi-terminal"></i> Vector-Log Python
|
<i class="bi bi-terminal"></i> Vector-Log Python
|
||||||
</a>
|
</a>
|
||||||
<a class="nav-link text-light {% if route starts with 'admin_job' %}active fw-bold{% endif %}"
|
<a class="nav-link text-light {% if route starts with 'admin_system_logs' %}active fw-bold{% endif %}"
|
||||||
href="{{ path('admin_system_logs_index') }}">
|
href="{{ path('admin_system_logs_index') }}">
|
||||||
<i class="bi bi-terminal"></i> System-Logs
|
<i class="bi bi-terminal"></i> System-Logs
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -226,6 +226,7 @@
|
|||||||
physischen Retrieval-Artefakte wieder gerade.
|
physischen Retrieval-Artefakte wieder gerade.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||||
<form method="post"
|
<form method="post"
|
||||||
action="{{ path('admin_global_reindex') }}"
|
action="{{ path('admin_global_reindex') }}"
|
||||||
onsubmit="return confirm('Global Reindex starten? Dies kann mehrere Minuten dauern.');">
|
onsubmit="return confirm('Global Reindex starten? Dies kann mehrere Minuten dauern.');">
|
||||||
@@ -239,6 +240,11 @@
|
|||||||
Global Reindex starten
|
Global Reindex starten
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-dark border border-secondary text-light small mb-0">
|
||||||
|
Global Reindex ist Super-Admins vorbehalten.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if anyHealthIssue %}
|
{% if anyHealthIssue %}
|
||||||
<div class="alert alert-dark border border-warning text-light small mt-3 mb-0">
|
<div class="alert alert-dark border border-warning text-light small mt-3 mb-0">
|
||||||
|
|||||||
@@ -14,10 +14,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if is_granted('ROLE_EDITOR') %}
|
||||||
<a href="{{ path('admin_document_new') }}"
|
<a href="{{ path('admin_document_new') }}"
|
||||||
class="btn btn-sm btn-outline-info">
|
class="btn btn-sm btn-outline-info">
|
||||||
Neues Dokument
|
Neues Dokument
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% for message in app.flashes('success') %}
|
{% for message in app.flashes('success') %}
|
||||||
@@ -189,10 +191,12 @@
|
|||||||
|
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<div class="d-flex justify-content-end flex-wrap gap-2">
|
<div class="d-flex justify-content-end flex-wrap gap-2">
|
||||||
|
{% if is_granted('ROLE_EDITOR') %}
|
||||||
<a class="btn btn-sm btn-outline-info"
|
<a class="btn btn-sm btn-outline-info"
|
||||||
href="{{ path('admin_document_tags_edit', {id: document.id}) }}">
|
href="{{ path('admin_document_tags_edit', {id: document.id}) }}">
|
||||||
Tags
|
Tags
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<a class="btn btn-sm btn-outline-light"
|
<a class="btn btn-sm btn-outline-light"
|
||||||
href="{{ path('admin_document_show', {id: document.id}) }}">
|
href="{{ path('admin_document_show', {id: document.id}) }}">
|
||||||
|
|||||||
@@ -138,7 +138,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
{% if is_granted('ROLE_EDITOR') %}
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<button class="btn btn-outline-info">
|
<button class="btn btn-outline-info">
|
||||||
Version hochladen
|
Version hochladen
|
||||||
|
|||||||
@@ -13,10 +13,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex flex-wrap gap-2">
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
{% if is_granted('ROLE_EDITOR') %}
|
||||||
<a href="{{ path('admin_document_tags_edit', {id: document.id}) }}"
|
<a href="{{ path('admin_document_tags_edit', {id: document.id}) }}"
|
||||||
class="btn btn-sm btn-outline-info">
|
class="btn btn-sm btn-outline-info">
|
||||||
Tags bearbeiten
|
Tags bearbeiten
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<a href="{{ path('admin_documents') }}"
|
<a href="{{ path('admin_documents') }}"
|
||||||
class="btn btn-sm btn-outline-secondary">
|
class="btn btn-sm btn-outline-secondary">
|
||||||
@@ -96,15 +98,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
{% if is_granted('ROLE_EDITOR') or is_granted('ROLE_SUPER_ADMIN') %}
|
||||||
<hr class="border-secondary">
|
<hr class="border-secondary">
|
||||||
|
|
||||||
<div class="d-flex flex-wrap gap-2">
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
{% if is_granted('ROLE_EDITOR') %}
|
||||||
<a href="{{ path('admin_document_version_new', {id: document.id}) }}"
|
<a href="{{ path('admin_document_version_new', {id: document.id}) }}"
|
||||||
class="btn btn-sm btn-outline-info">
|
class="btn btn-sm btn-outline-info">
|
||||||
Neue Version
|
Neue Version
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||||
<form method="post"
|
<form method="post"
|
||||||
action="{{ path('admin_document_delete', {id: document.id}) }}"
|
action="{{ path('admin_document_delete', {id: document.id}) }}"
|
||||||
class="d-inline"
|
class="d-inline"
|
||||||
@@ -116,6 +121,7 @@
|
|||||||
Dokument löschen
|
Dokument löschen
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -128,10 +134,12 @@
|
|||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<h5 class="text-info mb-0">Tags</h5>
|
<h5 class="text-info mb-0">Tags</h5>
|
||||||
|
|
||||||
|
{% if is_granted('ROLE_EDITOR') %}
|
||||||
<a href="{{ path('admin_document_tags_edit', {id: document.id}) }}"
|
<a href="{{ path('admin_document_tags_edit', {id: document.id}) }}"
|
||||||
class="btn btn-sm btn-outline-light">
|
class="btn btn-sm btn-outline-light">
|
||||||
Bearbeiten
|
Bearbeiten
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if document.tags is empty %}
|
{% if document.tags is empty %}
|
||||||
@@ -172,7 +180,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
{% if is_granted('ROLE_EDITOR') %}
|
||||||
<a href="{{ path('admin_document_version_new', {id: document.id}) }}"
|
<a href="{{ path('admin_document_version_new', {id: document.id}) }}"
|
||||||
class="btn btn-sm btn-outline-info">
|
class="btn btn-sm btn-outline-info">
|
||||||
Neue Version
|
Neue Version
|
||||||
@@ -258,7 +266,7 @@
|
|||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<div class="d-flex justify-content-end flex-wrap gap-2">
|
<div class="d-flex justify-content-end flex-wrap gap-2">
|
||||||
{% if version.isActive %}
|
{% if version.isActive %}
|
||||||
{% if version.ingestStatus in ['PENDING', 'FAILED'] and is_granted('ROLE_SUPER_ADMIN') %}
|
{% if version.ingestStatus in ['PENDING', 'FAILED'] and is_granted('ROLE_EDITOR') %}
|
||||||
<form method="post"
|
<form method="post"
|
||||||
action="{{ path('admin_document_version_ingest', {versionId: version.id}) }}"
|
action="{{ path('admin_document_version_ingest', {versionId: version.id}) }}"
|
||||||
class="d-inline"
|
class="d-inline"
|
||||||
@@ -276,7 +284,7 @@
|
|||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
{% if is_granted('ROLE_EDITOR') %}
|
||||||
<form method="post"
|
<form method="post"
|
||||||
action="{{ path('admin_document_version_activate', {versionId: version.id}) }}"
|
action="{{ path('admin_document_version_activate', {versionId: version.id}) }}"
|
||||||
class="d-inline"
|
class="d-inline"
|
||||||
|
|||||||
@@ -28,10 +28,12 @@
|
|||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h1 class="h3"><i class="bi bi-search"></i> Indexierungsprofile</h1>
|
<h1 class="h3"><i class="bi bi-search"></i> Indexierungsprofile</h1>
|
||||||
|
|
||||||
|
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||||
<a class="btn btn-sm btn-outline-info"
|
<a class="btn btn-sm btn-outline-info"
|
||||||
href="{{ path('admin_ingest_profile_create') }}">
|
href="{{ path('admin_ingest_profile_create') }}">
|
||||||
Neues Profil anlegen
|
Neues Profil anlegen
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# ========================================================= #}
|
{# ========================================================= #}
|
||||||
|
|||||||
@@ -106,6 +106,7 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{% if is_granted('ROLE_EDITOR') %}
|
||||||
<div class="card bg-black border-secondary text-light mb-4 shadow-sm">
|
<div class="card bg-black border-secondary text-light mb-4 shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="text-info mb-3">Neuen Tag hinzufügen</h5>
|
<h5 class="text-info mb-3">Neuen Tag hinzufügen</h5>
|
||||||
@@ -156,6 +157,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="card bg-black border-secondary text-light shadow-sm">
|
<div class="card bg-black border-secondary text-light shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -207,6 +209,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>{{ tag.description ?: '-' }}</td>
|
<td>{{ tag.description ?: '-' }}</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
|
{% if is_granted('ROLE_EDITOR') %}
|
||||||
<a href="{{ path('admin_tags_assign', { id: tag.id }) }}"
|
<a href="{{ path('admin_tags_assign', { id: tag.id }) }}"
|
||||||
class="btn btn-sm btn-outline-info me-2">
|
class="btn btn-sm btn-outline-info me-2">
|
||||||
Zuweisen
|
Zuweisen
|
||||||
@@ -224,6 +227,9 @@
|
|||||||
Löschen
|
Löschen
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted small">Nur Ansicht</span>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
Reference in New Issue
Block a user