120 lines
4.0 KiB
Go
120 lines
4.0 KiB
Go
|
|
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
|
|||
|
|
}
|