zwischenstand

This commit is contained in:
Christoph K.
2026-03-20 23:24:56 +01:00
parent b1a576f61e
commit 905981cd1e
25 changed files with 3607 additions and 217 deletions

138
internal/triage/triage.go Normal file
View File

@@ -0,0 +1,138 @@
// triage/triage.go Speichert und sucht Email-Triage-Entscheidungen in Qdrant (RAG-Lernen)
// Eigenes Package um Import-Zyklen zwischen brain und email zu vermeiden.
package triage
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"log/slog"
pb "github.com/qdrant/go-client/qdrant"
openai "github.com/sashabaranov/go-openai"
"google.golang.org/grpc/metadata"
"my-brain-importer/internal/config"
)
// TriageResult repräsentiert ein Suchergebnis aus vergangenen Triage-Entscheidungen.
type TriageResult struct {
Text string
Score float32
}
// StoreDecision speichert eine Triage-Entscheidung in Qdrant.
// Bei gleicher Email (deterministischer ID) wird die Entscheidung überschrieben.
func StoreDecision(subject, from string, isImportant bool) error {
label := "wichtig"
if !isImportant {
label = "unwichtig"
}
text := fmt.Sprintf("Email-Triage | Von: %s | Betreff: %s | Entscheidung: %s", from, subject, label)
ctx := context.Background()
ctx = metadata.AppendToOutgoingContext(ctx, "api-key", config.Cfg.Qdrant.APIKey)
embClient := config.NewEmbeddingClient()
embResp, err := embClient.CreateEmbeddings(ctx, openai.EmbeddingRequest{
Input: []string{text},
Model: openai.EmbeddingModel(config.Cfg.Embedding.Model),
})
if err != nil {
return fmt.Errorf("embedding: %w", err)
}
conn := config.NewQdrantConn()
defer conn.Close()
id := triageID(text)
wait := true
_, err = pb.NewPointsClient(conn).Upsert(ctx, &pb.UpsertPoints{
CollectionName: config.Cfg.Qdrant.Collection,
Points: []*pb.PointStruct{{
Id: &pb.PointId{
PointIdOptions: &pb.PointId_Uuid{Uuid: id},
},
Vectors: &pb.Vectors{
VectorsOptions: &pb.Vectors_Vector{
Vector: &pb.Vector{Data: embResp.Data[0].Embedding},
},
},
Payload: map[string]*pb.Value{
"text": {Kind: &pb.Value_StringValue{StringValue: text}},
"source": {Kind: &pb.Value_StringValue{StringValue: "email_triage"}},
"type": {Kind: &pb.Value_StringValue{StringValue: "email_triage"}},
},
}},
Wait: &wait,
})
if err != nil {
return fmt.Errorf("qdrant upsert: %w", err)
}
slog.Debug("[Triage] Entscheidung gespeichert", "betreff", subject, "wichtig", isImportant)
return nil
}
// SearchSimilar sucht ähnliche vergangene Triage-Entscheidungen in Qdrant.
// Gibt bis zu 3 Ergebnisse zurück (nur type=email_triage, Score ≥ 0.7).
func SearchSimilar(query string) []TriageResult {
ctx := context.Background()
ctx = metadata.AppendToOutgoingContext(ctx, "api-key", config.Cfg.Qdrant.APIKey)
embClient := config.NewEmbeddingClient()
embResp, err := embClient.CreateEmbeddings(ctx, openai.EmbeddingRequest{
Input: []string{query},
Model: openai.EmbeddingModel(config.Cfg.Embedding.Model),
})
if err != nil {
slog.Warn("[Triage] Embedding für RAG fehlgeschlagen", "fehler", err)
return nil
}
conn := config.NewQdrantConn()
defer conn.Close()
threshold := float32(0.7)
result, err := pb.NewPointsClient(conn).Search(ctx, &pb.SearchPoints{
CollectionName: config.Cfg.Qdrant.Collection,
Vector: embResp.Data[0].Embedding,
Limit: 3,
WithPayload: &pb.WithPayloadSelector{
SelectorOptions: &pb.WithPayloadSelector_Enable{Enable: true},
},
ScoreThreshold: &threshold,
Filter: &pb.Filter{
Must: []*pb.Condition{{
ConditionOneOf: &pb.Condition_Field{
Field: &pb.FieldCondition{
Key: "type",
Match: &pb.Match{
MatchValue: &pb.Match_Keyword{Keyword: "email_triage"},
},
},
},
}},
},
})
if err != nil {
slog.Warn("[Triage] RAG-Suche fehlgeschlagen", "fehler", err)
return nil
}
var results []TriageResult
for _, hit := range result.Result {
text := hit.Payload["text"].GetStringValue()
if text == "" {
continue
}
results = append(results, TriageResult{Text: text, Score: hit.Score})
}
return results
}
func triageID(text string) string {
hash := sha256.Sum256([]byte("email_triage:" + text))
return hex.EncodeToString(hash[:16])
}