@effect-uai/exa 0.9.0 → 0.10.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.
- package/dist/ExaContents.d.mts +48 -0
- package/dist/ExaContents.d.mts.map +1 -0
- package/dist/ExaContents.mjs +134 -0
- package/dist/ExaContents.mjs.map +1 -0
- package/dist/index.d.mts +3 -2
- package/dist/index.mjs +2 -1
- package/dist/models.d.mts +7 -1
- package/dist/models.d.mts.map +1 -1
- package/package.json +8 -3
- package/src/ExaContents.ts +200 -0
- package/src/index.ts +1 -0
- package/src/models.ts +13 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ExaLivecrawl } from "./models.mjs";
|
|
2
|
+
import { Context, Effect, Layer, Redacted } from "effect";
|
|
3
|
+
import { HttpClient } from "effect/unstable/http";
|
|
4
|
+
import * as AiError from "@effect-uai/core/AiError";
|
|
5
|
+
import { CommonReadRequest, ReadResponse, WebRead } from "@effect-uai/core/WebRead";
|
|
6
|
+
|
|
7
|
+
//#region src/ExaContents.d.ts
|
|
8
|
+
declare namespace ExaContents_d_exports {
|
|
9
|
+
export { Config, ExaContents, ExaContentsRequest, ExaContentsService, layer, make };
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Exa-typed read request. Widens {@link CommonReadRequest} with Exa
|
|
13
|
+
* `/contents` knobs; the common fields map onto Exa's wire body (see the
|
|
14
|
+
* codec below).
|
|
15
|
+
*/
|
|
16
|
+
type ExaContentsRequest = CommonReadRequest & {
|
|
17
|
+
/** Freshness / cache policy - see {@link ExaLivecrawl}. */readonly livecrawl?: ExaLivecrawl; /** Cap on the returned content length (Exa `text.maxCharacters`). */
|
|
18
|
+
readonly maxCharacters?: number;
|
|
19
|
+
};
|
|
20
|
+
type ExaContentsService = {
|
|
21
|
+
readonly read: (request: ExaContentsRequest) => Effect.Effect<ReadResponse, AiError.AiError>;
|
|
22
|
+
};
|
|
23
|
+
declare const ExaContents_base: Context.ServiceClass<ExaContents, "@betalyra/effect-uai/providers/exa/ExaContents", ExaContentsService>;
|
|
24
|
+
/**
|
|
25
|
+
* Provider-typed service tag. Yield this for Exa-typed reads (its own
|
|
26
|
+
* `/contents` knobs); yield the generic `WebRead` tag for provider-portable
|
|
27
|
+
* code. Both are registered by {@link layer}.
|
|
28
|
+
*/
|
|
29
|
+
declare class ExaContents extends ExaContents_base {}
|
|
30
|
+
type Config = {
|
|
31
|
+
readonly apiKey: Redacted.Redacted;
|
|
32
|
+
readonly baseUrl?: string;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Build an `ExaContentsService` value. For Layer-based setup, prefer
|
|
36
|
+
* {@link layer}.
|
|
37
|
+
*/
|
|
38
|
+
declare const make: (cfg: Config) => Effect.Effect<ExaContentsService, never, HttpClient.HttpClient>;
|
|
39
|
+
/**
|
|
40
|
+
* Layer registering both the provider-typed `ExaContents` tag and the generic
|
|
41
|
+
* `WebRead` tag over one implementation. A `CommonReadRequest` is structurally
|
|
42
|
+
* an `ExaContentsRequest` with the extra knobs unset, so the generic
|
|
43
|
+
* registration forwards directly.
|
|
44
|
+
*/
|
|
45
|
+
declare const layer: (cfg: Config) => Layer.Layer<ExaContents | WebRead, never, HttpClient.HttpClient>;
|
|
46
|
+
//#endregion
|
|
47
|
+
export { Config, ExaContents, ExaContentsRequest, ExaContentsService, layer, make, ExaContents_d_exports as t };
|
|
48
|
+
//# sourceMappingURL=ExaContents.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExaContents.d.mts","names":[],"sources":["../src/ExaContents.ts"],"mappings":";;;;;;;;;;;;;;;KAoBY,kBAAA,GAAqB,iBAAA;sEAEtB,SAAA,GAAY,YAAA;WAEZ,aAAA;AAAA;AAAA,KAGC,kBAAA;EAAA,SACD,IAAA,GAAO,OAAA,EAAS,kBAAA,KAAuB,MAAA,CAAO,MAAA,CAAO,YAAA,EAAc,OAAA,CAAQ,OAAA;AAAA;AAAA,cACrF,gBAAA;;;;;;cAOY,WAAA,SAAoB,gBAAA;AAAA,KAIrB,MAAA;EAAA,SACD,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SACjB,OAAA;AAAA;;;;;cAsIE,IAAA,GACX,GAAA,EAAK,MAAA,KACJ,MAAA,CAAO,MAAA,CAAO,kBAAA,SAA2B,UAAA,CAAW,UAAA;;;;;;;cAY1C,KAAA,GACX,GAAA,EAAK,MAAA,KACJ,KAAA,CAAM,KAAA,CAAM,WAAA,GAAc,OAAA,SAAgB,UAAA,CAAW,UAAA"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
|
|
2
|
+
import { Context, Effect, Layer, Redacted, Schema } from "effect";
|
|
3
|
+
import { HttpClient, HttpClientRequest } from "effect/unstable/http";
|
|
4
|
+
import * as AiError from "@effect-uai/core/AiError";
|
|
5
|
+
import { WebRead } from "@effect-uai/core/WebRead";
|
|
6
|
+
//#region src/ExaContents.ts
|
|
7
|
+
var ExaContents_exports = /* @__PURE__ */ __exportAll({
|
|
8
|
+
ExaContents: () => ExaContents,
|
|
9
|
+
layer: () => layer,
|
|
10
|
+
make: () => make
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* Provider-typed service tag. Yield this for Exa-typed reads (its own
|
|
14
|
+
* `/contents` knobs); yield the generic `WebRead` tag for provider-portable
|
|
15
|
+
* code. Both are registered by {@link layer}.
|
|
16
|
+
*/
|
|
17
|
+
var ExaContents = class extends Context.Service()("@betalyra/effect-uai/providers/exa/ExaContents") {};
|
|
18
|
+
const buildBody = (request) => {
|
|
19
|
+
const text = {
|
|
20
|
+
...request.maxCharacters !== void 0 && { maxCharacters: request.maxCharacters },
|
|
21
|
+
...request.format === "html" && { includeHtmlTags: true }
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
urls: [request.url],
|
|
25
|
+
text,
|
|
26
|
+
...request.livecrawl !== void 0 && { livecrawl: request.livecrawl }
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
const WireResult = Schema.Struct({
|
|
30
|
+
id: Schema.optional(Schema.String),
|
|
31
|
+
url: Schema.String,
|
|
32
|
+
title: Schema.optional(Schema.NullOr(Schema.String)),
|
|
33
|
+
text: Schema.optional(Schema.NullOr(Schema.String))
|
|
34
|
+
});
|
|
35
|
+
const WireStatus = Schema.Struct({
|
|
36
|
+
id: Schema.optional(Schema.String),
|
|
37
|
+
status: Schema.optional(Schema.String),
|
|
38
|
+
source: Schema.optional(Schema.String)
|
|
39
|
+
});
|
|
40
|
+
const WireResponse = Schema.Struct({
|
|
41
|
+
requestId: Schema.optional(Schema.String),
|
|
42
|
+
results: Schema.Array(WireResult),
|
|
43
|
+
statuses: Schema.optional(Schema.Array(WireStatus))
|
|
44
|
+
});
|
|
45
|
+
const toResponse = (wire, result) => {
|
|
46
|
+
const title = result.title ?? void 0;
|
|
47
|
+
return {
|
|
48
|
+
url: result.url,
|
|
49
|
+
content: result.text ?? "",
|
|
50
|
+
...title !== void 0 && { title },
|
|
51
|
+
raw: wire
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
const transportFailure = (cause) => new AiError.Unavailable({
|
|
55
|
+
provider: "exa",
|
|
56
|
+
raw: cause
|
|
57
|
+
});
|
|
58
|
+
const httpStatusError = (status, body) => {
|
|
59
|
+
const provider = "exa";
|
|
60
|
+
const raw = body;
|
|
61
|
+
if (status === 429) return new AiError.RateLimited({
|
|
62
|
+
provider,
|
|
63
|
+
raw
|
|
64
|
+
});
|
|
65
|
+
if (status === 408 || status === 504) return new AiError.Timeout({
|
|
66
|
+
provider,
|
|
67
|
+
raw
|
|
68
|
+
});
|
|
69
|
+
if (status === 401) return new AiError.AuthFailed({
|
|
70
|
+
provider,
|
|
71
|
+
subtype: "auth",
|
|
72
|
+
raw
|
|
73
|
+
});
|
|
74
|
+
if (status === 403) return new AiError.AuthFailed({
|
|
75
|
+
provider,
|
|
76
|
+
subtype: "permission",
|
|
77
|
+
raw
|
|
78
|
+
});
|
|
79
|
+
if (status === 402) return new AiError.AuthFailed({
|
|
80
|
+
provider,
|
|
81
|
+
subtype: "billing",
|
|
82
|
+
raw
|
|
83
|
+
});
|
|
84
|
+
if (status >= 500) return new AiError.Unavailable({
|
|
85
|
+
provider,
|
|
86
|
+
status,
|
|
87
|
+
raw
|
|
88
|
+
});
|
|
89
|
+
return new AiError.InvalidRequest({
|
|
90
|
+
provider,
|
|
91
|
+
raw
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
const baseUrl = (cfg) => cfg.baseUrl ?? "https://api.exa.ai";
|
|
95
|
+
const postContents = (cfg, body) => Effect.gen(function* () {
|
|
96
|
+
const client = yield* HttpClient.HttpClient;
|
|
97
|
+
const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/contents`).pipe(HttpClientRequest.setHeader("x-api-key", Redacted.value(cfg.apiKey)), HttpClientRequest.bodyJsonUnsafe(body));
|
|
98
|
+
const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure));
|
|
99
|
+
if (response.status >= 400) {
|
|
100
|
+
const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""));
|
|
101
|
+
return yield* httpStatusError(response.status, text);
|
|
102
|
+
}
|
|
103
|
+
const json = yield* response.json.pipe(Effect.mapError(transportFailure));
|
|
104
|
+
return yield* Schema.decodeUnknownEffect(WireResponse)(json).pipe(Effect.mapError(transportFailure));
|
|
105
|
+
});
|
|
106
|
+
const readImpl = (cfg) => (request) => Effect.gen(function* () {
|
|
107
|
+
const wire = yield* postContents(cfg, buildBody(request));
|
|
108
|
+
const result = wire.results[0];
|
|
109
|
+
if (result === void 0) return yield* new AiError.Unavailable({
|
|
110
|
+
provider: "exa",
|
|
111
|
+
raw: wire
|
|
112
|
+
});
|
|
113
|
+
return toResponse(wire, result);
|
|
114
|
+
});
|
|
115
|
+
/**
|
|
116
|
+
* Build an `ExaContentsService` value. For Layer-based setup, prefer
|
|
117
|
+
* {@link layer}.
|
|
118
|
+
*/
|
|
119
|
+
const make = (cfg) => Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({ read: (request) => readImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)) }));
|
|
120
|
+
/**
|
|
121
|
+
* Layer registering both the provider-typed `ExaContents` tag and the generic
|
|
122
|
+
* `WebRead` tag over one implementation. A `CommonReadRequest` is structurally
|
|
123
|
+
* an `ExaContentsRequest` with the extra knobs unset, so the generic
|
|
124
|
+
* registration forwards directly.
|
|
125
|
+
*/
|
|
126
|
+
const layer = (cfg) => {
|
|
127
|
+
const typed = Layer.effect(ExaContents, make(cfg));
|
|
128
|
+
const generic = Layer.effect(WebRead, Effect.map(make(cfg), (s) => ({ read: (request) => s.read(request) })));
|
|
129
|
+
return Layer.merge(typed, generic);
|
|
130
|
+
};
|
|
131
|
+
//#endregion
|
|
132
|
+
export { ExaContents, layer, make, ExaContents_exports as t };
|
|
133
|
+
|
|
134
|
+
//# sourceMappingURL=ExaContents.mjs.map
|
|
@@ -0,0 +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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"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import { ExaCategory, ExaSearchType } from "./models.mjs";
|
|
1
|
+
import { ExaCategory, ExaLivecrawl, ExaSearchType } from "./models.mjs";
|
|
2
|
+
import { t as ExaContents_d_exports } from "./ExaContents.mjs";
|
|
2
3
|
import { t as ExaSearch_d_exports } from "./ExaSearch.mjs";
|
|
3
|
-
export { ExaCategory, ExaSearch_d_exports as ExaSearch, ExaSearchType };
|
|
4
|
+
export { ExaCategory, ExaContents_d_exports as ExaContents, ExaLivecrawl, ExaSearch_d_exports as ExaSearch, ExaSearchType };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as ExaContents_exports } from "./ExaContents.mjs";
|
|
1
2
|
import { t as ExaSearch_exports } from "./ExaSearch.mjs";
|
|
2
3
|
import "./models.mjs";
|
|
3
|
-
export { ExaSearch_exports as ExaSearch };
|
|
4
|
+
export { ExaContents_exports as ExaContents, ExaSearch_exports as ExaSearch };
|
package/dist/models.d.mts
CHANGED
|
@@ -19,6 +19,12 @@ type ExaSearchType = "auto" | "fast" | "instant" | "deep-lite" | "deep" | "deep-
|
|
|
19
19
|
* Provider-specific, so it lives on `ExaSearchRequest`.
|
|
20
20
|
*/
|
|
21
21
|
type ExaCategory = "company" | "research paper" | "news" | "personal site" | "financial report" | "people" | (string & {});
|
|
22
|
+
/**
|
|
23
|
+
* Exa `/contents` freshness knob. Controls whether the page is fetched live
|
|
24
|
+
* or served from Exa's cache. Provider-specific, so it lives on
|
|
25
|
+
* `ExaContentsRequest`.
|
|
26
|
+
*/
|
|
27
|
+
type ExaLivecrawl = "never" | "fallback" | "preferred" | "always" | (string & {});
|
|
22
28
|
//#endregion
|
|
23
|
-
export { ExaCategory, ExaSearchType };
|
|
29
|
+
export { ExaCategory, ExaLivecrawl, ExaSearchType };
|
|
24
30
|
//# sourceMappingURL=models.d.mts.map
|
package/dist/models.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"models.d.mts","names":[],"sources":["../src/models.ts"],"mappings":";;AAcA;;;;;AAgBA
|
|
1
|
+
{"version":3,"file":"models.d.mts","names":[],"sources":["../src/models.ts"],"mappings":";;AAcA;;;;;AAgBA;;;;;AAeA;;;KA/BY,aAAA;;;;;KAgBA,WAAA;;;;;;KAeA,YAAA"}
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-uai/exa",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Exa provider for @effect-uai/core (web search).",
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "Exa provider for @effect-uai/core (web search, web read).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
7
7
|
"effect",
|
|
8
8
|
"exa",
|
|
9
9
|
"search",
|
|
10
|
+
"web-read",
|
|
10
11
|
"web-search"
|
|
11
12
|
],
|
|
12
13
|
"homepage": "https://github.com/betalyra/effect-uai/tree/main/packages/providers/exa",
|
|
@@ -34,6 +35,10 @@
|
|
|
34
35
|
"types": "./dist/index.d.mts",
|
|
35
36
|
"import": "./dist/index.mjs"
|
|
36
37
|
},
|
|
38
|
+
"./ExaContents": {
|
|
39
|
+
"types": "./dist/ExaContents.d.mts",
|
|
40
|
+
"import": "./dist/ExaContents.mjs"
|
|
41
|
+
},
|
|
37
42
|
"./ExaSearch": {
|
|
38
43
|
"types": "./dist/ExaSearch.d.mts",
|
|
39
44
|
"import": "./dist/ExaSearch.mjs"
|
|
@@ -45,7 +50,7 @@
|
|
|
45
50
|
"devDependencies": {
|
|
46
51
|
"effect": "4.0.0-beta.57",
|
|
47
52
|
"typescript": "^6.0.3",
|
|
48
|
-
"@effect-uai/core": "0.
|
|
53
|
+
"@effect-uai/core": "0.10.0"
|
|
49
54
|
},
|
|
50
55
|
"peerDependencies": {
|
|
51
56
|
"@effect-uai/core": ">=0.2.0 <1",
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { Context, Effect, Layer, Redacted, Schema } from "effect"
|
|
2
|
+
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
|
|
3
|
+
import * as AiError from "@effect-uai/core/AiError"
|
|
4
|
+
import {
|
|
5
|
+
type CommonReadRequest,
|
|
6
|
+
type ReadResponse,
|
|
7
|
+
WebRead,
|
|
8
|
+
type WebReadService,
|
|
9
|
+
} from "@effect-uai/core/WebRead"
|
|
10
|
+
import type { ExaLivecrawl } from "./models.js"
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Public types
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Exa-typed read request. Widens {@link CommonReadRequest} with Exa
|
|
18
|
+
* `/contents` knobs; the common fields map onto Exa's wire body (see the
|
|
19
|
+
* codec below).
|
|
20
|
+
*/
|
|
21
|
+
export type ExaContentsRequest = CommonReadRequest & {
|
|
22
|
+
/** Freshness / cache policy - see {@link ExaLivecrawl}. */
|
|
23
|
+
readonly livecrawl?: ExaLivecrawl
|
|
24
|
+
/** Cap on the returned content length (Exa `text.maxCharacters`). */
|
|
25
|
+
readonly maxCharacters?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type ExaContentsService = {
|
|
29
|
+
readonly read: (request: ExaContentsRequest) => Effect.Effect<ReadResponse, AiError.AiError>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Provider-typed service tag. Yield this for Exa-typed reads (its own
|
|
34
|
+
* `/contents` knobs); yield the generic `WebRead` tag for provider-portable
|
|
35
|
+
* code. Both are registered by {@link layer}.
|
|
36
|
+
*/
|
|
37
|
+
export class ExaContents extends Context.Service<ExaContents, ExaContentsService>()(
|
|
38
|
+
"@betalyra/effect-uai/providers/exa/ExaContents",
|
|
39
|
+
) {}
|
|
40
|
+
|
|
41
|
+
export type Config = {
|
|
42
|
+
readonly apiKey: Redacted.Redacted
|
|
43
|
+
readonly baseUrl?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Codec - request body
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
type WireText = {
|
|
51
|
+
readonly maxCharacters?: number
|
|
52
|
+
readonly includeHtmlTags?: boolean
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type WireBody = {
|
|
56
|
+
readonly urls: ReadonlyArray<string>
|
|
57
|
+
readonly text: WireText
|
|
58
|
+
readonly livecrawl?: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Exa returns markdown by default; `includeHtmlTags: true` returns HTML tags
|
|
62
|
+
// instead. Both land on the single `text` field of the result.
|
|
63
|
+
const buildBody = (request: ExaContentsRequest): WireBody => {
|
|
64
|
+
const text: WireText = {
|
|
65
|
+
...(request.maxCharacters !== undefined && { maxCharacters: request.maxCharacters }),
|
|
66
|
+
...(request.format === "html" && { includeHtmlTags: true }),
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
urls: [request.url],
|
|
70
|
+
text,
|
|
71
|
+
...(request.livecrawl !== undefined && { livecrawl: request.livecrawl }),
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Codec - response
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
const WireResult = Schema.Struct({
|
|
80
|
+
id: Schema.optional(Schema.String),
|
|
81
|
+
url: Schema.String,
|
|
82
|
+
title: Schema.optional(Schema.NullOr(Schema.String)),
|
|
83
|
+
text: Schema.optional(Schema.NullOr(Schema.String)),
|
|
84
|
+
})
|
|
85
|
+
type WireResult = typeof WireResult.Type
|
|
86
|
+
|
|
87
|
+
const WireStatus = Schema.Struct({
|
|
88
|
+
id: Schema.optional(Schema.String),
|
|
89
|
+
status: Schema.optional(Schema.String),
|
|
90
|
+
source: Schema.optional(Schema.String),
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const WireResponse = Schema.Struct({
|
|
94
|
+
requestId: Schema.optional(Schema.String),
|
|
95
|
+
results: Schema.Array(WireResult),
|
|
96
|
+
statuses: Schema.optional(Schema.Array(WireStatus)),
|
|
97
|
+
})
|
|
98
|
+
type WireResponse = typeof WireResponse.Type
|
|
99
|
+
|
|
100
|
+
const toResponse = (wire: WireResponse, result: WireResult): ReadResponse => {
|
|
101
|
+
const title = result.title ?? undefined
|
|
102
|
+
return {
|
|
103
|
+
url: result.url,
|
|
104
|
+
content: result.text ?? "",
|
|
105
|
+
...(title !== undefined && { title }),
|
|
106
|
+
raw: wire,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// HTTP
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
const transportFailure = (cause: unknown): AiError.AiError =>
|
|
115
|
+
new AiError.Unavailable({ provider: "exa", raw: cause })
|
|
116
|
+
|
|
117
|
+
const httpStatusError = (status: number, body: string): AiError.AiError => {
|
|
118
|
+
const provider = "exa"
|
|
119
|
+
const raw = body
|
|
120
|
+
if (status === 429) return new AiError.RateLimited({ provider, raw })
|
|
121
|
+
if (status === 408 || status === 504) return new AiError.Timeout({ provider, raw })
|
|
122
|
+
if (status === 401) return new AiError.AuthFailed({ provider, subtype: "auth", raw })
|
|
123
|
+
if (status === 403) return new AiError.AuthFailed({ provider, subtype: "permission", raw })
|
|
124
|
+
if (status === 402) return new AiError.AuthFailed({ provider, subtype: "billing", raw })
|
|
125
|
+
if (status >= 500) return new AiError.Unavailable({ provider, status, raw })
|
|
126
|
+
return new AiError.InvalidRequest({ provider, raw })
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const baseUrl = (cfg: Config): string => cfg.baseUrl ?? "https://api.exa.ai"
|
|
130
|
+
|
|
131
|
+
const postContents = (
|
|
132
|
+
cfg: Config,
|
|
133
|
+
body: WireBody,
|
|
134
|
+
): Effect.Effect<WireResponse, AiError.AiError, HttpClient.HttpClient> =>
|
|
135
|
+
Effect.gen(function* () {
|
|
136
|
+
const client = yield* HttpClient.HttpClient
|
|
137
|
+
const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/contents`).pipe(
|
|
138
|
+
HttpClientRequest.setHeader("x-api-key", Redacted.value(cfg.apiKey)),
|
|
139
|
+
HttpClientRequest.bodyJsonUnsafe(body),
|
|
140
|
+
)
|
|
141
|
+
const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))
|
|
142
|
+
if (response.status >= 400) {
|
|
143
|
+
const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""))
|
|
144
|
+
return yield* httpStatusError(response.status, text)
|
|
145
|
+
}
|
|
146
|
+
const json = yield* response.json.pipe(Effect.mapError(transportFailure))
|
|
147
|
+
return yield* Schema.decodeUnknownEffect(WireResponse)(json).pipe(
|
|
148
|
+
Effect.mapError(transportFailure),
|
|
149
|
+
)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
const readImpl =
|
|
153
|
+
(cfg: Config) =>
|
|
154
|
+
(
|
|
155
|
+
request: ExaContentsRequest,
|
|
156
|
+
): Effect.Effect<ReadResponse, AiError.AiError, HttpClient.HttpClient> =>
|
|
157
|
+
Effect.gen(function* () {
|
|
158
|
+
const wire = yield* postContents(cfg, buildBody(request))
|
|
159
|
+
// Exa reports per-URL crawl failures in `results` / `statuses` rather
|
|
160
|
+
// than an HTTP error; a missing first result means the single URL could
|
|
161
|
+
// not be read.
|
|
162
|
+
const result = wire.results[0]
|
|
163
|
+
if (result === undefined) {
|
|
164
|
+
return yield* new AiError.Unavailable({ provider: "exa", raw: wire })
|
|
165
|
+
}
|
|
166
|
+
return toResponse(wire, result)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// Constructors
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Build an `ExaContentsService` value. For Layer-based setup, prefer
|
|
175
|
+
* {@link layer}.
|
|
176
|
+
*/
|
|
177
|
+
export const make = (
|
|
178
|
+
cfg: Config,
|
|
179
|
+
): Effect.Effect<ExaContentsService, never, HttpClient.HttpClient> =>
|
|
180
|
+
Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({
|
|
181
|
+
read: (request) =>
|
|
182
|
+
readImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),
|
|
183
|
+
}))
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Layer registering both the provider-typed `ExaContents` tag and the generic
|
|
187
|
+
* `WebRead` tag over one implementation. A `CommonReadRequest` is structurally
|
|
188
|
+
* an `ExaContentsRequest` with the extra knobs unset, so the generic
|
|
189
|
+
* registration forwards directly.
|
|
190
|
+
*/
|
|
191
|
+
export const layer = (
|
|
192
|
+
cfg: Config,
|
|
193
|
+
): Layer.Layer<ExaContents | WebRead, never, HttpClient.HttpClient> => {
|
|
194
|
+
const typed = Layer.effect(ExaContents, make(cfg))
|
|
195
|
+
const generic = Layer.effect(
|
|
196
|
+
WebRead,
|
|
197
|
+
Effect.map(make(cfg), (s): WebReadService => ({ read: (request) => s.read(request) })),
|
|
198
|
+
)
|
|
199
|
+
return Layer.merge(typed, generic)
|
|
200
|
+
}
|
package/src/index.ts
CHANGED
package/src/models.ts
CHANGED
|
@@ -37,3 +37,16 @@ export type ExaCategory =
|
|
|
37
37
|
| "people"
|
|
38
38
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
39
39
|
| (string & {})
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Exa `/contents` freshness knob. Controls whether the page is fetched live
|
|
43
|
+
* or served from Exa's cache. Provider-specific, so it lives on
|
|
44
|
+
* `ExaContentsRequest`.
|
|
45
|
+
*/
|
|
46
|
+
export type ExaLivecrawl =
|
|
47
|
+
| "never"
|
|
48
|
+
| "fallback"
|
|
49
|
+
| "preferred"
|
|
50
|
+
| "always"
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
52
|
+
| (string & {})
|