@effect-uai/exa 0.9.0 → 0.11.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/ExaDeepResearch.d.mts +70 -0
- package/dist/ExaDeepResearch.d.mts.map +1 -0
- package/dist/ExaDeepResearch.mjs +183 -0
- package/dist/ExaDeepResearch.mjs.map +1 -0
- package/dist/ExaSearch.mjs +1 -1
- package/dist/ExaSearch.mjs.map +1 -1
- package/dist/index.d.mts +4 -2
- package/dist/index.mjs +3 -1
- package/dist/models.d.mts +19 -1
- package/dist/models.d.mts.map +1 -1
- package/package.json +14 -5
- package/src/ExaContents.ts +200 -0
- package/src/ExaDeepResearch.ts +294 -0
- package/src/ExaSearch.ts +1 -1
- package/src/index.ts +2 -0
- package/src/models.ts +31 -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, (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, (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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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { ExaResearchModel } from "./models.mjs";
|
|
2
|
+
import { Context, Effect, Layer, Redacted, Schema } from "effect";
|
|
3
|
+
import { HttpClient } from "effect/unstable/http";
|
|
4
|
+
import { DeepResearch, DeepResearchServiceShape } from "@effect-uai/core/DeepResearch";
|
|
5
|
+
import * as Job from "@effect-uai/core/Job";
|
|
6
|
+
import * as StructuredFormat from "@effect-uai/core/StructuredFormat";
|
|
7
|
+
import { ResearchRequest, ResearchState } from "@effect-uai/core/Research";
|
|
8
|
+
|
|
9
|
+
//#region src/ExaDeepResearch.d.ts
|
|
10
|
+
declare namespace ExaDeepResearch_d_exports {
|
|
11
|
+
export { Config, ExaDeepResearch, ExaDeepResearchService, ExaResearchRequest, WireResearch, jobStateOf, layer, make };
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Exa-typed research request. Narrows `model` and adds `outputSchema`.
|
|
15
|
+
* Provider-portable code uses the generic `DeepResearch` tag instead.
|
|
16
|
+
*/
|
|
17
|
+
type ExaResearchRequest = Omit<ResearchRequest, "model"> & {
|
|
18
|
+
readonly model?: ExaResearchModel;
|
|
19
|
+
/**
|
|
20
|
+
* Optional structured output. When set, Exa researches the web and returns
|
|
21
|
+
* JSON matching the schema (Draft 7) instead of a prose report, with
|
|
22
|
+
* field-level citations. The completed `Turn`'s text is that JSON, so decode
|
|
23
|
+
* it with `Turn.decodeStructured(turn, outputSchema)` — the same path as
|
|
24
|
+
* `LanguageModel` structured output.
|
|
25
|
+
*/
|
|
26
|
+
readonly outputSchema?: StructuredFormat.StructuredFormat<unknown>;
|
|
27
|
+
};
|
|
28
|
+
type ExaDeepResearchService = DeepResearchServiceShape<ExaResearchRequest>;
|
|
29
|
+
declare const ExaDeepResearch_base: Context.ServiceClass<ExaDeepResearch, "@betalyra/effect-uai/providers/exa/ExaDeepResearch", ExaDeepResearchService>;
|
|
30
|
+
/**
|
|
31
|
+
* Provider-typed tag for Exa deep research over the async task API
|
|
32
|
+
* (`POST /research/v0/tasks` create + `GET /research/v0/tasks/{id}` poll). Yield
|
|
33
|
+
* this for the model / `outputSchema` knobs; yield the generic `DeepResearch`
|
|
34
|
+
* tag for portable code. Poll-based, so `researchStream` / `streamFrom` are the
|
|
35
|
+
* synthesized default {@link fromJob} builds.
|
|
36
|
+
*/
|
|
37
|
+
declare class ExaDeepResearch extends ExaDeepResearch_base {}
|
|
38
|
+
type Config = {
|
|
39
|
+
readonly apiKey: Redacted.Redacted;
|
|
40
|
+
readonly baseUrl?: string; /** Poll cadence / overall timeout for the derived poll loops. */
|
|
41
|
+
readonly job?: Job.JobConfig;
|
|
42
|
+
};
|
|
43
|
+
declare const WireResearch: Schema.Struct<{
|
|
44
|
+
readonly id: Schema.optional<Schema.NullOr<Schema.String>>;
|
|
45
|
+
readonly status: Schema.optional<Schema.NullOr<Schema.String>>;
|
|
46
|
+
readonly schema: Schema.optional<Schema.NullOr<Schema.Unknown>>;
|
|
47
|
+
readonly data: Schema.optional<Schema.NullOr<Schema.$Record<Schema.String, Schema.Unknown>>>;
|
|
48
|
+
readonly citations: Schema.optional<Schema.NullOr<Schema.$Record<Schema.String, Schema.$Array<Schema.Struct<{
|
|
49
|
+
readonly id: Schema.optional<Schema.NullOr<Schema.String>>;
|
|
50
|
+
readonly url: Schema.optional<Schema.NullOr<Schema.String>>;
|
|
51
|
+
readonly title: Schema.optional<Schema.NullOr<Schema.String>>;
|
|
52
|
+
readonly snippet: Schema.optional<Schema.NullOr<Schema.String>>;
|
|
53
|
+
}>>>>>;
|
|
54
|
+
readonly error: Schema.optional<Schema.NullOr<Schema.Struct<{
|
|
55
|
+
readonly message: Schema.optional<Schema.NullOr<Schema.String>>;
|
|
56
|
+
}>>>;
|
|
57
|
+
}>;
|
|
58
|
+
type WireResearch = typeof WireResearch.Type;
|
|
59
|
+
declare const jobStateOf: (wire: WireResearch) => ResearchState;
|
|
60
|
+
/** Build an `ExaDeepResearchService`. For Layer setup, prefer {@link layer}. */
|
|
61
|
+
declare const make: (cfg: Config) => Effect.Effect<ExaDeepResearchService, never, HttpClient.HttpClient>;
|
|
62
|
+
/**
|
|
63
|
+
* Layer registering the provider-typed `ExaDeepResearch` tag and the generic
|
|
64
|
+
* `DeepResearch` tag. No capability markers: the surface (poll, collect,
|
|
65
|
+
* synthesized stream) is universal.
|
|
66
|
+
*/
|
|
67
|
+
declare const layer: (cfg: Config) => Layer.Layer<ExaDeepResearch | DeepResearch, never, HttpClient.HttpClient>;
|
|
68
|
+
//#endregion
|
|
69
|
+
export { Config, ExaDeepResearch, ExaDeepResearchService, ExaResearchRequest, WireResearch, jobStateOf, layer, make, ExaDeepResearch_d_exports as t };
|
|
70
|
+
//# sourceMappingURL=ExaDeepResearch.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExaDeepResearch.d.mts","names":[],"sources":["../src/ExaDeepResearch.ts"],"mappings":";;;;;;;;;;;;;;;;KAyBY,kBAAA,GAAqB,IAAA,CAAK,eAAA;EAAA,SAC3B,KAAA,GAAQ,gBAAA;;;;;;;;WAQR,YAAA,GAAe,gBAAA,CAAiB,gBAAA;AAAA;AAAA,KAG/B,sBAAA,GAAyB,wBAAA,CAAyB,kBAAA;AAAA,cAAmB,oBAAA;;;;;;;;cASpE,eAAA,SAAwB,oBAAA;AAAA,KAIzB,MAAA;EAAA,SACD,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SACjB,OAAA,WAlBgD;EAAA,SAoBhD,GAAA,GAAM,GAAA,CAAI,SAAA;AAAA;AAAA,cAwBR,YAAA,EAAY,MAAA,CAAA,MAAA;EAAA;;;;;;;;;;;;;;KAQb,YAAA,UAAsB,YAAA,CAAa,IAAA;AAAA,cAoDlC,UAAA,GAAc,IAAA,EAAM,YAAA,KAAe,aAAA;;cA8HnC,IAAA,GACX,GAAA,EAAK,MAAA,KACJ,MAAA,CAAO,MAAA,CAAO,sBAAA,SAA+B,UAAA,CAAW,UAAA;;;;;AAxN3D;cAgOa,KAAA,GACX,GAAA,EAAK,MAAA,KACJ,KAAA,CAAM,KAAA,CAAM,eAAA,GAAkB,YAAA,SAAqB,UAAA,CAAW,UAAA"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
|
|
2
|
+
import { Context, Effect, Layer, Match, Redacted, Schema } from "effect";
|
|
3
|
+
import { HttpClient, HttpClientRequest } from "effect/unstable/http";
|
|
4
|
+
import * as AiError from "@effect-uai/core/AiError";
|
|
5
|
+
import { DeepResearch, fromJob } from "@effect-uai/core/DeepResearch";
|
|
6
|
+
import * as Items from "@effect-uai/core/Items";
|
|
7
|
+
import * as Job from "@effect-uai/core/Job";
|
|
8
|
+
//#region src/ExaDeepResearch.ts
|
|
9
|
+
var ExaDeepResearch_exports = /* @__PURE__ */ __exportAll({
|
|
10
|
+
ExaDeepResearch: () => ExaDeepResearch,
|
|
11
|
+
WireResearch: () => WireResearch,
|
|
12
|
+
jobStateOf: () => jobStateOf,
|
|
13
|
+
layer: () => layer,
|
|
14
|
+
make: () => make
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Provider-typed tag for Exa deep research over the async task API
|
|
18
|
+
* (`POST /research/v0/tasks` create + `GET /research/v0/tasks/{id}` poll). Yield
|
|
19
|
+
* this for the model / `outputSchema` knobs; yield the generic `DeepResearch`
|
|
20
|
+
* tag for portable code. Poll-based, so `researchStream` / `streamFrom` are the
|
|
21
|
+
* synthesized default {@link fromJob} builds.
|
|
22
|
+
*/
|
|
23
|
+
var ExaDeepResearch = class extends Context.Service()("@betalyra/effect-uai/providers/exa/ExaDeepResearch") {};
|
|
24
|
+
const WireSubmit = Schema.Struct({ id: Schema.String });
|
|
25
|
+
const decodeSubmit = Schema.decodeUnknownEffect(WireSubmit);
|
|
26
|
+
const OptionalText = Schema.optional(Schema.NullOr(Schema.String));
|
|
27
|
+
const WireSource = Schema.Struct({
|
|
28
|
+
id: OptionalText,
|
|
29
|
+
url: OptionalText,
|
|
30
|
+
title: OptionalText,
|
|
31
|
+
snippet: OptionalText
|
|
32
|
+
});
|
|
33
|
+
const WireResearch = Schema.Struct({
|
|
34
|
+
id: OptionalText,
|
|
35
|
+
status: OptionalText,
|
|
36
|
+
schema: Schema.optional(Schema.NullOr(Schema.Unknown)),
|
|
37
|
+
data: Schema.optional(Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown))),
|
|
38
|
+
citations: Schema.optional(Schema.NullOr(Schema.Record(Schema.String, Schema.Array(WireSource)))),
|
|
39
|
+
error: Schema.optional(Schema.NullOr(Schema.Struct({ message: OptionalText })))
|
|
40
|
+
});
|
|
41
|
+
const decodeResearch = Schema.decodeUnknownEffect(WireResearch);
|
|
42
|
+
const reportText = (wire) => {
|
|
43
|
+
if (wire.schema != null) return JSON.stringify(wire.data ?? {});
|
|
44
|
+
const answer = wire.data?.answer;
|
|
45
|
+
return typeof answer === "string" ? answer : "";
|
|
46
|
+
};
|
|
47
|
+
const citationsToAnnotations = (wire) => Object.values(wire.citations ?? {}).flat().flatMap((c, i) => c.url == null ? [] : [{
|
|
48
|
+
type: "url_citation",
|
|
49
|
+
url: c.url,
|
|
50
|
+
title: c.title ?? c.url,
|
|
51
|
+
marker: i + 1,
|
|
52
|
+
...c.snippet != null && { cited_text: c.snippet }
|
|
53
|
+
}]);
|
|
54
|
+
const turnFromResearch = (wire) => {
|
|
55
|
+
const annotations = citationsToAnnotations(wire);
|
|
56
|
+
return {
|
|
57
|
+
items: [{
|
|
58
|
+
type: "message",
|
|
59
|
+
role: "assistant",
|
|
60
|
+
content: [{
|
|
61
|
+
type: "output_text",
|
|
62
|
+
text: reportText(wire),
|
|
63
|
+
...annotations.length > 0 && { annotations }
|
|
64
|
+
}],
|
|
65
|
+
providerData: wire
|
|
66
|
+
}],
|
|
67
|
+
usage: {},
|
|
68
|
+
stop_reason: "stop"
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
const jobStateOf = (wire) => Match.value(wire.status ?? "").pipe(Match.whenOr("pending", "queued", () => ({ _tag: "Pending" })), Match.whenOr("running", "in_progress", () => ({ _tag: "Running" })), Match.when("completed", () => ({
|
|
72
|
+
_tag: "Succeeded",
|
|
73
|
+
result: turnFromResearch(wire)
|
|
74
|
+
})), Match.whenOr("failed", "canceled", "cancelled", () => ({
|
|
75
|
+
_tag: "Failed",
|
|
76
|
+
...wire.error?.message != null && { reason: wire.error.message },
|
|
77
|
+
raw: wire
|
|
78
|
+
})), Match.orElse(() => ({ _tag: "Running" })));
|
|
79
|
+
const baseUrl = (cfg) => cfg.baseUrl ?? "https://api.exa.ai";
|
|
80
|
+
const authHeader = (cfg) => HttpClientRequest.setHeader("x-api-key", Redacted.value(cfg.apiKey));
|
|
81
|
+
const transportFailure = (cause) => new AiError.Unavailable({
|
|
82
|
+
provider: "exa",
|
|
83
|
+
raw: cause
|
|
84
|
+
});
|
|
85
|
+
const httpStatusError = (status, body) => {
|
|
86
|
+
const provider = "exa";
|
|
87
|
+
const raw = body;
|
|
88
|
+
if (status === 429) return new AiError.RateLimited({
|
|
89
|
+
provider,
|
|
90
|
+
raw
|
|
91
|
+
});
|
|
92
|
+
if (status === 408 || status === 504) return new AiError.Timeout({
|
|
93
|
+
provider,
|
|
94
|
+
raw
|
|
95
|
+
});
|
|
96
|
+
if (status === 401) return new AiError.AuthFailed({
|
|
97
|
+
provider,
|
|
98
|
+
subtype: "auth",
|
|
99
|
+
raw
|
|
100
|
+
});
|
|
101
|
+
if (status === 403) return new AiError.AuthFailed({
|
|
102
|
+
provider,
|
|
103
|
+
subtype: "permission",
|
|
104
|
+
raw
|
|
105
|
+
});
|
|
106
|
+
if (status === 402) return new AiError.AuthFailed({
|
|
107
|
+
provider,
|
|
108
|
+
subtype: "billing",
|
|
109
|
+
raw
|
|
110
|
+
});
|
|
111
|
+
if (status >= 500) return new AiError.Unavailable({
|
|
112
|
+
provider,
|
|
113
|
+
status,
|
|
114
|
+
raw
|
|
115
|
+
});
|
|
116
|
+
return new AiError.InvalidRequest({
|
|
117
|
+
provider,
|
|
118
|
+
raw
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
const instructions = (history) => history.filter(Items.isMessage).filter((m) => m.role === "user").flatMap((m) => m.content).filter(Items.isInputText).map((b) => b.text).join("\n");
|
|
122
|
+
const submitBody = (request) => ({
|
|
123
|
+
instructions: instructions(request.history),
|
|
124
|
+
model: request.model ?? "exa-research",
|
|
125
|
+
...request.outputSchema !== void 0 && { output: { schema: request.outputSchema.schema["~standard"].jsonSchema.input({ target: "draft-07" }) } }
|
|
126
|
+
});
|
|
127
|
+
const submitJob = (cfg, request) => Effect.gen(function* () {
|
|
128
|
+
const client = yield* HttpClient.HttpClient;
|
|
129
|
+
const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/research/v0/tasks`).pipe(authHeader(cfg), HttpClientRequest.bodyJsonUnsafe(submitBody(request)));
|
|
130
|
+
const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure));
|
|
131
|
+
if (response.status >= 400) {
|
|
132
|
+
const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""));
|
|
133
|
+
return yield* httpStatusError(response.status, text);
|
|
134
|
+
}
|
|
135
|
+
const wire = yield* decodeSubmit(yield* response.json.pipe(Effect.mapError(transportFailure))).pipe(Effect.mapError(transportFailure));
|
|
136
|
+
return Job.jobRef("exa", wire.id);
|
|
137
|
+
});
|
|
138
|
+
const pollJob = (cfg, ref) => Effect.gen(function* () {
|
|
139
|
+
const client = yield* HttpClient.HttpClient;
|
|
140
|
+
const httpRequest = HttpClientRequest.get(`${baseUrl(cfg)}/research/v0/tasks/${ref.id}`).pipe(authHeader(cfg));
|
|
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
|
+
return jobStateOf(yield* decodeResearch(yield* response.json.pipe(Effect.mapError(transportFailure))).pipe(Effect.mapError(transportFailure)));
|
|
147
|
+
});
|
|
148
|
+
const unsupported = (capability) => new AiError.Unsupported({
|
|
149
|
+
provider: "exa",
|
|
150
|
+
capability,
|
|
151
|
+
reason: "Exa's research task API exposes no verified cancel endpoint."
|
|
152
|
+
});
|
|
153
|
+
const jobOps = (cfg, client) => {
|
|
154
|
+
const withClient = (e) => Effect.provideService(e, HttpClient.HttpClient, client);
|
|
155
|
+
return {
|
|
156
|
+
submit: (request) => withClient(submitJob(cfg, request)),
|
|
157
|
+
poll: (ref) => withClient(pollJob(cfg, ref)),
|
|
158
|
+
cancel: () => Effect.fail(unsupported("cancel"))
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
/** Build an `ExaDeepResearchService`. For Layer setup, prefer {@link layer}. */
|
|
162
|
+
const make = (cfg) => Effect.map(HttpClient.HttpClient, (client) => fromJob(jobOps(cfg, client), cfg.job));
|
|
163
|
+
/**
|
|
164
|
+
* Layer registering the provider-typed `ExaDeepResearch` tag and the generic
|
|
165
|
+
* `DeepResearch` tag. No capability markers: the surface (poll, collect,
|
|
166
|
+
* synthesized stream) is universal.
|
|
167
|
+
*/
|
|
168
|
+
const layer = (cfg) => {
|
|
169
|
+
const typed = Layer.effect(ExaDeepResearch, make(cfg));
|
|
170
|
+
const generic = Layer.effect(DeepResearch, Effect.map(HttpClient.HttpClient, (client) => {
|
|
171
|
+
const ops = jobOps(cfg, client);
|
|
172
|
+
return fromJob({
|
|
173
|
+
submit: (request) => ops.submit(request),
|
|
174
|
+
poll: ops.poll,
|
|
175
|
+
cancel: ops.cancel
|
|
176
|
+
}, cfg.job);
|
|
177
|
+
}));
|
|
178
|
+
return Layer.merge(typed, generic);
|
|
179
|
+
};
|
|
180
|
+
//#endregion
|
|
181
|
+
export { ExaDeepResearch, WireResearch, jobStateOf, layer, make, ExaDeepResearch_exports as t };
|
|
182
|
+
|
|
183
|
+
//# sourceMappingURL=ExaDeepResearch.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExaDeepResearch.mjs","names":[],"sources":["../src/ExaDeepResearch.ts"],"sourcesContent":["import { Context, Effect, Layer, Match, Redacted, Schema } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport {\n DeepResearch,\n type DeepResearchService,\n type DeepResearchServiceShape,\n fromJob,\n type ResearchJobOps,\n} from \"@effect-uai/core/DeepResearch\"\nimport * as Items from \"@effect-uai/core/Items\"\nimport * as Job from \"@effect-uai/core/Job\"\nimport * as StructuredFormat from \"@effect-uai/core/StructuredFormat\"\nimport type { ResearchJobRef, ResearchRequest, ResearchState } from \"@effect-uai/core/Research\"\nimport type { Turn } from \"@effect-uai/core/Turn\"\nimport type { ExaResearchModel } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Exa-typed research request. Narrows `model` and adds `outputSchema`.\n * Provider-portable code uses the generic `DeepResearch` tag instead.\n */\nexport type ExaResearchRequest = Omit<ResearchRequest, \"model\"> & {\n readonly model?: ExaResearchModel\n /**\n * Optional structured output. When set, Exa researches the web and returns\n * JSON matching the schema (Draft 7) instead of a prose report, with\n * field-level citations. The completed `Turn`'s text is that JSON, so decode\n * it with `Turn.decodeStructured(turn, outputSchema)` — the same path as\n * `LanguageModel` structured output.\n */\n readonly outputSchema?: StructuredFormat.StructuredFormat<unknown>\n}\n\nexport type ExaDeepResearchService = DeepResearchServiceShape<ExaResearchRequest>\n\n/**\n * Provider-typed tag for Exa deep research over the async task API\n * (`POST /research/v0/tasks` create + `GET /research/v0/tasks/{id}` poll). Yield\n * this for the model / `outputSchema` knobs; yield the generic `DeepResearch`\n * tag for portable code. Poll-based, so `researchStream` / `streamFrom` are the\n * synthesized default {@link fromJob} builds.\n */\nexport class ExaDeepResearch extends Context.Service<ExaDeepResearch, ExaDeepResearchService>()(\n \"@betalyra/effect-uai/providers/exa/ExaDeepResearch\",\n) {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n /** Poll cadence / overall timeout for the derived poll loops. */\n readonly job?: Job.JobConfig\n}\n\n// ---------------------------------------------------------------------------\n// Wire codec\n// ---------------------------------------------------------------------------\n\nconst WireSubmit = Schema.Struct({ id: Schema.String })\nconst decodeSubmit = Schema.decodeUnknownEffect(WireSubmit)\n\nconst OptionalText = Schema.optional(Schema.NullOr(Schema.String))\n\n// A source backing one data field. `id` is Exa's dedup key (usually the url).\nconst WireSource = Schema.Struct({\n id: OptionalText,\n url: OptionalText,\n title: OptionalText,\n snippet: OptionalText,\n})\n\n// `data` holds the results: `{ answer }` for a prose task, or the schema-shaped\n// object when an `outputSchema` was given. `schema` (non-null) is the reliable\n// discriminator. `citations` is keyed by data-field path, each a list of\n// sources. Reference: exa-labs/openapi-spec `/research/v0/tasks`.\nexport const WireResearch = Schema.Struct({\n id: OptionalText,\n status: OptionalText,\n schema: Schema.optional(Schema.NullOr(Schema.Unknown)),\n data: Schema.optional(Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown))),\n citations: Schema.optional(Schema.NullOr(Schema.Record(Schema.String, Schema.Array(WireSource)))),\n error: Schema.optional(Schema.NullOr(Schema.Struct({ message: OptionalText }))),\n})\nexport type WireResearch = typeof WireResearch.Type\nconst decodeResearch = Schema.decodeUnknownEffect(WireResearch)\n\n// Structured → the whole `data` object serialized (decoded outside via\n// `Turn.decodeStructured`). Prose → `data.answer`.\nconst reportText = (wire: WireResearch): string => {\n if (wire.schema != null) return JSON.stringify(wire.data ?? {})\n const answer = wire.data?.answer\n return typeof answer === \"string\" ? answer : \"\"\n}\n\n// Flatten the field-keyed citations into the flat `Annotation` list. The\n// per-field grouping is lost in this view; the raw wire stays on `providerData`.\nconst citationsToAnnotations = (wire: WireResearch): ReadonlyArray<Items.Annotation> =>\n Object.values(wire.citations ?? {})\n .flat()\n .flatMap((c, i) =>\n c.url == null\n ? []\n : [\n {\n type: \"url_citation\" as const,\n url: c.url,\n title: c.title ?? c.url,\n marker: i + 1,\n ...(c.snippet != null && { cited_text: c.snippet }),\n },\n ],\n )\n\nconst turnFromResearch = (wire: WireResearch): Turn => {\n const annotations = citationsToAnnotations(wire)\n return {\n items: [\n {\n type: \"message\",\n role: \"assistant\",\n content: [\n {\n type: \"output_text\",\n text: reportText(wire),\n ...(annotations.length > 0 && { annotations }),\n },\n ],\n providerData: wire,\n },\n ],\n usage: {},\n stop_reason: \"stop\",\n }\n}\n\nexport const jobStateOf = (wire: WireResearch): ResearchState =>\n Match.value(wire.status ?? \"\").pipe(\n Match.whenOr(\"pending\", \"queued\", (): ResearchState => ({ _tag: \"Pending\" })),\n Match.whenOr(\"running\", \"in_progress\", (): ResearchState => ({ _tag: \"Running\" })),\n Match.when(\n \"completed\",\n (): ResearchState => ({ _tag: \"Succeeded\", result: turnFromResearch(wire) }),\n ),\n Match.whenOr(\n \"failed\",\n \"canceled\",\n \"cancelled\",\n (): ResearchState => ({\n _tag: \"Failed\",\n ...(wire.error?.message != null && { reason: wire.error.message }),\n raw: wire,\n }),\n ),\n Match.orElse((): ResearchState => ({ _tag: \"Running\" })),\n )\n\n// ---------------------------------------------------------------------------\n// HTTP\n// ---------------------------------------------------------------------------\n\nconst baseUrl = (cfg: Config): string => cfg.baseUrl ?? \"https://api.exa.ai\"\n\nconst authHeader = (cfg: Config) =>\n HttpClientRequest.setHeader(\"x-api-key\", Redacted.value(cfg.apiKey))\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\n// Deep research takes one instruction. Concatenate the user turns' text.\nconst instructions = (history: ReadonlyArray<Items.HistoryItem>): string =>\n history\n .filter(Items.isMessage)\n .filter((m) => m.role === \"user\")\n .flatMap((m) => m.content)\n .filter(Items.isInputText)\n .map((b) => b.text)\n .join(\"\\n\")\n\nconst submitBody = (request: ExaResearchRequest): Record<string, unknown> => ({\n instructions: instructions(request.history),\n model: request.model ?? \"exa-research\",\n ...(request.outputSchema !== undefined && {\n output: {\n schema: request.outputSchema.schema[\"~standard\"].jsonSchema.input({ target: \"draft-07\" }),\n },\n }),\n})\n\nconst submitJob = (\n cfg: Config,\n request: ExaResearchRequest,\n): Effect.Effect<ResearchJobRef, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/research/v0/tasks`).pipe(\n authHeader(cfg),\n HttpClientRequest.bodyJsonUnsafe(submitBody(request)),\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 const wire = yield* decodeSubmit(json).pipe(Effect.mapError(transportFailure))\n return Job.jobRef<Turn>(\"exa\", wire.id)\n })\n\nconst pollJob = (\n cfg: Config,\n ref: ResearchJobRef,\n): Effect.Effect<ResearchState, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const httpRequest = HttpClientRequest.get(`${baseUrl(cfg)}/research/v0/tasks/${ref.id}`).pipe(\n authHeader(cfg),\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 const wire = yield* decodeResearch(json).pipe(Effect.mapError(transportFailure))\n return jobStateOf(wire)\n })\n\nconst unsupported = (capability: string): AiError.Unsupported =>\n new AiError.Unsupported({\n provider: \"exa\",\n capability,\n reason: \"Exa's research task API exposes no verified cancel endpoint.\",\n })\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\nconst jobOps = (cfg: Config, client: HttpClient.HttpClient): ResearchJobOps<ExaResearchRequest> => {\n const withClient = <A>(e: Effect.Effect<A, AiError.AiError, HttpClient.HttpClient>) =>\n Effect.provideService(e, HttpClient.HttpClient, client)\n return {\n submit: (request) => withClient(submitJob(cfg, request)),\n poll: (ref) => withClient(pollJob(cfg, ref)),\n cancel: () => Effect.fail(unsupported(\"cancel\")),\n }\n}\n\n/** Build an `ExaDeepResearchService`. For Layer setup, prefer {@link layer}. */\nexport const make = (\n cfg: Config,\n): Effect.Effect<ExaDeepResearchService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient, (client) => fromJob(jobOps(cfg, client), cfg.job))\n\n/**\n * Layer registering the provider-typed `ExaDeepResearch` tag and the generic\n * `DeepResearch` tag. No capability markers: the surface (poll, collect,\n * synthesized stream) is universal.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<ExaDeepResearch | DeepResearch, never, HttpClient.HttpClient> => {\n const typed = Layer.effect(ExaDeepResearch, make(cfg))\n const generic = Layer.effect(\n DeepResearch,\n Effect.map(HttpClient.HttpClient, (client): DeepResearchService => {\n const ops = jobOps(cfg, client)\n return fromJob<ResearchRequest>(\n {\n submit: (request) => ops.submit(request as ExaResearchRequest),\n poll: ops.poll,\n cancel: ops.cancel,\n },\n cfg.job,\n )\n }),\n )\n return Layer.merge(typed, generic)\n}\n"],"mappings":"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"}
|
package/dist/ExaSearch.mjs
CHANGED
|
@@ -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
|
|
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`
|
package/dist/ExaSearch.mjs.map
CHANGED
|
@@ -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/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
import { ExaCategory, ExaSearchType } from "./models.mjs";
|
|
1
|
+
import { ExaCategory, ExaLivecrawl, ExaResearchModel, ExaSearchType } from "./models.mjs";
|
|
2
|
+
import { t as ExaContents_d_exports } from "./ExaContents.mjs";
|
|
3
|
+
import { t as ExaDeepResearch_d_exports } from "./ExaDeepResearch.mjs";
|
|
2
4
|
import { t as ExaSearch_d_exports } from "./ExaSearch.mjs";
|
|
3
|
-
export { ExaCategory, ExaSearch_d_exports as ExaSearch, ExaSearchType };
|
|
5
|
+
export { ExaCategory, ExaContents_d_exports as ExaContents, ExaDeepResearch_d_exports as ExaDeepResearch, ExaLivecrawl, ExaResearchModel, ExaSearch_d_exports as ExaSearch, ExaSearchType };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { t as ExaContents_exports } from "./ExaContents.mjs";
|
|
2
|
+
import { t as ExaDeepResearch_exports } from "./ExaDeepResearch.mjs";
|
|
1
3
|
import { t as ExaSearch_exports } from "./ExaSearch.mjs";
|
|
2
4
|
import "./models.mjs";
|
|
3
|
-
export { ExaSearch_exports as ExaSearch };
|
|
5
|
+
export { ExaContents_exports as ExaContents, ExaDeepResearch_exports as ExaDeepResearch, ExaSearch_exports as ExaSearch };
|
package/dist/models.d.mts
CHANGED
|
@@ -14,11 +14,29 @@
|
|
|
14
14
|
* cross-provider `CommonSearchRequest`.
|
|
15
15
|
*/
|
|
16
16
|
type ExaSearchType = "auto" | "fast" | "instant" | "deep-lite" | "deep" | "deep-reasoning" | "neural" | "keyword" | (string & {});
|
|
17
|
+
/**
|
|
18
|
+
* Exa research model for the async `/research/v1` task API (create + poll).
|
|
19
|
+
* `exa-research-fast` / `exa-research` / `exa-research-pro` trade latency for
|
|
20
|
+
* depth. The `(string & {})` tail accepts any string without an SDK bump.
|
|
21
|
+
*
|
|
22
|
+
* Note (2026-07): Exa announced `/research/v1` deprecation in favour of
|
|
23
|
+
* `/search` with `type: "deep-reasoning"`. Verify the endpoint and the
|
|
24
|
+
* completed-task response shape against live docs before relying on this.
|
|
25
|
+
*
|
|
26
|
+
* Reference: https://docs.exa.ai/reference/research/create-a-task
|
|
27
|
+
*/
|
|
28
|
+
type ExaResearchModel = "exa-research" | "exa-research-pro" | "exa-research-fast" | (string & {});
|
|
17
29
|
/**
|
|
18
30
|
* Exa's result category filter. Narrows the index to a content vertical.
|
|
19
31
|
* Provider-specific, so it lives on `ExaSearchRequest`.
|
|
20
32
|
*/
|
|
21
33
|
type ExaCategory = "company" | "research paper" | "news" | "personal site" | "financial report" | "people" | (string & {});
|
|
34
|
+
/**
|
|
35
|
+
* Exa `/contents` freshness knob. Controls whether the page is fetched live
|
|
36
|
+
* or served from Exa's cache. Provider-specific, so it lives on
|
|
37
|
+
* `ExaContentsRequest`.
|
|
38
|
+
*/
|
|
39
|
+
type ExaLivecrawl = "never" | "fallback" | "preferred" | "always" | (string & {});
|
|
22
40
|
//#endregion
|
|
23
|
-
export { ExaCategory, ExaSearchType };
|
|
41
|
+
export { ExaCategory, ExaLivecrawl, ExaResearchModel, ExaSearchType };
|
|
24
42
|
//# 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;;;;;
|
|
1
|
+
{"version":3,"file":"models.d.mts","names":[],"sources":["../src/models.ts"],"mappings":";;AAcA;;;;;AAuBA;;;;;AAWA;;;KAlCY,aAAA;;AAiDZ;;;;;;;;;;KA1BY,gBAAA;;;;;KAWA,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.11.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,14 @@
|
|
|
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
|
+
},
|
|
42
|
+
"./ExaDeepResearch": {
|
|
43
|
+
"types": "./dist/ExaDeepResearch.d.mts",
|
|
44
|
+
"import": "./dist/ExaDeepResearch.mjs"
|
|
45
|
+
},
|
|
37
46
|
"./ExaSearch": {
|
|
38
47
|
"types": "./dist/ExaSearch.d.mts",
|
|
39
48
|
"import": "./dist/ExaSearch.mjs"
|
|
@@ -43,13 +52,13 @@
|
|
|
43
52
|
"access": "public"
|
|
44
53
|
},
|
|
45
54
|
"devDependencies": {
|
|
46
|
-
"effect": "4.0.0-beta.
|
|
55
|
+
"effect": "4.0.0-beta.94",
|
|
47
56
|
"typescript": "^6.0.3",
|
|
48
|
-
"@effect-uai/core": "0.
|
|
57
|
+
"@effect-uai/core": "0.11.0"
|
|
49
58
|
},
|
|
50
59
|
"peerDependencies": {
|
|
51
60
|
"@effect-uai/core": ">=0.2.0 <1",
|
|
52
|
-
"effect": "4.0.0-beta.
|
|
61
|
+
"effect": ">=4.0.0-beta.94 <5.0.0"
|
|
53
62
|
},
|
|
54
63
|
"scripts": {
|
|
55
64
|
"build": "tsdown",
|
|
@@ -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, (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
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { Context, Effect, Layer, Match, Redacted, Schema } from "effect"
|
|
2
|
+
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
|
|
3
|
+
import * as AiError from "@effect-uai/core/AiError"
|
|
4
|
+
import {
|
|
5
|
+
DeepResearch,
|
|
6
|
+
type DeepResearchService,
|
|
7
|
+
type DeepResearchServiceShape,
|
|
8
|
+
fromJob,
|
|
9
|
+
type ResearchJobOps,
|
|
10
|
+
} from "@effect-uai/core/DeepResearch"
|
|
11
|
+
import * as Items from "@effect-uai/core/Items"
|
|
12
|
+
import * as Job from "@effect-uai/core/Job"
|
|
13
|
+
import * as StructuredFormat from "@effect-uai/core/StructuredFormat"
|
|
14
|
+
import type { ResearchJobRef, ResearchRequest, ResearchState } from "@effect-uai/core/Research"
|
|
15
|
+
import type { Turn } from "@effect-uai/core/Turn"
|
|
16
|
+
import type { ExaResearchModel } from "./models.js"
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Public types
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Exa-typed research request. Narrows `model` and adds `outputSchema`.
|
|
24
|
+
* Provider-portable code uses the generic `DeepResearch` tag instead.
|
|
25
|
+
*/
|
|
26
|
+
export type ExaResearchRequest = Omit<ResearchRequest, "model"> & {
|
|
27
|
+
readonly model?: ExaResearchModel
|
|
28
|
+
/**
|
|
29
|
+
* Optional structured output. When set, Exa researches the web and returns
|
|
30
|
+
* JSON matching the schema (Draft 7) instead of a prose report, with
|
|
31
|
+
* field-level citations. The completed `Turn`'s text is that JSON, so decode
|
|
32
|
+
* it with `Turn.decodeStructured(turn, outputSchema)` — the same path as
|
|
33
|
+
* `LanguageModel` structured output.
|
|
34
|
+
*/
|
|
35
|
+
readonly outputSchema?: StructuredFormat.StructuredFormat<unknown>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ExaDeepResearchService = DeepResearchServiceShape<ExaResearchRequest>
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Provider-typed tag for Exa deep research over the async task API
|
|
42
|
+
* (`POST /research/v0/tasks` create + `GET /research/v0/tasks/{id}` poll). Yield
|
|
43
|
+
* this for the model / `outputSchema` knobs; yield the generic `DeepResearch`
|
|
44
|
+
* tag for portable code. Poll-based, so `researchStream` / `streamFrom` are the
|
|
45
|
+
* synthesized default {@link fromJob} builds.
|
|
46
|
+
*/
|
|
47
|
+
export class ExaDeepResearch extends Context.Service<ExaDeepResearch, ExaDeepResearchService>()(
|
|
48
|
+
"@betalyra/effect-uai/providers/exa/ExaDeepResearch",
|
|
49
|
+
) {}
|
|
50
|
+
|
|
51
|
+
export type Config = {
|
|
52
|
+
readonly apiKey: Redacted.Redacted
|
|
53
|
+
readonly baseUrl?: string
|
|
54
|
+
/** Poll cadence / overall timeout for the derived poll loops. */
|
|
55
|
+
readonly job?: Job.JobConfig
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Wire codec
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
const WireSubmit = Schema.Struct({ id: Schema.String })
|
|
63
|
+
const decodeSubmit = Schema.decodeUnknownEffect(WireSubmit)
|
|
64
|
+
|
|
65
|
+
const OptionalText = Schema.optional(Schema.NullOr(Schema.String))
|
|
66
|
+
|
|
67
|
+
// A source backing one data field. `id` is Exa's dedup key (usually the url).
|
|
68
|
+
const WireSource = Schema.Struct({
|
|
69
|
+
id: OptionalText,
|
|
70
|
+
url: OptionalText,
|
|
71
|
+
title: OptionalText,
|
|
72
|
+
snippet: OptionalText,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// `data` holds the results: `{ answer }` for a prose task, or the schema-shaped
|
|
76
|
+
// object when an `outputSchema` was given. `schema` (non-null) is the reliable
|
|
77
|
+
// discriminator. `citations` is keyed by data-field path, each a list of
|
|
78
|
+
// sources. Reference: exa-labs/openapi-spec `/research/v0/tasks`.
|
|
79
|
+
export const WireResearch = Schema.Struct({
|
|
80
|
+
id: OptionalText,
|
|
81
|
+
status: OptionalText,
|
|
82
|
+
schema: Schema.optional(Schema.NullOr(Schema.Unknown)),
|
|
83
|
+
data: Schema.optional(Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown))),
|
|
84
|
+
citations: Schema.optional(Schema.NullOr(Schema.Record(Schema.String, Schema.Array(WireSource)))),
|
|
85
|
+
error: Schema.optional(Schema.NullOr(Schema.Struct({ message: OptionalText }))),
|
|
86
|
+
})
|
|
87
|
+
export type WireResearch = typeof WireResearch.Type
|
|
88
|
+
const decodeResearch = Schema.decodeUnknownEffect(WireResearch)
|
|
89
|
+
|
|
90
|
+
// Structured → the whole `data` object serialized (decoded outside via
|
|
91
|
+
// `Turn.decodeStructured`). Prose → `data.answer`.
|
|
92
|
+
const reportText = (wire: WireResearch): string => {
|
|
93
|
+
if (wire.schema != null) return JSON.stringify(wire.data ?? {})
|
|
94
|
+
const answer = wire.data?.answer
|
|
95
|
+
return typeof answer === "string" ? answer : ""
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Flatten the field-keyed citations into the flat `Annotation` list. The
|
|
99
|
+
// per-field grouping is lost in this view; the raw wire stays on `providerData`.
|
|
100
|
+
const citationsToAnnotations = (wire: WireResearch): ReadonlyArray<Items.Annotation> =>
|
|
101
|
+
Object.values(wire.citations ?? {})
|
|
102
|
+
.flat()
|
|
103
|
+
.flatMap((c, i) =>
|
|
104
|
+
c.url == null
|
|
105
|
+
? []
|
|
106
|
+
: [
|
|
107
|
+
{
|
|
108
|
+
type: "url_citation" as const,
|
|
109
|
+
url: c.url,
|
|
110
|
+
title: c.title ?? c.url,
|
|
111
|
+
marker: i + 1,
|
|
112
|
+
...(c.snippet != null && { cited_text: c.snippet }),
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
const turnFromResearch = (wire: WireResearch): Turn => {
|
|
118
|
+
const annotations = citationsToAnnotations(wire)
|
|
119
|
+
return {
|
|
120
|
+
items: [
|
|
121
|
+
{
|
|
122
|
+
type: "message",
|
|
123
|
+
role: "assistant",
|
|
124
|
+
content: [
|
|
125
|
+
{
|
|
126
|
+
type: "output_text",
|
|
127
|
+
text: reportText(wire),
|
|
128
|
+
...(annotations.length > 0 && { annotations }),
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
providerData: wire,
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
usage: {},
|
|
135
|
+
stop_reason: "stop",
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const jobStateOf = (wire: WireResearch): ResearchState =>
|
|
140
|
+
Match.value(wire.status ?? "").pipe(
|
|
141
|
+
Match.whenOr("pending", "queued", (): ResearchState => ({ _tag: "Pending" })),
|
|
142
|
+
Match.whenOr("running", "in_progress", (): ResearchState => ({ _tag: "Running" })),
|
|
143
|
+
Match.when(
|
|
144
|
+
"completed",
|
|
145
|
+
(): ResearchState => ({ _tag: "Succeeded", result: turnFromResearch(wire) }),
|
|
146
|
+
),
|
|
147
|
+
Match.whenOr(
|
|
148
|
+
"failed",
|
|
149
|
+
"canceled",
|
|
150
|
+
"cancelled",
|
|
151
|
+
(): ResearchState => ({
|
|
152
|
+
_tag: "Failed",
|
|
153
|
+
...(wire.error?.message != null && { reason: wire.error.message }),
|
|
154
|
+
raw: wire,
|
|
155
|
+
}),
|
|
156
|
+
),
|
|
157
|
+
Match.orElse((): ResearchState => ({ _tag: "Running" })),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// HTTP
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
const baseUrl = (cfg: Config): string => cfg.baseUrl ?? "https://api.exa.ai"
|
|
165
|
+
|
|
166
|
+
const authHeader = (cfg: Config) =>
|
|
167
|
+
HttpClientRequest.setHeader("x-api-key", Redacted.value(cfg.apiKey))
|
|
168
|
+
|
|
169
|
+
const transportFailure = (cause: unknown): AiError.AiError =>
|
|
170
|
+
new AiError.Unavailable({ provider: "exa", raw: cause })
|
|
171
|
+
|
|
172
|
+
const httpStatusError = (status: number, body: string): AiError.AiError => {
|
|
173
|
+
const provider = "exa"
|
|
174
|
+
const raw = body
|
|
175
|
+
if (status === 429) return new AiError.RateLimited({ provider, raw })
|
|
176
|
+
if (status === 408 || status === 504) return new AiError.Timeout({ provider, raw })
|
|
177
|
+
if (status === 401) return new AiError.AuthFailed({ provider, subtype: "auth", raw })
|
|
178
|
+
if (status === 403) return new AiError.AuthFailed({ provider, subtype: "permission", raw })
|
|
179
|
+
if (status === 402) return new AiError.AuthFailed({ provider, subtype: "billing", raw })
|
|
180
|
+
if (status >= 500) return new AiError.Unavailable({ provider, status, raw })
|
|
181
|
+
return new AiError.InvalidRequest({ provider, raw })
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Deep research takes one instruction. Concatenate the user turns' text.
|
|
185
|
+
const instructions = (history: ReadonlyArray<Items.HistoryItem>): string =>
|
|
186
|
+
history
|
|
187
|
+
.filter(Items.isMessage)
|
|
188
|
+
.filter((m) => m.role === "user")
|
|
189
|
+
.flatMap((m) => m.content)
|
|
190
|
+
.filter(Items.isInputText)
|
|
191
|
+
.map((b) => b.text)
|
|
192
|
+
.join("\n")
|
|
193
|
+
|
|
194
|
+
const submitBody = (request: ExaResearchRequest): Record<string, unknown> => ({
|
|
195
|
+
instructions: instructions(request.history),
|
|
196
|
+
model: request.model ?? "exa-research",
|
|
197
|
+
...(request.outputSchema !== undefined && {
|
|
198
|
+
output: {
|
|
199
|
+
schema: request.outputSchema.schema["~standard"].jsonSchema.input({ target: "draft-07" }),
|
|
200
|
+
},
|
|
201
|
+
}),
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
const submitJob = (
|
|
205
|
+
cfg: Config,
|
|
206
|
+
request: ExaResearchRequest,
|
|
207
|
+
): Effect.Effect<ResearchJobRef, AiError.AiError, HttpClient.HttpClient> =>
|
|
208
|
+
Effect.gen(function* () {
|
|
209
|
+
const client = yield* HttpClient.HttpClient
|
|
210
|
+
const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/research/v0/tasks`).pipe(
|
|
211
|
+
authHeader(cfg),
|
|
212
|
+
HttpClientRequest.bodyJsonUnsafe(submitBody(request)),
|
|
213
|
+
)
|
|
214
|
+
const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))
|
|
215
|
+
if (response.status >= 400) {
|
|
216
|
+
const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""))
|
|
217
|
+
return yield* httpStatusError(response.status, text)
|
|
218
|
+
}
|
|
219
|
+
const json = yield* response.json.pipe(Effect.mapError(transportFailure))
|
|
220
|
+
const wire = yield* decodeSubmit(json).pipe(Effect.mapError(transportFailure))
|
|
221
|
+
return Job.jobRef<Turn>("exa", wire.id)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
const pollJob = (
|
|
225
|
+
cfg: Config,
|
|
226
|
+
ref: ResearchJobRef,
|
|
227
|
+
): Effect.Effect<ResearchState, AiError.AiError, HttpClient.HttpClient> =>
|
|
228
|
+
Effect.gen(function* () {
|
|
229
|
+
const client = yield* HttpClient.HttpClient
|
|
230
|
+
const httpRequest = HttpClientRequest.get(`${baseUrl(cfg)}/research/v0/tasks/${ref.id}`).pipe(
|
|
231
|
+
authHeader(cfg),
|
|
232
|
+
)
|
|
233
|
+
const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))
|
|
234
|
+
if (response.status >= 400) {
|
|
235
|
+
const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""))
|
|
236
|
+
return yield* httpStatusError(response.status, text)
|
|
237
|
+
}
|
|
238
|
+
const json = yield* response.json.pipe(Effect.mapError(transportFailure))
|
|
239
|
+
const wire = yield* decodeResearch(json).pipe(Effect.mapError(transportFailure))
|
|
240
|
+
return jobStateOf(wire)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
const unsupported = (capability: string): AiError.Unsupported =>
|
|
244
|
+
new AiError.Unsupported({
|
|
245
|
+
provider: "exa",
|
|
246
|
+
capability,
|
|
247
|
+
reason: "Exa's research task API exposes no verified cancel endpoint.",
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
// Constructors
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
const jobOps = (cfg: Config, client: HttpClient.HttpClient): ResearchJobOps<ExaResearchRequest> => {
|
|
255
|
+
const withClient = <A>(e: Effect.Effect<A, AiError.AiError, HttpClient.HttpClient>) =>
|
|
256
|
+
Effect.provideService(e, HttpClient.HttpClient, client)
|
|
257
|
+
return {
|
|
258
|
+
submit: (request) => withClient(submitJob(cfg, request)),
|
|
259
|
+
poll: (ref) => withClient(pollJob(cfg, ref)),
|
|
260
|
+
cancel: () => Effect.fail(unsupported("cancel")),
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Build an `ExaDeepResearchService`. For Layer setup, prefer {@link layer}. */
|
|
265
|
+
export const make = (
|
|
266
|
+
cfg: Config,
|
|
267
|
+
): Effect.Effect<ExaDeepResearchService, never, HttpClient.HttpClient> =>
|
|
268
|
+
Effect.map(HttpClient.HttpClient, (client) => fromJob(jobOps(cfg, client), cfg.job))
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Layer registering the provider-typed `ExaDeepResearch` tag and the generic
|
|
272
|
+
* `DeepResearch` tag. No capability markers: the surface (poll, collect,
|
|
273
|
+
* synthesized stream) is universal.
|
|
274
|
+
*/
|
|
275
|
+
export const layer = (
|
|
276
|
+
cfg: Config,
|
|
277
|
+
): Layer.Layer<ExaDeepResearch | DeepResearch, never, HttpClient.HttpClient> => {
|
|
278
|
+
const typed = Layer.effect(ExaDeepResearch, make(cfg))
|
|
279
|
+
const generic = Layer.effect(
|
|
280
|
+
DeepResearch,
|
|
281
|
+
Effect.map(HttpClient.HttpClient, (client): DeepResearchService => {
|
|
282
|
+
const ops = jobOps(cfg, client)
|
|
283
|
+
return fromJob<ResearchRequest>(
|
|
284
|
+
{
|
|
285
|
+
submit: (request) => ops.submit(request as ExaResearchRequest),
|
|
286
|
+
poll: ops.poll,
|
|
287
|
+
cancel: ops.cancel,
|
|
288
|
+
},
|
|
289
|
+
cfg.job,
|
|
290
|
+
)
|
|
291
|
+
}),
|
|
292
|
+
)
|
|
293
|
+
return Layer.merge(typed, generic)
|
|
294
|
+
}
|
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
|
|
220
|
+
Effect.map(HttpClient.HttpClient, (client) => ({
|
|
221
221
|
search: (request) =>
|
|
222
222
|
searchImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),
|
|
223
223
|
}))
|
package/src/index.ts
CHANGED
package/src/models.ts
CHANGED
|
@@ -24,6 +24,24 @@ export type ExaSearchType =
|
|
|
24
24
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
25
25
|
| (string & {})
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Exa research model for the async `/research/v1` task API (create + poll).
|
|
29
|
+
* `exa-research-fast` / `exa-research` / `exa-research-pro` trade latency for
|
|
30
|
+
* depth. The `(string & {})` tail accepts any string without an SDK bump.
|
|
31
|
+
*
|
|
32
|
+
* Note (2026-07): Exa announced `/research/v1` deprecation in favour of
|
|
33
|
+
* `/search` with `type: "deep-reasoning"`. Verify the endpoint and the
|
|
34
|
+
* completed-task response shape against live docs before relying on this.
|
|
35
|
+
*
|
|
36
|
+
* Reference: https://docs.exa.ai/reference/research/create-a-task
|
|
37
|
+
*/
|
|
38
|
+
export type ExaResearchModel =
|
|
39
|
+
| "exa-research"
|
|
40
|
+
| "exa-research-pro"
|
|
41
|
+
| "exa-research-fast"
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
43
|
+
| (string & {})
|
|
44
|
+
|
|
27
45
|
/**
|
|
28
46
|
* Exa's result category filter. Narrows the index to a content vertical.
|
|
29
47
|
* Provider-specific, so it lives on `ExaSearchRequest`.
|
|
@@ -37,3 +55,16 @@ export type ExaCategory =
|
|
|
37
55
|
| "people"
|
|
38
56
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
39
57
|
| (string & {})
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Exa `/contents` freshness knob. Controls whether the page is fetched live
|
|
61
|
+
* or served from Exa's cache. Provider-specific, so it lives on
|
|
62
|
+
* `ExaContentsRequest`.
|
|
63
|
+
*/
|
|
64
|
+
export type ExaLivecrawl =
|
|
65
|
+
| "never"
|
|
66
|
+
| "fallback"
|
|
67
|
+
| "preferred"
|
|
68
|
+
| "always"
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
70
|
+
| (string & {})
|