feat: Dynamische KI-Modelle, verbessertes Memory-System und Chat-Überarbeitung

🎯 KI-Modellverwaltung
- Dynamisches Laden verfügbarer Modelle via opencode models
- 29 Modelle verfügbar (opencode, anthropic, ollama)
- Gruppierung nach Anbieter in UI
- Cache-Mechanismus (1h TTL) für Performance
- API-Endpoint /api/models für Modellabfrage

🧠 Memory-System komplett überarbeitet
- JSON-basierte strukturierte Erinnerungen statt Markdown-Chaos
- Separate Memory-Typen: tasks.json, notes.json, research.json
- Automatische Memory-Zusammenfassung im Systemprompt
- Limitierung auf letzte 100 Einträge pro Typ
- Vollständige Task-Ergebnisse statt abgeschnittener Texte

📁 Agenten-Ordnerstruktur
- work/ Verzeichnis für Agent-Dateien
- memory/ Verzeichnis für strukturierte Erinnerungen
- Agenten arbeiten nur in eigenem work-Verzeichnis
- Absolute Pfade werden übergeben
- Dateien-UI zeigt Agent-Work-Folders

💬 Chat-System überarbeitet
- Echte Agent-Ausführung statt Mock-Responses
- Server-Sent Events für Live-Streaming
- Session-basierte Chat-History
- Loading-Spinner und Status-Anzeigen
- Automatisches Speichern in Session

🎭 Personality Integration
- personality.md wird jetzt geladen
- Persönlichkeit vor Systemprompt eingefügt
- Gilt für alle: Chat, Tasks, Orchestrator, Email-Poller

 Weitere Verbesserungen
- Alle Agenten nutzen execute_agent_task() zentral
- Memory-Speicherung nach jedem Task
- Work-Files in Datei-Verwaltung sichtbar
- System-Dateien ausgeblendet
- API-Route für Agent-Work-Dateien
This commit is contained in:
pdyde 2026-02-21 11:44:06 +01:00
parent 84b2fe3dd7
commit 93eb8c6d47
83 changed files with 1692 additions and 1517 deletions

View file

@ -97,14 +97,25 @@
<div class="mb-3">
<label for="model_select" class="form-label">KI-Modell</label>
<div class="form-text mb-2">
Wähle das KI-Modell, das dieser Agent verwendet.
Wähle das KI-Modell, das dieser Agent verwendet. <span class="badge bg-secondary">{{ available_models.count }} Modelle</span>
<button type="button" class="btn btn-sm btn-outline-secondary ms-2" onclick="refreshModels()">
<span id="refresh-icon">🔄</span> Aktualisieren
</button>
</div>
<select class="form-select" id="model_select" name="model_select" onchange="saveModel('{{ edit_agent }}')">
<option value="opencode/big-pickle" {% if edit_model == 'opencode/big-pickle' %}selected{% endif %}>opencode/big-pickle</option>
<option value="opencode/gpt-5-nano" {% if edit_model == 'opencode/gpt-5-nano' %}selected{% endif %}>opencode/gpt-5-nano</option>
<option value="opencode/glm-5-free" {% if edit_model == 'opencode/glm-5-free' %}selected{% endif %}>opencode/glm-5-free</option>
<option value="opencode/minimax-m2.5-free" {% if edit_model == 'opencode/minimax-m2.5-free' %}selected{% endif %}>opencode/minimax-m2.5-free</option>
<option value="opencode/trinity-large-preview-free" {% if edit_model == 'opencode/trinity-large-preview-free' %}selected{% endif %}>opencode/trinity-large-preview-free</option>
{% if available_models.grouped %}
{% for provider, models in available_models.grouped.items() %}
<optgroup label="{{ provider }}">
{% for model in models %}
<option value="{{ model }}" {% if edit_model == model %}selected{% endif %}>{{ model }}</option>
{% endfor %}
</optgroup>
{% endfor %}
{% else %}
{% for model in available_models.models %}
<option value="{{ model }}" {% if edit_model == model %}selected{% endif %}>{{ model }}</option>
{% endfor %}
{% endif %}
</select>
<div id="modelStatus" class="form-text mt-2"></div>
</div>
@ -206,6 +217,66 @@ function saveModel(agentName) {
});
}
function refreshModels() {
const icon = document.getElementById('refresh-icon');
const status = document.getElementById('modelStatus');
const select = document.getElementById('model_select');
const currentModel = select.value;
icon.textContent = '⏳';
status.textContent = 'Lade Modelle...';
status.className = 'form-text mt-2 text-info';
fetch('/api/models?refresh=true')
.then(r => r.json())
.then(data => {
// Dropdown neu aufbauen
select.innerHTML = '';
if (data.grouped) {
Object.keys(data.grouped).forEach(provider => {
const optgroup = document.createElement('optgroup');
optgroup.label = provider;
data.grouped[provider].forEach(model => {
const option = document.createElement('option');
option.value = model;
option.textContent = model;
if (model === currentModel) {
option.selected = true;
}
optgroup.appendChild(option);
});
select.appendChild(optgroup);
});
} else {
data.models.forEach(model => {
const option = document.createElement('option');
option.value = model;
option.textContent = model;
if (model === currentModel) {
option.selected = true;
}
select.appendChild(option);
});
}
icon.textContent = '🔄';
status.textContent = '✓ ' + data.count + ' Modelle geladen';
status.className = 'form-text mt-2 text-success';
setTimeout(() => {
status.textContent = '';
}, 3000);
})
.catch(err => {
icon.textContent = '🔄';
status.textContent = 'Fehler beim Laden: ' + err.message;
status.className = 'form-text mt-2 text-danger';
});
}
function deleteAgent(agentName) {
if (!confirm('Willst du den Agenten "' + agentName + '" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden!')) {
return;

View file

@ -14,7 +14,7 @@
<span>💬 Neue Anfrage</span>
</div>
<div class="card-body">
<form method="POST" action="/chat">
<form id="chatForm" onsubmit="sendChat(event)">
<div class="mb-3">
<label for="agent" class="form-label">Agent</label>
<select class="form-select" id="agent" name="agent" required>
@ -29,7 +29,11 @@
<textarea class="form-control" id="prompt" name="prompt" rows="5"
placeholder="Was soll der Agent erledigen?" required></textarea>
</div>
<button type="submit" class="btn btn-primary w-100">Absenden</button>
<button type="submit" class="btn btn-primary w-100" id="sendBtn">
<span id="sendBtnText">Absenden</span>
<span id="sendBtnSpinner" class="d-none">⏳ Verarbeite...</span>
</button>
<div id="chatStatus" class="mt-2 text-center" style="font-size:0.875rem;"></div>
</form>
</div>
</div>
@ -68,3 +72,163 @@
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let eventSource = null;
function sendChat(event) {
event.preventDefault();
const form = document.getElementById('chatForm');
const agent = document.getElementById('agent').value;
const prompt = document.getElementById('prompt').value.trim();
const sendBtn = document.getElementById('sendBtn');
const sendBtnText = document.getElementById('sendBtnText');
const sendBtnSpinner = document.getElementById('sendBtnSpinner');
const chatStatus = document.getElementById('chatStatus');
const chatContainer = document.getElementById('chatContainer');
if (!agent || !prompt) {
chatStatus.innerHTML = '<span style="color:var(--danger);">Bitte Agent und Anfrage auswählen</span>';
return;
}
// Button deaktivieren
sendBtn.disabled = true;
sendBtnText.classList.add('d-none');
sendBtnSpinner.classList.remove('d-none');
chatStatus.innerHTML = '<span style="color:var(--info);">Anfrage wird gesendet...</span>';
// EventSource für Streaming
if (eventSource) {
eventSource.close();
}
// Anfrage per fetch senden
fetch('/chat/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ agent: agent, prompt: prompt })
})
.then(response => {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let currentResponse = '';
let currentTimestamp = new Date().toLocaleString('de-DE', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit'
});
let currentAgent = '';
// Neue Chat-Message erstellen
const chatMessage = document.createElement('div');
chatMessage.className = 'chat-message';
chatMessage.innerHTML = `
<div class="chat-timestamp">
${currentTimestamp}
<span class="badge bg-primary" id="chatAgentBadge">...</span>
</div>
<div class="chat-prompt"><strong>Sie:</strong> ${escapeHtml(prompt)}</div>
<div class="chat-response mt-1"><strong>Antwort:</strong> <span id="chatResponseText">⏳ Warte auf Antwort...</span></div>
`;
// Placeholder entfernen falls vorhanden
const placeholder = chatContainer.querySelector('.text-center.py-5');
if (placeholder) {
placeholder.remove();
}
chatContainer.appendChild(chatMessage);
chatContainer.scrollTop = chatContainer.scrollHeight;
const responseText = document.getElementById('chatResponseText');
const agentBadge = document.getElementById('chatAgentBadge');
function processChunk() {
reader.read().then(({ done, value }) => {
if (done) {
// Stream beendet
sendBtn.disabled = false;
sendBtnText.classList.remove('d-none');
sendBtnSpinner.classList.add('d-none');
chatStatus.innerHTML = '<span style="color:var(--success);">✓ Antwort erhalten</span>';
setTimeout(() => { chatStatus.innerHTML = ''; }, 3000);
// Formular zurücksetzen
document.getElementById('prompt').value = '';
// Nach kurzer Verzögerung Seite neu laden für aktualisierte History
setTimeout(() => { location.reload(); }, 1000);
return;
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop();
lines.forEach(line => {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.substring(6));
if (data.type === 'agent_selected') {
currentAgent = data.agent;
agentBadge.textContent = currentAgent;
} else if (data.type === 'processing') {
chatStatus.innerHTML = `<span style="color:var(--info);">${data.message}</span>`;
} else if (data.type === 'response') {
currentResponse = data.text;
responseText.textContent = currentResponse;
chatContainer.scrollTop = chatContainer.scrollHeight;
} else if (data.type === 'complete') {
chatStatus.innerHTML = `<span style="color:var(--success);">${data.message}</span>`;
// In Session speichern
fetch('/chat/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
timestamp: data.timestamp,
agent: currentAgent,
agent_key: agent,
prompt: prompt,
response: data.response
})
});
} else if (data.type === 'error') {
responseText.innerHTML = `<span style="color:var(--danger);">⚠️ Fehler: ${escapeHtml(data.message)}</span>`;
chatStatus.innerHTML = `<span style="color:var(--danger);">Fehler aufgetreten</span>`;
sendBtn.disabled = false;
sendBtnText.classList.remove('d-none');
sendBtnSpinner.classList.add('d-none');
}
} catch (e) {
console.error('Parse error:', e, line);
}
}
});
processChunk();
});
}
processChunk();
})
.catch(error => {
chatStatus.innerHTML = `<span style="color:var(--danger);">⚠️ ${error.message}</span>`;
sendBtn.disabled = false;
sendBtnText.classList.remove('d-none');
sendBtnSpinner.classList.add('d-none');
});
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
{% endblock %}

View file

@ -42,6 +42,10 @@
<span style="font-size:.85rem;color:var(--text-secondary);">📄 Projektdokumente</span>
<span class="badge bg-secondary">{{ project_files|length }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center" style="padding:.7rem 1rem;">
<span style="font-size:.85rem;color:var(--text-secondary);">🤖 Agenten-Dateien</span>
<span class="badge bg-secondary">{{ agent_work_folders|length }}</span>
</li>
</ul>
</div>
</div>
@ -116,7 +120,7 @@
</div>
<!-- Projektdokumente -->
<div class="card">
<div class="card mb-3">
<div class="card-header bg-secondary d-flex justify-content-between align-items-center">
<span>📄 Projektdokumente <small style="font-weight:400;font-size:.72rem;color:var(--text-muted);margin-left:.4rem;">Arbeitsverzeichnis</small></span>
<span class="badge bg-secondary">{{ project_files|length }}</span>
@ -144,6 +148,32 @@
</div>
</div>
<!-- Agent Work Folders -->
{% if agent_work_folders %}
{% for agent_key, files in agent_work_folders.items() %}
<div class="card mb-3">
<div class="card-header bg-success d-flex justify-content-between align-items-center">
<span>🤖 {{ agent_key.replace('_', ' ').title() }} <small style="font-weight:400;font-size:.72rem;color:var(--text-muted);margin-left:.4rem;">agents/{{ agent_key }}/work/</small></span>
<span class="badge bg-secondary">{{ files|length }}</span>
</div>
<div class="card-body">
{% for file in files %}
<div class="file-item">
<span class="file-icon">{{ '📋' if file.name.endswith('.docx') else '📝' if file.name.endswith('.md') else '📊' if file.name.endswith(('.json', '.csv')) else '📄' }}</span>
<div style="flex:1;min-width:0;">
<div class="file-name">{{ file.name }}</div>
<div class="file-meta">{{ (file.size / 1024)|round(1) }} KB · {{ file.modified[:10] }}</div>
</div>
<div class="file-actions">
<a href="/files/agent/{{ agent_key }}/{{ file.name }}" class="btn btn-sm btn-secondary" title="Herunterladen"></a>
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
{% endif %}
</div>
</div>

View file

@ -200,14 +200,18 @@ function distributeTodos() {
})
.then(r => r.json())
.then(data => {
if (data.success) {
status.textContent = '✓ ' + data.message + ' - ' + data.results.length + ' Tasks gestartet';
console.log('Response:', data);
if (data && data.success) {
status.textContent = '✓ ' + data.message;
status.className = 'form-text mt-2 text-success';
if (data.results && data.results.length > 0) {
if (data.tasks && data.tasks.length > 0) {
location.reload();
}
} else if (data) {
status.textContent = 'Fehler: ' + (data.error || 'Unbekannter Fehler');
status.className = 'form-text mt-2 text-danger';
} else {
status.textContent = 'Fehler: ' + data.error;
status.textContent = 'Fehler: Keine Antwort vom Server';
status.className = 'form-text mt-2 text-danger';
}
})