184 lines
6.5 KiB
Python
184 lines
6.5 KiB
Python
|
|
"""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")
|
||
|
|
|
||
|
|
# User-Message speichern
|
||
|
|
await add_message(db, conversation_id, "user", user_message)
|
||
|
|
|
||
|
|
# History laden (letzte 20 Messages)
|
||
|
|
history = await get_messages(db, conversation_id, limit=20)
|
||
|
|
messages: List[dict] = [{"role": "system", "content": agent["system_prompt"]}]
|
||
|
|
for row in history[:-1]: # ohne die gerade gespeicherte user-Message
|
||
|
|
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)
|
||
|
|
|
||
|
|
# 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,
|
||
|
|
}
|