@hasna-internal/kai-web-search-exa 0.1.1-rc.2
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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +42 -0
- package/README.zh.md +42 -0
- package/lib/index.js +157 -0
- package/lib/invariant.js +23 -0
- package/lib/types/index.d.ts +34 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/provider.d.ts +57 -0
- package/lib/types/types.d.ts +39 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/web/web-search-exa/README.md
|
|
5
|
+
README.md: 5b1212c1fb210a3aca7836875cc295fad87e7437
|
|
6
|
+
README.zh.md: 0d236cb1cb42ab3742d86872a2329060f8ceb09f
|
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @hasna-internal/kai-web-search-exa
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
An [Exa](https://exa.ai)-backed `WebSearchProvider` for the harness [web capability seam](../web/README.md) (`ctx.web`). It calls Exa's `POST /search` endpoint with highlight contents and maps the flat `results[]` into the seam's normalized `WebSearchResult`.
|
|
6
|
+
|
|
7
|
+
This is an **implementation** package: it registers a provider into `ctx.web`, it does not own the `ctx.web` key and it does not register a model-facing tool (that is `@hasna-internal/kai-tool-web`). Like `@hasna-internal/kai-llm-deepseek`, it is a function/namespace plugin (`inject: ['web']`) that registers its backend, not a default-export service.
|
|
8
|
+
|
|
9
|
+
## Config
|
|
10
|
+
|
|
11
|
+
| Key | Default | Meaning |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `apiKey` | `$EXA_API_KEY` | Exa API key. Empty/absent makes the provider unavailable. |
|
|
14
|
+
| `baseURL` | `https://api.exa.ai` | Endpoint base; `/search` is appended. An unparseable value makes the provider unavailable. |
|
|
15
|
+
| `searchType` | `auto` | Retrieval mode sent as Exa's `type`: `auto` (Exa decides), `keyword`, or `neural`. |
|
|
16
|
+
| `numResults` | (unset) | Default result count when a request carries no `maxResults`. Unset sends no default. Must be a positive integer. |
|
|
17
|
+
| `highlightsPerResult` | `1` | Highlight sentences requested per result (Exa's `highlightsPerUrl`). Must be a positive integer. |
|
|
18
|
+
|
|
19
|
+
```yaml
|
|
20
|
+
- id: web-search-exa
|
|
21
|
+
name: '@hasna-internal/kai-web-search-exa'
|
|
22
|
+
config:
|
|
23
|
+
apiKey: !!js process.env.EXA_API_KEY
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Mapping
|
|
27
|
+
|
|
28
|
+
Exa returns a flat `results[]` and no generated answer, so `content` is omitted. Each result maps to a `WebSearchSource`: `url` ← `url`, `title` ← `title`, `snippet` ← the first non-empty `highlights[]` entry (a result with no highlight has no portable snippet and is dropped), `publishedAt` ← `publishedDate`. A request's `maxResults` wins over the configured `numResults` default and is sent as Exa's `numResults` for a cost/latency optimization; the final bound is enforced by the seam. Provider failures (HTTP errors, network failure, unparseable or wrong-shape bodies) surface as `WebError` `WEB_PROVIDER_ERROR`; an aborted request surfaces as `WEB_ABORTED`. HTTP redirects are rejected before the `Location` target is contacted and surface as `WEB_PROVIDER_ERROR`.
|
|
29
|
+
|
|
30
|
+
## Model Experience
|
|
31
|
+
|
|
32
|
+
Indirectly, through [`dsh-tool-web`](../tool-web/README.md), which retains this provider's `maxResults`-bounded URLs, titles, first highlights, and publication dates or its exact `Exa search aborted`, `Exa search request failed: <error>`, and `Exa returned an unprocessable response body: <error>` failures under the consumer's error wrapper while generated answers and provider-private fields remain outside context.
|
|
33
|
+
|
|
34
|
+
#### KV Cache effect
|
|
35
|
+
|
|
36
|
+
No direct invalidation; the named consumer owns any request-prefix changes.
|
|
37
|
+
|
|
38
|
+
## Known Limitations and Deferred Work
|
|
39
|
+
|
|
40
|
+
- **A result with no non-blank highlight is dropped entirely** — no portable snippet to map, so fewer sources than the requested count can return.
|
|
41
|
+
- **Only `searchType`/`numResults`/`highlightsPerResult` are exposed** — Exa's other controls (livecrawl, category, domain/date filters, full-text contents) wait on provider-neutral Service Definition fields ([seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md)).
|
|
42
|
+
- **Abort classification is error-shape-based** — only a `DOMException` named `AbortError` maps to `WEB_ABORTED`; an abort carrying a custom reason (e.g. `dsh-timeout`'s `TimeoutReason`) surfaces as `WEB_PROVIDER_ERROR`.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @hasna-internal/kai-web-search-exa
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
由 [Exa](https://exa.ai) 支持的 `WebSearchProvider`,用于 harness [web 能力 seam](../web/README.zh.md)(`ctx.web`)。它调用 Exa 的 `POST /search` 端点并请求高亮摘要内容,把扁平 `results[]` 映射为 seam 规范化的 `WebSearchResult`。
|
|
6
|
+
|
|
7
|
+
这是一个**实现**包:它向 `ctx.web` 注册提供方,不拥有 `ctx.web` 键,也不注册面向模型的工具(后者属于 `@hasna-internal/kai-tool-web`)。与 `@hasna-internal/kai-llm-deepseek` 一样,它是函数/命名空间插件(`inject: ['web']`),负责注册后端,而非默认导出服务。
|
|
8
|
+
|
|
9
|
+
## 配置
|
|
10
|
+
|
|
11
|
+
| 配置键 | 默认值 | 含义 |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `apiKey` | `$EXA_API_KEY` | Exa API 密钥。为空或缺失时提供方不可用。 |
|
|
14
|
+
| `baseURL` | `https://api.exa.ai` | 端点基址;追加 `/search`。无法解析时提供方不可用。 |
|
|
15
|
+
| `searchType` | `auto` | 以 Exa `type` 发送的检索模式:`auto`(由 Exa 决定)、`keyword` 或 `neural`。 |
|
|
16
|
+
| `numResults` | (未设置) | 请求不含 `maxResults` 时使用的默认结果数。未设置时不发送默认值。必须是正整数。 |
|
|
17
|
+
| `highlightsPerResult` | `1` | 每个结果请求的 highlight 句子数(Exa `highlightsPerUrl`)。必须是正整数。 |
|
|
18
|
+
|
|
19
|
+
```yaml
|
|
20
|
+
- id: web-search-exa
|
|
21
|
+
name: '@hasna-internal/kai-web-search-exa'
|
|
22
|
+
config:
|
|
23
|
+
apiKey: !!js process.env.EXA_API_KEY
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 映射
|
|
27
|
+
|
|
28
|
+
Exa 返回扁平 `results[]`,不返回生成答案,因此省略 `content`。每项结果映射为 `WebSearchSource`:`url` ← `url`、`title` ← `title`、`snippet` ← 第一个非空的 `highlights[]` 条目(没有高亮摘要的结果缺少可移植的 snippet,会被丢弃)、`publishedAt` ← `publishedDate`。请求的 `maxResults` 优先于已配置的默认 `numResults`,并作为 Exa `numResults` 发送,以优化成本和延迟;最终上限由 seam 强制执行。提供方失败(HTTP 错误、网络失败、响应体无法解析或结构不符)以 `WebError` `WEB_PROVIDER_ERROR` 呈现;中止请求以 `WEB_ABORTED` 呈现。HTTP 重定向会在访问 `Location` 指向的目标之前被拒绝,并以 `WEB_PROVIDER_ERROR` 呈现。
|
|
29
|
+
|
|
30
|
+
## 模型体验
|
|
31
|
+
|
|
32
|
+
通过 [`dsh-tool-web`](../tool-web/README.zh.md) 间接影响;该工具保留此提供方经 `maxResults` 限制的 URL、标题、首条 highlight 与发布日期,或将确切的错误消息 `Exa search aborted`、`Exa search request failed: <error>` 和 `Exa returned an unprocessable response body: <error>` 置于消费方的错误包装层内;生成答案与提供方私有字段不进入上下文。
|
|
33
|
+
|
|
34
|
+
#### KV Cache 影响
|
|
35
|
+
|
|
36
|
+
不会直接导致 KV Cache 失效;请求前缀变更由上述消费方负责。
|
|
37
|
+
|
|
38
|
+
## 已知限制与暂缓事项
|
|
39
|
+
|
|
40
|
+
- **没有非空白高亮摘要的结果会被整个丢弃**:没有可映射的可移植 snippet,因此返回源可能少于请求数量。
|
|
41
|
+
- **只公开 `searchType`/`numResults`/`highlightsPerResult`**:Exa 的其他控制项(livecrawl、category、域名/日期过滤条件、全文内容)等待提供方无关的 Service Definition 字段(见 [seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.zh.md))。
|
|
42
|
+
- **按错误形状分类中止**:只有 `DOMException` 且名为 `AbortError` 时才映射为 `WEB_ABORTED`;携带自定义原因的中止(例如 `dsh-timeout` 的 `TimeoutReason`)会呈现为 `WEB_PROVIDER_ERROR`。
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { launchEnvironmentOf } from "@hasna-internal/kai-launch-environment";
|
|
2
|
+
import z from "@deepseek-ai/schemastery";
|
|
3
|
+
import { WebError } from "@hasna-internal/kai-web";
|
|
4
|
+
//#region lib/types/provider.js
|
|
5
|
+
/**
|
|
6
|
+
* `ExaSearchProvider`: a `WebSearchProvider` backed by the Exa search API (`POST /search` with
|
|
7
|
+
* highlight contents). It maps the first non-blank highlight to `snippet`, maps
|
|
8
|
+
* `publishedDate` to `publishedAt`, drops entries without a snippet, and omits `content`
|
|
9
|
+
* because Exa returns no generated answer.
|
|
10
|
+
* @module @hasna-internal/kai-web-search-exa/provider
|
|
11
|
+
*/
|
|
12
|
+
/** Stable id this provider registers under. */
|
|
13
|
+
const EXA_PROVIDER_ID = "exa";
|
|
14
|
+
/** Default Exa search endpoint; `/search` is the operation. */
|
|
15
|
+
const EXA_DEFAULT_BASE_URL = "https://api.exa.ai";
|
|
16
|
+
/** Default retrieval mode: let Exa pick between keyword and neural search. */
|
|
17
|
+
const EXA_DEFAULT_SEARCH_TYPE = "auto";
|
|
18
|
+
/** Default number of highlight sentences requested per result. */
|
|
19
|
+
const EXA_DEFAULT_HIGHLIGHTS_PER_RESULT = 1;
|
|
20
|
+
/** Attribution header sent on every request. Bump with the package version. */
|
|
21
|
+
const USER_AGENT = "deepseek-harness/0.0.1";
|
|
22
|
+
/**
|
|
23
|
+
* Map one Exa result to a normalized source, or `undefined` when it carries no
|
|
24
|
+
* portable snippet (an entry with no highlight is dropped — the seam has no
|
|
25
|
+
* other field to derive a snippet from, and inventing one would lie).
|
|
26
|
+
*
|
|
27
|
+
* @param result - one entry of Exa's `results[]`.
|
|
28
|
+
* @returns the normalized source, or `undefined` when the entry has no
|
|
29
|
+
* non-blank highlight.
|
|
30
|
+
*/
|
|
31
|
+
function mapExaResult(result) {
|
|
32
|
+
const snippet = result.highlights?.find((highlight) => highlight.trim().length > 0);
|
|
33
|
+
if (snippet === void 0) return void 0;
|
|
34
|
+
return {
|
|
35
|
+
url: result.url,
|
|
36
|
+
...result.title != null && result.title.length > 0 ? { title: result.title } : {},
|
|
37
|
+
snippet,
|
|
38
|
+
...result.publishedDate != null && result.publishedDate.length > 0 ? { publishedAt: result.publishedDate } : {}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Map an Exa response envelope to a normalized search result.
|
|
43
|
+
*
|
|
44
|
+
* @param response - the parsed `POST /search` response body.
|
|
45
|
+
* @returns the normalized result; snippet-less entries are dropped
|
|
46
|
+
* ({@link mapExaResult}).
|
|
47
|
+
*/
|
|
48
|
+
function mapExaResponse(response) {
|
|
49
|
+
return {
|
|
50
|
+
sources: (response.results ?? []).map(mapExaResult).filter((source) => source !== void 0),
|
|
51
|
+
truncated: false
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** The Exa-backed search provider; HTTP redirects fail as `WEB_PROVIDER_ERROR`. */
|
|
55
|
+
var ExaSearchProvider = class {
|
|
56
|
+
options;
|
|
57
|
+
id = "exa";
|
|
58
|
+
constructor(options) {
|
|
59
|
+
this.options = options;
|
|
60
|
+
}
|
|
61
|
+
available() {
|
|
62
|
+
return this.options.apiKey.length > 0 && isValidBaseUrl(this.options.baseURL) && isPositiveInteger(this.options.highlightsPerResult) && (this.options.numResults === void 0 || isPositiveInteger(this.options.numResults));
|
|
63
|
+
}
|
|
64
|
+
async search(request, signal) {
|
|
65
|
+
const numResults = request.maxResults ?? this.options.numResults;
|
|
66
|
+
let response;
|
|
67
|
+
try {
|
|
68
|
+
response = await fetch(`${this.options.baseURL}/search`, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
redirect: "error",
|
|
71
|
+
headers: {
|
|
72
|
+
"authorization": `Bearer ${this.options.apiKey}`,
|
|
73
|
+
"content-type": "application/json",
|
|
74
|
+
"accept": "application/json",
|
|
75
|
+
"user-agent": USER_AGENT
|
|
76
|
+
},
|
|
77
|
+
body: JSON.stringify({
|
|
78
|
+
query: request.query,
|
|
79
|
+
type: this.options.searchType,
|
|
80
|
+
contents: { highlights: { highlightsPerUrl: this.options.highlightsPerResult } },
|
|
81
|
+
...numResults !== void 0 ? { numResults } : {}
|
|
82
|
+
}),
|
|
83
|
+
...signal !== void 0 ? { signal } : {}
|
|
84
|
+
});
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (isAbortError(error)) throw new WebError("Exa search aborted", "WEB_ABORTED", { cause: error });
|
|
87
|
+
throw new WebError(`Exa search request failed: ${String(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
|
|
88
|
+
}
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
let message = `Exa API error (HTTP ${response.status})`;
|
|
91
|
+
try {
|
|
92
|
+
const parsed = await response.json();
|
|
93
|
+
const detail = parsed.error ?? parsed.message;
|
|
94
|
+
if (detail !== void 0 && detail.length > 0) message = detail;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (isAbortError(error)) throw new WebError("Exa search aborted", "WEB_ABORTED", { cause: error });
|
|
97
|
+
}
|
|
98
|
+
throw new WebError(message, "WEB_PROVIDER_ERROR");
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
return mapExaResponse(await response.json());
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (isAbortError(error)) throw new WebError("Exa search aborted", "WEB_ABORTED", { cause: error });
|
|
104
|
+
throw new WebError(`Exa returned an unprocessable response body: ${String(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
/** True when `baseURL` parses as an absolute URL (a cheap local config check). */
|
|
109
|
+
function isValidBaseUrl(baseURL) {
|
|
110
|
+
return URL.canParse(baseURL);
|
|
111
|
+
}
|
|
112
|
+
/** True for a request limit that can be sent to Exa (a positive whole number). */
|
|
113
|
+
function isPositiveInteger(value) {
|
|
114
|
+
return Number.isInteger(value) && value > 0;
|
|
115
|
+
}
|
|
116
|
+
/** True for a fetch/`AbortSignal` abort, surfaced as `WEB_ABORTED`. */
|
|
117
|
+
function isAbortError(error) {
|
|
118
|
+
return error instanceof DOMException && error.name === "AbortError";
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region lib/types/index.js
|
|
122
|
+
/**
|
|
123
|
+
* `@hasna-internal/kai-web-search-exa`: registers an Exa-backed `WebSearchProvider`
|
|
124
|
+
* with `ctx.web`. A function/namespace plugin (NOT a default-export service):
|
|
125
|
+
* a search provider does not own the `ctx.web` key — it registers INTO the
|
|
126
|
+
* seam's provider registry, exactly as `@hasna-internal/kai-llm-deepseek`
|
|
127
|
+
* registers an adapter into `ctx.llm`. The key is owned by `@hasna-internal/kai-web`.
|
|
128
|
+
*
|
|
129
|
+
* @module @hasna-internal/kai-web-search-exa
|
|
130
|
+
*/
|
|
131
|
+
/** Cordis plugin name used by loader diagnostics. */
|
|
132
|
+
const name = "web-search-exa";
|
|
133
|
+
/** The web seam this provider registers into. */
|
|
134
|
+
const inject = ["web"];
|
|
135
|
+
const Config = z.object({
|
|
136
|
+
apiKey: z.string(),
|
|
137
|
+
baseURL: z.string(),
|
|
138
|
+
searchType: z.union([
|
|
139
|
+
"auto",
|
|
140
|
+
"keyword",
|
|
141
|
+
"neural"
|
|
142
|
+
]),
|
|
143
|
+
numResults: z.number().step(1).min(1),
|
|
144
|
+
highlightsPerResult: z.number().step(1).min(1)
|
|
145
|
+
});
|
|
146
|
+
/** Register the Exa search provider with `ctx.web`. */
|
|
147
|
+
function apply(ctx, config) {
|
|
148
|
+
ctx.web.registerSearchProvider(new ExaSearchProvider({
|
|
149
|
+
apiKey: config.apiKey ?? launchEnvironmentOf(ctx).get("EXA_API_KEY")?.value ?? "",
|
|
150
|
+
baseURL: config.baseURL ?? "https://api.exa.ai",
|
|
151
|
+
searchType: config.searchType ?? "auto",
|
|
152
|
+
highlightsPerResult: config.highlightsPerResult ?? 1,
|
|
153
|
+
...config.numResults !== void 0 ? { numResults: config.numResults } : {}
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
export { Config, EXA_DEFAULT_BASE_URL, EXA_DEFAULT_HIGHLIGHTS_PER_RESULT, EXA_DEFAULT_SEARCH_TYPE, EXA_PROVIDER_ID, ExaSearchProvider, apply, inject, name };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@hasna-internal/kai-web-search-exa`.
|
|
4
|
+
* @module @hasna-internal/kai-web-search-exa/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@hasna-internal/kai-web-search-exa";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "web-search-exa-invariant";
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/**
|
|
12
|
+
* No runtime invariant: this package exposes no independent event sequence or mutable data relation
|
|
13
|
+
* beyond contracts enforced at its owning seam.
|
|
14
|
+
*/
|
|
15
|
+
const install = () => {};
|
|
16
|
+
/**
|
|
17
|
+
* Register this package's invariant companion.
|
|
18
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
19
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
20
|
+
*/
|
|
21
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
22
|
+
//#endregion
|
|
23
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@hasna-internal/kai-web-search-exa`: registers an Exa-backed `WebSearchProvider`
|
|
3
|
+
* with `ctx.web`. A function/namespace plugin (NOT a default-export service):
|
|
4
|
+
* a search provider does not own the `ctx.web` key — it registers INTO the
|
|
5
|
+
* seam's provider registry, exactly as `@hasna-internal/kai-llm-deepseek`
|
|
6
|
+
* registers an adapter into `ctx.llm`. The key is owned by `@hasna-internal/kai-web`.
|
|
7
|
+
*
|
|
8
|
+
* @module @hasna-internal/kai-web-search-exa
|
|
9
|
+
*/
|
|
10
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
11
|
+
import z from '@deepseek-ai/schemastery';
|
|
12
|
+
export { EXA_DEFAULT_BASE_URL, EXA_DEFAULT_HIGHLIGHTS_PER_RESULT, EXA_DEFAULT_SEARCH_TYPE, EXA_PROVIDER_ID, ExaSearchProvider, } from './provider.ts';
|
|
13
|
+
export type { ExaSearchProviderOptions } from './provider.ts';
|
|
14
|
+
/** Cordis plugin name used by loader diagnostics. */
|
|
15
|
+
export declare const name = "web-search-exa";
|
|
16
|
+
/** The web seam this provider registers into. */
|
|
17
|
+
export declare const inject: string[];
|
|
18
|
+
/** Plugin config (all optional — `apply` fills env-var and constant defaults). */
|
|
19
|
+
export interface Config {
|
|
20
|
+
/** Exa API key. Falls back to `$EXA_API_KEY`. Empty → provider unavailable. */
|
|
21
|
+
apiKey?: string;
|
|
22
|
+
/** Endpoint base; `/search` is appended. Defaults to the public API. */
|
|
23
|
+
baseURL?: string;
|
|
24
|
+
/** Retrieval mode sent as Exa's `type`. Defaults to `auto`. */
|
|
25
|
+
searchType?: 'auto' | 'keyword' | 'neural';
|
|
26
|
+
/** Default result count when a request carries no `maxResults`. Omitted = none. */
|
|
27
|
+
numResults?: number;
|
|
28
|
+
/** Highlight sentences requested per result. Defaults to 1. */
|
|
29
|
+
highlightsPerResult?: number;
|
|
30
|
+
}
|
|
31
|
+
export declare const Config: z<Config>;
|
|
32
|
+
/** Register the Exa search provider with `ctx.web`. */
|
|
33
|
+
export declare function apply(ctx: Context, config: Config): void;
|
|
34
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@hasna-internal/kai-web-search-exa`.
|
|
3
|
+
* @module @hasna-internal/kai-web-search-exa/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "web-search-exa-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ExaSearchProvider`: a `WebSearchProvider` backed by the Exa search API (`POST /search` with
|
|
3
|
+
* highlight contents). It maps the first non-blank highlight to `snippet`, maps
|
|
4
|
+
* `publishedDate` to `publishedAt`, drops entries without a snippet, and omits `content`
|
|
5
|
+
* because Exa returns no generated answer.
|
|
6
|
+
* @module @hasna-internal/kai-web-search-exa/provider
|
|
7
|
+
*/
|
|
8
|
+
import type { WebSearchProvider, WebSearchRequest, WebSearchResult, WebSearchSource } from '@hasna-internal/kai-web';
|
|
9
|
+
import type { ExaResult, ExaSearchResponse } from './types.ts';
|
|
10
|
+
/** Stable id this provider registers under. */
|
|
11
|
+
export declare const EXA_PROVIDER_ID = "exa";
|
|
12
|
+
/** Default Exa search endpoint; `/search` is the operation. */
|
|
13
|
+
export declare const EXA_DEFAULT_BASE_URL = "https://api.exa.ai";
|
|
14
|
+
/** Default retrieval mode: let Exa pick between keyword and neural search. */
|
|
15
|
+
export declare const EXA_DEFAULT_SEARCH_TYPE = "auto";
|
|
16
|
+
/** Default number of highlight sentences requested per result. */
|
|
17
|
+
export declare const EXA_DEFAULT_HIGHLIGHTS_PER_RESULT = 1;
|
|
18
|
+
/** Resolved provider options (the plugin's `apply` supplies env-var and constant defaults). */
|
|
19
|
+
export interface ExaSearchProviderOptions {
|
|
20
|
+
/** Exa API key. Empty/absent makes the provider unavailable. */
|
|
21
|
+
apiKey: string;
|
|
22
|
+
/** Endpoint base; `/search` is appended. */
|
|
23
|
+
baseURL: string;
|
|
24
|
+
/** Retrieval mode sent as Exa's `type`. */
|
|
25
|
+
searchType: 'auto' | 'keyword' | 'neural';
|
|
26
|
+
/** Default result count when a request carries no `maxResults`. */
|
|
27
|
+
numResults?: number;
|
|
28
|
+
/** Highlight sentences requested per result (Exa's `highlightsPerUrl`). */
|
|
29
|
+
highlightsPerResult: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Map one Exa result to a normalized source, or `undefined` when it carries no
|
|
33
|
+
* portable snippet (an entry with no highlight is dropped — the seam has no
|
|
34
|
+
* other field to derive a snippet from, and inventing one would lie).
|
|
35
|
+
*
|
|
36
|
+
* @param result - one entry of Exa's `results[]`.
|
|
37
|
+
* @returns the normalized source, or `undefined` when the entry has no
|
|
38
|
+
* non-blank highlight.
|
|
39
|
+
*/
|
|
40
|
+
export declare function mapExaResult(result: ExaResult): WebSearchSource | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Map an Exa response envelope to a normalized search result.
|
|
43
|
+
*
|
|
44
|
+
* @param response - the parsed `POST /search` response body.
|
|
45
|
+
* @returns the normalized result; snippet-less entries are dropped
|
|
46
|
+
* ({@link mapExaResult}).
|
|
47
|
+
*/
|
|
48
|
+
export declare function mapExaResponse(response: ExaSearchResponse): WebSearchResult;
|
|
49
|
+
/** The Exa-backed search provider; HTTP redirects fail as `WEB_PROVIDER_ERROR`. */
|
|
50
|
+
export declare class ExaSearchProvider implements WebSearchProvider {
|
|
51
|
+
private readonly options;
|
|
52
|
+
readonly id = "exa";
|
|
53
|
+
constructor(options: ExaSearchProviderOptions);
|
|
54
|
+
available(): boolean;
|
|
55
|
+
search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire types for the Exa search API (`POST https://api.exa.ai/search`). Types
|
|
3
|
+
* only — no runtime code. Exa returns a flat `results[]`; each entry carries a
|
|
4
|
+
* URL, optional title, optional `publishedDate`, and (when highlights are
|
|
5
|
+
* requested) a `highlights[]` array of salient sentences.
|
|
6
|
+
*
|
|
7
|
+
* @module @hasna-internal/kai-web-search-exa/types
|
|
8
|
+
*/
|
|
9
|
+
/** Request body sent to Exa's search endpoint. */
|
|
10
|
+
export interface ExaSearchRequest {
|
|
11
|
+
query: string;
|
|
12
|
+
/** Retrieval mode: keyword, neural (embeddings), or auto (Exa decides). */
|
|
13
|
+
type: 'auto' | 'keyword' | 'neural';
|
|
14
|
+
/** Exa's result-count control; the seam still enforces the bound on return. */
|
|
15
|
+
numResults?: number;
|
|
16
|
+
/** Ask Exa to return highlight sentences per result. */
|
|
17
|
+
contents: {
|
|
18
|
+
highlights: {
|
|
19
|
+
highlightsPerUrl: number;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** One entry of Exa's flat `results[]`. */
|
|
24
|
+
export interface ExaResult {
|
|
25
|
+
url: string;
|
|
26
|
+
title?: string | null;
|
|
27
|
+
publishedDate?: string | null;
|
|
28
|
+
highlights?: string[];
|
|
29
|
+
}
|
|
30
|
+
/** Exa's search response envelope. */
|
|
31
|
+
export interface ExaSearchResponse {
|
|
32
|
+
results?: ExaResult[];
|
|
33
|
+
}
|
|
34
|
+
/** Exa's error response envelope (best-effort; fields vary by failure). */
|
|
35
|
+
export interface ExaError {
|
|
36
|
+
error?: string;
|
|
37
|
+
message?: string;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=types.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hasna-internal/kai-web-search-exa",
|
|
3
|
+
"description": "Exa-backed search provider for the DeepSeek Harness web capability seam (ctx.web)",
|
|
4
|
+
"version": "0.1.1-rc.2",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/web/web-search-exa"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./invariant": {
|
|
22
|
+
"types": "./lib/types/invariant.d.ts",
|
|
23
|
+
"default": "./lib/invariant.js"
|
|
24
|
+
},
|
|
25
|
+
"./src/*": "./src/*",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"lib/index.js",
|
|
30
|
+
"lib/invariant.js",
|
|
31
|
+
"lib/types/**/*.d.ts"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@hasna-internal/kai-invariants": "^0.1.1-rc.2",
|
|
36
|
+
"@hasna-internal/kai-launch-environment": "^0.1.1-rc.2",
|
|
37
|
+
"@hasna-internal/kai-web": "^0.1.1-rc.2",
|
|
38
|
+
"@deepseek-ai/cordis": "^4.0.1"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@hasna-internal/kai-launch-environment": "^0.1.1-rc.2",
|
|
45
|
+
"@hasna-internal/kai-invariants": "^0.1.1-rc.2",
|
|
46
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
47
|
+
"@hasna-internal/kai-web": "^0.1.1-rc.2"
|
|
48
|
+
}
|
|
49
|
+
}
|