Files
ai-agent/internal/brain/ask.go
Christoph K. 92f520101a sync
2026-03-12 17:34:49 +01:00

158 lines
4.4 KiB
Go
Executable File
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.
// ask.go Sucht relevante Chunks in Qdrant und beantwortet Fragen mit einem LLM
package brain
import (
"context"
"fmt"
"log"
"strings"
pb "github.com/qdrant/go-client/qdrant"
openai "github.com/sashabaranov/go-openai"
"google.golang.org/grpc/metadata"
"my-brain-importer/internal/config"
)
// KnowledgeChunk repräsentiert ein Suchergebnis aus Qdrant.
type KnowledgeChunk struct {
Text string
Score float32
Source string
}
// Ask sucht relevante Chunks und generiert eine LLM-Antwort per Streaming.
func Ask(question string) {
ctx := context.Background()
ctx = metadata.AppendToOutgoingContext(ctx, "api-key", config.Cfg.Qdrant.APIKey)
fmt.Printf("🤔 Frage: \"%s\"\n\n", question)
embClient := config.NewEmbeddingClient()
chatClient := config.NewChatClient()
fmt.Println("🔍 Durchsuche lokale Wissensdatenbank...")
chunks := searchKnowledge(ctx, embClient, question)
if len(chunks) == 0 {
fmt.Println("\n❌ Keine relevanten Informationen in der Datenbank gefunden.")
fmt.Println(" Füge mehr Daten mit './bin/ingest' hinzu.")
return
}
contextText := buildContext(chunks)
fmt.Printf("✅ %d relevante Informationen gefunden\n\n", len(chunks))
systemPrompt := `Du bist ein hilfreicher persönlicher Assistent.
Deine Aufgabe ist es, Fragen basierend auf den bereitgestellten Informationen zu beantworten.
WICHTIGE REGELN:
- Antworte nur basierend auf den bereitgestellten Informationen
- Wenn die Informationen die Frage nicht beantworten, sage das ehrlich
- Antworte auf Deutsch
- Sei präzise und direkt
- Erfinde keine Informationen hinzu`
userPrompt := fmt.Sprintf(`Hier sind die relevanten Informationen aus meiner Wissensdatenbank:
%s
Basierend auf diesen Informationen, beantworte bitte folgende Frage:
%s`, contextText, question)
fmt.Println("🧠 Generiere Antwort mit lokalem Modell...")
fmt.Println(strings.Repeat("═", 80))
stream, err := chatClient.CreateChatCompletionStream(ctx, openai.ChatCompletionRequest{
Model: config.Cfg.Chat.Model,
Messages: []openai.ChatCompletionMessage{
{Role: openai.ChatMessageRoleSystem, Content: systemPrompt},
{Role: openai.ChatMessageRoleUser, Content: userPrompt},
},
Temperature: 0.7,
MaxTokens: 500,
})
if err != nil {
log.Fatalf("❌ LLM Fehler: %v", err)
}
defer stream.Close()
fmt.Print("\n💬 Antwort:\n\n")
for {
response, err := stream.Recv()
if err != nil {
break
}
if len(response.Choices) > 0 {
fmt.Print(response.Choices[0].Delta.Content)
}
}
fmt.Print("\n\n")
fmt.Println(strings.Repeat("═", 80))
fmt.Print("\n📚 Verwendete Quellen:\n")
for i, chunk := range chunks {
preview := chunk.Text
if len(preview) > 80 {
preview = preview[:80] + "..."
}
fmt.Printf(" [%d] %.1f%% - %s\n %s\n", i+1, chunk.Score*100, chunk.Source, preview)
}
}
func searchKnowledge(ctx context.Context, embClient *openai.Client, query string) []KnowledgeChunk {
embResp, err := embClient.CreateEmbeddings(ctx, openai.EmbeddingRequest{
Input: []string{query},
Model: openai.EmbeddingModel(config.Cfg.Embedding.Model),
})
if err != nil {
log.Printf("❌ Embedding Fehler: %v", err)
return nil
}
conn := config.NewQdrantConn()
defer conn.Close()
searchResult, err := pb.NewPointsClient(conn).Search(ctx, &pb.SearchPoints{
CollectionName: config.Cfg.Qdrant.Collection,
Vector: embResp.Data[0].Embedding,
Limit: config.Cfg.TopK,
WithPayload: &pb.WithPayloadSelector{
SelectorOptions: &pb.WithPayloadSelector_Enable{Enable: true},
},
ScoreThreshold: floatPtr(config.Cfg.ScoreThreshold),
})
if err != nil {
log.Printf("❌ Suche fehlgeschlagen: %v", err)
return nil
}
var chunks []KnowledgeChunk
seen := make(map[string]bool)
for _, hit := range searchResult.Result {
text := hit.Payload["text"].GetStringValue()
if seen[text] {
continue
}
seen[text] = true
chunks = append(chunks, KnowledgeChunk{
Text: text,
Score: hit.Score,
Source: hit.Payload["source"].GetStringValue(),
})
}
return chunks
}
func buildContext(chunks []KnowledgeChunk) string {
var b strings.Builder
for i, chunk := range chunks {
fmt.Fprintf(&b, "--- Information %d (Relevanz: %.1f%%) ---\n", i+1, chunk.Score*100)
b.WriteString(chunk.Text)
b.WriteString("\n\n")
}
return b.String()
}
func floatPtr(f float32) *float32 { return &f }