/** * GroundedSearchClient — provider-agnostic web-search grounding * (in-house-ai-visibility, Sprint 1; * arch-20260717-in-house-oss-ai-visibility-architecture.md:118-129,247-248). * * Wraps an injected `LLMClient.chat` (`providers/types.ts:282`) with a * `web_search` `ToolDef` and normalizes the vendor-native citation payload * (surfaced via `ChatResponse.groundingCitations`, `types.ts`) into a plain * `GroundedAnswer`. No `@anthropic-ai/sdk` or `openai` type crosses this * file's boundary (sc-1-4) — it imports ONLY from `./types.js`. * * The `LLMClient` is CONSTRUCTOR-injected (mirrors `SeoAnalyzer(llm, model)`, * `seo/analyzer.ts:304-307`) — a scripted fake drives every test here; the * real client (and any seam wiring into `AiVisibilityProvider`) is built by * a LATER sprint (nonGoals: no adapter wiring, no multiplexer, no factory, * no `src/seo/` changes this sprint). * * `anthropic.ts`'s `normalizeContent` currently DROPS `web_search` citation * blocks, so no real adapter populates `ChatResponse.groundingCitations` * yet — that population is deferred to a later sprint (see the field's doc * comment in `types.ts`). This client already reads and normalizes the * field; it just has nothing to read from a live call today. * * Sprint 7 adds `PerplexitySonarClient`, a SECOND `GroundedSearchClient` * implementation in this same file. Perplexity Sonar is a direct HTTP * `chat/completions` API, not reachable through `LLMClient.chat` — that * class does not wrap an `LLMClient` and introduces the file's only * `fetch` reference, behind an injectable transport. All Perplexity- * specific types stay local/unexported to this file (sc-7-4). */ import type { LLMClient } from "./types.js"; /** * Grounded-search engines. `"perplexity"` is part of the type (sc-1-1 * requires it in the union) but has NO implementation this sprint — it is * Sprint 7 (nonGoal here). `LiveGroundedSearchClient.search` returns * `citations: []` for it without throwing. */ export type GroundedEngine = "anthropic" | "openai" | "perplexity"; /** A single normalized citation: just enough to attribute a claim. */ export interface GroundedCitation { url: string; title: string; } /** The normalized result of one grounded-search turn. */ export interface GroundedAnswer { answerText: string; citations: GroundedCitation[]; /** USD cost of the underlying LLM call, when known. Key omitted (never * `costUsd: undefined`) when the cost cannot be determined — mirrors the * `ChatResponse.costUsd` convention (`types.ts`, `anthropic.ts:53,63`). */ costUsd?: number; } /** * Provider-agnostic grounded-search surface. Implementations run one * web-search-enabled LLM turn and return a normalized `GroundedAnswer`. */ export interface GroundedSearchClient { readonly engine: GroundedEngine; search(prompt: string, locale?: string): Promise; } /** * Wraps an injected `LLMClient.chat` to run a single web-search-grounded * turn and normalize its citations. Never constructs a real provider * client itself — the caller injects one built by `createClient` * (`factory.ts`) at wiring time (a later sprint). */ export declare class LiveGroundedSearchClient implements GroundedSearchClient { readonly engine: GroundedEngine; private readonly llm; private readonly model; constructor(engine: GroundedEngine, llm: LLMClient, model: string); /** * Runs one grounded-search turn. Never throws on a non-grounded response * (sc-1-3): `groundingCitations` is guarded with `?? []` before mapping, * so `citations` is simply `[]` when the model didn't ground its answer. */ search(prompt: string, locale?: string): Promise; } /** * Duck-typed response — deliberately NOT the global `Response` type, so * tests can construct fakes without touching the real fetch API. Mirrors * `HttpResponse` (`seo/adapters/http.ts:23-27`) in SHAPE only — defined * LOCALLY here (unexported) because `src/providers/` must not import from * `src/seo/` (sc-7-4 / sprint briefing Pattern C). */ interface PerplexityTransportResponse { ok: boolean; status: number; json(): Promise; } /** * Fetch-like injectable transport — the SOLE seam through which * `PerplexitySonarClient` reaches the network. Defaults to a thin global- * `fetch` wrapper (`defaultPerplexityTransport` below); tests inject a fake * so no socket ever opens (sc-7-3). */ type PerplexityTransport = (url: string, init: { method: string; headers: Record; body: string; }) => Promise; /** * `PerplexitySonarClient` — the third `GroundedSearchClient` arm (sc-7-1.. * sc-7-4). Mirrors the DataForSEO credential-injection idiom * (`dataforseo-adapter.ts:162-178`): the API key is read from * `PERPLEXITY_API_KEY` via an injected `getApiKey` (never hardcoded), and * the network call goes through an injected `transport` — both default to * real implementations so production wiring needs no arguments, while tests * stub both and never open a real socket (sc-7-3). */ export declare class PerplexitySonarClient implements GroundedSearchClient { private readonly transport; private readonly getApiKey; private readonly model; readonly engine: GroundedEngine; constructor(transport?: PerplexityTransport, getApiKey?: () => string | undefined, model?: string); /** * Runs one Sonar-grounded turn. Never throws (sc-7-1): a missing key, an * `!res.ok` response, a network error, or malformed JSON all degrade to * `{ answerText: "", citations: [] }` — there is no `DataOutcome`/abstain * at this layer (that happens upstream in `ApiSpineEngineProvider.probe`, * api-spine-provider.ts:80-84). `costUsd` is intentionally OMITTED * (Pattern D) — Sonar cost is already N-baked via `perCallUsd`. */ search(prompt: string, locale?: string): Promise; } export {}; //# sourceMappingURL=grounded-search.d.ts.map