zwischenstand
This commit is contained in:
138
internal/triage/triage.go
Normal file
138
internal/triage/triage.go
Normal 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])
|
||||
}
|
||||
Reference in New Issue
Block a user