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
148 lines
4.7 KiB
C#
148 lines
4.7 KiB
C#
using System.Collections.Generic;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace Gott.Models;
|
|
|
|
public class ChatMessage
|
|
{
|
|
[JsonPropertyName("role")]
|
|
public string Role { get; set; } = "";
|
|
|
|
[JsonPropertyName("content")]
|
|
public JsonElement Content { get; set; }
|
|
|
|
/// <summary>
|
|
/// Extrahiert den reinen Text aus Content (egal ob string oder content-block array).
|
|
/// </summary>
|
|
public string GetTextContent()
|
|
{
|
|
if (Content.ValueKind == JsonValueKind.String)
|
|
return Content.GetString() ?? "";
|
|
|
|
if (Content.ValueKind == JsonValueKind.Array)
|
|
{
|
|
var parts = new List<string>();
|
|
foreach (var block in Content.EnumerateArray())
|
|
{
|
|
if (block.TryGetProperty("type", out var t) && t.GetString() == "text"
|
|
&& block.TryGetProperty("text", out var txt))
|
|
parts.Add(txt.GetString() ?? "");
|
|
}
|
|
return string.Join("\n", parts);
|
|
}
|
|
|
|
return Content.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Prüft ob Datei-Attachments (image/document) enthalten sind.
|
|
/// </summary>
|
|
public bool HasAttachments()
|
|
{
|
|
if (Content.ValueKind != JsonValueKind.Array) return false;
|
|
foreach (var block in Content.EnumerateArray())
|
|
{
|
|
if (block.TryGetProperty("type", out var t))
|
|
{
|
|
var type = t.GetString();
|
|
if (type == "image" || type == "document") return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gibt Attachment-Infos zurück (type + media_type).
|
|
/// </summary>
|
|
public List<AttachmentInfo> GetAttachments()
|
|
{
|
|
var result = new List<AttachmentInfo>();
|
|
if (Content.ValueKind != JsonValueKind.Array) return result;
|
|
foreach (var block in Content.EnumerateArray())
|
|
{
|
|
if (block.TryGetProperty("type", out var t))
|
|
{
|
|
var type = t.GetString();
|
|
if (type == "image" || type == "document")
|
|
{
|
|
var mediaType = "";
|
|
if (block.TryGetProperty("source", out var src) && src.TryGetProperty("media_type", out var mt))
|
|
mediaType = mt.GetString() ?? "";
|
|
result.Add(new AttachmentInfo { Type = type, MediaType = mediaType });
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gibt vollständige Attachment-Blöcke zurück (inkl. Base64-Daten).
|
|
/// </summary>
|
|
public List<AttachmentBlock> GetAttachmentBlocks()
|
|
{
|
|
var result = new List<AttachmentBlock>();
|
|
if (Content.ValueKind != JsonValueKind.Array) return result;
|
|
foreach (var block in Content.EnumerateArray())
|
|
{
|
|
if (block.TryGetProperty("type", out var t))
|
|
{
|
|
var type = t.GetString();
|
|
if (type == "image" || type == "document")
|
|
{
|
|
var mediaType = "";
|
|
var data = "";
|
|
var fileName = "";
|
|
if (block.TryGetProperty("source", out var src))
|
|
{
|
|
if (src.TryGetProperty("media_type", out var mt))
|
|
mediaType = mt.GetString() ?? "";
|
|
if (src.TryGetProperty("data", out var d))
|
|
data = d.GetString() ?? "";
|
|
}
|
|
if (block.TryGetProperty("_file_name", out var fn))
|
|
fileName = fn.GetString() ?? "";
|
|
result.Add(new AttachmentBlock { Type = type, MediaType = mediaType, Data = data, FileName = fileName });
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
public class AttachmentBlock
|
|
{
|
|
public string Type { get; set; } = "";
|
|
public string MediaType { get; set; } = "";
|
|
public string Data { get; set; } = "";
|
|
public string FileName { get; set; } = "";
|
|
}
|
|
|
|
public class AttachmentInfo
|
|
{
|
|
public string Type { get; set; } = "";
|
|
public string MediaType { get; set; } = "";
|
|
}
|
|
|
|
public class ChatRequest
|
|
{
|
|
[JsonPropertyName("content")]
|
|
public JsonElement Content { get; set; }
|
|
|
|
[JsonPropertyName("sessionId")]
|
|
public string SessionId { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// Wenn gesetzt: Dies ist eine Antwort auf ein ask_user_choice Tool.
|
|
/// Enthält die tool_use_id des offenen ask_user_choice-Calls.
|
|
/// </summary>
|
|
[JsonPropertyName("choiceToolCallId")]
|
|
public string ChoiceToolCallId { get; set; }
|
|
|
|
/// <summary>
|
|
/// Der gewählte Text aus den Auswahloptionen.
|
|
/// </summary>
|
|
[JsonPropertyName("choiceResult")]
|
|
public string ChoiceResult { get; set; }
|
|
}
|