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

550
app.py
View file

@ -20,6 +20,189 @@ load_dotenv()
# ── Agent Konfiguration ───────────────────────────────────────────────────────
AGENT_CONFIG_FILE = os.path.join(os.path.dirname(__file__), 'agent_config.json')
AGENTS_BASE_DIR = os.path.join(os.path.dirname(__file__), 'agents')
# Cache für verfügbare Modelle
_available_models_cache = None
_models_cache_time = None
MODELS_CACHE_TTL = 3600 # Cache für 1 Stunde
# ── Agent Memory System ────────────────────────────────────────────────────────
def ensure_agent_structure(agent_key):
"""Stellt sicher, dass die Ordnerstruktur für einen Agenten existiert."""
agent_dir = os.path.join(AGENTS_BASE_DIR, agent_key)
work_dir = os.path.join(agent_dir, 'work')
memory_dir = os.path.join(agent_dir, 'memory')
os.makedirs(agent_dir, exist_ok=True)
os.makedirs(work_dir, exist_ok=True)
os.makedirs(memory_dir, exist_ok=True)
return {
'agent_dir': agent_dir,
'work_dir': work_dir,
'memory_dir': memory_dir
}
def get_agent_memory(agent_key, memory_type='tasks'):
"""Lädt Erinnerungen eines Agenten aus JSON-Datei.
memory_type kann sein:
- 'tasks': Erledigte Tasks
- 'notes': Notizen
- 'conversations': Konversationen
- 'research': Research-Ergebnisse
"""
dirs = ensure_agent_structure(agent_key)
memory_file = os.path.join(dirs['memory_dir'], f'{memory_type}.json')
if os.path.exists(memory_file):
try:
with open(memory_file, 'r', encoding='utf-8') as f:
return json.load(f)
except:
pass
return []
def add_agent_memory(agent_key, memory_type, entry):
"""Fügt eine Erinnerung hinzu.
entry sollte ein dict sein mit mindestens:
- timestamp: ISO-Format
- title: Kurze Beschreibung
- content: Detaillierter Inhalt
- metadata: Zusätzliche Infos (optional)
"""
dirs = ensure_agent_structure(agent_key)
memory_file = os.path.join(dirs['memory_dir'], f'{memory_type}.json')
memories = get_agent_memory(agent_key, memory_type)
# Timestamp hinzufügen wenn nicht vorhanden
if 'timestamp' not in entry:
entry['timestamp'] = datetime.now().isoformat()
# ID hinzufügen
entry['id'] = len(memories) + 1
memories.append(entry)
# Nur die letzten 100 Einträge behalten
memories = memories[-100:]
with open(memory_file, 'w', encoding='utf-8') as f:
json.dump(memories, f, indent=2, ensure_ascii=False)
return entry
def get_agent_work_files(agent_key):
"""Gibt alle Dateien im work-Ordner eines Agenten zurück."""
dirs = ensure_agent_structure(agent_key)
work_dir = dirs['work_dir']
files = []
if os.path.exists(work_dir):
for filename in os.listdir(work_dir):
filepath = os.path.join(work_dir, filename)
if os.path.isfile(filepath):
stat = os.stat(filepath)
files.append({
'name': filename,
'size': stat.st_size,
'modified': datetime.fromtimestamp(stat.st_mtime).isoformat(),
'path': filepath
})
return sorted(files, key=lambda x: x['modified'], reverse=True)
def get_agent_memory_summary(agent_key):
"""Generiert eine Zusammenfassung aller Erinnerungen für den Systemprompt."""
tasks = get_agent_memory(agent_key, 'tasks')
notes = get_agent_memory(agent_key, 'notes')
summary = []
if tasks:
summary.append("## Letzte Tasks (letzte 5)")
for task in tasks[-5:]:
summary.append(f"- [{task.get('timestamp', 'N/A')}] {task.get('title', 'Unbekannt')}")
if task.get('result'):
summary.append(f" Ergebnis: {task.get('result', '')[:200]}")
if notes:
summary.append("\n## Wichtige Notizen")
for note in notes[-5:]:
summary.append(f"- {note.get('content', '')[:100]}")
return '\n'.join(summary) if summary else "Keine Erinnerungen vorhanden."
def get_available_models(force_refresh=False):
"""Lädt die verfügbaren KI-Modelle dynamisch von opencode."""
global _available_models_cache, _models_cache_time
# Cache prüfen
if not force_refresh and _available_models_cache is not None and _models_cache_time is not None:
if (time.time() - _models_cache_time) < MODELS_CACHE_TTL:
return _available_models_cache
try:
# opencode models ausführen
result = subprocess.run(
['opencode', 'models'],
capture_output=True,
text=True,
timeout=10
)
if result.returncode == 0:
models = []
for line in result.stdout.strip().split('\n'):
line = line.strip()
if line:
models.append(line)
# Nach Anbieter gruppieren
grouped = {}
for model in models:
if '/' in model:
provider, name = model.split('/', 1)
if provider not in grouped:
grouped[provider] = []
grouped[provider].append(model)
_available_models_cache = {
'models': models,
'grouped': grouped,
'count': len(models)
}
_models_cache_time = time.time()
return _available_models_cache
except Exception as e:
logging.warning(f"[ModelLoader] Fehler beim Laden der Modelle: {e}")
# Fallback auf hardcodierte Modelle wenn Laden fehlschlägt
fallback = {
'models': [
'opencode/big-pickle',
'opencode/gpt-5-nano',
'opencode/glm-5-free',
'opencode/minimax-m2.5-free',
'opencode/trinity-large-preview-free'
],
'grouped': {
'opencode': [
'opencode/big-pickle',
'opencode/gpt-5-nano',
'opencode/glm-5-free',
'opencode/minimax-m2.5-free',
'opencode/trinity-large-preview-free'
]
},
'count': 5
}
_available_models_cache = fallback
_models_cache_time = time.time()
return fallback
def get_agent_config():
"""Lädt die Agentenkonfiguration aus der JSON-Datei."""
@ -173,12 +356,27 @@ task_queue_lock = threading.Lock()
def get_agent_prompt(agent_key):
"""Liest den System-Prompt eines Agenten aus der Datei."""
prompt_file = os.path.join(os.path.dirname(__file__), 'agents', agent_key, 'systemprompt.md')
"""Liest den System-Prompt und Persönlichkeit eines Agenten aus den Dateien."""
agent_dir = os.path.join(os.path.dirname(__file__), 'agents', agent_key)
prompt_file = os.path.join(agent_dir, 'systemprompt.md')
personality_file = os.path.join(agent_dir, 'personality.md')
prompt_content = ""
personality_content = ""
if os.path.exists(prompt_file):
with open(prompt_file, 'r', encoding='utf-8') as f:
return f.read()
return ""
prompt_content = f.read()
if os.path.exists(personality_file):
with open(personality_file, 'r', encoding='utf-8') as f:
personality_content = f.read().strip()
# Persönlichkeit vor dem System-Prompt einfügen, falls vorhanden
if personality_content:
return f"{personality_content}\n\n---\n\n{prompt_content}"
return prompt_content
def execute_agent_task(agent_key, user_prompt, extra_context=""):
@ -191,21 +389,34 @@ def execute_agent_task(agent_key, user_prompt, extra_context=""):
if not system_prompt:
return f"⚠️ Kein System-Prompt für Agent '{agent_key}' gefunden."
# Agent-Struktur sicherstellen
dirs = ensure_agent_structure(agent_key)
work_dir = dirs['work_dir']
# Memory-Zusammenfassung laden
memory_summary = get_agent_memory_summary(agent_key)
kb_file = os.path.join(os.path.dirname(__file__), 'diversityball_knowledge.md')
kb_content = ""
if os.path.exists(kb_file):
with open(kb_file, 'r', encoding='utf-8') as f:
kb_content = f.read()
# System-Prompt = Agent-Rolle + Wissensdatenbank
# System-Prompt = Agent-Rolle + Wissensdatenbank + Memory
full_system = f"""{system_prompt}
## Wissensdatenbank (Diversity-Ball):
{kb_content}
## Deine Erinnerungen:
{memory_summary}
## Wichtig:
- Du hast Zugriff auf das Internet via WebFetch-Tool - nutze es aktiv!
- Du kannst Emails versenden - nutze send_email wenn beauftragt
- Dein Arbeitsverzeichnis: {work_dir}
- Speichere ALLE erstellten Dateien in diesem Verzeichnis!
- Verwende absolute Pfade für Dateien: {work_dir}/dateiname.ext
- Liefere immer eine vollständige, direkt verwertbare Antwort
{extra_context}"""
@ -222,7 +433,7 @@ def execute_agent_task(agent_key, user_prompt, extra_context=""):
capture_output=True,
text=True,
timeout=300,
cwd=os.path.dirname(__file__)
cwd=work_dir # Agent arbeitet in seinem work-Verzeichnis
)
if result.returncode == 0:
@ -238,7 +449,6 @@ def execute_agent_task(agent_key, user_prompt, extra_context=""):
return response_text if response_text else "⚠️ Keine Antwort erhalten."
else:
return f"⚠️ Fehler: {result.stderr}"
return f"⚠️ Fehler: {result.stderr}"
except FileNotFoundError:
return "⚠️ OpenCode CLI nicht gefunden. Bitte installiere opencode."
except subprocess.TimeoutExpired:
@ -820,8 +1030,122 @@ def start_email_poller():
logger.info("[TaskWorker] Daemon-Thread gestartet.")
def process_beat_tasks():
"""Background-Beat: Verarbeitet offene Tasks automatisch."""
logger.info("[TaskBeat] Hintergrund-Thread gestartet.")
while True:
try:
pending_tasks = [t for t in tasks if t.get('status') == 'pending' and t.get('type') in ('agent_created', 'manual', 'orchestrated')]
for task in pending_tasks:
agent_key = task.get('agent_key') or task.get('assigned_agent', '')
if task.get('agent_key') == 'orchestrator':
task['status'] = 'in_progress'
logger.info("[TaskBeat] Planungsphase für Task #%d", task['id'])
sub_tasks = task.get('sub_tasks', [])
available_agents = task.get('available_agents', list(AGENTS.keys()))
prompt = f"""Du bist der Master-Orchestrator. Analysiere folgende Tasks und weise sie den richtigen Agenten zu:
Tasks:
{chr(10).join(['- ' + t for t in sub_tasks])}
Verfügbare Agenten: {', '.join(available_agents)}
Agent-Beschreibungen:
"""
for a_key, a_info in AGENTS.items():
prompt += f"- {a_key}: {a_info.get('description', 'Keine Beschreibung')[:100]}\n"
prompt += "\nAntworte in diesem Format (einen Agent pro Task):\n"
for i, t in enumerate(sub_tasks):
prompt += f"Task {i+1}: [Agent-Key] - Kurze Begründung\n"
response = execute_agent_task('orchestrator', prompt)
task['plan_response'] = response
import re
agent_assignments = re.findall(r'Task \d+: (\w+)', response)
created_sub_tasks = []
for i, t in enumerate(sub_tasks):
assigned = agent_assignments[i] if i < len(agent_assignments) else available_agents[i % len(available_agents)]
if assigned not in AGENTS:
assigned = available_agents[0]
sub_task = {
'id': len(tasks) + 1 + len(created_sub_tasks),
'title': t[:80],
'description': f"Von Orchestrator zugewiesen: {response[:200]}...",
'assigned_agent': AGENTS.get(assigned, {}).get('name', assigned),
'agent_key': assigned,
'status': 'pending',
'created': datetime.now().strftime('%Y-%m-%d %H:%M'),
'type': 'orchestrated',
'parent_task': task['id']
}
tasks.append(sub_task)
created_sub_tasks.append(sub_task['id'])
logger.info("[TaskBeat] Sub-Task #%d zugewiesen an %s", sub_task['id'], assigned)
task['status'] = 'completed'
task['sub_task_ids'] = created_sub_tasks
logger.info("[TaskBeat] Planungs-Task #%d abgeschlossen. %d Sub-Tasks erstellt.", task['id'], len(created_sub_tasks))
continue
if not agent_key:
available_agents = list(AGENTS.keys())
if available_agents:
agent_key = available_agents[0]
task['agent_key'] = agent_key
if agent_key and agent_key in AGENTS:
task['status'] = 'in_progress'
logger.info("[TaskBeat] Verarbeite Task #%d Agent: %s", task['id'], agent_key)
response = execute_agent_task(agent_key, task.get('title', '') + '\n\n' + task.get('description', ''))
task['response'] = response
# Neues Memory-System: Task als strukturierte Erinnerung speichern
try:
add_agent_memory(agent_key, 'tasks', {
'task_id': task['id'],
'title': task.get('title', 'Unbekannt'),
'description': task.get('description', ''),
'result': response,
'status': 'completed',
'metadata': {
'assigned_by': task.get('agent', 'system'),
'duration': None
}
})
except Exception as e:
logger.warning("[TaskBeat] Konnte Erinnerung nicht speichern: %s", str(e))
task['status'] = 'completed'
logger.info("[TaskBeat] Task #%d abgeschlossen.", task['id'])
except Exception as e:
logger.error("[TaskBeat] Fehler: %s", str(e))
time.sleep(30)
def start_task_beat():
"""Startet den Task-Beat als Daemon-Thread."""
beat_thread = threading.Thread(target=process_beat_tasks, name='TaskBeat', daemon=True)
beat_thread.start()
logger.info("[TaskBeat] Daemon-Thread gestartet.")
# Poller beim App-Start starten
start_email_poller()
start_task_beat()
@app.route('/')
@ -831,24 +1155,74 @@ def index():
@app.route('/chat', methods=['GET', 'POST'])
def chat():
if request.method == 'POST':
prompt = request.form.get('prompt', '').strip()
agent = request.form.get('agent', '')
if prompt and agent:
response = f"Anfrage an {AGENTS.get(agent, {}).get('name', agent)}: {prompt}"
chat_history.append({
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M'),
'agent': AGENTS.get(agent, {}).get('name', agent),
'prompt': prompt,
'response': response
})
session['chat_history'] = chat_history[-20:]
flash('Anfrage gesendet!', 'success')
# Chat-Verlauf aus Session laden
if 'chat_history' not in session:
session['chat_history'] = []
chat_display = session.get('chat_history', [])
return render_template('chat.html', agents=AGENTS, chat_history=chat_display)
@app.route('/chat/send', methods=['POST'])
def chat_send():
"""Führt einen Agent aus und gibt die Antwort per Server-Sent Events zurück."""
data = request.get_json()
prompt = data.get('prompt', '').strip()
agent_key = data.get('agent', '').strip()
# Validierung vor dem Generator
if not prompt or not agent_key:
return jsonify({'type': 'error', 'message': 'Fehlende Eingabe'}), 400
if agent_key not in AGENTS:
return jsonify({'type': 'error', 'message': 'Agent nicht gefunden'}), 404
agent_info = AGENTS.get(agent_key, {})
agent_name = agent_info.get('name', agent_key)
def generate():
# Agent-Info senden
yield f"data: {json.dumps({'type': 'agent_selected', 'agent': agent_name, 'agent_key': agent_key})}\n\n"
yield f"data: {json.dumps({'type': 'processing', 'message': f'{agent_name} arbeitet...'})}\n\n"
try:
# Agent ausführen (mit Memory und Work-Dir)
response = execute_agent_task(agent_key, prompt)
# Antwort streamen
yield f"data: {json.dumps({'type': 'response', 'text': response})}\n\n"
# Erfolg melden
yield f"data: {json.dumps({'type': 'complete', 'message': '✓ Fertig', 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M'), 'response': response})}\n\n"
except Exception as e:
logger.error(f"[Chat] Fehler beim Ausführen von {agent_key}: {str(e)}")
yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n"
return Response(generate(), mimetype='text/event-stream',
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
@app.route('/chat/save', methods=['POST'])
def chat_save():
"""Speichert eine Chat-Nachricht in der Session."""
data = request.get_json()
if 'chat_history' not in session:
session['chat_history'] = []
session['chat_history'].append({
'timestamp': data.get('timestamp'),
'agent': data.get('agent'),
'agent_key': data.get('agent_key'),
'prompt': data.get('prompt'),
'response': data.get('response')
})
session['chat_history'] = session['chat_history'][-30:]
session.modified = True
return jsonify({'success': True})
@app.route('/tasks', methods=['GET', 'POST'])
def task_list():
if request.method == 'POST':
@ -1065,7 +1439,18 @@ def agents():
flash(f'Daten für "{agent_name}" gespeichert!', 'success')
return redirect(url_for('agents', edit=agent_name))
return render_template('agents.html', agents=AGENTS, agents_list=agents_list, edit_agent=edit_agent, edit_prompt=edit_prompt, edit_reminders=edit_reminders, edit_personality=edit_personality, edit_model=edit_model)
# Verfügbare Modelle laden
available_models = get_available_models()
return render_template('agents.html',
agents=AGENTS,
agents_list=agents_list,
edit_agent=edit_agent,
edit_prompt=edit_prompt,
edit_reminders=edit_reminders,
edit_personality=edit_personality,
edit_model=edit_model,
available_models=available_models)
@app.route('/files', methods=['GET', 'POST'])
@ -1085,8 +1470,19 @@ def files():
file_list = get_uploaded_files()
email_files = get_email_folder_files()
project_files = get_project_files()
return render_template('files.html', files=file_list,
email_files=email_files, project_files=project_files)
# Agent Work Folders sammeln
agent_work_folders = {}
for agent_key in AGENTS.keys():
work_files = get_agent_work_files(agent_key)
if work_files: # Nur Agenten mit Dateien anzeigen
agent_work_folders[agent_key] = work_files
return render_template('files.html',
files=file_list,
email_files=email_files,
project_files=project_files,
agent_work_folders=agent_work_folders)
@app.route('/files/delete/<filename>')
@ -1104,6 +1500,26 @@ def download_file(filename):
return send_from_directory(app.config['UPLOAD_FOLDER'], filename, as_attachment=False)
@app.route('/files/agent/<agent_key>/<filename>')
def download_agent_file(agent_key, filename):
"""Liefert eine Datei aus dem Work-Ordner eines Agenten."""
if agent_key not in AGENTS:
return jsonify({'error': 'Agent nicht gefunden'}), 404
dirs = ensure_agent_structure(agent_key)
work_dir = dirs['work_dir']
filepath = os.path.join(work_dir, filename)
# Security: Stelle sicher, dass die Datei im work_dir ist
if not os.path.abspath(filepath).startswith(os.path.abspath(work_dir)):
return jsonify({'error': 'Zugriff verweigert'}), 403
if not os.path.isfile(filepath):
return jsonify({'error': 'Datei nicht gefunden'}), 404
return send_from_directory(work_dir, filename, as_attachment=False)
@app.route('/files/email/view/<filename>')
def view_email_file(filename):
"""Gibt Inhalt einer Email-Vorlage als JSON oder direkten Text zurück."""
@ -1326,6 +1742,14 @@ def update_task_api(task_id):
return jsonify({'error': 'Task nicht gefunden'}), 404
@app.route('/api/models', methods=['GET'])
def get_models():
"""Gibt die Liste der verfügbaren KI-Modelle zurück."""
force_refresh = request.args.get('refresh', 'false').lower() == 'true'
models_data = get_available_models(force_refresh=force_refresh)
return jsonify(models_data)
@app.route('/api/agent/<agent_name>/model', methods=['POST'])
def set_agent_model(agent_name):
"""Setzt das Modell für einen Agenten."""
@ -1389,59 +1813,36 @@ def agent_reminders(agent_name):
@app.route('/api/orchestrator-distribute', methods=['POST'])
def distribute_tasks():
"""Verteilt Tasks parallel an mehrere Agenten."""
"""Erstellt einen Planungs-Task für den Orchestrator - dieser weist dann die richtigen Agenten zu."""
data = request.get_json()
tasks_list = data.get('tasks', [])
selected_agents = data.get('agents', [])
if not tasks_list:
return jsonify({'error': 'Keine Tasks übergeben'}), 400
if not selected_agents:
return jsonify({'error': 'Keine Agenten ausgewählt'}), 400
results = []
tasks_text = '\n'.join([f"- {t}" for t in tasks_list])
agents_text = ', '.join(selected_agents) if selected_agents else 'alle verfügbaren Agenten'
def run_task_for_agent(agent_key, task):
response = execute_agent_task(agent_key, task)
reminders_file = os.path.join(os.path.dirname(__file__), 'agents', agent_key, 'reminders.md')
if os.path.exists(reminders_file):
try:
with open(reminders_file, 'r', encoding='utf-8') as f:
content = f.read()
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M')
new_entry = f"\n- {timestamp}: {task[:100]}... - erledigt"
if '## Letzte Aktionen' in content:
content = content.replace('## Letzte Aktionen', f'## Letzte Aktionen{new_entry}')
else:
content += new_entry
with open(reminders_file, 'w', encoding='utf-8') as f:
f.write(content)
except:
pass
return {
'agent': agent_key,
'task': task,
'response': response[:200] + '...' if len(response) > 200 else response
}
planning_task = {
'id': len(tasks) + 1,
'title': f"Planungsphase: {tasks_list[0][:50]}{'...' if len(tasks_list[0]) > 50 else ''}",
'description': f"Tasks:\n{tasks_text}\n\nVerfügbare Agenten: {agents_text}\n\nDer Orchestrator soll diese Tasks analysieren und den richtigen Agenten zuweisen.",
'assigned_agent': 'Orchestrator',
'agent_key': 'orchestrator',
'status': 'pending',
'created': datetime.now().strftime('%Y-%m-%d %H:%M'),
'type': 'orchestrated',
'sub_tasks': tasks_list,
'available_agents': selected_agents
}
tasks.append(planning_task)
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor(max_workers=len(selected_agents)) as executor:
futures = []
for i, task in enumerate(tasks_list):
agent = selected_agents[i % len(selected_agents)]
future = executor.submit(run_task_for_agent, agent, task)
futures.append(future)
for future in concurrent.futures.as_completed(futures):
try:
results.append(future.result())
except Exception as e:
results.append({'error': str(e)})
return jsonify({
'success': True,
'message': f'Planungs-Task erstellt. Der Orchestrator wird die richtigen Agenten zuweisen.',
'tasks': [planning_task['id']]
})
for i, task_text in enumerate(tasks_list):
agent_key = selected_agents[i % len(selected_agents)]
@ -1453,17 +1854,24 @@ def distribute_tasks():
'title': task_text[:80],
'description': 'Automatisch erstellt durch Orchestrator',
'assigned_agent': agent_name,
'status': 'completed',
'status': 'in_progress',
'created': datetime.now().strftime('%Y-%m-%d %H:%M'),
'type': 'orchestrated',
'agent_key': agent_key
}
tasks.append(new_task)
created_tasks.append(new_task['id'])
executor = concurrent.futures.ThreadPoolExecutor(max_workers=len(selected_agents))
for i, task_text in enumerate(tasks_list):
agent_key = selected_agents[i % len(selected_agents)]
task_id = created_tasks[i]
executor.submit(run_task_async, agent_key, task_text, task_id)
return jsonify({
'success': True,
'message': 'Tasks verteilt',
'results': results
'message': f'{len(created_tasks)} Tasks erstellt und werden im Hintergrund ausgeführt',
'tasks': created_tasks
})