p64
This commit is contained in:
@@ -30,3 +30,46 @@ parameters:
|
|||||||
storage:
|
storage:
|
||||||
directory_create_failed: 'Stream job directory could not be created.'
|
directory_create_failed: 'Stream job directory could not be created.'
|
||||||
write_failed: 'Stream job could not be written.'
|
write_failed: 'Stream job could not be written.'
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
document:
|
||||||
|
title: 'AI Agent'
|
||||||
|
ui:
|
||||||
|
header_title: 'KI-Agent'
|
||||||
|
footer_disclaimer: 'powered by mitho® | RetrieX kann fehlerhafte Ausgaben machen. RetrieX verwendet alle Daten zum Trainieren seiner Modelle.'
|
||||||
|
buttons:
|
||||||
|
clear: 'Diesen Chat löschen'
|
||||||
|
send: 'Send'
|
||||||
|
abort: 'Abbrechen'
|
||||||
|
options:
|
||||||
|
aria_label: 'Chat-Anzeigeoptionen'
|
||||||
|
status_info: 'Statusinfo anzeigen'
|
||||||
|
input:
|
||||||
|
prompt_placeholder: 'Stelle eine Frage'
|
||||||
|
assistant:
|
||||||
|
loader: 'Antwort wird vorbereitet…'
|
||||||
|
aborted: '[aborted]'
|
||||||
|
history_cleared: 'History cleared.'
|
||||||
|
source_chips:
|
||||||
|
live_shop_data: 'Live-Shopdaten'
|
||||||
|
run_meta:
|
||||||
|
completed_title: 'Abgeschlossen'
|
||||||
|
interrupted_title: 'Antwort wurde unterbrochen'
|
||||||
|
completed_status: 'Status: abgeschlossen'
|
||||||
|
interrupted_status: 'Status: unterbrochen'
|
||||||
|
completed_empty_source: 'keine belastbare Datenbasis'
|
||||||
|
interrupted_empty_source: 'nicht vollständig geprüft'
|
||||||
|
pending_source_marker: 'wird geprüft'
|
||||||
|
stream:
|
||||||
|
incomplete: 'Der Antwort-Stream wurde beendet, bevor die Antwort abgeschlossen werden konnte.'
|
||||||
|
job_not_found_retry: 'Der Antwort-Job wurde nicht mehr gefunden. Bitte sende die Anfrage erneut.'
|
||||||
|
failed_retry: 'Der Antwort-Stream ist fehlgeschlagen. Bitte sende die Anfrage erneut.'
|
||||||
|
interrupted_retry: 'Der Antwort-Stream wurde durch einen Verbindungsabbruch unterbrochen. Bitte sende die Anfrage erneut, falls die Antwort unvollständig ist.'
|
||||||
|
missing_retry: 'Der Antwort-Job wurde nicht gefunden. Bitte sende die Anfrage erneut.'
|
||||||
|
stale_retry: 'Der Antwort-Job liefert seit längerer Zeit keine neuen Daten. Der Stream wurde beendet.'
|
||||||
|
connection_interrupted_retry: 'Die Verbindung zum Antwort-Stream wurde unterbrochen. Bitte sende die Anfrage erneut, falls die Antwort unvollständig ist.'
|
||||||
|
guards:
|
||||||
|
no_concrete_shop_response_markers:
|
||||||
|
- 'keine konkrete shop-suchanfrage erkannt'
|
||||||
|
- 'shop-suche noch nicht belastbar auflösen'
|
||||||
|
- 'shop-suche noch nicht belastbar aufloesen'
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# RetrieX Patch 64 - Frontend Chat Messages Config
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
p64 erweitert den mit p63 eingeführten `chat-messages.yaml`-Schnitt auf die sichtbaren Frontend-Chattexte in `public/index.html` und `public/assets/js/base.js`.
|
||||||
|
|
||||||
|
Ziel ist, dass die im Browser sichtbaren Chat-UI-, Loader-, Status- und Stream-Fallback-Texte nicht mehr hart in HTML/JavaScript gepflegt werden müssen, sondern zentral über `config/retriex/chat-messages.yaml` konfigurierbar sind.
|
||||||
|
|
||||||
|
## Änderungen
|
||||||
|
|
||||||
|
- `config/retriex/chat-messages.yaml`
|
||||||
|
- neuer Bereich `frontend`
|
||||||
|
- UI-Texte für Titel, Header, Buttons, Placeholder, Footer und Optionen
|
||||||
|
- Assistant-Hinweise wie Loader, Abbruch- und History-Cleared-Meldung
|
||||||
|
- Frontend-Stream-Fallback-Meldungen
|
||||||
|
- Run-Meta-Statuslabels
|
||||||
|
- Live-Shopdaten-Chip-Label
|
||||||
|
- konfigurierbare Marker für No-Concrete-Shop-Response-Erkennung
|
||||||
|
|
||||||
|
- `src/Config/ChatMessagesConfig.php`
|
||||||
|
- `getFrontendMessages()` ergänzt
|
||||||
|
- Validation um Frontend-Message-Pfade erweitert
|
||||||
|
- List-Validation für konfigurierbare Guard-Marker ergänzt
|
||||||
|
|
||||||
|
- `src/Controller/ChatMessagesController.php`
|
||||||
|
- neuer JSON-Endpunkt `/chat-messages/frontend`
|
||||||
|
- liefert den `frontend`-Teil aus `chat-messages.yaml` an das Browser-Frontend
|
||||||
|
- no-store/no-cache Header gesetzt
|
||||||
|
|
||||||
|
- `public/assets/js/base.js`
|
||||||
|
- lädt Chat-Frontend-Messages beim Start aus `/chat-messages/frontend`
|
||||||
|
- wendet UI-Texte über `data-chat-message-*` Attribute an
|
||||||
|
- ersetzt sichtbare Loader-/Stream-/Status-/Abbruch-/History-Texte durch YAML-Werte
|
||||||
|
- No-Concrete-Shop-Response-Marker kommen aus YAML statt aus JS-Stringliteralen
|
||||||
|
|
||||||
|
- `public/index.html`
|
||||||
|
- sichtbare harte UI-Texte entfernt
|
||||||
|
- stattdessen `data-chat-message-text`, `data-chat-message-placeholder` und `data-chat-message-aria-label`
|
||||||
|
- Dokumenttitel wird durch `base.js` aus YAML gesetzt
|
||||||
|
|
||||||
|
## Bewusste Nicht-Ziele
|
||||||
|
|
||||||
|
- p64 verschiebt die bereits YAML-basierten Agent-/Production-UI-Blöcke aus `agent.yaml` noch nicht nach `chat-messages.yaml`.
|
||||||
|
- p64 ändert keine RAG-, Shop-, Prompt-, Retrieval- oder Scoring-Logik.
|
||||||
|
- Technische Protokollwerte wie SSE-Eventnamen, Job-Statuswerte, Storage Keys, CSS-Klassen und Debug-/Console-Meldungen bleiben im Code.
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
|
||||||
|
Lokal geprüft:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php -l src/Config/ChatMessagesConfig.php
|
||||||
|
php -l src/Controller/ChatMessagesController.php
|
||||||
|
php -l src/Controller/AskSseController.php
|
||||||
|
node --check public/assets/js/base.js
|
||||||
|
python3 - <<'PY'
|
||||||
|
import yaml
|
||||||
|
for path in ['config/retriex/chat-messages.yaml', 'config/services.yaml']:
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
yaml.safe_load(f)
|
||||||
|
print(f'YAML OK: {path}')
|
||||||
|
PY
|
||||||
|
```
|
||||||
|
|
||||||
|
Nicht ausführbar im entpackten ZIP ohne `vendor/`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php bin/console mto:agent:config:validate
|
||||||
|
```
|
||||||
|
|
||||||
|
Fehler:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Dependencies are missing. Try running "composer install".
|
||||||
|
```
|
||||||
|
|
||||||
|
## Empfohlene manuelle Regression
|
||||||
|
|
||||||
|
Nach Einspielen im echten Projekt mit Vendor-Abhängigkeiten:
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
Browser-Check:
|
||||||
|
|
||||||
|
1. Startseite öffnen.
|
||||||
|
2. Prüfen, ob Titel, Header, Buttons, Placeholder und Footer korrekt erscheinen.
|
||||||
|
3. Eine normale Anfrage senden und Loader/Statuskarten prüfen.
|
||||||
|
4. Stream abbrechen und Abbruchmeldung prüfen.
|
||||||
|
5. Chat löschen und History-Cleared-Meldung prüfen.
|
||||||
|
6. Einen Stream-/Reconnect-Fehlerfall prüfen, falls reproduzierbar.
|
||||||
|
|
||||||
|
## Folgearbeit
|
||||||
|
|
||||||
|
Falls wirklich alle chat-sichtbaren Texte in exakt einer Datei liegen sollen, sollte ein späterer Patch die bereits YAML-konfigurierten Agent-UI-Bereiche aus `agent.yaml` nach `chat-messages.yaml` verschieben und die Getter in `AgentRunnerConfig` sauber delegieren oder auf eine Message-spezifische Config-Fassade umstellen.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
const chatEl = document.getElementById('chat');
|
const chatEl = document.getElementById('chat');
|
||||||
const promptEl = document.getElementById('prompt');
|
const promptEl = document.getElementById('prompt');
|
||||||
const sendBtn = document.getElementById('send');
|
const sendBtn = document.getElementById('send');
|
||||||
@@ -12,6 +12,106 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const JOB_COMPLETION_CATCHUP_GRACE_MS = 10000;
|
const JOB_COMPLETION_CATCHUP_GRACE_MS = 10000;
|
||||||
const JOB_CLIENT_STALE_GRACE_MS = 150000;
|
const JOB_CLIENT_STALE_GRACE_MS = 150000;
|
||||||
|
|
||||||
|
let chatMessages = {};
|
||||||
|
|
||||||
|
function configuredMessage(path, parameters = {}) {
|
||||||
|
const segments = String(path || '').split('.').filter(Boolean);
|
||||||
|
let current = chatMessages;
|
||||||
|
|
||||||
|
for (const segment of segments) {
|
||||||
|
if (!current || typeof current !== 'object' || !Object.prototype.hasOwnProperty.call(current, segment)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
current = current[segment];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof current !== 'string') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return current.replace(/\{([a-zA-Z0-9_]+)\}/g, (match, key) => {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(parameters, key)) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(parameters[key] ?? '').replace(/\s+/g, ' ').trim();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function configuredMessageList(path) {
|
||||||
|
const segments = String(path || '').split('.').filter(Boolean);
|
||||||
|
let current = chatMessages;
|
||||||
|
|
||||||
|
for (const segment of segments) {
|
||||||
|
if (!current || typeof current !== 'object' || !Object.prototype.hasOwnProperty.call(current, segment)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
current = current[segment];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.isArray(current) ? current.filter((item) => typeof item === 'string' && item.trim() !== '') : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value || '')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function configuredEmphasis(path) {
|
||||||
|
const text = configuredMessage(path);
|
||||||
|
|
||||||
|
return text === '' ? '' : `<em>${escapeHtml(text)}</em>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadConfiguredChatMessages() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/chat-messages/frontend', {
|
||||||
|
method: 'GET',
|
||||||
|
cache: 'no-store',
|
||||||
|
headers: {'Accept': 'application/json'},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('Chat messages request failed with status:', response.status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = await response.json();
|
||||||
|
chatMessages = payload && typeof payload === 'object' ? payload : {};
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Chat messages load failed:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyConfiguredUiMessages(root = document) {
|
||||||
|
const documentTitle = configuredMessage('document.title');
|
||||||
|
|
||||||
|
if (documentTitle !== '') {
|
||||||
|
document.title = documentTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.querySelectorAll('[data-chat-message-text]').forEach((element) => {
|
||||||
|
element.textContent = configuredMessage(element.getAttribute('data-chat-message-text') || '');
|
||||||
|
});
|
||||||
|
|
||||||
|
root.querySelectorAll('[data-chat-message-placeholder]').forEach((element) => {
|
||||||
|
element.setAttribute('placeholder', configuredMessage(element.getAttribute('data-chat-message-placeholder') || ''));
|
||||||
|
});
|
||||||
|
|
||||||
|
root.querySelectorAll('[data-chat-message-aria-label]').forEach((element) => {
|
||||||
|
element.setAttribute('aria-label', configuredMessage(element.getAttribute('data-chat-message-aria-label') || ''));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadConfiguredChatMessages();
|
||||||
|
applyConfiguredUiMessages();
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
abortRequested: false,
|
abortRequested: false,
|
||||||
isStreaming: false,
|
isStreaming: false,
|
||||||
@@ -118,9 +218,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
function isNoConcreteShopResponse(value) {
|
function isNoConcreteShopResponse(value) {
|
||||||
const normalized = normalizeContextHintText(value).toLowerCase();
|
const normalized = normalizeContextHintText(value).toLowerCase();
|
||||||
|
|
||||||
return normalized.includes('keine konkrete shop-suchanfrage erkannt')
|
return configuredMessageList('guards.no_concrete_shop_response_markers')
|
||||||
|| normalized.includes('shop-suche noch nicht belastbar auflösen')
|
.some((marker) => normalized.includes(marker.toLowerCase()));
|
||||||
|| normalized.includes('shop-suche noch nicht belastbar aufloesen');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function rememberCompletedTurn(userPrompt, assistantText) {
|
function rememberCompletedTurn(userPrompt, assistantText) {
|
||||||
@@ -278,7 +377,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addLoader() {
|
function addLoader() {
|
||||||
return addMessage('assistant', 'Antwort wird vorbereitet…', 'loader');
|
return addMessage('assistant', escapeHtml(configuredMessage('assistant.loader')), 'loader');
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasMeaningfulChildContent(element) {
|
function hasMeaningfulChildContent(element) {
|
||||||
@@ -504,7 +603,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
let key = canonicalRetriexSourceChipKey(chip.textContent);
|
let key = canonicalRetriexSourceChipKey(chip.textContent);
|
||||||
|
|
||||||
if (key === 'shopsystem' || key === 'liveshopdaten') {
|
if (key === 'shopsystem' || key === 'liveshopdaten') {
|
||||||
chip.textContent = 'Live-Shopdaten';
|
chip.textContent = configuredMessage('source_chips.live_shop_data');
|
||||||
key = 'liveshopdaten';
|
key = 'liveshopdaten';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -643,9 +742,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isError = options.state === 'error';
|
const isError = options.state === 'error';
|
||||||
const titleText = isError ? 'Antwort wurde unterbrochen' : 'Abgeschlossen';
|
const titleText = isError ? configuredMessage('run_meta.interrupted_title') : configuredMessage('run_meta.completed_title');
|
||||||
const statusText = isError ? 'Status: unterbrochen' : 'Status: abgeschlossen';
|
const statusText = isError ? configuredMessage('run_meta.interrupted_status') : configuredMessage('run_meta.completed_status');
|
||||||
const emptySourceText = isError ? 'nicht vollständig geprüft' : 'keine belastbare Datenbasis';
|
const emptySourceText = isError ? configuredMessage('run_meta.interrupted_empty_source') : configuredMessage('run_meta.completed_empty_source');
|
||||||
|
|
||||||
container.querySelectorAll('.retriex-run-meta[data-retriex-meta-id="run-status"]').forEach((card) => {
|
container.querySelectorAll('.retriex-run-meta[data-retriex-meta-id="run-status"]').forEach((card) => {
|
||||||
card.setAttribute('data-retriex-meta-state', isError ? 'error' : 'completed');
|
card.setAttribute('data-retriex-meta-state', isError ? 'error' : 'completed');
|
||||||
@@ -664,7 +763,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
const emptySource = card.querySelector('.retriex-source-overview__empty');
|
const emptySource = card.querySelector('.retriex-source-overview__empty');
|
||||||
|
|
||||||
if (emptySource && emptySource.textContent.trim() === 'wird geprüft') {
|
if (emptySource && emptySource.textContent.trim() === configuredMessage('run_meta.pending_source_marker')) {
|
||||||
emptySource.textContent = emptySourceText;
|
emptySource.textContent = emptySourceText;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -919,7 +1018,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const completeWithJobError = (message) => {
|
const completeWithJobError = (message) => {
|
||||||
appendError(message || 'Der Antwort-Stream wurde beendet, bevor die Antwort abgeschlossen werden konnte.');
|
appendError(message || configuredMessage('stream.incomplete'));
|
||||||
complete();
|
complete();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -937,7 +1036,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
if (res.status === 404 && Date.now() - lastClientProgressAt > JOB_CLIENT_STALE_GRACE_MS) {
|
if (res.status === 404 && Date.now() - lastClientProgressAt > JOB_CLIENT_STALE_GRACE_MS) {
|
||||||
completeWithJobError('Der Antwort-Job wurde nicht mehr gefunden. Bitte sende die Anfrage erneut.');
|
completeWithJobError(configuredMessage('stream.job_not_found_retry'));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -952,17 +1051,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'failed') {
|
if (status === 'failed') {
|
||||||
completeWithJobError(message || 'Der Antwort-Stream ist fehlgeschlagen. Bitte sende die Anfrage erneut.');
|
completeWithJobError(message || configuredMessage('stream.failed_retry'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'interrupted') {
|
if (status === 'interrupted') {
|
||||||
completeWithJobError(message || 'Der Antwort-Stream wurde durch einen Verbindungsabbruch unterbrochen. Bitte sende die Anfrage erneut, falls die Antwort unvollständig ist.');
|
completeWithJobError(message || configuredMessage('stream.interrupted_retry'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'missing') {
|
if (status === 'missing') {
|
||||||
completeWithJobError(message || 'Der Antwort-Job wurde nicht gefunden. Bitte sende die Anfrage erneut.');
|
completeWithJobError(message || configuredMessage('stream.missing_retry'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -972,7 +1071,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const staleAfterSeconds = parsePositiveInteger(statusPayload?.runningStaleAfterSeconds);
|
const staleAfterSeconds = parsePositiveInteger(statusPayload?.runningStaleAfterSeconds);
|
||||||
|
|
||||||
if (updatedAt > 0 && serverTime > 0 && staleAfterSeconds > 0 && serverTime - updatedAt > staleAfterSeconds + 15) {
|
if (updatedAt > 0 && serverTime > 0 && staleAfterSeconds > 0 && serverTime - updatedAt > staleAfterSeconds + 15) {
|
||||||
completeWithJobError(message || 'Der Antwort-Job liefert seit längerer Zeit keine neuen Daten. Der Stream wurde beendet.');
|
completeWithJobError(message || configuredMessage('stream.stale_retry'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1065,7 +1164,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
bubble.classList.remove('loader');
|
bubble.classList.remove('loader');
|
||||||
|
|
||||||
const userMessage = 'Die Verbindung zum Antwort-Stream wurde unterbrochen. Bitte sende die Anfrage erneut, falls die Antwort unvollständig ist.';
|
const userMessage = configuredMessage('stream.connection_interrupted_retry');
|
||||||
|
|
||||||
if (raw.trim() !== '') {
|
if (raw.trim() !== '') {
|
||||||
const formattedMessage = `<em>${userMessage}</em>`;
|
const formattedMessage = `<em>${userMessage}</em>`;
|
||||||
@@ -1094,7 +1193,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
state.completeStream?.();
|
state.completeStream?.();
|
||||||
await releaseStreamResources();
|
await releaseStreamResources();
|
||||||
setBusyUi(false);
|
setBusyUi(false);
|
||||||
addMessage('assistant', '<em>[aborted]</em>');
|
addMessage('assistant', configuredEmphasis('assistant.aborted'));
|
||||||
});
|
});
|
||||||
|
|
||||||
clearBtn.addEventListener('click', async () => {
|
clearBtn.addEventListener('click', async () => {
|
||||||
@@ -1116,6 +1215,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
console.debug('Could not clear last completed turn:', err);
|
console.debug('Could not clear last completed turn:', err);
|
||||||
}
|
}
|
||||||
chatEl.innerHTML = '';
|
chatEl.innerHTML = '';
|
||||||
addMessage('assistant', '<em>History cleared.</em>');
|
addMessage('assistant', configuredEmphasis('assistant.history_cleared'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>AI Agent</title>
|
<title></title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
<!-- Markdown + Sanitizer -->
|
<!-- Markdown + Sanitizer -->
|
||||||
@@ -25,29 +25,29 @@
|
|||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<img src="/assets/img/logo.png" style="max-height: 20px;">
|
<img src="/assets/img/logo.png" style="max-height: 20px;">
|
||||||
</div>
|
</div>
|
||||||
<div class="text-info fw-bold" style="font-size: 12px">KI-Agent</div>
|
<div class="text-info fw-bold" style="font-size: 12px" data-chat-message-text="ui.header_title"></div>
|
||||||
</div>
|
</div>
|
||||||
<img src="/assets/img/logo.svg" style="max-height: 20px;">
|
<img src="/assets/img/logo.svg" style="max-height: 20px;">
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
|
|
||||||
<button id="clear" class="btn btn-trans">Diesen Chat löschen</button>
|
<button id="clear" class="btn btn-trans" data-chat-message-text="ui.buttons.clear"></button>
|
||||||
</div>
|
</div>
|
||||||
<div id="ai-cloud" class="ai-cloud d-none"></div>
|
<div id="ai-cloud" class="ai-cloud d-none"></div>
|
||||||
<div id="chat" class="chat"></div>
|
<div id="chat" class="chat"></div>
|
||||||
|
|
||||||
<div id="retriex-chat-options" class="retriex-chat-options p-2" aria-label="Chat-Anzeigeoptionen">
|
<div id="retriex-chat-options" class="retriex-chat-options p-2" data-chat-message-aria-label="ui.options.aria_label">
|
||||||
<label class="retriex-option-toggle" for="toggle-retriex-cards">
|
<label class="retriex-option-toggle" for="toggle-retriex-cards">
|
||||||
<input id="toggle-retriex-cards" type="checkbox">
|
<input id="toggle-retriex-cards" type="checkbox">
|
||||||
<span>Statusinfo anzeigen</span>
|
<span data-chat-message-text="ui.options.status_info"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-area">
|
<div class="input-area">
|
||||||
<textarea id="prompt" class="form-control bg-dark" placeholder="Stelle eine Frage"></textarea>
|
<textarea id="prompt" class="form-control bg-dark" data-chat-message-placeholder="ui.input.prompt_placeholder"></textarea>
|
||||||
<button id="send" class="btn btn-trans">Send</button>
|
<button id="send" class="btn btn-trans" data-chat-message-text="ui.buttons.send"></button>
|
||||||
<button id="abort" class="btn btn-trans" disabled>Abbrechen</button>
|
<button id="abort" class="btn btn-trans" disabled data-chat-message-text="ui.buttons.abort"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="small mt-2 text-center text-secondary">powered by mitho® | RetrieX kann fehlerhafte Ausgaben machen. RetrieX verwendet alle Daten zum Trainieren seiner Modelle.</div>
|
<div class="small mt-2 text-center text-secondary" data-chat-message-text="ui.footer_disclaimer"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
44
qwen3-8b-system-pormpt.txt
Normal file
44
qwen3-8b-system-pormpt.txt
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# ==========================================
|
||||||
|
# QWEN 3 8B - RAG OPTIMIZED CONFIGURATION
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# Basis-Modell festlegen
|
||||||
|
FROM qwen3:8b
|
||||||
|
|
||||||
|
# --- TECHNISCHE PARAMETER (Sampling & Performance) ---
|
||||||
|
# Schaltet Kreativität aus, um Halluzinationen in RAG zu vermeiden
|
||||||
|
PARAMETER temperature 0.0
|
||||||
|
# Sorgt für deterministische (gleichbleibende) Antworten
|
||||||
|
PARAMETER seed 42
|
||||||
|
# Erweitert den Arbeitsspeicher für Dokumente (16k Token)
|
||||||
|
PARAMETER num_ctx 16384
|
||||||
|
# Verhindert Wort-Wiederholungen aus den Quelltexten
|
||||||
|
PARAMETER repeat_penalty 1.1
|
||||||
|
# Definiert, wie viele Tokens gleichzeitig verarbeitet werden
|
||||||
|
PARAMETER num_predict 2048
|
||||||
|
|
||||||
|
# --- SYSTEM PROMPT (Verhalten & Regeln) ---
|
||||||
|
SYSTEM """
|
||||||
|
Du bist ein spezialisierter RAG-Analyst für Qwen 3. Deine einzige Aufgabe ist die präzise Extraktion von Informationen aus dem bereitgestellten KONTEXT.
|
||||||
|
|
||||||
|
### STRIKTE ARBEITSANWEISUNGEN:
|
||||||
|
1. QUELLEN-TREUE: Antworte AUSSCHLIESSLICH basierend auf den übergebenen Dokumenten.
|
||||||
|
2. UNWISSENHEIT: Wenn die Information nicht im Kontext steht, antworte exakt mit: "Information nicht in den Dokumenten vorhanden."
|
||||||
|
3. KEINE HALLUZINATIONEN: Erfinde keine Fakten und ergänze kein externes Wissen.
|
||||||
|
4. ZITIERPFLICHT: Füge hinter jede Fakten-Aussage die Quelle in eckigen Klammern an, z.B. [Dokument Name, Seite X].
|
||||||
|
5. FORMATIERUNG: Nutze Markdown-Tabellen für Datenvergleiche und Bulletpoints für Listen.
|
||||||
|
|
||||||
|
### SPRACHE & TON:
|
||||||
|
- Sprache: Deutsch (Sachlich, professionell).
|
||||||
|
- Keine Höflichkeitsfloskeln am Anfang oder Ende.
|
||||||
|
- Direkt auf den Punkt kommen.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# --- TEMPLATE (ChatML Struktur) ---
|
||||||
|
TEMPLATE """{{ if .System }}<|im_start|>system
|
||||||
|
{{ .System }}<|im_end|>
|
||||||
|
{{ end }}{{ if .Prompt }}<|im_start|>user
|
||||||
|
{{ .Prompt }}<|im_end|>
|
||||||
|
{{ end }}<|im_start|>assistant
|
||||||
|
{{ .Response }}<|im_end|>"""
|
||||||
|
|
||||||
@@ -142,6 +142,20 @@ final class ChatMessagesConfig
|
|||||||
return $this->string('sse.storage.write_failed');
|
return $this->string('sse.storage.write_failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function getFrontendMessages(): array
|
||||||
|
{
|
||||||
|
$messages = $this->value('frontend');
|
||||||
|
|
||||||
|
if (is_array($messages)) {
|
||||||
|
return $messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \InvalidArgumentException('RetrieX chat messages config key "frontend" must be an array.');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
@@ -165,6 +179,14 @@ final class ChatMessagesConfig
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach ($this->requiredStringListPaths() as $path) {
|
||||||
|
try {
|
||||||
|
$this->stringList($path);
|
||||||
|
} catch (\InvalidArgumentException $e) {
|
||||||
|
$errors[] = $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'status' => $errors === [] ? 'OK' : 'ERROR',
|
'status' => $errors === [] ? 'OK' : 'ERROR',
|
||||||
'errors' => $errors,
|
'errors' => $errors,
|
||||||
@@ -202,6 +224,43 @@ final class ChatMessagesConfig
|
|||||||
'sse.claim.missing',
|
'sse.claim.missing',
|
||||||
'sse.storage.directory_create_failed',
|
'sse.storage.directory_create_failed',
|
||||||
'sse.storage.write_failed',
|
'sse.storage.write_failed',
|
||||||
|
'frontend.document.title',
|
||||||
|
'frontend.ui.header_title',
|
||||||
|
'frontend.ui.footer_disclaimer',
|
||||||
|
'frontend.ui.buttons.clear',
|
||||||
|
'frontend.ui.buttons.send',
|
||||||
|
'frontend.ui.buttons.abort',
|
||||||
|
'frontend.ui.options.aria_label',
|
||||||
|
'frontend.ui.options.status_info',
|
||||||
|
'frontend.ui.input.prompt_placeholder',
|
||||||
|
'frontend.assistant.loader',
|
||||||
|
'frontend.assistant.aborted',
|
||||||
|
'frontend.assistant.history_cleared',
|
||||||
|
'frontend.source_chips.live_shop_data',
|
||||||
|
'frontend.run_meta.completed_title',
|
||||||
|
'frontend.run_meta.interrupted_title',
|
||||||
|
'frontend.run_meta.completed_status',
|
||||||
|
'frontend.run_meta.interrupted_status',
|
||||||
|
'frontend.run_meta.completed_empty_source',
|
||||||
|
'frontend.run_meta.interrupted_empty_source',
|
||||||
|
'frontend.run_meta.pending_source_marker',
|
||||||
|
'frontend.stream.incomplete',
|
||||||
|
'frontend.stream.job_not_found_retry',
|
||||||
|
'frontend.stream.failed_retry',
|
||||||
|
'frontend.stream.interrupted_retry',
|
||||||
|
'frontend.stream.missing_retry',
|
||||||
|
'frontend.stream.stale_retry',
|
||||||
|
'frontend.stream.connection_interrupted_retry',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private function requiredStringListPaths(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'frontend.guards.no_concrete_shop_response_markers',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,6 +296,34 @@ final class ChatMessagesConfig
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private function stringList(string $path): array
|
||||||
|
{
|
||||||
|
$value = $this->value($path);
|
||||||
|
|
||||||
|
if (!is_array($value)) {
|
||||||
|
throw new \InvalidArgumentException(sprintf('RetrieX chat messages config key "%s" must be a list of non-empty strings.', $path));
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = [];
|
||||||
|
|
||||||
|
foreach ($value as $item) {
|
||||||
|
if (!is_string($item) || trim($item) === '') {
|
||||||
|
throw new \InvalidArgumentException(sprintf('RetrieX chat messages config key "%s" must contain only non-empty strings.', $path));
|
||||||
|
}
|
||||||
|
|
||||||
|
$items[] = $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($items === []) {
|
||||||
|
throw new \InvalidArgumentException(sprintf('RetrieX chat messages config key "%s" must not be empty.', $path));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $items;
|
||||||
|
}
|
||||||
|
|
||||||
private function string(string $path): string
|
private function string(string $path): string
|
||||||
{
|
{
|
||||||
$value = $this->value($path);
|
$value = $this->value($path);
|
||||||
|
|||||||
27
src/Controller/ChatMessagesController.php
Normal file
27
src/Controller/ChatMessagesController.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Config\ChatMessagesConfig;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
|
final readonly class ChatMessagesController
|
||||||
|
{
|
||||||
|
public function __construct(private ChatMessagesConfig $chatMessages)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/chat-messages/frontend', name: 'chat_messages_frontend', methods: ['GET'])]
|
||||||
|
public function frontend(): JsonResponse
|
||||||
|
{
|
||||||
|
$response = new JsonResponse($this->chatMessages->getFrontendMessages());
|
||||||
|
$response->headers->set('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
|
||||||
|
$response->headers->set('Pragma', 'no-cache');
|
||||||
|
$response->headers->set('Expires', '0');
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user