@effect-uai/exa 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -116,7 +116,7 @@ const readImpl = (cfg) => (request) => Effect.gen(function* () {
116
116
  * Build an `ExaContentsService` value. For Layer-based setup, prefer
117
117
  * {@link layer}.
118
118
  */
119
- const make = (cfg) => Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({ read: (request) => readImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)) }));
119
+ const make = (cfg) => Effect.map(HttpClient.HttpClient, (client) => ({ read: (request) => readImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)) }));
120
120
  /**
121
121
  * Layer registering both the provider-typed `ExaContents` tag and the generic
122
122
  * `WebRead` tag over one implementation. A `CommonReadRequest` is structurally
@@ -1 +1 @@
1
- {"version":3,"file":"ExaContents.mjs","names":[],"sources":["../src/ExaContents.ts"],"sourcesContent":["import { Context, Effect, Layer, Redacted, Schema } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport {\n type CommonReadRequest,\n type ReadResponse,\n WebRead,\n type WebReadService,\n} from \"@effect-uai/core/WebRead\"\nimport type { ExaLivecrawl } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Exa-typed read request. Widens {@link CommonReadRequest} with Exa\n * `/contents` knobs; the common fields map onto Exa's wire body (see the\n * codec below).\n */\nexport type ExaContentsRequest = CommonReadRequest & {\n /** Freshness / cache policy - see {@link ExaLivecrawl}. */\n readonly livecrawl?: ExaLivecrawl\n /** Cap on the returned content length (Exa `text.maxCharacters`). */\n readonly maxCharacters?: number\n}\n\nexport type ExaContentsService = {\n readonly read: (request: ExaContentsRequest) => Effect.Effect<ReadResponse, AiError.AiError>\n}\n\n/**\n * Provider-typed service tag. Yield this for Exa-typed reads (its own\n * `/contents` knobs); yield the generic `WebRead` tag for provider-portable\n * code. Both are registered by {@link layer}.\n */\nexport class ExaContents extends Context.Service<ExaContents, ExaContentsService>()(\n \"@betalyra/effect-uai/providers/exa/ExaContents\",\n) {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n}\n\n// ---------------------------------------------------------------------------\n// Codec - request body\n// ---------------------------------------------------------------------------\n\ntype WireText = {\n readonly maxCharacters?: number\n readonly includeHtmlTags?: boolean\n}\n\ntype WireBody = {\n readonly urls: ReadonlyArray<string>\n readonly text: WireText\n readonly livecrawl?: string\n}\n\n// Exa returns markdown by default; `includeHtmlTags: true` returns HTML tags\n// instead. Both land on the single `text` field of the result.\nconst buildBody = (request: ExaContentsRequest): WireBody => {\n const text: WireText = {\n ...(request.maxCharacters !== undefined && { maxCharacters: request.maxCharacters }),\n ...(request.format === \"html\" && { includeHtmlTags: true }),\n }\n return {\n urls: [request.url],\n text,\n ...(request.livecrawl !== undefined && { livecrawl: request.livecrawl }),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Codec - response\n// ---------------------------------------------------------------------------\n\nconst WireResult = Schema.Struct({\n id: Schema.optional(Schema.String),\n url: Schema.String,\n title: Schema.optional(Schema.NullOr(Schema.String)),\n text: Schema.optional(Schema.NullOr(Schema.String)),\n})\ntype WireResult = typeof WireResult.Type\n\nconst WireStatus = Schema.Struct({\n id: Schema.optional(Schema.String),\n status: Schema.optional(Schema.String),\n source: Schema.optional(Schema.String),\n})\n\nconst WireResponse = Schema.Struct({\n requestId: Schema.optional(Schema.String),\n results: Schema.Array(WireResult),\n statuses: Schema.optional(Schema.Array(WireStatus)),\n})\ntype WireResponse = typeof WireResponse.Type\n\nconst toResponse = (wire: WireResponse, result: WireResult): ReadResponse => {\n const title = result.title ?? undefined\n return {\n url: result.url,\n content: result.text ?? \"\",\n ...(title !== undefined && { title }),\n raw: wire,\n }\n}\n\n// ---------------------------------------------------------------------------\n// HTTP\n// ---------------------------------------------------------------------------\n\nconst transportFailure = (cause: unknown): AiError.AiError =>\n new AiError.Unavailable({ provider: \"exa\", raw: cause })\n\nconst httpStatusError = (status: number, body: string): AiError.AiError => {\n const provider = \"exa\"\n const raw = body\n if (status === 429) return new AiError.RateLimited({ provider, raw })\n if (status === 408 || status === 504) return new AiError.Timeout({ provider, raw })\n if (status === 401) return new AiError.AuthFailed({ provider, subtype: \"auth\", raw })\n if (status === 403) return new AiError.AuthFailed({ provider, subtype: \"permission\", raw })\n if (status === 402) return new AiError.AuthFailed({ provider, subtype: \"billing\", raw })\n if (status >= 500) return new AiError.Unavailable({ provider, status, raw })\n return new AiError.InvalidRequest({ provider, raw })\n}\n\nconst baseUrl = (cfg: Config): string => cfg.baseUrl ?? \"https://api.exa.ai\"\n\nconst postContents = (\n cfg: Config,\n body: WireBody,\n): Effect.Effect<WireResponse, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/contents`).pipe(\n HttpClientRequest.setHeader(\"x-api-key\", Redacted.value(cfg.apiKey)),\n HttpClientRequest.bodyJsonUnsafe(body),\n )\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* httpStatusError(response.status, text)\n }\n const json = yield* response.json.pipe(Effect.mapError(transportFailure))\n return yield* Schema.decodeUnknownEffect(WireResponse)(json).pipe(\n Effect.mapError(transportFailure),\n )\n })\n\nconst readImpl =\n (cfg: Config) =>\n (\n request: ExaContentsRequest,\n ): Effect.Effect<ReadResponse, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const wire = yield* postContents(cfg, buildBody(request))\n // Exa reports per-URL crawl failures in `results` / `statuses` rather\n // than an HTTP error; a missing first result means the single URL could\n // not be read.\n const result = wire.results[0]\n if (result === undefined) {\n return yield* new AiError.Unavailable({ provider: \"exa\", raw: wire })\n }\n return toResponse(wire, result)\n })\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\n/**\n * Build an `ExaContentsService` value. For Layer-based setup, prefer\n * {@link layer}.\n */\nexport const make = (\n cfg: Config,\n): Effect.Effect<ExaContentsService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({\n read: (request) =>\n readImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n }))\n\n/**\n * Layer registering both the provider-typed `ExaContents` tag and the generic\n * `WebRead` tag over one implementation. A `CommonReadRequest` is structurally\n * an `ExaContentsRequest` with the extra knobs unset, so the generic\n * registration forwards directly.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<ExaContents | WebRead, never, HttpClient.HttpClient> => {\n const typed = Layer.effect(ExaContents, make(cfg))\n const generic = Layer.effect(\n WebRead,\n Effect.map(make(cfg), (s): WebReadService => ({ read: (request) => s.read(request) })),\n )\n return Layer.merge(typed, generic)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ExaContents.mjs","names":[],"sources":["../src/ExaContents.ts"],"sourcesContent":["import { Context, Effect, Layer, Redacted, Schema } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport {\n type CommonReadRequest,\n type ReadResponse,\n WebRead,\n type WebReadService,\n} from \"@effect-uai/core/WebRead\"\nimport type { ExaLivecrawl } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Exa-typed read request. Widens {@link CommonReadRequest} with Exa\n * `/contents` knobs; the common fields map onto Exa's wire body (see the\n * codec below).\n */\nexport type ExaContentsRequest = CommonReadRequest & {\n /** Freshness / cache policy - see {@link ExaLivecrawl}. */\n readonly livecrawl?: ExaLivecrawl\n /** Cap on the returned content length (Exa `text.maxCharacters`). */\n readonly maxCharacters?: number\n}\n\nexport type ExaContentsService = {\n readonly read: (request: ExaContentsRequest) => Effect.Effect<ReadResponse, AiError.AiError>\n}\n\n/**\n * Provider-typed service tag. Yield this for Exa-typed reads (its own\n * `/contents` knobs); yield the generic `WebRead` tag for provider-portable\n * code. Both are registered by {@link layer}.\n */\nexport class ExaContents extends Context.Service<ExaContents, ExaContentsService>()(\n \"@betalyra/effect-uai/providers/exa/ExaContents\",\n) {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n}\n\n// ---------------------------------------------------------------------------\n// Codec - request body\n// ---------------------------------------------------------------------------\n\ntype WireText = {\n readonly maxCharacters?: number\n readonly includeHtmlTags?: boolean\n}\n\ntype WireBody = {\n readonly urls: ReadonlyArray<string>\n readonly text: WireText\n readonly livecrawl?: string\n}\n\n// Exa returns markdown by default; `includeHtmlTags: true` returns HTML tags\n// instead. Both land on the single `text` field of the result.\nconst buildBody = (request: ExaContentsRequest): WireBody => {\n const text: WireText = {\n ...(request.maxCharacters !== undefined && { maxCharacters: request.maxCharacters }),\n ...(request.format === \"html\" && { includeHtmlTags: true }),\n }\n return {\n urls: [request.url],\n text,\n ...(request.livecrawl !== undefined && { livecrawl: request.livecrawl }),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Codec - response\n// ---------------------------------------------------------------------------\n\nconst WireResult = Schema.Struct({\n id: Schema.optional(Schema.String),\n url: Schema.String,\n title: Schema.optional(Schema.NullOr(Schema.String)),\n text: Schema.optional(Schema.NullOr(Schema.String)),\n})\ntype WireResult = typeof WireResult.Type\n\nconst WireStatus = Schema.Struct({\n id: Schema.optional(Schema.String),\n status: Schema.optional(Schema.String),\n source: Schema.optional(Schema.String),\n})\n\nconst WireResponse = Schema.Struct({\n requestId: Schema.optional(Schema.String),\n results: Schema.Array(WireResult),\n statuses: Schema.optional(Schema.Array(WireStatus)),\n})\ntype WireResponse = typeof WireResponse.Type\n\nconst toResponse = (wire: WireResponse, result: WireResult): ReadResponse => {\n const title = result.title ?? undefined\n return {\n url: result.url,\n content: result.text ?? \"\",\n ...(title !== undefined && { title }),\n raw: wire,\n }\n}\n\n// ---------------------------------------------------------------------------\n// HTTP\n// ---------------------------------------------------------------------------\n\nconst transportFailure = (cause: unknown): AiError.AiError =>\n new AiError.Unavailable({ provider: \"exa\", raw: cause })\n\nconst httpStatusError = (status: number, body: string): AiError.AiError => {\n const provider = \"exa\"\n const raw = body\n if (status === 429) return new AiError.RateLimited({ provider, raw })\n if (status === 408 || status === 504) return new AiError.Timeout({ provider, raw })\n if (status === 401) return new AiError.AuthFailed({ provider, subtype: \"auth\", raw })\n if (status === 403) return new AiError.AuthFailed({ provider, subtype: \"permission\", raw })\n if (status === 402) return new AiError.AuthFailed({ provider, subtype: \"billing\", raw })\n if (status >= 500) return new AiError.Unavailable({ provider, status, raw })\n return new AiError.InvalidRequest({ provider, raw })\n}\n\nconst baseUrl = (cfg: Config): string => cfg.baseUrl ?? \"https://api.exa.ai\"\n\nconst postContents = (\n cfg: Config,\n body: WireBody,\n): Effect.Effect<WireResponse, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/contents`).pipe(\n HttpClientRequest.setHeader(\"x-api-key\", Redacted.value(cfg.apiKey)),\n HttpClientRequest.bodyJsonUnsafe(body),\n )\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* httpStatusError(response.status, text)\n }\n const json = yield* response.json.pipe(Effect.mapError(transportFailure))\n return yield* Schema.decodeUnknownEffect(WireResponse)(json).pipe(\n Effect.mapError(transportFailure),\n )\n })\n\nconst readImpl =\n (cfg: Config) =>\n (\n request: ExaContentsRequest,\n ): Effect.Effect<ReadResponse, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const wire = yield* postContents(cfg, buildBody(request))\n // Exa reports per-URL crawl failures in `results` / `statuses` rather\n // than an HTTP error; a missing first result means the single URL could\n // not be read.\n const result = wire.results[0]\n if (result === undefined) {\n return yield* new AiError.Unavailable({ provider: \"exa\", raw: wire })\n }\n return toResponse(wire, result)\n })\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\n/**\n * Build an `ExaContentsService` value. For Layer-based setup, prefer\n * {@link layer}.\n */\nexport const make = (\n cfg: Config,\n): Effect.Effect<ExaContentsService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient, (client) => ({\n read: (request) =>\n readImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n }))\n\n/**\n * Layer registering both the provider-typed `ExaContents` tag and the generic\n * `WebRead` tag over one implementation. A `CommonReadRequest` is structurally\n * an `ExaContentsRequest` with the extra knobs unset, so the generic\n * registration forwards directly.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<ExaContents | WebRead, never, HttpClient.HttpClient> => {\n const typed = Layer.effect(ExaContents, make(cfg))\n const generic = Layer.effect(\n WebRead,\n Effect.map(make(cfg), (s): WebReadService => ({ read: (request) => s.read(request) })),\n )\n return Layer.merge(typed, generic)\n}\n"],"mappings":"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"}
@@ -132,7 +132,7 @@ const searchImpl = (cfg) => (request) => Effect.gen(function* () {
132
132
  * Build an `ExaSearchService` value. For Layer-based setup, prefer
133
133
  * {@link layer}.
134
134
  */
135
- const make = (cfg) => Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({ search: (request) => searchImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)) }));
135
+ const make = (cfg) => Effect.map(HttpClient.HttpClient, (client) => ({ search: (request) => searchImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)) }));
136
136
  /**
137
137
  * Layer registering both the provider-typed `ExaSearch` tag and the
138
138
  * generic `WebSearch` tag over one implementation. A `CommonSearchRequest`
@@ -1 +1 @@
1
- {"version":3,"file":"ExaSearch.mjs","names":[],"sources":["../src/ExaSearch.ts"],"sourcesContent":["import { Context, DateTime, type Duration, Effect, Layer, Option, Redacted, Schema } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport * as Capabilities from \"@effect-uai/core/Capabilities\"\nimport {\n type CommonSearchRequest,\n type SearchRecency,\n type SearchResponse,\n type SearchResult,\n WebSearch,\n type WebSearchService,\n} from \"@effect-uai/core/WebSearch\"\nimport type { ExaCategory, ExaSearchType } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Exa-typed search request. Widens {@link CommonSearchRequest} with Exa's\n * own knobs; the common fields map onto Exa's wire filters (see the codec\n * below). There is no `model` - `/search` ranks the web directly, with\n * `type` choosing the retrieval strategy.\n */\nexport type ExaSearchRequest = CommonSearchRequest & {\n /** Retrieval strategy - see {@link ExaSearchType}. Defaults to `auto`. */\n readonly type?: ExaSearchType\n /** Content-vertical filter - see {@link ExaCategory}. */\n readonly category?: ExaCategory\n}\n\nexport type ExaSearchService = {\n readonly search: (request: ExaSearchRequest) => Effect.Effect<SearchResponse, AiError.AiError>\n}\n\n/**\n * Provider-typed service tag. Yield this for Exa-typed search (`type`,\n * `category`, the per-result `score`); yield the generic `WebSearch` tag\n * for provider-portable code. Both are registered by {@link layer}.\n */\nexport class ExaSearch extends Context.Service<ExaSearch, ExaSearchService>()(\n \"@betalyra/effect-uai/providers/exa/ExaSearch\",\n) {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n}\n\n// ---------------------------------------------------------------------------\n// Codec - request body\n// ---------------------------------------------------------------------------\n\ntype WireBody = {\n readonly query: string\n readonly numResults?: number\n readonly type?: string\n readonly category?: string\n readonly includeDomains?: ReadonlyArray<string>\n readonly excludeDomains?: ReadonlyArray<string>\n readonly startPublishedDate?: string\n readonly endPublishedDate?: string\n readonly userLocation?: string\n}\n\n// Exa has no recency enum; map the coarse enum onto a published-after date\n// computed from \"now\" (per the capabilities policy: lossless translation).\nconst recencyDuration: Record<SearchRecency, Duration.Input> = {\n hour: \"1 hour\",\n day: \"1 day\",\n week: \"7 days\",\n month: \"30 days\",\n year: \"365 days\",\n}\n\nconst buildBody = (request: ExaSearchRequest, now: DateTime.DateTime): WireBody => {\n // Explicit `startDate` wins over the `recency` sugar; otherwise derive the\n // published-after bound from the recency window.\n const startPublishedDate =\n request.startDate !== undefined\n ? DateTime.formatIso(request.startDate)\n : request.recency !== undefined\n ? DateTime.formatIso(DateTime.subtractDuration(now, recencyDuration[request.recency]))\n : undefined\n\n return {\n query: request.query,\n ...(request.maxResults !== undefined && { numResults: request.maxResults }),\n ...(request.type !== undefined && { type: request.type }),\n ...(request.category !== undefined && { category: request.category }),\n ...(request.includeDomains !== undefined && { includeDomains: request.includeDomains }),\n ...(request.excludeDomains !== undefined && { excludeDomains: request.excludeDomains }),\n ...(startPublishedDate !== undefined && { startPublishedDate }),\n ...(request.endDate !== undefined && { endPublishedDate: DateTime.formatIso(request.endDate) }),\n ...(request.country !== undefined && { userLocation: request.country }),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Codec - response\n// ---------------------------------------------------------------------------\n\nconst WireResult = Schema.Struct({\n id: Schema.optional(Schema.String),\n url: Schema.String,\n title: Schema.optional(Schema.NullOr(Schema.String)),\n score: Schema.optional(Schema.Number),\n publishedDate: Schema.optional(Schema.NullOr(Schema.String)),\n author: Schema.optional(Schema.NullOr(Schema.String)),\n})\ntype WireResult = typeof WireResult.Type\n\nconst WireCost = Schema.Struct({\n total: Schema.optional(Schema.Number),\n})\n\nconst WireResponse = Schema.Struct({\n requestId: Schema.optional(Schema.String),\n results: Schema.Array(WireResult),\n costDollars: Schema.optional(WireCost),\n})\ntype WireResponse = typeof WireResponse.Type\n\n/** Lenient parse of Exa's ISO date strings; unparseable -> omitted. */\nconst parseDate = (s: string | null | undefined): DateTime.DateTime | undefined =>\n s == null ? undefined : Option.getOrUndefined(DateTime.make(s))\n\n// Exa's pure `/search` returns no text excerpt unless you request `contents`\n// (the out-of-scope extract feature), so `snippet` stays undefined here -\n// the honest result is url + title + score + publishedDate. `author` and\n// the full `raw` object remain on `raw` for typed-tag callers.\nconst toResult = (r: WireResult): SearchResult => {\n const publishedDate = parseDate(r.publishedDate)\n return {\n url: r.url,\n ...(r.title != null && { title: r.title }),\n ...(r.score !== undefined && { score: r.score }),\n ...(publishedDate !== undefined && { publishedDate }),\n raw: r,\n }\n}\n\n// `costDollars` is still decoded (so it survives on `raw`) but not promoted\n// to a typed cost field: search cost/usage is deferred to the unified usage\n// design (see plans/usage-tracking.md).\nconst toResponse = (wire: WireResponse): SearchResponse => ({\n results: wire.results.map(toResult),\n raw: wire,\n})\n\n// ---------------------------------------------------------------------------\n// HTTP\n// ---------------------------------------------------------------------------\n\nconst transportFailure = (cause: unknown): AiError.AiError =>\n new AiError.Unavailable({ provider: \"exa\", raw: cause })\n\nconst httpStatusError = (status: number, body: string): AiError.AiError => {\n const provider = \"exa\"\n const raw = body\n if (status === 429) return new AiError.RateLimited({ provider, raw })\n if (status === 408 || status === 504) return new AiError.Timeout({ provider, raw })\n if (status === 401) return new AiError.AuthFailed({ provider, subtype: \"auth\", raw })\n if (status === 403) return new AiError.AuthFailed({ provider, subtype: \"permission\", raw })\n if (status === 402) return new AiError.AuthFailed({ provider, subtype: \"billing\", raw })\n if (status >= 500) return new AiError.Unavailable({ provider, status, raw })\n return new AiError.InvalidRequest({ provider, raw })\n}\n\nconst baseUrl = (cfg: Config): string => cfg.baseUrl ?? \"https://api.exa.ai\"\n\nconst postSearch = (\n cfg: Config,\n body: WireBody,\n): Effect.Effect<WireResponse, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/search`).pipe(\n HttpClientRequest.setHeader(\"x-api-key\", Redacted.value(cfg.apiKey)),\n HttpClientRequest.bodyJsonUnsafe(body),\n )\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* httpStatusError(response.status, text)\n }\n const json = yield* response.json.pipe(Effect.mapError(transportFailure))\n return yield* Schema.decodeUnknownEffect(WireResponse)(json).pipe(\n Effect.mapError(transportFailure),\n )\n })\n\nconst searchImpl =\n (cfg: Config) =>\n (\n request: ExaSearchRequest,\n ): Effect.Effect<SearchResponse, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n // Exa has no language filter (bucket 2: warn, don't fail).\n yield* Capabilities.warnDroppedWhen(request.language, {\n provider: \"exa\",\n capability: \"language\",\n field: \"language\",\n reason: \"Exa /search has no language filter; the hint is ignored.\",\n })\n const now = yield* DateTime.now\n const wire = yield* postSearch(cfg, buildBody(request, now))\n return toResponse(wire)\n })\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\n/**\n * Build an `ExaSearchService` value. For Layer-based setup, prefer\n * {@link layer}.\n */\nexport const make = (cfg: Config): Effect.Effect<ExaSearchService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({\n search: (request) =>\n searchImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n }))\n\n/**\n * Layer registering both the provider-typed `ExaSearch` tag and the\n * generic `WebSearch` tag over one implementation. A `CommonSearchRequest`\n * is structurally an `ExaSearchRequest` with `type` / `category` unset, so\n * the generic registration forwards directly.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<ExaSearch | WebSearch, never, HttpClient.HttpClient> => {\n const typed = Layer.effect(ExaSearch, make(cfg))\n const generic = Layer.effect(\n WebSearch,\n Effect.map(make(cfg), (s): WebSearchService => ({ search: (request) => s.search(request) })),\n )\n return Layer.merge(typed, generic)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ExaSearch.mjs","names":[],"sources":["../src/ExaSearch.ts"],"sourcesContent":["import { Context, DateTime, type Duration, Effect, Layer, Option, Redacted, Schema } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport * as Capabilities from \"@effect-uai/core/Capabilities\"\nimport {\n type CommonSearchRequest,\n type SearchRecency,\n type SearchResponse,\n type SearchResult,\n WebSearch,\n type WebSearchService,\n} from \"@effect-uai/core/WebSearch\"\nimport type { ExaCategory, ExaSearchType } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Exa-typed search request. Widens {@link CommonSearchRequest} with Exa's\n * own knobs; the common fields map onto Exa's wire filters (see the codec\n * below). There is no `model` - `/search` ranks the web directly, with\n * `type` choosing the retrieval strategy.\n */\nexport type ExaSearchRequest = CommonSearchRequest & {\n /** Retrieval strategy - see {@link ExaSearchType}. Defaults to `auto`. */\n readonly type?: ExaSearchType\n /** Content-vertical filter - see {@link ExaCategory}. */\n readonly category?: ExaCategory\n}\n\nexport type ExaSearchService = {\n readonly search: (request: ExaSearchRequest) => Effect.Effect<SearchResponse, AiError.AiError>\n}\n\n/**\n * Provider-typed service tag. Yield this for Exa-typed search (`type`,\n * `category`, the per-result `score`); yield the generic `WebSearch` tag\n * for provider-portable code. Both are registered by {@link layer}.\n */\nexport class ExaSearch extends Context.Service<ExaSearch, ExaSearchService>()(\n \"@betalyra/effect-uai/providers/exa/ExaSearch\",\n) {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n}\n\n// ---------------------------------------------------------------------------\n// Codec - request body\n// ---------------------------------------------------------------------------\n\ntype WireBody = {\n readonly query: string\n readonly numResults?: number\n readonly type?: string\n readonly category?: string\n readonly includeDomains?: ReadonlyArray<string>\n readonly excludeDomains?: ReadonlyArray<string>\n readonly startPublishedDate?: string\n readonly endPublishedDate?: string\n readonly userLocation?: string\n}\n\n// Exa has no recency enum; map the coarse enum onto a published-after date\n// computed from \"now\" (per the capabilities policy: lossless translation).\nconst recencyDuration: Record<SearchRecency, Duration.Input> = {\n hour: \"1 hour\",\n day: \"1 day\",\n week: \"7 days\",\n month: \"30 days\",\n year: \"365 days\",\n}\n\nconst buildBody = (request: ExaSearchRequest, now: DateTime.DateTime): WireBody => {\n // Explicit `startDate` wins over the `recency` sugar; otherwise derive the\n // published-after bound from the recency window.\n const startPublishedDate =\n request.startDate !== undefined\n ? DateTime.formatIso(request.startDate)\n : request.recency !== undefined\n ? DateTime.formatIso(DateTime.subtractDuration(now, recencyDuration[request.recency]))\n : undefined\n\n return {\n query: request.query,\n ...(request.maxResults !== undefined && { numResults: request.maxResults }),\n ...(request.type !== undefined && { type: request.type }),\n ...(request.category !== undefined && { category: request.category }),\n ...(request.includeDomains !== undefined && { includeDomains: request.includeDomains }),\n ...(request.excludeDomains !== undefined && { excludeDomains: request.excludeDomains }),\n ...(startPublishedDate !== undefined && { startPublishedDate }),\n ...(request.endDate !== undefined && { endPublishedDate: DateTime.formatIso(request.endDate) }),\n ...(request.country !== undefined && { userLocation: request.country }),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Codec - response\n// ---------------------------------------------------------------------------\n\nconst WireResult = Schema.Struct({\n id: Schema.optional(Schema.String),\n url: Schema.String,\n title: Schema.optional(Schema.NullOr(Schema.String)),\n score: Schema.optional(Schema.Number),\n publishedDate: Schema.optional(Schema.NullOr(Schema.String)),\n author: Schema.optional(Schema.NullOr(Schema.String)),\n})\ntype WireResult = typeof WireResult.Type\n\nconst WireCost = Schema.Struct({\n total: Schema.optional(Schema.Number),\n})\n\nconst WireResponse = Schema.Struct({\n requestId: Schema.optional(Schema.String),\n results: Schema.Array(WireResult),\n costDollars: Schema.optional(WireCost),\n})\ntype WireResponse = typeof WireResponse.Type\n\n/** Lenient parse of Exa's ISO date strings; unparseable -> omitted. */\nconst parseDate = (s: string | null | undefined): DateTime.DateTime | undefined =>\n s == null ? undefined : Option.getOrUndefined(DateTime.make(s))\n\n// Exa's pure `/search` returns no text excerpt unless you request `contents`\n// (the out-of-scope extract feature), so `snippet` stays undefined here -\n// the honest result is url + title + score + publishedDate. `author` and\n// the full `raw` object remain on `raw` for typed-tag callers.\nconst toResult = (r: WireResult): SearchResult => {\n const publishedDate = parseDate(r.publishedDate)\n return {\n url: r.url,\n ...(r.title != null && { title: r.title }),\n ...(r.score !== undefined && { score: r.score }),\n ...(publishedDate !== undefined && { publishedDate }),\n raw: r,\n }\n}\n\n// `costDollars` is still decoded (so it survives on `raw`) but not promoted\n// to a typed cost field: search cost/usage is deferred to the unified usage\n// design (see plans/usage-tracking.md).\nconst toResponse = (wire: WireResponse): SearchResponse => ({\n results: wire.results.map(toResult),\n raw: wire,\n})\n\n// ---------------------------------------------------------------------------\n// HTTP\n// ---------------------------------------------------------------------------\n\nconst transportFailure = (cause: unknown): AiError.AiError =>\n new AiError.Unavailable({ provider: \"exa\", raw: cause })\n\nconst httpStatusError = (status: number, body: string): AiError.AiError => {\n const provider = \"exa\"\n const raw = body\n if (status === 429) return new AiError.RateLimited({ provider, raw })\n if (status === 408 || status === 504) return new AiError.Timeout({ provider, raw })\n if (status === 401) return new AiError.AuthFailed({ provider, subtype: \"auth\", raw })\n if (status === 403) return new AiError.AuthFailed({ provider, subtype: \"permission\", raw })\n if (status === 402) return new AiError.AuthFailed({ provider, subtype: \"billing\", raw })\n if (status >= 500) return new AiError.Unavailable({ provider, status, raw })\n return new AiError.InvalidRequest({ provider, raw })\n}\n\nconst baseUrl = (cfg: Config): string => cfg.baseUrl ?? \"https://api.exa.ai\"\n\nconst postSearch = (\n cfg: Config,\n body: WireBody,\n): Effect.Effect<WireResponse, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/search`).pipe(\n HttpClientRequest.setHeader(\"x-api-key\", Redacted.value(cfg.apiKey)),\n HttpClientRequest.bodyJsonUnsafe(body),\n )\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* httpStatusError(response.status, text)\n }\n const json = yield* response.json.pipe(Effect.mapError(transportFailure))\n return yield* Schema.decodeUnknownEffect(WireResponse)(json).pipe(\n Effect.mapError(transportFailure),\n )\n })\n\nconst searchImpl =\n (cfg: Config) =>\n (\n request: ExaSearchRequest,\n ): Effect.Effect<SearchResponse, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n // Exa has no language filter (bucket 2: warn, don't fail).\n yield* Capabilities.warnDroppedWhen(request.language, {\n provider: \"exa\",\n capability: \"language\",\n field: \"language\",\n reason: \"Exa /search has no language filter; the hint is ignored.\",\n })\n const now = yield* DateTime.now\n const wire = yield* postSearch(cfg, buildBody(request, now))\n return toResponse(wire)\n })\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\n/**\n * Build an `ExaSearchService` value. For Layer-based setup, prefer\n * {@link layer}.\n */\nexport const make = (cfg: Config): Effect.Effect<ExaSearchService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient, (client) => ({\n search: (request) =>\n searchImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n }))\n\n/**\n * Layer registering both the provider-typed `ExaSearch` tag and the\n * generic `WebSearch` tag over one implementation. A `CommonSearchRequest`\n * is structurally an `ExaSearchRequest` with `type` / `category` unset, so\n * the generic registration forwards directly.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<ExaSearch | WebSearch, never, HttpClient.HttpClient> => {\n const typed = Layer.effect(ExaSearch, make(cfg))\n const generic = Layer.effect(\n WebSearch,\n Effect.map(make(cfg), (s): WebSearchService => ({ search: (request) => s.search(request) })),\n )\n return Layer.merge(typed, generic)\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-uai/exa",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Exa provider for @effect-uai/core (web search, web read).",
5
5
  "keywords": [
6
6
  "ai",
@@ -48,13 +48,13 @@
48
48
  "access": "public"
49
49
  },
50
50
  "devDependencies": {
51
- "effect": "4.0.0-beta.57",
51
+ "effect": "4.0.0-beta.94",
52
52
  "typescript": "^6.0.3",
53
- "@effect-uai/core": "0.10.0"
53
+ "@effect-uai/core": "0.12.0"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "@effect-uai/core": ">=0.2.0 <1",
57
- "effect": "4.0.0-beta.57"
57
+ "effect": ">=4.0.0-beta.94 <5.0.0"
58
58
  },
59
59
  "scripts": {
60
60
  "build": "tsdown",
@@ -177,7 +177,7 @@ const readImpl =
177
177
  export const make = (
178
178
  cfg: Config,
179
179
  ): Effect.Effect<ExaContentsService, never, HttpClient.HttpClient> =>
180
- Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({
180
+ Effect.map(HttpClient.HttpClient, (client) => ({
181
181
  read: (request) =>
182
182
  readImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),
183
183
  }))
package/src/ExaSearch.ts CHANGED
@@ -217,7 +217,7 @@ const searchImpl =
217
217
  * {@link layer}.
218
218
  */
219
219
  export const make = (cfg: Config): Effect.Effect<ExaSearchService, never, HttpClient.HttpClient> =>
220
- Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({
220
+ Effect.map(HttpClient.HttpClient, (client) => ({
221
221
  search: (request) =>
222
222
  searchImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),
223
223
  }))