fix: ask_user_choice Short-Poll, Freitext-Option, Stop-Kontext-Fix
- Long-Poll durch Short-Poll ersetzt (MCP-Server pollt jede Sekunde statt einer blockierenden HTTP-Anfrage)
- Expliziter Lambda-Rückgabetyp Task<IResult> für ASP.NET Minimal API Endpoints (behebt 200 empty body Bug)
- Freitext-Option bei Choice-Buttons ("Eigene Antwort…") für freie User-Eingabe
- Stop während ask_user_choice markiert Chain als awaiting_user_choice statt stopped (Kontext bleibt erhalten)
- ask_user_choice System-Prompt-Instruktion breiter formuliert (Quiz, Multiple-Choice etc.)
- Text wird jetzt immer in DB gespeichert, auch bei ask_user_choice
- Key-Check Model auf claude-haiku-4-5 geändert
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
3e0a69e7a4
commit
92ecaf9dac
8 files changed
+184
-66
No files matched your search
@@ -24,16 +24,19 @@ public static class InternalEndpoint
|
||||
public static string InternalToken { get; } = Guid.NewGuid().ToString("N");
|
||||
|
||||
/// <summary>
|
||||
/// Pending user choices – Key ist sessionId, Value wird completed wenn der User klickt.
|
||||
/// Beantwortete User-Choices – Key ist sessionId, Value ist die gewählte Option.
|
||||
/// MCP-Server pollt dieses Dictionary (Short-Poll statt Long-Poll).
|
||||
/// </summary>
|
||||
public static readonly ConcurrentDictionary<string, TaskCompletionSource<string>> PendingChoices = new();
|
||||
public static readonly ConcurrentDictionary<string, string> ChoiceAnswers = new();
|
||||
|
||||
public static void MapInternalEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
app.MapPost("/api/internal/remote-execute", HandleRemoteExecute);
|
||||
app.MapPost("/api/internal/ask-user-choice", HandleAskUserChoice);
|
||||
// WICHTIG: Expliziter Rückgabetyp Task<IResult> nötig! Ohne ihn interpretiert ASP.NET den Lambda als
|
||||
// RequestDelegate (Func<HttpContext,Task>) → IResult-Rückgabe wird ignoriert → 200 empty body.
|
||||
app.MapPost("/api/internal/ask-user-choice", async Task<IResult> (HttpContext ctx) => await HandleAskUserChoice(ctx));
|
||||
// choice-respond ist NICHT unter /api/internal/ weil es vom Frontend aufgerufen wird (normale Auth)
|
||||
app.MapPost("/api/chat/choice-respond", HandleChoiceRespond);
|
||||
app.MapPost("/api/chat/choice-respond", async Task<IResult> (HttpContext ctx) => await HandleChoiceRespond(ctx));
|
||||
app.MapPost("/api/internal/memory-read", HandleMemoryRead);
|
||||
app.MapPost("/api/internal/memory-write", HandleMemoryWrite);
|
||||
app.MapPost("/api/internal/memory-list", HandleMemoryList);
|
||||
@@ -85,8 +88,8 @@ public static class InternalEndpoint
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MCP-Server ruft diesen Endpoint auf und blockiert bis der User eine Auswahl trifft.
|
||||
/// Der TCS wird vorher von AnthropicService erstellt (beim tool_use Event).
|
||||
/// MCP-Server pollt diesen Endpoint bis der User eine Auswahl getroffen hat.
|
||||
/// Gibt 200 mit { choice } zurück wenn beantwortet, 404 wenn noch ausstehend.
|
||||
/// </summary>
|
||||
private static async Task<IResult> HandleAskUserChoice(HttpContext ctx)
|
||||
{
|
||||
@@ -105,34 +108,12 @@ public static class InternalEndpoint
|
||||
if (string.IsNullOrEmpty(sessionId))
|
||||
return Results.BadRequest(new { error = "sessionId erforderlich" });
|
||||
|
||||
// Warte auf den TCS der von AnthropicService erstellt wurde
|
||||
if (!PendingChoices.TryGetValue(sessionId, out var tcs))
|
||||
{
|
||||
// TCS noch nicht erstellt (Race Condition) – kurz warten und nochmal probieren
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
await Task.Delay(500, ctx.RequestAborted);
|
||||
if (PendingChoices.TryGetValue(sessionId, out tcs)) break;
|
||||
}
|
||||
if (tcs == null)
|
||||
return Results.Json(new { error = "Keine ausstehende Auswahl für diese Session" }, statusCode: 404);
|
||||
}
|
||||
|
||||
// Blockieren bis User antwortet (max 5 Minuten)
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ctx.RequestAborted);
|
||||
cts.CancelAfter(TimeSpan.FromMinutes(5));
|
||||
|
||||
try
|
||||
{
|
||||
var choice = await tcs.Task.WaitAsync(cts.Token);
|
||||
PendingChoices.TryRemove(sessionId, out _);
|
||||
// Prüfen ob bereits eine Antwort vorliegt (Short-Poll)
|
||||
if (ChoiceAnswers.TryRemove(sessionId, out var choice))
|
||||
return Results.Ok(new { choice });
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
PendingChoices.TryRemove(sessionId, out _);
|
||||
return Results.Json(new { error = "Timeout – keine Auswahl getroffen" }, statusCode: 408);
|
||||
}
|
||||
|
||||
// Noch keine Antwort – MCP-Server soll erneut pollen
|
||||
return Results.Json(new { status = "pending" }, statusCode: 202);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -142,6 +123,7 @@ public static class InternalEndpoint
|
||||
|
||||
/// <summary>
|
||||
/// Frontend ruft diesen Endpoint auf wenn der User einen Choice-Button klickt (CLI-Modus).
|
||||
/// Speichert die Antwort in ChoiceAnswers – MCP-Server holt sie beim nächsten Poll ab.
|
||||
/// </summary>
|
||||
private static async Task<IResult> HandleChoiceRespond(HttpContext ctx)
|
||||
{
|
||||
@@ -162,13 +144,9 @@ public static class InternalEndpoint
|
||||
if (string.IsNullOrEmpty(choice))
|
||||
return Results.BadRequest(new { error = "choice erforderlich" });
|
||||
|
||||
if (PendingChoices.TryGetValue(sessionId, out var tcs))
|
||||
{
|
||||
tcs.TrySetResult(choice);
|
||||
return Results.Ok(new { success = true });
|
||||
}
|
||||
|
||||
return Results.NotFound(new { error = "Keine ausstehende Auswahl für diese Session" });
|
||||
// Antwort speichern – MCP-Server holt sie beim nächsten Poll ab
|
||||
ChoiceAnswers[sessionId] = choice;
|
||||
return Results.Ok(new { success = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user