Files
agent-platform/agent_platform/agent.py
T
Leopold 64b840c94c feat: agent platform initial commit
- FastAPI + HTMX UI: dashboard, agents CRUD, chat, audit, tools
- SQLite schema: agents, conversations, messages, audit, sessions
- Code-agent sync (agents/*.py -> DB on startup)
- MCP client with health check
- Token auth (Bearer + session)
- LiteLLM integration via llm.py
- Docker Compose: agent-platform + mcp-tools services
- Smoke tests pass: /health 200, /api/agents 200, / 401
2026-07-05 22:18:00 +02:00

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,
}