Files
query-orchestration/internal/bot/payload.go
T

120 lines
4.0 KiB
Go
Raw Normal View History

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
}