Supervisor S-3/S-4: MCP-Light-Server, Profile, gespeicherte Berichte, Designer-UI

S-4 MCP-Light (Daten-Tuer fuer externe KI-Clients, KEIN Modell-Zugang - Modelle laufen
weiter ueber OpenRouter):
- McpJsonRpc (pur): JSON-RPC 2.0 fuer initialize/ping/tools/list/tools/call ueber die
  read-only Tool-Registry; Notifications/Fehlerfaelle spezifikationskonform.
- McpLightServer (HostedService): lokaler Streamable-HTTP-Endpoint. OPT-IN via
  POLYTRADER_MCP_PORT, bindet NUR 127.0.0.1. Claude Code:
  claude mcp add --transport http polytrader http://127.0.0.1:PORT/mcp
- End-to-End-Test ueber echtes HTTP (initialize, tools/call, Notification=202, GET=405).

S-3 Profile + Berichte:
- SupervisorProfiles: Allgemein/Technik/CopyTrading/ResolutionFarming als System-Prompt-
  Zusatz + Tool-Subset ueber EINER Agent-Infrastruktur (Technik z.B. ohne Strategie-Tools).
  Agent filtert Tools je Profil.
- sup_reports (SupervisorDbContext, Migration generiert UND angewendet): jede Analyse wird
  mit Profil/Modell/Frage/Antwort/Tool-Aufrufen/Token gespeichert -> Supervisor auditierbar.

UI (Richards Vorgabe: designerfaehig):
- SupervisorMainForm auf partial + .Designer.cs umgestellt - alle Controls im Designer
  (3 Tabs: Analyse mit Profil-Combo+Modellfeld, Dossiers, Berichte mit Split/Grid/Detail).

Tests: +16 (MCP-JSON-RPC 6, MCP-HTTP-E2E 1, Profile 2, bestehende erweitert). Build 0 Fehler,
360 Tests gruen, --smoke-ui alle 5 Views gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-18 11:00:28 +02:00
co-authored by Claude Opus 4.8
parent 5d732277b2
commit b7b2a141e3
16 changed files with 1369 additions and 111 deletions
@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -34,22 +35,36 @@ namespace PolyTrader.Modules.Supervisor.Agent
_tools = tools; _tools = tools;
} }
private static string SystemPrompt() => private static string SystemPrompt(SupervisorProfile profile)
{
string basePrompt =
"Du bist der Supervisor von PolyTrader: ein Analyse-Agent für automatisierten Polymarket-Handel. " + "Du bist der Supervisor von PolyTrader: ein Analyse-Agent für automatisierten Polymarket-Handel. " +
"Du bist strikt read-only du kannst und darfst nicht handeln. Nutze die Tools, um Entscheidungsjournal, " + "Du bist strikt read-only du kannst und darfst nicht handeln. Nutze die Tools, um Entscheidungsjournal, " +
"Order-Events, Trades und Logs abzufragen, BEVOR du Schlüsse ziehst. Zitiere konkrete Daten " + "Order-Events, Trades und Logs abzufragen, BEVOR du Schlüsse ziehst. Zitiere konkrete Daten " +
"(SignalIds, Zeiten, Preise, ReasonCodes). Antworte auf Deutsch, präzise und mit klarer Schlussfolgerung.\n\n" + "(SignalIds, Zeiten, Preise, ReasonCodes). Antworte auf Deutsch, präzise und mit klarer Schlussfolgerung.";
"=== ARCHITEKTUR-KONTEXT ===\n" + ArchitectureContext.Load(); if (!string.IsNullOrEmpty(profile.PromptAddendum))
basePrompt += "\n\n" + profile.PromptAddendum;
return basePrompt + "\n\n=== ARCHITEKTUR-KONTEXT ===\n" + ArchitectureContext.Load();
}
/// <summary>Tools des Profils (Subset oder alle).</summary>
private IReadOnlyList<SupervisorTool> ToolsFor(SupervisorProfile profile) =>
profile.ToolFilter == null
? _tools.Tools
: _tools.Tools.Where(t => Array.Exists(profile.ToolFilter, n =>
string.Equals(n, t.Name, StringComparison.OrdinalIgnoreCase))).ToList();
/// <summary> /// <summary>
/// Beantwortet eine Analyse-Frage. <paramref name="progress"/> meldet Tool-Aufrufe live an die UI. /// Beantwortet eine Analyse-Frage. <paramref name="progress"/> meldet Tool-Aufrufe live an die UI.
/// </summary> /// </summary>
public async Task<AgentResult> AskAsync(string question, string? model = null, public async Task<AgentResult> AskAsync(string question, string? model = null,
IProgress<string>? progress = null, CancellationToken ct = default) IProgress<string>? progress = null, SupervisorProfile? profile = null, CancellationToken ct = default)
{ {
var activeProfile = profile ?? SupervisorProfiles.Allgemein;
var activeTools = ToolsFor(activeProfile);
var messages = new List<ChatMessage> var messages = new List<ChatMessage>
{ {
ChatMessage.System(SystemPrompt()), ChatMessage.System(SystemPrompt(activeProfile)),
ChatMessage.User(question) ChatMessage.User(question)
}; };
var invocations = new List<(string, string, string)>(); var invocations = new List<(string, string, string)>();
@@ -59,7 +74,7 @@ namespace PolyTrader.Modules.Supervisor.Agent
for (int i = 0; i < MaxIterations; i++) for (int i = 0; i < MaxIterations; i++)
{ {
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
var response = await _chat.CompleteAsync(usedModel, messages, _tools.Tools, ct); var response = await _chat.CompleteAsync(usedModel, messages, activeTools, ct);
promptTokens += response.PromptTokens; promptTokens += response.PromptTokens;
completionTokens += response.CompletionTokens; completionTokens += response.CompletionTokens;
@@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace PolyTrader.Modules.Supervisor.Agent
{
/// <summary>
/// Ein Supervisor-Profil (S-3): Fokus-Anweisung + optionales Tool-Subset über EINER gemeinsamen
/// Agent-Infrastruktur (Konzept §4a) — bewusst KEINE Agent-zu-Agent-Orchestrierung.
/// </summary>
public sealed record SupervisorProfile(string Name, string PromptAddendum, string[]? ToolFilter)
{
public override string ToString() => Name;
}
/// <summary>Die eingebauten Profile. Modul-Wissen liegt im Architektur-Kontext; hier nur der Fokus.</summary>
public static class SupervisorProfiles
{
public static readonly SupervisorProfile Allgemein = new(
"Allgemein",
"",
null);
public static readonly SupervisorProfile Technik = new(
"Technik",
"FOKUS TECHNIK-SUPERVISOR: Du prüfst ausschließlich die technische Gesundheit — Fehler-/Warning-" +
"Muster in den Logs, fehlgeschlagene/stornierte Orders, CLOB-Fehlerantworten, auffällige " +
"Latenzen und Lücken in den Datenketten. KEINE Strategie-Bewertung (ob ein Trade klug war, " +
"ist nicht dein Thema — nur ob die Software korrekt funktioniert hat).",
new[] { "read_logs", "query_order_events", "query_decisions", "get_dossier", "get_architecture_context" });
public static readonly SupervisorProfile CopyTrading = new(
"CopyTrading",
"FOKUS COPYTRADING-SUPERVISOR: Du bewertest die CopyTrading-Strategie — Master-Qualität vs. " +
"Ausführungsqualität (Latenz/Preisdifferenz Signal→Fill), Verhalten der SELL-Eskalationsleiter, " +
"Reject-Muster (waren die Risk-Limits klug oder haben sie profitable Trades verhindert?). " +
"Filtere Daten auf ModuleName='CopyTrading'.",
null);
public static readonly SupervisorProfile ResolutionFarming = new(
"ResolutionFarming",
"FOKUS RF-SUPERVISOR: Du bewertest die ResolutionFarming-Strategie — Kalibrierung (realisierte " +
"Winrate je Einstiegspreisband vs. implizite Wahrscheinlichkeit), Cluster-Risiken, Scanner-" +
"Qualität. Filtere Daten auf ModuleName='ResolutionFarming'.",
null);
public static IReadOnlyList<SupervisorProfile> All { get; } =
new[] { Allgemein, Technik, CopyTrading, ResolutionFarming };
public static SupervisorProfile ByName(string? name) =>
All.FirstOrDefault(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase)) ?? Allgemein;
}
}
@@ -0,0 +1,149 @@
using System;
using System.IO;
using System.Text;
using System.Text.Json;
using PolyTrader.Modules.Supervisor.Agent;
namespace PolyTrader.Modules.Supervisor.Mcp
{
/// <summary>
/// MCP-Light (S-4): purer JSON-RPC-2.0-Handler für das Model Context Protocol über die
/// read-only <see cref="SupervisorToolRegistry"/>. Externe KI-Clients (z. B. Claude Code)
/// erhalten damit dieselben Analyse-Tools wie der In-App-Agent — **kein** Modell-Zugang,
/// nur die Daten-Tür. Unterstützt: initialize, ping, tools/list, tools/call.
/// Pur und seiteneffektfrei → unit-getestet; der HTTP-Host ist nur eine dünne Hülle.
/// </summary>
public static class McpJsonRpc
{
public const string ProtocolVersion = "2025-03-26";
public const string ServerName = "polytrader-supervisor";
public const string ServerVersion = "1.0";
/// <summary>
/// Verarbeitet eine JSON-RPC-Nachricht. Liefert die Antwort als JSON-String —
/// oder null für Notifications (kein id) und unparsbare Eingaben ohne id.
/// </summary>
public static string? Handle(string requestJson, SupervisorToolRegistry registry)
{
JsonDocument doc;
try { doc = JsonDocument.Parse(requestJson); }
catch (JsonException) { return Error(null, -32700, "Parse error"); }
using (doc)
{
var root = doc.RootElement;
JsonElement? id = root.TryGetProperty("id", out var idProp) ? idProp.Clone() : (JsonElement?)null;
string method = root.TryGetProperty("method", out var m) ? m.GetString() ?? "" : "";
// Notifications (kein id) werden nicht beantwortet.
if (id == null) return null;
try
{
return method switch
{
"initialize" => Result(id.Value, w =>
{
w.WriteString("protocolVersion", ProtocolVersion);
w.WriteStartObject("capabilities");
w.WriteStartObject("tools");
w.WriteEndObject();
w.WriteEndObject();
w.WriteStartObject("serverInfo");
w.WriteString("name", ServerName);
w.WriteString("version", ServerVersion);
w.WriteEndObject();
}),
"ping" => Result(id.Value, _ => { }),
"tools/list" => Result(id.Value, w =>
{
w.WriteStartArray("tools");
foreach (var tool in registry.Tools)
{
w.WriteStartObject();
w.WriteString("name", tool.Name);
w.WriteString("description", tool.Description);
w.WritePropertyName("inputSchema");
using (var schema = JsonDocument.Parse(tool.ParametersJsonSchema))
schema.RootElement.WriteTo(w);
w.WriteEndObject();
}
w.WriteEndArray();
}),
"tools/call" => HandleToolCall(id.Value, root, registry),
_ => Error(id, -32601, $"Method not found: {method}")
};
}
catch (Exception ex)
{
return Error(id, -32603, $"Internal error: {ex.Message}");
}
}
}
private static string HandleToolCall(JsonElement id, JsonElement root, SupervisorToolRegistry registry)
{
if (!root.TryGetProperty("params", out var p) || p.ValueKind != JsonValueKind.Object)
return Error(id, -32602, "Invalid params");
string name = p.TryGetProperty("name", out var n) ? n.GetString() ?? "" : "";
string argsJson = p.TryGetProperty("arguments", out var a) && a.ValueKind == JsonValueKind.Object
? a.GetRawText() : "{}";
string toolResult = registry.Execute(name, argsJson);
bool isError = toolResult.StartsWith("FEHLER", StringComparison.OrdinalIgnoreCase);
return Result(id, w =>
{
w.WriteStartArray("content");
w.WriteStartObject();
w.WriteString("type", "text");
w.WriteString("text", toolResult);
w.WriteEndObject();
w.WriteEndArray();
w.WriteBoolean("isError", isError);
});
}
// ----- JSON-RPC-Hüllen -----
private static string Result(JsonElement id, Action<Utf8JsonWriter> writeResult)
{
using var ms = new MemoryStream();
using (var w = new Utf8JsonWriter(ms))
{
w.WriteStartObject();
w.WriteString("jsonrpc", "2.0");
w.WritePropertyName("id");
id.WriteTo(w);
w.WriteStartObject("result");
writeResult(w);
w.WriteEndObject();
w.WriteEndObject();
}
return Encoding.UTF8.GetString(ms.ToArray());
}
private static string Error(JsonElement? id, int code, string message)
{
using var ms = new MemoryStream();
using (var w = new Utf8JsonWriter(ms))
{
w.WriteStartObject();
w.WriteString("jsonrpc", "2.0");
w.WritePropertyName("id");
if (id.HasValue) id.Value.WriteTo(w); else w.WriteNullValue();
w.WriteStartObject("error");
w.WriteNumber("code", code);
w.WriteString("message", message);
w.WriteEndObject();
w.WriteEndObject();
}
return Encoding.UTF8.GetString(ms.ToArray());
}
}
}
@@ -0,0 +1,109 @@
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using PolyTrader.Modules.Supervisor.Agent;
using PolyTraderSharp.Services;
namespace PolyTrader.Modules.Supervisor.Mcp
{
/// <summary>
/// MCP-Light-Host (S-4): lokaler HTTP-Endpoint (Streamable-HTTP-Transport, nur POST-JSON), der die
/// read-only Tool-Registry per Model Context Protocol exponiert. Externe Clients wie Claude Code
/// verbinden sich mit: <c>claude mcp add --transport http polytrader http://127.0.0.1:PORT/mcp</c>.
///
/// SICHERHEIT: bewusst OPT-IN (startet nur, wenn die Umgebungsvariable POLYTRADER_MCP_PORT gesetzt
/// ist) und bindet ausschließlich an 127.0.0.1 (kein Netzwerkzugriff). Die Tools sind read-only —
/// es existiert kein Mechanismus zum Handeln/Schreiben. Kein Modell-Zugang: MCP ist nur die
/// Daten-Tür; Modelle laufen weiterhin über OpenRouter (In-App-Agent) bzw. den externen Client.
/// </summary>
public sealed class McpLightServer : BackgroundService
{
private readonly SupervisorToolRegistry _registry;
private readonly TerminalLogger _logger;
public McpLightServer(SupervisorToolRegistry registry, TerminalLogger logger)
{
_registry = registry;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
string? portRaw = Environment.GetEnvironmentVariable("POLYTRADER_MCP_PORT");
if (string.IsNullOrWhiteSpace(portRaw))
{
_logger.Info("MCP-Light: deaktiviert (POLYTRADER_MCP_PORT nicht gesetzt).");
return;
}
if (!int.TryParse(portRaw, out int port) || port is < 1024 or > 65535)
{
_logger.Warning($"MCP-Light: ungültiger Port '{portRaw}' Server startet nicht.");
return;
}
using var listener = new HttpListener();
listener.Prefixes.Add($"http://127.0.0.1:{port}/mcp/");
try { listener.Start(); }
catch (Exception ex)
{
_logger.Error($"MCP-Light: Start auf Port {port} fehlgeschlagen: {ex.Message}");
return;
}
_logger.Info($"🔌 MCP-Light aktiv: http://127.0.0.1:{port}/mcp (read-only, {_registry.Tools.Count} Tools). " +
$"Claude Code: claude mcp add --transport http polytrader http://127.0.0.1:{port}/mcp");
using var reg = stoppingToken.Register(() => { try { listener.Stop(); } catch { } });
while (!stoppingToken.IsCancellationRequested)
{
HttpListenerContext ctx;
try { ctx = await listener.GetContextAsync(); }
catch when (stoppingToken.IsCancellationRequested) { break; }
catch (Exception ex) { _logger.Warning($"MCP-Light: Listener-Fehler: {ex.Message}"); continue; }
_ = Task.Run(() => HandleRequestAsync(ctx), stoppingToken);
}
}
private async Task HandleRequestAsync(HttpListenerContext ctx)
{
try
{
if (ctx.Request.HttpMethod != "POST")
{
ctx.Response.StatusCode = 405; // GET/SSE-Stream bieten wir bewusst nicht an
ctx.Response.Close();
return;
}
string body;
using (var reader = new StreamReader(ctx.Request.InputStream, ctx.Request.ContentEncoding))
body = await reader.ReadToEndAsync();
string? response = McpJsonRpc.Handle(body, _registry);
if (response == null)
{
ctx.Response.StatusCode = 202; // Notification: angenommen, keine Antwort
ctx.Response.Close();
return;
}
byte[] bytes = Encoding.UTF8.GetBytes(response);
ctx.Response.StatusCode = 200;
ctx.Response.ContentType = "application/json";
ctx.Response.ContentLength64 = bytes.Length;
await ctx.Response.OutputStream.WriteAsync(bytes);
ctx.Response.Close();
}
catch (Exception ex)
{
_logger.Warning($"MCP-Light: Request-Fehler: {ex.Message}");
try { ctx.Response.StatusCode = 500; ctx.Response.Close(); } catch { }
}
}
}
}
@@ -0,0 +1,80 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using PolyTrader.Modules.Supervisor.Persistence;
#nullable disable
namespace PolyTrader.Modules.Supervisor.Persistence.Migrations
{
[DbContext(typeof(SupervisorDbContext))]
[Migration("20260718085811_InitialSupervisor")]
partial class InitialSupervisor
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.13")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
modelBuilder.Entity("PolyTrader.Modules.Supervisor.Persistence.SupervisorReport", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("Answer")
.IsRequired()
.HasColumnType("text");
b.Property<int>("CompletionTokens")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Model")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("varchar(120)");
b.Property<string>("Profile")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<int>("PromptTokens")
.HasColumnType("int");
b.Property<string>("Question")
.IsRequired()
.HasMaxLength(4000)
.HasColumnType("varchar(4000)");
b.Property<int>("ToolCallCount")
.HasColumnType("int");
b.Property<string>("ToolCallsJson")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("CreatedAt");
b.ToTable("sup_reports", (string)null);
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,58 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace PolyTrader.Modules.Supervisor.Persistence.Migrations
{
/// <inheritdoc />
public partial class InitialSupervisor : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "sup_reports",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
Profile = table.Column<string>(type: "varchar(50)", maxLength: 50, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Model = table.Column<string>(type: "varchar(120)", maxLength: 120, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Question = table.Column<string>(type: "varchar(4000)", maxLength: 4000, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Answer = table.Column<string>(type: "text", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
ToolCallsJson = table.Column<string>(type: "text", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
ToolCallCount = table.Column<int>(type: "int", nullable: false),
PromptTokens = table.Column<int>(type: "int", nullable: false),
CompletionTokens = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_sup_reports", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_sup_reports_CreatedAt",
table: "sup_reports",
column: "CreatedAt");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "sup_reports");
}
}
}
@@ -0,0 +1,77 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using PolyTrader.Modules.Supervisor.Persistence;
#nullable disable
namespace PolyTrader.Modules.Supervisor.Persistence.Migrations
{
[DbContext(typeof(SupervisorDbContext))]
partial class SupervisorDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.13")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
modelBuilder.Entity("PolyTrader.Modules.Supervisor.Persistence.SupervisorReport", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("Answer")
.IsRequired()
.HasColumnType("text");
b.Property<int>("CompletionTokens")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Model")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("varchar(120)");
b.Property<string>("Profile")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<int>("PromptTokens")
.HasColumnType("int");
b.Property<string>("Question")
.IsRequired()
.HasMaxLength(4000)
.HasColumnType("varchar(4000)");
b.Property<int>("ToolCallCount")
.HasColumnType("int");
b.Property<string>("ToolCallsJson")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("CreatedAt");
b.ToTable("sup_reports", (string)null);
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using PolyTrader.Core.Configuration;
namespace PolyTrader.Modules.Supervisor.Persistence
{
/// <summary>EF-Kontext des Supervisor-Moduls (gleiche MySQL-DB, Tabellen mit Präfix sup_).</summary>
public class SupervisorDbContext : DbContext
{
public SupervisorDbContext(DbContextOptions<SupervisorDbContext> options) : base(options) { }
public DbSet<SupervisorReport> Reports => Set<SupervisorReport>();
protected override void OnModelCreating(ModelBuilder b)
{
b.Entity<SupervisorReport>(e =>
{
e.ToTable("sup_reports");
e.HasKey(x => x.Id);
e.Property(x => x.Id).ValueGeneratedOnAdd();
e.Property(x => x.Profile).HasMaxLength(50);
e.Property(x => x.Model).HasMaxLength(120);
e.Property(x => x.Question).HasMaxLength(4000);
e.Property(x => x.Answer).HasColumnType("text");
e.Property(x => x.ToolCallsJson).HasColumnType("text");
e.HasIndex(x => x.CreatedAt);
});
}
}
/// <summary>
/// Design-Time-Factory (fixe Server-Version → Migrations-Scaffolding OHNE DB-Verbindung).
/// </summary>
public class SupervisorDbContextFactory : IDesignTimeDbContextFactory<SupervisorDbContext>
{
public SupervisorDbContext CreateDbContext(string[] args)
{
var conn = Environment.GetEnvironmentVariable("POLYTRADER_MYSQL")
?? "Server=localhost;Port=3306;Database=polytrader;User ID=root;Password=;";
var options = new DbContextOptionsBuilder<SupervisorDbContext>()
.UseMySql(conn, DatabaseServerVersion.Value)
.Options;
return new SupervisorDbContext(options);
}
}
/// <summary>Bericht-Ablage. Write fehlertolerant (Analyse darf nie an der Persistenz scheitern).</summary>
public interface ISupervisorReportRepository
{
void Insert(SupervisorReport report);
List<SupervisorReport> GetRecent(int limit);
}
public class EfSupervisorReportRepository : ISupervisorReportRepository
{
private readonly IDbContextFactory<SupervisorDbContext> _factory;
public EfSupervisorReportRepository(IDbContextFactory<SupervisorDbContext> factory) => _factory = factory;
public void Insert(SupervisorReport report)
{
try
{
using var ctx = _factory.CreateDbContext();
ctx.Reports.Add(report);
ctx.SaveChanges();
}
catch (Exception ex)
{
Console.WriteLine($"[SupervisorReports] Write fehlgeschlagen (ignoriert): {ex.Message}");
}
}
public List<SupervisorReport> GetRecent(int limit)
{
using var ctx = _factory.CreateDbContext();
return ctx.Reports.AsNoTracking().OrderByDescending(r => r.CreatedAt).Take(limit).ToList();
}
}
}
@@ -0,0 +1,27 @@
using System;
namespace PolyTrader.Modules.Supervisor.Persistence
{
/// <summary>
/// Gespeicherte Analyse (Tabelle sup_reports): Frage, Antwort, Profil/Modell und die
/// Tool-Aufruf-Historie — macht den Supervisor selbst auditierbar (Konzept §6) und
/// füttert später den „Chef"-Supervisor sowie Tagesberichte.
/// </summary>
public class SupervisorReport
{
public long Id { get; set; } // DB-Autoincrement
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public string Profile { get; set; } = string.Empty;
public string Model { get; set; } = string.Empty;
public string Question { get; set; } = string.Empty;
public string Answer { get; set; } = string.Empty;
/// <summary>Tool-Aufrufe als JSON [{tool,args}] (Ergebnisse sind reproduzierbar, daher nicht gespeichert).</summary>
public string ToolCallsJson { get; set; } = string.Empty;
public int ToolCallCount { get; set; }
public int PromptTokens { get; set; }
public int CompletionTokens { get; set; }
}
}
@@ -4,6 +4,14 @@
<ProjectReference Include="..\PolyTrader.Core\PolyTrader.Core.csproj" /> <ProjectReference Include="..\PolyTrader.Core\PolyTrader.Core.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<!-- Erlaubt dem Testprojekt, interne Methoden zu testen. --> <!-- Erlaubt dem Testprojekt, interne Methoden zu testen. -->
<ItemGroup> <ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute"> <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
@@ -1,5 +1,6 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using PolyTrader.Core.Modularity; using PolyTrader.Core.Modularity;
@@ -34,7 +35,15 @@ namespace PolyTrader.Modules.Supervisor
new Agent.OpenRouterClient(new System.Net.Http.HttpClient { Timeout = TimeSpan.FromMinutes(3) })); new Agent.OpenRouterClient(new System.Net.Http.HttpClient { Timeout = TimeSpan.FromMinutes(3) }));
services.AddSingleton<Agent.SupervisorAgent>(); services.AddSingleton<Agent.SupervisorAgent>();
// sup_-Persistenz (Berichte/Konversationen) + Profile folgen mit S-3. // S-3: sup_-Persistenz (gespeicherte Analysen/Berichte).
var conn = configuration["Database:MySqlConnectionString"] ?? string.Empty;
services.AddDbContextFactory<Persistence.SupervisorDbContext>(o =>
o.UseMySql(conn, PolyTrader.Core.Configuration.DatabaseServerVersion.Value));
services.AddSingleton<Persistence.ISupervisorReportRepository, Persistence.EfSupervisorReportRepository>();
// S-4: MCP-Light exponiert die read-only Tool-Registry für externe KI-Clients
// (z. B. Claude Code). OPT-IN via POLYTRADER_MCP_PORT, bindet nur 127.0.0.1.
services.AddHostedService<Mcp.McpLightServer>();
} }
public void RegisterUi(IModuleUiHost host, System.IServiceProvider services) public void RegisterUi(IModuleUiHost host, System.IServiceProvider services)
@@ -0,0 +1,412 @@
namespace PolyTrader.Modules.Supervisor.Ui
{
partial class SupervisorMainForm
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Vom Komponenten-Designer generierter Code
private void InitializeComponent()
{
this.tabControlSup = new System.Windows.Forms.TabControl();
this.tabAnalyse = new System.Windows.Forms.TabPage();
this.rtbChat = new System.Windows.Forms.RichTextBox();
this.pnlChatInput = new System.Windows.Forms.Panel();
this.tbChatInput = new System.Windows.Forms.TextBox();
this.btnSend = new System.Windows.Forms.Button();
this.toolStripChat = new System.Windows.Forms.ToolStrip();
this.lblProfil = new System.Windows.Forms.ToolStripLabel();
this.cbProfile = new System.Windows.Forms.ToolStripComboBox();
this.lblModel = new System.Windows.Forms.ToolStripLabel();
this.tbModel = new System.Windows.Forms.ToolStripTextBox();
this.sepChat = new System.Windows.Forms.ToolStripSeparator();
this.btnClearChat = new System.Windows.Forms.ToolStripButton();
this.tabDossiers = new System.Windows.Forms.TabPage();
this.splitDossiers = new System.Windows.Forms.SplitContainer();
this.dgvSignals = new System.Windows.Forms.DataGridView();
this.tbDossier = new System.Windows.Forms.TextBox();
this.toolStripDossier = new System.Windows.Forms.ToolStrip();
this.btnDossierRefresh = new System.Windows.Forms.ToolStripButton();
this.sepDossier = new System.Windows.Forms.ToolStripSeparator();
this.lblSignal = new System.Windows.Forms.ToolStripLabel();
this.tbSignalId = new System.Windows.Forms.ToolStripTextBox();
this.btnDossierOpen = new System.Windows.Forms.ToolStripButton();
this.tabBerichte = new System.Windows.Forms.TabPage();
this.splitReports = new System.Windows.Forms.SplitContainer();
this.dgvReports = new System.Windows.Forms.DataGridView();
this.tbReport = new System.Windows.Forms.TextBox();
this.toolStripReports = new System.Windows.Forms.ToolStrip();
this.btnReportsRefresh = new System.Windows.Forms.ToolStripButton();
this.lblStatus = new System.Windows.Forms.Label();
this.tabControlSup.SuspendLayout();
this.tabAnalyse.SuspendLayout();
this.pnlChatInput.SuspendLayout();
this.toolStripChat.SuspendLayout();
this.tabDossiers.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.splitDossiers)).BeginInit();
this.splitDossiers.Panel1.SuspendLayout();
this.splitDossiers.Panel2.SuspendLayout();
this.splitDossiers.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.dgvSignals)).BeginInit();
this.toolStripDossier.SuspendLayout();
this.tabBerichte.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.splitReports)).BeginInit();
this.splitReports.Panel1.SuspendLayout();
this.splitReports.Panel2.SuspendLayout();
this.splitReports.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.dgvReports)).BeginInit();
this.toolStripReports.SuspendLayout();
this.SuspendLayout();
//
// tabControlSup
//
this.tabControlSup.Controls.Add(this.tabAnalyse);
this.tabControlSup.Controls.Add(this.tabDossiers);
this.tabControlSup.Controls.Add(this.tabBerichte);
this.tabControlSup.Dock = System.Windows.Forms.DockStyle.Fill;
this.tabControlSup.Location = new System.Drawing.Point(0, 0);
this.tabControlSup.Name = "tabControlSup";
this.tabControlSup.SelectedIndex = 0;
this.tabControlSup.Size = new System.Drawing.Size(1250, 676);
this.tabControlSup.TabIndex = 0;
//
// tabAnalyse
//
this.tabAnalyse.Controls.Add(this.rtbChat);
this.tabAnalyse.Controls.Add(this.pnlChatInput);
this.tabAnalyse.Controls.Add(this.toolStripChat);
this.tabAnalyse.Location = new System.Drawing.Point(4, 24);
this.tabAnalyse.Name = "tabAnalyse";
this.tabAnalyse.Padding = new System.Windows.Forms.Padding(3);
this.tabAnalyse.Size = new System.Drawing.Size(1242, 648);
this.tabAnalyse.TabIndex = 0;
this.tabAnalyse.Text = "Analyse";
this.tabAnalyse.UseVisualStyleBackColor = true;
//
// rtbChat
//
this.rtbChat.BackColor = System.Drawing.Color.White;
this.rtbChat.Dock = System.Windows.Forms.DockStyle.Fill;
this.rtbChat.Font = new System.Drawing.Font("Segoe UI", 9.5F);
this.rtbChat.Location = new System.Drawing.Point(3, 28);
this.rtbChat.Name = "rtbChat";
this.rtbChat.ReadOnly = true;
this.rtbChat.Size = new System.Drawing.Size(1236, 557);
this.rtbChat.TabIndex = 1;
this.rtbChat.Text = "";
//
// pnlChatInput
//
this.pnlChatInput.Controls.Add(this.tbChatInput);
this.pnlChatInput.Controls.Add(this.btnSend);
this.pnlChatInput.Dock = System.Windows.Forms.DockStyle.Bottom;
this.pnlChatInput.Location = new System.Drawing.Point(3, 585);
this.pnlChatInput.Name = "pnlChatInput";
this.pnlChatInput.Padding = new System.Windows.Forms.Padding(4);
this.pnlChatInput.Size = new System.Drawing.Size(1236, 60);
this.pnlChatInput.TabIndex = 2;
//
// tbChatInput
//
this.tbChatInput.Dock = System.Windows.Forms.DockStyle.Fill;
this.tbChatInput.Location = new System.Drawing.Point(4, 4);
this.tbChatInput.Multiline = true;
this.tbChatInput.Name = "tbChatInput";
this.tbChatInput.PlaceholderText = "Analyse-Frage stellen … (Strg+Enter zum Senden)";
this.tbChatInput.Size = new System.Drawing.Size(1118, 52);
this.tbChatInput.TabIndex = 0;
//
// btnSend
//
this.btnSend.Dock = System.Windows.Forms.DockStyle.Right;
this.btnSend.Location = new System.Drawing.Point(1122, 4);
this.btnSend.Name = "btnSend";
this.btnSend.Size = new System.Drawing.Size(110, 52);
this.btnSend.TabIndex = 1;
this.btnSend.Text = "Senden";
this.btnSend.UseVisualStyleBackColor = true;
//
// toolStripChat
//
this.toolStripChat.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.lblProfil, this.cbProfile, this.lblModel, this.tbModel, this.sepChat, this.btnClearChat});
this.toolStripChat.Location = new System.Drawing.Point(3, 3);
this.toolStripChat.Name = "toolStripChat";
this.toolStripChat.Size = new System.Drawing.Size(1236, 25);
this.toolStripChat.TabIndex = 0;
//
// lblProfil
//
this.lblProfil.Name = "lblProfil";
this.lblProfil.Text = "Profil:";
//
// cbProfile
//
this.cbProfile.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.cbProfile.Name = "cbProfile";
this.cbProfile.Size = new System.Drawing.Size(160, 25);
//
// lblModel
//
this.lblModel.Name = "lblModel";
this.lblModel.Text = "Modell:";
//
// tbModel
//
this.tbModel.AutoSize = false;
this.tbModel.Name = "tbModel";
this.tbModel.Size = new System.Drawing.Size(220, 25);
this.tbModel.Text = "openrouter/auto";
//
// sepChat
//
this.sepChat.Name = "sepChat";
//
// btnClearChat
//
this.btnClearChat.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.btnClearChat.Name = "btnClearChat";
this.btnClearChat.Text = "Verlauf leeren";
//
// tabDossiers
//
this.tabDossiers.Controls.Add(this.splitDossiers);
this.tabDossiers.Controls.Add(this.toolStripDossier);
this.tabDossiers.Location = new System.Drawing.Point(4, 24);
this.tabDossiers.Name = "tabDossiers";
this.tabDossiers.Padding = new System.Windows.Forms.Padding(3);
this.tabDossiers.Size = new System.Drawing.Size(1242, 648);
this.tabDossiers.TabIndex = 1;
this.tabDossiers.Text = "Dossiers";
this.tabDossiers.UseVisualStyleBackColor = true;
//
// splitDossiers
//
this.splitDossiers.Dock = System.Windows.Forms.DockStyle.Fill;
this.splitDossiers.Location = new System.Drawing.Point(3, 28);
this.splitDossiers.Name = "splitDossiers";
this.splitDossiers.Panel1.Controls.Add(this.dgvSignals);
this.splitDossiers.Panel2.Controls.Add(this.tbDossier);
this.splitDossiers.Size = new System.Drawing.Size(1236, 617);
this.splitDossiers.SplitterDistance = 420;
this.splitDossiers.TabIndex = 1;
//
// dgvSignals
//
this.dgvSignals.AllowUserToAddRows = false;
this.dgvSignals.AllowUserToDeleteRows = false;
this.dgvSignals.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dgvSignals.Dock = System.Windows.Forms.DockStyle.Fill;
this.dgvSignals.Location = new System.Drawing.Point(0, 0);
this.dgvSignals.MultiSelect = false;
this.dgvSignals.Name = "dgvSignals";
this.dgvSignals.ReadOnly = true;
this.dgvSignals.RowHeadersVisible = false;
this.dgvSignals.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect;
this.dgvSignals.Size = new System.Drawing.Size(420, 617);
this.dgvSignals.TabIndex = 0;
//
// tbDossier
//
this.tbDossier.Dock = System.Windows.Forms.DockStyle.Fill;
this.tbDossier.Font = new System.Drawing.Font("Consolas", 9.5F);
this.tbDossier.Location = new System.Drawing.Point(0, 0);
this.tbDossier.Multiline = true;
this.tbDossier.Name = "tbDossier";
this.tbDossier.ReadOnly = true;
this.tbDossier.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.tbDossier.Size = new System.Drawing.Size(812, 617);
this.tbDossier.TabIndex = 0;
this.tbDossier.WordWrap = false;
//
// toolStripDossier
//
this.toolStripDossier.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.btnDossierRefresh, this.sepDossier, this.lblSignal, this.tbSignalId, this.btnDossierOpen});
this.toolStripDossier.Location = new System.Drawing.Point(3, 3);
this.toolStripDossier.Name = "toolStripDossier";
this.toolStripDossier.Size = new System.Drawing.Size(1236, 25);
this.toolStripDossier.TabIndex = 0;
//
// btnDossierRefresh
//
this.btnDossierRefresh.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.btnDossierRefresh.Name = "btnDossierRefresh";
this.btnDossierRefresh.Text = "Aktualisieren";
//
// sepDossier
//
this.sepDossier.Name = "sepDossier";
//
// lblSignal
//
this.lblSignal.Name = "lblSignal";
this.lblSignal.Text = "SignalId:";
//
// tbSignalId
//
this.tbSignalId.AutoSize = false;
this.tbSignalId.Name = "tbSignalId";
this.tbSignalId.Size = new System.Drawing.Size(220, 25);
//
// btnDossierOpen
//
this.btnDossierOpen.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.btnDossierOpen.Name = "btnDossierOpen";
this.btnDossierOpen.Text = "Dossier öffnen";
//
// tabBerichte
//
this.tabBerichte.Controls.Add(this.splitReports);
this.tabBerichte.Controls.Add(this.toolStripReports);
this.tabBerichte.Location = new System.Drawing.Point(4, 24);
this.tabBerichte.Name = "tabBerichte";
this.tabBerichte.Padding = new System.Windows.Forms.Padding(3);
this.tabBerichte.Size = new System.Drawing.Size(1242, 648);
this.tabBerichte.TabIndex = 2;
this.tabBerichte.Text = "Berichte";
this.tabBerichte.UseVisualStyleBackColor = true;
//
// splitReports
//
this.splitReports.Dock = System.Windows.Forms.DockStyle.Fill;
this.splitReports.Location = new System.Drawing.Point(3, 28);
this.splitReports.Name = "splitReports";
this.splitReports.Panel1.Controls.Add(this.dgvReports);
this.splitReports.Panel2.Controls.Add(this.tbReport);
this.splitReports.Size = new System.Drawing.Size(1236, 617);
this.splitReports.SplitterDistance = 520;
this.splitReports.TabIndex = 1;
//
// dgvReports
//
this.dgvReports.AllowUserToAddRows = false;
this.dgvReports.AllowUserToDeleteRows = false;
this.dgvReports.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dgvReports.Dock = System.Windows.Forms.DockStyle.Fill;
this.dgvReports.Location = new System.Drawing.Point(0, 0);
this.dgvReports.MultiSelect = false;
this.dgvReports.Name = "dgvReports";
this.dgvReports.ReadOnly = true;
this.dgvReports.RowHeadersVisible = false;
this.dgvReports.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect;
this.dgvReports.Size = new System.Drawing.Size(520, 617);
this.dgvReports.TabIndex = 0;
//
// tbReport
//
this.tbReport.Dock = System.Windows.Forms.DockStyle.Fill;
this.tbReport.Font = new System.Drawing.Font("Segoe UI", 9.5F);
this.tbReport.Location = new System.Drawing.Point(0, 0);
this.tbReport.Multiline = true;
this.tbReport.Name = "tbReport";
this.tbReport.ReadOnly = true;
this.tbReport.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.tbReport.Size = new System.Drawing.Size(712, 617);
this.tbReport.TabIndex = 0;
//
// toolStripReports
//
this.toolStripReports.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.btnReportsRefresh});
this.toolStripReports.Location = new System.Drawing.Point(3, 3);
this.toolStripReports.Name = "toolStripReports";
this.toolStripReports.Size = new System.Drawing.Size(1236, 25);
this.toolStripReports.TabIndex = 0;
//
// btnReportsRefresh
//
this.btnReportsRefresh.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.btnReportsRefresh.Name = "btnReportsRefresh";
this.btnReportsRefresh.Text = "Aktualisieren";
//
// lblStatus
//
this.lblStatus.Dock = System.Windows.Forms.DockStyle.Bottom;
this.lblStatus.Location = new System.Drawing.Point(0, 676);
this.lblStatus.Name = "lblStatus";
this.lblStatus.Padding = new System.Windows.Forms.Padding(6, 2, 6, 2);
this.lblStatus.Size = new System.Drawing.Size(1250, 22);
this.lblStatus.TabIndex = 1;
this.lblStatus.Text = "";
//
// SupervisorMainForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(1250, 698);
this.Controls.Add(this.tabControlSup);
this.Controls.Add(this.lblStatus);
this.Name = "SupervisorMainForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "Supervisor";
this.tabControlSup.ResumeLayout(false);
this.tabAnalyse.ResumeLayout(false);
this.pnlChatInput.ResumeLayout(false);
this.pnlChatInput.PerformLayout();
this.toolStripChat.ResumeLayout(false);
this.toolStripChat.PerformLayout();
this.tabDossiers.ResumeLayout(false);
this.splitDossiers.Panel1.ResumeLayout(false);
this.splitDossiers.Panel2.ResumeLayout(false);
this.splitDossiers.Panel2.PerformLayout();
((System.ComponentModel.ISupportInitialize)(this.splitDossiers)).EndInit();
this.splitDossiers.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)(this.dgvSignals)).EndInit();
this.toolStripDossier.ResumeLayout(false);
this.toolStripDossier.PerformLayout();
this.tabBerichte.ResumeLayout(false);
this.splitReports.Panel1.ResumeLayout(false);
this.splitReports.Panel2.ResumeLayout(false);
this.splitReports.Panel2.PerformLayout();
((System.ComponentModel.ISupportInitialize)(this.splitReports)).EndInit();
this.splitReports.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)(this.dgvReports)).EndInit();
this.toolStripReports.ResumeLayout(false);
this.toolStripReports.PerformLayout();
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.TabControl tabControlSup;
private System.Windows.Forms.TabPage tabAnalyse;
private System.Windows.Forms.ToolStrip toolStripChat;
private System.Windows.Forms.ToolStripLabel lblProfil;
private System.Windows.Forms.ToolStripComboBox cbProfile;
private System.Windows.Forms.ToolStripLabel lblModel;
private System.Windows.Forms.ToolStripTextBox tbModel;
private System.Windows.Forms.ToolStripSeparator sepChat;
private System.Windows.Forms.ToolStripButton btnClearChat;
private System.Windows.Forms.RichTextBox rtbChat;
private System.Windows.Forms.Panel pnlChatInput;
private System.Windows.Forms.TextBox tbChatInput;
private System.Windows.Forms.Button btnSend;
private System.Windows.Forms.TabPage tabDossiers;
private System.Windows.Forms.ToolStrip toolStripDossier;
private System.Windows.Forms.ToolStripButton btnDossierRefresh;
private System.Windows.Forms.ToolStripSeparator sepDossier;
private System.Windows.Forms.ToolStripLabel lblSignal;
private System.Windows.Forms.ToolStripTextBox tbSignalId;
private System.Windows.Forms.ToolStripButton btnDossierOpen;
private System.Windows.Forms.SplitContainer splitDossiers;
private System.Windows.Forms.DataGridView dgvSignals;
private System.Windows.Forms.TextBox tbDossier;
private System.Windows.Forms.TabPage tabBerichte;
private System.Windows.Forms.ToolStrip toolStripReports;
private System.Windows.Forms.ToolStripButton btnReportsRefresh;
private System.Windows.Forms.SplitContainer splitReports;
private System.Windows.Forms.DataGridView dgvReports;
private System.Windows.Forms.TextBox tbReport;
private System.Windows.Forms.Label lblStatus;
}
}
@@ -4,110 +4,52 @@ using System.Windows.Forms;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using PolyTrader.Core.Analytics; using PolyTrader.Core.Analytics;
using PolyTrader.Modules.Supervisor.Agent; using PolyTrader.Modules.Supervisor.Agent;
using PolyTrader.Modules.Supervisor.Persistence;
using PolyTrader.Modules.Supervisor.Services; using PolyTrader.Modules.Supervisor.Services;
namespace PolyTrader.Modules.Supervisor.Ui namespace PolyTrader.Modules.Supervisor.Ui
{ {
/// <summary> /// <summary>
/// Hauptfenster des Supervisor-Moduls: Tab „Analyse" (Chat mit dem read-only-Agenten, /// Hauptfenster des Supervisor-Moduls: Tab „Analyse" (Chat mit Profil-/Modellwahl, Tool-Aufrufe
/// Tool-Aufrufe transparent im Verlauf) und Tab „Dossiers" (Signal-Browser mit Markdown-Dossier). /// transparent), Tab „Dossiers" (Signal-Browser), Tab „Berichte" (gespeicherte Analysen).
/// Code-only konstruiert (Muster RF-Modul); DB-/API-Zugriffe defensiv. /// Layout im Designer (SupervisorMainForm.Designer.cs), Verhalten/Daten hier.
/// </summary> /// </summary>
public sealed class SupervisorMainForm : Form public partial class SupervisorMainForm : Form
{ {
private DossierService? _dossiers; private DossierService? _dossiers;
private SupervisorAgent? _agent; private SupervisorAgent? _agent;
private ISupervisorReportRepository? _reports;
// ----- Tab Analyse (Chat) -----
private readonly ToolStrip _chatStrip = new();
private readonly ToolStripLabel _lblModel = new() { Text = "Modell:" };
private readonly ToolStripTextBox _tbModel = new() { AutoSize = false, Width = 220, Text = SupervisorAgent.DefaultModel };
private readonly ToolStripButton _btnClearChat = new() { Text = "Verlauf leeren", DisplayStyle = ToolStripItemDisplayStyle.Text };
private readonly RichTextBox _chatLog = new()
{
Dock = DockStyle.Fill, ReadOnly = true, BackColor = System.Drawing.Color.White,
Font = new System.Drawing.Font("Segoe UI", 9.5f)
};
private readonly TextBox _chatInput = new()
{
Dock = DockStyle.Fill, Multiline = true, Height = 54,
PlaceholderText = "Analyse-Frage stellen … (Strg+Enter zum Senden)"
};
private readonly Button _btnSend = new() { Text = "Senden", Dock = DockStyle.Right, Width = 110 };
// ----- Tab Dossiers (Browser) -----
private readonly ToolStrip _dossierStrip = new();
private readonly ToolStripButton _btnRefresh = new() { Text = "Aktualisieren", DisplayStyle = ToolStripItemDisplayStyle.Text };
private readonly ToolStripLabel _lblSearch = new() { Text = "SignalId:" };
private readonly ToolStripTextBox _tbSignalId = new() { AutoSize = false, Width = 220 };
private readonly ToolStripButton _btnOpen = new() { Text = "Dossier öffnen", DisplayStyle = ToolStripItemDisplayStyle.Text };
private readonly SplitContainer _split = new() { Dock = DockStyle.Fill, SplitterDistance = 420 };
private readonly DataGridView _grid = new()
{
Dock = DockStyle.Fill, ReadOnly = true, AllowUserToAddRows = false, AllowUserToDeleteRows = false,
AutoGenerateColumns = true, SelectionMode = DataGridViewSelectionMode.FullRowSelect,
RowHeadersVisible = false, MultiSelect = false
};
private readonly TextBox _dossierText = new()
{
Dock = DockStyle.Fill, Multiline = true, ReadOnly = true, ScrollBars = ScrollBars.Both,
Font = new System.Drawing.Font("Consolas", 9.5f), WordWrap = false
};
private readonly Label _status = new() { Dock = DockStyle.Bottom, Height = 22, Padding = new Padding(6, 2, 6, 2), Text = "" };
public SupervisorMainForm() public SupervisorMainForm()
{ {
Text = "Supervisor"; InitializeComponent();
Width = 1250;
Height = 720;
StartPosition = FormStartPosition.CenterScreen;
var tabs = new TabControl { Dock = DockStyle.Fill }; foreach (var profile in SupervisorProfiles.All)
cbProfile.Items.Add(profile);
cbProfile.SelectedIndex = 0;
// --- Tab Analyse --- btnSend.Click += async (_, _) => await SendQuestionAsync();
var tabAnalyse = new TabPage("Analyse"); tbChatInput.KeyDown += async (_, e) =>
_chatStrip.Items.AddRange(new ToolStripItem[] { _lblModel, _tbModel, new ToolStripSeparator(), _btnClearChat });
var inputPanel = new Panel { Dock = DockStyle.Bottom, Height = 60, Padding = new Padding(4) };
inputPanel.Controls.Add(_chatInput);
inputPanel.Controls.Add(_btnSend);
tabAnalyse.Controls.Add(_chatLog);
tabAnalyse.Controls.Add(inputPanel);
tabAnalyse.Controls.Add(_chatStrip);
_chatStrip.Dock = DockStyle.Top;
// --- Tab Dossiers ---
var tabDossiers = new TabPage("Dossiers");
_dossierStrip.Items.AddRange(new ToolStripItem[] { _btnRefresh, new ToolStripSeparator(), _lblSearch, _tbSignalId, _btnOpen });
_split.Panel1.Controls.Add(_grid);
_split.Panel2.Controls.Add(_dossierText);
tabDossiers.Controls.Add(_split);
tabDossiers.Controls.Add(_dossierStrip);
_dossierStrip.Dock = DockStyle.Top;
tabs.TabPages.AddRange(new[] { tabAnalyse, tabDossiers });
Controls.Add(tabs);
Controls.Add(_status);
// Verhalten
_btnSend.Click += async (_, _) => await SendQuestionAsync();
_chatInput.KeyDown += async (_, e) =>
{ {
if (e.Control && e.KeyCode == Keys.Enter) { e.SuppressKeyPress = true; await SendQuestionAsync(); } if (e.Control && e.KeyCode == Keys.Enter) { e.SuppressKeyPress = true; await SendQuestionAsync(); }
}; };
_btnClearChat.Click += (_, _) => _chatLog.Clear(); btnClearChat.Click += (_, _) => rtbChat.Clear();
_btnRefresh.Click += (_, _) => LoadSignals(); btnDossierRefresh.Click += (_, _) => LoadSignals();
_btnOpen.Click += (_, _) => OpenDossier(_tbSignalId.Text.Trim()); btnDossierOpen.Click += (_, _) => OpenDossier(tbSignalId.Text.Trim());
_grid.SelectionChanged += (_, _) => OpenSelected(); dgvSignals.SelectionChanged += (_, _) => OpenSelectedSignal();
btnReportsRefresh.Click += (_, _) => LoadReports();
dgvReports.SelectionChanged += (_, _) => ShowSelectedReport();
} }
public void Initialize(IServiceProvider services) public void Initialize(IServiceProvider services)
{ {
_dossiers = services.GetRequiredService<DossierService>(); _dossiers = services.GetRequiredService<DossierService>();
_agent = services.GetRequiredService<SupervisorAgent>(); _agent = services.GetRequiredService<SupervisorAgent>();
_reports = services.GetRequiredService<ISupervisorReportRepository>();
LoadSignals(); LoadSignals();
AppendChat("System", "Supervisor bereit. Read-only-Analyse über Entscheidungsjournal, Order-Events, Trades und Logs. " + LoadReports();
"API-Key: POLYTRADER_OPENROUTER_KEY oder Datei openrouter.key.", System.Drawing.Color.Gray); AppendChat("System", "Supervisor bereit (read-only). API-Key: POLYTRADER_OPENROUTER_KEY oder Datei openrouter.key. " +
"MCP-Light für externe Clients: POLYTRADER_MCP_PORT setzen.", System.Drawing.Color.Gray);
} }
// ===== Analyse-Chat ===== // ===== Analyse-Chat =====
@@ -115,40 +57,66 @@ namespace PolyTrader.Modules.Supervisor.Ui
private async System.Threading.Tasks.Task SendQuestionAsync() private async System.Threading.Tasks.Task SendQuestionAsync()
{ {
if (_agent == null) return; if (_agent == null) return;
string question = _chatInput.Text.Trim(); string question = tbChatInput.Text.Trim();
if (question.Length == 0) return; if (question.Length == 0) return;
var profile = cbProfile.SelectedItem as SupervisorProfile ?? SupervisorProfiles.Allgemein;
_chatInput.Text = ""; tbChatInput.Text = "";
_btnSend.Enabled = false; btnSend.Enabled = false;
AppendChat("Du", question, System.Drawing.Color.DarkBlue); AppendChat($"Du ({profile.Name})", question, System.Drawing.Color.DarkBlue);
_status.Text = "Analyse läuft …"; lblStatus.Text = "Analyse läuft …";
var progress = new Progress<string>(msg => AppendChat("Tool", msg, System.Drawing.Color.DarkGoldenrod)); var progress = new Progress<string>(msg => AppendChat("Tool", msg, System.Drawing.Color.DarkGoldenrod));
try try
{ {
string model = tbModel.Text;
var result = await System.Threading.Tasks.Task.Run(() => var result = await System.Threading.Tasks.Task.Run(() =>
_agent.AskAsync(question, _tbModel.Text, progress)); _agent.AskAsync(question, model, progress, profile));
AppendChat("Supervisor", result.Answer, System.Drawing.Color.Black); AppendChat("Supervisor", result.Answer, System.Drawing.Color.Black);
_status.Text = $"Fertig. {result.ToolInvocations.Count} Tool-Aufruf(e), ~{result.PromptTokens + result.CompletionTokens} Tokens."; lblStatus.Text = $"Fertig. {result.ToolInvocations.Count} Tool-Aufruf(e), ~{result.PromptTokens + result.CompletionTokens} Tokens.";
SaveReport(profile, model, question, result);
} }
catch (Exception ex) catch (Exception ex)
{ {
AppendChat("Fehler", ex.Message, System.Drawing.Color.Firebrick); AppendChat("Fehler", ex.Message, System.Drawing.Color.Firebrick);
_status.Text = "Fehler bei der Analyse."; lblStatus.Text = "Fehler bei der Analyse.";
} }
finally finally
{ {
_btnSend.Enabled = true; btnSend.Enabled = true;
} }
} }
private void SaveReport(SupervisorProfile profile, string model, string question, AgentResult result)
{
try
{
var calls = new List<object>();
foreach (var (tool, args, _) in result.ToolInvocations)
calls.Add(new { tool, args });
_reports?.Insert(new SupervisorReport
{
Profile = profile.Name,
Model = model,
Question = question,
Answer = result.Answer,
ToolCallsJson = System.Text.Json.JsonSerializer.Serialize(calls),
ToolCallCount = result.ToolInvocations.Count,
PromptTokens = result.PromptTokens,
CompletionTokens = result.CompletionTokens
});
LoadReports();
}
catch { /* Bericht-Ablage ist Beiwerk Analyse-Ergebnis steht im Chat */ }
}
private void AppendChat(string who, string text, System.Drawing.Color color) private void AppendChat(string who, string text, System.Drawing.Color color)
{ {
if (InvokeRequired) { BeginInvoke(() => AppendChat(who, text, color)); return; } if (InvokeRequired) { BeginInvoke(() => AppendChat(who, text, color)); return; }
_chatLog.SelectionStart = _chatLog.TextLength; rtbChat.SelectionStart = rtbChat.TextLength;
_chatLog.SelectionColor = color; rtbChat.SelectionColor = color;
_chatLog.AppendText($"[{DateTime.Now:HH:mm:ss}] {who}: {text}{Environment.NewLine}{Environment.NewLine}"); rtbChat.AppendText($"[{DateTime.Now:HH:mm:ss}] {who}: {text}{Environment.NewLine}{Environment.NewLine}");
_chatLog.ScrollToCaret(); rtbChat.ScrollToCaret();
} }
// ===== Dossier-Browser ===== // ===== Dossier-Browser =====
@@ -159,20 +127,20 @@ namespace PolyTrader.Modules.Supervisor.Ui
try try
{ {
List<SignalSummary> signals = _dossiers.RecentSignals(200); List<SignalSummary> signals = _dossiers.RecentSignals(200);
_grid.DataSource = signals; dgvSignals.DataSource = signals;
_status.Text = signals.Count == 0 lblStatus.Text = signals.Count == 0
? "Noch keine Journal-Einträge (Entscheidungen entstehen, sobald Signale verarbeitet werden)." ? "Noch keine Journal-Einträge (Entscheidungen entstehen, sobald Signale verarbeitet werden)."
: $"{signals.Count} Signale."; : $"{signals.Count} Signale.";
} }
catch (Exception ex) catch (Exception ex)
{ {
_status.Text = $"Journal nicht lesbar: {ex.Message}"; lblStatus.Text = $"Journal nicht lesbar: {ex.Message}";
} }
} }
private void OpenSelected() private void OpenSelectedSignal()
{ {
if (_grid.CurrentRow?.DataBoundItem is SignalSummary s) if (dgvSignals.CurrentRow?.DataBoundItem is SignalSummary s)
OpenDossier(s.SignalId); OpenDossier(s.SignalId);
} }
@@ -182,13 +150,37 @@ namespace PolyTrader.Modules.Supervisor.Ui
try try
{ {
var dossier = _dossiers.BuildForSignal(signalId); var dossier = _dossiers.BuildForSignal(signalId);
_dossierText.Text = DossierBuilder.ToMarkdown(dossier).Replace("\n", Environment.NewLine); tbDossier.Text = DossierBuilder.ToMarkdown(dossier).Replace("\n", Environment.NewLine);
_tbSignalId.Text = signalId; tbSignalId.Text = signalId;
} }
catch (Exception ex) catch (Exception ex)
{ {
_dossierText.Text = $"Dossier konnte nicht geladen werden: {ex.Message}"; tbDossier.Text = $"Dossier konnte nicht geladen werden: {ex.Message}";
} }
} }
// ===== Berichte =====
private void LoadReports()
{
if (_reports == null) return;
try
{
dgvReports.DataSource = _reports.GetRecent(100);
}
catch (Exception ex)
{
lblStatus.Text = $"Berichte nicht lesbar (sup_-Migration angewendet?): {ex.Message}";
}
}
private void ShowSelectedReport()
{
if (dgvReports.CurrentRow?.DataBoundItem is SupervisorReport r)
tbReport.Text =
$"[{r.CreatedAt:dd.MM.yyyy HH:mm}] Profil {r.Profile} · Modell {r.Model} · {r.ToolCallCount} Tool-Aufrufe{Environment.NewLine}{Environment.NewLine}" +
$"FRAGE:{Environment.NewLine}{r.Question}{Environment.NewLine}{Environment.NewLine}" +
$"ANTWORT:{Environment.NewLine}{r.Answer.Replace("\n", Environment.NewLine)}";
}
} }
} }
+89
View File
@@ -0,0 +1,89 @@
using System.Text.Json;
using PolyTrader.Modules.Supervisor.Agent;
using PolyTrader.Modules.Supervisor.Mcp;
using Xunit;
namespace PolyTrader.Tests
{
/// <summary>
/// Sicherheitsnetz für MCP-Light (S-4): JSON-RPC-Handling (initialize, tools/list, tools/call,
/// Notifications, Fehlerfälle) über die read-only Tool-Registry — pur, ohne HTTP.
/// </summary>
public class McpJsonRpcTests
{
private static SupervisorToolRegistry Registry()
{
var reg = new SupervisorToolRegistry();
reg.Register(new SupervisorTool("echo", "Echo-Tool",
"""{"type":"object","properties":{"text":{"type":"string"}}}""",
args => "ECHO:" + (SupervisorToolRegistry.GetString(args, "text") ?? "")));
return reg;
}
[Fact]
public void Initialize_returns_protocol_and_serverinfo()
{
string? resp = McpJsonRpc.Handle(
"""{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26"}}""",
Registry());
Assert.NotNull(resp);
using var doc = JsonDocument.Parse(resp!);
var result = doc.RootElement.GetProperty("result");
Assert.Equal(McpJsonRpc.ProtocolVersion, result.GetProperty("protocolVersion").GetString());
Assert.Equal(McpJsonRpc.ServerName, result.GetProperty("serverInfo").GetProperty("name").GetString());
Assert.True(result.GetProperty("capabilities").TryGetProperty("tools", out _));
}
[Fact]
public void ToolsList_exposes_registry_tools_with_schema()
{
string? resp = McpJsonRpc.Handle("""{"jsonrpc":"2.0","id":2,"method":"tools/list"}""", Registry());
using var doc = JsonDocument.Parse(resp!);
var tools = doc.RootElement.GetProperty("result").GetProperty("tools");
Assert.Equal(1, tools.GetArrayLength());
Assert.Equal("echo", tools[0].GetProperty("name").GetString());
Assert.Equal("object", tools[0].GetProperty("inputSchema").GetProperty("type").GetString());
}
[Fact]
public void ToolsCall_executes_and_wraps_result_as_text_content()
{
string? resp = McpJsonRpc.Handle(
"""{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo","arguments":{"text":"hallo"}}}""",
Registry());
using var doc = JsonDocument.Parse(resp!);
var result = doc.RootElement.GetProperty("result");
Assert.Equal("ECHO:hallo", result.GetProperty("content")[0].GetProperty("text").GetString());
Assert.False(result.GetProperty("isError").GetBoolean());
}
[Fact]
public void ToolsCall_unknown_tool_sets_isError()
{
string? resp = McpJsonRpc.Handle(
"""{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"nix","arguments":{}}}""",
Registry());
using var doc = JsonDocument.Parse(resp!);
Assert.True(doc.RootElement.GetProperty("result").GetProperty("isError").GetBoolean());
}
[Fact]
public void Notification_returns_null_and_unknown_method_returns_error()
{
Assert.Null(McpJsonRpc.Handle("""{"jsonrpc":"2.0","method":"notifications/initialized"}""", Registry()));
string? resp = McpJsonRpc.Handle("""{"jsonrpc":"2.0","id":5,"method":"gibtsnicht"}""", Registry());
using var doc = JsonDocument.Parse(resp!);
Assert.Equal(-32601, doc.RootElement.GetProperty("error").GetProperty("code").GetInt32());
}
[Fact]
public void Parse_error_returns_minus32700()
{
string? resp = McpJsonRpc.Handle("{kaputt", Registry());
using var doc = JsonDocument.Parse(resp!);
Assert.Equal(-32700, doc.RootElement.GetProperty("error").GetProperty("code").GetInt32());
}
}
}
@@ -0,0 +1,69 @@
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using PolyTrader.Modules.Supervisor.Agent;
using PolyTrader.Modules.Supervisor.Mcp;
using PolyTraderSharp.Services;
using Xunit;
namespace PolyTrader.Tests
{
/// <summary>End-to-End-Test des MCP-Light-HTTP-Hosts (echter HttpListener auf 127.0.0.1).</summary>
public class McpLightServerTests
{
[Fact]
public async Task Server_answers_initialize_and_tools_call_over_http()
{
int port = 52000 + new Random().Next(1000, 9000);
Environment.SetEnvironmentVariable("POLYTRADER_MCP_PORT", port.ToString());
try
{
var reg = new SupervisorToolRegistry();
reg.Register(new SupervisorTool("echo", "Echo",
"""{"type":"object","properties":{"text":{"type":"string"}}}""",
args => "ECHO:" + (SupervisorToolRegistry.GetString(args, "text") ?? "")));
var server = new McpLightServer(reg, new TerminalLogger());
await server.StartAsync(CancellationToken.None);
await Task.Delay(300); // Listener-Start abwarten
using var http = new HttpClient();
string url = $"http://127.0.0.1:{port}/mcp/";
// initialize
var initResp = await http.PostAsync(url, new StringContent(
"""{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}""", Encoding.UTF8, "application/json"));
Assert.True(initResp.IsSuccessStatusCode);
using (var doc = JsonDocument.Parse(await initResp.Content.ReadAsStringAsync()))
Assert.Equal(McpJsonRpc.ServerName,
doc.RootElement.GetProperty("result").GetProperty("serverInfo").GetProperty("name").GetString());
// tools/call
var callResp = await http.PostAsync(url, new StringContent(
"""{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"echo","arguments":{"text":"mcp"}}}""",
Encoding.UTF8, "application/json"));
using (var doc = JsonDocument.Parse(await callResp.Content.ReadAsStringAsync()))
Assert.Equal("ECHO:mcp",
doc.RootElement.GetProperty("result").GetProperty("content")[0].GetProperty("text").GetString());
// Notification -> 202
var notifyResp = await http.PostAsync(url, new StringContent(
"""{"jsonrpc":"2.0","method":"notifications/initialized"}""", Encoding.UTF8, "application/json"));
Assert.Equal(202, (int)notifyResp.StatusCode);
// GET -> 405 (kein SSE-Stream)
var getResp = await http.GetAsync(url);
Assert.Equal(405, (int)getResp.StatusCode);
await server.StopAsync(CancellationToken.None);
}
finally
{
Environment.SetEnvironmentVariable("POLYTRADER_MCP_PORT", null);
}
}
}
}
@@ -51,6 +51,7 @@ namespace PolyTrader.Tests
{ {
private readonly Queue<ChatResponse> _script; private readonly Queue<ChatResponse> _script;
public List<IReadOnlyList<ChatMessage>> Requests { get; } = new(); public List<IReadOnlyList<ChatMessage>> Requests { get; } = new();
public List<IReadOnlyList<SupervisorTool>> OfferedTools { get; } = new();
public ScriptedChatClient(params ChatResponse[] script) => _script = new Queue<ChatResponse>(script); public ScriptedChatClient(params ChatResponse[] script) => _script = new Queue<ChatResponse>(script);
@@ -58,6 +59,7 @@ namespace PolyTrader.Tests
IReadOnlyList<SupervisorTool> tools, CancellationToken ct) IReadOnlyList<SupervisorTool> tools, CancellationToken ct)
{ {
Requests.Add(new List<ChatMessage>(messages)); Requests.Add(new List<ChatMessage>(messages));
OfferedTools.Add(new List<SupervisorTool>(tools));
return Task.FromResult(_script.Count > 0 ? _script.Dequeue() : new ChatResponse { Content = "leer" }); return Task.FromResult(_script.Count > 0 ? _script.Dequeue() : new ChatResponse { Content = "leer" });
} }
} }
@@ -154,5 +156,32 @@ namespace PolyTrader.Tests
Assert.Contains("PolyTrader", ctx); Assert.Contains("PolyTrader", ctx);
Assert.Contains("read-only", ctx); Assert.Contains("read-only", ctx);
} }
// ----- Profile (S-3) -----
[Fact]
public async Task Technik_profile_filters_tools_and_extends_prompt()
{
var reg = RegistryWithEcho();
reg.Register(new SupervisorTool("read_logs", "Logs", """{"type":"object","properties":{}}""", _ => "logs"));
var chat = new ScriptedChatClient(new ChatResponse { Content = "ok" });
var agent = new SupervisorAgent(chat, reg);
await agent.AskAsync("check", profile: SupervisorProfiles.Technik);
// Prompt trägt den Profil-Fokus; dem Modell wurde NUR das Technik-Subset angeboten
// (Registry hat echo/boom/read_logs → im Technik-Filter ist davon nur read_logs).
Assert.Contains("FOKUS TECHNIK-SUPERVISOR", chat.Requests[0][0].Content);
Assert.Single(chat.OfferedTools[0]);
Assert.Equal("read_logs", chat.OfferedTools[0][0].Name);
}
[Fact]
public void Profiles_lookup_is_case_insensitive_with_fallback()
{
Assert.Equal("Technik", SupervisorProfiles.ByName("technik").Name);
Assert.Equal("Allgemein", SupervisorProfiles.ByName("gibtsnicht").Name);
Assert.Equal(4, SupervisorProfiles.All.Count);
}
} }
} }