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,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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user