diff --git a/.env.example b/.env.example index 17aba42c79..9c3d6c493a 100644 --- a/.env.example +++ b/.env.example @@ -28,7 +28,7 @@ STRIPE_SUBSCRIPTION_200_PRICE_ID=price_dummy_subscription_200_id STRIPE_SUBSCRIPTION_500_PRICE_ID=price_dummy_subscription_500_id # External Services -LINKUP_API_KEY=dummy_linkup_key +SERPER_API_KEY=dummy_serper_key LOOPS_API_KEY=dummy_loops_key ZEROCLICK_API_KEY=dummy_zeroclick_key diff --git a/agents/researcher/researcher-web.ts b/agents/researcher/researcher-web.ts index 289f1b14f4..28b1027689 100644 --- a/agents/researcher/researcher-web.ts +++ b/agents/researcher/researcher-web.ts @@ -1,6 +1,5 @@ import { publisher } from '../constants' -import type { ToolCall } from '../types/agent-definition' import type { SecretAgentDefinition } from '../types/secret-agent-definition' const definition: SecretAgentDefinition = { @@ -17,36 +16,18 @@ const definition: SecretAgentDefinition = { }, outputMode: 'last_message', includeMessageHistory: false, - toolNames: ['web_search'], + toolNames: ['web_search', 'run_terminal_command'], spawnableAgents: [], - 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.`, + 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.`, instructionsPrompt: `Provide comprehensive research on the user's prompt. -Use web_search to find current information. Repeat the web_search tool call until you have gathered all the relevant information. +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. -Then, write up a concise report that includes key findings for the user's prompt. -`.trim(), - - handleSteps: function* ({ agentState, prompt, params }) { - const { toolResult } = yield { - toolName: 'web_search' as const, - input: { query: prompt || '', depth: 'standard' as const }, - includeToolCall: false, - } satisfies ToolCall<'web_search'> - - const results = (toolResult - ?.filter((r) => r.type === 'json') - ?.map((r) => r.value)?.[0] ?? {}) as { - result: string | undefined - errorMessage: string | undefined - } +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. - yield { - type: 'STEP_TEXT', - text: results.result ?? results.errorMessage ?? '', - } - }, +Then, write up a concise answer that includes key findings for the user's prompt and cites source URLs when useful. +`.trim(), } export default definition diff --git a/agents/types/tools.ts b/agents/types/tools.ts index cb3882fc04..c3b627859e 100644 --- a/agents/types/tools.ts +++ b/agents/types/tools.ts @@ -398,7 +398,7 @@ export interface ThinkDeeplyParams { } /** - * Search the web for current information using Linkup API. + * Search the web for current information using Serper API. */ export interface WebSearchParams { /** The search query to find relevant web content */ diff --git a/cli/src/__tests__/test-utils.ts b/cli/src/__tests__/test-utils.ts index 704259fad9..be23aa1a4b 100644 --- a/cli/src/__tests__/test-utils.ts +++ b/cli/src/__tests__/test-utils.ts @@ -70,7 +70,7 @@ const TEST_SERVER_ENV_DEFAULTS: Record = { OPEN_ROUTER_API_KEY: 'test', OPENAI_API_KEY: 'test', ANTHROPIC_API_KEY: 'test', - LINKUP_API_KEY: 'test', + SERPER_API_KEY: 'test', GRAVITY_API_KEY: 'test', PORT: '4242', DATABASE_URL: 'postgres://user:pass@localhost:5432/db', diff --git a/common/src/templates/initial-agents-dir/types/tools.ts b/common/src/templates/initial-agents-dir/types/tools.ts index cb3882fc04..c3b627859e 100644 --- a/common/src/templates/initial-agents-dir/types/tools.ts +++ b/common/src/templates/initial-agents-dir/types/tools.ts @@ -398,7 +398,7 @@ export interface ThinkDeeplyParams { } /** - * Search the web for current information using Linkup API. + * Search the web for current information using Serper API. */ export interface WebSearchParams { /** The search query to find relevant web content */ diff --git a/common/src/tools/params/tool/web-search.ts b/common/src/tools/params/tool/web-search.ts index e87c8f2715..ba705295c0 100644 --- a/common/src/tools/params/tool/web-search.ts +++ b/common/src/tools/params/tool/web-search.ts @@ -20,9 +20,9 @@ const inputSchema = z `Search depth - 'standard' for quick results, 'deep' for more comprehensive search. Default is 'standard'.`, ), }) - .describe(`Search the web for current information using Linkup API.`) + .describe(`Search the web for current information using Serper API.`) const description = ` -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. +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. Use cases: - Finding current information about technologies, libraries, or frameworks @@ -31,7 +31,7 @@ Use cases: - Finding examples and tutorials - Checking current status of services or APIs -The tool will return search results with titles, URLs, and content snippets. +The tool will return JSON search results with titles, URLs, content snippets, and other available SERP fields such as answer boxes or related questions. Example: ${$getNativeToolCallExampleString({ diff --git a/packages/agent-runtime/src/__tests__/web-search-tool.test.ts b/packages/agent-runtime/src/__tests__/web-search-tool.test.ts index 51ec761ab1..69145b6561 100644 --- a/packages/agent-runtime/src/__tests__/web-search-tool.test.ts +++ b/packages/agent-runtime/src/__tests__/web-search-tool.test.ts @@ -5,7 +5,6 @@ import { getInitialSessionState } from '@codebuff/common/types/session-state' import { promptSuccess, success } from '@codebuff/common/util/error' import { afterEach, - beforeEach, describe, expect, @@ -243,7 +242,7 @@ describe('web_search tool with researcher agent (via web API facade)', () => { test('should handle API errors gracefully', async () => { spyOn(webApi, 'callWebSearchAPI').mockResolvedValue({ - error: 'Linkup API timeout', + error: 'Serper API timeout', }) mockAgentStream([ @@ -275,7 +274,7 @@ describe('web_search tool with researcher agent (via web API facade)', () => { expect(toolMsgs.length).toBeGreaterThan(0) const last = JSON.stringify(toolMsgs[toolMsgs.length - 1].content) expect(last).toContain('errorMessage') - expect(last).toContain('Linkup API timeout') + expect(last).toContain('Serper API timeout') }) test('should handle non-Error exceptions from facade', async () => { diff --git a/packages/agent-runtime/src/llm-api/__tests__/linkup-api.test.ts b/packages/agent-runtime/src/llm-api/__tests__/serper-api.test.ts similarity index 73% rename from packages/agent-runtime/src/llm-api/__tests__/linkup-api.test.ts rename to packages/agent-runtime/src/llm-api/__tests__/serper-api.test.ts index b5c933d962..7342e948d9 100644 --- a/packages/agent-runtime/src/llm-api/__tests__/linkup-api.test.ts +++ b/packages/agent-runtime/src/llm-api/__tests__/serper-api.test.ts @@ -14,18 +14,16 @@ import { test, } from 'bun:test' -import { searchWeb } from '../linkup-api' +import { searchWeb } from '../serper-api' import type { AgentRuntimeDeps } from '@codebuff/common/types/contracts/agent-runtime' -// Test server env for Linkup API -const testServerEnv = { LINKUP_API_KEY: 'test-api-key' } +const testServerEnv = { SERPER_API_KEY: 'test-api-key' } -describe('Linkup API', () => { +describe('Serper API', () => { let agentRuntimeImpl: AgentRuntimeDeps & { serverEnv: typeof testServerEnv } beforeAll(async () => { - // Mock withTimeout utility await mockModule('@codebuff/common/util/promise', () => ({ withTimeout: async (promise: Promise, timeout: number) => promise, })) @@ -48,14 +46,14 @@ describe('Linkup API', () => { test('should successfully search with basic query', async () => { const mockResponse = { - answer: - 'React is a JavaScript library for building user interfaces. You can learn how to build your first React application by following the official documentation.', - sources: [ + searchParameters: { q: 'React tutorial', type: 'search', num: 10 }, + organic: [ { - name: 'React Documentation', - url: 'https://react.dev', + title: 'React Documentation', + link: 'https://react.dev', snippet: 'React is a JavaScript library for building user interfaces.', + position: 1, }, ], } @@ -74,23 +72,18 @@ describe('Linkup API', () => { query: 'React tutorial', }) - expect(result).toBe( - 'React is a JavaScript library for building user interfaces. You can learn how to build your first React application by following the official documentation.', - ) - - // Verify fetch was called with correct parameters + expect(JSON.parse(result!)).toEqual(mockResponse) expect(agentRuntimeImpl.fetch).toHaveBeenCalledWith( - 'https://api.linkup.so/v1/search', + 'https://google.serper.dev/search', expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: 'Bearer test-api-key', + 'X-API-KEY': 'test-api-key', }, body: JSON.stringify({ q: 'React tutorial', - depth: 'standard', - outputType: 'sourcedAnswer', + num: 10, }), }), ) @@ -98,13 +91,13 @@ describe('Linkup API', () => { test('should handle custom depth', async () => { const mockResponse = { - answer: - 'Advanced React patterns include render props, higher-order components, and custom hooks for building reusable and maintainable components.', - sources: [ + searchParameters: { q: 'React patterns', type: 'search', num: 20 }, + organic: [ { - name: 'Advanced React Patterns', - url: 'https://example.com/advanced-react', + title: 'Advanced React Patterns', + link: 'https://example.com/advanced-react', snippet: 'Deep dive into React patterns and best practices.', + position: 1, }, ], } @@ -124,18 +117,13 @@ describe('Linkup API', () => { depth: 'deep', }) - expect(result).toBe( - 'Advanced React patterns include render props, higher-order components, and custom hooks for building reusable and maintainable components.', - ) - - // Verify fetch was called with correct parameters + expect(JSON.parse(result!)).toEqual(mockResponse) expect(agentRuntimeImpl.fetch).toHaveBeenCalledWith( - 'https://api.linkup.so/v1/search', + 'https://google.serper.dev/search', expect.objectContaining({ body: JSON.stringify({ q: 'React patterns', - depth: 'deep', - outputType: 'sourcedAnswer', + num: 20, }), }), ) @@ -169,7 +157,7 @@ describe('Linkup API', () => { test('should handle invalid response format', async () => { agentRuntimeImpl.fetch = mock(() => { return Promise.resolve( - new Response(JSON.stringify({ invalid: 'format' }), { + new Response(JSON.stringify(['invalid']), { status: 200, headers: { 'Content-Type': 'application/json' }, }), @@ -181,10 +169,21 @@ describe('Linkup API', () => { expect(result).toBeNull() }) - test('should handle missing answer field', async () => { + test('should return JSON search results without an answer field', async () => { + const mockResponse = { + organic: [ + { + title: 'Test result', + link: 'https://example.com', + snippet: 'Test snippet', + position: 1, + }, + ], + } + agentRuntimeImpl.fetch = mock(() => { return Promise.resolve( - new Response(JSON.stringify({ sources: [] }), { + new Response(JSON.stringify(mockResponse), { status: 200, headers: { 'Content-Type': 'application/json' }, }), @@ -196,12 +195,13 @@ describe('Linkup API', () => { query: 'test query', }) - expect(result).toBeNull() + expect(JSON.parse(result!)).toEqual(mockResponse) }) - test('should handle empty answer', async () => { + + test('should return sparse JSON search results', async () => { const mockResponse = { - answer: '', - sources: [], + searchParameters: { q: 'test query', type: 'search' }, + organic: [], } agentRuntimeImpl.fetch = mock(() => { @@ -215,14 +215,13 @@ describe('Linkup API', () => { const result = await searchWeb({ ...agentRuntimeImpl, query: 'test query' }) - expect(result).toBeNull() + expect(JSON.parse(result!)).toEqual(mockResponse) }) test('should use default options when none provided', async () => { const mockResponse = { - answer: 'Test answer content', - sources: [ - { name: 'Test', url: 'https://example.com', snippet: 'Test content' }, + organic: [ + { title: 'Test', link: 'https://example.com', snippet: 'Test content' }, ], } @@ -237,14 +236,12 @@ describe('Linkup API', () => { await searchWeb({ ...agentRuntimeImpl, query: 'test query' }) - // Verify fetch was called with default parameters expect(agentRuntimeImpl.fetch).toHaveBeenCalledWith( - 'https://api.linkup.so/v1/search', + 'https://google.serper.dev/search', expect.objectContaining({ body: JSON.stringify({ q: 'test query', - depth: 'standard', - outputType: 'sourcedAnswer', + num: 10, }), }), ) @@ -264,7 +261,6 @@ describe('Linkup API', () => { const result = await searchWeb({ ...agentRuntimeImpl, query: 'test query' }) expect(result).toBeNull() - // Verify that error logging was called expect(agentRuntimeImpl.logger.error).toHaveBeenCalled() }) @@ -287,13 +283,12 @@ describe('Linkup API', () => { }) expect(result).toBeNull() - // Verify that detailed error logging was called with 404 info expect(agentRuntimeImpl.logger.error).toHaveBeenCalledWith( expect.objectContaining({ status: 404, statusText: 'Not Found', responseBody: mockErrorResponse, - requestUrl: 'https://api.linkup.so/v1/search', + requestUrl: 'https://google.serper.dev/search', query: 'test query for 404', }), expect.stringContaining('404'), diff --git a/packages/agent-runtime/src/llm-api/linkup-api.ts b/packages/agent-runtime/src/llm-api/serper-api.ts similarity index 75% rename from packages/agent-runtime/src/llm-api/linkup-api.ts rename to packages/agent-runtime/src/llm-api/serper-api.ts index dd52206d5b..79d117f791 100644 --- a/packages/agent-runtime/src/llm-api/linkup-api.ts +++ b/packages/agent-runtime/src/llm-api/serper-api.ts @@ -2,22 +2,31 @@ import { withTimeout } from '@codebuff/common/util/promise' import type { Logger } from '@codebuff/common/types/contracts/logger' -export interface LinkupEnv { - LINKUP_API_KEY: string +export interface SerperEnv { + SERPER_API_KEY?: string } -const LINKUP_API_BASE_URL = 'https://api.linkup.so/v1' +const SERPER_API_BASE_URL = 'https://google.serper.dev' const FETCH_TIMEOUT_MS = 30_000 -export interface LinkupSearchResult { - name: string - snippet: string - url: string +export interface SerperOrganicResult { + title?: string + link?: string + snippet?: string + position?: number } -export interface LinkupSearchResponse { - answer: string - sources: LinkupSearchResult[] +export interface SerperSearchResponse { + searchParameters?: { + q?: string + type?: string + num?: number + } + knowledgeGraph?: unknown + answerBox?: unknown + organic?: SerperOrganicResult[] + peopleAlsoAsk?: unknown[] + relatedSearches?: unknown[] } const headersToRecord = (headers: Headers): Record => { @@ -33,21 +42,20 @@ export async function searchWeb(options: { depth?: 'standard' | 'deep' logger: Logger fetch: typeof globalThis.fetch - serverEnv: LinkupEnv + serverEnv: SerperEnv }): Promise { const { query, depth = 'standard', logger, fetch, serverEnv } = options const apiStartTime = Date.now() - if (!serverEnv.LINKUP_API_KEY) { - return 'No API key found. Please set LINKUP_API_KEY in your environment.' + if (!serverEnv.SERPER_API_KEY) { + return 'No API key found. Please set SERPER_API_KEY in your environment.' } const requestBody = { q: query, - depth, - outputType: 'sourcedAnswer' as const, + num: depth === 'deep' ? 20 : 10, } - const requestUrl = `${LINKUP_API_BASE_URL}/search` + const requestUrl = `${SERPER_API_BASE_URL}/search` const apiContext = { query, @@ -63,7 +71,7 @@ export async function searchWeb(options: { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: `Bearer ${serverEnv.LINKUP_API_KEY}`, + 'X-API-KEY': serverEnv.SERPER_API_KEY, }, body: JSON.stringify(requestBody), }), @@ -101,12 +109,12 @@ export async function searchWeb(options: { return null } - let data: LinkupSearchResponse + let data: SerperSearchResponse let parseDuration = 0 try { const parseStartTime = Date.now() const responseBody = await response.json() - data = responseBody as LinkupSearchResponse + data = responseBody as SerperSearchResponse parseDuration = Date.now() - parseStartTime } catch (jsonError) { logger.error( @@ -130,29 +138,29 @@ export async function searchWeb(options: { return null } - if (!data.answer || typeof data.answer !== 'string') { + if (!data || typeof data !== 'object' || Array.isArray(data)) { logger.error( { ...apiContext, responseKeys: Object.keys(data || {}), - answerType: typeof data?.answer, - answerLength: data?.answer?.length || 0, - sourcesCount: data?.sources?.length || 0, fetchDuration, parseDuration, totalDuration: Date.now() - apiStartTime, }, - 'Invalid response format - missing or invalid answer field', + 'Invalid response format from Serper', ) return null } + const result = JSON.stringify(data, null, 2) const totalDuration = Date.now() - apiStartTime logger.info( { ...apiContext, - answerLength: data.answer.length, - sourcesCount: data.sources?.length || 0, + resultLength: result.length, + organicCount: data.organic?.length || 0, + hasAnswerBox: Boolean(data.answerBox), + hasKnowledgeGraph: Boolean(data.knowledgeGraph), fetchDuration, parseDuration, totalDuration, @@ -161,8 +169,7 @@ export async function searchWeb(options: { 'Completed web search', ) - // Return the answer as a single result for compatibility - return data.answer + return result } catch (error) { const totalDuration = Date.now() - apiStartTime logger.error( diff --git a/packages/internal/src/env-schema.ts b/packages/internal/src/env-schema.ts index 6f5bda7fcf..54aa3a9b8c 100644 --- a/packages/internal/src/env-schema.ts +++ b/packages/internal/src/env-schema.ts @@ -12,7 +12,7 @@ export const serverEnvSchema = clientEnvSchema.extend({ DEEPSEEK_API_KEY: z.string().min(1).optional(), SILICONFLOW_API_KEY: z.string().min(1).optional(), OPENCODE_API_KEY: z.string().min(1).optional(), - LINKUP_API_KEY: z.string().min(1), + SERPER_API_KEY: z.string().min(1), CONTEXT7_API_KEY: z.string().optional(), GRAVITY_API_KEY: z.string().min(1), IPINFO_TOKEN: z.string().min(1), @@ -105,7 +105,7 @@ export const serverProcessEnv: ServerInput = { DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY, SILICONFLOW_API_KEY: process.env.SILICONFLOW_API_KEY, OPENCODE_API_KEY: process.env.OPENCODE_API_KEY, - LINKUP_API_KEY: process.env.LINKUP_API_KEY, + SERPER_API_KEY: process.env.SERPER_API_KEY, CONTEXT7_API_KEY: process.env.CONTEXT7_API_KEY, GRAVITY_API_KEY: process.env.GRAVITY_API_KEY, IPINFO_TOKEN: process.env.IPINFO_TOKEN, diff --git a/packages/internal/src/env.ts b/packages/internal/src/env.ts index c9e4a1279c..ca4bd25c34 100644 --- a/packages/internal/src/env.ts +++ b/packages/internal/src/env.ts @@ -21,7 +21,7 @@ if (isCI) { ensureEnvDefault('CANOPYWAVE_API_KEY', 'test') ensureEnvDefault('DEEPSEEK_API_KEY', 'test') ensureEnvDefault('OPENCODE_API_KEY', 'test') - ensureEnvDefault('LINKUP_API_KEY', 'test') + ensureEnvDefault('SERPER_API_KEY', 'test') ensureEnvDefault('GRAVITY_API_KEY', 'test') ensureEnvDefault('IPINFO_TOKEN', 'test') ensureEnvDefault('SPUR_TOKEN', 'test') diff --git a/sdk/src/__tests__/researcher-web.integration.test.ts b/sdk/src/__tests__/researcher-web.integration.test.ts new file mode 100644 index 0000000000..d35498bec4 --- /dev/null +++ b/sdk/src/__tests__/researcher-web.integration.test.ts @@ -0,0 +1,129 @@ +import { existsSync, readFileSync } from 'fs' +import { homedir } from 'os' +import path from 'path' + +import { describe, expect, it } from 'bun:test' + +import { CodebuffClient } from '../client' +import { loadLocalAgents } from '../agents/load-agents' + +import type { AgentOutput } from '@codebuff/common/types/session-state' +import type { PrintModeEvent } from '@codebuff/common/types/print-mode' + +const DEFAULT_TIMEOUT_MS = 120_000 +const EXPECTED_KEYWORD = 'useActionState' + +function loadEnvValue(name: string): string | undefined { + if (process.env[name] && process.env[name] !== 'test') { + return process.env[name] + } + + for (const envPath of [ + path.join(homedir(), 'codebuff', '.env.local'), + path.join(process.cwd(), '.env.local'), + ]) { + if (!existsSync(envPath)) continue + + const contents = readFileSync(envPath, 'utf8') + const match = contents.match(new RegExp(`^${name}=(.*)$`, 'm')) + const value = match?.[1]?.trim().replace(/^['"]|['"]$/g, '') + if (value && value !== 'test') return value + } + + return undefined +} + +function extractOutputText(output: AgentOutput): string { + if (output.type === 'error') return output.message + if (output.type === 'structuredOutput') { + return JSON.stringify(output.value ?? {}) + } + + const assistantText = output.value.flatMap((message) => { + if ((message as { role?: unknown }).role !== 'assistant') return [] + + const content = (message as { content?: unknown }).content + if (typeof content === 'string') return [content] + if (!Array.isArray(content)) return [] + + return content.flatMap((part) => { + if ( + part && + typeof part === 'object' && + 'type' in part && + part.type === 'text' && + 'text' in part + ) { + return [String(part.text)] + } + return [] + }) + }) + + return assistantText.join('\n') +} + +describe('researcher-web SDK integration', () => { + it( + `runs researcher-web through the SDK and answers with ${EXPECTED_KEYWORD}`, + async () => { + const apiKey = loadEnvValue('CODEBUFF_API_KEY') + if (!apiKey) { + console.log( + 'Skipping researcher-web SDK integration test: set CODEBUFF_API_KEY to run.', + ) + return + } + + const agentsPath = path.resolve( + import.meta.dir, + '../../../agents/researcher', + ) + const loadedAgents = await loadLocalAgents({ agentsPath }) + const researcherWeb = loadedAgents['researcher-web'] + expect(researcherWeb).toBeDefined() + + const events: PrintModeEvent[] = [] + const client = new CodebuffClient({ + apiKey, + cwd: process.cwd(), + }) + + const result = await client.run({ + agent: 'researcher-web', + agentDefinitions: [researcherWeb], + maxAgentSteps: 8, + handleEvent: (event) => { + events.push(event) + }, + prompt: [ + 'Use web search to answer this React docs question.', + 'After searching, fetch the most relevant React docs page with run_terminal_command before answering.', + 'In React 19, which hook returns state, a form action, and an isPending value for form actions?', + 'Answer with the exact hook name and one short sentence.', + ].join(' '), + }) + + const outputText = extractOutputText(result.output) + console.log('researcher-web SDK output:', outputText) + + expect(result.output.type).not.toBe('error') + expect(outputText).toContain(EXPECTED_KEYWORD) + expect(events.some((event) => event.type === 'tool_call')).toBe(true) + expect( + events.some( + (event) => + event.type === 'tool_call' && event.toolName === 'web_search', + ), + ).toBe(true) + expect( + events.some( + (event) => + event.type === 'tool_call' && + event.toolName === 'run_terminal_command', + ), + ).toBe(true) + }, + DEFAULT_TIMEOUT_MS, + ) +}) diff --git a/sdk/test/setup-env.ts b/sdk/test/setup-env.ts index 45b4fa8148..381bb09691 100644 --- a/sdk/test/setup-env.ts +++ b/sdk/test/setup-env.ts @@ -18,7 +18,7 @@ const testDefaults: Record = { const serverDefaults: Record = { OPEN_ROUTER_API_KEY: 'test', OPENAI_API_KEY: 'test', - LINKUP_API_KEY: 'test', + SERPER_API_KEY: 'test', PORT: '4242', DATABASE_URL: 'postgres://user:pass@localhost:5432/db', CODEBUFF_GITHUB_ID: 'test-id', diff --git a/web/src/__tests__/playwright-runner.e2e.ts b/web/src/__tests__/playwright-runner.e2e.ts index 28686d50bd..a107424668 100644 --- a/web/src/__tests__/playwright-runner.e2e.ts +++ b/web/src/__tests__/playwright-runner.e2e.ts @@ -22,7 +22,7 @@ describe('playwright e2e suite', () => { env.NEXT_PUBLIC_WEB_PORT ||= '3000' env.OPEN_ROUTER_API_KEY ||= 'test' env.OPENAI_API_KEY ||= 'test' - env.LINKUP_API_KEY ||= 'test' + env.SERPER_API_KEY ||= 'test' env.PORT = env.NEXT_PUBLIC_WEB_PORT env.DATABASE_URL = getE2EDatabaseUrl() env.CODEBUFF_GITHUB_ID ||= 'test-id' diff --git a/web/src/app/api/v1/web-search/__tests__/web-search.test.ts b/web/src/app/api/v1/web-search/__tests__/web-search.test.ts index 6be2f09b81..c5971737e1 100644 --- a/web/src/app/api/v1/web-search/__tests__/web-search.test.ts +++ b/web/src/app/api/v1/web-search/__tests__/web-search.test.ts @@ -15,7 +15,7 @@ import type { } from '@codebuff/common/types/contracts/logger' import type { BlockGrantResult } from '@codebuff/billing/subscription' -const testServerEnv = { LINKUP_API_KEY: 'test-linkup-key' } +const testServerEnv = { SERPER_API_KEY: 'test-serper-key' } describe('/api/v1/web-search POST endpoint', () => { let mockLogger: Logger @@ -55,13 +55,25 @@ describe('/api/v1/web-search POST endpoint', () => { value: { chargedToOrganization: false }, })) as ConsumeCreditsWithFallbackFn - // Mock fetch to return Linkup-like response + // Mock fetch to return Serper-like response mockFetch = Object.assign( async () => - new Response(JSON.stringify({ answer: 'result', sources: [] }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }), + new Response( + JSON.stringify({ + organic: [ + { + title: 'Result', + link: 'https://example.com', + snippet: 'result', + position: 1, + }, + ], + }), + { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }, + ), { preconnect: () => {} }, ) as typeof fetch }) diff --git a/web/src/app/api/v1/web-search/_post.ts b/web/src/app/api/v1/web-search/_post.ts index fa276d0c9e..f5d1b07852 100644 --- a/web/src/app/api/v1/web-search/_post.ts +++ b/web/src/app/api/v1/web-search/_post.ts @@ -1,4 +1,4 @@ -import { searchWeb } from '@codebuff/agent-runtime/llm-api/linkup-api' +import { searchWeb } from '@codebuff/agent-runtime/llm-api/serper-api' import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events' import { sleep } from '@codebuff/common/util/promise' import { NextResponse } from 'next/server' @@ -10,7 +10,7 @@ import { requireUserFromApiKey, } from '../_helpers' -import type { LinkupEnv } from '@codebuff/agent-runtime/llm-api/linkup-api' +import type { SerperEnv } from '@codebuff/agent-runtime/llm-api/serper-api' import type { TrackEventFn } from '@codebuff/common/types/contracts/analytics' import type { GetUserUsageDataFn, @@ -39,7 +39,7 @@ export async function postWebSearch(params: { getUserUsageData: GetUserUsageDataFn consumeCreditsWithFallback: ConsumeCreditsWithFallbackFn fetch: typeof globalThis.fetch - serverEnv: LinkupEnv + serverEnv: SerperEnv ensureSubscriberBlockGrant?: (params: { userId: string logger: Logger diff --git a/web/src/app/api/v1/web-search/route.ts b/web/src/app/api/v1/web-search/route.ts index 8e274e6e82..5beef29246 100644 --- a/web/src/app/api/v1/web-search/route.ts +++ b/web/src/app/api/v1/web-search/route.ts @@ -21,7 +21,7 @@ export async function POST(req: NextRequest) { getUserUsageData, consumeCreditsWithFallback, fetch, - serverEnv: { LINKUP_API_KEY: env.LINKUP_API_KEY }, + serverEnv: { SERPER_API_KEY: env.SERPER_API_KEY }, ensureSubscriberBlockGrant, }) }