mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
feat(providers): add gemini web search provider (#2763)
* add gemini web search provider * fix(web): prefer free providers before Gemini in auto mode * fix(web): expose gemini api key and model settings * fix(web): prefer configured providers before Gemini in auto mode * fix(web): satisfy gemini lint checks * fix(web): address gemini provider review feedback * test(web): align auto-provider expectations * fix(web): let gemini ignore search range
This commit is contained in:
committed by
GitHub
parent
eb0653074b
commit
794eb04f32
@@ -1853,6 +1853,191 @@ func TestWebTool_AutoProviderPrefersConfiguredProvidersBeforeSogou(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebTool_AutoProviderPrefersConfiguredProvidersBeforeGemini(t *testing.T) {
|
||||
opts := WebSearchToolOptions{
|
||||
GeminiEnabled: true,
|
||||
GeminiAPIKey: "google-key",
|
||||
GeminiModel: "gemini-2.5-flash",
|
||||
GeminiMaxResults: 5,
|
||||
BraveEnabled: true,
|
||||
BraveAPIKeys: []string{"brave-key"},
|
||||
BraveMaxResults: 5,
|
||||
SogouEnabled: true,
|
||||
SogouMaxResults: 5,
|
||||
DuckDuckGoEnabled: true,
|
||||
DuckDuckGoMaxResults: 5,
|
||||
}
|
||||
|
||||
name, err := ResolveWebSearchProviderName(opts, "best robotics companies")
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveWebSearchProviderName() error: %v", err)
|
||||
}
|
||||
if name != "brave" {
|
||||
t.Fatalf("provider = %q, want brave", name)
|
||||
}
|
||||
|
||||
name, err = ResolveWebSearchProviderName(opts, "今天上海天气")
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveWebSearchProviderName() error: %v", err)
|
||||
}
|
||||
if name != "brave" {
|
||||
t.Fatalf("provider = %q, want brave", name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebTool_GeminiRequiresAPIKey(t *testing.T) {
|
||||
tool, err := NewWebSearchTool(WebSearchToolOptions{
|
||||
Provider: "gemini",
|
||||
GeminiEnabled: true,
|
||||
SogouEnabled: true,
|
||||
SogouMaxResults: 5,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewWebSearchTool() error: %v", err)
|
||||
}
|
||||
if _, ok := tool.provider.(*SogouSearchProvider); !ok {
|
||||
t.Fatalf("expected SogouSearchProvider after missing Gemini API key fallback, got %T", tool.provider)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeminiSearchProvider_SearchSuccess(t *testing.T) {
|
||||
provider := &GeminiSearchProvider{
|
||||
apiKey: "google-key",
|
||||
model: "gemini-2.5-flash",
|
||||
client: &http.Client{
|
||||
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
if req.Method != http.MethodPost {
|
||||
t.Fatalf("method = %s, want POST", req.Method)
|
||||
}
|
||||
if got := req.Header.Get("X-Goog-Api-Key"); got != "google-key" {
|
||||
t.Fatalf("X-Goog-Api-Key = %q, want google-key", got)
|
||||
}
|
||||
if !strings.Contains(req.URL.String(), "/models/gemini-2.5-flash:generateContent") {
|
||||
t.Fatalf("unexpected URL: %s", req.URL.String())
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
rec.WriteHeader(http.StatusOK)
|
||||
fmt.Fprint(rec, `{
|
||||
"candidates": [
|
||||
{
|
||||
"content": {
|
||||
"parts": [
|
||||
{"text": "Answer paragraph one."},
|
||||
{"text": "Answer paragraph two."}
|
||||
]
|
||||
},
|
||||
"groundingMetadata": {
|
||||
"groundingChunks": [
|
||||
{"web": {"uri": "https://example.com/a", "title": "Result A"}},
|
||||
{"web": {"uri": "https://example.com/b", "title": "Result B"}},
|
||||
{"web": {"uri": "https://example.com/c", "title": "Result C"}}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}`)
|
||||
return rec.Result(), nil
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
out, err := provider.Search(context.Background(), "robotics", 2, "")
|
||||
if err != nil {
|
||||
t.Fatalf("Search() error: %v", err)
|
||||
}
|
||||
if !strings.Contains(out, "Results for: robotics (via Gemini Google Search)") {
|
||||
t.Fatalf("missing header in output: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, "Answer paragraph one.") || !strings.Contains(out, "Answer paragraph two.") {
|
||||
t.Fatalf("missing response text in output: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, "1. Result A") || !strings.Contains(out, "2. Result B") {
|
||||
t.Fatalf("missing citations in output: %s", out)
|
||||
}
|
||||
if strings.Contains(out, "Result C") {
|
||||
t.Fatalf("expected citations to be limited to count=2, got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeminiSearchProvider_SearchIgnoresRange(t *testing.T) {
|
||||
provider := &GeminiSearchProvider{
|
||||
apiKey: "google-key",
|
||||
model: "gemini-2.5-flash",
|
||||
client: &http.Client{
|
||||
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
rec := httptest.NewRecorder()
|
||||
rec.WriteHeader(http.StatusOK)
|
||||
fmt.Fprint(rec, `{
|
||||
"candidates": [
|
||||
{
|
||||
"content": {
|
||||
"parts": [
|
||||
{"text": "Recent robotics result."}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}`)
|
||||
return rec.Result(), nil
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
out, err := provider.Search(context.Background(), "robotics", 2, "d")
|
||||
if err != nil {
|
||||
t.Fatalf("Search() error: %v", err)
|
||||
}
|
||||
if !strings.Contains(out, "Recent robotics result.") {
|
||||
t.Fatalf("missing response text in output: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeminiSearchProvider_SearchAPIError(t *testing.T) {
|
||||
provider := &GeminiSearchProvider{
|
||||
apiKey: "google-key",
|
||||
model: "gemini-2.5-flash",
|
||||
client: &http.Client{
|
||||
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
rec := httptest.NewRecorder()
|
||||
rec.WriteHeader(http.StatusTooManyRequests)
|
||||
fmt.Fprint(rec, `{"error":"quota exceeded"}`)
|
||||
return rec.Result(), nil
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
_, err := provider.Search(context.Background(), "robotics", 2, "")
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "status 429") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeminiSearchProvider_SearchEmptyCandidates(t *testing.T) {
|
||||
provider := &GeminiSearchProvider{
|
||||
apiKey: "google-key",
|
||||
model: "gemini-2.5-flash",
|
||||
client: &http.Client{
|
||||
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
rec := httptest.NewRecorder()
|
||||
rec.WriteHeader(http.StatusOK)
|
||||
fmt.Fprint(rec, `{"candidates":[]}`)
|
||||
return rec.Result(), nil
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
out, err := provider.Search(context.Background(), "robotics", 2, "")
|
||||
if err != nil {
|
||||
t.Fatalf("Search() error: %v", err)
|
||||
}
|
||||
if out != "No results for: robotics" {
|
||||
t.Fatalf("output = %q, want %q", out, "No results for: robotics")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebTool_ExplicitProviderFallsBackWhenMissingCredentials(t *testing.T) {
|
||||
tool, err := NewWebSearchTool(WebSearchToolOptions{
|
||||
Provider: "brave",
|
||||
|
||||
Reference in New Issue
Block a user