refactor: prune unused web seam fields
This commit is contained in:
@@ -8,8 +8,8 @@ This is an **implementation** package: it registers a provider into `ctx.web`, i
|
||||
|
||||
| Key | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `apiKey` | `$EXA_API_KEY` | Exa API key. Empty/absent → provider `status()` reports `missing-credential` (the seam reports `configured-unavailable`/`none`). |
|
||||
| `baseURL` | `https://api.exa.ai` | Endpoint base; `/search` is appended. An unparseable value makes `status()` report `misconfigured`. |
|
||||
| `apiKey` | `$EXA_API_KEY` | Exa API key. Empty/absent makes the provider unavailable. |
|
||||
| `baseURL` | `https://api.exa.ai` | Endpoint base; `/search` is appended. An unparseable value makes the provider unavailable. |
|
||||
| `searchType` | `auto` | Retrieval mode sent as Exa's `type`: `auto` (Exa decides), `keyword`, or `neural`. |
|
||||
| `numResults` | (unset) | Default result count when a request carries no `maxResults`. Unset sends no default. Must be a positive integer. |
|
||||
| `highlightsPerResult` | `1` | Highlight sentences requested per result (Exa's `highlightsPerUrl`). Must be a positive integer. |
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
|
||||
import { WebError } from '@deepseek-ai/dsh-web'
|
||||
import type {
|
||||
WebProviderStatus,
|
||||
WebSearchProvider,
|
||||
WebSearchRequest,
|
||||
WebSearchResult,
|
||||
@@ -39,7 +38,7 @@ const USER_AGENT = 'deepseek-harness/0.0.1'
|
||||
|
||||
/** Resolved provider options (the plugin's `apply` supplies env-var and constant defaults). */
|
||||
export interface ExaSearchProviderOptions {
|
||||
/** Exa API key. Empty/absent → `status()` reports `missing-credential`. */
|
||||
/** Exa API key. Empty/absent makes the provider unavailable. */
|
||||
apiKey: string
|
||||
/** Endpoint base; `/search` is appended. */
|
||||
baseURL: string
|
||||
@@ -74,18 +73,17 @@ export function mapExaResult(result: ExaResult): WebSearchSource | undefined {
|
||||
/**
|
||||
* Map an Exa response envelope to a normalized search result.
|
||||
*
|
||||
* @param query - the original request query, echoed on the result.
|
||||
* @param response - the parsed `POST /search` response body.
|
||||
* @returns the normalized result; snippet-less entries are dropped
|
||||
* ({@link mapExaResult}).
|
||||
*/
|
||||
export function mapExaResponse(query: string, response: ExaSearchResponse): WebSearchResult {
|
||||
export function mapExaResponse(response: ExaSearchResponse): WebSearchResult {
|
||||
const sources = (response.results ?? [])
|
||||
.map(mapExaResult)
|
||||
.filter((source): source is WebSearchSource => source !== undefined)
|
||||
// Exa returns no generated answer, so `content` is omitted. The seam owns the
|
||||
// final `maxResults` truncation, so this provider reports `truncated: false`.
|
||||
return { providerId: EXA_PROVIDER_ID, query, sources, truncated: false }
|
||||
return { sources, truncated: false }
|
||||
}
|
||||
|
||||
/** The Exa-backed search provider. */
|
||||
@@ -94,15 +92,14 @@ export class ExaSearchProvider implements WebSearchProvider {
|
||||
|
||||
constructor(private readonly options: ExaSearchProviderOptions) {}
|
||||
|
||||
status(): WebProviderStatus {
|
||||
if (this.options.apiKey.length === 0) return { available: false, reason: 'missing-credential' }
|
||||
if (!isValidBaseUrl(this.options.baseURL)) return { available: false, reason: 'misconfigured' }
|
||||
if (!isPositiveInteger(this.options.highlightsPerResult)) return { available: false, reason: 'misconfigured' }
|
||||
if (this.options.numResults !== undefined && !isPositiveInteger(this.options.numResults)) return { available: false, reason: 'misconfigured' }
|
||||
return { available: true }
|
||||
available(): boolean {
|
||||
return this.options.apiKey.length > 0
|
||||
&& isValidBaseUrl(this.options.baseURL)
|
||||
&& isPositiveInteger(this.options.highlightsPerResult)
|
||||
&& (this.options.numResults === undefined || isPositiveInteger(this.options.numResults))
|
||||
}
|
||||
|
||||
async search(request: WebSearchRequest, exec?: { readonly signal?: AbortSignal }): Promise<WebSearchResult> {
|
||||
async search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult> {
|
||||
// A per-request bound wins over the configured default; either may be absent.
|
||||
const numResults = request.maxResults ?? this.options.numResults
|
||||
let response: Response
|
||||
@@ -121,7 +118,7 @@ export class ExaSearchProvider implements WebSearchProvider {
|
||||
contents: { highlights: { highlightsPerUrl: this.options.highlightsPerResult } },
|
||||
...numResults !== undefined ? { numResults } : {},
|
||||
}),
|
||||
...exec?.signal ? { signal: exec.signal } : {},
|
||||
...signal !== undefined ? { signal } : {},
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
if (isAbortError(error)) throw new WebError('Exa search aborted', 'WEB_ABORTED', { cause: error })
|
||||
@@ -149,7 +146,7 @@ export class ExaSearchProvider implements WebSearchProvider {
|
||||
|
||||
try {
|
||||
const payload = await response.json() as ExaSearchResponse
|
||||
return mapExaResponse(request.query, payload)
|
||||
return mapExaResponse(payload)
|
||||
} catch (error: unknown) {
|
||||
if (isAbortError(error)) throw new WebError('Exa search aborted', 'WEB_ABORTED', { cause: error })
|
||||
throw new WebError(`Exa returned an unprocessable response body: ${String(error)}`, 'WEB_PROVIDER_ERROR', { cause: error })
|
||||
|
||||
@@ -17,7 +17,6 @@ maybe('ExaSearchProvider real API', () => {
|
||||
highlightsPerResult: EXA_DEFAULT_HIGHLIGHTS_PER_RESULT,
|
||||
})
|
||||
const result = await provider.search({ query: 'DeepSeek Harness SDK', maxResults: 5 })
|
||||
expect(result.providerId).toBe('exa')
|
||||
expect(result.sources.length).toBeGreaterThan(0)
|
||||
for (const source of result.sources) expect(source.url).toMatch(/^https?:\/\//)
|
||||
}, 30_000)
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('Exa result mapping', () => {
|
||||
})
|
||||
|
||||
it('maps a response to a result with no content and filtered sources', () => {
|
||||
const result = mapExaResponse('q', {
|
||||
const result = mapExaResponse({
|
||||
results: [
|
||||
{ url: 'https://a.test', highlights: ['one'] },
|
||||
{ url: 'https://b.test' },
|
||||
@@ -46,8 +46,6 @@ describe('Exa result mapping', () => {
|
||||
],
|
||||
})
|
||||
expect(result).toEqual({
|
||||
providerId: EXA_PROVIDER_ID,
|
||||
query: 'q',
|
||||
sources: [
|
||||
{ url: 'https://a.test', snippet: 'one' },
|
||||
{ url: 'https://c.test', title: 'C', snippet: 'three' },
|
||||
@@ -58,36 +56,31 @@ describe('Exa result mapping', () => {
|
||||
})
|
||||
|
||||
it('tolerates a missing results array', () => {
|
||||
expect(mapExaResponse('q', {}).sources).toEqual([])
|
||||
expect(mapExaResponse({}).sources).toEqual([])
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('ExaSearchProvider status', () => {
|
||||
describe('ExaSearchProvider availability', () => {
|
||||
it('is unavailable without a key', () => {
|
||||
expect(new ExaSearchProvider({ ...options, apiKey: '' }).status())
|
||||
.toEqual({ available: false, reason: 'missing-credential' })
|
||||
expect(new ExaSearchProvider({ ...options, apiKey: '' }).available()).toBe(false)
|
||||
})
|
||||
|
||||
it('is available with a key', () => {
|
||||
expect(new ExaSearchProvider(options).status()).toEqual({ available: true })
|
||||
expect(new ExaSearchProvider(options).available()).toBe(true)
|
||||
})
|
||||
|
||||
it('is misconfigured when the base URL is unparseable', () => {
|
||||
expect(new ExaSearchProvider({ ...options, baseURL: 'not a url' }).status())
|
||||
.toEqual({ available: false, reason: 'misconfigured' })
|
||||
expect(new ExaSearchProvider({ ...options, baseURL: 'not a url' }).available()).toBe(false)
|
||||
})
|
||||
|
||||
it('is misconfigured when highlightsPerResult is not a positive integer', () => {
|
||||
expect(new ExaSearchProvider({ ...options, highlightsPerResult: 0 }).status())
|
||||
.toEqual({ available: false, reason: 'misconfigured' })
|
||||
expect(new ExaSearchProvider({ ...options, highlightsPerResult: 1.5 }).status())
|
||||
.toEqual({ available: false, reason: 'misconfigured' })
|
||||
expect(new ExaSearchProvider({ ...options, highlightsPerResult: 0 }).available()).toBe(false)
|
||||
expect(new ExaSearchProvider({ ...options, highlightsPerResult: 1.5 }).available()).toBe(false)
|
||||
})
|
||||
|
||||
it('is misconfigured when numResults is set but not a positive integer', () => {
|
||||
expect(new ExaSearchProvider({ ...options, numResults: -1 }).status())
|
||||
.toEqual({ available: false, reason: 'misconfigured' })
|
||||
expect(new ExaSearchProvider({ ...options, numResults: -1 }).available()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -139,7 +132,7 @@ describe('ExaSearchProvider request mapping', () => {
|
||||
const fetchMock = vi.fn(async () => jsonResponse({ results: [] }))
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
const controller = new AbortController()
|
||||
await new ExaSearchProvider(options).search({ query: 'q' }, { signal: controller.signal })
|
||||
await new ExaSearchProvider(options).search({ query: 'q' }, controller.signal)
|
||||
const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]
|
||||
expect(init.signal).toBe(controller.signal)
|
||||
})
|
||||
@@ -209,7 +202,7 @@ describe('web-search-exa plugin registration', () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(WebService, { searchProvider: EXA_PROVIDER_ID })
|
||||
const fiber = await ctx.plugin(exaPlugin, { apiKey: 'exa-key' })
|
||||
await expect(ctx.web.search({ query: 'q' })).resolves.toMatchObject({ providerId: EXA_PROVIDER_ID })
|
||||
await expect(ctx.web.search({ query: 'q' })).resolves.toMatchObject({ sources: [], truncated: false })
|
||||
await fiber.dispose()
|
||||
await expect(ctx.web.search({ query: 'q' }))
|
||||
.rejects.toThrow(expect.objectContaining({ code: 'WEB_PROVIDER_CONFIGURED_MISSING' }))
|
||||
|
||||
Reference in New Issue
Block a user