feat: Semantische Suche über Chat-History (memory_search)
- Lokales Embedding-Model (all-MiniLM-L6-v2, ONNX, CPU) - EmbeddingService: WordPiece-Tokenizer + Mean Pooling (384 Dim) - VectorSearchService: SQLite-basierte Vektor-Speicherung + Cosine Similarity - EmbeddingBackgroundService: Backfill bestehender Messages + Live-Queue - Neues Tool memory_search: Semantische Suche über alle Sessions (inkl. archivierte) - ~9000 Messages embedded, Suche über komplette Chat-History
This commit is contained in:
1 parent
baf53b7dac
commit
add111ffe1
9 files changed
+727
-15
No files matched your search
@@ -22,7 +22,7 @@ public static class ChatEndpoint
|
|||||||
app.MapPost("/api/chat/stop", HandleStop);
|
app.MapPost("/api/chat/stop", HandleStop);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task HandleChat(HttpRequest req, HttpResponse res, IConfiguration config, ChatService chatService, DatabaseService db, StreamingTracker streamingTracker, AnthropicService anthropic, RemoteConnectionManager remoteMgr, CancellationToken cancellationToken)
|
private static async Task HandleChat(HttpRequest req, HttpResponse res, IConfiguration config, ChatService chatService, DatabaseService db, StreamingTracker streamingTracker, AnthropicService anthropic, RemoteConnectionManager remoteMgr, EmbeddingBackgroundService embeddingBg, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var body = await JsonSerializer.DeserializeAsync<ChatRequest>(req.Body);
|
var body = await JsonSerializer.DeserializeAsync<ChatRequest>(req.Body);
|
||||||
if (body == null)
|
if (body == null)
|
||||||
@@ -108,7 +108,7 @@ public static class ChatEndpoint
|
|||||||
Data = b.Data
|
Data = b.Data
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
await db.SaveMessageWithAttachments(new MessageRecord
|
var userMsgId = await db.SaveMessageWithAttachments(new MessageRecord
|
||||||
{
|
{
|
||||||
ChainId = ctx.ChainId,
|
ChainId = ctx.ChainId,
|
||||||
Iteration = 0,
|
Iteration = 0,
|
||||||
@@ -116,6 +116,7 @@ public static class ChatEndpoint
|
|||||||
Type = "text",
|
Type = "text",
|
||||||
Content = textContent
|
Content = textContent
|
||||||
}, attachmentRecords);
|
}, attachmentRecords);
|
||||||
|
embeddingBg?.Enqueue(userMsgId, ctx.ChainId, ctx.SessionId, ctx.AgentId, "user", textContent ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
res.Headers["Content-Type"] = "text/event-stream";
|
res.Headers["Content-Type"] = "text/event-stream";
|
||||||
|
|||||||
+12
-1
@@ -43,6 +43,15 @@ var reminderInterval = builder.Configuration.GetValue<int>("ReminderInterval", 3
|
|||||||
var chatService = new ChatService(dbService, toolService, memoryService, skillService, maxHistory, reminderInterval);
|
var chatService = new ChatService(dbService, toolService, memoryService, skillService, maxHistory, reminderInterval);
|
||||||
builder.Services.AddSingleton(chatService);
|
builder.Services.AddSingleton(chatService);
|
||||||
|
|
||||||
|
// Embedding-Services (lokal, ONNX)
|
||||||
|
var embeddingService = new Gott.Services.EmbeddingService();
|
||||||
|
builder.Services.AddSingleton(embeddingService);
|
||||||
|
var vectorSearchService = new Gott.Services.VectorSearchService(dbPath, embeddingService);
|
||||||
|
builder.Services.AddSingleton(vectorSearchService);
|
||||||
|
var embeddingBgService = new Gott.Services.EmbeddingBackgroundService(vectorSearchService, embeddingService, dbPath);
|
||||||
|
builder.Services.AddSingleton(embeddingBgService);
|
||||||
|
builder.Services.AddHostedService(sp => sp.GetRequiredService<Gott.Services.EmbeddingBackgroundService>());
|
||||||
|
|
||||||
// AnthropicService (Singleton – shared zwischen ChatEndpoint und TelegramService)
|
// AnthropicService (Singleton – shared zwischen ChatEndpoint und TelegramService)
|
||||||
var anthropicToken = builder.Configuration["AnthropicToken"] ?? "";
|
var anthropicToken = builder.Configuration["AnthropicToken"] ?? "";
|
||||||
var payloadLogging = builder.Configuration.GetValue<bool>("PayloadLogging", false);
|
var payloadLogging = builder.Configuration.GetValue<bool>("PayloadLogging", false);
|
||||||
@@ -51,7 +60,9 @@ builder.Services.AddSingleton<AnthropicService>(sp =>
|
|||||||
new AnthropicService(
|
new AnthropicService(
|
||||||
sp.GetRequiredService<IHttpClientFactory>(),
|
sp.GetRequiredService<IHttpClientFactory>(),
|
||||||
anthropicToken, toolService, dbService, memoryService,
|
anthropicToken, toolService, dbService, memoryService,
|
||||||
payloadLogging, payloadLoggingGzip));
|
payloadLogging, payloadLoggingGzip,
|
||||||
|
vectorSearch: vectorSearchService,
|
||||||
|
embeddingBg: embeddingBgService));
|
||||||
|
|
||||||
// Telegram Bot (pro Session – Token aus Session-Config)
|
// Telegram Bot (pro Session – Token aus Session-Config)
|
||||||
var maxIterations = builder.Configuration.GetValue<int>("MaxIterations", 100);
|
var maxIterations = builder.Configuration.GetValue<int>("MaxIterations", 100);
|
||||||
|
|||||||
@@ -22,8 +22,10 @@ public class AnthropicService
|
|||||||
private readonly MemoryService _memoryService;
|
private readonly MemoryService _memoryService;
|
||||||
private readonly bool _payloadLogging;
|
private readonly bool _payloadLogging;
|
||||||
private readonly bool _payloadLoggingGzip;
|
private readonly bool _payloadLoggingGzip;
|
||||||
|
private readonly VectorSearchService _vectorSearch;
|
||||||
|
private readonly EmbeddingBackgroundService _embeddingBg;
|
||||||
|
|
||||||
public AnthropicService(IHttpClientFactory httpFactory, string token, ToolService toolService, DatabaseService db, MemoryService memoryService = null, bool payloadLogging = false, bool payloadLoggingGzip = false)
|
public AnthropicService(IHttpClientFactory httpFactory, string token, ToolService toolService, DatabaseService db, MemoryService memoryService = null, bool payloadLogging = false, bool payloadLoggingGzip = false, VectorSearchService vectorSearch = null, EmbeddingBackgroundService embeddingBg = null)
|
||||||
{
|
{
|
||||||
_httpFactory = httpFactory;
|
_httpFactory = httpFactory;
|
||||||
_token = token;
|
_token = token;
|
||||||
@@ -32,6 +34,8 @@ public class AnthropicService
|
|||||||
_memoryService = memoryService;
|
_memoryService = memoryService;
|
||||||
_payloadLogging = payloadLogging;
|
_payloadLogging = payloadLogging;
|
||||||
_payloadLoggingGzip = payloadLoggingGzip;
|
_payloadLoggingGzip = payloadLoggingGzip;
|
||||||
|
_vectorSearch = vectorSearch;
|
||||||
|
_embeddingBg = embeddingBg;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StreamChat(
|
public async Task StreamChat(
|
||||||
@@ -153,12 +157,14 @@ public class AnthropicService
|
|||||||
|
|
||||||
if (blockType == "text")
|
if (blockType == "text")
|
||||||
{
|
{
|
||||||
await _db.SaveMessage(new MessageRecord
|
var textContent = el.GetProperty("text").GetString();
|
||||||
|
var msgId = await _db.SaveMessage(new MessageRecord
|
||||||
{
|
{
|
||||||
ChainId = chainId, Iteration = iterations,
|
ChainId = chainId, Iteration = iterations,
|
||||||
Role = "assistant", Type = "text",
|
Role = "assistant", Type = "text",
|
||||||
Content = el.GetProperty("text").GetString()
|
Content = textContent
|
||||||
});
|
});
|
||||||
|
_embeddingBg?.Enqueue(msgId, chainId, sessionId, agentId, "assistant", textContent ?? "");
|
||||||
}
|
}
|
||||||
else if (blockType == "tool_use")
|
else if (blockType == "tool_use")
|
||||||
{
|
{
|
||||||
@@ -832,7 +838,7 @@ public class AnthropicService
|
|||||||
|
|
||||||
string toolResult;
|
string toolResult;
|
||||||
bool isError = false;
|
bool isError = false;
|
||||||
try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, agentWorkspace, _memoryService, agentId, workCt); }
|
try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, agentWorkspace, _memoryService, agentId, workCt, vectorSearch: _vectorSearch); }
|
||||||
catch (OperationCanceledException) { throw; }
|
catch (OperationCanceledException) { throw; }
|
||||||
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
|
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
|
||||||
|
|
||||||
@@ -1038,7 +1044,7 @@ public class AnthropicService
|
|||||||
|
|
||||||
string toolResult;
|
string toolResult;
|
||||||
bool isError = false;
|
bool isError = false;
|
||||||
try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, ctx.Workspace, _memoryService, ctx.AgentId); }
|
try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, ctx.Workspace, _memoryService, ctx.AgentId, vectorSearch: _vectorSearch); }
|
||||||
catch (OperationCanceledException) { throw; }
|
catch (OperationCanceledException) { throw; }
|
||||||
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
|
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
|
||||||
|
|
||||||
@@ -1339,7 +1345,7 @@ public class AnthropicService
|
|||||||
|
|
||||||
string toolResult;
|
string toolResult;
|
||||||
bool isError = false;
|
bool isError = false;
|
||||||
try { toolResult = await _toolService.ExecuteTool(currentToolName, inputEl, workspace, _memoryService, agentId, workCt, remoteMgr: remoteMgr, sessionId: sessionId); }
|
try { toolResult = await _toolService.ExecuteTool(currentToolName, inputEl, workspace, _memoryService, agentId, workCt, remoteMgr: remoteMgr, sessionId: sessionId, vectorSearch: _vectorSearch); }
|
||||||
catch (OperationCanceledException) { throw; }
|
catch (OperationCanceledException) { throw; }
|
||||||
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
|
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
|
||||||
|
|
||||||
@@ -1516,7 +1522,8 @@ public class AnthropicService
|
|||||||
{
|
{
|
||||||
var hasWorkspace = !string.IsNullOrEmpty(workspace);
|
var hasWorkspace = !string.IsNullOrEmpty(workspace);
|
||||||
var includeMemory = useMemory && _memoryService != null && !string.IsNullOrEmpty(agentId);
|
var includeMemory = useMemory && _memoryService != null && !string.IsNullOrEmpty(agentId);
|
||||||
var hasTools = hasWorkspace || includeMemory || includeRemote;
|
var includeMemorySearch = _vectorSearch != null && _vectorSearch.GetEmbeddingCount() > 0;
|
||||||
|
var hasTools = hasWorkspace || includeMemory || includeRemote || includeMemorySearch;
|
||||||
var hasWebSearch = !string.IsNullOrEmpty(webSearchVersion);
|
var hasWebSearch = !string.IsNullOrEmpty(webSearchVersion);
|
||||||
|
|
||||||
if (!hasTools && !hasWebSearch) return;
|
if (!hasTools && !hasWebSearch) return;
|
||||||
@@ -1524,7 +1531,7 @@ public class AnthropicService
|
|||||||
var tools = new List<object>();
|
var tools = new List<object>();
|
||||||
if (hasTools)
|
if (hasTools)
|
||||||
{
|
{
|
||||||
var allTools = _toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory, includeUserChoice: allowUserChoice, includeRemote: includeRemote);
|
var allTools = _toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory, includeUserChoice: allowUserChoice, includeRemote: includeRemote, includeMemorySearch: includeMemorySearch);
|
||||||
if (planMode)
|
if (planMode)
|
||||||
allTools = _toolService.FilterReadOnlyTools(allTools);
|
allTools = _toolService.FilterReadOnlyTools(allTools);
|
||||||
tools.AddRange(allTools);
|
tools.AddRange(allTools);
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dapper;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace Gott.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Background-Service der Messages embedded:
|
||||||
|
/// 1. Beim Start: Backfill aller bestehenden Text-Messages
|
||||||
|
/// 2. Laufend: Neue Messages über Queue verarbeiten
|
||||||
|
/// </summary>
|
||||||
|
public class EmbeddingBackgroundService : BackgroundService
|
||||||
|
{
|
||||||
|
readonly VectorSearchService _vectorSearch;
|
||||||
|
readonly EmbeddingService _embedding;
|
||||||
|
readonly string _dbConnectionString;
|
||||||
|
readonly ConcurrentQueue<EmbeddingJob> _queue = new();
|
||||||
|
readonly SemaphoreSlim _signal = new(0);
|
||||||
|
|
||||||
|
public EmbeddingBackgroundService(VectorSearchService vectorSearch, EmbeddingService embedding, string dbPath)
|
||||||
|
{
|
||||||
|
_vectorSearch = vectorSearch;
|
||||||
|
_embedding = embedding;
|
||||||
|
_dbConnectionString = $"Data Source={dbPath}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Enqueue eine neue Message zum Embedden.</summary>
|
||||||
|
public void Enqueue(long messageId, string chainId, string sessionId, string agentId, string role, string text)
|
||||||
|
{
|
||||||
|
_queue.Enqueue(new EmbeddingJob(messageId, chainId, sessionId, agentId, role, text));
|
||||||
|
_signal.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (!_embedding.IsAvailable)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[EmbeddingBG] Embedding model not available – service disabled.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kurz warten bis App vollständig gestartet
|
||||||
|
await Task.Delay(3000, ct);
|
||||||
|
|
||||||
|
// Backfill bestehender Messages
|
||||||
|
await BackfillAsync(ct);
|
||||||
|
|
||||||
|
// Live-Embedding neuer Messages
|
||||||
|
Console.WriteLine("[EmbeddingBG] Waiting for new messages...");
|
||||||
|
while (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _signal.WaitAsync(ct);
|
||||||
|
while (_queue.TryDequeue(out var job))
|
||||||
|
{
|
||||||
|
if (ct.IsCancellationRequested) break;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _vectorSearch.EmbedMessageAsync(job.MessageId, job.ChainId, job.SessionId,
|
||||||
|
job.AgentId, job.Role, job.Text);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[EmbeddingBG] Error embedding message {job.MessageId}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task BackfillAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[EmbeddingBG] Starting backfill of existing messages...");
|
||||||
|
|
||||||
|
var existingIds = await _vectorSearch.GetEmbeddedMessageIdsAsync();
|
||||||
|
Console.WriteLine($"[EmbeddingBG] Already embedded: {existingIds.Count} messages");
|
||||||
|
|
||||||
|
using var conn = new SqliteConnection(_dbConnectionString);
|
||||||
|
await conn.OpenAsync(ct);
|
||||||
|
|
||||||
|
// Alle text-Messages laden (user + assistant, inkl. archivierte Sessions)
|
||||||
|
var messages = await conn.QueryAsync<BackfillRow>(@"
|
||||||
|
SELECT m.id, m.chain_id, c.session_id, c.agent_id, m.role, m.content, m.type
|
||||||
|
FROM messages m
|
||||||
|
JOIN chains c ON m.chain_id = c.chain_id
|
||||||
|
WHERE m.type = 'text'
|
||||||
|
AND m.role IN ('user', 'assistant')
|
||||||
|
AND m.content IS NOT NULL
|
||||||
|
AND LENGTH(m.content) >= 10
|
||||||
|
AND m.deleted_on IS NULL
|
||||||
|
ORDER BY m.id ASC
|
||||||
|
");
|
||||||
|
|
||||||
|
int total = 0;
|
||||||
|
int embedded = 0;
|
||||||
|
int skipped = 0;
|
||||||
|
|
||||||
|
foreach (var msg in messages)
|
||||||
|
{
|
||||||
|
if (ct.IsCancellationRequested) break;
|
||||||
|
total++;
|
||||||
|
|
||||||
|
if (existingIds.Contains(msg.Id))
|
||||||
|
{
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// SessionId kann auch aus archive kommen
|
||||||
|
var sessionId = msg.SessionId ?? "";
|
||||||
|
await _vectorSearch.EmbedMessageAsync(msg.Id, msg.ChainId, sessionId,
|
||||||
|
msg.AgentId, msg.Role, msg.Content);
|
||||||
|
embedded++;
|
||||||
|
|
||||||
|
if (embedded % 100 == 0)
|
||||||
|
Console.WriteLine($"[EmbeddingBG] Backfill progress: {embedded} embedded, {skipped} skipped, {total} total");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[EmbeddingBG] Error backfilling message {msg.Id}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"[EmbeddingBG] Backfill complete: {embedded} new embeddings, {skipped} skipped, {total} total messages.");
|
||||||
|
}
|
||||||
|
|
||||||
|
record EmbeddingJob(long MessageId, string ChainId, string SessionId, string AgentId, string Role, string Text);
|
||||||
|
|
||||||
|
class BackfillRow
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string ChainId { get; set; }
|
||||||
|
public string SessionId { get; set; }
|
||||||
|
public string AgentId { get; set; }
|
||||||
|
public string Role { get; set; }
|
||||||
|
public string Content { get; set; }
|
||||||
|
public string Type { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.ML.OnnxRuntime;
|
||||||
|
using Microsoft.ML.OnnxRuntime.Tensors;
|
||||||
|
|
||||||
|
namespace Gott.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lokaler Embedding-Service basierend auf all-MiniLM-L6-v2 (ONNX).
|
||||||
|
/// Generiert 384-dimensionale Sentence-Embeddings für semantische Suche.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class EmbeddingService : IDisposable
|
||||||
|
{
|
||||||
|
readonly InferenceSession _session;
|
||||||
|
readonly Dictionary<string, int> _vocab;
|
||||||
|
readonly int _clsId;
|
||||||
|
readonly int _sepId;
|
||||||
|
readonly int _unkId;
|
||||||
|
readonly int _maxSeqLen = 128;
|
||||||
|
bool _disposed;
|
||||||
|
|
||||||
|
public bool IsAvailable { get; }
|
||||||
|
public int Dimensions => 384;
|
||||||
|
|
||||||
|
public EmbeddingService()
|
||||||
|
{
|
||||||
|
var modelDir = Path.Combine(AppContext.BaseDirectory, "data", "models", "all-MiniLM-L6-v2");
|
||||||
|
var modelPath = Path.Combine(modelDir, "model.onnx");
|
||||||
|
var vocabPath = Path.Combine(modelDir, "vocab.txt");
|
||||||
|
|
||||||
|
if (!File.Exists(modelPath) || !File.Exists(vocabPath))
|
||||||
|
{
|
||||||
|
Console.WriteLine("[Embedding] Model not found at " + modelDir + " – service disabled.");
|
||||||
|
_vocab = new Dictionary<string, int>();
|
||||||
|
IsAvailable = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var opts = new SessionOptions();
|
||||||
|
opts.InterOpNumThreads = 1;
|
||||||
|
opts.IntraOpNumThreads = 2;
|
||||||
|
opts.GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL;
|
||||||
|
_session = new InferenceSession(modelPath, opts);
|
||||||
|
|
||||||
|
_vocab = new Dictionary<string, int>(50000);
|
||||||
|
var lines = File.ReadAllLines(vocabPath);
|
||||||
|
for (int i = 0; i < lines.Length; i++)
|
||||||
|
_vocab[lines[i]] = i;
|
||||||
|
|
||||||
|
_clsId = _vocab.GetValueOrDefault("[CLS]", 101);
|
||||||
|
_sepId = _vocab.GetValueOrDefault("[SEP]", 102);
|
||||||
|
_unkId = _vocab.GetValueOrDefault("[UNK]", 100);
|
||||||
|
|
||||||
|
IsAvailable = true;
|
||||||
|
Console.WriteLine($"[Embedding] Model loaded. Vocab: {_vocab.Count} tokens.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Embedding] Failed to load model: {ex.Message}");
|
||||||
|
_vocab = new Dictionary<string, int>();
|
||||||
|
IsAvailable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Generiert ein 384-dim Embedding für einen Text.</summary>
|
||||||
|
public float[] GenerateEmbedding(string text)
|
||||||
|
{
|
||||||
|
if (!IsAvailable) return Array.Empty<float>();
|
||||||
|
|
||||||
|
var tokenIds = Tokenize(text);
|
||||||
|
var seqLen = tokenIds.Count;
|
||||||
|
|
||||||
|
var inputIds = new long[seqLen];
|
||||||
|
var attentionMask = new long[seqLen];
|
||||||
|
var tokenTypeIds = new long[seqLen];
|
||||||
|
|
||||||
|
for (int i = 0; i < seqLen; i++)
|
||||||
|
{
|
||||||
|
inputIds[i] = tokenIds[i];
|
||||||
|
attentionMask[i] = 1;
|
||||||
|
tokenTypeIds[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var shape = new long[] { 1, seqLen };
|
||||||
|
|
||||||
|
var inputs = new List<NamedOnnxValue>
|
||||||
|
{
|
||||||
|
NamedOnnxValue.CreateFromTensor("input_ids",
|
||||||
|
new DenseTensor<long>(inputIds, new[] { 1, seqLen })),
|
||||||
|
NamedOnnxValue.CreateFromTensor("attention_mask",
|
||||||
|
new DenseTensor<long>(attentionMask, new[] { 1, seqLen })),
|
||||||
|
NamedOnnxValue.CreateFromTensor("token_type_ids",
|
||||||
|
new DenseTensor<long>(tokenTypeIds, new[] { 1, seqLen }))
|
||||||
|
};
|
||||||
|
|
||||||
|
using var results = _session.Run(inputs);
|
||||||
|
var output = results.First().AsTensor<float>();
|
||||||
|
|
||||||
|
// Mean Pooling: Mittelwert über alle Token-Embeddings (attention_mask berücksichtigen)
|
||||||
|
var embedding = new float[Dimensions];
|
||||||
|
for (int t = 0; t < seqLen; t++)
|
||||||
|
{
|
||||||
|
for (int d = 0; d < Dimensions; d++)
|
||||||
|
embedding[d] += output[0, t, d];
|
||||||
|
}
|
||||||
|
for (int d = 0; d < Dimensions; d++)
|
||||||
|
embedding[d] /= seqLen;
|
||||||
|
|
||||||
|
// L2 Normalize
|
||||||
|
Normalize(embedding);
|
||||||
|
|
||||||
|
return embedding;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>WordPiece-Tokenisierung mit BERT-Normalisierung.</summary>
|
||||||
|
List<int> Tokenize(string text)
|
||||||
|
{
|
||||||
|
// BERT Normalization: lowercase, clean control chars
|
||||||
|
text = text.ToLowerInvariant();
|
||||||
|
var sb = new StringBuilder(text.Length);
|
||||||
|
foreach (var c in text)
|
||||||
|
{
|
||||||
|
if (c == 0 || c == 0xFFFD || char.IsControl(c)) continue;
|
||||||
|
if (IsChineseChar(c)) { sb.Append(' '); sb.Append(c); sb.Append(' '); }
|
||||||
|
else sb.Append(c);
|
||||||
|
}
|
||||||
|
text = sb.ToString();
|
||||||
|
|
||||||
|
// BERT Pre-tokenize: split on whitespace and punctuation
|
||||||
|
var words = BertPreTokenize(text);
|
||||||
|
|
||||||
|
var tokenIds = new List<int> { _clsId };
|
||||||
|
int maxTokens = _maxSeqLen - 2; // Reserve [CLS] and [SEP]
|
||||||
|
|
||||||
|
foreach (var word in words)
|
||||||
|
{
|
||||||
|
if (tokenIds.Count - 1 >= maxTokens) break;
|
||||||
|
|
||||||
|
var subTokens = WordPieceTokenize(word);
|
||||||
|
foreach (var st in subTokens)
|
||||||
|
{
|
||||||
|
if (tokenIds.Count - 1 >= maxTokens) break;
|
||||||
|
tokenIds.Add(st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenIds.Add(_sepId);
|
||||||
|
return tokenIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> WordPieceTokenize(string word)
|
||||||
|
{
|
||||||
|
if (word.Length > 100)
|
||||||
|
return new List<int> { _unkId };
|
||||||
|
|
||||||
|
var tokens = new List<int>();
|
||||||
|
int start = 0;
|
||||||
|
|
||||||
|
while (start < word.Length)
|
||||||
|
{
|
||||||
|
int end = word.Length;
|
||||||
|
int foundId = -1;
|
||||||
|
|
||||||
|
while (start < end)
|
||||||
|
{
|
||||||
|
var substr = word.Substring(start, end - start);
|
||||||
|
if (start > 0) substr = "##" + substr;
|
||||||
|
|
||||||
|
if (_vocab.TryGetValue(substr, out var id))
|
||||||
|
{
|
||||||
|
foundId = id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
end--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundId < 0)
|
||||||
|
{
|
||||||
|
tokens.Add(_unkId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens.Add(foundId);
|
||||||
|
start = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<string> BertPreTokenize(string text)
|
||||||
|
{
|
||||||
|
var words = new List<string>();
|
||||||
|
var current = new StringBuilder();
|
||||||
|
|
||||||
|
foreach (var c in text)
|
||||||
|
{
|
||||||
|
if (char.IsWhiteSpace(c))
|
||||||
|
{
|
||||||
|
if (current.Length > 0) { words.Add(current.ToString()); current.Clear(); }
|
||||||
|
}
|
||||||
|
else if (IsPunctuation(c))
|
||||||
|
{
|
||||||
|
if (current.Length > 0) { words.Add(current.ToString()); current.Clear(); }
|
||||||
|
words.Add(c.ToString());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
current.Append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current.Length > 0) words.Add(current.ToString());
|
||||||
|
return words;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsPunctuation(char c)
|
||||||
|
{
|
||||||
|
var cat = char.GetUnicodeCategory(c);
|
||||||
|
return cat == UnicodeCategory.ConnectorPunctuation ||
|
||||||
|
cat == UnicodeCategory.DashPunctuation ||
|
||||||
|
cat == UnicodeCategory.OpenPunctuation ||
|
||||||
|
cat == UnicodeCategory.ClosePunctuation ||
|
||||||
|
cat == UnicodeCategory.InitialQuotePunctuation ||
|
||||||
|
cat == UnicodeCategory.FinalQuotePunctuation ||
|
||||||
|
cat == UnicodeCategory.OtherPunctuation;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsChineseChar(char c)
|
||||||
|
{
|
||||||
|
int cp = c;
|
||||||
|
return (cp >= 0x4E00 && cp <= 0x9FFF) || (cp >= 0x3400 && cp <= 0x4DBF) ||
|
||||||
|
(cp >= 0x20000 && cp <= 0x2A6DF) || (cp >= 0x2A700 && cp <= 0x2B73F) ||
|
||||||
|
(cp >= 0x2B740 && cp <= 0x2B81F) || (cp >= 0x2B820 && cp <= 0x2CEAF) ||
|
||||||
|
(cp >= 0xF900 && cp <= 0xFAFF) || (cp >= 0x2F800 && cp <= 0x2FA1F);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Normalize(float[] vec)
|
||||||
|
{
|
||||||
|
double sum = 0;
|
||||||
|
for (int i = 0; i < vec.Length; i++) sum += vec[i] * vec[i];
|
||||||
|
var norm = (float)Math.Sqrt(sum);
|
||||||
|
if (norm > 0)
|
||||||
|
for (int i = 0; i < vec.Length; i++) vec[i] /= norm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Cosine Similarity zwischen zwei normalisierten Vektoren (= Dot Product).</summary>
|
||||||
|
public static float CosineSimilarity(float[] a, float[] b)
|
||||||
|
{
|
||||||
|
if (a.Length != b.Length) return 0;
|
||||||
|
float dot = 0;
|
||||||
|
for (int i = 0; i < a.Length; i++) dot += a[i] * b[i];
|
||||||
|
return dot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Serialisiert float[] zu byte[] für DB-Speicherung.</summary>
|
||||||
|
public static byte[] ToBytes(float[] vec)
|
||||||
|
{
|
||||||
|
var bytes = new byte[vec.Length * 4];
|
||||||
|
Buffer.BlockCopy(vec, 0, bytes, 0, bytes.Length);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Deserialisiert byte[] aus DB zu float[].</summary>
|
||||||
|
public static float[] FromBytes(byte[] bytes)
|
||||||
|
{
|
||||||
|
var vec = new float[bytes.Length / 4];
|
||||||
|
Buffer.BlockCopy(bytes, 0, vec, 0, bytes.Length);
|
||||||
|
return vec;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (!_disposed)
|
||||||
|
{
|
||||||
|
_session?.Dispose();
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+47
-3
@@ -33,7 +33,7 @@ public class ToolService
|
|||||||
private static readonly HashSet<string> ReadOnlyToolNames = new HashSet<string>
|
private static readonly HashSet<string> ReadOnlyToolNames = new HashSet<string>
|
||||||
{
|
{
|
||||||
"read_file", "list_files", "glob", "grep", "execute", "todo_write",
|
"read_file", "list_files", "glob", "grep", "execute", "todo_write",
|
||||||
"memory_read", "memory_write", "memory_list",
|
"memory_read", "memory_write", "memory_list", "memory_search",
|
||||||
"remote_execute", AskUserChoiceToolName
|
"remote_execute", AskUserChoiceToolName
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ public class ToolService
|
|||||||
return filtered.ToArray();
|
return filtered.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public object[] GetToolDefinitions(bool includeFileTools = false, bool includeMemory = false, bool includeUserChoice = true, bool includeRemote = false)
|
public object[] GetToolDefinitions(bool includeFileTools = false, bool includeMemory = false, bool includeUserChoice = true, bool includeRemote = false, bool includeMemorySearch = false)
|
||||||
{
|
{
|
||||||
var allTools = new List<object>();
|
var allTools = new List<object>();
|
||||||
if (includeUserChoice)
|
if (includeUserChoice)
|
||||||
@@ -63,6 +63,8 @@ public class ToolService
|
|||||||
allTools.AddRange(GetFileTools());
|
allTools.AddRange(GetFileTools());
|
||||||
if (includeMemory)
|
if (includeMemory)
|
||||||
allTools.AddRange(GetMemoryToolDefinitions());
|
allTools.AddRange(GetMemoryToolDefinitions());
|
||||||
|
if (includeMemorySearch)
|
||||||
|
allTools.Add(GetMemorySearchTool());
|
||||||
if (includeRemote)
|
if (includeRemote)
|
||||||
allTools.Add(GetRemoteExecuteTool());
|
allTools.Add(GetRemoteExecuteTool());
|
||||||
return allTools.ToArray();
|
return allTools.ToArray();
|
||||||
@@ -264,6 +266,25 @@ public class ToolService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private object GetMemorySearchTool()
|
||||||
|
{
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
name = "memory_search",
|
||||||
|
description = "Semantische Suche über alle vergangenen Chat-Nachrichten (inklusive archivierter Sessions). Findet relevante Informationen aus früheren Gesprächen anhand von Bedeutung, nicht exakten Wörtern. Nutze dies wenn du Kontext aus vergangenen Interaktionen brauchst.",
|
||||||
|
input_schema = new
|
||||||
|
{
|
||||||
|
type = "object",
|
||||||
|
properties = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["query"] = new { type = "string", description = "Suchanfrage – beschreibe wonach du suchst (z.B. 'Druckerproblem letzte Woche', 'Server-Migration Konfiguration')" },
|
||||||
|
["top_n"] = new { type = "integer", description = "Anzahl der Ergebnisse (Default: 10, Max: 30)" }
|
||||||
|
},
|
||||||
|
required = new[] { "query" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private object GetRemoteExecuteTool()
|
private object GetRemoteExecuteTool()
|
||||||
{
|
{
|
||||||
return new
|
return new
|
||||||
@@ -333,7 +354,7 @@ public class ToolService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> ExecuteTool(string name, JsonElement input, string workspace, MemoryService memoryService = null, string agentId = null, CancellationToken cancellationToken = default, RemoteConnectionManager remoteMgr = null, string sessionId = null)
|
public async Task<string> ExecuteTool(string name, JsonElement input, string workspace, MemoryService memoryService = null, string agentId = null, CancellationToken cancellationToken = default, RemoteConnectionManager remoteMgr = null, string sessionId = null, VectorSearchService vectorSearch = null)
|
||||||
{
|
{
|
||||||
switch (name)
|
switch (name)
|
||||||
{
|
{
|
||||||
@@ -617,6 +638,29 @@ public class ToolService
|
|||||||
var files = memoryService.ListMemoryFiles(agentId, subPath);
|
var files = memoryService.ListMemoryFiles(agentId, subPath);
|
||||||
return files.Count == 0 ? "(keine Memory-Dateien vorhanden)" : string.Join("\n", files);
|
return files.Count == 0 ? "(keine Memory-Dateien vorhanden)" : string.Join("\n", files);
|
||||||
}
|
}
|
||||||
|
case "memory_search":
|
||||||
|
{
|
||||||
|
if (vectorSearch == null)
|
||||||
|
return "Fehler: Semantische Suche nicht verfügbar";
|
||||||
|
var query = input.GetProperty("query").GetString() ?? "";
|
||||||
|
var topN = input.TryGetProperty("top_n", out var tn) ? tn.GetInt32() : 10;
|
||||||
|
if (topN > 30) topN = 30;
|
||||||
|
if (topN < 1) topN = 1;
|
||||||
|
var results = await vectorSearch.SearchAsync(query, agentId, topN);
|
||||||
|
if (results.Count == 0)
|
||||||
|
return "Keine relevanten Ergebnisse gefunden.";
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine($"Gefunden: {results.Count} relevante Einträge\n");
|
||||||
|
for (int i = 0; i < results.Count; i++)
|
||||||
|
{
|
||||||
|
var r = results[i];
|
||||||
|
var date = DateTime.TryParse(r.CreatedAt, out var dt) ? dt.ToString("yyyy-MM-dd HH:mm") : r.CreatedAt;
|
||||||
|
sb.AppendLine($"[{i + 1}] Score: {r.Score:F2} | {r.Role} | {date} | Session: {r.SessionId}");
|
||||||
|
sb.AppendLine(r.TextPreview);
|
||||||
|
sb.AppendLine();
|
||||||
|
}
|
||||||
|
return sb.ToString().TrimEnd();
|
||||||
|
}
|
||||||
case "remote_execute":
|
case "remote_execute":
|
||||||
{
|
{
|
||||||
if (remoteMgr == null || string.IsNullOrEmpty(sessionId))
|
if (remoteMgr == null || string.IsNullOrEmpty(sessionId))
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dapper;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
|
||||||
|
namespace Gott.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verwaltet Message-Embeddings in SQLite und bietet semantische Suche.
|
||||||
|
/// </summary>
|
||||||
|
public class VectorSearchService
|
||||||
|
{
|
||||||
|
readonly string _connectionString;
|
||||||
|
readonly EmbeddingService _embedding;
|
||||||
|
readonly SemaphoreSlim _embeddingLock = new(1, 1);
|
||||||
|
|
||||||
|
public VectorSearchService(string dbPath, EmbeddingService embedding)
|
||||||
|
{
|
||||||
|
_connectionString = $"Data Source={dbPath}";
|
||||||
|
_embedding = embedding;
|
||||||
|
InitializeSchema();
|
||||||
|
}
|
||||||
|
|
||||||
|
SqliteConnection CreateConnection()
|
||||||
|
{
|
||||||
|
var conn = new SqliteConnection(_connectionString);
|
||||||
|
conn.Open();
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InitializeSchema()
|
||||||
|
{
|
||||||
|
using var conn = CreateConnection();
|
||||||
|
conn.Execute(@"
|
||||||
|
CREATE TABLE IF NOT EXISTS message_embeddings (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
message_id INTEGER NOT NULL UNIQUE,
|
||||||
|
chain_id TEXT NOT NULL,
|
||||||
|
session_id TEXT NOT NULL,
|
||||||
|
agent_id TEXT,
|
||||||
|
role TEXT NOT NULL,
|
||||||
|
text_preview TEXT,
|
||||||
|
vector BLOB NOT NULL,
|
||||||
|
created_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_emb_session ON message_embeddings(session_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_emb_agent ON message_embeddings(agent_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_emb_message ON message_embeddings(message_id);
|
||||||
|
");
|
||||||
|
Console.WriteLine("[VectorSearch] Schema initialized.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generiert und speichert ein Embedding für eine Message.
|
||||||
|
/// Thread-safe durch SemaphoreSlim (ONNX InferenceSession ist nicht thread-safe).
|
||||||
|
/// </summary>
|
||||||
|
public async Task EmbedMessageAsync(long messageId, string chainId, string sessionId,
|
||||||
|
string agentId, string role, string text)
|
||||||
|
{
|
||||||
|
if (!_embedding.IsAvailable || string.IsNullOrWhiteSpace(text)) return;
|
||||||
|
if (text.Length < 10) return; // Zu kurze Texte sind nicht sinnvoll
|
||||||
|
|
||||||
|
// Prüfe ob schon embedded
|
||||||
|
using var conn = CreateConnection();
|
||||||
|
var exists = await conn.ExecuteScalarAsync<int>(
|
||||||
|
"SELECT COUNT(*) FROM message_embeddings WHERE message_id = @messageId",
|
||||||
|
new { messageId });
|
||||||
|
if (exists > 0) return;
|
||||||
|
|
||||||
|
// Embedding generieren (thread-safe)
|
||||||
|
float[] vector;
|
||||||
|
await _embeddingLock.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
vector = _embedding.GenerateEmbedding(text);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_embeddingLock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vector.Length == 0) return;
|
||||||
|
|
||||||
|
var preview = text.Length > 200 ? text.Substring(0, 200) : text;
|
||||||
|
|
||||||
|
await conn.ExecuteAsync(@"
|
||||||
|
INSERT OR IGNORE INTO message_embeddings
|
||||||
|
(message_id, chain_id, session_id, agent_id, role, text_preview, vector, created_at)
|
||||||
|
VALUES
|
||||||
|
(@messageId, @chainId, @sessionId, @agentId, @role, @preview, @vector, @createdAt)",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
messageId,
|
||||||
|
chainId,
|
||||||
|
sessionId,
|
||||||
|
agentId = agentId ?? "",
|
||||||
|
role,
|
||||||
|
preview,
|
||||||
|
vector = EmbeddingService.ToBytes(vector),
|
||||||
|
createdAt = DateTime.UtcNow.ToString("o")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Semantische Suche: Findet die N ähnlichsten Messages zu einer Query.
|
||||||
|
/// Durchsucht alle Sessions eines Agents (oder alle wenn agentId null).
|
||||||
|
/// </summary>
|
||||||
|
public async Task<List<SearchResult>> SearchAsync(string query, string agentId = null,
|
||||||
|
int topN = 10, float minScore = 0.3f)
|
||||||
|
{
|
||||||
|
if (!_embedding.IsAvailable) return new List<SearchResult>();
|
||||||
|
|
||||||
|
// Query embedden
|
||||||
|
float[] queryVec;
|
||||||
|
await _embeddingLock.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
queryVec = _embedding.GenerateEmbedding(query);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_embeddingLock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryVec.Length == 0) return new List<SearchResult>();
|
||||||
|
|
||||||
|
// Alle Embeddings laden und Cosine Similarity berechnen
|
||||||
|
using var conn = CreateConnection();
|
||||||
|
string sql;
|
||||||
|
object param;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(agentId))
|
||||||
|
{
|
||||||
|
sql = "SELECT message_id, chain_id, session_id, agent_id, role, text_preview, vector, created_at FROM message_embeddings WHERE agent_id = @agentId";
|
||||||
|
param = new { agentId };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sql = "SELECT message_id, chain_id, session_id, agent_id, role, text_preview, vector, created_at FROM message_embeddings";
|
||||||
|
param = new { };
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows = await conn.QueryAsync<EmbeddingRow>(sql, param);
|
||||||
|
var results = new List<SearchResult>();
|
||||||
|
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
var vec = EmbeddingService.FromBytes(row.Vector);
|
||||||
|
var score = EmbeddingService.CosineSimilarity(queryVec, vec);
|
||||||
|
if (score >= minScore)
|
||||||
|
{
|
||||||
|
results.Add(new SearchResult
|
||||||
|
{
|
||||||
|
MessageId = row.MessageId,
|
||||||
|
ChainId = row.ChainId,
|
||||||
|
SessionId = row.SessionId,
|
||||||
|
AgentId = row.AgentId,
|
||||||
|
Role = row.Role,
|
||||||
|
TextPreview = row.TextPreview,
|
||||||
|
Score = score,
|
||||||
|
CreatedAt = row.CreatedAt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.OrderByDescending(r => r.Score).Take(topN).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gibt die Anzahl der gespeicherten Embeddings zurück.
|
||||||
|
/// </summary>
|
||||||
|
public int GetEmbeddingCount()
|
||||||
|
{
|
||||||
|
using var conn = CreateConnection();
|
||||||
|
return conn.ExecuteScalar<int>("SELECT COUNT(*) FROM message_embeddings");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gibt die IDs aller bereits eingebetteten Messages zurück.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<HashSet<long>> GetEmbeddedMessageIdsAsync()
|
||||||
|
{
|
||||||
|
using var conn = CreateConnection();
|
||||||
|
var ids = await conn.QueryAsync<long>("SELECT message_id FROM message_embeddings");
|
||||||
|
return new HashSet<long>(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EmbeddingRow
|
||||||
|
{
|
||||||
|
public long MessageId { get; set; }
|
||||||
|
public string ChainId { get; set; }
|
||||||
|
public string SessionId { get; set; }
|
||||||
|
public string AgentId { get; set; }
|
||||||
|
public string Role { get; set; }
|
||||||
|
public string TextPreview { get; set; }
|
||||||
|
public byte[] Vector { get; set; }
|
||||||
|
public string CreatedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SearchResult
|
||||||
|
{
|
||||||
|
public long MessageId { get; set; }
|
||||||
|
public string ChainId { get; set; }
|
||||||
|
public string SessionId { get; set; }
|
||||||
|
public string AgentId { get; set; }
|
||||||
|
public string Role { get; set; }
|
||||||
|
public string TextPreview { get; set; }
|
||||||
|
public float Score { get; set; }
|
||||||
|
public string CreatedAt { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,7 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.3" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.3" />
|
||||||
<PackageReference Include="Dapper" Version="2.1.66" />
|
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.3" />
|
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.3" />
|
||||||
|
<PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.24.3" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ const TOOL_DISPLAY_NAMES = {
|
|||||||
delete_file: 'Datei löschen',
|
delete_file: 'Datei löschen',
|
||||||
memory_write: 'Erinnerungen festhalten',
|
memory_write: 'Erinnerungen festhalten',
|
||||||
web_search: 'Web-Suche',
|
web_search: 'Web-Suche',
|
||||||
remote_execute: 'Remote-Befehl'
|
remote_execute: 'Remote-Befehl',
|
||||||
|
memory_search: 'Erinnerungen durchsuchen'
|
||||||
};
|
};
|
||||||
|
|
||||||
function isToolVisible(toolName) {
|
function isToolVisible(toolName) {
|
||||||
|
|||||||
Reference in new issue
Block a user