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
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
"""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,
|
||||
}
|
||||
Reference in New Issue
Block a user