Files
Enterprise-Knowledge-Base/internal/application/service/session_agent_qa.go
T
wizardchen 4022f134bd feat: Introduce agent type presets for smart-reasoning mode
- Added a new configuration file `agent_type_presets.yaml` to define various agent type presets, including RAG Q&A, Wiki Q&A, Hybrid (Wiki + RAG), and Data Analysis.
- Updated the `builtin_agents.yaml` to associate built-in agents with the new agent type presets, enhancing their configuration options.
- Enhanced the agent system prompt templates to reflect the new agent types and their functionalities.
- Implemented API endpoints to retrieve agent type presets, allowing for dynamic loading in the frontend.
- Updated the frontend to support agent type selection in the agent editor modal, improving user experience by auto-filling relevant configurations based on selected presets.

These changes significantly enhance the flexibility and usability of the agent system, allowing users to easily configure agents based on predefined types.
2026-04-22 21:18:20 +08:00

379 lines
15 KiB
Go

package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"github.com/Tencent/WeKnora/internal/agent/tools"
llmcontext "github.com/Tencent/WeKnora/internal/application/service/llmcontext"
"github.com/Tencent/WeKnora/internal/event"
"github.com/Tencent/WeKnora/internal/logger"
"github.com/Tencent/WeKnora/internal/models/chat"
"github.com/Tencent/WeKnora/internal/models/rerank"
"github.com/Tencent/WeKnora/internal/types"
"github.com/Tencent/WeKnora/internal/types/interfaces"
)
// AgentQA performs agent-based question answering with conversation history and streaming support
// customAgent is optional - if provided, uses custom agent configuration instead of tenant defaults
// summaryModelID is optional - if provided, overrides the model from customAgent config
func (s *sessionService) AgentQA(
ctx context.Context,
req *types.QARequest,
eventBus *event.EventBus,
) error {
sessionID := req.Session.ID
sessionJSON, err := json.Marshal(req.Session)
if err != nil {
logger.Errorf(ctx, "Failed to marshal session, session ID: %s, error: %v", sessionID, err)
return fmt.Errorf("failed to marshal session: %w", err)
}
// customAgent is required for AgentQA (handler has already done permission check for shared agent)
if req.CustomAgent == nil {
logger.Warnf(ctx, "Custom agent not provided for session: %s", sessionID)
return errors.New("custom agent configuration is required for agent QA")
}
// Resolve retrieval tenant using shared helper
agentTenantID := s.resolveRetrievalTenantID(ctx, req)
logger.Infof(ctx, "Start agent-based question answering, session ID: %s, agent tenant ID: %d, query: %s, session: %s",
sessionID, agentTenantID, req.Query, string(sessionJSON))
var tenantInfo *types.Tenant
if v := ctx.Value(types.TenantInfoContextKey); v != nil {
tenantInfo, _ = v.(*types.Tenant)
}
// When agent belongs to another tenant (shared agent), use agent's tenant for KB/model scope; load tenantInfo if needed
if tenantInfo == nil || tenantInfo.ID != agentTenantID {
if s.tenantService != nil {
if agentTenant, err := s.tenantService.GetTenantByID(ctx, agentTenantID); err == nil && agentTenant != nil {
tenantInfo = agentTenant
logger.Infof(ctx, "Using agent tenant info for retrieval scope, tenant ID: %d", agentTenantID)
}
}
}
if tenantInfo == nil {
logger.Warnf(ctx, "Tenant info not available for agent tenant %d, proceeding with defaults", agentTenantID)
tenantInfo = &types.Tenant{ID: agentTenantID}
}
// Ensure defaults are set
req.CustomAgent.EnsureDefaults()
// Build AgentConfig from custom agent and tenant info
agentConfig, err := s.buildAgentConfig(ctx, req, tenantInfo, agentTenantID)
if err != nil {
return err
}
// Set VLM model ID for tool result image analysis (runtime-only field)
if req.CustomAgent != nil && req.CustomAgent.Config.VLMModelID != "" {
agentConfig.VLMModelID = req.CustomAgent.Config.VLMModelID
}
// Resolve model ID using shared helper (AgentQA requires a model, so error if not found)
effectiveModelID, err := s.resolveChatModelID(ctx, req, agentConfig.KnowledgeBases, agentConfig.KnowledgeIDs)
if err != nil {
return err
}
if effectiveModelID == "" {
logger.Warnf(ctx, "No summary model configured for custom agent %s", req.CustomAgent.ID)
return errors.New("summary model (model_id) is not configured in custom agent settings")
}
summaryModel, err := s.modelService.GetChatModel(ctx, effectiveModelID)
if err != nil {
logger.Warnf(ctx, "Failed to get chat model: %v", err)
return fmt.Errorf("failed to get chat model: %w", err)
}
// Get rerank model from custom agent config (only required when knowledge_search is allowed)
var rerankModel rerank.Reranker
hasKnowledgeSearchTool := false
for _, tool := range agentConfig.AllowedTools {
if tool == tools.ToolKnowledgeSearch {
hasKnowledgeSearchTool = true
break
}
}
if hasKnowledgeSearchTool {
rerankModelID := req.CustomAgent.Config.RerankModelID
if rerankModelID == "" {
logger.Warnf(ctx, "No rerank model configured for custom agent %s, but knowledge_search tool is enabled", req.CustomAgent.ID)
return errors.New("rerank model (rerank_model_id) is not configured in custom agent settings")
}
rerankModel, err = s.modelService.GetRerankModel(ctx, rerankModelID)
if err != nil {
logger.Warnf(ctx, "Failed to get rerank model: %v", err)
return fmt.Errorf("failed to get rerank model: %w", err)
}
} else {
logger.Infof(ctx, "knowledge_search tool not enabled, skipping rerank model initialization")
}
// Get or create contextManager for this session
contextManager := s.getContextManagerForSession()
// Set system prompt for the current agent in context manager
// This ensures the context uses the correct system prompt when switching agents
systemPrompt := agentConfig.ResolveSystemPrompt(agentConfig.WebSearchEnabled)
if systemPrompt != "" {
if err := contextManager.SetSystemPrompt(ctx, sessionID, systemPrompt); err != nil {
logger.Warnf(ctx, "Failed to set system prompt in context manager: %v", err)
} else {
logger.Infof(ctx, "System prompt updated in context manager for agent")
}
}
// 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))
// Apply multi-turn configuration for Agent mode
// Note: In Agent mode, context is managed by contextManager with compression strategies,
// so we don't apply HistoryTurns limit here. HistoryTurns is used in normal (KnowledgeQA) mode.
if !agentConfig.MultiTurnEnabled {
// Multi-turn disabled, clear history
logger.Infof(ctx, "Multi-turn disabled for this agent, clearing history context")
llmContext = []chat.Message{}
}
// Create agent engine with EventBus and ContextManager
logger.Info(ctx, "Creating agent engine")
engine, err := s.agentService.CreateAgentEngine(
ctx,
agentConfig,
summaryModel,
rerankModel,
eventBus,
contextManager,
sessionID,
)
if err != nil {
logger.Errorf(ctx, "Failed to create agent engine: %v", err)
return err
}
// Route image data based on agent model's vision capability
var agentModelSupportsVision bool
if effectiveModelID != "" {
if modelInfo, err := s.modelService.GetModelByID(ctx, effectiveModelID); err == nil && modelInfo != nil {
agentModelSupportsVision = modelInfo.Parameters.SupportsVision
}
}
agentQuery := req.Query
var agentImageURLs []string
if agentModelSupportsVision && len(req.ImageURLs) > 0 {
agentImageURLs = req.ImageURLs
logger.Infof(ctx, "Agent model supports vision, passing %d image(s) directly", len(agentImageURLs))
} else if req.ImageDescription != "" {
agentQuery = req.Query + "\n\n[用户上传图片内容]\n" + req.ImageDescription
logger.Infof(ctx, "Agent model does not support vision, appending image description (%d chars)", len(req.ImageDescription))
}
if req.QuotedContext != "" {
agentQuery += "\n\n" + req.QuotedContext
}
// Execute agent with streaming (asynchronously)
// Events will be emitted to EventBus and handled by the Handler layer
logger.Info(ctx, "Executing agent with streaming")
if _, err := engine.Execute(ctx, sessionID, req.AssistantMessageID, agentQuery, llmContext, agentImageURLs); 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
}
// buildAgentConfig creates a runtime AgentConfig from the QARequest's custom agent configuration,
// tenant info, and resolved knowledge bases / search targets.
func (s *sessionService) buildAgentConfig(
ctx context.Context,
req *types.QARequest,
tenantInfo *types.Tenant,
agentTenantID uint64,
) (*types.AgentConfig, error) {
customAgent := req.CustomAgent
agentConfig := &types.AgentConfig{
MaxIterations: customAgent.Config.MaxIterations,
Temperature: customAgent.Config.Temperature,
WebSearchEnabled: customAgent.Config.WebSearchEnabled && req.WebSearchEnabled,
WebSearchMaxResults: customAgent.Config.WebSearchMaxResults,
WebSearchProviderID: customAgent.Config.WebSearchProviderID,
MultiTurnEnabled: customAgent.Config.MultiTurnEnabled,
HistoryTurns: customAgent.Config.HistoryTurns,
MCPSelectionMode: customAgent.Config.MCPSelectionMode,
MCPServices: customAgent.Config.MCPServices,
Thinking: customAgent.Config.Thinking,
RetrieveKBOnlyWhenMentioned: customAgent.Config.RetrieveKBOnlyWhenMentioned,
LLMCallTimeout: customAgent.Config.LLMCallTimeout,
RetainRetrievalHistory: customAgent.Config.RetainRetrievalHistory,
}
// Falls back to global configuration if no specific timeout is set for the agent.
if agentConfig.LLMCallTimeout == 0 && s.cfg.Agent != nil && s.cfg.Agent.LLMCallTimeout > 0 {
agentConfig.LLMCallTimeout = s.cfg.Agent.LLMCallTimeout
}
// Configure skills based on CustomAgentConfig
s.configureSkillsFromAgent(ctx, agentConfig, customAgent)
// Resolve knowledge bases using shared helper
agentConfig.KnowledgeBases, agentConfig.KnowledgeIDs = s.resolveKnowledgeBases(ctx, req)
// Use custom agent's allowed tools if specified, otherwise use defaults
if len(customAgent.Config.AllowedTools) > 0 {
agentConfig.AllowedTools = customAgent.Config.AllowedTools
} else {
agentConfig.AllowedTools = tools.DefaultAllowedTools()
}
// Use custom agent's system prompt if specified
if customAgent.Config.SystemPrompt != "" {
agentConfig.UseCustomSystemPrompt = true
agentConfig.SystemPrompt = customAgent.Config.SystemPrompt
}
logger.Infof(ctx, "Custom agent config applied: MaxIterations=%d, Temperature=%.2f, AllowedTools=%v, WebSearchEnabled=%v",
agentConfig.MaxIterations, agentConfig.Temperature, agentConfig.AllowedTools, agentConfig.WebSearchEnabled)
// Set web search max results from tenant config if not set (default: 5)
if agentConfig.WebSearchMaxResults == 0 {
agentConfig.WebSearchMaxResults = 5
if tenantInfo.WebSearchConfig != nil && tenantInfo.WebSearchConfig.MaxResults > 0 {
agentConfig.WebSearchMaxResults = tenantInfo.WebSearchConfig.MaxResults
}
}
// Resolve web search provider ID: agent-level > tenant default (is_default=true)
if agentConfig.WebSearchProviderID == "" {
if defaultProvider, err := s.webSearchProviderRepo.GetDefault(ctx, tenantInfo.ID); err == nil && defaultProvider != nil {
agentConfig.WebSearchProviderID = defaultProvider.ID
}
}
logger.Infof(ctx, "Merged agent config from tenant %d and session %s", tenantInfo.ID, req.Session.ID)
// Log knowledge bases if present
if len(agentConfig.KnowledgeBases) > 0 {
logger.Infof(ctx, "Agent configured with %d knowledge base(s): %v",
len(agentConfig.KnowledgeBases), agentConfig.KnowledgeBases)
} else {
logger.Infof(ctx, "No knowledge bases specified for agent, running in pure agent mode")
}
// Build search targets using agent's tenant (handler has validated access for shared agent)
searchTargets, err := s.buildSearchTargets(ctx, agentTenantID, agentConfig.KnowledgeBases, agentConfig.KnowledgeIDs)
if err != nil {
logger.Warnf(ctx, "Failed to build search targets for agent: %v", err)
}
agentConfig.SearchTargets = searchTargets
logger.Infof(ctx, "Agent search targets built: %d targets", len(searchTargets))
if agentConfig.MaxContextTokens <= 0 {
agentConfig.MaxContextTokens = types.DefaultMaxContextTokens
}
return agentConfig, nil
}
// configureSkillsFromAgent configures skills settings in AgentConfig based on CustomAgentConfig
// Returns the skill directories and allowed skills based on the selection mode:
// - "all": uses all preloaded skills
// - "selected": uses the explicitly selected skills
// - "none" or "": skills are disabled
func (s *sessionService) configureSkillsFromAgent(
ctx context.Context,
agentConfig *types.AgentConfig,
customAgent *types.CustomAgent,
) {
if customAgent == nil {
return
}
// When sandbox is disabled, skills cannot be enabled (no script execution environment)
sandboxMode := os.Getenv("WEKNORA_SANDBOX_MODE")
if sandboxMode == "" || sandboxMode == "disabled" {
agentConfig.SkillsEnabled = false
agentConfig.SkillDirs = nil
agentConfig.AllowedSkills = nil
logger.Infof(ctx, "Sandbox is disabled: skills are not available")
return
}
switch customAgent.Config.SkillsSelectionMode {
case "all":
// Enable all preloaded skills
agentConfig.SkillsEnabled = true
agentConfig.SkillDirs = []string{DefaultPreloadedSkillsDir}
agentConfig.AllowedSkills = nil // Empty means all skills allowed
logger.Infof(ctx, "SkillsSelectionMode=all: enabled all preloaded skills")
case "selected":
// Enable only selected skills
if len(customAgent.Config.SelectedSkills) > 0 {
agentConfig.SkillsEnabled = true
agentConfig.SkillDirs = []string{DefaultPreloadedSkillsDir}
agentConfig.AllowedSkills = customAgent.Config.SelectedSkills
logger.Infof(ctx, "SkillsSelectionMode=selected: enabled %d selected skills: %v",
len(customAgent.Config.SelectedSkills), customAgent.Config.SelectedSkills)
} else {
agentConfig.SkillsEnabled = false
logger.Infof(ctx, "SkillsSelectionMode=selected but no skills selected: skills disabled")
}
case "none", "":
// Skills disabled
agentConfig.SkillsEnabled = false
logger.Infof(ctx, "SkillsSelectionMode=%s: skills disabled", customAgent.Config.SkillsSelectionMode)
default:
// Unknown mode, disable skills
agentConfig.SkillsEnabled = false
logger.Warnf(ctx, "Unknown SkillsSelectionMode=%s: skills disabled", customAgent.Config.SkillsSelectionMode)
}
}
// getContextManagerForSession creates a context manager for the session.
func (s *sessionService) getContextManagerForSession() interfaces.ContextManager {
return llmcontext.NewContextManagerFromConfig(s.sessionStorage, s.messageRepo)
}
// 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
}