"""Agent-Runner: führt einen Agent-Turn aus. Agents leben in der DB (Tabelle `agents`). Beim Start werden Code-Agents aus agents/*.py in DB gesynct (source='code'). """ import json import time import logging from typing import Dict, Any, List import aiosqlite from db import get_agent, add_message, get_messages, touch_conversation from llm import chat from mcp_client import get_mcp_client logger = logging.getLogger("agent") async def sync_code_agents(db: aiosqlite.Connection): """Lädt Code-Agents aus agents/*.py und synct sie in die DB.""" from agents import discover_agents for instance in discover_agents().values(): cfg = instance.config existing = await get_agent(db, cfg.id) if existing: # Update nur Code-Felder, behalte User-Overrides (enabled, custom) merged = { "id": cfg.id, "name": cfg.name, "description": cfg.description, "system_prompt": cfg.system_prompt, "allowed_tools": cfg.allowed_tools, "model": cfg.model, "temperature": cfg.temperature, "max_tokens": cfg.max_tokens, "enabled": existing.get("enabled", cfg.enabled), } await db.execute( """UPDATE agents SET name=?, description=?, system_prompt=?, allowed_tools=?, model=?, temperature=?, max_tokens=?, updated_at=CURRENT_TIMESTAMP WHERE id=?""", ( merged["name"], merged["description"], merged["system_prompt"], json.dumps(merged["allowed_tools"]), merged["model"], merged["temperature"], merged["max_tokens"], cfg.id, ) ) else: await db.execute( """INSERT INTO agents (id, name, description, system_prompt, allowed_tools, model, temperature, max_tokens, enabled, source) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'code')""", ( cfg.id, cfg.name, cfg.description, cfg.system_prompt, json.dumps(cfg.allowed_tools), cfg.model, cfg.temperature, cfg.max_tokens, int(cfg.enabled), ) ) await db.commit() async def run_agent_turn( db: aiosqlite.Connection, agent_id: str, user_id: str, user_message: str, conversation_id: str, ) -> Dict[str, Any]: """Führt einen Agent-Turn aus: User-Message → LLM → optional Tool-Calls → Antwort.""" agent = await get_agent(db, agent_id) if not agent: raise ValueError(f"Agent '{agent_id}' nicht gefunden") if not agent["enabled"]: raise ValueError(f"Agent '{agent_id}' ist deaktiviert") # History laden (letzte 20 Messages) — VOR dem Speichern der neuen User-Message, # damit die neue Turn-User-Message nicht versehentlich aus dem Slice fällt. history = await get_messages(db, conversation_id, limit=20) # User-Message speichern await add_message(db, conversation_id, "user", user_message) messages: List[dict] = [{"role": "system", "content": agent["system_prompt"]}] for row in history: role, content, tool_calls_json, _, _ = row if role == "tool": continue msg = {"role": role, "content": content or ""} if tool_calls_json: try: msg["tool_calls"] = json.loads(tool_calls_json) except Exception: pass messages.append(msg) # Aktuelle User-Message explizit anhängen, damit das LLM den neuen Turn sieht. messages.append({"role": "user", "content": user_message}) # MCP-Tools laden (gefiltert nach allowed_tools) mcp_client = get_mcp_client() tools = [] try: all_tools = await mcp_client.get_tools_for_llm() if agent["allowed_tools"]: tools = [t for t in all_tools if t.get("function", {}).get("name") in agent["allowed_tools"]] else: tools = all_tools except Exception as e: logger.warning(f"MCP-Tools konnten nicht geladen werden: {e}") # LLM-Loop MAX_ITER = 10 iterations = 0 total_input = 0 total_output = 0 final_content = "" while iterations < MAX_ITER: iterations += 1 kwargs = {} if agent["model"]: kwargs["model"] = agent["model"] start = time.time() response = await chat( messages=messages, tools=tools or None, temperature=agent["temperature"], max_tokens=agent["max_tokens"], **kwargs, ) duration_ms = int((time.time() - start) * 1000) total_input += response.input_tokens total_output += response.output_tokens assistant_msg = {"role": "assistant", "content": response.content} if response.tool_calls: assistant_msg["tool_calls"] = [ { "id": tc["id"], "type": "function", "function": {"name": tc["name"], "arguments": tc["arguments"]}, } for tc in response.tool_calls ] messages.append(assistant_msg) if not response.tool_calls: final_content = response.content break for tc in response.tool_calls: if agent["allowed_tools"] and tc["name"] not in agent["allowed_tools"]: messages.append({ "role": "tool", "tool_call_id": tc["id"], "content": f"Tool '{tc['name']}' ist nicht erlaubt", }) continue try: args = json.loads(tc["arguments"]) if isinstance(tc["arguments"], str) else tc["arguments"] except Exception: args = {} try: result = await mcp_client.call_tool(tc["name"], args) content_str = json.dumps(result, default=str)[:8000] except Exception as e: content_str = f"Tool-Fehler: {e}" messages.append({"role": "tool", "tool_call_id": tc["id"], "content": content_str}) # Assistant-Message speichern await add_message( db, conversation_id, "assistant", final_content, tool_calls=response.tool_calls if (response and response.tool_calls) else None, token_count=total_input + total_output, ) await touch_conversation(db, conversation_id) return { "content": final_content, "iterations": iterations, "input_tokens": total_input, "output_tokens": total_output, "duration_ms": duration_ms, }