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:
Leopold
2026-07-05 22:18:00 +02:00
commit 64b840c94c
30 changed files with 2690 additions and 0 deletions
@@ -0,0 +1,60 @@
{% extends "base.html" %}
{% block title %}{{ agent.name }} Agent Platform{% endblock %}
{% block content %}
<div class="page-header">
<h1>{{ agent.name }}</h1>
<p class="subtitle"><code>{{ agent.id }}</code> · {{ agent.description }}</p>
</div>
<section class="card">
<h2>Definition (aus Code)</h2>
<p><small>Diese Definition liegt in <code>agents/*.py</code> und ist read-only im UI. Änderungen am Code erfordern Server-Neustart.</small></p>
<div class="form-group">
<label>System-Prompt</label>
<textarea readonly rows="10">{{ agent.system_prompt }}</textarea>
</div>
<div class="form-group">
<label>Erlaubte Tools</label>
<input type="text" value="{{ agent.allowed_tools|join(', ') }}" readonly>
</div>
{% if agent.model %}
<div class="form-group">
<label>LLM-Override</label>
<input type="text" value="{{ agent.model }}" readonly>
</div>
{% endif %}
</section>
<section class="card">
<h2>Runtime-Settings</h2>
<form hx-put="/api/agents/{{ agent.id }}" hx-target="#settings-result" class="form">
<div class="form-group">
<label>
<input type="checkbox" name="enabled" {% if agent.enabled %}checked{% endif %} value="true">
Aktiv (Agent darf verwendet werden)
</label>
</div>
<div class="form-group">
<label>Custom-Settings (JSON, optional)</label>
<textarea name="custom_settings" rows="4" placeholder='{"temperature": 0.5, "max_tokens": 1000}'>{{ agent.custom_settings | tojson(indent=2) if agent.custom_settings else "{}" }}</textarea>
</div>
<button type="submit" class="btn btn-primary">Settings speichern</button>
<a href="/agents" class="btn">Zurück</a>
</form>
<div id="settings-result"></div>
</section>
<section class="card">
<h2>Chat starten</h2>
<form hx-post="/api/chat/{{ agent.id }}" hx-target="#chat-result" hx-swap="innerHTML" class="form">
<div class="form-group">
<label>Erste Nachricht</label>
<textarea name="message" rows="3" required placeholder="Hallo!"></textarea>
</div>
<button type="submit" class="btn btn-primary">Chat starten</button>
</form>
<div id="chat-result"></div>
</section>
{% endblock %}
+95
View File
@@ -0,0 +1,95 @@
{% extends "base.html" %}
{% block title %}{% if agent %}Agent bearbeiten{% else %}Neuer Agent{% endif %} Agent Platform{% endblock %}
{% block content %}
<div class="page-header">
<h1>{% if agent %}{{ agent.name }}{% else %}Neuer Agent{% endif %}</h1>
<a href="/agents" class="btn">Zurück</a>
</div>
<form {% if agent %}hx-put="/api/agents/{{ agent.id }}"{% else %}hx-post="/api/agents"{% endif %} hx-target="#result" hx-swap="innerHTML" class="card form">
<div class="form-group">
<label>ID (nur Kleinbuchstaben, Zahlen, _ und -)</label>
{% if agent %}
<input type="text" value="{{ agent.id }}" readonly>
{% else %}
<input type="text" name="id" required pattern="^[a-z0-9_-]+$" placeholder="mein_agent">
{% endif %}
</div>
<div class="form-group">
<label>Name (Anzeigename)</label>
<input type="text" name="name" required value="{{ agent.name if agent else '' }}">
</div>
<div class="form-group">
<label>Beschreibung</label>
<input type="text" name="description" value="{{ agent.description if agent else '' }}">
</div>
<div class="form-group">
<label>System-Prompt</label>
<textarea name="system_prompt" rows="6" required>{{ agent.system_prompt if agent else '' }}</textarea>
</div>
<div class="form-group">
<label>Erlaubte Tools (Mehrfachauswahl)</label>
<div class="checkbox-group">
{% set selected = agent.allowed_tools if agent else [] %}
{% if available_tools %}
{% for tool in available_tools %}
<label class="checkbox-label">
<input type="checkbox" name="allowed_tools" value="{{ tool.name }}" {% if tool.name in selected %}checked{% endif %}>
<code>{{ tool.name }}</code>
</label>
{% endfor %}
{% else %}
<input type="text" name="allowed_tools_csv" placeholder="tool1,tool2 (MCP nicht erreichbar)" value="{{ selected|join(',') }}">
{% endif %}
</div>
<small>Verfügbare Tools vom MCP-Server. Falls nicht erreichbar, manuell als CSV.</small>
</div>
<div class="form-row">
<div class="form-group" style="flex:1">
<label>LLM-Modell (leer = Default aus Config)</label>
<input type="text" name="model" placeholder="z.B. anthropic/claude-sonnet-4.5" value="{{ agent.model if agent and agent.model else '' }}">
</div>
<div class="form-group" style="width:120px">
<label>Temperature</label>
<input type="number" name="temperature" step="0.1" min="0" max="2" value="{{ agent.temperature if agent else 0.7 }}">
</div>
<div class="form-group" style="width:140px">
<label>Max Tokens</label>
<input type="number" name="max_tokens" min="100" max="32000" value="{{ agent.max_tokens if agent else 2000 }}">
</div>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" name="enabled" value="true" {% if not agent or agent.enabled %}checked{% endif %}>
Aktiv (Agent darf verwendet werden)
</label>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">{% if agent %}Speichern{% else %}Anlegen{% endif %}</button>
<a href="/agents" class="btn">Abbrechen</a>
</div>
<div id="result"></div>
</form>
{% if agent %}
<section class="card">
<h2>Chat testen</h2>
<form hx-post="/api/chat/{{ agent.id }}" hx-target="#chat-result" hx-swap="innerHTML" class="form">
<textarea name="message" rows="3" required placeholder="Hallo, was kannst du?"></textarea>
<button type="submit" class="btn">Senden</button>
</form>
<div id="chat-result"></div>
</section>
{% endif %}
{% endblock %}
+59
View File
@@ -0,0 +1,59 @@
{% extends "base.html" %}
{% block title %}Agents Agent Platform{% endblock %}
{% block content %}
<div class="page-header">
<h1>Agents</h1>
<a href="/agents/new" class="btn btn-primary">+ Neuer Agent</a>
</div>
<section class="card">
<p><small><strong>Hinweis:</strong> Code-Agents (definiert in <code>agents/*.py</code>) werden beim Start in die DB synchronisiert. Du kannst sie hier anpassen oder neue Agents rein in der DB anlegen.</small></p>
</section>
<section class="card">
<h2>{{ agents|length }} Agent(s)</h2>
{% if agents %}
<table class="table">
<thead>
<tr><th>ID</th><th>Name</th><th>Quelle</th><th>Tools</th><th>Status</th><th>Aktionen</th></tr>
</thead>
<tbody>
{% for agent in agents %}
<tr>
<td><code>{{ agent.id }}</code></td>
<td>{{ agent.name }}</td>
<td>
{% if agent.source == "code" %}
<span class="badge badge-blue">Code</span>
{% else %}
<span class="badge">DB</span>
{% endif %}
</td>
<td><small>{{ agent.allowed_tools|length }} Tool(s)</small></td>
<td>
{% if agent.enabled %}
<span class="badge badge-green">aktiv</span>
{% else %}
<span class="badge badge-red">inaktiv</span>
{% endif %}
</td>
<td>
<a href="/agents/{{ agent.id }}" class="btn btn-sm">Edit</a>
{% if agent.enabled %}
<button hx-post="/api/agents/{{ agent.id }}/disable" hx-swap="none" hx-on::after-request="location.reload()" class="btn btn-sm">Disable</button>
{% else %}
<button hx-post="/api/agents/{{ agent.id }}/enable" hx-swap="none" hx-on::after-request="location.reload()" class="btn btn-sm">Enable</button>
{% endif %}
<button hx-delete="/api/agents/{{ agent.id }}" hx-confirm="Wirklich löschen?" hx-swap="none" hx-on::after-request="location.reload()" class="btn btn-sm btn-danger">Delete</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="empty-state">Keine Agents vorhanden. <a href="/agents/new">Ersten Agent anlegen</a></p>
{% endif %}
</section>
{% endblock %}
+39
View File
@@ -0,0 +1,39 @@
{% extends "base.html" %}
{% block title %}Audit-Log Agent Platform{% endblock %}
{% block content %}
<div class="page-header">
<h1>Audit-Log</h1>
<p class="subtitle">{{ entries|length }} Einträge (neueste zuerst)</p>
</div>
<section class="card">
{% if entries %}
<table class="table">
<thead>
<tr>
<th>Zeitstempel</th>
<th>Agent</th>
<th>Aktion</th>
<th>Target</th>
<th>Dauer (ms)</th>
</tr>
</thead>
<tbody>
{% for entry in entries %}
<tr>
<td><small>{{ entry.timestamp }}</small></td>
<td><code>{{ entry.agent_id or '-' }}</code></td>
<td><span class="badge">{{ entry.action }}</span></td>
<td><code>{{ entry.target or '-' }}</code></td>
<td>{{ entry.duration_ms or '-' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="empty-state">Noch keine Audit-Einträge.</p>
{% endif %}
</section>
{% endblock %}
+30
View File
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Agent Platform{% endblock %}</title>
<link rel="stylesheet" href="/static/style.css">
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
</head>
<body>
<nav class="navbar">
<div class="nav-brand">⚡ Agent Platform</div>
<div class="nav-links">
<a href="/" class="nav-link">Dashboard</a>
<a href="/agents" class="nav-link">Agents</a>
<a href="/tools" class="nav-link">Tools</a>
<a href="/audit" class="nav-link">Audit</a>
</div>
<div class="nav-user">👤 {{ user_id }}</div>
</nav>
<main class="container">
{% block content %}{% endblock %}
</main>
<footer class="footer">
<small>Agent Platform v0.1.0 · LiteLLM · Pydantic AI · MCP</small>
</footer>
</body>
</html>
+36
View File
@@ -0,0 +1,36 @@
{% extends "base.html" %}
{% block title %}Chat Agent Platform{% endblock %}
{% block content %}
<div class="page-header">
<h1>Chat</h1>
<p class="subtitle">Konversation: <code>{{ conv_id }}</code></p>
</div>
<section class="card chat-container">
<div id="messages" class="messages">
{% for msg in messages %}
<div class="message message-{{ msg.role }}">
<div class="message-role">{{ msg.role }}</div>
<div class="message-content">{{ msg.content }}</div>
</div>
{% endfor %}
{% if not messages %}
<p class="empty-state">Noch keine Nachrichten.</p>
{% endif %}
</div>
</section>
<section class="card">
<form hx-post="" hx-target="#messages" hx-swap="beforeend" hx-on::after-request="this.reset()" class="form" id="chat-form">
<input type="hidden" name="conv_id" value="{{ conv_id }}">
<div class="form-row">
<div class="form-group" style="flex: 1;">
<textarea name="message" rows="2" required placeholder="Nachricht eingeben..." autofocus></textarea>
</div>
<button type="submit" class="btn btn-primary">Senden</button>
</div>
</form>
</section>
{% endblock %}
+69
View File
@@ -0,0 +1,69 @@
{% extends "base.html" %}
{% block title %}Dashboard Agent Platform{% endblock %}
{% block content %}
<div class="page-header">
<h1>Dashboard</h1>
<p class="subtitle">Übersicht aller Agents und recent Aktivitäten</p>
</div>
<section class="card">
<h2>Agents ({{ agents|length }})</h2>
{% if agents %}
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Beschreibung</th>
<th>Status</th>
<th>Aktion</th>
</tr>
</thead>
<tbody>
{% for agent in agents %}
<tr>
<td><code>{{ agent.id }}</code></td>
<td>{{ agent.name }}</td>
<td>{{ agent.description }}</td>
<td>
{% if agent.enabled %}
<span class="badge badge-green">aktiv</span>
{% else %}
<span class="badge badge-red">inaktiv</span>
{% endif %}
</td>
<td><a href="/agents/{{ agent.id }}" class="btn btn-sm">Details</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-state">
<p>Noch keine Agents angelegt.</p>
<a href="/agents" class="btn btn-primary">Ersten Agent erstellen</a>
</div>
{% endif %}
</section>
<section class="card">
<h2>Schnellstart</h2>
<form hx-post="/api/chat" hx-target="#chat-result" class="form">
<div class="form-group">
<label for="agent_id">Agent wählen</label>
<select name="agent_id" id="agent_id" required>
{% for agent in agents %}
<option value="{{ agent.id }}">{{ agent.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="message">Nachricht</label>
<textarea name="message" id="message" rows="3" required placeholder="Deine Nachricht an den Agent..."></textarea>
</div>
<button type="submit" class="btn btn-primary">Senden</button>
</form>
<div id="chat-result"></div>
</section>
{% endblock %}
+46
View File
@@ -0,0 +1,46 @@
{% extends "base.html" %}
{% block title %}MCP Tools Agent Platform{% endblock %}
{% block content %}
<div class="page-header">
<h1>MCP Tool-Server</h1>
<p class="subtitle">
Status:
{% if reachable %}
<span class="badge badge-green">erreichbar</span>
{% else %}
<span class="badge badge-red">nicht erreichbar</span>
{% if error %}<br><small>{{ error }}</small>{% endif %}
{% endif %}
</p>
</div>
<section class="card">
<h2>Verfügbare Tools ({{ tools|length }})</h2>
{% if tools %}
<div class="tools-grid">
{% for tool in tools %}
<div class="tool-card">
<h3><code>{{ tool.name }}</code></h3>
<p>{{ tool.description }}</p>
{% if tool.inputSchema %}
<details>
<summary>Schema</summary>
<pre><code>{{ tool.inputSchema | tojson(indent=2) }}</code></pre>
</details>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<p class="empty-state">
{% if reachable %}
MCP-Server erreichbar, aber keine Tools registriert.
{% else %}
MCP-Server nicht erreichbar. Prüfe die Konfiguration.
{% endif %}
</p>
{% endif %}
</section>
{% endblock %}