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 }