From a5f80b747caa736ae171e68f86ace05e192f8007 Mon Sep 17 00:00:00 2001 From: sqkstwj Date: Thu, 30 Apr 2026 10:00:42 +0800 Subject: [PATCH] fix(web-search): normalize default tenant web search config at runtime --- internal/agent/tools/web_search.go | 25 +++++---- .../service/chat_pipeline/search.go | 16 +++--- .../service/session_knowledge_qa.go | 6 +-- internal/handler/tenant.go | 6 ++- internal/types/web_search.go | 35 ++++++++++++ internal/types/web_search_test.go | 54 +++++++++++++++++++ 6 files changed, 114 insertions(+), 28 deletions(-) create mode 100644 internal/types/web_search_test.go diff --git a/internal/agent/tools/web_search.go b/internal/agent/tools/web_search.go index d3da4accb..0056d965c 100644 --- a/internal/agent/tools/web_search.go +++ b/internal/agent/tools/web_search.go @@ -152,20 +152,19 @@ func (t *WebSearchTool) Execute(ctx context.Context, args json.RawMessage) (*typ } // Get tenant info from context (same approach as search.go) - tenant := ctx.Value(types.TenantInfoContextKey).(*types.Tenant) - if tenant == nil || tenant.WebSearchConfig == nil { - logger.Errorf(ctx, "[Tool][WebSearch] Web search not configured for tenant %d", tenantID) - return &types.ToolResult{ - Success: false, - Error: "web search is not configured for this tenant", - }, fmt.Errorf("web search is not configured for tenant %d", tenantID) + var tenant *types.Tenant + if tenantValue := ctx.Value(types.TenantInfoContextKey); tenantValue != nil { + tenant, _ = tenantValue.(*types.Tenant) } // Resolve provider ID: tool-level (set from agent config, which already resolved default) resolvedProviderID := t.providerID - // Create a copy of web search config with maxResults from agent config - searchConfig := *tenant.WebSearchConfig + // Create a copy of the effective web search config with maxResults from agent config. + searchConfig := types.EffectiveWebSearchConfig(nil) + if tenant != nil { + searchConfig = types.EffectiveWebSearchConfig(tenant.WebSearchConfig) + } searchConfig.MaxResults = t.maxResults // Perform web search @@ -175,7 +174,7 @@ func (t *WebSearchTool) Execute(ctx context.Context, args json.RawMessage) (*typ resolvedProviderID, searchConfig.MaxResults, ) - webResults, err := t.webSearchService.Search(ctx, resolvedProviderID, &searchConfig, query) + webResults, err := t.webSearchService.Search(ctx, resolvedProviderID, searchConfig, query) if err != nil { logger.Errorf(ctx, "[Tool][WebSearch] Web search failed: %v", err) return &types.ToolResult{ @@ -187,8 +186,8 @@ func (t *WebSearchTool) Execute(ctx context.Context, args json.RawMessage) (*typ logger.Infof(ctx, "[Tool][WebSearch] Web search returned %d results", len(webResults)) // Apply RAG compression if configured - if len(webResults) > 0 && tenant.WebSearchConfig.CompressionMethod != "none" && - tenant.WebSearchConfig.CompressionMethod != "" { + if len(webResults) > 0 && searchConfig.CompressionMethod != "none" && + searchConfig.CompressionMethod != "" { // Load session-scoped temp KB state from Redis using WebSearchStateRepository tempKBID, seen, ids := t.webSearchStateService.GetWebSearchTempKBState(ctx, t.sessionID) @@ -197,7 +196,7 @@ func (t *WebSearchTool) Execute(ctx context.Context, args json.RawMessage) (*typ logger.Infof(ctx, "[Tool][WebSearch] Applying RAG compression") compressed, kbID, newSeen, newIDs, err := t.webSearchService.CompressWithRAG( - ctx, t.sessionID, tempKBID, questions, webResults, tenant.WebSearchConfig, + ctx, t.sessionID, tempKBID, questions, webResults, searchConfig, t.knowledgeBaseService, t.knowledgeService, seen, ids, ) if err != nil { diff --git a/internal/application/service/chat_pipeline/search.go b/internal/application/service/chat_pipeline/search.go index 947a2aa6e..08ea312b4 100644 --- a/internal/application/service/chat_pipeline/search.go +++ b/internal/application/service/chat_pipeline/search.go @@ -624,22 +624,18 @@ func (p *PluginSearch) searchWebIfEnabled(ctx context.Context, chatManage *types tenant, _ := types.TenantInfoFromContext(ctx) providerID := chatManage.WebSearchProviderID - var webConfig *types.WebSearchConfig - if tenant != nil && tenant.WebSearchConfig != nil { - // Clone tenant config so we can safely override MaxResults - cfg := *tenant.WebSearchConfig - webConfig = &cfg - } else if providerID != "" { - webConfig = &types.WebSearchConfig{ - MaxResults: 10, - } - } else { + if providerID == "" { pipelineWarn(ctx, "Search", "web_config_missing", map[string]interface{}{ "tenant_id": chatManage.TenantID, }) return nil } + webConfig := types.EffectiveWebSearchConfig(nil) + if tenant != nil { + webConfig = types.EffectiveWebSearchConfig(tenant.WebSearchConfig) + } + // Apply agent-level web search overrides if chatManage.WebSearchMaxResults > 0 { webConfig.MaxResults = chatManage.WebSearchMaxResults diff --git a/internal/application/service/session_knowledge_qa.go b/internal/application/service/session_knowledge_qa.go index e633a050a..d430ce90d 100644 --- a/internal/application/service/session_knowledge_qa.go +++ b/internal/application/service/session_knowledge_qa.go @@ -962,8 +962,8 @@ func (s *sessionService) resolveWebSearchMaxResults(ctx context.Context, req *ty return req.CustomAgent.Config.WebSearchMaxResults } tenantInfo, _ := types.TenantInfoFromContext(ctx) - if tenantInfo != nil && tenantInfo.WebSearchConfig != nil && tenantInfo.WebSearchConfig.MaxResults > 0 { - return tenantInfo.WebSearchConfig.MaxResults + if tenantInfo != nil { + return types.EffectiveWebSearchConfig(tenantInfo.WebSearchConfig).MaxResults } - return 10 + return types.DefaultWebSearchMaxResults } diff --git a/internal/handler/tenant.go b/internal/handler/tenant.go index 6ae24b34f..685f28284 100644 --- a/internal/handler/tenant.go +++ b/internal/handler/tenant.go @@ -750,6 +750,8 @@ func (h *TenantHandler) updateTenantWebSearchConfigInternal(c *gin.Context) { return } + cfg = *types.EffectiveWebSearchConfig(&cfg) + // Validate configuration if cfg.MaxResults < 1 || cfg.MaxResults > 50 { c.Error(errors.NewBadRequestError("max_results must be between 1 and 50")) @@ -777,7 +779,7 @@ func (h *TenantHandler) updateTenantWebSearchConfigInternal(c *gin.Context) { } c.JSON(http.StatusOK, gin.H{ "success": true, - "data": updatedTenant.WebSearchConfig, + "data": types.EffectiveWebSearchConfig(updatedTenant.WebSearchConfig), "message": "Web search configuration updated successfully", }) } @@ -807,7 +809,7 @@ func (h *TenantHandler) GetTenantWebSearchConfig(c *gin.Context) { logger.Infof(ctx, "Tenant web search config retrieved successfully, Tenant ID: %d", tenant.ID) c.JSON(http.StatusOK, gin.H{ "success": true, - "data": tenant.WebSearchConfig, + "data": types.EffectiveWebSearchConfig(tenant.WebSearchConfig), }) } diff --git a/internal/types/web_search.go b/internal/types/web_search.go index 1cf59210a..a77b694a8 100644 --- a/internal/types/web_search.go +++ b/internal/types/web_search.go @@ -25,6 +25,41 @@ type WebSearchConfig struct { ProxyURL string `json:"proxy_url,omitempty"` // Optional per-request proxy override; normally empty — use WebSearchProviderEntity.Parameters.proxy_url. Merged at call time when set. } +const ( + DefaultWebSearchMaxResults = 10 + DefaultWebSearchCompressionMethod = "none" +) + +// DefaultWebSearchConfig returns the shared default tenant-level web search configuration. +func DefaultWebSearchConfig() *WebSearchConfig { + return &WebSearchConfig{ + MaxResults: DefaultWebSearchMaxResults, + IncludeDate: false, + CompressionMethod: DefaultWebSearchCompressionMethod, + Blacklist: []string{}, + } +} + +// EffectiveWebSearchConfig normalizes a possibly empty config to the effective runtime config. +func EffectiveWebSearchConfig(cfg *WebSearchConfig) *WebSearchConfig { + if cfg == nil { + return DefaultWebSearchConfig() + } + + normalized := *cfg + if normalized.MaxResults <= 0 { + normalized.MaxResults = DefaultWebSearchMaxResults + } + if normalized.CompressionMethod == "" { + normalized.CompressionMethod = DefaultWebSearchCompressionMethod + } + if normalized.Blacklist == nil { + normalized.Blacklist = []string{} + } + + return &normalized +} + // Value implements driver.Valuer interface for WebSearchConfig func (c WebSearchConfig) Value() (driver.Value, error) { return json.Marshal(c) diff --git a/internal/types/web_search_test.go b/internal/types/web_search_test.go new file mode 100644 index 000000000..9c0d797f3 --- /dev/null +++ b/internal/types/web_search_test.go @@ -0,0 +1,54 @@ +package types + +import "testing" + +func TestEffectiveWebSearchConfigUsesDefaultsForNilConfig(t *testing.T) { + cfg := EffectiveWebSearchConfig(nil) + + if cfg.MaxResults != DefaultWebSearchMaxResults { + t.Fatalf("MaxResults = %d, want %d", cfg.MaxResults, DefaultWebSearchMaxResults) + } + if cfg.CompressionMethod != DefaultWebSearchCompressionMethod { + t.Fatalf("CompressionMethod = %q, want %q", cfg.CompressionMethod, DefaultWebSearchCompressionMethod) + } + if cfg.Blacklist == nil { + t.Fatal("Blacklist = nil, want empty slice") + } + if len(cfg.Blacklist) != 0 { + t.Fatalf("Blacklist length = %d, want 0", len(cfg.Blacklist)) + } +} + +func TestEffectiveWebSearchConfigNormalizesZeroValuesWithoutMutatingSource(t *testing.T) { + source := &WebSearchConfig{ + IncludeDate: true, + } + + cfg := EffectiveWebSearchConfig(source) + + if cfg == source { + t.Fatal("EffectiveWebSearchConfig returned original pointer, want normalized copy") + } + if cfg.MaxResults != DefaultWebSearchMaxResults { + t.Fatalf("MaxResults = %d, want %d", cfg.MaxResults, DefaultWebSearchMaxResults) + } + if cfg.CompressionMethod != DefaultWebSearchCompressionMethod { + t.Fatalf("CompressionMethod = %q, want %q", cfg.CompressionMethod, DefaultWebSearchCompressionMethod) + } + if cfg.Blacklist == nil { + t.Fatal("Blacklist = nil, want empty slice") + } + if !cfg.IncludeDate { + t.Fatal("IncludeDate = false, want true") + } + + if source.MaxResults != 0 { + t.Fatalf("source MaxResults mutated to %d, want 0", source.MaxResults) + } + if source.CompressionMethod != "" { + t.Fatalf("source CompressionMethod mutated to %q, want empty string", source.CompressionMethod) + } + if source.Blacklist != nil { + t.Fatalf("source Blacklist mutated to non-nil value: %#v", source.Blacklist) + } +}