- 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
149 lines
5.1 KiB
C#
149 lines
5.1 KiB
C#
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; }
|
||
}
|
||
}
|