first commit

This commit is contained in:
team 1
2026-04-20 16:36:28 +02:00
parent a0ec07a99c
commit 2587ac8b4b
41 changed files with 5126 additions and 2280 deletions

View File

@@ -4,19 +4,24 @@
{% block body %}
{# ========================================================= #}
{# LIVE REBUILD STATUS (SSE) #}
{# ========================================================= #}
<div id="rebuild-status" class="mb-5">
<div class="alert alert-secondary shadow-sm">
Status wird geladen…
</div>
<div id="rebuild-status" class="mb-4">
{% if latestJob %}
<div class="alert alert-secondary shadow-sm mb-0">
Status wird geladen…
</div>
{% endif %}
</div>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0">
<i class="bi bi-tag-fill"></i> Tag: {{ tag.label }}
</h1>
<div>
<h1 class="h3 mb-1">
<i class="bi bi-tag-fill"></i> Tag: {{ tag.label }}
</h1>
<div class="small text-muted">
Slug: <code>{{ tag.slug }}</code>
</div>
</div>
<a href="{{ path('admin_tags_index') }}"
class="btn btn-sm btn-outline-secondary">
@@ -24,7 +29,6 @@
</a>
</div>
<script>
const statusBox = document.getElementById('rebuild-status');
const source = new EventSource("{{ path('admin_tags_rebuild_stream') }}");
@@ -35,9 +39,9 @@
if (data.status === '{{ statusRunning }}') {
html = `
<div class="alert alert-info shadow-sm d-flex justify-content-between align-items-center">
<div class="alert alert-info shadow-sm d-flex justify-content-between align-items-center mb-0">
<div>
Tag-Rebuild läuft<br>
<strong>Tag-Rebuild läuft</strong><br>
${data.startedAt ? 'Gestartet: ' + new Date(data.startedAt).toLocaleString() : ''}
</div>
<div class="spinner-border spinner-border-sm"></div>
@@ -45,20 +49,20 @@
`;
} else if (data.status === '{{ statusQueued }}') {
html = `
<div class="alert alert-secondary shadow-sm">
Tag-Rebuild in Warteschlange
<div class="alert alert-secondary shadow-sm mb-0">
<strong>Tag-Rebuild in Warteschlange</strong>
</div>
`;
} else if (data.status === '{{ statusCompleted }}') {
html = `
<div class="alert alert-success shadow-sm">
<div class="alert alert-success shadow-sm mb-0">
<i class="bi bi-check-lg"></i> Tag-Rebuild erfolgreich abgeschlossen
</div>
`;
} else if (data.status === '{{ statusFailed }}') {
html = `
<div class="alert alert-danger shadow-sm">
Tag-Rebuild fehlgeschlagen<br>
<div class="alert alert-danger shadow-sm mb-0">
<strong>Tag-Rebuild fehlgeschlagen</strong><br>
${data.error ? '<code>' + data.error + '</code>' : ''}
</div>
`;
@@ -70,100 +74,179 @@
source.onerror = function () {
console.warn('SSE Verbindung verloren');
};
window.addEventListener('beforeunload', function () {
source.close();
});
</script>
{# ============================= #}
{# Flash Messages #}
{# ============================= #}
{% for message in app.flashes('success') %}
<div class="alert alert-success">
<div class="alert alert-success shadow-sm">
{{ message }}
</div>
{% endfor %}
{% for message in app.flashes('danger') %}
<div class="alert alert-danger">
<div class="alert alert-danger shadow-sm">
{{ message }}
</div>
{% endfor %}
{# ============================= #}
{# Tag → Dokumente #}
{# ============================= #}
<div class="card bg-dark border-secondary text-light mb-4 shadow-sm">
<div class="card-body row g-4">
<div class="col-lg-7">
<h5 class="text-info mb-3">Einordnung des Tags</h5>
<div class="mb-2">
{% if tag.type == 'catalog_entity' %}
<span class="badge text-bg-info">Catalog Entity</span>
{% elseif tag.type == 'sales_signal' %}
<span class="badge text-bg-warning">Sales Signal</span>
{% else %}
<span class="badge text-bg-secondary">Generic</span>
{% endif %}
</div>
<p class="small mb-2">
{{ tag.description ?: 'Keine Beschreibung hinterlegt.' }}
</p>
<p class="small text-muted mb-0">
Weise diesen Tag nur Dokumenten zu, die fachlich wirklich denselben Gegenstand,
dieselbe Produktfamilie oder denselben Anwendungsfall abbilden.
Zu breite Zuweisungen machen das Routing weicher.
</p>
</div>
<div class="col-lg-5">
<h5 class="text-info mb-3">Aktueller Stand</h5>
<div class="d-flex flex-wrap gap-2">
<span class="badge text-bg-dark border border-secondary">
Zugewiesen: {{ assignedDocIds|length }}
</span>
<span class="badge text-bg-dark border border-secondary">
Verfügbar: {{ documents|length }}
</span>
<span class="badge text-bg-dark border border-secondary">
Nicht zugewiesen: {{ documents|length - assignedDocIds|length }}
</span>
</div>
</div>
</div>
</div>
<form method="post">
<input type="hidden"
name="_token"
value="{{ csrf_token('assign_tag_' ~ tag.id) }}">
<div class="card bg-black border-secondary">
<div class="card-body p-0 row">
<div class=" col-lg-6">
<table class="table table-dark table-striped table-hover mb-0 align-middle">
<thead class="table-secondary text-dark">
<tr>
<th style="width:60px;"><i class="bi bi-three-dots"></i></th>
<th>Zugewiesene Dokumente</th>
</tr>
</thead>
<div class="row g-4">
<div class="col-lg-6">
<div class="card bg-black border-secondary shadow-sm h-100">
<div class="card-header bg-secondary-subtle text-dark fw-semibold">
Zugewiesene Dokumente
</div>
<tbody>
{% for doc in documents %}
{% if doc.id in assignedDocIds %}
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-dark table-striped table-hover mb-0 align-middle">
<thead class="table-secondary text-dark">
<tr>
<td>
<input type="checkbox"
name="documents[]"
value="{{ doc.id }}"
checked>
</td>
<td>
{{ doc.title }}
</td>
<th style="width: 60px;">
<i class="bi bi-check2-square"></i>
</th>
<th>Dokument</th>
</tr>
{% endif %}
{% endfor %}
</thead>
<tbody>
{% set hasAssigned = false %}
{% for doc in documents %}
{% if doc.id in assignedDocIds %}
{% set hasAssigned = true %}
<tr>
<td>
<input type="checkbox"
name="documents[]"
value="{{ doc.id }}"
checked>
</td>
<td class="fw-semibold">
{{ doc.title }}
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
{% if not hasAssigned %}
<tr>
<td colspan="2" class="text-center text-muted p-4">
Noch keine Dokumente zugewiesen.
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
<div class=" col-lg-6">
<table class="table table-dark table-striped table-hover mb-0 align-middle col-lg-6">
<thead class="table-secondary text-dark">
<tr>
<th style="width:60px;"><i class="bi bi-three-dots"></i></th>
<th>Nicht zugewiesene Dokumente</th>
</tr>
</thead>
</div>
<tbody>
{% for doc in documents %}
{% if doc.id not in assignedDocIds %}
<div class="col-lg-6">
<div class="card bg-black border-secondary shadow-sm h-100">
<div class="card-header bg-secondary-subtle text-dark fw-semibold">
Verfügbare Dokumente
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-dark table-striped table-hover mb-0 align-middle">
<thead class="table-secondary text-dark">
<tr>
<td>
<input type="checkbox"
name="documents[]"
value="{{ doc.id }}"
>
</td>
<td class="opacity-50">
{{ doc.title }}
</td>
<th style="width: 60px;">
<i class="bi bi-square"></i>
</th>
<th>Dokument</th>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</thead>
<tbody>
{% set hasUnassigned = false %}
{% for doc in documents %}
{% if doc.id not in assignedDocIds %}
{% set hasUnassigned = true %}
<tr>
<td>
<input type="checkbox"
name="documents[]"
value="{{ doc.id }}">
</td>
<td class="opacity-75">
{{ doc.title }}
</td>
</tr>
{% endif %}
{% endfor %}
{% if not hasUnassigned %}
<tr>
<td colspan="2" class="text-center text-muted p-4">
Keine weiteren aktiven Dokumente verfügbar.
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<button class="btn btn-primary mt-3">
Speichern
</button>
<div class="d-flex justify-content-end mt-4">
<button class="btn btn-primary">
Zuweisungen speichern
</button>
</div>
</form>
{% endblock %}

View File

@@ -4,77 +4,52 @@
{% block body %}
{# ========================================================= #}
{# LIVE REBUILD STATUS (SSE) #}
{# ========================================================= #}
<div id="rebuild-status" class="mb-5">
<div id="rebuild-status" class="mb-4">
{% if latestJob %}
<div class="alert alert-secondary shadow-sm">
<div class="alert alert-secondary shadow-sm mb-0">
Status wird geladen…
</div>
{% endif %}
</div>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0"><i class="bi bi-tag-fill"></i> Tag-Management</h1>
<h1 class="h3 mb-0">
<i class="bi bi-tag-fill"></i> Tag-Management
</h1>
</div>
{# ========================================================= #}
{# TAG SYSTEM DESCRIPTION #}
{# ========================================================= #}
<div class="card bg-dark border-secondary text-light mb-4 shadow-sm">
<div class="card-body row">
<div class="card-body row g-4">
<div class="col-lg-6">
<h5 class="text-info mb-3">Was machen Tags im System?</h5>
<p class="small text-light mb-2">
Tags dienen als semantische Routing-Ebene innerhalb des RAG-Systems.
Sie strukturieren Dokumente thematisch und beeinflussen,
welche Inhalte bei einer Nutzeranfrage priorisiert werden.
Tags sind die semantische Routing-Ebene innerhalb des Systems.
Sie helfen dabei, thematisch passende Dokumenträume schneller zu erkennen
und gute Retrieval-Kandidaten zu priorisieren.
</p>
<ul class="small text-light mb-3">
<li>
Tags werden Dokumenten manuell zugewiesen.
</li>
<li>
Beim Rebuild wird aus allen Tags eine eigene
<code>tags.ndjson</code> erzeugt.
</li>
<li>
Zusätzlich wird ein separater Vektorindex
(<code>vector_tags.index</code>) aufgebaut.
</li>
<li>
Bei einer Anfrage erfolgt zunächst ein Tag-Matching,
danach wird das Chunk-Retrieval entsprechend gewichtet.
</li>
<ul class="small text-light mb-0">
<li>Tags werden Dokumenten manuell zugewiesen.</li>
<li>Beim Rebuild wird aus den aktiven Tag-Zuordnungen eine <code>tags.ndjson</code> erzeugt.</li>
<li>Zusätzlich wird ein eigener Tag-Vektorindex (<code>vector_tags.index</code>) gebaut.</li>
<li>Bei Anfragen erfolgt zunächst ein semantisches Tag-Matching, danach das eigentliche Chunk-Retrieval.</li>
</ul>
</div>
<div class="col-lg-6">
<h6 class="text-info mt-3">Wie werden Tags bewertet?</h6>
<h5 class="text-info mb-3">Was ist gutes Tagging?</h5>
<p class="small text-light mb-2">
Die Bewertung erfolgt über einen eigenen Vektor-Similarity-Score
im Tag-Index. Das System berechnet:
</p>
<ul class="small text-light">
<li>
Ähnlichkeit zwischen Nutzeranfrage und Tag-Embedding
</li>
<li>
Top-K Treffer im Tag-Index
</li>
<li>
Gewichtete Übergabe an das Chunk-Retrieval
</li>
<ul class="small text-light mb-3">
<li><strong>Präzise statt generisch:</strong> lieber <code>Produktnamen</code> als <code>Gerät</code>.</li>
<li><strong>Fachlich sauber:</strong> Tags sollen echte Produktfamilien, Anwendungsfälle oder Entitäten abbilden.</li>
<li><strong>Wenig Überschneidung:</strong> keine unnötig breiten oder doppeldeutigen Tags.</li>
<li><strong>Bewusst typisieren:</strong> <code>catalog_entity</code> für echte Katalog-/Entity-Tags, <code>generic</code> nur für allgemeine Zusatzsemantik.</li>
</ul>
<p class="small text-light mt-2 mb-0">
Tags wirken somit als semantischer Verstärker.
Sie ersetzen kein Chunk-Retrieval, sondern steuern dessen Priorisierung.
<p class="small text-warning mb-0">
Zu breite Tags wie „Produkt“, „System“ oder „Gerät“ machen das Routing weicher
und bringen meist weniger Nutzen als präzise fachliche Tags.
</p>
</div>
</div>
@@ -90,9 +65,9 @@
if (data.status === '{{ statusRunning }}') {
html = `
<div class="alert alert-info shadow-sm d-flex justify-content-between align-items-center">
<div class="alert alert-info shadow-sm d-flex justify-content-between align-items-center mb-0">
<div>
Tag-Rebuild läuft<br>
<strong>Tag-Rebuild läuft</strong><br>
${data.startedAt ? 'Gestartet: ' + new Date(data.startedAt).toLocaleString() : ''}
</div>
<div class="spinner-border spinner-border-sm"></div>
@@ -100,20 +75,20 @@
`;
} else if (data.status === '{{ statusQueued }}') {
html = `
<div class="alert alert-secondary shadow-sm">
Tag-Rebuild in Warteschlange
<div class="alert alert-secondary shadow-sm mb-0">
<strong>Tag-Rebuild in Warteschlange</strong>
</div>
`;
} else if (data.status === '{{ statusCompleted }}') {
html = `
<div class="alert alert-success shadow-sm">
<div class="alert alert-success shadow-sm mb-0">
<i class="bi bi-check-lg"></i> Tag-Rebuild erfolgreich abgeschlossen
</div>
`;
} else if (data.status === '{{ statusFailed }}') {
html = `
<div class="alert alert-danger shadow-sm">
Tag-Rebuild fehlgeschlagen<br>
<div class="alert alert-danger shadow-sm mb-0">
<strong>Tag-Rebuild fehlgeschlagen</strong><br>
${data.error ? '<code>' + data.error + '</code>' : ''}
</div>
`;
@@ -125,11 +100,12 @@
source.onerror = function () {
console.warn('SSE Verbindung verloren');
};
window.addEventListener('beforeunload', function () {
source.close();
});
</script>
{# ========================================================= #}
{# Create Tag Card #}
{# ========================================================= #}
<div class="card bg-black border-secondary text-light mb-4 shadow-sm">
<div class="card-body">
<h5 class="text-info mb-3">Neuen Tag hinzufügen</h5>
@@ -153,24 +129,26 @@
required/>
</div>
<div class="col-md-4">
<label class="form-label small text-muted">Beschreibung</label>
<input class="form-control form-control-sm"
name="description"
placeholder="Semantische Beschreibung des Tags"
required/>
</div>
<div class="mb-3">
<label class="form-label">Type</label>
<select name="type" class="form-select">
<option value="generic">Generic</option>
<option value="catalog_entity">Catalog Entity</option>
<option value="sales_signal">Sales Signal</option>
<div class="col-md-3">
<label class="form-label small text-muted">Typ</label>
<select name="type" class="form-select form-select-sm">
{% for choiceLabel, choiceValue in tagTypeChoices %}
<option value="{{ choiceValue }}"
{% if choiceValue == 'generic' %}selected{% endif %}>
{{ choiceLabel }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-2 d-grid align-items-end">
<div class="col-md-3">
<label class="form-label small text-muted">Beschreibung</label>
<input class="form-control form-control-sm"
name="description"
placeholder="Optional: fachlicher Kontext des Tags"/>
</div>
<div class="col-12 d-grid d-md-flex justify-content-md-end">
<button class="btn btn-sm btn-outline-info">
Anlegen
</button>
@@ -179,66 +157,85 @@
</div>
</div>
{# ========================================================= #}
{# Tag Table #}
{# ========================================================= #}
<div class="card bg-black border-secondary text-light shadow-sm">
<div class="card-body">
<div class="mb-3">
<strong class="text-info">Vorhandene Tags:</strong>
<span class="text-muted small ms-2">
{{ tags|length }} Einträge
</span>
<div class="mb-3 d-flex justify-content-between align-items-center flex-wrap gap-2">
<div>
<strong class="text-info">Vorhandene Tags:</strong>
<span class="text-muted small ms-2">
{{ tags|length }} Einträge
</span>
</div>
<div class="small text-muted">
Dokumentanzahl bezieht sich auf aktive Dokumente.
</div>
</div>
<table class="table table-dark table-striped table-hover mb-0 align-middle">
<thead class="table-secondary text-dark">
<tr>
<th style="width: 25%">Label</th>
<th style="width: 25%">Slug</th>
<th style="width: 35%">Beschreibung</th>
<th class="text-end" style="width: 15%">Aktion</th>
</tr>
</thead>
<tbody>
{% for tag in tags %}
<div class="table-responsive">
<table class="table table-dark table-striped table-hover mb-0 align-middle">
<thead class="table-secondary text-dark">
<tr>
<td class="fw-semibold">{{ tag.label }}</td>
<td><code>{{ tag.slug }}</code></td>
<td>{{ tag.description ?: '-' }}</td>
<td class="text-end">
<a href="{{ path('admin_tags_assign', { id: tag.id }) }}"
class="btn btn-sm btn-outline-info me-2">
Zuweisen
</a>
<form method="post"
action="{{ path('admin_tags_delete', {id: tag.id}) }}"
style="display:inline-block;">
<input type="hidden"
name="_token"
value="{{ csrf_token('admin_tag_delete_' ~ tag.id) }}"/>
<button class="btn btn-sm btn-outline-danger"
onclick="return confirm('Tag wirklich löschen? Zuweisungen werden entfernt.')">
Löschen
</button>
</form>
</td>
<th style="width: 18%">Label</th>
<th style="width: 18%">Slug</th>
<th style="width: 14%">Typ</th>
<th style="width: 10%">Aktive Dokumente</th>
<th style="width: 25%">Beschreibung</th>
<th class="text-end" style="width: 15%">Aktion</th>
</tr>
{% else %}
<tr>
<td colspan="4" class="p-4 text-center text-muted">
Noch keine Tags vorhanden.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</thead>
<tbody>
{% for tag in tags %}
{% set activeDocumentCount = documentCountByTagId[tag.id.toRfc4122] ?? 0 %}
<tr>
<td class="fw-semibold">{{ tag.label }}</td>
<td><code>{{ tag.slug }}</code></td>
<td>
{% if tag.type == 'catalog_entity' %}
<span class="badge text-bg-info">Catalog Entity</span>
{% elseif tag.type == 'sales_signal' %}
<span class="badge text-bg-warning">Sales Signal</span>
{% else %}
<span class="badge text-bg-secondary">Generic</span>
{% endif %}
</td>
<td>
<span class="badge text-bg-dark border border-secondary">
{{ activeDocumentCount }}
</span>
</td>
<td>{{ tag.description ?: '-' }}</td>
<td class="text-end">
<a href="{{ path('admin_tags_assign', { id: tag.id }) }}"
class="btn btn-sm btn-outline-info me-2">
Zuweisen
</a>
<form method="post"
action="{{ path('admin_tags_delete', {id: tag.id}) }}"
style="display:inline-block;">
<input type="hidden"
name="_token"
value="{{ csrf_token('admin_tag_delete_' ~ tag.id) }}"/>
<button class="btn btn-sm btn-outline-danger"
onclick="return confirm('Tag wirklich löschen? Zuweisungen werden entfernt.')">
Löschen
</button>
</form>
</td>
</tr>
{% else %}
<tr>
<td colspan="6" class="p-4 text-center text-muted">
Noch keine Tags vorhanden.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>