diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..3ae0c73af5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,124 @@ +# Normalize line endings for all text files +* text=auto + +# Source code +*.py text diff=python +*.js text +*.ts text +*.jsx text +*.tsx text +*.json text +*.yaml text +*.yml text +*.toml text +*.ini text +*.cfg text + +# Shell scripts (must use LF) +*.sh text eol=lf +quickstart.sh text eol=lf + +# PowerShell scripts (Windows-friendly) +*.ps1 text eol=lf +*.psm1 text eol=lf + +# Windows batch files (must use CRLF) +*.bat text eol=crlf +*.cmd text eol=crlf + +# Documentation +*.md text +*.txt text +*.rst text +*.tex text + +# Configuration files +.gitignore text +.gitattributes text +.editorconfig text +Dockerfile text +docker-compose.yml text +requirements*.txt text +pyproject.toml text +setup.py text +setup.cfg text +MANIFEST.in text +LICENSE text +README* text +CHANGELOG* text +CONTRIBUTING* text +CODE_OF_CONDUCT* text + +# Web files +*.html text +*.css text +*.scss text +*.sass text + +# Data files +*.xml text +*.csv text +*.sql text + +# Graphics (binary) +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.svg text +*.eps binary +*.bmp binary +*.tif binary +*.tiff binary + +# Archives (binary) +*.zip binary +*.tar binary +*.gz binary +*.bz2 binary +*.7z binary +*.rar binary + +# Python compiled (binary) +*.pyc binary +*.pyo binary +*.pyd binary +*.whl binary +*.egg binary + +# System libraries (binary) +*.so binary +*.dll binary +*.dylib binary +*.lib binary +*.a binary + +# Documents (binary) +*.pdf binary +*.doc binary +*.docx binary +*.ppt binary +*.pptx binary +*.xls binary +*.xlsx binary + +# Fonts (binary) +*.ttf binary +*.otf binary +*.woff binary +*.woff2 binary +*.eot binary + +# Audio/Video (binary) +*.mp3 binary +*.mp4 binary +*.wav binary +*.avi binary +*.mov binary +*.flv binary + +# Database files (binary) +*.db binary +*.sqlite binary +*.sqlite3 binary diff --git a/src/tools/WebSearchTool/providers/brave.test.ts b/src/tools/WebSearchTool/providers/brave.test.ts index a57f2014f1..45a93039ba 100644 --- a/src/tools/WebSearchTool/providers/brave.test.ts +++ b/src/tools/WebSearchTool/providers/brave.test.ts @@ -5,11 +5,11 @@ import { } from '../../../test/sharedMutationLock.js' import { braveProvider } from './brave.ts' - -const originalEnv = { - BRAVE_API_KEY: process.env.BRAVE_API_KEY, -} - + +const originalEnv = { + BRAVE_API_KEY: process.env.BRAVE_API_KEY, +} + const originalFetch = globalThis.fetch beforeEach(async () => { @@ -27,87 +27,87 @@ afterEach(() => { releaseSharedMutationLock() } }) - -describe('braveProvider isConfigured', () => { - test('true when BRAVE_API_KEY is set', () => { - process.env.BRAVE_API_KEY = 'brv-test-key' - expect(braveProvider.isConfigured()).toBe(true) - }) - - test('false when BRAVE_API_KEY is missing', () => { - delete process.env.BRAVE_API_KEY - expect(braveProvider.isConfigured()).toBe(false) - }) -}) - -describe('braveProvider search', () => { - beforeEach(() => { - process.env.BRAVE_API_KEY = 'brv-test-key' - }) - - test('sends bare token in X-Subscription-Token (no Bearer prefix)', async () => { - let capturedHeaders: Record = {} - let capturedUrl = '' - globalThis.fetch = (async (input: any, init: any) => { - capturedUrl = typeof input === 'string' ? input : input.toString() - capturedHeaders = (init?.headers ?? {}) as Record - return new Response(JSON.stringify({ web: { results: [] } }), { status: 200 }) - }) as typeof fetch - - await braveProvider.search({ query: 'hello' }) - - expect(capturedHeaders['X-Subscription-Token']).toBe('brv-test-key') - expect(capturedUrl).toContain('https://api.search.brave.com/res/v1/web/search') - expect(capturedUrl).toContain('q=hello') - }) - - test('maps web.results into SearchHit shape', async () => { - globalThis.fetch = (async (_input: any, _init: any) => new Response(JSON.stringify({ - web: { - results: [ - { title: 'Example', url: 'https://example.com/a', description: 'snippet a' }, - { title: 'Other', url: 'https://other.com/b', description: 'snippet b' }, - ], - }, - }), { status: 200 })) as typeof fetch - - const out = await braveProvider.search({ query: 'hello' }) - - expect(out.providerName).toBe('brave') - expect(out.hits).toHaveLength(2) - expect(out.hits[0]).toEqual({ - title: 'Example', - url: 'https://example.com/a', - description: 'snippet a', - source: 'example.com', - }) - }) - - test('applies blocked_domains client-side', async () => { - globalThis.fetch = (async (_input: any, _init: any) => new Response(JSON.stringify({ - web: { - results: [ - { title: 'Keep', url: 'https://keep.com/a', description: 'k' }, - { title: 'Drop', url: 'https://drop.com/b', description: 'd' }, - ], - }, - }), { status: 200 })) as typeof fetch - - const out = await braveProvider.search({ query: 'q', blocked_domains: ['drop.com'] }) - expect(out.hits).toHaveLength(1) - expect(out.hits[0].url).toBe('https://keep.com/a') - }) - - test('throws on non-2xx response with status code', async () => { - globalThis.fetch = (async (_input: any, _init: any) => - new Response('rate limited', { status: 429 })) as typeof fetch - await expect(braveProvider.search({ query: 'q' })).rejects.toThrow(/429/) - }) - - test('returns empty hits when web.results is missing', async () => { - globalThis.fetch = (async (_input: any, _init: any) => - new Response(JSON.stringify({}), { status: 200 })) as typeof fetch - const out = await braveProvider.search({ query: 'q' }) - expect(out.hits).toHaveLength(0) - }) -}) + +describe('braveProvider isConfigured', () => { + test('true when BRAVE_API_KEY is set', () => { + process.env.BRAVE_API_KEY = 'brv-test-key' + expect(braveProvider.isConfigured()).toBe(true) + }) + + test('false when BRAVE_API_KEY is missing', () => { + delete process.env.BRAVE_API_KEY + expect(braveProvider.isConfigured()).toBe(false) + }) +}) + +describe('braveProvider search', () => { + beforeEach(() => { + process.env.BRAVE_API_KEY = 'brv-test-key' + }) + + test('sends bare token in X-Subscription-Token (no Bearer prefix)', async () => { + let capturedHeaders: Record = {} + let capturedUrl = '' + globalThis.fetch = (async (input: any, init: any) => { + capturedUrl = typeof input === 'string' ? input : input.toString() + capturedHeaders = (init?.headers ?? {}) as Record + return new Response(JSON.stringify({ web: { results: [] } }), { status: 200 }) + }) as typeof fetch + + await braveProvider.search({ query: 'hello' }) + + expect(capturedHeaders['X-Subscription-Token']).toBe('brv-test-key') + expect(capturedUrl).toContain('https://api.search.brave.com/res/v1/web/search') + expect(capturedUrl).toContain('q=hello') + }) + + test('maps web.results into SearchHit shape', async () => { + globalThis.fetch = (async (_input: any, _init: any) => new Response(JSON.stringify({ + web: { + results: [ + { title: 'Example', url: 'https://example.com/a', description: 'snippet a' }, + { title: 'Other', url: 'https://other.com/b', description: 'snippet b' }, + ], + }, + }), { status: 200 })) as typeof fetch + + const out = await braveProvider.search({ query: 'hello' }) + + expect(out.providerName).toBe('brave') + expect(out.hits).toHaveLength(2) + expect(out.hits[0]).toEqual({ + title: 'Example', + url: 'https://example.com/a', + description: 'snippet a', + source: 'example.com', + }) + }) + + test('applies blocked_domains client-side', async () => { + globalThis.fetch = (async (_input: any, _init: any) => new Response(JSON.stringify({ + web: { + results: [ + { title: 'Keep', url: 'https://keep.com/a', description: 'k' }, + { title: 'Drop', url: 'https://drop.com/b', description: 'd' }, + ], + }, + }), { status: 200 })) as typeof fetch + + const out = await braveProvider.search({ query: 'q', blocked_domains: ['drop.com'] }) + expect(out.hits).toHaveLength(1) + expect(out.hits[0].url).toBe('https://keep.com/a') + }) + + test('throws on non-2xx response with status code', async () => { + globalThis.fetch = (async (_input: any, _init: any) => + new Response('rate limited', { status: 429 })) as typeof fetch + await expect(braveProvider.search({ query: 'q' })).rejects.toThrow(/429/) + }) + + test('returns empty hits when web.results is missing', async () => { + globalThis.fetch = (async (_input: any, _init: any) => + new Response(JSON.stringify({}), { status: 200 })) as typeof fetch + const out = await braveProvider.search({ query: 'q' }) + expect(out.hits).toHaveLength(0) + }) +}) diff --git a/src/tools/WebSearchTool/providers/brave.ts b/src/tools/WebSearchTool/providers/brave.ts index 2d8d5f3a90..cc0a2a903c 100644 --- a/src/tools/WebSearchTool/providers/brave.ts +++ b/src/tools/WebSearchTool/providers/brave.ts @@ -1,53 +1,53 @@ -/** - * Brave Search API adapter. - * GET https://api.search.brave.com/res/v1/web/search?q=... - * Auth: X-Subscription-Token: (bare token — no "Bearer" prefix) - * - * Brave runs an independent web index (~30B pages) — useful as a non-Google, - * non-Bing fallback in the auto chain. - */ - -import type { SearchInput, SearchProvider } from './types.js' -import { applyDomainFilters, safeHostname, type ProviderOutput } from './types.js' - -export const braveProvider: SearchProvider = { - name: 'brave', - - isConfigured() { - return Boolean(process.env.BRAVE_API_KEY) - }, - - async search(input: SearchInput, signal?: AbortSignal): Promise { - const start = performance.now() - - const url = new URL('https://api.search.brave.com/res/v1/web/search') - url.searchParams.set('q', input.query) - url.searchParams.set('count', '15') - - const res = await fetch(url.toString(), { - headers: { - 'X-Subscription-Token': process.env.BRAVE_API_KEY!, - Accept: 'application/json', - }, - signal, - }) - - if (!res.ok) { - throw new Error(`Brave search error ${res.status}: ${await res.text().catch(() => '')}`) - } - - const data = await res.json() - const hits = (data.web?.results ?? []).map((r: any) => ({ - title: r.title ?? '', - url: r.url ?? '', - description: r.description, - source: r.url ? safeHostname(r.url) : undefined, - })) - - return { - hits: applyDomainFilters(hits, input), - providerName: 'brave', - durationSeconds: (performance.now() - start) / 1000, - } - }, -} +/** + * Brave Search API adapter. + * GET https://api.search.brave.com/res/v1/web/search?q=... + * Auth: X-Subscription-Token: (bare token — no "Bearer" prefix) + * + * Brave runs an independent web index (~30B pages) — useful as a non-Google, + * non-Bing fallback in the auto chain. + */ + +import type { SearchInput, SearchProvider } from './types.js' +import { applyDomainFilters, safeHostname, type ProviderOutput } from './types.js' + +export const braveProvider: SearchProvider = { + name: 'brave', + + isConfigured() { + return Boolean(process.env.BRAVE_API_KEY) + }, + + async search(input: SearchInput, signal?: AbortSignal): Promise { + const start = performance.now() + + const url = new URL('https://api.search.brave.com/res/v1/web/search') + url.searchParams.set('q', input.query) + url.searchParams.set('count', '15') + + const res = await fetch(url.toString(), { + headers: { + 'X-Subscription-Token': process.env.BRAVE_API_KEY!, + Accept: 'application/json', + }, + signal, + }) + + if (!res.ok) { + throw new Error(`Brave search error ${res.status}: ${await res.text().catch(() => '')}`) + } + + const data = await res.json() + const hits = (data.web?.results ?? []).map((r: any) => ({ + title: r.title ?? '', + url: r.url ?? '', + description: r.description, + source: r.url ? safeHostname(r.url) : undefined, + })) + + return { + hits: applyDomainFilters(hits, input), + providerName: 'brave', + durationSeconds: (performance.now() - start) / 1000, + } + }, +} diff --git a/src/tools/WebSearchTool/providers/exa.test.ts b/src/tools/WebSearchTool/providers/exa.test.ts index 050c129808..ae80066908 100644 --- a/src/tools/WebSearchTool/providers/exa.test.ts +++ b/src/tools/WebSearchTool/providers/exa.test.ts @@ -5,11 +5,11 @@ import { } from '../../../test/sharedMutationLock.js' import { exaProvider } from './exa.ts' - -const originalEnv = { - EXA_API_KEY: process.env.EXA_API_KEY, -} - + +const originalEnv = { + EXA_API_KEY: process.env.EXA_API_KEY, +} + const originalFetch = globalThis.fetch beforeEach(async () => { @@ -27,128 +27,128 @@ afterEach(() => { releaseSharedMutationLock() } }) - -describe('exaProvider isConfigured', () => { - test('true when EXA_API_KEY is set', () => { - process.env.EXA_API_KEY = 'exa-test-key' - expect(exaProvider.isConfigured()).toBe(true) - }) - - test('false when EXA_API_KEY is missing', () => { - delete process.env.EXA_API_KEY - expect(exaProvider.isConfigured()).toBe(false) - }) -}) - -describe('exaProvider search request shape', () => { - beforeEach(() => { - process.env.EXA_API_KEY = 'exa-test-key' - }) - - test('requests contents.highlights so descriptions are populated', async () => { - let capturedBody: any = null - let capturedHeaders: Record = {} - globalThis.fetch = (async (_input: any, init: any) => { - capturedHeaders = (init?.headers ?? {}) as Record - capturedBody = init?.body ? JSON.parse(init.body as string) : null - return new Response(JSON.stringify({ results: [] }), { status: 200 }) - }) as typeof fetch - - await exaProvider.search({ query: 'gpus' }) - - expect(capturedHeaders['x-api-key']).toBe('exa-test-key') - expect(capturedBody).toMatchObject({ - query: 'gpus', - type: 'auto', - numResults: 15, - contents: { highlights: true }, - }) - }) - - test('forwards allowed_domains/blocked_domains as includeDomains/excludeDomains', async () => { - let capturedBody: any = null - globalThis.fetch = (async (_input: any, init: any) => { - capturedBody = JSON.parse(init.body as string) - return new Response(JSON.stringify({ results: [] }), { status: 200 }) - }) as typeof fetch - - await exaProvider.search({ - query: 'q', - allowed_domains: ['arxiv.org'], - blocked_domains: ['pinterest.com'], - }) - - expect(capturedBody.includeDomains).toEqual(['arxiv.org']) - expect(capturedBody.excludeDomains).toEqual(['pinterest.com']) - }) -}) - -describe('exaProvider response mapping', () => { - beforeEach(() => { - process.env.EXA_API_KEY = 'exa-test-key' - }) - - test('maps highlights[] into description (joined with ellipsis)', async () => { - globalThis.fetch = (async (_input: any, _init: any) => new Response(JSON.stringify({ - results: [{ - title: 'Nvidia post-Blackwell roadmap', - url: 'https://example.com/nv', - highlights: [ - 'Nvidia announced its next-gen GPU.', - 'Performance gains of ~2x over the prior generation.', - 'Shipping in Q4.', - ], - highlightScores: [0.91, 0.84, 0.71], - }], - }), { status: 200 })) as typeof fetch - - const out = await exaProvider.search({ query: 'q' }) - - expect(out.hits).toHaveLength(1) - expect(out.hits[0].title).toBe('Nvidia post-Blackwell roadmap') - expect(out.hits[0].url).toBe('https://example.com/nv') - expect(out.hits[0].source).toBe('example.com') - expect(out.hits[0].description).toBe( - 'Nvidia announced its next-gen GPU. … Performance gains of ~2x over the prior generation. … Shipping in Q4.', - ) - }) - - test('caps the joined description at 3 highlights', async () => { - globalThis.fetch = (async (_input: any, _init: any) => new Response(JSON.stringify({ - results: [{ - title: 't', url: 'https://e.com/x', - highlights: ['a', 'b', 'c', 'd', 'e'], - }], - }), { status: 200 })) as typeof fetch - - const out = await exaProvider.search({ query: 'q' }) - expect(out.hits[0].description).toBe('a … b … c') - }) - - test('falls back to text when highlights is empty/missing', async () => { - globalThis.fetch = (async (_input: any, _init: any) => new Response(JSON.stringify({ - results: [{ - title: 't', url: 'https://e.com/x', - text: 'Full page body content.', - }], - }), { status: 200 })) as typeof fetch - - const out = await exaProvider.search({ query: 'q' }) - expect(out.hits[0].description).toBe('Full page body content.') - }) - - test('description is undefined when neither highlights nor text is present', async () => { - globalThis.fetch = (async (_input: any, _init: any) => new Response(JSON.stringify({ - results: [{ title: 't', url: 'https://e.com/x' }], - }), { status: 200 })) as typeof fetch - - const out = await exaProvider.search({ query: 'q' }) - expect(out.hits[0].description).toBeUndefined() - }) - - test('throws on non-2xx response with status code', async () => { - globalThis.fetch = (async (_input: any, _init: any) => - new Response('quota exceeded', { status: 402 })) as typeof fetch - await expect(exaProvider.search({ query: 'q' })).rejects.toThrow(/402/) - }) -}) + +describe('exaProvider isConfigured', () => { + test('true when EXA_API_KEY is set', () => { + process.env.EXA_API_KEY = 'exa-test-key' + expect(exaProvider.isConfigured()).toBe(true) + }) + + test('false when EXA_API_KEY is missing', () => { + delete process.env.EXA_API_KEY + expect(exaProvider.isConfigured()).toBe(false) + }) +}) + +describe('exaProvider search request shape', () => { + beforeEach(() => { + process.env.EXA_API_KEY = 'exa-test-key' + }) + + test('requests contents.highlights so descriptions are populated', async () => { + let capturedBody: any = null + let capturedHeaders: Record = {} + globalThis.fetch = (async (_input: any, init: any) => { + capturedHeaders = (init?.headers ?? {}) as Record + capturedBody = init?.body ? JSON.parse(init.body as string) : null + return new Response(JSON.stringify({ results: [] }), { status: 200 }) + }) as typeof fetch + + await exaProvider.search({ query: 'gpus' }) + + expect(capturedHeaders['x-api-key']).toBe('exa-test-key') + expect(capturedBody).toMatchObject({ + query: 'gpus', + type: 'auto', + numResults: 15, + contents: { highlights: true }, + }) + }) + + test('forwards allowed_domains/blocked_domains as includeDomains/excludeDomains', async () => { + let capturedBody: any = null + globalThis.fetch = (async (_input: any, init: any) => { + capturedBody = JSON.parse(init.body as string) + return new Response(JSON.stringify({ results: [] }), { status: 200 }) + }) as typeof fetch + + await exaProvider.search({ + query: 'q', + allowed_domains: ['arxiv.org'], + blocked_domains: ['pinterest.com'], + }) + + expect(capturedBody.includeDomains).toEqual(['arxiv.org']) + expect(capturedBody.excludeDomains).toEqual(['pinterest.com']) + }) +}) + +describe('exaProvider response mapping', () => { + beforeEach(() => { + process.env.EXA_API_KEY = 'exa-test-key' + }) + + test('maps highlights[] into description (joined with ellipsis)', async () => { + globalThis.fetch = (async (_input: any, _init: any) => new Response(JSON.stringify({ + results: [{ + title: 'Nvidia post-Blackwell roadmap', + url: 'https://example.com/nv', + highlights: [ + 'Nvidia announced its next-gen GPU.', + 'Performance gains of ~2x over the prior generation.', + 'Shipping in Q4.', + ], + highlightScores: [0.91, 0.84, 0.71], + }], + }), { status: 200 })) as typeof fetch + + const out = await exaProvider.search({ query: 'q' }) + + expect(out.hits).toHaveLength(1) + expect(out.hits[0].title).toBe('Nvidia post-Blackwell roadmap') + expect(out.hits[0].url).toBe('https://example.com/nv') + expect(out.hits[0].source).toBe('example.com') + expect(out.hits[0].description).toBe( + 'Nvidia announced its next-gen GPU. … Performance gains of ~2x over the prior generation. … Shipping in Q4.', + ) + }) + + test('caps the joined description at 3 highlights', async () => { + globalThis.fetch = (async (_input: any, _init: any) => new Response(JSON.stringify({ + results: [{ + title: 't', url: 'https://e.com/x', + highlights: ['a', 'b', 'c', 'd', 'e'], + }], + }), { status: 200 })) as typeof fetch + + const out = await exaProvider.search({ query: 'q' }) + expect(out.hits[0].description).toBe('a … b … c') + }) + + test('falls back to text when highlights is empty/missing', async () => { + globalThis.fetch = (async (_input: any, _init: any) => new Response(JSON.stringify({ + results: [{ + title: 't', url: 'https://e.com/x', + text: 'Full page body content.', + }], + }), { status: 200 })) as typeof fetch + + const out = await exaProvider.search({ query: 'q' }) + expect(out.hits[0].description).toBe('Full page body content.') + }) + + test('description is undefined when neither highlights nor text is present', async () => { + globalThis.fetch = (async (_input: any, _init: any) => new Response(JSON.stringify({ + results: [{ title: 't', url: 'https://e.com/x' }], + }), { status: 200 })) as typeof fetch + + const out = await exaProvider.search({ query: 'q' }) + expect(out.hits[0].description).toBeUndefined() + }) + + test('throws on non-2xx response with status code', async () => { + globalThis.fetch = (async (_input: any, _init: any) => + new Response('quota exceeded', { status: 402 })) as typeof fetch + await expect(exaProvider.search({ query: 'q' })).rejects.toThrow(/402/) + }) +})