Skip to content

Commit 76d3fca

Browse files
committed
feat(web-search): provider interface + brave key rename + typed schemas
1 parent 46bf29b commit 76d3fca

16 files changed

+514
-140
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ nekobot/
195195
- **write_file**: Write content to files
196196
- **list_dir**: List directory contents
197197
- **exec**: Execute shell commands
198-
- **web_search**: Search the web using Brave Search API
198+
- **web_search**: Search the web using Brave Search (with DuckDuckGo fallback)
199199
- **web_fetch**: Fetch and extract content from URLs
200200
- **message**: Send messages to user
201201

docs/ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
**Built-in Tools**:
7070
- **file.go** - Read, write, list, edit, append files
7171
- **exec.go** - Shell command execution
72-
- **web_search.go** - Brave Search API integration
72+
- **web_search.go** - Brave Search + DuckDuckGo fallback integration
7373
- **web_fetch.go** - URL content fetching with HTML parsing
7474
- **browser.go** - Chrome CDP automation (navigate, screenshot, click, type, execute JS)
7575
- **message.go** - Direct user communication via bus

docs/PROGRESS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575
- ✅ Tool registry and discovery
7676
- ✅ File operations (read, write, edit, append, list)
7777
- ✅ Shell execution (exec with safety guards)
78-
- ✅ Web search (Brave API)
78+
- ✅ Web search (Brave API + DuckDuckGo fallback)
7979
- ✅ Web fetch (HTTP content)
8080
- ✅ Message tool (user communication)
8181
- ✅ Spawn/subagent (async tasks)

docs/WEB_TOOLS.md

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,22 @@ Nekobot includes web tools that enable the agent to search the internet and fetc
66

77
### 1. web_search
88

9-
Search the web for current information using Brave Search API.
9+
Search the web for current information.
10+
11+
Provider behavior:
12+
- If `brave_api_key` is configured, use **Brave Search API**
13+
- If Brave fails (or key is missing), fallback/use **DuckDuckGo HTML search**
1014

1115
**Configuration:**
1216
```json
1317
{
1418
"tools": {
1519
"web": {
1620
"search": {
17-
"api_key": "your-brave-search-api-key",
18-
"max_results": 5
21+
"brave_api_key": "your-brave-search-api-key",
22+
"max_results": 5,
23+
"duckduckgo_enabled": true,
24+
"duckduckgo_max_results": 5
1925
}
2026
}
2127
}
@@ -30,7 +36,7 @@ Search the web for current information using Brave Search API.
3036

3137
**Environment Variable:**
3238
```bash
33-
export NEKOBOT_TOOLS_WEB_SEARCH_API_KEY="your-brave-api-key"
39+
export NEKOBOT_TOOLS_WEB_SEARCH_BRAVE_API_KEY="your-brave-api-key"
3440
```
3541

3642
**Usage Example:**
@@ -200,26 +206,13 @@ The agent automatically decides when to use these tools based on the conversatio
200206

201207
## Troubleshooting
202208

203-
### web_search Not Available
209+
### web_search Results Are Weak/Empty
204210

205-
**Issue:** "web search API key not configured"
211+
**Issue:** DuckDuckGo fallback returns low-quality/limited results in some regions.
206212

207-
**Solution:**
213+
**Solution:** configure Brave API key for better relevance and stability.
208214
```bash
209-
export NEKOBOT_TOOLS_WEB_SEARCH_API_KEY="your-api-key"
210-
```
211-
212-
Or add to config.json:
213-
```json
214-
{
215-
"tools": {
216-
"web": {
217-
"search": {
218-
"api_key": "your-api-key"
219-
}
220-
}
221-
}
222-
}
215+
export NEKOBOT_TOOLS_WEB_SEARCH_BRAVE_API_KEY="your-api-key"
223216
```
224217

225218
### web_fetch Fails
@@ -257,7 +250,7 @@ Or add to config.json:
257250
2. **URL Validation**: Only http:// and https:// URLs are allowed
258251
3. **Content Filtering**: HTML is sanitized during extraction
259252
4. **Rate Limiting**: Respect API rate limits
260-
5. **Privacy**: Web requests are logged by Brave Search
253+
5. **Privacy**: Web requests may be logged by upstream search providers (Brave/DuckDuckGo)
261254

262255
## Cost
263256

@@ -274,7 +267,7 @@ Or add to config.json:
274267
## Future Enhancements
275268

276269
Planned improvements:
277-
- [ ] Support for alternative search providers (Google, DuckDuckGo)
270+
- [ ] Support for additional providers (Google, Bing, etc.)
278271
- [ ] Image search and fetch
279272
- [ ] PDF content extraction
280273
- [ ] Markdown output option

pkg/agent/agent.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,15 @@ func New(cfg *config.Config, log *logger.Logger, providerClient *providers.Clien
7272
toolRegistry.MustRegister(tools.NewProcessTool(processMgr))
7373
log.Info("PTY process management enabled")
7474

75-
// Register web tools if configured
76-
if cfg.Tools.Web.Search.APIKey != "" {
77-
toolRegistry.MustRegister(tools.NewWebSearchTool(
78-
cfg.Tools.Web.Search.APIKey,
79-
cfg.Tools.Web.Search.MaxResults,
80-
))
81-
log.Info("Web search tool enabled")
75+
// Register web search tool (Brave first, optional DuckDuckGo fallback)
76+
if webSearch := tools.NewWebSearchTool(tools.WebSearchToolOptions{
77+
BraveAPIKey: cfg.Tools.Web.Search.GetBraveAPIKey(),
78+
BraveMaxResults: cfg.Tools.Web.Search.MaxResults,
79+
DuckDuckGoEnabled: cfg.Tools.Web.Search.DuckDuckGoEnabled,
80+
DuckDuckGoMaxResults: cfg.Tools.Web.Search.DuckDuckGoMaxResults,
81+
}); webSearch != nil {
82+
toolRegistry.MustRegister(webSearch)
83+
log.Info("Web search tool enabled", zap.String("providers", webSearch.ProviderSummary()))
8284
}
8385

8486
// Web fetch tool (always available)

pkg/config/config.example.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,10 @@
258258
},
259259
"web": {
260260
"search": {
261-
"api_key": "",
262-
"max_results": 5
261+
"brave_api_key": "",
262+
"max_results": 5,
263+
"duckduckgo_enabled": true,
264+
"duckduckgo_max_results": 5
263265
},
264266
"fetch": {
265267
"max_chars": 50000

pkg/config/config.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package config
88

99
import (
1010
"os"
11+
"strings"
1112
"sync"
1213
)
1314

@@ -259,8 +260,11 @@ type WebToolsConfig struct {
259260

260261
// WebSearchConfig for web search tool.
261262
type WebSearchConfig struct {
262-
APIKey string `mapstructure:"api_key" json:"api_key"`
263-
MaxResults int `mapstructure:"max_results" json:"max_results"`
263+
BraveAPIKey string `mapstructure:"brave_api_key" json:"brave_api_key"`
264+
LegacyAPIKey string `mapstructure:"api_key" json:"-"`
265+
MaxResults int `mapstructure:"max_results" json:"max_results"`
266+
DuckDuckGoEnabled bool `mapstructure:"duckduckgo_enabled" json:"duckduckgo_enabled"`
267+
DuckDuckGoMaxResults int `mapstructure:"duckduckgo_max_results" json:"duckduckgo_max_results"`
264268
}
265269

266270
// WebFetchConfig for web fetch tool.
@@ -383,7 +387,9 @@ func DefaultConfig() *Config {
383387
Tools: ToolsConfig{
384388
Web: WebToolsConfig{
385389
Search: WebSearchConfig{
386-
MaxResults: 5,
390+
MaxResults: 5,
391+
DuckDuckGoEnabled: true,
392+
DuckDuckGoMaxResults: 5,
387393
},
388394
Fetch: WebFetchConfig{
389395
MaxChars: 50000,
@@ -478,6 +484,14 @@ func (p *ProviderProfile) GetTimeout() int {
478484
return 30 // Default 30 seconds
479485
}
480486

487+
// GetBraveAPIKey returns the Brave search API key with backward compatibility.
488+
func (w WebSearchConfig) GetBraveAPIKey() string {
489+
if strings.TrimSpace(w.BraveAPIKey) != "" {
490+
return strings.TrimSpace(w.BraveAPIKey)
491+
}
492+
return strings.TrimSpace(w.LegacyAPIKey)
493+
}
494+
481495
// expandPath expands ~ to home directory.
482496
func expandPath(path string) string {
483497
if path == "" {

pkg/config/loader.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ func (l *Loader) Load(configPath string) (*Config, error) {
7070
return nil, fmt.Errorf("unmarshaling config: %w", err)
7171
}
7272

73+
// Backward compatibility: migrate tools.web.search.api_key -> brave_api_key in-memory.
74+
if cfg.Tools.Web.Search.BraveAPIKey == "" {
75+
cfg.Tools.Web.Search.BraveAPIKey = cfg.Tools.Web.Search.LegacyAPIKey
76+
}
77+
7378
return cfg, nil
7479
}
7580

pkg/config/loader_env_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package config
22

33
import (
4+
"os"
45
"path/filepath"
56
"testing"
67
)
@@ -27,3 +28,35 @@ func TestLoad_UsesConfigPathEnvWhenPathEmpty(t *testing.T) {
2728
t.Fatalf("expected gateway port 29999, got %d", got.Gateway.Port)
2829
}
2930
}
31+
32+
func TestLoad_MigratesLegacySearchAPIKey(t *testing.T) {
33+
tmpDir := t.TempDir()
34+
cfgPath := filepath.Join(tmpDir, "legacy-search-key.json")
35+
36+
content := `{
37+
"tools": {
38+
"web": {
39+
"search": {
40+
"api_key": "legacy-key",
41+
"max_results": 5,
42+
"duckduckgo_enabled": true,
43+
"duckduckgo_max_results": 5
44+
}
45+
}
46+
}
47+
}`
48+
if err := os.WriteFile(cfgPath, []byte(content), 0644); err != nil {
49+
t.Fatalf("write config: %v", err)
50+
}
51+
52+
got, err := NewLoader().Load(cfgPath)
53+
if err != nil {
54+
t.Fatalf("load config: %v", err)
55+
}
56+
if got.Tools.Web.Search.GetBraveAPIKey() != "legacy-key" {
57+
t.Fatalf("expected migrated brave key, got %q", got.Tools.Web.Search.GetBraveAPIKey())
58+
}
59+
if got.Tools.Web.Search.BraveAPIKey != "legacy-key" {
60+
t.Fatalf("expected brave_api_key field to be normalized, got %q", got.Tools.Web.Search.BraveAPIKey)
61+
}
62+
}

pkg/config/validator.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,9 @@ func (v *Validator) validateTools(cfg *ToolsConfig) {
247247
if cfg.Web.Search.MaxResults < 1 {
248248
v.addError("tools.web.search.max_results", "max_results must be at least 1")
249249
}
250+
if cfg.Web.Search.DuckDuckGoEnabled && cfg.Web.Search.DuckDuckGoMaxResults < 1 {
251+
v.addError("tools.web.search.duckduckgo_max_results", "duckduckgo_max_results must be at least 1 when duckduckgo is enabled")
252+
}
250253
if cfg.Exec.TimeoutSeconds < 1 {
251254
v.addError("tools.exec.timeout_seconds", "timeout_seconds must be at least 1")
252255
}

0 commit comments

Comments
 (0)