139 lines
3.9 KiB
Go
139 lines
3.9 KiB
Go
// 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])
|
||
}
|