feat: ask_user_choice Tool – interaktive Auswahlmöglichkeiten im Chat

Neues Tool 'ask_user_choice' analog zum AskUserQuestion aus dem Claude Agent SDK.
Claude kann dem User Multiple-Choice-Fragen stellen, wenn es mehrere sinnvolle
Wege gibt. Der User wählt per Button, Claude arbeitet mit der Antwort weiter.

Backend:
- ToolService: ask_user_choice Tool-Definition (question + choices[2-5])
- AnthropicService/ProcessStream: Tool wird NICHT ausgeführt, sondern als
  SSE-Event 'user_choice' an den Client gesendet. stopReason wird auf
  'awaiting_user_choice' gesetzt (mit Guard gegen Überschreibung durch
  message_delta)
- ChatEndpoint: Erkennung von Choice-Responses (choiceToolCallId + choiceResult),
  tool_result wird in die korrekte Chain gespeichert (wo der tool_use liegt),
  toolChoice wird auf 'auto' gesetzt damit Claude frei antworten kann
- ChatService: FindChainWithToolCall() zum Auffinden der richtigen Chain,
  BuildHistoryFromDb() erweitert um Choice-Chain-Erkennung (wenn letzte Chain
  leer/nur tool_result → vorletzte Chain vollständig rekonstruieren)
- ChatModels: ChatRequest um ChoiceToolCallId + ChoiceResult erweitert

Frontend:
- api.js: apiChatChoice() für Choice-Responses
- chat.js: handleStreamEvent() als shared Handler extrahiert (sendMessage +
  handleChoiceClick nutzen denselben Code), handleChoiceClick() startet neuen
  Stream nach Button-Klick
- ui.js: addChoiceBubble() (live/interaktiv) + addChoiceBubbleStatic() (History)
- history.js: Choice-Bubbles in _renderMessagesIntoFragment + renderMessages,
  Voraus-Suche nach zugehörigem tool_result, unbeantwortet = klickbare Buttons
- chat.css: Styling für Choice-Bubbles (Primary-Color-Outline-Buttons, Hover,
  Selected/Unselected States)

Robustheit:
- Backend-Fallback wenn Claude choices als String statt Array sendet
- Frontend-Guard: Array.isArray(choices) an allen 4 Render-Stellen
- Page-Refresh mit offener Choice → aktive Buttons werden korrekt gerendert
This commit is contained in:
Meleki committed 2026-03-07 11:30:55 +01:00
1 parent 72347ed2f5
commit 5f3e308975
10 files changed
+628 -140

No files matched your search

+30
View File
@@ -15,9 +15,13 @@ namespace Gott.Services;
public class ToolService
{
public const string AskUserChoiceToolName = "ask_user_choice";
public object[] GetToolDefinitions(bool includeFileTools = false, bool includeMemory = false)
{
var allTools = new List<object>();
// ask_user_choice ist immer verfügbar wenn Tools aktiv sind
allTools.Add(GetAskUserChoiceTool());
if (includeFileTools)
allTools.AddRange(GetFileTools());
if (includeMemory)
@@ -25,6 +29,32 @@ public class ToolService
return allTools.ToArray();
}
private object GetAskUserChoiceTool()
{
return new
{
name = AskUserChoiceToolName,
description = "Stellt dem User eine Frage mit Auswahlmöglichkeiten, wenn du unsicher bist oder es mehrere sinnvolle Wege gibt. Der User wählt eine Option und du arbeitest mit seiner Antwort weiter. Nutze dies sparsam – nur wenn die Entscheidung wirklich vom User getroffen werden sollte.",
input_schema = new
{
type = "object",
properties = new Dictionary<string, object>
{
["question"] = new { type = "string", description = "Die Frage an den User" },
["choices"] = new
{
type = "array",
description = "Liste der Auswahlmöglichkeiten (2-5 Optionen). MUSS ein JSON-Array mit Strings sein, z.B. [\"Option A\", \"Option B\", \"Option C\"]. Niemals einen einzelnen String verwenden.",
items = new { type = "string", description = "Eine einzelne Auswahlmöglichkeit als kurzer, prägnanter Text" },
minItems = 2,
maxItems = 5
}
},
required = new[] { "question", "choices" }
}
};
}
private object[] GetFileTools()
{
return new object[]