655df49d7d
- Updated chat message sending logic to conditionally validate knowledge base IDs based on agent mode status. - Improved endpoint selection for chat requests, allowing for dynamic routing between agent and knowledge chat. - Introduced a new method to select the appropriate chat model ID, prioritizing remote models for enhanced flexibility. - Changed JSON column types for agent and context configurations in session and tenant types to jsonb for better performance.
864 lines
33 KiB
Go
864 lines
33 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
chatpipline "github.com/Tencent/WeKnora/internal/application/service/chat_pipline"
|
|
llmcontext "github.com/Tencent/WeKnora/internal/application/service/llmcontext"
|
|
"github.com/Tencent/WeKnora/internal/config"
|
|
"github.com/Tencent/WeKnora/internal/event"
|
|
"github.com/Tencent/WeKnora/internal/logger"
|
|
"github.com/Tencent/WeKnora/internal/models/chat"
|
|
"github.com/Tencent/WeKnora/internal/tracing"
|
|
"github.com/Tencent/WeKnora/internal/types"
|
|
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/codes"
|
|
)
|
|
|
|
// sessionService implements the SessionService interface for managing conversation sessions
|
|
type sessionService struct {
|
|
cfg *config.Config // Application configuration
|
|
sessionRepo interfaces.SessionRepository // Repository for session data
|
|
messageRepo interfaces.MessageRepository // Repository for message data
|
|
knowledgeBaseService interfaces.KnowledgeBaseService // Service for knowledge base operations
|
|
modelService interfaces.ModelService // Service for model operations
|
|
tenantService interfaces.TenantService // Service for tenant operations
|
|
eventManager *chatpipline.EventManager // Event manager for chat pipeline
|
|
agentService interfaces.AgentService // Service for agent operations
|
|
sessionStorage llmcontext.ContextStorage // Session storage
|
|
}
|
|
|
|
// NewSessionService creates a new session service instance with all required dependencies
|
|
func NewSessionService(cfg *config.Config,
|
|
sessionRepo interfaces.SessionRepository,
|
|
messageRepo interfaces.MessageRepository,
|
|
knowledgeBaseService interfaces.KnowledgeBaseService,
|
|
modelService interfaces.ModelService,
|
|
tenantService interfaces.TenantService,
|
|
eventManager *chatpipline.EventManager,
|
|
agentService interfaces.AgentService,
|
|
sessionStorage llmcontext.ContextStorage,
|
|
) interfaces.SessionService {
|
|
return &sessionService{
|
|
cfg: cfg,
|
|
sessionRepo: sessionRepo,
|
|
messageRepo: messageRepo,
|
|
knowledgeBaseService: knowledgeBaseService,
|
|
modelService: modelService,
|
|
tenantService: tenantService,
|
|
eventManager: eventManager,
|
|
agentService: agentService,
|
|
sessionStorage: sessionStorage,
|
|
}
|
|
}
|
|
|
|
// CreateSession creates a new conversation session
|
|
func (s *sessionService) CreateSession(ctx context.Context, session *types.Session) (*types.Session, error) {
|
|
logger.Info(ctx, "Start creating session")
|
|
|
|
// Validate tenant ID
|
|
if session.TenantID == 0 {
|
|
logger.Error(ctx, "Failed to create session: tenant ID cannot be empty")
|
|
return nil, errors.New("tenant ID is required")
|
|
}
|
|
|
|
logger.Infof(ctx, "Creating session, tenant ID: %d, model ID: %s, knowledge base ID: %s",
|
|
session.TenantID, session.SummaryModelID, session.KnowledgeBaseID)
|
|
|
|
// Create session in repository
|
|
createdSession, err := s.sessionRepo.Create(ctx, session)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
logger.Infof(ctx, "Session created successfully, ID: %s, tenant ID: %d", createdSession.ID, createdSession.TenantID)
|
|
return createdSession, nil
|
|
}
|
|
|
|
// GetSession retrieves a session by its ID
|
|
func (s *sessionService) GetSession(ctx context.Context, id string) (*types.Session, error) {
|
|
logger.Info(ctx, "Start retrieving session")
|
|
|
|
// Validate session ID
|
|
if id == "" {
|
|
logger.Error(ctx, "Failed to get session: session ID cannot be empty")
|
|
return nil, errors.New("session id is required")
|
|
}
|
|
|
|
// Get tenant ID from context
|
|
tenantID := ctx.Value(types.TenantIDContextKey).(uint)
|
|
logger.Infof(ctx, "Retrieving session, ID: %s, tenant ID: %d", id, tenantID)
|
|
|
|
// Get session from repository
|
|
session, err := s.sessionRepo.Get(ctx, tenantID, id)
|
|
if err != nil {
|
|
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
|
"session_id": id,
|
|
"tenant_id": tenantID,
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
logger.Infof(ctx, "Session retrieved successfully, ID: %s, tenant ID: %d", session.ID, session.TenantID)
|
|
return session, nil
|
|
}
|
|
|
|
// GetSessionsByTenant retrieves all sessions for the current tenant
|
|
func (s *sessionService) GetSessionsByTenant(ctx context.Context) ([]*types.Session, error) {
|
|
logger.Info(ctx, "Start retrieving all sessions for tenant")
|
|
|
|
// Get tenant ID from context
|
|
tenantID := ctx.Value(types.TenantIDContextKey).(uint)
|
|
logger.Infof(ctx, "Retrieving all sessions for tenant, tenant ID: %d", tenantID)
|
|
|
|
// Get sessions from repository
|
|
sessions, err := s.sessionRepo.GetByTenantID(ctx, tenantID)
|
|
if err != nil {
|
|
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
|
"tenant_id": tenantID,
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
logger.Infof(
|
|
ctx, "Tenant sessions retrieved successfully, tenant ID: %d, session count: %d", tenantID, len(sessions),
|
|
)
|
|
return sessions, nil
|
|
}
|
|
|
|
// GetPagedSessionsByTenant retrieves sessions for the current tenant with pagination
|
|
func (s *sessionService) GetPagedSessionsByTenant(ctx context.Context,
|
|
pagination *types.Pagination,
|
|
) (*types.PageResult, error) {
|
|
logger.Info(ctx, "Start retrieving paged sessions for tenant")
|
|
|
|
// Get tenant ID from context
|
|
tenantID := ctx.Value(types.TenantIDContextKey).(uint)
|
|
logger.Infof(ctx, "Retrieving paged sessions for tenant, tenant ID: %d, page: %d, page size: %d",
|
|
tenantID, pagination.Page, pagination.PageSize)
|
|
|
|
// Get paged sessions from repository
|
|
sessions, total, err := s.sessionRepo.GetPagedByTenantID(ctx, tenantID, pagination)
|
|
if err != nil {
|
|
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
|
"tenant_id": tenantID,
|
|
"page": pagination.Page,
|
|
"page_size": pagination.PageSize,
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
logger.Infof(ctx, "Tenant paged sessions retrieved successfully, tenant ID: %d, total: %d", tenantID, total)
|
|
return types.NewPageResult(total, pagination, sessions), nil
|
|
}
|
|
|
|
// UpdateSession updates an existing session's properties
|
|
func (s *sessionService) UpdateSession(ctx context.Context, session *types.Session) error {
|
|
logger.Info(ctx, "Start updating session")
|
|
|
|
// Validate session ID
|
|
if session.ID == "" {
|
|
logger.Error(ctx, "Failed to update session: session ID cannot be empty")
|
|
return errors.New("session id is required")
|
|
}
|
|
|
|
logger.Infof(ctx, "Updating session, ID: %s, tenant ID: %d", session.ID, session.TenantID)
|
|
|
|
// Update session in repository
|
|
err := s.sessionRepo.Update(ctx, session)
|
|
if err != nil {
|
|
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
|
"session_id": session.ID,
|
|
"tenant_id": session.TenantID,
|
|
})
|
|
return err
|
|
}
|
|
|
|
logger.Infof(ctx, "Session updated successfully, ID: %s", session.ID)
|
|
return nil
|
|
}
|
|
|
|
// DeleteSession removes a session by its ID
|
|
func (s *sessionService) DeleteSession(ctx context.Context, id string) error {
|
|
logger.Info(ctx, "Start deleting session")
|
|
|
|
// Validate session ID
|
|
if id == "" {
|
|
logger.Error(ctx, "Failed to delete session: session ID cannot be empty")
|
|
return errors.New("session id is required")
|
|
}
|
|
|
|
// Get tenant ID from context
|
|
tenantID := ctx.Value(types.TenantIDContextKey).(uint)
|
|
logger.Infof(ctx, "Deleting session, ID: %s, tenant ID: %d", id, tenantID)
|
|
|
|
// Delete session from repository
|
|
err := s.sessionRepo.Delete(ctx, tenantID, id)
|
|
if err != nil {
|
|
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
|
"session_id": id,
|
|
"tenant_id": tenantID,
|
|
})
|
|
return err
|
|
}
|
|
|
|
logger.Infof(ctx, "Session deleted successfully, ID: %s", id)
|
|
return nil
|
|
}
|
|
|
|
// GenerateTitle generates a title for the current conversation content
|
|
func (s *sessionService) GenerateTitle(ctx context.Context,
|
|
session *types.Session, messages []types.Message,
|
|
) (string, error) {
|
|
logger.Info(ctx, "Start generating session title")
|
|
|
|
if session == nil {
|
|
logger.Error(ctx, "Failed to generate title: session cannot be empty")
|
|
return "", errors.New("session cannot be empty")
|
|
}
|
|
|
|
// Skip if title already exists
|
|
if session.Title != "" {
|
|
logger.Infof(ctx, "Session already has a title, session ID: %s, title: %s", session.ID, session.Title)
|
|
return session.Title, nil
|
|
}
|
|
var err error
|
|
// Get the first user message, either from provided messages or repository
|
|
var message *types.Message
|
|
if len(messages) == 0 {
|
|
logger.Info(ctx, "Message list is empty, getting the first user message")
|
|
message, err = s.messageRepo.GetFirstMessageOfUser(ctx, session.ID)
|
|
if err != nil {
|
|
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
|
"session_id": session.ID,
|
|
})
|
|
return "", err
|
|
}
|
|
} else {
|
|
logger.Info(ctx, "Searching for user message in message list")
|
|
for _, m := range messages {
|
|
if m.Role == "user" {
|
|
message = &m
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ensure a user message was found
|
|
if message == nil {
|
|
logger.Error(ctx, "No user message found, cannot generate title")
|
|
return "", errors.New("no user message found")
|
|
}
|
|
|
|
// Get chat model, use default if SummaryModelID is empty
|
|
modelID := session.SummaryModelID
|
|
if modelID == "" {
|
|
logger.Info(ctx, "Session SummaryModelID is empty, trying to get default chat model")
|
|
// Try to get default KnowledgeQA model
|
|
models, err := s.modelService.ListModels(ctx)
|
|
if err != nil {
|
|
logger.ErrorWithFields(ctx, err, nil)
|
|
return "", fmt.Errorf("failed to list models: %w", err)
|
|
}
|
|
// Find default KnowledgeQA model or first KnowledgeQA model
|
|
for _, model := range models {
|
|
if model.Type == types.ModelTypeKnowledgeQA {
|
|
if model.IsDefault {
|
|
modelID = model.ID
|
|
logger.Infof(ctx, "Using default KnowledgeQA model: %s", modelID)
|
|
break
|
|
} else if modelID == "" {
|
|
modelID = model.ID
|
|
logger.Infof(ctx, "Using first available KnowledgeQA model: %s", modelID)
|
|
}
|
|
}
|
|
}
|
|
if modelID == "" {
|
|
logger.Error(ctx, "No KnowledgeQA model found")
|
|
return "", errors.New("no KnowledgeQA model available for title generation")
|
|
}
|
|
}
|
|
|
|
logger.Infof(ctx, "Getting chat model, model ID: %s", modelID)
|
|
chatModel, err := s.modelService.GetChatModel(ctx, modelID)
|
|
if err != nil {
|
|
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
|
"model_id": modelID,
|
|
})
|
|
return "", err
|
|
}
|
|
|
|
// Prepare messages for title generation
|
|
logger.Info(ctx, "Preparing to generate session title")
|
|
var chatMessages []chat.Message
|
|
chatMessages = append(chatMessages,
|
|
chat.Message{Role: "system", Content: s.cfg.Conversation.GenerateSessionTitlePrompt},
|
|
)
|
|
chatMessages = append(chatMessages,
|
|
chat.Message{Role: "user", Content: message.Content + " /no_think"},
|
|
)
|
|
|
|
// Call model to generate title
|
|
thinking := false
|
|
logger.Info(ctx, "Calling model to generate title")
|
|
response, err := chatModel.Chat(ctx, chatMessages, &chat.ChatOptions{
|
|
Temperature: 0.3,
|
|
Thinking: &thinking,
|
|
})
|
|
if err != nil {
|
|
logger.ErrorWithFields(ctx, err, nil)
|
|
return "", err
|
|
}
|
|
|
|
// Process and store the generated title
|
|
session.Title = strings.TrimPrefix(response.Content, "<think>\n\n</think>")
|
|
logger.Infof(ctx, "Title generated successfully: %s", session.Title)
|
|
|
|
// Update session with new title
|
|
logger.Info(ctx, "Updating session title")
|
|
err = s.sessionRepo.Update(ctx, session)
|
|
if err != nil {
|
|
logger.ErrorWithFields(ctx, err, nil)
|
|
return "", err
|
|
}
|
|
|
|
logger.Infof(ctx, "Session title updated successfully, ID: %s, title: %s", session.ID, session.Title)
|
|
return session.Title, nil
|
|
}
|
|
|
|
// GenerateTitleAsync generates a title for the session asynchronously
|
|
// This method clones the session and generates the title in a goroutine
|
|
// It emits an event when the title is generated
|
|
func (s *sessionService) GenerateTitleAsync(ctx context.Context, session *types.Session, userQuery string, eventBus *event.EventBus) {
|
|
// Extract values from context before cloning
|
|
tenantID := ctx.Value(types.TenantIDContextKey)
|
|
requestID := ctx.Value(types.RequestIDContextKey)
|
|
go func() {
|
|
// Create new background context and copy values
|
|
bgCtx := context.Background()
|
|
if tenantID != nil {
|
|
bgCtx = context.WithValue(bgCtx, types.TenantIDContextKey, tenantID)
|
|
}
|
|
if requestID != nil {
|
|
bgCtx = context.WithValue(bgCtx, types.RequestIDContextKey, requestID)
|
|
}
|
|
|
|
logger.Info(bgCtx, "Starting async title generation")
|
|
|
|
// Skip if title already exists
|
|
if session.Title != "" {
|
|
logger.Infof(bgCtx, "Session already has a title, skipping generation, session ID: %s", session.ID)
|
|
return
|
|
}
|
|
|
|
// Generate title using the first user message
|
|
messages := []types.Message{
|
|
{
|
|
Role: "user",
|
|
Content: userQuery,
|
|
},
|
|
}
|
|
|
|
title, err := s.GenerateTitle(bgCtx, session, messages)
|
|
if err != nil {
|
|
logger.ErrorWithFields(bgCtx, err, map[string]interface{}{
|
|
"session_id": session.ID,
|
|
})
|
|
return
|
|
}
|
|
|
|
// Emit title update event - BUG FIX: use bgCtx instead of ctx
|
|
// The original ctx is from the HTTP request and may be cancelled by the time we get here
|
|
if eventBus != nil {
|
|
if err := eventBus.Emit(bgCtx, event.Event{
|
|
Type: event.EventSessionTitle,
|
|
SessionID: session.ID,
|
|
Data: event.SessionTitleData{
|
|
SessionID: session.ID,
|
|
Title: title,
|
|
},
|
|
}); err != nil {
|
|
logger.ErrorWithFields(bgCtx, err, map[string]interface{}{
|
|
"session_id": session.ID,
|
|
})
|
|
} else {
|
|
logger.Infof(bgCtx, "Title update event emitted successfully, session ID: %s, title: %s", session.ID, title)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// KnowledgeQA performs knowledge base question answering with LLM summarization
|
|
func (s *sessionService) KnowledgeQA(ctx context.Context, session *types.Session, query string, knowledgeBaseIDs []string) (
|
|
[]*types.SearchResult, <-chan types.StreamResponse, error,
|
|
) {
|
|
logger.Infof(ctx, "Knowledge base question answering parameters, session ID: %s, query: %s", session.ID, query)
|
|
|
|
// If no knowledge base IDs provided, fall back to session's default
|
|
if len(knowledgeBaseIDs) == 0 {
|
|
if session.KnowledgeBaseID != "" {
|
|
knowledgeBaseIDs = []string{session.KnowledgeBaseID}
|
|
logger.Infof(ctx, "No knowledge base IDs provided, using session default: %s", session.KnowledgeBaseID)
|
|
} else {
|
|
logger.Warnf(ctx, "Session has no associated knowledge base, session ID: %s", session.ID)
|
|
return nil, nil, errors.New("session has no knowledge base")
|
|
}
|
|
}
|
|
|
|
logger.Infof(ctx, "Using knowledge bases: %v", knowledgeBaseIDs)
|
|
|
|
// Determine chat model ID: prioritize Remote models
|
|
chatModelID, err := s.selectChatModelID(ctx, session, knowledgeBaseIDs)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Create chat management object with session settings
|
|
logger.Infof(ctx, "Creating chat manage object, knowledge base IDs: %v, chat model ID: %s", knowledgeBaseIDs, chatModelID)
|
|
chatManage := &types.ChatManage{
|
|
Query: query,
|
|
RewriteQuery: query,
|
|
SessionID: session.ID,
|
|
KnowledgeBaseID: knowledgeBaseIDs[0], // For backward compatibility, use first KB ID
|
|
KnowledgeBaseIDs: knowledgeBaseIDs, // Multi-KB support
|
|
VectorThreshold: session.VectorThreshold,
|
|
KeywordThreshold: session.KeywordThreshold,
|
|
EmbeddingTopK: session.EmbeddingTopK,
|
|
RerankModelID: session.RerankModelID,
|
|
RerankTopK: session.RerankTopK,
|
|
RerankThreshold: session.RerankThreshold,
|
|
ChatModelID: chatModelID,
|
|
SummaryConfig: types.SummaryConfig{
|
|
MaxTokens: session.SummaryParameters.MaxTokens,
|
|
RepeatPenalty: session.SummaryParameters.RepeatPenalty,
|
|
TopK: session.SummaryParameters.TopK,
|
|
TopP: session.SummaryParameters.TopP,
|
|
FrequencyPenalty: session.SummaryParameters.FrequencyPenalty,
|
|
PresencePenalty: session.SummaryParameters.PresencePenalty,
|
|
Prompt: session.SummaryParameters.Prompt,
|
|
ContextTemplate: session.SummaryParameters.ContextTemplate,
|
|
Temperature: session.SummaryParameters.Temperature,
|
|
Seed: session.SummaryParameters.Seed,
|
|
NoMatchPrefix: session.SummaryParameters.NoMatchPrefix,
|
|
MaxCompletionTokens: session.SummaryParameters.MaxCompletionTokens,
|
|
},
|
|
FallbackResponse: session.FallbackResponse,
|
|
}
|
|
|
|
// Start knowledge QA event processing
|
|
logger.Info(ctx, "Triggering knowledge base question answering event")
|
|
err = s.KnowledgeQAByEvent(ctx, chatManage, types.Pipline["rag_stream"])
|
|
if err != nil {
|
|
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
|
"session_id": session.ID,
|
|
"knowledge_base_id": session.KnowledgeBaseID,
|
|
})
|
|
return nil, nil, err
|
|
}
|
|
|
|
logger.Info(ctx, "Knowledge base question answering completed")
|
|
return chatManage.MergeResult, chatManage.ResponseChan, nil
|
|
}
|
|
|
|
// selectChatModelID selects the appropriate chat model ID with priority for Remote models
|
|
// Priority order:
|
|
// 1. Session's SummaryModelID if it's a Remote model
|
|
// 2. First knowledge base with a Remote model
|
|
// 3. Session's SummaryModelID (if not Remote)
|
|
// 4. First knowledge base's SummaryModelID
|
|
func (s *sessionService) selectChatModelID(ctx context.Context, session *types.Session, knowledgeBaseIDs []string) (string, error) {
|
|
chatModelID := ""
|
|
|
|
// First, check if session has a SummaryModelID and if it's a Remote model
|
|
if session.SummaryModelID != "" {
|
|
model, err := s.modelService.GetModelByID(ctx, session.SummaryModelID)
|
|
if err == nil && model != nil && model.Source == types.ModelSourceRemote {
|
|
chatModelID = session.SummaryModelID
|
|
logger.Infof(ctx, "Using session's Remote summary model: %s", chatModelID)
|
|
return chatModelID, nil
|
|
} else if err == nil && model != nil {
|
|
// Session has a model but it's not Remote, we'll check knowledge bases for Remote models
|
|
logger.Infof(ctx, "Session has summary model %s but it's not Remote, checking knowledge bases for Remote models", session.SummaryModelID)
|
|
}
|
|
}
|
|
|
|
// If no Remote model found from session, check knowledge bases for Remote models
|
|
if chatModelID == "" && len(knowledgeBaseIDs) > 0 {
|
|
// Try to find a knowledge base with Remote model
|
|
for _, kbID := range knowledgeBaseIDs {
|
|
kb, err := s.knowledgeBaseService.GetKnowledgeBaseByID(ctx, kbID)
|
|
if err != nil {
|
|
logger.Warnf(ctx, "Failed to get knowledge base %s: %v", kbID, err)
|
|
continue
|
|
}
|
|
if kb != nil && kb.SummaryModelID != "" {
|
|
model, err := s.modelService.GetModelByID(ctx, kb.SummaryModelID)
|
|
if err == nil && model != nil && model.Source == types.ModelSourceRemote {
|
|
chatModelID = kb.SummaryModelID
|
|
logger.Infof(ctx, "Using Remote summary model from knowledge base %s: %s", kbID, chatModelID)
|
|
return chatModelID, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// If still no Remote model found, use session's SummaryModelID if available
|
|
if chatModelID == "" && session.SummaryModelID != "" {
|
|
chatModelID = session.SummaryModelID
|
|
logger.Infof(ctx, "No Remote model found, using session's summary model: %s", chatModelID)
|
|
return chatModelID, nil
|
|
}
|
|
|
|
// If still empty, use first knowledge base's model
|
|
if chatModelID == "" {
|
|
kb, err := s.knowledgeBaseService.GetKnowledgeBaseByID(ctx, knowledgeBaseIDs[0])
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to get knowledge base for model ID: %v", err)
|
|
return "", fmt.Errorf("failed to get knowledge base %s: %w", knowledgeBaseIDs[0], err)
|
|
}
|
|
if kb != nil && kb.SummaryModelID != "" {
|
|
chatModelID = kb.SummaryModelID
|
|
logger.Infof(ctx, "Using summary model from first knowledge base %s: %s", knowledgeBaseIDs[0], chatModelID)
|
|
return chatModelID, nil
|
|
} else {
|
|
logger.Errorf(ctx, "Knowledge base %s has no summary model ID", knowledgeBaseIDs[0])
|
|
return "", fmt.Errorf("knowledge base %s has no summary model configured", knowledgeBaseIDs[0])
|
|
}
|
|
}
|
|
}
|
|
|
|
if chatModelID == "" {
|
|
logger.Error(ctx, "No chat model ID available")
|
|
return "", errors.New("no chat model ID available: session has no SummaryModelID and knowledge bases have no SummaryModelID")
|
|
}
|
|
|
|
return chatModelID, nil
|
|
}
|
|
|
|
// KnowledgeQAByEvent processes knowledge QA through a series of events in the pipeline
|
|
func (s *sessionService) KnowledgeQAByEvent(ctx context.Context,
|
|
chatManage *types.ChatManage, eventList []types.EventType,
|
|
) error {
|
|
ctx, span := tracing.ContextWithSpan(ctx, "SessionService.KnowledgeQAByEvent")
|
|
defer span.End()
|
|
|
|
logger.Info(ctx, "Start processing knowledge base question answering through events")
|
|
logger.Infof(ctx, "Knowledge base question answering parameters, session ID: %s, knowledge base ID: %s, query: %s",
|
|
chatManage.SessionID, chatManage.KnowledgeBaseID, chatManage.Query)
|
|
|
|
// Prepare method list for logging and tracing
|
|
methods := []string{}
|
|
for _, event := range eventList {
|
|
methods = append(methods, string(event))
|
|
}
|
|
|
|
// Set up tracing attributes
|
|
logger.Infof(ctx, "Trigger event list: %v", methods)
|
|
span.SetAttributes(
|
|
attribute.String("request_id", ctx.Value(types.RequestIDContextKey).(string)),
|
|
attribute.String("query", chatManage.Query),
|
|
attribute.String("method", strings.Join(methods, ",")),
|
|
)
|
|
|
|
// Process each event in sequence
|
|
for _, event := range eventList {
|
|
logger.Infof(ctx, "Starting to trigger event: %v", event)
|
|
err := s.eventManager.Trigger(ctx, event, chatManage)
|
|
|
|
// Handle case where search returns no results
|
|
if err == chatpipline.ErrSearchNothing {
|
|
logger.Warnf(ctx, "Event %v triggered, search result is empty, using fallback response", event)
|
|
chatManage.ResponseChan = chatpipline.NewFallbackChan(ctx, chatManage.FallbackResponse)
|
|
chatManage.ChatResponse = &types.ChatResponse{Content: chatManage.FallbackResponse}
|
|
return nil
|
|
}
|
|
|
|
// Handle other errors
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Event triggering failed, event: %v, error type: %s, description: %s, error: %v",
|
|
event, err.ErrorType, err.Description, err.Err)
|
|
span.RecordError(err.Err)
|
|
span.SetStatus(codes.Error, err.Description)
|
|
span.SetAttributes(attribute.String("error_type", err.ErrorType))
|
|
return err.Err
|
|
}
|
|
logger.Infof(ctx, "Event %v triggered successfully", event)
|
|
}
|
|
|
|
logger.Info(ctx, "All events triggered successfully")
|
|
return nil
|
|
}
|
|
|
|
// SearchKnowledge performs knowledge base search without LLM summarization
|
|
func (s *sessionService) SearchKnowledge(ctx context.Context,
|
|
knowledgeBaseID, query string,
|
|
) ([]*types.SearchResult, error) {
|
|
logger.Info(ctx, "Start knowledge base search without LLM summary")
|
|
logger.Infof(ctx, "Knowledge base search parameters, knowledge base ID: %s, query: %s", knowledgeBaseID, query)
|
|
|
|
// Create default retrieval parameters
|
|
chatManage := &types.ChatManage{
|
|
Query: query,
|
|
RewriteQuery: query,
|
|
KnowledgeBaseID: knowledgeBaseID,
|
|
VectorThreshold: s.cfg.Conversation.VectorThreshold, // Use default configuration
|
|
KeywordThreshold: s.cfg.Conversation.KeywordThreshold, // Use default configuration
|
|
EmbeddingTopK: s.cfg.Conversation.EmbeddingTopK, // Use default configuration
|
|
RerankTopK: s.cfg.Conversation.RerankTopK, // Use default configuration
|
|
RerankThreshold: s.cfg.Conversation.RerankThreshold, // Use default configuration
|
|
}
|
|
|
|
// Get default models
|
|
models, err := s.modelService.ListModels(ctx)
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to get models: %v", err)
|
|
return nil, err
|
|
}
|
|
|
|
// Find the first available rerank model
|
|
for _, model := range models {
|
|
if model.Type == types.ModelTypeRerank {
|
|
chatManage.RerankModelID = model.ID
|
|
break
|
|
}
|
|
}
|
|
|
|
// Use specific event list, only including retrieval-related events, not LLM summarization
|
|
searchEvents := []types.EventType{
|
|
types.PREPROCESS_QUERY, // Preprocess query
|
|
types.CHUNK_SEARCH, // Vector search
|
|
types.CHUNK_RERANK, // Rerank search results
|
|
types.CHUNK_MERGE, // Merge search results
|
|
types.FILTER_TOP_K, // Filter top K results
|
|
}
|
|
|
|
ctx, span := tracing.ContextWithSpan(ctx, "SessionService.SearchKnowledge")
|
|
defer span.End()
|
|
|
|
// Prepare method list for logging and tracing
|
|
methods := []string{}
|
|
for _, event := range searchEvents {
|
|
methods = append(methods, string(event))
|
|
}
|
|
|
|
// Set up tracing attributes
|
|
logger.Infof(ctx, "Trigger search event list: %v", methods)
|
|
span.SetAttributes(
|
|
attribute.String("query", query),
|
|
attribute.String("knowledge_base_id", knowledgeBaseID),
|
|
attribute.String("method", strings.Join(methods, ",")),
|
|
)
|
|
|
|
// Process each search event in sequence
|
|
for _, event := range searchEvents {
|
|
logger.Infof(ctx, "Starting to trigger search event: %v", event)
|
|
err := s.eventManager.Trigger(ctx, event, chatManage)
|
|
|
|
// Handle case where search returns no results
|
|
if err == chatpipline.ErrSearchNothing {
|
|
logger.Warnf(ctx, "Event %v triggered, search result is empty", event)
|
|
return []*types.SearchResult{}, nil
|
|
}
|
|
|
|
// Handle other errors
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Event triggering failed, event: %v, error type: %s, description: %s, error: %v",
|
|
event, err.ErrorType, err.Description, err.Err)
|
|
span.RecordError(err.Err)
|
|
span.SetStatus(codes.Error, err.Description)
|
|
span.SetAttributes(attribute.String("error_type", err.ErrorType))
|
|
return nil, err.Err
|
|
}
|
|
logger.Infof(ctx, "Event %v triggered successfully", event)
|
|
}
|
|
|
|
logger.Infof(ctx, "Knowledge base search completed, found %d results", len(chatManage.MergeResult))
|
|
return chatManage.MergeResult, nil
|
|
}
|
|
|
|
// AgentQA performs agent-based question answering with conversation history and streaming support
|
|
func (s *sessionService) AgentQA(ctx context.Context, session *types.Session, query string, assistantMessageID string, eventBus *event.EventBus) (
|
|
[]*types.SearchResult, error,
|
|
) {
|
|
sessionID := session.ID
|
|
tenantID := ctx.Value(types.TenantIDContextKey).(uint)
|
|
logger.Infof(ctx, "Start agent-based question answering, session ID: %s, tenant ID: %d, query: %s", sessionID, tenantID, query)
|
|
|
|
// Build effective agent configuration by merging session and tenant configs
|
|
// Session-level config: Enabled, KnowledgeBases (stored in session.AgentConfig)
|
|
// Tenant-level config: MaxIterations, Temperature, Models, Tools, etc. (from tenant.AgentConfig)
|
|
|
|
tenantInfo := ctx.Value(types.TenantInfoContextKey).(*types.Tenant)
|
|
|
|
// Check if agent is enabled at session level
|
|
if session.AgentConfig == nil || !session.AgentConfig.Enabled {
|
|
logger.Warnf(ctx, "Agent not enabled for session: %s", sessionID)
|
|
return nil, errors.New("agent not enabled for this session")
|
|
}
|
|
|
|
// Check if tenant has agent configuration
|
|
if tenantInfo.AgentConfig == nil {
|
|
logger.Warnf(ctx, "Tenant %d has no agent configuration", tenantInfo.ID)
|
|
return nil, errors.New("tenant has no agent configuration")
|
|
}
|
|
|
|
// Create runtime AgentConfig by merging session and tenant configs
|
|
// Tenant config provides the runtime parameters (MaxIterations, Temperature, Tools, Models)
|
|
// Session config provides Enabled and KnowledgeBases
|
|
agentConfig := &types.AgentConfig{
|
|
Enabled: session.AgentConfig.Enabled,
|
|
MaxIterations: tenantInfo.AgentConfig.MaxIterations,
|
|
ReflectionEnabled: tenantInfo.AgentConfig.ReflectionEnabled,
|
|
AllowedTools: tenantInfo.AgentConfig.AllowedTools,
|
|
Temperature: tenantInfo.AgentConfig.Temperature,
|
|
ThinkingModelID: tenantInfo.AgentConfig.ThinkingModelID,
|
|
RerankModelID: tenantInfo.AgentConfig.RerankModelID,
|
|
KnowledgeBases: session.AgentConfig.KnowledgeBases, // Use session's knowledge bases
|
|
}
|
|
|
|
logger.Infof(ctx, "Merged agent config from tenant %d and session %s", tenantInfo.ID, sessionID)
|
|
|
|
// Determine knowledge bases for agent
|
|
// Priority: Session.AgentConfig.KnowledgeBases > Session.KnowledgeBaseID > All tenant knowledge bases
|
|
if len(agentConfig.KnowledgeBases) == 0 {
|
|
if session.KnowledgeBaseID != "" {
|
|
// Use session's knowledge base as fallback
|
|
agentConfig.KnowledgeBases = []string{session.KnowledgeBaseID}
|
|
logger.Infof(ctx, "Using session's knowledge base for agent: %s", session.KnowledgeBaseID)
|
|
} else {
|
|
// Default to all knowledge bases under the tenant
|
|
logger.Infof(ctx, "No knowledge bases specified, fetching all knowledge bases for tenant")
|
|
allKBs, err := s.knowledgeBaseService.ListKnowledgeBases(ctx)
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to list knowledge bases for tenant: %v", err)
|
|
return nil, fmt.Errorf("failed to list knowledge bases: %w", err)
|
|
}
|
|
|
|
if len(allKBs) == 0 {
|
|
logger.Warnf(ctx, "No knowledge bases available for agent session: %s", sessionID)
|
|
return nil, errors.New("no knowledge bases available for agent")
|
|
}
|
|
|
|
// Extract knowledge base IDs
|
|
agentConfig.KnowledgeBases = make([]string, len(allKBs))
|
|
for i, kb := range allKBs {
|
|
agentConfig.KnowledgeBases[i] = kb.ID
|
|
}
|
|
logger.Infof(ctx, "Agent defaulting to all %d knowledge base(s) in tenant: %v",
|
|
len(agentConfig.KnowledgeBases), agentConfig.KnowledgeBases)
|
|
}
|
|
} else {
|
|
logger.Infof(ctx, "Agent configured with %d knowledge base(s): %v", len(agentConfig.KnowledgeBases), agentConfig.KnowledgeBases)
|
|
}
|
|
|
|
// Set ThinkingModelID from session's SummaryModelID if not already set in tenant config
|
|
if agentConfig.ThinkingModelID == "" && session.SummaryModelID != "" {
|
|
agentConfig.ThinkingModelID = session.SummaryModelID
|
|
logger.Infof(ctx, "Using session's SummaryModelID as ThinkingModelID: %s", session.SummaryModelID)
|
|
}
|
|
|
|
// Get chat model
|
|
summaryModel, err := s.modelService.GetChatModel(ctx, agentConfig.ThinkingModelID)
|
|
if err != nil {
|
|
logger.Warnf(ctx, "Failed to get chat model: %v", err)
|
|
return nil, fmt.Errorf("failed to get chat model: %w", err)
|
|
}
|
|
|
|
// Get or create contextManager for this session
|
|
contextManager := s.getContextManagerForSession(ctx, session, summaryModel)
|
|
// Get LLM context from context manager
|
|
llmContext, err := s.getContextForSession(ctx, contextManager, sessionID)
|
|
if err != nil {
|
|
logger.Warnf(ctx, "Failed to get LLM context: %v, continuing without history", err)
|
|
llmContext = []chat.Message{}
|
|
}
|
|
logger.Infof(ctx, "Loaded %d messages from LLM context manager", len(llmContext))
|
|
|
|
// Create agent engine with EventBus and ContextManager
|
|
logger.Info(ctx, "Creating agent engine")
|
|
engine, err := s.agentService.CreateAgentEngine(ctx, agentConfig, eventBus, contextManager, session.ID)
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to create agent engine: %v", err)
|
|
return nil, err
|
|
}
|
|
|
|
// Execute agent with streaming (asynchronously)
|
|
// Events will be emitted to EventBus and handled by the Handler layer
|
|
logger.Info(ctx, "Executing agent with streaming")
|
|
go func() {
|
|
if _, err := engine.Execute(ctx, sessionID, assistantMessageID, query, llmContext); err != nil {
|
|
logger.Errorf(ctx, "Agent execution failed: %v", err)
|
|
// Emit error event to the EventBus used by this agent
|
|
eventBus.Emit(ctx, event.Event{
|
|
Type: event.EventError,
|
|
SessionID: sessionID,
|
|
Data: event.ErrorData{
|
|
Error: err.Error(),
|
|
Stage: "agent_execution",
|
|
SessionID: sessionID,
|
|
},
|
|
})
|
|
}
|
|
}()
|
|
|
|
// Return empty - events will be handled by Handler via EventBus subscription
|
|
return nil, nil
|
|
}
|
|
|
|
// getContextManagerForSession creates a context manager for the session based on configuration
|
|
// Returns the configured context manager (tenant-level or session-level) or default
|
|
func (s *sessionService) getContextManagerForSession(ctx context.Context, session *types.Session, chatModel chat.Chat) interfaces.ContextManager {
|
|
// Get tenant to access global context configuration
|
|
tenant, _ := ctx.Value(types.TenantInfoContextKey).(*types.Tenant)
|
|
// Determine which context config to use: session-specific or tenant-level
|
|
var contextConfig *types.ContextConfig
|
|
if session.ContextConfig != nil {
|
|
// Use session-specific configuration
|
|
contextConfig = session.ContextConfig
|
|
logger.Infof(ctx, "Using session-specific context config for session %s", session.ID)
|
|
} else if tenant.ContextConfig != nil {
|
|
// Use tenant-level configuration
|
|
contextConfig = tenant.ContextConfig
|
|
logger.Infof(ctx, "Using tenant-level context config for session %s", session.ID)
|
|
} else {
|
|
// Use service's default context manager
|
|
logger.Debugf(ctx, "Using default context manager for session %s", session.ID)
|
|
contextConfig = &types.ContextConfig{
|
|
MaxTokens: llmcontext.DefaultMaxTokens,
|
|
CompressionStrategy: llmcontext.DefaultCompressionStrategy,
|
|
RecentMessageCount: llmcontext.DefaultRecentMessageCount,
|
|
SummarizeThreshold: llmcontext.DefaultSummarizeThreshold,
|
|
}
|
|
}
|
|
return llmcontext.NewContextManagerFromConfig(contextConfig, s.sessionStorage, chatModel)
|
|
}
|
|
|
|
// getContextForSession retrieves LLM context for a session
|
|
func (s *sessionService) getContextForSession(ctx context.Context, contextManager interfaces.ContextManager, sessionID string) ([]chat.Message, error) {
|
|
history, err := contextManager.GetContext(ctx, sessionID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get context: %w", err)
|
|
}
|
|
|
|
// Log context statistics
|
|
stats, _ := contextManager.GetContextStats(ctx, sessionID)
|
|
if stats != nil {
|
|
logger.Infof(ctx, "LLM context stats for session %s: messages=%d, tokens=~%d, compressed=%v",
|
|
sessionID, stats.MessageCount, stats.TokenCount, stats.IsCompressed)
|
|
}
|
|
|
|
return history, nil
|
|
}
|
|
|
|
// ClearContext clears the LLM context for a session
|
|
// This is useful when switching knowledge bases or agent modes to prevent context contamination
|
|
func (s *sessionService) ClearContext(ctx context.Context, sessionID string) error {
|
|
logger.Infof(ctx, "Clearing context for session: %s", sessionID)
|
|
return s.sessionStorage.Delete(ctx, sessionID)
|
|
}
|
|
|
|
// processAgentEvents is no longer needed - events are handled directly by Handler layer via EventBus subscription
|