Skip to content

Commit ae86981

Browse files
authored
Replace Linkup web search with Serper (#717)
1 parent 0b22c48 commit ae86981

17 files changed

Lines changed: 251 additions & 128 deletions

File tree

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ STRIPE_SUBSCRIPTION_200_PRICE_ID=price_dummy_subscription_200_id
2828
STRIPE_SUBSCRIPTION_500_PRICE_ID=price_dummy_subscription_500_id
2929

3030
# External Services
31-
LINKUP_API_KEY=dummy_linkup_key
31+
SERPER_API_KEY=dummy_serper_key
3232
LOOPS_API_KEY=dummy_loops_key
3333
ZEROCLICK_API_KEY=dummy_zeroclick_key
3434

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { publisher } from '../constants'
22

3-
import type { ToolCall } from '../types/agent-definition'
43
import type { SecretAgentDefinition } from '../types/secret-agent-definition'
54

65
const definition: SecretAgentDefinition = {
@@ -17,36 +16,18 @@ const definition: SecretAgentDefinition = {
1716
},
1817
outputMode: 'last_message',
1918
includeMessageHistory: false,
20-
toolNames: ['web_search'],
19+
toolNames: ['web_search', 'run_terminal_command'],
2120
spawnableAgents: [],
2221

23-
systemPrompt: `You are an expert researcher who can search the web to find relevant information. Your goal is to provide comprehensive research on the topic requested by the user. Use web_search to find current information.`,
22+
systemPrompt: `You are an expert researcher who can search the web to find relevant information. Your goal is to answer the user's question from current search results and any useful source pages. Use web_search to get Serper JSON search results. Use run_terminal_command with tools like curl to fetch web pages that would help answer the user's question.`,
2423
instructionsPrompt: `Provide comprehensive research on the user's prompt.
2524
26-
Use web_search to find current information. Repeat the web_search tool call until you have gathered all the relevant information.
25+
Use web_search to find current information. The tool returns JSON search results, so inspect the titles, links, snippets, answer boxes, and related results before deciding what to fetch next.
2726
28-
Then, write up a concise report that includes key findings for the user's prompt.
29-
`.trim(),
30-
31-
handleSteps: function* ({ agentState, prompt, params }) {
32-
const { toolResult } = yield {
33-
toolName: 'web_search' as const,
34-
input: { query: prompt || '', depth: 'standard' as const },
35-
includeToolCall: false,
36-
} satisfies ToolCall<'web_search'>
37-
38-
const results = (toolResult
39-
?.filter((r) => r.type === 'json')
40-
?.map((r) => r.value)?.[0] ?? {}) as {
41-
result: string | undefined
42-
errorMessage: string | undefined
43-
}
27+
Use run_terminal_command to fetch any web page that would help answer the user's question. Prefer targeted, relevant pages from the search results. Avoid fetching pages that are unlikely to add useful evidence.
4428
45-
yield {
46-
type: 'STEP_TEXT',
47-
text: results.result ?? results.errorMessage ?? '',
48-
}
49-
},
29+
Then, write up a concise answer that includes key findings for the user's prompt and cites source URLs when useful.
30+
`.trim(),
5031
}
5132

5233
export default definition

agents/types/tools.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ export interface ThinkDeeplyParams {
398398
}
399399

400400
/**
401-
* Search the web for current information using Linkup API.
401+
* Search the web for current information using Serper API.
402402
*/
403403
export interface WebSearchParams {
404404
/** The search query to find relevant web content */

cli/src/__tests__/test-utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ const TEST_SERVER_ENV_DEFAULTS: Record<string, string> = {
7070
OPEN_ROUTER_API_KEY: 'test',
7171
OPENAI_API_KEY: 'test',
7272
ANTHROPIC_API_KEY: 'test',
73-
LINKUP_API_KEY: 'test',
73+
SERPER_API_KEY: 'test',
7474
GRAVITY_API_KEY: 'test',
7575
PORT: '4242',
7676
DATABASE_URL: 'postgres://user:pass@localhost:5432/db',

common/src/templates/initial-agents-dir/types/tools.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ export interface ThinkDeeplyParams {
398398
}
399399

400400
/**
401-
* Search the web for current information using Linkup API.
401+
* Search the web for current information using Serper API.
402402
*/
403403
export interface WebSearchParams {
404404
/** The search query to find relevant web content */

common/src/tools/params/tool/web-search.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ const inputSchema = z
2020
`Search depth - 'standard' for quick results, 'deep' for more comprehensive search. Default is 'standard'.`,
2121
),
2222
})
23-
.describe(`Search the web for current information using Linkup API.`)
23+
.describe(`Search the web for current information using Serper API.`)
2424
const description = `
25-
Purpose: Search the web for current, up-to-date information on any topic. This tool uses Linkup's web search API to find relevant content from across the internet.
25+
Purpose: Search the web for current, up-to-date information on any topic. This tool uses Serper's Google Search API to find relevant content from across the internet.
2626
2727
Use cases:
2828
- Finding current information about technologies, libraries, or frameworks
@@ -31,7 +31,7 @@ Use cases:
3131
- Finding examples and tutorials
3232
- Checking current status of services or APIs
3333
34-
The tool will return search results with titles, URLs, and content snippets.
34+
The tool will return JSON search results with titles, URLs, content snippets, and other available SERP fields such as answer boxes or related questions.
3535
3636
Example:
3737
${$getNativeToolCallExampleString({

packages/agent-runtime/src/__tests__/web-search-tool.test.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { getInitialSessionState } from '@codebuff/common/types/session-state'
55
import { promptSuccess, success } from '@codebuff/common/util/error'
66
import {
77
afterEach,
8-
98
beforeEach,
109
describe,
1110
expect,
@@ -243,7 +242,7 @@ describe('web_search tool with researcher agent (via web API facade)', () => {
243242

244243
test('should handle API errors gracefully', async () => {
245244
spyOn(webApi, 'callWebSearchAPI').mockResolvedValue({
246-
error: 'Linkup API timeout',
245+
error: 'Serper API timeout',
247246
})
248247

249248
mockAgentStream([
@@ -275,7 +274,7 @@ describe('web_search tool with researcher agent (via web API facade)', () => {
275274
expect(toolMsgs.length).toBeGreaterThan(0)
276275
const last = JSON.stringify(toolMsgs[toolMsgs.length - 1].content)
277276
expect(last).toContain('errorMessage')
278-
expect(last).toContain('Linkup API timeout')
277+
expect(last).toContain('Serper API timeout')
279278
})
280279

281280
test('should handle non-Error exceptions from facade', async () => {

packages/agent-runtime/src/llm-api/__tests__/linkup-api.test.ts renamed to packages/agent-runtime/src/llm-api/__tests__/serper-api.test.ts

Lines changed: 45 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,16 @@ import {
1414
test,
1515
} from 'bun:test'
1616

17-
import { searchWeb } from '../linkup-api'
17+
import { searchWeb } from '../serper-api'
1818

1919
import type { AgentRuntimeDeps } from '@codebuff/common/types/contracts/agent-runtime'
2020

21-
// Test server env for Linkup API
22-
const testServerEnv = { LINKUP_API_KEY: 'test-api-key' }
21+
const testServerEnv = { SERPER_API_KEY: 'test-api-key' }
2322

24-
describe('Linkup API', () => {
23+
describe('Serper API', () => {
2524
let agentRuntimeImpl: AgentRuntimeDeps & { serverEnv: typeof testServerEnv }
2625

2726
beforeAll(async () => {
28-
// Mock withTimeout utility
2927
await mockModule('@codebuff/common/util/promise', () => ({
3028
withTimeout: async (promise: Promise<any>, timeout: number) => promise,
3129
}))
@@ -48,14 +46,14 @@ describe('Linkup API', () => {
4846

4947
test('should successfully search with basic query', async () => {
5048
const mockResponse = {
51-
answer:
52-
'React is a JavaScript library for building user interfaces. You can learn how to build your first React application by following the official documentation.',
53-
sources: [
49+
searchParameters: { q: 'React tutorial', type: 'search', num: 10 },
50+
organic: [
5451
{
55-
name: 'React Documentation',
56-
url: 'https://react.dev',
52+
title: 'React Documentation',
53+
link: 'https://react.dev',
5754
snippet:
5855
'React is a JavaScript library for building user interfaces.',
56+
position: 1,
5957
},
6058
],
6159
}
@@ -74,37 +72,32 @@ describe('Linkup API', () => {
7472
query: 'React tutorial',
7573
})
7674

77-
expect(result).toBe(
78-
'React is a JavaScript library for building user interfaces. You can learn how to build your first React application by following the official documentation.',
79-
)
80-
81-
// Verify fetch was called with correct parameters
75+
expect(JSON.parse(result!)).toEqual(mockResponse)
8276
expect(agentRuntimeImpl.fetch).toHaveBeenCalledWith(
83-
'https://api.linkup.so/v1/search',
77+
'https://google.serper.dev/search',
8478
expect.objectContaining({
8579
method: 'POST',
8680
headers: {
8781
'Content-Type': 'application/json',
88-
Authorization: 'Bearer test-api-key',
82+
'X-API-KEY': 'test-api-key',
8983
},
9084
body: JSON.stringify({
9185
q: 'React tutorial',
92-
depth: 'standard',
93-
outputType: 'sourcedAnswer',
86+
num: 10,
9487
}),
9588
}),
9689
)
9790
})
9891

9992
test('should handle custom depth', async () => {
10093
const mockResponse = {
101-
answer:
102-
'Advanced React patterns include render props, higher-order components, and custom hooks for building reusable and maintainable components.',
103-
sources: [
94+
searchParameters: { q: 'React patterns', type: 'search', num: 20 },
95+
organic: [
10496
{
105-
name: 'Advanced React Patterns',
106-
url: 'https://example.com/advanced-react',
97+
title: 'Advanced React Patterns',
98+
link: 'https://example.com/advanced-react',
10799
snippet: 'Deep dive into React patterns and best practices.',
100+
position: 1,
108101
},
109102
],
110103
}
@@ -124,18 +117,13 @@ describe('Linkup API', () => {
124117
depth: 'deep',
125118
})
126119

127-
expect(result).toBe(
128-
'Advanced React patterns include render props, higher-order components, and custom hooks for building reusable and maintainable components.',
129-
)
130-
131-
// Verify fetch was called with correct parameters
120+
expect(JSON.parse(result!)).toEqual(mockResponse)
132121
expect(agentRuntimeImpl.fetch).toHaveBeenCalledWith(
133-
'https://api.linkup.so/v1/search',
122+
'https://google.serper.dev/search',
134123
expect.objectContaining({
135124
body: JSON.stringify({
136125
q: 'React patterns',
137-
depth: 'deep',
138-
outputType: 'sourcedAnswer',
126+
num: 20,
139127
}),
140128
}),
141129
)
@@ -169,7 +157,7 @@ describe('Linkup API', () => {
169157
test('should handle invalid response format', async () => {
170158
agentRuntimeImpl.fetch = mock(() => {
171159
return Promise.resolve(
172-
new Response(JSON.stringify({ invalid: 'format' }), {
160+
new Response(JSON.stringify(['invalid']), {
173161
status: 200,
174162
headers: { 'Content-Type': 'application/json' },
175163
}),
@@ -181,10 +169,21 @@ describe('Linkup API', () => {
181169
expect(result).toBeNull()
182170
})
183171

184-
test('should handle missing answer field', async () => {
172+
test('should return JSON search results without an answer field', async () => {
173+
const mockResponse = {
174+
organic: [
175+
{
176+
title: 'Test result',
177+
link: 'https://example.com',
178+
snippet: 'Test snippet',
179+
position: 1,
180+
},
181+
],
182+
}
183+
185184
agentRuntimeImpl.fetch = mock(() => {
186185
return Promise.resolve(
187-
new Response(JSON.stringify({ sources: [] }), {
186+
new Response(JSON.stringify(mockResponse), {
188187
status: 200,
189188
headers: { 'Content-Type': 'application/json' },
190189
}),
@@ -196,12 +195,13 @@ describe('Linkup API', () => {
196195
query: 'test query',
197196
})
198197

199-
expect(result).toBeNull()
198+
expect(JSON.parse(result!)).toEqual(mockResponse)
200199
})
201-
test('should handle empty answer', async () => {
200+
201+
test('should return sparse JSON search results', async () => {
202202
const mockResponse = {
203-
answer: '',
204-
sources: [],
203+
searchParameters: { q: 'test query', type: 'search' },
204+
organic: [],
205205
}
206206

207207
agentRuntimeImpl.fetch = mock(() => {
@@ -215,14 +215,13 @@ describe('Linkup API', () => {
215215

216216
const result = await searchWeb({ ...agentRuntimeImpl, query: 'test query' })
217217

218-
expect(result).toBeNull()
218+
expect(JSON.parse(result!)).toEqual(mockResponse)
219219
})
220220

221221
test('should use default options when none provided', async () => {
222222
const mockResponse = {
223-
answer: 'Test answer content',
224-
sources: [
225-
{ name: 'Test', url: 'https://example.com', snippet: 'Test content' },
223+
organic: [
224+
{ title: 'Test', link: 'https://example.com', snippet: 'Test content' },
226225
],
227226
}
228227

@@ -237,14 +236,12 @@ describe('Linkup API', () => {
237236

238237
await searchWeb({ ...agentRuntimeImpl, query: 'test query' })
239238

240-
// Verify fetch was called with default parameters
241239
expect(agentRuntimeImpl.fetch).toHaveBeenCalledWith(
242-
'https://api.linkup.so/v1/search',
240+
'https://google.serper.dev/search',
243241
expect.objectContaining({
244242
body: JSON.stringify({
245243
q: 'test query',
246-
depth: 'standard',
247-
outputType: 'sourcedAnswer',
244+
num: 10,
248245
}),
249246
}),
250247
)
@@ -264,7 +261,6 @@ describe('Linkup API', () => {
264261
const result = await searchWeb({ ...agentRuntimeImpl, query: 'test query' })
265262

266263
expect(result).toBeNull()
267-
// Verify that error logging was called
268264
expect(agentRuntimeImpl.logger.error).toHaveBeenCalled()
269265
})
270266

@@ -287,13 +283,12 @@ describe('Linkup API', () => {
287283
})
288284

289285
expect(result).toBeNull()
290-
// Verify that detailed error logging was called with 404 info
291286
expect(agentRuntimeImpl.logger.error).toHaveBeenCalledWith(
292287
expect.objectContaining({
293288
status: 404,
294289
statusText: 'Not Found',
295290
responseBody: mockErrorResponse,
296-
requestUrl: 'https://api.linkup.so/v1/search',
291+
requestUrl: 'https://google.serper.dev/search',
297292
query: 'test query for 404',
298293
}),
299294
expect.stringContaining('404'),

0 commit comments

Comments
 (0)