Fix: Chat scrollt nach Font-Loading nochmal nach unten
scrollDown() wartet jetzt auf document.fonts.ready und scrollt erneut. chat-page.js wartet nach loadHistory() zusätzlich per requestAnimationFrame auf den Repaint nach Font-Swap.
This commit is contained in:
1 parent
3cb468f4e8
commit
76341dc42b
14 files changed
+349
-121
No files matched your search
+144
-104
@@ -5,6 +5,7 @@ using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Gott.Models;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -34,7 +35,8 @@ public class AnthropicService
|
||||
string chainId,
|
||||
string workspace,
|
||||
string sessionId = null,
|
||||
int maxIterations = 25)
|
||||
int maxIterations = 25,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
var usage = new TokenUsage();
|
||||
@@ -48,128 +50,152 @@ public class AnthropicService
|
||||
Model = modelId
|
||||
};
|
||||
|
||||
while (iterations < maxIterations)
|
||||
try
|
||||
{
|
||||
iterations++;
|
||||
|
||||
var payload = new Dictionary<string, object>
|
||||
while (iterations < maxIterations)
|
||||
{
|
||||
["model"] = modelId,
|
||||
["max_tokens"] = 4096,
|
||||
["system"] = systemPrompt,
|
||||
["messages"] = messages,
|
||||
["stream"] = true
|
||||
};
|
||||
iterations++;
|
||||
|
||||
if (!string.IsNullOrEmpty(workspace))
|
||||
payload["tools"] = _toolService.GetToolDefinitions();
|
||||
|
||||
var client = CreateClient();
|
||||
var json = JsonSerializer.Serialize(payload);
|
||||
var httpContent = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
|
||||
var httpResp = await client.SendAsync(
|
||||
new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages") { Content = httpContent },
|
||||
HttpCompletionOption.ResponseHeadersRead
|
||||
);
|
||||
|
||||
if (!httpResp.IsSuccessStatusCode)
|
||||
{
|
||||
var errBody = await httpResp.Content.ReadAsStringAsync();
|
||||
var errMsg = $"API {(int)httpResp.StatusCode}: {errBody}";
|
||||
await _db.SaveMessage(new MessageRecord
|
||||
var payload = new Dictionary<string, object>
|
||||
{
|
||||
ChainId = chainId, Iteration = iterations,
|
||||
Role = "system", Type = "error",
|
||||
Content = errMsg, IsError = true
|
||||
});
|
||||
await WriteSseEvent(response, "error", new { error = errMsg });
|
||||
await FinalizeChain(chain, usage, iterations, "error", sw);
|
||||
await WriteSseEvent(response, "done", new { });
|
||||
return;
|
||||
}
|
||||
["model"] = modelId,
|
||||
["max_tokens"] = 16384,
|
||||
["system"] = systemPrompt,
|
||||
["messages"] = messages,
|
||||
["stream"] = true
|
||||
};
|
||||
|
||||
var (stopReason, assistantContent, executedTools) = await ProcessStream(response, httpResp, usage, workspace);
|
||||
lastStopReason = stopReason;
|
||||
if (!string.IsNullOrEmpty(workspace))
|
||||
payload["tools"] = _toolService.GetToolDefinitions();
|
||||
|
||||
messages.Add(new { role = "assistant", content = assistantContent });
|
||||
var client = CreateClient();
|
||||
var json = JsonSerializer.Serialize(payload);
|
||||
var httpContent = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
|
||||
// Assistant-Blöcke persistieren
|
||||
foreach (var block in assistantContent)
|
||||
{
|
||||
var blockJson = JsonSerializer.Serialize(block);
|
||||
var el = JsonSerializer.Deserialize<JsonElement>(blockJson);
|
||||
var blockType = el.GetProperty("type").GetString() ?? "";
|
||||
var httpResp = await client.SendAsync(
|
||||
new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages") { Content = httpContent },
|
||||
HttpCompletionOption.ResponseHeadersRead,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
if (blockType == "text")
|
||||
if (!httpResp.IsSuccessStatusCode)
|
||||
{
|
||||
var errBody = await httpResp.Content.ReadAsStringAsync(cancellationToken);
|
||||
var errMsg = $"API {(int)httpResp.StatusCode}: {errBody}";
|
||||
await _db.SaveMessage(new MessageRecord
|
||||
{
|
||||
ChainId = chainId, Iteration = iterations,
|
||||
Role = "system", Type = "error",
|
||||
Content = errMsg, IsError = true
|
||||
});
|
||||
await TryWriteSse(response, "error", new { error = errMsg }, cancellationToken);
|
||||
await FinalizeChain(chain, usage, iterations, "error", sw);
|
||||
await TryWriteSse(response, "done", new { }, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var (stopReason, assistantContent, executedTools) = await ProcessStream(response, httpResp, usage, workspace, cancellationToken);
|
||||
lastStopReason = stopReason;
|
||||
|
||||
messages.Add(new { role = "assistant", content = assistantContent });
|
||||
|
||||
// Assistant-Blöcke persistieren
|
||||
foreach (var block in assistantContent)
|
||||
{
|
||||
var blockJson = JsonSerializer.Serialize(block);
|
||||
var el = JsonSerializer.Deserialize<JsonElement>(blockJson);
|
||||
var blockType = el.GetProperty("type").GetString() ?? "";
|
||||
|
||||
if (blockType == "text")
|
||||
{
|
||||
await _db.SaveMessage(new MessageRecord
|
||||
{
|
||||
ChainId = chainId, Iteration = iterations,
|
||||
Role = "assistant", Type = "text",
|
||||
Content = el.GetProperty("text").GetString()
|
||||
});
|
||||
}
|
||||
else if (blockType == "tool_use")
|
||||
{
|
||||
await _db.SaveMessage(new MessageRecord
|
||||
{
|
||||
ChainId = chainId, Iteration = iterations,
|
||||
Role = "assistant", Type = "tool_use",
|
||||
ToolName = el.GetProperty("name").GetString(),
|
||||
ToolCallId = el.GetProperty("id").GetString(),
|
||||
Content = el.GetProperty("input").ToString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Tool Results persistieren
|
||||
foreach (var tr in executedTools)
|
||||
{
|
||||
await _db.SaveMessage(new MessageRecord
|
||||
{
|
||||
ChainId = chainId, Iteration = iterations,
|
||||
Role = "assistant", Type = "text",
|
||||
Content = el.GetProperty("text").GetString()
|
||||
Role = "user", Type = "tool_result",
|
||||
ToolCallId = tr.Id,
|
||||
Content = tr.Result,
|
||||
IsError = tr.IsError
|
||||
});
|
||||
}
|
||||
else if (blockType == "tool_use")
|
||||
|
||||
if (stopReason != "tool_use")
|
||||
{
|
||||
await _db.SaveMessage(new MessageRecord
|
||||
var finalStopReason = string.IsNullOrEmpty(stopReason) ? "unexpected_end" : stopReason;
|
||||
await FinalizeChain(chain, usage, iterations, finalStopReason, sw);
|
||||
await TryWriteSse(response, "done", new
|
||||
{
|
||||
ChainId = chainId, Iteration = iterations,
|
||||
Role = "assistant", Type = "tool_use",
|
||||
ToolName = el.GetProperty("name").GetString(),
|
||||
ToolCallId = el.GetProperty("id").GetString(),
|
||||
Content = el.GetProperty("input").ToString()
|
||||
model = modelId,
|
||||
inputTokens = usage.InputTokens,
|
||||
outputTokens = usage.OutputTokens,
|
||||
iterations,
|
||||
chainId,
|
||||
durationMs = sw.ElapsedMilliseconds,
|
||||
stopReason = finalStopReason
|
||||
}, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var toolResults = new List<object>();
|
||||
foreach (var tr in executedTools)
|
||||
{
|
||||
toolResults.Add(new
|
||||
{
|
||||
type = "tool_result",
|
||||
tool_use_id = tr.Id,
|
||||
content = tr.Result,
|
||||
is_error = tr.IsError
|
||||
});
|
||||
}
|
||||
|
||||
messages.Add(new { role = "user", content = toolResults });
|
||||
}
|
||||
|
||||
// Tool Results persistieren
|
||||
foreach (var tr in executedTools)
|
||||
await FinalizeChain(chain, usage, iterations, "max_iterations", sw);
|
||||
await TryWriteSse(response, "error", new { error = "Max iterations erreicht" }, cancellationToken);
|
||||
await TryWriteSse(response, "done", new { }, cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Client hat die Verbindung getrennt (z.B. Browser-Refresh)
|
||||
await FinalizeChain(chain, usage, iterations, "client_disconnected", sw);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Unerwarteter Fehler – in DB speichern und versuchen an Client zu senden
|
||||
var errMsg = $"Stream-Fehler: {ex.Message}";
|
||||
await _db.SaveMessage(new MessageRecord
|
||||
{
|
||||
await _db.SaveMessage(new MessageRecord
|
||||
{
|
||||
ChainId = chainId, Iteration = iterations,
|
||||
Role = "user", Type = "tool_result",
|
||||
ToolCallId = tr.Id,
|
||||
Content = tr.Result,
|
||||
IsError = tr.IsError
|
||||
});
|
||||
}
|
||||
|
||||
if (stopReason != "tool_use")
|
||||
{
|
||||
await FinalizeChain(chain, usage, iterations, stopReason, sw);
|
||||
await WriteSseEvent(response, "done", new
|
||||
{
|
||||
model = modelId,
|
||||
inputTokens = usage.InputTokens,
|
||||
outputTokens = usage.OutputTokens,
|
||||
iterations,
|
||||
chainId,
|
||||
durationMs = sw.ElapsedMilliseconds,
|
||||
stopReason
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var toolResults = new List<object>();
|
||||
foreach (var tr in executedTools)
|
||||
{
|
||||
toolResults.Add(new
|
||||
{
|
||||
type = "tool_result",
|
||||
tool_use_id = tr.Id,
|
||||
content = tr.Result,
|
||||
is_error = tr.IsError
|
||||
});
|
||||
}
|
||||
|
||||
messages.Add(new { role = "user", content = toolResults });
|
||||
ChainId = chainId, Iteration = iterations,
|
||||
Role = "system", Type = "error",
|
||||
Content = errMsg, IsError = true
|
||||
});
|
||||
await FinalizeChain(chain, usage, iterations, "error", sw);
|
||||
await TryWriteSse(response, "error", new { error = errMsg }, cancellationToken);
|
||||
await TryWriteSse(response, "done", new { }, cancellationToken);
|
||||
}
|
||||
|
||||
await FinalizeChain(chain, usage, iterations, "max_iterations", sw);
|
||||
await WriteSseEvent(response, "error", new { error = "Max iterations erreicht" });
|
||||
await WriteSseEvent(response, "done", new { });
|
||||
}
|
||||
|
||||
private async Task FinalizeChain(ChainRecord chain, TokenUsage usage, int iterations, string stopReason, System.Diagnostics.Stopwatch sw)
|
||||
@@ -186,9 +212,10 @@ public class AnthropicService
|
||||
HttpResponse response,
|
||||
HttpResponseMessage httpResp,
|
||||
TokenUsage usage,
|
||||
string workspace)
|
||||
string workspace,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var stream = await httpResp.Content.ReadAsStreamAsync();
|
||||
var stream = await httpResp.Content.ReadAsStreamAsync(cancellationToken);
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
var stopReason = "";
|
||||
@@ -202,7 +229,7 @@ public class AnthropicService
|
||||
var executedTools = new List<ToolResult>();
|
||||
|
||||
string line;
|
||||
while ((line = await reader.ReadLineAsync()) != null)
|
||||
while ((line = await reader.ReadLineAsync(cancellationToken)) != null)
|
||||
{
|
||||
if (!line.StartsWith("data: ")) continue;
|
||||
var data = line["data: ".Length..];
|
||||
@@ -357,6 +384,19 @@ public class AnthropicService
|
||||
await response.WriteAsync($"event: {eventType}\ndata: {json}\n\n");
|
||||
await response.Body.FlushAsync();
|
||||
}
|
||||
|
||||
private static async Task TryWriteSse(HttpResponse response, string eventType, object data, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!ct.IsCancellationRequested)
|
||||
await WriteSseEvent(response, eventType, data);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Client bereits disconnected – ignorieren
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record ToolResult(string Id, string Result, bool IsError);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Gott.Services;
|
||||
|
||||
public class StreamingTracker
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, string> _streaming = new ConcurrentDictionary<string, string>();
|
||||
|
||||
public void Start(string sessionId, string chainId) => _streaming[sessionId] = chainId;
|
||||
|
||||
public void Stop(string sessionId) => _streaming.TryRemove(sessionId, out _);
|
||||
|
||||
public bool IsStreaming(string sessionId) => _streaming.ContainsKey(sessionId);
|
||||
|
||||
public string GetChainId(string sessionId) => _streaming.TryGetValue(sessionId, out var chainId) ? chainId : null;
|
||||
}
|
||||
Reference in new issue
Block a user