Files
gott/Services/EmbeddingBackgroundService.cs
T
Meleki add111ffe1 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
2026-03-11 09:22:11 +01:00

149 lines
5.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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; }
}
}