Files
Enterprise-Knowledge-Base/internal/application/service/session_qa_helpers.go
T

259 lines
9.7 KiB
Go

package service
import (
"context"
"github.com/Tencent/WeKnora/internal/logger"
"github.com/Tencent/WeKnora/internal/types"
)
// ---------------------------------------------------------------------------
// Shared QA helpers: KB resolution, model resolution, retrieval tenant
// ---------------------------------------------------------------------------
// resolveKnowledgeBases resolves the effective knowledge base IDs and knowledge IDs
// for a QA request. Priority:
// 1. Explicit @mentions (request-specified kbIDs / knowledgeIDs)
// 2. RetrieveKBOnlyWhenMentioned -> disable KB if no mention
// 3. Agent's configured knowledge bases (via KBSelectionMode)
func (s *sessionService) resolveKnowledgeBases(
ctx context.Context,
req *types.QARequest,
) (kbIDs []string, knowledgeIDs []string) {
kbIDs = req.KnowledgeBaseIDs
knowledgeIDs = req.KnowledgeIDs
customAgent := req.CustomAgent
hasExplicitMention := len(kbIDs) > 0 || len(knowledgeIDs) > 0
if customAgent != nil {
logger.Infof(ctx, "KB resolution: hasExplicitMention=%v, RetrieveKBOnlyWhenMentioned=%v, KBSelectionMode=%s",
hasExplicitMention, customAgent.Config.RetrieveKBOnlyWhenMentioned, customAgent.Config.KBSelectionMode)
}
if hasExplicitMention {
logger.Infof(ctx, "Using request-specified targets: kbs=%v, docs=%v", kbIDs, knowledgeIDs)
// When using a shared agent, restrict @mentions to the agent's allowed KB scope
// to prevent users from injecting KB/knowledge IDs outside the agent's configured range.
if customAgent != nil && req.Session != nil && req.Session.TenantID != customAgent.TenantID {
kbIDs, knowledgeIDs = s.restrictMentionsToAgentScope(ctx, customAgent, req.Session.TenantID, kbIDs, knowledgeIDs)
}
} else if customAgent != nil && customAgent.Config.RetrieveKBOnlyWhenMentioned {
kbIDs = nil
knowledgeIDs = nil
logger.Infof(ctx, "RetrieveKBOnlyWhenMentioned is enabled and no @ mention found, KB retrieval disabled for this request")
} else if customAgent != nil {
kbIDs = s.resolveKnowledgeBasesFromAgent(ctx, customAgent, req.Session.TenantID)
}
return kbIDs, knowledgeIDs
}
// resolveChatModelID resolves the effective chat model ID for a QA request.
// Priority:
// 1. Request's SummaryModelID (explicit override, validated)
// 2. Custom agent's ModelID
// 3. KB / session / system default (via selectChatModelID)
func (s *sessionService) resolveChatModelID(
ctx context.Context,
req *types.QARequest,
knowledgeBaseIDs []string,
knowledgeIDs []string,
) (string, error) {
summaryModelID := req.SummaryModelID
customAgent := req.CustomAgent
session := req.Session
if summaryModelID != "" {
if model, err := s.modelService.GetModelByID(ctx, summaryModelID); err == nil && model != nil {
logger.Infof(ctx, "Using request's summary model override: %s", summaryModelID)
return summaryModelID, nil
}
logger.Warnf(ctx, "Request provided invalid summary model ID %s, falling back", summaryModelID)
}
if customAgent != nil && customAgent.Config.ModelID != "" {
logger.Infof(ctx, "Using custom agent's model_id: %s", customAgent.Config.ModelID)
return customAgent.Config.ModelID, nil
}
return s.selectChatModelID(ctx, session, knowledgeBaseIDs, knowledgeIDs)
}
// resolveRetrievalTenantID determines the tenant ID to use for retrieval scope.
// Priority: agent's tenant > context tenant > session tenant.
func (s *sessionService) resolveRetrievalTenantID(
ctx context.Context,
req *types.QARequest,
) uint64 {
session := req.Session
customAgent := req.CustomAgent
retrievalTenantID := session.TenantID
if customAgent != nil && customAgent.TenantID != 0 {
retrievalTenantID = customAgent.TenantID
logger.Infof(ctx, "Using agent tenant %d for retrieval scope", retrievalTenantID)
} else if v := ctx.Value(types.TenantIDContextKey); v != nil {
if tid, ok := v.(uint64); ok && tid != 0 {
retrievalTenantID = tid
logger.Infof(ctx, "Using effective tenant %d for retrieval from context", retrievalTenantID)
}
}
return retrievalTenantID
}
// applyAgentOverridesToChatManage applies custom agent configuration overrides
// to a ChatManage object that was initialized with system defaults.
// This covers: system prompt, context template, temperature, max tokens, thinking,
// retrieval thresholds, rewrite settings, fallback settings, FAQ strategy, and history turns.
func (s *sessionService) applyAgentOverridesToChatManage(
ctx context.Context,
customAgent *types.CustomAgent,
cm *types.ChatManage,
) {
if customAgent == nil {
return
}
// Ensure defaults are set
customAgent.EnsureDefaults()
// Override summary config fields
if customAgent.Config.SystemPrompt != "" {
cm.SummaryConfig.Prompt = customAgent.Config.SystemPrompt
logger.Infof(ctx, "Using custom agent's system_prompt")
}
if customAgent.Config.ContextTemplate != "" {
cm.SummaryConfig.ContextTemplate = customAgent.Config.ContextTemplate
logger.Infof(ctx, "Using custom agent's context_template")
}
if customAgent.Config.Temperature >= 0 {
cm.SummaryConfig.Temperature = customAgent.Config.Temperature
logger.Infof(ctx, "Using custom agent's temperature: %f", customAgent.Config.Temperature)
}
if customAgent.Config.MaxCompletionTokens > 0 {
cm.SummaryConfig.MaxCompletionTokens = customAgent.Config.MaxCompletionTokens
logger.Infof(ctx, "Using custom agent's max_completion_tokens: %d", customAgent.Config.MaxCompletionTokens)
}
// Agent-level thinking setting takes full control (no global fallback)
cm.SummaryConfig.Thinking = customAgent.Config.Thinking
if customAgent.Config.Thinking != nil {
logger.Infof(ctx, "Using custom agent's thinking: %v", *customAgent.Config.Thinking)
}
// Override retrieval strategy settings
if customAgent.Config.EmbeddingTopK > 0 {
cm.EmbeddingTopK = customAgent.Config.EmbeddingTopK
}
if customAgent.Config.KeywordThreshold > 0 {
cm.KeywordThreshold = customAgent.Config.KeywordThreshold
}
if customAgent.Config.VectorThreshold > 0 {
cm.VectorThreshold = customAgent.Config.VectorThreshold
}
if customAgent.Config.RerankTopK > 0 {
cm.RerankTopK = customAgent.Config.RerankTopK
}
cm.RerankThreshold = customAgent.Config.RerankThreshold
if customAgent.Config.RerankModelID != "" {
cm.RerankModelID = customAgent.Config.RerankModelID
}
// Override rewrite settings
cm.EnableRewrite = customAgent.Config.EnableRewrite
cm.EnableQueryExpansion = customAgent.Config.EnableQueryExpansion
if customAgent.Config.RewritePromptSystem != "" {
cm.RewritePromptSystem = customAgent.Config.RewritePromptSystem
}
if customAgent.Config.RewritePromptUser != "" {
cm.RewritePromptUser = customAgent.Config.RewritePromptUser
}
// Override fallback settings
if customAgent.Config.FallbackStrategy != "" {
cm.FallbackStrategy = types.FallbackStrategy(customAgent.Config.FallbackStrategy)
}
if customAgent.Config.FallbackResponse != "" {
cm.FallbackResponse = customAgent.Config.FallbackResponse
}
if customAgent.Config.FallbackPrompt != "" {
cm.FallbackPrompt = customAgent.Config.FallbackPrompt
}
// Override web search settings
if customAgent.Config.WebSearchMaxResults > 0 {
cm.WebSearchMaxResults = customAgent.Config.WebSearchMaxResults
}
// Override history turns
if customAgent.Config.HistoryTurns > 0 {
cm.MaxRounds = customAgent.Config.HistoryTurns
logger.Infof(ctx, "Using custom agent's history_turns: %d", cm.MaxRounds)
}
if !customAgent.Config.MultiTurnEnabled {
cm.MaxRounds = 0
logger.Infof(ctx, "Multi-turn disabled by custom agent, clearing history")
}
// FAQ strategy settings
cm.FAQPriorityEnabled = customAgent.Config.FAQPriorityEnabled
cm.FAQDirectAnswerThreshold = customAgent.Config.FAQDirectAnswerThreshold
cm.FAQScoreBoost = customAgent.Config.FAQScoreBoost
if cm.FAQPriorityEnabled {
logger.Infof(ctx, "FAQ priority enabled: threshold=%.2f, boost=%.2f",
cm.FAQDirectAnswerThreshold, cm.FAQScoreBoost)
}
}
// restrictMentionsToAgentScope filters user-provided @mention targets (KB IDs
// and knowledge IDs) so that only those within the shared agent's allowed KB
// scope are retained. This prevents users from bypassing the agent's
// KBSelectionMode by injecting arbitrary KB/knowledge IDs into the request.
func (s *sessionService) restrictMentionsToAgentScope(
ctx context.Context,
agent *types.CustomAgent,
sessionTenantID uint64,
kbIDs []string,
knowledgeIDs []string,
) ([]string, []string) {
allowedKBIDs := s.resolveKnowledgeBasesFromAgent(ctx, agent, sessionTenantID)
if len(allowedKBIDs) == 0 {
logger.Warnf(ctx, "Shared agent has no allowed KBs, blocking all @mentions")
return nil, nil
}
allowedSet := make(map[string]bool, len(allowedKBIDs))
for _, id := range allowedKBIDs {
allowedSet[id] = true
}
filteredKBs := make([]string, 0, len(kbIDs))
for _, id := range kbIDs {
if allowedSet[id] {
filteredKBs = append(filteredKBs, id)
} else {
logger.Warnf(ctx, "Blocking @mentioned KB %s: not in shared agent's allowed scope", id)
}
}
filteredKnowledge := knowledgeIDs
if len(knowledgeIDs) > 0 {
knowledgeList, err := s.knowledgeService.GetKnowledgeBatch(ctx, agent.TenantID, knowledgeIDs)
if err != nil {
logger.Warnf(ctx, "Failed to validate knowledge IDs against agent scope: %v, blocking all", err)
filteredKnowledge = nil
} else {
filteredKnowledge = make([]string, 0, len(knowledgeList))
for _, k := range knowledgeList {
if k != nil && allowedSet[k.KnowledgeBaseID] {
filteredKnowledge = append(filteredKnowledge, k.ID)
} else if k != nil {
logger.Warnf(ctx, "Blocking @mentioned knowledge %s (KB %s): not in shared agent's allowed scope",
k.ID, k.KnowledgeBaseID)
}
}
}
}
logger.Infof(ctx, "Restricted @mentions to agent scope: kbs %d->%d, knowledge %d->%d",
len(kbIDs), len(filteredKBs), len(knowledgeIDs), len(filteredKnowledge))
return filteredKBs, filteredKnowledge
}