// ask.go – Sucht relevante Chunks in Qdrant und beantwortet Fragen mit einem LLM package brain import ( "context" "fmt" "log" "log/slog" "strings" "time" pb "github.com/qdrant/go-client/qdrant" openai "github.com/sashabaranov/go-openai" "google.golang.org/grpc/metadata" "my-brain-importer/internal/agents" "my-brain-importer/internal/config" ) // KnowledgeChunk repräsentiert ein Suchergebnis aus Qdrant. type KnowledgeChunk struct { Text string Score float32 Source string } // AskQuery sucht relevante Chunks und generiert eine LLM-Antwort. // Gibt die Antwort als String und die verwendeten Quellen zurück. // history enthält vorherige Gesprächsnachrichten (optional, nil für stateless). func AskQuery(question string, history []agents.HistoryMessage) (string, []KnowledgeChunk, error) { ctx := context.Background() ctx = metadata.AppendToOutgoingContext(ctx, "api-key", config.Cfg.Qdrant.APIKey) embClient := config.NewEmbeddingClient() chatClient := config.NewChatClient() chunks := searchKnowledge(ctx, embClient, question) if len(chunks) == 0 { return "", nil, nil } contextText := buildContext(chunks) coreMemory := LoadCoreMemory() systemPromptBase := `Du bist ein hilfreicher persönlicher Assistent. Beantworte Fragen primär anhand der bereitgestellten Informationen aus der Wissensdatenbank. Ergänze fehlende Details mit deinem eigenen Wissen, kennzeichne dies aber klar mit "Aus meinem Wissen:". WICHTIGE REGELN: - Nutze die bereitgestellten Informationen als Hauptquelle - Ergänze mit eigenem Wissen wenn sinnvoll, kennzeichne es deutlich - Antworte auf Deutsch - Sei präzise und direkt` systemPrompt := systemPromptBase if coreMemory != "" { systemPrompt = systemPromptBase + "\n\n## Fakten über den Nutzer:\n" + coreMemory } userPrompt := fmt.Sprintf(`Hier sind die relevanten Informationen aus meiner Wissensdatenbank: %s Basierend auf diesen Informationen, beantworte bitte folgende Frage: %s`, contextText, question) slog.Debug("[LLM] AskQuery Prompt", "model", config.Cfg.Chat.Model, "history_len", len(history), "system", systemPrompt, "user", userPrompt, ) msgs := []openai.ChatCompletionMessage{ {Role: openai.ChatMessageRoleSystem, Content: systemPrompt}, } for _, h := range history { msgs = append(msgs, openai.ChatCompletionMessage{ Role: h.Role, Content: h.Content, }) } msgs = append(msgs, openai.ChatCompletionMessage{ Role: openai.ChatMessageRoleUser, Content: userPrompt, }) start := time.Now() stream, err := chatClient.CreateChatCompletionStream(ctx, openai.ChatCompletionRequest{ Model: config.Cfg.Chat.Model, Messages: msgs, Temperature: 0.7, MaxTokens: 500, }) if err != nil { return "", nil, fmt.Errorf("LLM Fehler: %w", err) } defer stream.Close() var answer strings.Builder for { response, err := stream.Recv() if err != nil { break } if len(response.Choices) > 0 { answer.WriteString(response.Choices[0].Delta.Content) } } slog.Debug("[LLM] AskQuery Antwort", "dauer", time.Since(start).Round(time.Millisecond), "zeichen", answer.Len(), "antwort", answer.String(), ) return answer.String(), chunks, nil } // Ask sucht relevante Chunks und gibt Antwort + Quellen auf stdout aus. func Ask(question string) { fmt.Printf("🤔 Frage: \"%s\"\n\n", question) fmt.Println("🔍 Durchsuche lokale Wissensdatenbank...") answer, chunks, err := AskQuery(question, nil) if err != nil { log.Fatalf("❌ %v", err) } 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 } fmt.Printf("✅ %d relevante Informationen gefunden\n\n", len(chunks)) fmt.Println("🧠 Generiere Antwort mit lokalem Modell...") fmt.Println(strings.Repeat("═", 80)) fmt.Print("\n💬 Antwort:\n\n") fmt.Print(answer) 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 text == "" || 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() } // ChatDirect stellt eine Frage direkt an das LLM ohne Datenbankkontext. func ChatDirect(question string) (string, error) { ctx := context.Background() chatClient := config.NewChatClient() systemPrompt := `Du bist ein hilfreicher persönlicher Assistent. Antworte auf Deutsch, präzise und direkt.` slog.Debug("[LLM] ChatDirect Prompt", "model", config.Cfg.Chat.Model, "user", question, ) start := time.Now() stream, err := chatClient.CreateChatCompletionStream(ctx, openai.ChatCompletionRequest{ Model: config.Cfg.Chat.Model, Messages: []openai.ChatCompletionMessage{ {Role: openai.ChatMessageRoleSystem, Content: systemPrompt}, {Role: openai.ChatMessageRoleUser, Content: question}, }, Temperature: 0.7, MaxTokens: 500, }) if err != nil { return "", fmt.Errorf("LLM Fehler: %w", err) } defer stream.Close() var answer strings.Builder for { response, err := stream.Recv() if err != nil { break } if len(response.Choices) > 0 { answer.WriteString(response.Choices[0].Delta.Content) } } slog.Debug("[LLM] ChatDirect Antwort", "dauer", time.Since(start).Round(time.Millisecond), "zeichen", answer.Len(), "antwort", answer.String(), ) return answer.String(), nil } func floatPtr(f float32) *float32 { return &f }