Supervisor S-2: OpenRouter-Agent, read-only Tool-Registry, Analyse-Chat

Der KI-Analyse-Agent (docs/konzepte/KONZEPT-Modul-Supervisor.md, Phase S-2):
- SupervisorToolRegistry (transport-agnostisch, spaeter auch MCP-Light): 7 read-only-Tools -
  query_decisions (inkl. Rejects+ReasonCodes), query_order_events, query_trades, get_dossier
  (Markdown-Kette), read_logs (JSONL je Tag, CID-Filter), get_kpis (TradeAnalytics),
  get_architecture_context. Ausfuehrung fehlertolerant (Exception -> Fehlertext, wirft nie).
  KEIN Tool kann handeln/schreiben.
- OpenRouterClient (IChatCompletionClient): OpenAI-kompatibles Chat-Completions-Schema inkl.
  Function-Calling; Request-Bau + Response-Parsing pur/testbar. API-Key GETRENNT vom Trading:
  env POLYTRADER_OPENROUTER_KEY oder gitignorierte openrouter.key (in .gitignore aufgenommen).
- SupervisorAgent: Function-Calling-Loop (max 8 Iterationen), System-Prompt = Arbeitsanweisung +
  eingebettetes Architektur-Kontext-Dokument (Context/ArchitectureContext.md, EmbeddedResource,
  mit dem Code versioniert - beschreibt Entscheidungswege, ReasonCodes, Leiter-Mechanik, Eigenheiten).
  Tool-Aufrufe werden gesammelt und in der UI transparent angezeigt.
- SupervisorMainForm: neuer Tab 'Analyse' (Chat, Modellwahl default openrouter/auto, Tool-Aufrufe
  live im Verlauf, Token-Zaehler) neben dem Dossiers-Tab.
- Sicherheitskonzept: OpenRouter als bewusst freigegebener Egress dokumentiert (nur Tool-Ergebnisse,
  nie Secrets; Spend-Limit je Key empfohlen).

Tests: +7 (Registry-Ausfuehrung/Fehler, Agent-Loop mit Tool-Rueckfluss, Iterationsgrenze,
Request-Body/Response-Parsing, eingebetteter Kontext). Build 0 Fehler, 351 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-17 18:06:28 +02:00
co-authored by Claude Opus 4.8
parent c75a958e36
commit 5d732277b2
13 changed files with 941 additions and 17 deletions
@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace PolyTrader.Modules.Supervisor.Agent
{
/// <summary>Ergebnis einer Agenten-Anfrage inkl. transparenter Tool-Aufruf-Historie.</summary>
public sealed class AgentResult
{
public string Answer { get; init; } = string.Empty;
public List<(string Tool, string Arguments, string Result)> ToolInvocations { get; init; } = new();
public int PromptTokens { get; init; }
public int CompletionTokens { get; init; }
}
/// <summary>
/// Der Analyse-Agent (S-2): Function-Calling-Loop gegen einen <see cref="IChatCompletionClient"/>
/// mit der read-only <see cref="SupervisorToolRegistry"/>. System-Kontext = Arbeitsanweisung +
/// Architektur-Dokument. Harte Iterationsgrenze gegen Endlosschleifen; jeder Tool-Aufruf wird
/// festgehalten (Nachvollziehbarkeit in der UI).
/// </summary>
public sealed class SupervisorAgent
{
public const int MaxIterations = 8;
public const string DefaultModel = "openrouter/auto";
private readonly IChatCompletionClient _chat;
private readonly SupervisorToolRegistry _tools;
public SupervisorAgent(IChatCompletionClient chat, SupervisorToolRegistry tools)
{
_chat = chat;
_tools = tools;
}
private static string SystemPrompt() =>
"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();
/// <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)
{
var messages = new List<ChatMessage>
{
ChatMessage.System(SystemPrompt()),
ChatMessage.User(question)
};
var invocations = new List<(string, string, string)>();
int promptTokens = 0, completionTokens = 0;
string usedModel = string.IsNullOrWhiteSpace(model) ? DefaultModel : model.Trim();
for (int i = 0; i < MaxIterations; i++)
{
ct.ThrowIfCancellationRequested();
var response = await _chat.CompleteAsync(usedModel, messages, _tools.Tools, ct);
promptTokens += response.PromptTokens;
completionTokens += response.CompletionTokens;
if (response.ToolCalls.Count == 0)
{
return new AgentResult
{
Answer = response.Content ?? "(keine Antwort)",
ToolInvocations = invocations,
PromptTokens = promptTokens,
CompletionTokens = completionTokens
};
}
messages.Add(ChatMessage.Assistant(response.Content, response.ToolCalls));
foreach (var call in response.ToolCalls)
{
progress?.Report($"🔧 {call.Name}({call.ArgumentsJson})");
string result = _tools.Execute(call.Name, call.ArgumentsJson);
invocations.Add((call.Name, call.ArgumentsJson, result));
messages.Add(ChatMessage.ToolResult(call.Id, result));
}
}
return new AgentResult
{
Answer = "Abbruch: maximale Tool-Iterationen erreicht (Frage ggf. eingrenzen).",
ToolInvocations = invocations,
PromptTokens = promptTokens,
CompletionTokens = completionTokens
};
}
}
}