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:
co-authored by
Claude Opus 4.8
parent
5d732277b2
commit
b7b2a141e3
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -34,22 +35,36 @@ namespace PolyTrader.Modules.Supervisor.Agent
|
||||
_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 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 " +
|
||||
"(SignalIds, Zeiten, Preise, ReasonCodes). Antworte auf Deutsch, präzise und mit klarer Schlussfolgerung.\n\n" +
|
||||
"=== ARCHITEKTUR-KONTEXT ===\n" + ArchitectureContext.Load();
|
||||
"(SignalIds, Zeiten, Preise, ReasonCodes). Antworte auf Deutsch, präzise und mit klarer Schlussfolgerung.";
|
||||
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>
|
||||
/// Beantwortet eine Analyse-Frage. <paramref name="progress"/> meldet Tool-Aufrufe live an die UI.
|
||||
/// </summary>
|
||||
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>
|
||||
{
|
||||
ChatMessage.System(SystemPrompt()),
|
||||
ChatMessage.System(SystemPrompt(activeProfile)),
|
||||
ChatMessage.User(question)
|
||||
};
|
||||
var invocations = new List<(string, string, string)>();
|
||||
@@ -59,7 +74,7 @@ namespace PolyTrader.Modules.Supervisor.Agent
|
||||
for (int i = 0; i < MaxIterations; i++)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var response = await _chat.CompleteAsync(usedModel, messages, _tools.Tools, ct);
|
||||
var response = await _chat.CompleteAsync(usedModel, messages, activeTools, ct);
|
||||
promptTokens += response.PromptTokens;
|
||||
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 { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+80
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+58
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+77
@@ -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" />
|
||||
</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. -->
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using PolyTrader.Core.Modularity;
|
||||
@@ -34,7 +35,15 @@ namespace PolyTrader.Modules.Supervisor
|
||||
new Agent.OpenRouterClient(new System.Net.Http.HttpClient { Timeout = TimeSpan.FromMinutes(3) }));
|
||||
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)
|
||||
|
||||
@@ -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 PolyTrader.Core.Analytics;
|
||||
using PolyTrader.Modules.Supervisor.Agent;
|
||||
using PolyTrader.Modules.Supervisor.Persistence;
|
||||
using PolyTrader.Modules.Supervisor.Services;
|
||||
|
||||
namespace PolyTrader.Modules.Supervisor.Ui
|
||||
{
|
||||
/// <summary>
|
||||
/// Hauptfenster des Supervisor-Moduls: Tab „Analyse" (Chat mit dem read-only-Agenten,
|
||||
/// Tool-Aufrufe transparent im Verlauf) und Tab „Dossiers" (Signal-Browser mit Markdown-Dossier).
|
||||
/// Code-only konstruiert (Muster RF-Modul); DB-/API-Zugriffe defensiv.
|
||||
/// Hauptfenster des Supervisor-Moduls: Tab „Analyse" (Chat mit Profil-/Modellwahl, Tool-Aufrufe
|
||||
/// transparent), Tab „Dossiers" (Signal-Browser), Tab „Berichte" (gespeicherte Analysen).
|
||||
/// Layout im Designer (SupervisorMainForm.Designer.cs), Verhalten/Daten hier.
|
||||
/// </summary>
|
||||
public sealed class SupervisorMainForm : Form
|
||||
public partial class SupervisorMainForm : Form
|
||||
{
|
||||
private DossierService? _dossiers;
|
||||
private SupervisorAgent? _agent;
|
||||
|
||||
// ----- 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 = "" };
|
||||
private ISupervisorReportRepository? _reports;
|
||||
|
||||
public SupervisorMainForm()
|
||||
{
|
||||
Text = "Supervisor";
|
||||
Width = 1250;
|
||||
Height = 720;
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
InitializeComponent();
|
||||
|
||||
var tabs = new TabControl { Dock = DockStyle.Fill };
|
||||
foreach (var profile in SupervisorProfiles.All)
|
||||
cbProfile.Items.Add(profile);
|
||||
cbProfile.SelectedIndex = 0;
|
||||
|
||||
// --- Tab Analyse ---
|
||||
var tabAnalyse = new TabPage("Analyse");
|
||||
_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) =>
|
||||
btnSend.Click += async (_, _) => await SendQuestionAsync();
|
||||
tbChatInput.KeyDown += async (_, e) =>
|
||||
{
|
||||
if (e.Control && e.KeyCode == Keys.Enter) { e.SuppressKeyPress = true; await SendQuestionAsync(); }
|
||||
};
|
||||
_btnClearChat.Click += (_, _) => _chatLog.Clear();
|
||||
_btnRefresh.Click += (_, _) => LoadSignals();
|
||||
_btnOpen.Click += (_, _) => OpenDossier(_tbSignalId.Text.Trim());
|
||||
_grid.SelectionChanged += (_, _) => OpenSelected();
|
||||
btnClearChat.Click += (_, _) => rtbChat.Clear();
|
||||
btnDossierRefresh.Click += (_, _) => LoadSignals();
|
||||
btnDossierOpen.Click += (_, _) => OpenDossier(tbSignalId.Text.Trim());
|
||||
dgvSignals.SelectionChanged += (_, _) => OpenSelectedSignal();
|
||||
btnReportsRefresh.Click += (_, _) => LoadReports();
|
||||
dgvReports.SelectionChanged += (_, _) => ShowSelectedReport();
|
||||
}
|
||||
|
||||
public void Initialize(IServiceProvider services)
|
||||
{
|
||||
_dossiers = services.GetRequiredService<DossierService>();
|
||||
_agent = services.GetRequiredService<SupervisorAgent>();
|
||||
_reports = services.GetRequiredService<ISupervisorReportRepository>();
|
||||
LoadSignals();
|
||||
AppendChat("System", "Supervisor bereit. Read-only-Analyse über Entscheidungsjournal, Order-Events, Trades und Logs. " +
|
||||
"API-Key: POLYTRADER_OPENROUTER_KEY oder Datei openrouter.key.", System.Drawing.Color.Gray);
|
||||
LoadReports();
|
||||
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 =====
|
||||
@@ -115,40 +57,66 @@ namespace PolyTrader.Modules.Supervisor.Ui
|
||||
private async System.Threading.Tasks.Task SendQuestionAsync()
|
||||
{
|
||||
if (_agent == null) return;
|
||||
string question = _chatInput.Text.Trim();
|
||||
string question = tbChatInput.Text.Trim();
|
||||
if (question.Length == 0) return;
|
||||
var profile = cbProfile.SelectedItem as SupervisorProfile ?? SupervisorProfiles.Allgemein;
|
||||
|
||||
_chatInput.Text = "";
|
||||
_btnSend.Enabled = false;
|
||||
AppendChat("Du", question, System.Drawing.Color.DarkBlue);
|
||||
_status.Text = "Analyse läuft …";
|
||||
tbChatInput.Text = "";
|
||||
btnSend.Enabled = false;
|
||||
AppendChat($"Du ({profile.Name})", question, System.Drawing.Color.DarkBlue);
|
||||
lblStatus.Text = "Analyse läuft …";
|
||||
|
||||
var progress = new Progress<string>(msg => AppendChat("Tool", msg, System.Drawing.Color.DarkGoldenrod));
|
||||
try
|
||||
{
|
||||
string model = tbModel.Text;
|
||||
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);
|
||||
_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)
|
||||
{
|
||||
AppendChat("Fehler", ex.Message, System.Drawing.Color.Firebrick);
|
||||
_status.Text = "Fehler bei der Analyse.";
|
||||
lblStatus.Text = "Fehler bei der Analyse.";
|
||||
}
|
||||
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)
|
||||
{
|
||||
if (InvokeRequired) { BeginInvoke(() => AppendChat(who, text, color)); return; }
|
||||
_chatLog.SelectionStart = _chatLog.TextLength;
|
||||
_chatLog.SelectionColor = color;
|
||||
_chatLog.AppendText($"[{DateTime.Now:HH:mm:ss}] {who}: {text}{Environment.NewLine}{Environment.NewLine}");
|
||||
_chatLog.ScrollToCaret();
|
||||
rtbChat.SelectionStart = rtbChat.TextLength;
|
||||
rtbChat.SelectionColor = color;
|
||||
rtbChat.AppendText($"[{DateTime.Now:HH:mm:ss}] {who}: {text}{Environment.NewLine}{Environment.NewLine}");
|
||||
rtbChat.ScrollToCaret();
|
||||
}
|
||||
|
||||
// ===== Dossier-Browser =====
|
||||
@@ -159,20 +127,20 @@ namespace PolyTrader.Modules.Supervisor.Ui
|
||||
try
|
||||
{
|
||||
List<SignalSummary> signals = _dossiers.RecentSignals(200);
|
||||
_grid.DataSource = signals;
|
||||
_status.Text = signals.Count == 0
|
||||
dgvSignals.DataSource = signals;
|
||||
lblStatus.Text = signals.Count == 0
|
||||
? "Noch keine Journal-Einträge (Entscheidungen entstehen, sobald Signale verarbeitet werden)."
|
||||
: $"{signals.Count} Signale.";
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -182,13 +150,37 @@ namespace PolyTrader.Modules.Supervisor.Ui
|
||||
try
|
||||
{
|
||||
var dossier = _dossiers.BuildForSignal(signalId);
|
||||
_dossierText.Text = DossierBuilder.ToMarkdown(dossier).Replace("\n", Environment.NewLine);
|
||||
_tbSignalId.Text = signalId;
|
||||
tbDossier.Text = DossierBuilder.ToMarkdown(dossier).Replace("\n", Environment.NewLine);
|
||||
tbSignalId.Text = signalId;
|
||||
}
|
||||
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)}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
public List<IReadOnlyList<ChatMessage>> Requests { get; } = new();
|
||||
public List<IReadOnlyList<SupervisorTool>> OfferedTools { get; } = new();
|
||||
|
||||
public ScriptedChatClient(params ChatResponse[] script) => _script = new Queue<ChatResponse>(script);
|
||||
|
||||
@@ -58,6 +59,7 @@ namespace PolyTrader.Tests
|
||||
IReadOnlyList<SupervisorTool> tools, CancellationToken ct)
|
||||
{
|
||||
Requests.Add(new List<ChatMessage>(messages));
|
||||
OfferedTools.Add(new List<SupervisorTool>(tools));
|
||||
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("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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user