Persistenz: SQLite + Dapper, chains/messages Tabellen, History-Endpoint, Frontend History-Load
This commit is contained in:
1 parent
5f210323fd
commit
9f2f3fee1f
9 files changed
+370
-6
No files matched your search
@@ -1,4 +1,6 @@
|
|||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -19,7 +21,7 @@ public static class ChatEndpoint
|
|||||||
app.MapPost("/api/chat", HandleChat);
|
app.MapPost("/api/chat", HandleChat);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task HandleChat(HttpRequest req, HttpResponse res, IHttpClientFactory httpFactory, IConfiguration config, ToolService toolService)
|
private static async Task HandleChat(HttpRequest req, HttpResponse res, IHttpClientFactory httpFactory, IConfiguration config, ToolService toolService, DatabaseService db)
|
||||||
{
|
{
|
||||||
var body = await JsonSerializer.DeserializeAsync<ChatRequest>(req.Body);
|
var body = await JsonSerializer.DeserializeAsync<ChatRequest>(req.Body);
|
||||||
if (body == null || body.Messages == null || body.Messages.Count == 0)
|
if (body == null || body.Messages == null || body.Messages.Count == 0)
|
||||||
@@ -48,10 +50,34 @@ public static class ChatEndpoint
|
|||||||
res.Headers["Cache-Control"] = "no-cache";
|
res.Headers["Cache-Control"] = "no-cache";
|
||||||
res.Headers["Connection"] = "keep-alive";
|
res.Headers["Connection"] = "keep-alive";
|
||||||
|
|
||||||
|
var chainId = Guid.NewGuid().ToString("N");
|
||||||
|
|
||||||
|
// Chain zuerst anlegen (FK für messages)
|
||||||
|
await db.CreateChain(new ChainRecord
|
||||||
|
{
|
||||||
|
ChainId = chainId,
|
||||||
|
Model = modelId,
|
||||||
|
SystemPrompt = body.SystemPrompt
|
||||||
|
});
|
||||||
|
|
||||||
var messages = new List<object>();
|
var messages = new List<object>();
|
||||||
foreach (var msg in body.Messages)
|
foreach (var msg in body.Messages)
|
||||||
messages.Add(new { role = msg.Role, content = msg.Content });
|
messages.Add(new { role = msg.Role, content = msg.Content });
|
||||||
|
|
||||||
|
// Nur die letzte User-Message speichern (= die neue)
|
||||||
|
var lastUserMsg = body.Messages.LastOrDefault(m => m.Role == "user");
|
||||||
|
if (lastUserMsg != null)
|
||||||
|
{
|
||||||
|
await db.SaveMessage(new MessageRecord
|
||||||
|
{
|
||||||
|
ChainId = chainId,
|
||||||
|
Iteration = 0,
|
||||||
|
Role = "user",
|
||||||
|
Type = "text",
|
||||||
|
Content = lastUserMsg.Content
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var systemPrompt = new List<object>
|
var systemPrompt = new List<object>
|
||||||
{
|
{
|
||||||
new { type = "text", text = "You are Claude Code, Anthropic's official CLI for Claude." }
|
new { type = "text", text = "You are Claude Code, Anthropic's official CLI for Claude." }
|
||||||
@@ -59,7 +85,7 @@ public static class ChatEndpoint
|
|||||||
if (!string.IsNullOrWhiteSpace(body.SystemPrompt))
|
if (!string.IsNullOrWhiteSpace(body.SystemPrompt))
|
||||||
systemPrompt.Add(new { type = "text", text = body.SystemPrompt });
|
systemPrompt.Add(new { type = "text", text = body.SystemPrompt });
|
||||||
|
|
||||||
var anthropic = new AnthropicService(httpFactory, token, toolService);
|
var anthropic = new AnthropicService(httpFactory, token, toolService, db);
|
||||||
await anthropic.StreamChat(res, modelId, systemPrompt, messages);
|
await anthropic.StreamChat(res, modelId, systemPrompt, messages, chainId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Gott.Models;
|
||||||
|
using Gott.Services;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
|
||||||
|
namespace Gott.Endpoints;
|
||||||
|
|
||||||
|
public static class HistoryEndpoint
|
||||||
|
{
|
||||||
|
public static void MapHistoryEndpoint(this IEndpointRouteBuilder app)
|
||||||
|
{
|
||||||
|
app.MapGet("/api/history", HandleHistory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task HandleHistory(HttpContext ctx, DatabaseService db)
|
||||||
|
{
|
||||||
|
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
|
||||||
|
var limitStr = ctx.Request.Query["limit"].FirstOrDefault();
|
||||||
|
var limit = int.TryParse(limitStr, out var l) ? l : 50;
|
||||||
|
|
||||||
|
IEnumerable<ChainRecord> chains;
|
||||||
|
if (string.IsNullOrEmpty(sessionId) || sessionId == "null")
|
||||||
|
chains = await db.GetRecentChains(limit);
|
||||||
|
else
|
||||||
|
chains = await db.GetSessionChains(sessionId, limit);
|
||||||
|
|
||||||
|
var result = new List<object>();
|
||||||
|
foreach (var chain in chains)
|
||||||
|
{
|
||||||
|
var messages = await db.GetChainMessages(chain.ChainId);
|
||||||
|
result.Add(new { chain, messages });
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.Response.WriteAsJsonAsync(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
namespace Gott.Models;
|
||||||
|
|
||||||
|
public class ChainRecord
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string ChainId { get; set; }
|
||||||
|
public string SessionId { get; set; }
|
||||||
|
public string Model { get; set; }
|
||||||
|
public string SystemPrompt { get; set; }
|
||||||
|
public long InputTokens { get; set; }
|
||||||
|
public long OutputTokens { get; set; }
|
||||||
|
public int Iterations { get; set; }
|
||||||
|
public string StopReason { get; set; }
|
||||||
|
public long DurationMs { get; set; }
|
||||||
|
public string CreatedAt { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace Gott.Models;
|
||||||
|
|
||||||
|
public class MessageRecord
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string ChainId { get; set; }
|
||||||
|
public string Role { get; set; } // user, assistant
|
||||||
|
public string Type { get; set; } // text, tool_use, tool_result
|
||||||
|
public string Content { get; set; } // Text oder JSON
|
||||||
|
public string ToolName { get; set; } // null bei text
|
||||||
|
public string ToolCallId { get; set; } // tool_use id
|
||||||
|
public bool IsError { get; set; }
|
||||||
|
public int Iteration { get; set; } // welche Loop-Iteration
|
||||||
|
public string CreatedAt { get; set; }
|
||||||
|
}
|
||||||
@@ -13,11 +13,13 @@ var workspaceRoot = Path.Combine(dataDir, "workspace");
|
|||||||
Directory.CreateDirectory(workspaceRoot);
|
Directory.CreateDirectory(workspaceRoot);
|
||||||
|
|
||||||
builder.Services.AddSingleton(new ToolService(workspaceRoot));
|
builder.Services.AddSingleton(new ToolService(workspaceRoot));
|
||||||
|
builder.Services.AddSingleton(new DatabaseService(Path.Combine(dataDir, "gott.db")));
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
app.UseDefaultFiles();
|
app.UseDefaultFiles();
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
|
|
||||||
app.MapChatEndpoint();
|
app.MapChatEndpoint();
|
||||||
|
app.MapHistoryEndpoint();
|
||||||
|
|
||||||
app.Run("http://0.0.0.0:19000");
|
app.Run("http://0.0.0.0:19000");
|
||||||
@@ -6,6 +6,7 @@ using System.Net.Http.Headers;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Gott.Models;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace Gott.Services;
|
namespace Gott.Services;
|
||||||
@@ -15,12 +16,14 @@ public class AnthropicService
|
|||||||
private readonly IHttpClientFactory _httpFactory;
|
private readonly IHttpClientFactory _httpFactory;
|
||||||
private readonly string _token;
|
private readonly string _token;
|
||||||
private readonly ToolService _toolService;
|
private readonly ToolService _toolService;
|
||||||
|
private readonly DatabaseService _db;
|
||||||
|
|
||||||
public AnthropicService(IHttpClientFactory httpFactory, string token, ToolService toolService)
|
public AnthropicService(IHttpClientFactory httpFactory, string token, ToolService toolService, DatabaseService db)
|
||||||
{
|
{
|
||||||
_httpFactory = httpFactory;
|
_httpFactory = httpFactory;
|
||||||
_token = token;
|
_token = token;
|
||||||
_toolService = toolService;
|
_toolService = toolService;
|
||||||
|
_db = db;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StreamChat(
|
public async Task StreamChat(
|
||||||
@@ -28,10 +31,21 @@ public class AnthropicService
|
|||||||
string modelId,
|
string modelId,
|
||||||
List<object> systemPrompt,
|
List<object> systemPrompt,
|
||||||
List<object> messages,
|
List<object> messages,
|
||||||
|
string chainId,
|
||||||
|
string sessionId = null,
|
||||||
int maxIterations = 25)
|
int maxIterations = 25)
|
||||||
{
|
{
|
||||||
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||||
var usage = new TokenUsage();
|
var usage = new TokenUsage();
|
||||||
int iterations = 0;
|
int iterations = 0;
|
||||||
|
string lastStopReason = "";
|
||||||
|
|
||||||
|
var chain = new ChainRecord
|
||||||
|
{
|
||||||
|
ChainId = chainId,
|
||||||
|
SessionId = sessionId,
|
||||||
|
Model = modelId
|
||||||
|
};
|
||||||
|
|
||||||
while (iterations < maxIterations)
|
while (iterations < maxIterations)
|
||||||
{
|
{
|
||||||
@@ -59,23 +73,77 @@ public class AnthropicService
|
|||||||
if (!httpResp.IsSuccessStatusCode)
|
if (!httpResp.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var errBody = await httpResp.Content.ReadAsStringAsync();
|
var errBody = await httpResp.Content.ReadAsStringAsync();
|
||||||
await WriteSseEvent(response, "error", new { error = $"API {(int)httpResp.StatusCode}: {errBody}" });
|
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 WriteSseEvent(response, "error", new { error = errMsg });
|
||||||
|
await FinalizeChain(chain, usage, iterations, "error", sw);
|
||||||
await WriteSseEvent(response, "done", new { });
|
await WriteSseEvent(response, "done", new { });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var (stopReason, assistantContent, executedTools) = await ProcessStream(response, httpResp, usage);
|
var (stopReason, assistantContent, executedTools) = await ProcessStream(response, httpResp, usage);
|
||||||
|
lastStopReason = stopReason;
|
||||||
|
|
||||||
messages.Add(new { role = "assistant", content = assistantContent });
|
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 = "user", Type = "tool_result",
|
||||||
|
ToolCallId = tr.Id,
|
||||||
|
Content = tr.Result,
|
||||||
|
IsError = tr.IsError
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (stopReason != "tool_use")
|
if (stopReason != "tool_use")
|
||||||
{
|
{
|
||||||
|
await FinalizeChain(chain, usage, iterations, stopReason, sw);
|
||||||
await WriteSseEvent(response, "done", new
|
await WriteSseEvent(response, "done", new
|
||||||
{
|
{
|
||||||
model = modelId,
|
model = modelId,
|
||||||
inputTokens = usage.InputTokens,
|
inputTokens = usage.InputTokens,
|
||||||
outputTokens = usage.OutputTokens,
|
outputTokens = usage.OutputTokens,
|
||||||
iterations
|
iterations,
|
||||||
|
chainId,
|
||||||
|
durationMs = sw.ElapsedMilliseconds
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -95,10 +163,21 @@ public class AnthropicService
|
|||||||
messages.Add(new { role = "user", content = toolResults });
|
messages.Add(new { role = "user", content = toolResults });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await FinalizeChain(chain, usage, iterations, "max_iterations", sw);
|
||||||
await WriteSseEvent(response, "error", new { error = "Max iterations erreicht" });
|
await WriteSseEvent(response, "error", new { error = "Max iterations erreicht" });
|
||||||
await WriteSseEvent(response, "done", new { });
|
await WriteSseEvent(response, "done", new { });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task FinalizeChain(ChainRecord chain, TokenUsage usage, int iterations, string stopReason, System.Diagnostics.Stopwatch sw)
|
||||||
|
{
|
||||||
|
chain.InputTokens = usage.InputTokens;
|
||||||
|
chain.OutputTokens = usage.OutputTokens;
|
||||||
|
chain.Iterations = iterations;
|
||||||
|
chain.StopReason = stopReason;
|
||||||
|
chain.DurationMs = sw.ElapsedMilliseconds;
|
||||||
|
await _db.UpdateChain(chain);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<(string StopReason, List<object> Content, List<ToolResult> Tools)> ProcessStream(
|
private async Task<(string StopReason, List<object> Content, List<ToolResult> Tools)> ProcessStream(
|
||||||
HttpResponse response,
|
HttpResponse response,
|
||||||
HttpResponseMessage httpResp,
|
HttpResponseMessage httpResp,
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dapper;
|
||||||
|
using Gott.Models;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
|
||||||
|
namespace Gott.Services;
|
||||||
|
|
||||||
|
public class DatabaseService
|
||||||
|
{
|
||||||
|
private readonly string _connectionString;
|
||||||
|
|
||||||
|
public DatabaseService(string dbPath)
|
||||||
|
{
|
||||||
|
_connectionString = $"Data Source={dbPath}";
|
||||||
|
InitializeDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
private SqliteConnection CreateConnection()
|
||||||
|
{
|
||||||
|
var conn = new SqliteConnection(_connectionString);
|
||||||
|
conn.Open();
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeDatabase()
|
||||||
|
{
|
||||||
|
using var conn = CreateConnection();
|
||||||
|
|
||||||
|
// Dapper: snake_case → PascalCase Mapping
|
||||||
|
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
|
||||||
|
conn.Execute(@"
|
||||||
|
CREATE TABLE IF NOT EXISTS chains (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
chain_id TEXT NOT NULL UNIQUE,
|
||||||
|
session_id TEXT,
|
||||||
|
model TEXT NOT NULL,
|
||||||
|
system_prompt TEXT,
|
||||||
|
input_tokens INTEGER NOT NULL DEFAULT 0,
|
||||||
|
output_tokens INTEGER NOT NULL DEFAULT 0,
|
||||||
|
iterations INTEGER NOT NULL DEFAULT 0,
|
||||||
|
stop_reason TEXT,
|
||||||
|
duration_ms INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS messages (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
chain_id TEXT NOT NULL,
|
||||||
|
role TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
content TEXT,
|
||||||
|
tool_name TEXT,
|
||||||
|
tool_call_id TEXT,
|
||||||
|
is_error INTEGER NOT NULL DEFAULT 0,
|
||||||
|
iteration INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (chain_id) REFERENCES chains(chain_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_chains_session ON chains(session_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_messages_chain ON messages(chain_id);
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Chains ──
|
||||||
|
|
||||||
|
public async Task CreateChain(ChainRecord chain)
|
||||||
|
{
|
||||||
|
using var conn = CreateConnection();
|
||||||
|
chain.CreatedAt = DateTime.UtcNow.ToString("o");
|
||||||
|
await conn.ExecuteAsync(@"
|
||||||
|
INSERT INTO chains (chain_id, session_id, model, system_prompt, input_tokens, output_tokens, iterations, stop_reason, duration_ms, created_at)
|
||||||
|
VALUES (@ChainId, @SessionId, @Model, @SystemPrompt, @InputTokens, @OutputTokens, @Iterations, @StopReason, @DurationMs, @CreatedAt)
|
||||||
|
", chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateChain(ChainRecord chain)
|
||||||
|
{
|
||||||
|
using var conn = CreateConnection();
|
||||||
|
await conn.ExecuteAsync(@"
|
||||||
|
UPDATE chains SET
|
||||||
|
input_tokens = @InputTokens,
|
||||||
|
output_tokens = @OutputTokens,
|
||||||
|
iterations = @Iterations,
|
||||||
|
stop_reason = @StopReason,
|
||||||
|
duration_ms = @DurationMs
|
||||||
|
WHERE chain_id = @ChainId
|
||||||
|
", chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ChainRecord> GetChain(string chainId)
|
||||||
|
{
|
||||||
|
using var conn = CreateConnection();
|
||||||
|
return await conn.QuerySingleOrDefaultAsync<ChainRecord>(
|
||||||
|
"SELECT * FROM chains WHERE chain_id = @chainId", new { chainId }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<ChainRecord>> GetRecentChains(int limit = 50)
|
||||||
|
{
|
||||||
|
using var conn = CreateConnection();
|
||||||
|
return await conn.QueryAsync<ChainRecord>(
|
||||||
|
"SELECT * FROM chains ORDER BY id DESC LIMIT @limit", new { limit }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<ChainRecord>> GetSessionChains(string sessionId, int limit = 100)
|
||||||
|
{
|
||||||
|
using var conn = CreateConnection();
|
||||||
|
return await conn.QueryAsync<ChainRecord>(
|
||||||
|
"SELECT * FROM chains WHERE session_id = @sessionId ORDER BY id DESC LIMIT @limit",
|
||||||
|
new { sessionId, limit }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Messages ──
|
||||||
|
|
||||||
|
public async Task<long> SaveMessage(MessageRecord record)
|
||||||
|
{
|
||||||
|
using var conn = CreateConnection();
|
||||||
|
record.CreatedAt = DateTime.UtcNow.ToString("o");
|
||||||
|
return await conn.ExecuteScalarAsync<long>(@"
|
||||||
|
INSERT INTO messages (chain_id, role, type, content, tool_name, tool_call_id, is_error, iteration, created_at)
|
||||||
|
VALUES (@ChainId, @Role, @Type, @Content, @ToolName, @ToolCallId, @IsError, @Iteration, @CreatedAt);
|
||||||
|
SELECT last_insert_rowid();
|
||||||
|
", record);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<MessageRecord>> GetChainMessages(string chainId)
|
||||||
|
{
|
||||||
|
using var conn = CreateConnection();
|
||||||
|
return await conn.QueryAsync<MessageRecord>(
|
||||||
|
"SELECT * FROM messages WHERE chain_id = @chainId ORDER BY id", new { chainId }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Anthropic" Version="12.8.0" />
|
<PackageReference Include="Anthropic" Version="12.8.0" />
|
||||||
|
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||||
|
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -269,6 +269,52 @@ document.getElementById('input').addEventListener('input', function() {
|
|||||||
this.style.height = 'auto';
|
this.style.height = 'auto';
|
||||||
this.style.height = Math.min(this.scrollHeight, 150) + 'px';
|
this.style.height = Math.min(this.scrollHeight, 150) + 'px';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function loadHistory() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/api/history?sessionId=null');
|
||||||
|
const data = await resp.json();
|
||||||
|
const chat = document.getElementById('chat');
|
||||||
|
|
||||||
|
// Chains sind DESC sortiert, also umkehren für chronologische Reihenfolge
|
||||||
|
data.reverse();
|
||||||
|
|
||||||
|
for (const entry of data) {
|
||||||
|
for (const msg of entry.messages) {
|
||||||
|
if (msg.role === 'user' && msg.type === 'text') {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'msg user';
|
||||||
|
div.textContent = msg.content;
|
||||||
|
chat.appendChild(div);
|
||||||
|
messages.push({ role: 'user', content: msg.content });
|
||||||
|
} else if (msg.role === 'assistant' && msg.type === 'text') {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'msg assistant';
|
||||||
|
div.textContent = msg.content;
|
||||||
|
chat.appendChild(div);
|
||||||
|
messages.push({ role: 'assistant', content: msg.content });
|
||||||
|
} else if (msg.role === 'assistant' && msg.type === 'tool_use') {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'tool-box';
|
||||||
|
div.innerHTML = '<span class="tool-name">' + esc(msg.toolName) + '</span>'
|
||||||
|
+ '<span class="tool-input">' + esc(truncate(msg.content, 120)) + '</span>';
|
||||||
|
chat.appendChild(div);
|
||||||
|
} else if (msg.type === 'tool_result') {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'tool-box';
|
||||||
|
div.innerHTML = '<div class="tool-result ' + (msg.isError ? 'tool-error' : '') + '">'
|
||||||
|
+ esc(truncate(msg.content, 300)) + '</div>';
|
||||||
|
chat.appendChild(div);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scrollDown();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('History laden fehlgeschlagen:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadHistory();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in new issue
Block a user