feat: MCP-Tools ask_user_choice + memory_read/write/list, Text-Marker entfernt

MCP-Server erweitert um ask_user_choice (Long-Poll), memory_read, memory_write
und memory_list Tools. MCP-Config wird jetzt immer generiert (nicht nur bei
Remote-Verbindung). InternalEndpoint um PendingChoices + choice-respond Endpoint
ergänzt. Frontend differenziert CLI-Modus (choice-respond API) vs. API-Modus.
Gesamter :::ASK_USER_CHOICE::: Text-Marker-Mechanismus aus ClaudeCliService,
AnthropicService, markdown.js und stream-ui.js entfernt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
MelekiandClaude Opus 4.6 committed 2026-03-18 09:54:30 +01:00
1 parent 3b068a7866
commit 3e0a69e7a4
10 files changed
+493 -319

No files matched your search

+46 -63
View File
@@ -9,6 +9,7 @@ using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Gott.Endpoints;
using Gott.Models;
using Microsoft.AspNetCore.Http;
@@ -1737,7 +1738,7 @@ public class AnthropicService
/// Generiert eine temporäre MCP-Config-Datei für die Claude CLI.
/// Enthält den MCP-Server für remote_execute.
/// </summary>
private string GenerateMcpConfig(string sessionId)
private string GenerateMcpConfig(string sessionId, string agentId = null, bool hasRemote = false)
{
// Port aus Konfiguration oder Default
var port = 19000;
@@ -1746,12 +1747,20 @@ public class AnthropicService
// Fallback: Quell-Verzeichnis prüfen (für Dev-Modus)
if (!File.Exists(scriptPath))
{
// wwwroot ist typischerweise /app/gott/wwwroot → eine Ebene hoch = Projekt-Root
var projectRoot = Path.GetDirectoryName(AppPaths.WwwRoot) ?? AppContext.BaseDirectory;
var srcPath = Path.Combine(projectRoot, "scripts", "mcp-remote-execute.py");
if (File.Exists(srcPath)) scriptPath = srcPath;
}
var env = new Dictionary<string, string>
{
["GOTT_API_URL"] = $"http://localhost:{port}",
["GOTT_SESSION_ID"] = sessionId ?? "",
["GOTT_AUTH_TOKEN"] = Endpoints.InternalEndpoint.InternalToken,
["GOTT_AGENT_ID"] = agentId ?? "",
["GOTT_HAS_REMOTE"] = hasRemote ? "true" : "false"
};
var config = new
{
mcpServers = new Dictionary<string, object>
@@ -1760,12 +1769,7 @@ public class AnthropicService
{
command = "python3",
args = new[] { scriptPath },
env = new Dictionary<string, string>
{
["GOTT_API_URL"] = $"http://localhost:{port}",
["GOTT_SESSION_ID"] = sessionId,
["GOTT_AUTH_TOKEN"] = Endpoints.InternalEndpoint.InternalToken
}
env
}
}
};
@@ -1929,6 +1933,7 @@ public class AnthropicService
bool planMode = false,
bool webSearch = false,
string sessionId = null,
string agentId = null,
RemoteConnectionManager remoteMgr = null,
CancellationToken workCt = default,
CancellationToken sseCt = default)
@@ -1955,11 +1960,9 @@ public class AnthropicService
try
{
// MCP-Config für remote_execute generieren (wenn Remote-Verbindung vorhanden)
if (remoteMgr != null && !string.IsNullOrEmpty(sessionId) && remoteMgr.GetBySessionId(sessionId) != null)
{
mcpConfigPath = GenerateMcpConfig(sessionId);
}
// MCP-Config generieren (immer – für ask_user_choice, optional Memory-Tools + remote_execute)
var hasRemote = remoteMgr != null && !string.IsNullOrEmpty(sessionId) && remoteMgr.GetBySessionId(sessionId) != null;
mcpConfigPath = GenerateMcpConfig(sessionId, agentId, hasRemote);
// System-Prompt als Text extrahieren
var systemText = new StringBuilder();
@@ -1974,19 +1977,6 @@ public class AnthropicService
}
}
// ask_user_choice Instruktion anhängen
if (allowUserChoice)
{
systemText.AppendLine();
systemText.AppendLine(@"## Rückfragen
Wenn du dem User eine Frage mit Auswahlmöglichkeiten stellen möchtest (2-5 Optionen), gib EXAKT diesen Block aus, auf eigenen Zeilen, ohne weiteren Text drumherum:
:::ASK_USER_CHOICE:::
{""question"": ""Deine Frage"", ""choices"": [""Option A"", ""Option B""]}
:::END_ASK_USER_CHOICE:::
Danach STOPPE die Textgenerierung sofort. Nutze dies sparsam — nur wenn die Entscheidung wirklich vom User getroffen werden muss.");
}
// Letzte User-Message als Prompt extrahieren
var prompt = ExtractLastUserText(messages) ?? "";
@@ -2060,6 +2050,36 @@ Danach STOPPE die Textgenerierung sofort. Nutze dies sparsam — nur wenn die En
Content = mappedInput
});
await SseHelper.TryWriteSse(response, "tool_end", new { name = mappedName, id = evt.ToolId, input = mappedInput }, sseCt);
// ask_user_choice via MCP: TCS erstellen + SSE user_choice Event
if (mappedName == "ask_user_choice")
{
try
{
var choiceInput = JsonSerializer.Deserialize<JsonElement>(mappedInput);
var cQuestion = choiceInput.TryGetProperty("question", out var cq) ? cq.GetString() ?? "" : "";
var cChoices = new List<string>();
if (choiceInput.TryGetProperty("choices", out var cc) && cc.ValueKind == JsonValueKind.Array)
foreach (var cOpt in cc.EnumerateArray())
cChoices.Add(cOpt.GetString() ?? "");
// TCS erstellen bevor der MCP-Server den Endpoint aufruft
InternalEndpoint.PendingChoices[sessionId] = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
// SSE Event mit viaCli-Flag → Frontend zeigt Buttons + nutzt choice-respond Endpoint
choiceToolCallId = evt.ToolId;
await SseHelper.TryWriteSse(response, "user_choice", new
{
toolCallId = evt.ToolId,
question = cQuestion,
choices = cChoices,
viaCli = true
}, sseCt);
isUserChoice = true;
}
catch (JsonException) { /* Input nicht parsbar – MCP-Server gibt Fehler */ }
}
break;
}
case CliEventType.ToolResult:
@@ -2085,43 +2105,6 @@ Danach STOPPE die Textgenerierung sofort. Nutze dies sparsam — nur wenn die En
}, sseCt);
break;
}
case CliEventType.UserChoice:
// Text bisher in DB speichern
if (fullText.Length > 0)
{
await _db.SaveMessage(new MessageRecord
{
ChainId = chainId,
Iteration = 1,
Role = "assistant",
Type = "text",
Content = fullText.ToString()
});
}
// Synthetische tool_use speichern (für History-Rebuild)
choiceToolCallId = $"toolu_cli_{Guid.NewGuid():N}";
await _db.SaveMessage(new MessageRecord
{
ChainId = chainId,
Iteration = 1,
Role = "assistant",
Type = "tool_use",
ToolName = "ask_user_choice",
ToolCallId = choiceToolCallId,
Content = JsonSerializer.Serialize(new { question = evt.ChoiceQuestion, choices = evt.ChoiceOptions })
});
// SSE user_choice Event (Frontend zeigt Buttons)
await SseHelper.TryWriteSse(response, "user_choice", new
{
toolCallId = choiceToolCallId,
question = evt.ChoiceQuestion,
choices = evt.ChoiceOptions
}, sseCt);
isUserChoice = true;
break;
}
},
modelId: modelId,