fix: strip think tags from heartbeat messages and log web search provider

- Strip <think> blocks from force response path (max iterations) in
  runLLMIteration, which was missing StripThinkBlocks
- Add StripThinkBlocks safety net in heartbeat sendResponse to prevent
  LLM reasoning artifacts from leaking to users
- Add ProviderName() to WebSearchTool and log selected search provider
  (brave/perplexity/tavily/duckduckgo) at startup for diagnostics
- Show web search provider in gateway startup console output

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-03-01 04:52:31 +09:00
parent 3e5612be4b
commit 9af0dde1f2
4 changed files with 43 additions and 7 deletions

View file

@ -80,6 +80,11 @@ func gatewayCmd(debug bool, orchestration bool, enableStats bool) error {
toolsInfo := startupInfo["tools"].(map[string]any) toolsInfo := startupInfo["tools"].(map[string]any)
skillsInfo := startupInfo["skills"].(map[string]any) skillsInfo := startupInfo["skills"].(map[string]any)
fmt.Printf(" • Tools: %d loaded\n", toolsInfo["count"]) fmt.Printf(" • Tools: %d loaded\n", toolsInfo["count"])
if wsProvider, ok := toolsInfo["web_search_provider"].(string); ok {
fmt.Printf(" • Web search: %s\n", wsProvider)
} else {
fmt.Println(" • Web search: disabled")
}
fmt.Printf(" • Skills: %d/%d available\n", fmt.Printf(" • Skills: %d/%d available\n",
skillsInfo["available"], skillsInfo["available"],
skillsInfo["total"]) skillsInfo["total"])

View file

@ -240,6 +240,14 @@ func registerSharedTools(
Proxy: cfg.Tools.Web.Proxy, Proxy: cfg.Tools.Web.Proxy,
}); searchTool != nil { }); searchTool != nil {
agent.Tools.Register(searchTool) agent.Tools.Register(searchTool)
logger.InfoCF("agent", "Web search provider registered", map[string]any{
"agent_id": agentID,
"provider": searchTool.ProviderName(),
})
} else {
logger.WarnCF("agent", "No web search provider configured", map[string]any{
"agent_id": agentID,
})
} }
agent.Tools.Register(tools.NewWebFetchToolWithProxy(50000, cfg.Tools.Web.Proxy)) agent.Tools.Register(tools.NewWebFetchToolWithProxy(50000, cfg.Tools.Web.Proxy))
@ -2775,7 +2783,7 @@ func (al *AgentLoop) runLLMIteration(
"prompt_cache_key": agent.ID, "prompt_cache_key": agent.ID,
}) })
if forceErr == nil && forceResp.Content != "" { if forceErr == nil && forceResp.Content != "" {
finalContent = forceResp.Content finalContent = utils.StripThinkBlocks(forceResp.Content)
if forceResp.Usage != nil && al.stats != nil { if forceResp.Usage != nil && al.stats != nil {
al.stats.RecordUsage( al.stats.RecordUsage(
forceResp.Usage.PromptTokens, forceResp.Usage.PromptTokens,
@ -2896,10 +2904,17 @@ func (al *AgentLoop) GetStartupInfo() map[string]any {
// Tools info // Tools info
toolsList := agent.Tools.List() toolsList := agent.Tools.List()
info["tools"] = map[string]any{ toolsMap := map[string]any{
"count": len(toolsList), "count": len(toolsList),
"names": toolsList, "names": toolsList,
} }
// Report web search provider if registered
if t, ok := agent.Tools.Get("web_search"); ok {
if wst, ok := t.(*tools.WebSearchTool); ok {
toolsMap["web_search_provider"] = wst.ProviderName()
}
}
info["tools"] = toolsMap
// Skills info // Skills info
info["skills"] = agent.ContextBuilder.GetSkillsInfo() info["skills"] = agent.ContextBuilder.GetSkillsInfo()

View file

@ -21,6 +21,7 @@ import (
"github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/logger"
"github.com/sipeed/picoclaw/pkg/state" "github.com/sipeed/picoclaw/pkg/state"
"github.com/sipeed/picoclaw/pkg/tools" "github.com/sipeed/picoclaw/pkg/tools"
"github.com/sipeed/picoclaw/pkg/utils"
) )
const ( const (
@ -308,8 +309,11 @@ Add your heartbeat tasks below this line:
} }
} }
// sendResponse sends the heartbeat response to the last channel // sendResponse sends the heartbeat response to the last channel.
// Think blocks are stripped as a safety net to prevent LLM reasoning
// artifacts from leaking into user-facing messages.
func (hs *HeartbeatService) sendResponse(response string) { func (hs *HeartbeatService) sendResponse(response string) {
response = utils.StripThinkBlocks(response)
hs.mu.RLock() hs.mu.RLock()
msgBus := hs.bus msgBus := hs.bus
hs.mu.RUnlock() hs.mu.RUnlock()

View file

@ -387,8 +387,14 @@ func (p *PerplexitySearchProvider) Search(ctx context.Context, query string, cou
} }
type WebSearchTool struct { type WebSearchTool struct {
provider SearchProvider provider SearchProvider
maxResults int providerName string
maxResults int
}
// ProviderName returns the name of the active search provider (e.g. "brave", "perplexity").
func (t *WebSearchTool) ProviderName() string {
return t.providerName
} }
type WebSearchToolOptions struct { type WebSearchToolOptions struct {
@ -409,16 +415,19 @@ type WebSearchToolOptions struct {
func NewWebSearchTool(opts WebSearchToolOptions) *WebSearchTool { func NewWebSearchTool(opts WebSearchToolOptions) *WebSearchTool {
var provider SearchProvider var provider SearchProvider
var providerName string
maxResults := 5 maxResults := 5
// Priority: Perplexity > Brave > Tavily > DuckDuckGo // Priority: Perplexity > Brave > Tavily > DuckDuckGo
if opts.PerplexityEnabled && opts.PerplexityAPIKey != "" { if opts.PerplexityEnabled && opts.PerplexityAPIKey != "" {
provider = &PerplexitySearchProvider{apiKey: opts.PerplexityAPIKey, proxy: opts.Proxy} provider = &PerplexitySearchProvider{apiKey: opts.PerplexityAPIKey, proxy: opts.Proxy}
providerName = "perplexity"
if opts.PerplexityMaxResults > 0 { if opts.PerplexityMaxResults > 0 {
maxResults = opts.PerplexityMaxResults maxResults = opts.PerplexityMaxResults
} }
} else if opts.BraveEnabled && opts.BraveAPIKey != "" { } else if opts.BraveEnabled && opts.BraveAPIKey != "" {
provider = &BraveSearchProvider{apiKey: opts.BraveAPIKey, proxy: opts.Proxy} provider = &BraveSearchProvider{apiKey: opts.BraveAPIKey, proxy: opts.Proxy}
providerName = "brave"
if opts.BraveMaxResults > 0 { if opts.BraveMaxResults > 0 {
maxResults = opts.BraveMaxResults maxResults = opts.BraveMaxResults
} }
@ -428,11 +437,13 @@ func NewWebSearchTool(opts WebSearchToolOptions) *WebSearchTool {
baseURL: opts.TavilyBaseURL, baseURL: opts.TavilyBaseURL,
proxy: opts.Proxy, proxy: opts.Proxy,
} }
providerName = "tavily"
if opts.TavilyMaxResults > 0 { if opts.TavilyMaxResults > 0 {
maxResults = opts.TavilyMaxResults maxResults = opts.TavilyMaxResults
} }
} else if opts.DuckDuckGoEnabled { } else if opts.DuckDuckGoEnabled {
provider = &DuckDuckGoSearchProvider{proxy: opts.Proxy} provider = &DuckDuckGoSearchProvider{proxy: opts.Proxy}
providerName = "duckduckgo"
if opts.DuckDuckGoMaxResults > 0 { if opts.DuckDuckGoMaxResults > 0 {
maxResults = opts.DuckDuckGoMaxResults maxResults = opts.DuckDuckGoMaxResults
} }
@ -441,8 +452,9 @@ func NewWebSearchTool(opts WebSearchToolOptions) *WebSearchTool {
} }
return &WebSearchTool{ return &WebSearchTool{
provider: provider, provider: provider,
maxResults: maxResults, providerName: providerName,
maxResults: maxResults,
} }
} }