Files
Jay Brown 72de690894 Merged in feature/chatbot1 (pull request #223)
Chatbot functionality

* baseline working

* missing test file

* more tests
2026-05-07 20:56:18 +00:00

120 lines
4.0 KiB
Go
Raw Permalink 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.
package bot
import (
"context"
"fmt"
"queryorchestration/internal/database/repository"
"github.com/google/uuid"
)
// BuildConversationHistory returns the last cfg.RecentTurns completed
// turns for the session as a flat slice of ConversationMessage entries
// in ascending ordinal order: each turn produces (user prompt, assistant
// completion). Plan §7 mapping table.
//
// An empty session returns a non-nil zero-length slice so the FastAPI
// payload emits "conversation_history": [] (the swagger requires a
// non-null array).
func (s *Service) BuildConversationHistory(ctx context.Context, sessionID uuid.UUID) ([]ConversationMessage, error) {
rows, err := s.dbCfg.GetDBQueries().GetCompletedTurnsForSessionAscending(ctx, &repository.GetCompletedTurnsForSessionAscendingParams{
SessionID: sessionID,
TurnLimit: int32(s.cfg.RecentTurns), //nolint:gosec // RecentTurns is bounded [1, 5] by ChatbotConfig.Validate
})
if err != nil {
return nil, fmt.Errorf("bot: list completed turns: %w", err)
}
out := make([]ConversationMessage, 0, len(rows)*2)
for _, r := range rows {
userMsg := ConversationMessage{
Role: "user",
Content: r.Prompt,
Timestamp: r.CreatedAt.Time,
}
assistantContent := ""
if r.Completion != nil {
assistantContent = *r.Completion
}
assistantMsg := ConversationMessage{
Role: "assistant",
Content: assistantContent,
Timestamp: r.CreatedAt.Time,
}
out = append(out, userMsg, assistantMsg)
}
return out, nil
}
// BuildChatScope assembles the ChatScope payload sent to FastAPI. Plan
// §7 mapping table:
//
// - Stored doc ids folder-expanded doc ids; stored folder ids preserved.
// - Plan §4 fallback: empty scope tables mean "all documents for the
// client", with folders returning an empty list.
//
// All ids are returned as strings so the swagger's array-of-string
// schema matches without callers re-stringifying. Document deduplication
// is performed in-memory using a UUID set; folder ids are emitted in
// stable insertion order (sorted by SQL).
func (s *Service) BuildChatScope(ctx context.Context, sessionID uuid.UUID, clientID string) (ChatScope, error) {
queries := s.dbCfg.GetDBQueries()
storedDocs, err := queries.ListBotSessionDocumentsForSession(ctx, sessionID)
if err != nil {
return ChatScope{}, fmt.Errorf("bot: list session documents: %w", err)
}
storedFolders, err := queries.ListBotSessionFoldersForSession(ctx, sessionID)
if err != nil {
return ChatScope{}, fmt.Errorf("bot: list session folders: %w", err)
}
// Plan §4 fallback: empty scope tables → all client docs, no folders.
if len(storedDocs) == 0 && len(storedFolders) == 0 {
all, err := queries.ListDocumentsByClient(ctx, clientID)
if err != nil {
return ChatScope{}, fmt.Errorf("bot: list all client documents: %w", err)
}
docIDs := make([]string, 0, len(all))
for _, row := range all {
docIDs = append(docIDs, row.ID.String())
}
return ChatScope{DocumentIDs: docIDs, FolderIDs: []string{}}, nil
}
// Stored docs + folder-expansion. Dedup via set since a stored doc
// may also live under a stored folder.
seen := make(map[uuid.UUID]struct{}, len(storedDocs))
docIDs := make([]string, 0, len(storedDocs))
for _, id := range storedDocs {
if _, dup := seen[id]; dup {
continue
}
seen[id] = struct{}{}
docIDs = append(docIDs, id.String())
}
for _, folderID := range storedFolders {
expanded, err := queries.GetDocumentIDsInFolderTreeForClient(ctx, &repository.GetDocumentIDsInFolderTreeForClientParams{
FolderID: folderID,
ClientID: clientID,
})
if err != nil {
return ChatScope{}, fmt.Errorf("bot: expand folder tree: %w", err)
}
for _, id := range expanded {
if _, dup := seen[id]; dup {
continue
}
seen[id] = struct{}{}
docIDs = append(docIDs, id.String())
}
}
folderIDs := make([]string, 0, len(storedFolders))
for _, id := range storedFolders {
folderIDs = append(folderIDs, id.String())
}
return ChatScope{DocumentIDs: docIDs, FolderIDs: folderIDs}, nil
}