Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,7 @@ PicoClaw can search the web to provide up-to-date information. Configure in `too
| DuckDuckGo | Not needed | Unlimited | Built-in fallback |
| [Gemini Google Search](https://aistudio.google.com/apikey) | Required | Varies | Gemini with Google Search grounding |
| [Baidu Search](https://cloud.baidu.com/doc/qianfan-api/s/Wmbq4z7e5) | Required | 1500/month (daily allocation) | AI-powered, China-optimized |
| [Exa](https://exa.ai) | Required | Free $20 in credits on signup, $10 every month after that | Semantic search with highlights |
| [Tavily](https://tavily.com) | Required | 1000 queries/month | Optimized for AI Agents |
| [Brave Search](https://brave.com/search/api) | Required | 2000 queries/month | Fast and private |
| [Kagi Search](https://help.kagi.com/kagi/api/search.html) | Required | Paid/limited by API setup | Premium search results |
Expand Down
6 changes: 6 additions & 0 deletions config/config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,12 @@
"base_url": "",
"max_results": 0
},
"exa": {
"enabled": false,
"api_key": "",
"api_keys": ["YOUR_EXA_API_KEY"],
"max_results": 5
},
"kagi": {
"enabled": false,
"api_key": "",
Expand Down
37 changes: 37 additions & 0 deletions docs/reference/tools_configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,43 @@ Kagi API usage may be billed or limited separately from a normal Kagi subscripti
| `base_url` | string | - | Custom Tavily API base URL |
| `max_results` | int | 5 | Maximum number of results |

### Exa

[Exa](https://exa.ai) is a semantic web search API. PicoClaw calls `POST /search` with `type: "auto"` and requests
`contents.highlights`, so each result carries a relevant excerpt of the page.

| Config | Type | Default | Description |
|---------------|----------|---------|------------------------------------------------|
| `enabled` | bool | false | Enable Exa search |
| `api_key` | string | - | Exa API key |
| `api_keys` | string[] | - | Multiple API keys for rotation (takes priority over `api_key`) |
| `max_results` | int | 5 | Maximum number of results |

```json
{
"tools": {
"web": {
"provider": "exa",
"exa": {
"enabled": true,
"max_results": 5
}
}
}
}
```

Store Exa API keys in `.security.yml`:

```yaml
web:
exa:
api_keys:
- "YOUR_EXA_API_KEY"
```

For Exa, `d`, `w`, `m`, and `y` map to a `startPublishedDate` filter relative to the current time.

### SearXNG

| Config | Type | Default | Description |
Expand Down
18 changes: 14 additions & 4 deletions docs/security/security_configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ channel_list:

### Web Tools

**Brave, Tavily, Perplexity, Kagi:**
**Brave, Tavily, Perplexity, Kagi, Exa:**
```yaml
web:
brave:
Expand All @@ -258,6 +258,9 @@ web:
kagi:
api_keys:
- "your-kagi-api-key"
exa:
api_keys:
- "your-exa-api-key"
```
- Use `api_keys` (plural) array format

Expand Down Expand Up @@ -318,7 +321,7 @@ model_list:
- **Rate limit management**: Distribute usage across multiple keys
- **High availability**: Reduce downtime during API provider issues

### Web Tools (Brave/Tavily/Perplexity/Kagi) - Single key
### Web Tools (Brave/Tavily/Perplexity/Kagi/Exa) - Single key

```yaml
web:
Expand All @@ -328,9 +331,12 @@ web:
kagi:
api_keys:
- "your-kagi-api-key"
exa:
api_keys:
- "your-exa-api-key"
```

### Web Tools (Brave/Tavily/Perplexity/Kagi) - Multiple keys
### Web Tools (Brave/Tavily/Perplexity/Kagi/Exa) - Multiple keys

```yaml
web:
Expand All @@ -342,6 +348,10 @@ web:
api_keys:
- "kagi-key-1"
- "kagi-key-2"
exa:
api_keys:
- "exa-key-1"
- "exa-key-2"
```

### Web Tool (GLMSearch/BaiduSearch) - Single key only
Expand Down Expand Up @@ -568,7 +578,7 @@ go test ./pkg/config -run TestSecurityConfig

- Ensure you're using `api_keys` (plural) in `.security.yml` for models and web tools (except GLMSearch/BaiduSearch)
- Check that the array format is correct in YAML (proper indentation with dashes)
- Remember: Models, Brave, Tavily, Perplexity, Kagi MUST use `api_keys` (array format)
- Remember: Models, Brave, Tavily, Perplexity, Kagi, Exa MUST use `api_keys` (array format)
- GLMSearch and BaiduSearch MUST use `api_key` (single string format)

### Load Balancing/Failover Issues
Expand Down
25 changes: 25 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -941,6 +941,30 @@ func (c *TavilyConfig) SetAPIKeys(keys []string) {
}
}

type ExaConfig struct {
Enabled bool `json:"enabled" yaml:"-" env:"PICOCLAW_TOOLS_WEB_EXA_ENABLED"`
APIKeys SecureStrings `json:"api_keys,omitzero" yaml:"api_keys,omitempty" env:"PICOCLAW_TOOLS_WEB_EXA_API_KEYS"`
MaxResults int `json:"max_results" yaml:"-" env:"PICOCLAW_TOOLS_WEB_EXA_MAX_RESULTS"`
}

// APIKey returns the Exa API key
func (c *ExaConfig) APIKey() string {
if len(c.APIKeys) == 0 {
return ""
}
return c.APIKeys[0].String()
}

// SetAPIKey sets the Exa API key
func (c *ExaConfig) SetAPIKey(key string) {
c.APIKeys = SimpleSecureStrings(key)
}

// SetAPIKeys sets the Exa API keys
func (c *ExaConfig) SetAPIKeys(keys []string) {
c.APIKeys = SimpleSecureStrings(keys...)
}

type KagiConfig struct {
Enabled bool `json:"enabled" yaml:"-" env:"PICOCLAW_TOOLS_WEB_KAGI_ENABLED"`
APIKeys SecureStrings `json:"api_keys,omitzero" yaml:"api_keys,omitempty" env:"PICOCLAW_TOOLS_WEB_KAGI_API_KEYS"`
Expand Down Expand Up @@ -1029,6 +1053,7 @@ type WebToolsConfig struct {
ToolConfig ` yaml:"-" envPrefix:"PICOCLAW_TOOLS_WEB_"`
Brave BraveConfig `yaml:"brave,omitempty" json:"brave"`
Tavily TavilyConfig `yaml:"tavily,omitempty" json:"tavily"`
Exa ExaConfig `yaml:"exa,omitempty" json:"exa"`
Kagi KagiConfig `yaml:"kagi,omitempty" json:"kagi"`
Sogou SogouConfig `yaml:"-" json:"sogou"`
DuckDuckGo DuckDuckGoConfig `yaml:"-" json:"duckduckgo"`
Expand Down
4 changes: 4 additions & 0 deletions pkg/config/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,10 @@ func DefaultConfig() *Config {
Enabled: false,
MaxResults: 5,
},
Exa: ExaConfig{
Enabled: false,
MaxResults: 5,
},
Kagi: KagiConfig{
Enabled: false,
BaseURL: "https://kagi.com/api/v1/search",
Expand Down
14 changes: 10 additions & 4 deletions pkg/config/example_security_usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ channels:
token: "your-discord-bot-token"

# Web Tool Keys
# Brave, Tavily, Perplexity, Kagi: Use 'api_keys' array
# Brave, Tavily, Perplexity, Kagi, Exa: Use 'api_keys' array
# GLMSearch, BaiduSearch: Use 'api_key' single string
web:

Expand All @@ -68,6 +68,9 @@ web:
kagi:
api_keys:
- "your-kagi-api-key" # Single key in array format
exa:
api_keys:
- "your-exa-api-key" # Single key in array format
glm_search:
api_key: "your-glm-search-api-key" # Single key (not array)
baidu_search:
Expand Down Expand Up @@ -242,7 +245,7 @@ channels:

## Web Tool API Keys

**Brave, Tavily, Perplexity, Kagi:**
**Brave, Tavily, Perplexity, Kagi, Exa:**
```yaml
web:

Expand All @@ -259,6 +262,9 @@ web:
kagi:
api_keys:
- "kagi-key"
exa:
api_keys:
- "exa-key"

```
Use `api_keys` (plural) array format.
Expand Down Expand Up @@ -449,7 +455,7 @@ web:

## Single Key Format

**Models, Brave, Tavily, Perplexity, Kagi:**
**Models, Brave, Tavily, Perplexity, Kagi, Exa:**
```yaml
model_list:

Expand Down Expand Up @@ -571,7 +577,7 @@ and .security.yml values.
## Multiple API Keys Not Working
- Ensure you're using `api_keys` (plural) in .security.yml for models and web tools (except GLMSearch/BaiduSearch)
- Check that the array format is correct in YAML (proper indentation with dashes)
- Remember: Models, Brave, Tavily, Perplexity, Kagi MUST use `api_keys` (array format)
- Remember: Models, Brave, Tavily, Perplexity, Kagi, Exa MUST use `api_keys` (array format)
- GLMSearch and BaiduSearch MUST use `api_key` (single string format)

## Keys Not Being Applied
Expand Down
13 changes: 13 additions & 0 deletions pkg/config/security_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,10 @@ func TestAllSecurityKeysAccessible(t *testing.T) {
err = os.WriteFile(kagiAPIKeyFile, []byte("kagi-from-file-33333"), 0o600)
require.NoError(t, err)

exaAPIKeyFile := filepath.Join(tmpDir, "exa_api_key.txt")
err = os.WriteFile(exaAPIKeyFile, []byte("exa-from-file-44444"), 0o600)
require.NoError(t, err)

githubTokenFile := filepath.Join(tmpDir, "github_token.txt")
err = os.WriteFile(githubTokenFile, []byte("ghp-github-from-file-abc123"), 0o600)
require.NoError(t, err)
Expand Down Expand Up @@ -277,6 +281,9 @@ func TestAllSecurityKeysAccessible(t *testing.T) {
"kagi": {
"enabled": true
},
"exa": {
"enabled": true
},
"glm_search": {
"enabled": true
}
Expand Down Expand Up @@ -341,6 +348,9 @@ web:
kagi:
api_keys:
- "file://kagi_api_key.txt"
exa:
api_keys:
- "file://exa_api_key.txt"
glm_search:
api_key: "glm-test-glm-search-key"

Expand Down Expand Up @@ -469,6 +479,9 @@ skills:
assert.Equal(t, "kagi-from-file-33333", cfg.Tools.Web.Kagi.APIKey())
t.Logf("Kagi APIKey(): %s", cfg.Tools.Web.Kagi.APIKey())

assert.Equal(t, "exa-from-file-44444", cfg.Tools.Web.Exa.APIKey())
t.Logf("Exa APIKey(): %s", cfg.Tools.Web.Exa.APIKey())

// GLM Search - Note: GLM uses SetAPIKey (lowercase) internally
t.Logf("GLMSearch APIKey(): %s", cfg.Tools.Web.GLMSearch.APIKey.String())
assert.Equal(t, "glm-test-glm-search-key", cfg.Tools.Web.GLMSearch.APIKey.String())
Expand Down
Loading