@nimble-way/ai-sdk 0.1.0 → 0.2.1
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/README.md +59 -4
- package/dist/index.cjs +87 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +124 -1
- package/dist/index.d.ts +124 -1
- package/dist/index.js +83 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# @nimble-way/ai-sdk
|
|
2
2
|
|
|
3
|
-
Nimble Web Search as
|
|
3
|
+
Nimble Web Search and Extract as ready-made [Vercel AI SDK](https://ai-sdk.dev) tools. Give any AI SDK agent the ability to search the web and read pages with [Nimble](https://nimbleway.com) in a few lines.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **Web Search** — a `nimbleSearch()` tool the model can call to retrieve ranked, real-time web results and ground its answers in them.
|
|
8
|
-
- **
|
|
8
|
+
- **Extract** — a `nimbleExtract()` tool that fetches a URL and returns clean markdown (or HTML) for the model to read, quote, or summarize.
|
|
9
|
+
- **Model- and gateway-agnostic** — app-side `tool()`s; work the same with the Vercel AI Gateway or a direct provider.
|
|
9
10
|
- **Typed** — typed config and normalized output; an injectable client for testing.
|
|
10
11
|
|
|
11
|
-
>
|
|
12
|
+
> Map and Crawl tools are planned follow-ups.
|
|
12
13
|
|
|
13
14
|
## Install
|
|
14
15
|
|
|
@@ -77,6 +78,32 @@ export async function POST(req: Request) {
|
|
|
77
78
|
|
|
78
79
|
The tool runs **in your app** (an app-side `tool()`, not a provider/server-executed search). It is therefore **gateway-agnostic**: it behaves identically whether you route your model through the [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) (plain-string model IDs like `'openai/gpt-4o-mini'`) or call a provider SDK directly. The gateway, if present, only routes the *model* call.
|
|
79
80
|
|
|
81
|
+
## Extract
|
|
82
|
+
|
|
83
|
+
Give the model a URL and get back clean content to read, quote, or summarize:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { generateText, stepCountIs } from 'ai';
|
|
87
|
+
import { nimbleExtract } from '@nimble-way/ai-sdk';
|
|
88
|
+
|
|
89
|
+
const { text } = await generateText({
|
|
90
|
+
model: 'openai/gpt-4o-mini',
|
|
91
|
+
prompt: 'Summarize https://en.wikipedia.org/wiki/Web_scraping',
|
|
92
|
+
tools: { extract: nimbleExtract({ format: 'markdown' }) },
|
|
93
|
+
// Allow a step after the tool call so the model can summarize the page.
|
|
94
|
+
stopWhen: stepCountIs(2),
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Register both tools together so the model can search, then read the best result:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
tools: {
|
|
102
|
+
webSearch: nimbleSearch(),
|
|
103
|
+
extract: nimbleExtract(),
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
80
107
|
## Options
|
|
81
108
|
|
|
82
109
|
`nimbleSearch(config)` — all fields optional:
|
|
@@ -94,8 +121,22 @@ The tool runs **in your app** (an app-side `tool()`, not a provider/server-execu
|
|
|
94
121
|
|
|
95
122
|
The **model-facing input** is just `{ query: string, maxResults?: number }` — all policy above is developer-controlled, not model-controlled.
|
|
96
123
|
|
|
124
|
+
`nimbleExtract(config)` — all fields optional:
|
|
125
|
+
|
|
126
|
+
| Option | Type | Default | Notes |
|
|
127
|
+
|---|---|---|---|
|
|
128
|
+
| `apiKey` | `string` | `process.env.NIMBLE_API_KEY` | Nimble API key. |
|
|
129
|
+
| `client` | `NimbleExtractClient` | — | Inject a pre-built/mock client. |
|
|
130
|
+
| `format` | `'markdown' \| 'html'` | `'markdown'` | Content format returned to the model. |
|
|
131
|
+
| `country` | `string` | — | ISO country for geolocation / proxy. |
|
|
132
|
+
| `maxContentLength` | `number` | `50_000` | Truncate the extracted content. |
|
|
133
|
+
|
|
134
|
+
The **model-facing input** is just `{ url: string }`.
|
|
135
|
+
|
|
97
136
|
## Output shape
|
|
98
137
|
|
|
138
|
+
`nimbleSearch`:
|
|
139
|
+
|
|
99
140
|
```ts
|
|
100
141
|
{
|
|
101
142
|
query: string;
|
|
@@ -112,9 +153,22 @@ The **model-facing input** is just `{ query: string, maxResults?: number }` —
|
|
|
112
153
|
}
|
|
113
154
|
```
|
|
114
155
|
|
|
156
|
+
`nimbleExtract`:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
{
|
|
160
|
+
url: string;
|
|
161
|
+
status: string; // e.g. 'success'
|
|
162
|
+
statusCode?: number;
|
|
163
|
+
format: 'markdown' | 'html';
|
|
164
|
+
content: string; // truncated to maxContentLength
|
|
165
|
+
links?: string[];
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
115
169
|
## Limitations
|
|
116
170
|
|
|
117
|
-
- **Search
|
|
171
|
+
- **Search + Extract.** Map / Crawl / Agents are follow-ups.
|
|
118
172
|
- **No answer generation.** `include_answer` is intentionally not exposed.
|
|
119
173
|
- **`searchDepth: 'fast'` is not available** (enterprise-gated).
|
|
120
174
|
- **Runtime:** targets the **Node.js runtime** (Node ≥ 18). Edge/serverless is expected to work but not yet verified — prefer the Node runtime.
|
|
@@ -124,6 +178,7 @@ The **model-facing input** is just `{ query: string, maxResults?: number }` —
|
|
|
124
178
|
| Symptom | Fix |
|
|
125
179
|
|---|---|
|
|
126
180
|
| `NimbleConfigError: missing API key` | Set `NIMBLE_API_KEY` or pass `apiKey`. |
|
|
181
|
+
| `NimbleExtractError` with a status | The Nimble Extract API returned an error; the HTTP status is on `err.status`. |
|
|
127
182
|
| `NimbleSearchError` with a status | The Nimble API returned an error; the HTTP status is on `err.status`. |
|
|
128
183
|
| Tool never called | Ensure your prompt invites tool use and `stopWhen` allows multiple steps. |
|
|
129
184
|
|
package/dist/index.cjs
CHANGED
|
@@ -9,6 +9,9 @@ var nimbleSearchInputSchema = zod.z.object({
|
|
|
9
9
|
query: zod.z.string().min(1).describe("The web search query."),
|
|
10
10
|
maxResults: zod.z.number().int().positive().optional().describe("How many results to return (clamped to the developer-configured cap).")
|
|
11
11
|
});
|
|
12
|
+
var nimbleExtractInputSchema = zod.z.object({
|
|
13
|
+
url: zod.z.string().url().describe("The URL of the web page to extract clean content from.")
|
|
14
|
+
});
|
|
12
15
|
|
|
13
16
|
// src/normalize.ts
|
|
14
17
|
function isSerpMetadata(metadata) {
|
|
@@ -44,6 +47,23 @@ function normalizeSearchResponse(response, options) {
|
|
|
44
47
|
results
|
|
45
48
|
};
|
|
46
49
|
}
|
|
50
|
+
function normalizeExtractResponse(response, options) {
|
|
51
|
+
const data = response.data;
|
|
52
|
+
const otherFormat = options.format === "html" ? "markdown" : "html";
|
|
53
|
+
const primary = options.format === "html" ? data.html : data.markdown;
|
|
54
|
+
const fallback = options.format === "html" ? data.markdown : data.html;
|
|
55
|
+
const usedFormat = primary && primary.length > 0 ? options.format : fallback && fallback.length > 0 ? otherFormat : options.format;
|
|
56
|
+
const content = truncate(primary || fallback || "", options.maxContentLength);
|
|
57
|
+
const out = {
|
|
58
|
+
url: response.url,
|
|
59
|
+
status: response.status,
|
|
60
|
+
format: usedFormat,
|
|
61
|
+
content
|
|
62
|
+
};
|
|
63
|
+
if (typeof response.status_code === "number") out.statusCode = response.status_code;
|
|
64
|
+
if (data.links && data.links.length > 0) out.links = data.links;
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
47
67
|
|
|
48
68
|
// src/errors.ts
|
|
49
69
|
var NimbleConfigError = class extends Error {
|
|
@@ -60,6 +80,14 @@ var NimbleSearchError = class extends Error {
|
|
|
60
80
|
this.status = options?.status;
|
|
61
81
|
}
|
|
62
82
|
};
|
|
83
|
+
var NimbleExtractError = class extends Error {
|
|
84
|
+
status;
|
|
85
|
+
constructor(message, options) {
|
|
86
|
+
super(message, options?.cause !== void 0 ? { cause: options.cause } : void 0);
|
|
87
|
+
this.name = "NimbleExtractError";
|
|
88
|
+
this.status = options?.status;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
63
91
|
|
|
64
92
|
// src/nimble-search.ts
|
|
65
93
|
var NIMBLE_SEARCH_DEFAULTS = {
|
|
@@ -133,12 +161,71 @@ function nimbleSearch(config = {}) {
|
|
|
133
161
|
}
|
|
134
162
|
});
|
|
135
163
|
}
|
|
164
|
+
var NIMBLE_EXTRACT_DEFAULTS = {
|
|
165
|
+
format: "markdown",
|
|
166
|
+
maxContentLength: 5e4
|
|
167
|
+
};
|
|
168
|
+
function readStatus2(err) {
|
|
169
|
+
if (typeof err === "object" && err !== null && "status" in err) {
|
|
170
|
+
const status = err.status;
|
|
171
|
+
return typeof status === "number" ? status : void 0;
|
|
172
|
+
}
|
|
173
|
+
return void 0;
|
|
174
|
+
}
|
|
175
|
+
function toExtractError(err) {
|
|
176
|
+
if (err instanceof NimbleExtractError) return err;
|
|
177
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
178
|
+
return new NimbleExtractError(`Nimble extract failed: ${message}`, {
|
|
179
|
+
status: readStatus2(err),
|
|
180
|
+
cause: err
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
function resolveClient2(config) {
|
|
184
|
+
if (config.client) return config.client;
|
|
185
|
+
const apiKey = config.apiKey ?? process.env.NIMBLE_API_KEY;
|
|
186
|
+
if (!apiKey) {
|
|
187
|
+
throw new NimbleConfigError(
|
|
188
|
+
"Missing Nimble API key: set NIMBLE_API_KEY or pass { apiKey } to nimbleExtract()."
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
return new nimbleJs.Nimble({ apiKey });
|
|
192
|
+
}
|
|
193
|
+
function nimbleExtract(config = {}) {
|
|
194
|
+
const format = config.format ?? NIMBLE_EXTRACT_DEFAULTS.format;
|
|
195
|
+
const maxContentLength = config.maxContentLength ?? NIMBLE_EXTRACT_DEFAULTS.maxContentLength;
|
|
196
|
+
const country = config.country;
|
|
197
|
+
return ai.tool({
|
|
198
|
+
description: "Fetch a web page by URL with Nimble and return its clean, readable content (markdown or HTML) \u2014 use this to read, quote, or summarize a specific page.",
|
|
199
|
+
inputSchema: nimbleExtractInputSchema,
|
|
200
|
+
execute: async (input) => {
|
|
201
|
+
const client = resolveClient2(config);
|
|
202
|
+
const params = {
|
|
203
|
+
url: input.url,
|
|
204
|
+
formats: format === "html" ? ["html", "markdown", "links"] : ["markdown", "html", "links"]
|
|
205
|
+
};
|
|
206
|
+
if (country) params.country = country;
|
|
207
|
+
if (format === "markdown") params.markdown_backend = "main_content";
|
|
208
|
+
let raw;
|
|
209
|
+
try {
|
|
210
|
+
raw = await client.extract(params);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
throw toExtractError(err);
|
|
213
|
+
}
|
|
214
|
+
return normalizeExtractResponse(raw, { format, maxContentLength });
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
136
218
|
|
|
219
|
+
exports.NIMBLE_EXTRACT_DEFAULTS = NIMBLE_EXTRACT_DEFAULTS;
|
|
137
220
|
exports.NIMBLE_SEARCH_DEFAULTS = NIMBLE_SEARCH_DEFAULTS;
|
|
138
221
|
exports.NimbleConfigError = NimbleConfigError;
|
|
222
|
+
exports.NimbleExtractError = NimbleExtractError;
|
|
139
223
|
exports.NimbleSearchError = NimbleSearchError;
|
|
224
|
+
exports.nimbleExtract = nimbleExtract;
|
|
225
|
+
exports.nimbleExtractInputSchema = nimbleExtractInputSchema;
|
|
140
226
|
exports.nimbleSearch = nimbleSearch;
|
|
141
227
|
exports.nimbleSearchInputSchema = nimbleSearchInputSchema;
|
|
228
|
+
exports.normalizeExtractResponse = normalizeExtractResponse;
|
|
142
229
|
exports.normalizeSearchResponse = normalizeSearchResponse;
|
|
143
230
|
//# sourceMappingURL=index.cjs.map
|
|
144
231
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/schemas.ts","../src/normalize.ts","../src/errors.ts","../src/nimble-search.ts"],"names":["z","Nimble","tool"],"mappings":"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","file":"index.cjs","sourcesContent":["import { z } from 'zod';\n\n/**\n * The tool input the model fills in. Kept deliberately small: the model only\n * chooses the query and (optionally) how many results it wants. All policy\n * (depth, focus, region, caps) is fixed by the developer via the factory\n * config, not by the model.\n */\nexport const nimbleSearchInputSchema = z.object({\n query: z.string().min(1).describe('The web search query.'),\n maxResults: z\n .number()\n .int()\n .positive()\n .optional()\n .describe('How many results to return (clamped to the developer-configured cap).'),\n});\n\nexport type NimbleSearchInput = z.infer<typeof nimbleSearchInputSchema>;\n\n/**\n * v1 exposes only the two non-enterprise depths. `fast` is enterprise-gated and\n * intentionally not offered here.\n */\nexport type SearchDepth = 'lite' | 'deep';\n\n/**\n * Developer-facing factory config. `focus` is fixed to `general` in v1 and is\n * not exposed. `include_answer` and `search_depth: 'fast'` are intentionally\n * absent (enterprise / unverified-entitlement surface).\n */\nexport interface NimbleSearchToolConfig {\n /** Nimble API key. Defaults to `process.env.NIMBLE_API_KEY`. */\n apiKey?: string;\n /** Inject a pre-built / mock Nimble client (tests, advanced users). */\n client?: NimbleSearchClient;\n /** Default number of results when the model doesn't specify. Default 5. */\n maxResults?: number;\n /** Hard upper bound on results, regardless of model request. Default 10. */\n maxResultsCap?: number;\n /** Search depth. Default `lite`. */\n searchDepth?: SearchDepth;\n /** ISO country for result localization. Default `US`. */\n country?: string;\n /** Locale for result localization. Default `en`. */\n locale?: string;\n /** Truncate each result's body to this many characters. Default 10_000. */\n maxContentLength?: number;\n}\n\n/**\n * Structural surface of `@nimble-way/nimble-js`'s `client.search()` that this\n * package relies on. Declared structurally so the scaffold typechecks without\n * pinning to the SDK's generated type names, and so tests can inject a mock.\n *\n * Phase B: reconcile field casing/names against\n * `sdks/nimble-js/checkout/src/` after `./tools/sync-sdk.sh nimble-js`.\n */\nexport interface NimbleSearchParams {\n query: string;\n max_results?: number;\n search_depth?: SearchDepth;\n focus?: string;\n country?: string;\n locale?: string;\n}\n\n/** Metadata for SERP-based results (general/news/location focus). */\nexport interface NimbleSerpMetadata {\n country: string;\n entity_type: string;\n locale: string;\n position: number;\n driver?: string | null;\n}\n\n/** Metadata for WSA-based results (shopping/social/geo focus). */\nexport interface NimbleWsaMetadata {\n agent_name: string;\n}\n\nexport interface NimbleRawSearchResult {\n /** Full page text in `deep`; may be empty in `lite`. */\n content: string;\n description: string;\n title: string;\n url: string;\n /** SERP focus (v1 `general`) yields {@link NimbleSerpMetadata}. */\n metadata: NimbleSerpMetadata | NimbleWsaMetadata;\n /** Platform-specific extras (price, publish_date, …); omitted when none. */\n additional_data?: Record<string, unknown> | null;\n}\n\nexport interface NimbleRawSearchResponse {\n request_id: string;\n results: NimbleRawSearchResult[];\n total_results: number;\n /** Intentionally never surfaced in v1 (include_answer is off). */\n answer?: string | null;\n}\n\nexport interface NimbleSearchClient {\n search(params: NimbleSearchParams): Promise<NimbleRawSearchResponse>;\n}\n\n/** A single normalized result item returned to the model. */\nexport interface NimbleSearchResultItem {\n title: string;\n url: string;\n description?: string;\n content?: string;\n position?: number;\n entityType?: string;\n}\n\n/** The normalized tool output. `answer` is intentionally omitted in v1. */\nexport interface NimbleSearchOutput {\n query: string;\n requestId?: string;\n totalResults?: number;\n results: NimbleSearchResultItem[];\n}\n","import type {\n NimbleRawSearchResponse,\n NimbleRawSearchResult,\n NimbleSearchOutput,\n NimbleSearchResultItem,\n NimbleSerpMetadata,\n} from './schemas';\n\nexport interface NormalizeOptions {\n query: string;\n maxContentLength: number;\n}\n\n/** SERP focus (v1 `general`) carries position + entity_type; WSA does not. */\nfunction isSerpMetadata(\n metadata: NimbleRawSearchResult['metadata'],\n): metadata is NimbleSerpMetadata {\n return 'position' in metadata;\n}\n\nfunction truncate(text: string, max: number): string {\n return text.length > max ? text.slice(0, max) : text;\n}\n\n/**\n * Map a raw `/v1/search` response into the package's normalized output shape.\n *\n * - `description` is the snippet (always present when the API returns one).\n * - `content` is the full page text, present only in `deep` depth (empty in\n * `lite`), truncated to `maxContentLength`.\n * - `position` / `entityType` come from SERP metadata; for WSA results\n * `position` falls back to the array index and `entityType` is omitted.\n * - Results without a URL are dropped (defensive).\n * - `response.answer` is never surfaced in v1.\n */\nexport function normalizeSearchResponse(\n response: NimbleRawSearchResponse,\n options: NormalizeOptions,\n): NimbleSearchOutput {\n const results: NimbleSearchResultItem[] = [];\n\n response.results.forEach((raw, index) => {\n if (!raw.url) return;\n\n const item: NimbleSearchResultItem = {\n title: raw.title ?? '',\n url: raw.url,\n };\n if (raw.description) item.description = raw.description;\n if (raw.content && raw.content.trim().length > 0) {\n item.content = truncate(raw.content, options.maxContentLength);\n }\n\n if (isSerpMetadata(raw.metadata)) {\n item.position = raw.metadata.position;\n item.entityType = raw.metadata.entity_type;\n } else {\n item.position = index + 1;\n }\n\n results.push(item);\n });\n\n return {\n query: options.query,\n requestId: response.request_id,\n totalResults: response.total_results,\n results,\n };\n}\n","/**\n * Thrown when the tool is invoked without a resolvable API key (no `apiKey`\n * config and no `NIMBLE_API_KEY` in the environment) and no injected client.\n * Raised at execute time, not at factory-construction time, so the tool can be\n * constructed in environments without a key (e.g. unit tests, type-checking).\n */\nexport class NimbleConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'NimbleConfigError';\n }\n}\n\n/**\n * Wraps an error surfaced by the Nimble client / API during a search call,\n * preserving the HTTP status when available. The AI SDK surfaces a thrown\n * tool error back to the model as a tool-call failure.\n */\nexport class NimbleSearchError extends Error {\n readonly status?: number;\n\n constructor(message: string, options?: { status?: number; cause?: unknown }) {\n super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);\n this.name = 'NimbleSearchError';\n this.status = options?.status;\n }\n}\n","import { tool } from 'ai';\nimport { Nimble } from '@nimble-way/nimble-js';\nimport { nimbleSearchInputSchema } from './schemas';\nimport type {\n NimbleSearchClient,\n NimbleSearchOutput,\n NimbleSearchParams,\n NimbleSearchToolConfig,\n} from './schemas';\nimport { normalizeSearchResponse } from './normalize';\nimport { NimbleConfigError, NimbleSearchError } from './errors';\n\n/** v1 factory defaults. `focus` is fixed to `general` and not user-exposed. */\nexport const NIMBLE_SEARCH_DEFAULTS = {\n maxResults: 5,\n maxResultsCap: 10,\n searchDepth: 'lite',\n country: 'US',\n locale: 'en',\n maxContentLength: 10_000,\n focus: 'general',\n} as const;\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max);\n}\n\nfunction readStatus(err: unknown): number | undefined {\n if (typeof err === 'object' && err !== null && 'status' in err) {\n const status = (err as { status?: unknown }).status;\n return typeof status === 'number' ? status : undefined;\n }\n return undefined;\n}\n\nfunction toSearchError(err: unknown): NimbleSearchError {\n if (err instanceof NimbleSearchError) return err;\n const message = err instanceof Error ? err.message : String(err);\n return new NimbleSearchError(`Nimble search failed: ${message}`, {\n status: readStatus(err),\n cause: err,\n });\n}\n\nfunction resolveClient(config: NimbleSearchToolConfig): NimbleSearchClient {\n if (config.client) return config.client;\n const apiKey = config.apiKey ?? process.env.NIMBLE_API_KEY;\n if (!apiKey) {\n throw new NimbleConfigError(\n 'Missing Nimble API key: set NIMBLE_API_KEY or pass { apiKey } to nimbleSearch().',\n );\n }\n return new Nimble({ apiKey }) as unknown as NimbleSearchClient;\n}\n\n/**\n * Create a ready-made Vercel AI SDK web-search tool backed by Nimble Search\n * (`@nimble-way/nimble-js` → `POST /v1/search`).\n *\n * The model only chooses `{ query, maxResults? }`; all policy (depth, focus,\n * region, caps) is fixed by `config` — mirroring the Exa `webSearch()` shape.\n *\n * @example\n * ```ts\n * import { generateText } from 'ai';\n * import { nimbleSearch } from '@nimble-way/ai-sdk';\n *\n * const { text } = await generateText({\n * model: 'anthropic/claude-sonnet-4.6',\n * prompt: 'What are the latest Nimble release notes?',\n * tools: { webSearch: nimbleSearch({ searchDepth: 'lite', maxResults: 5 }) },\n * });\n * ```\n */\nexport function nimbleSearch(config: NimbleSearchToolConfig = {}) {\n const maxResults = config.maxResults ?? NIMBLE_SEARCH_DEFAULTS.maxResults;\n const maxResultsCap = config.maxResultsCap ?? NIMBLE_SEARCH_DEFAULTS.maxResultsCap;\n const searchDepth = config.searchDepth ?? NIMBLE_SEARCH_DEFAULTS.searchDepth;\n const country = config.country ?? NIMBLE_SEARCH_DEFAULTS.country;\n const locale = config.locale ?? NIMBLE_SEARCH_DEFAULTS.locale;\n const maxContentLength = config.maxContentLength ?? NIMBLE_SEARCH_DEFAULTS.maxContentLength;\n\n return tool({\n description:\n 'Search the web with Nimble and return ranked results (title, url, ' +\n 'snippet, and page content) for answering questions about current or ' +\n 'factual information.',\n inputSchema: nimbleSearchInputSchema,\n execute: async (input): Promise<NimbleSearchOutput> => {\n const client = resolveClient(config);\n\n const params: NimbleSearchParams = {\n query: input.query,\n max_results: clamp(input.maxResults ?? maxResults, 1, maxResultsCap),\n search_depth: searchDepth,\n focus: NIMBLE_SEARCH_DEFAULTS.focus, // fixed 'general' in v1\n country,\n locale,\n };\n\n let raw;\n try {\n raw = await client.search(params);\n } catch (err) {\n throw toSearchError(err);\n }\n\n return normalizeSearchResponse(raw, {\n query: input.query,\n maxContentLength,\n });\n },\n });\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/schemas.ts","../src/normalize.ts","../src/errors.ts","../src/nimble-search.ts","../src/nimble-extract.ts"],"names":["z","Nimble","tool","readStatus","resolveClient"],"mappings":"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","file":"index.cjs","sourcesContent":["import { z } from 'zod';\n\n/**\n * The tool input the model fills in. Kept deliberately small: the model only\n * chooses the query and (optionally) how many results it wants. All policy\n * (depth, focus, region, caps) is fixed by the developer via the factory\n * config, not by the model.\n */\nexport const nimbleSearchInputSchema = z.object({\n query: z.string().min(1).describe('The web search query.'),\n maxResults: z\n .number()\n .int()\n .positive()\n .optional()\n .describe('How many results to return (clamped to the developer-configured cap).'),\n});\n\nexport type NimbleSearchInput = z.infer<typeof nimbleSearchInputSchema>;\n\n/**\n * v1 exposes only the two non-enterprise depths. `fast` is enterprise-gated and\n * intentionally not offered here.\n */\nexport type SearchDepth = 'lite' | 'deep';\n\n/**\n * Developer-facing factory config. `focus` is fixed to `general` in v1 and is\n * not exposed. `include_answer` and `search_depth: 'fast'` are intentionally\n * absent (enterprise / unverified-entitlement surface).\n */\nexport interface NimbleSearchToolConfig {\n /** Nimble API key. Defaults to `process.env.NIMBLE_API_KEY`. */\n apiKey?: string;\n /** Inject a pre-built / mock Nimble client (tests, advanced users). */\n client?: NimbleSearchClient;\n /** Default number of results when the model doesn't specify. Default 5. */\n maxResults?: number;\n /** Hard upper bound on results, regardless of model request. Default 10. */\n maxResultsCap?: number;\n /** Search depth. Default `lite`. */\n searchDepth?: SearchDepth;\n /** ISO country for result localization. Default `US`. */\n country?: string;\n /** Locale for result localization. Default `en`. */\n locale?: string;\n /** Truncate each result's body to this many characters. Default 10_000. */\n maxContentLength?: number;\n}\n\n/**\n * Structural surface of `@nimble-way/nimble-js`'s `client.search()` that this\n * package relies on. Declared structurally so the scaffold typechecks without\n * pinning to the SDK's generated type names, and so tests can inject a mock.\n *\n * Phase B: reconcile field casing/names against\n * `sdks/nimble-js/checkout/src/` after `./tools/sync-sdk.sh nimble-js`.\n */\nexport interface NimbleSearchParams {\n query: string;\n max_results?: number;\n search_depth?: SearchDepth;\n focus?: string;\n country?: string;\n locale?: string;\n}\n\n/** Metadata for SERP-based results (general/news/location focus). */\nexport interface NimbleSerpMetadata {\n country: string;\n entity_type: string;\n locale: string;\n position: number;\n driver?: string | null;\n}\n\n/** Metadata for WSA-based results (shopping/social/geo focus). */\nexport interface NimbleWsaMetadata {\n agent_name: string;\n}\n\nexport interface NimbleRawSearchResult {\n /** Full page text in `deep`; may be empty in `lite`. */\n content: string;\n description: string;\n title: string;\n url: string;\n /** SERP focus (v1 `general`) yields {@link NimbleSerpMetadata}. */\n metadata: NimbleSerpMetadata | NimbleWsaMetadata;\n /** Platform-specific extras (price, publish_date, …); omitted when none. */\n additional_data?: Record<string, unknown> | null;\n}\n\nexport interface NimbleRawSearchResponse {\n request_id: string;\n results: NimbleRawSearchResult[];\n total_results: number;\n /** Intentionally never surfaced in v1 (include_answer is off). */\n answer?: string | null;\n}\n\nexport interface NimbleSearchClient {\n search(params: NimbleSearchParams): Promise<NimbleRawSearchResponse>;\n}\n\n/** A single normalized result item returned to the model. */\nexport interface NimbleSearchResultItem {\n title: string;\n url: string;\n description?: string;\n content?: string;\n position?: number;\n entityType?: string;\n}\n\n/** The normalized tool output. `answer` is intentionally omitted in v1. */\nexport interface NimbleSearchOutput {\n query: string;\n requestId?: string;\n totalResults?: number;\n results: NimbleSearchResultItem[];\n}\n\n// ── Extract ────────────────────────────────────────────────────────────────\n\n/** Output format for extracted page content. */\nexport type ExtractFormat = 'markdown' | 'html';\n\n/**\n * The extract tool input the model fills in: just the URL to read. All policy\n * (format, region, length cap) is fixed by the developer via the factory config.\n */\nexport const nimbleExtractInputSchema = z.object({\n url: z.string().url().describe('The URL of the web page to extract clean content from.'),\n});\n\nexport type NimbleExtractInput = z.infer<typeof nimbleExtractInputSchema>;\n\n/** Developer-facing factory config for the extract tool. */\nexport interface NimbleExtractToolConfig {\n /** Nimble API key. Defaults to `process.env.NIMBLE_API_KEY`. */\n apiKey?: string;\n /** Inject a pre-built / mock Nimble client (tests, advanced users). */\n client?: NimbleExtractClient;\n /** Content format. Default `markdown`. */\n format?: ExtractFormat;\n /** ISO country for geolocation / proxy selection. */\n country?: string;\n /** Truncate the extracted content to this many characters. Default 50_000. */\n maxContentLength?: number;\n}\n\n/** Params this package sends to the SDK's `client.extract()`. */\nexport interface NimbleExtractParams {\n url: string;\n country?: string;\n /** Which renderings to request; `data.<format>` is populated per entry. */\n formats?: Array<'html' | 'markdown' | 'links'>;\n /** Refines Markdown extraction; `main_content` yields the cleaned article. */\n markdown_backend?: 'full_page' | 'main_content';\n}\n\n/** Structural surface of the SDK extract response data this package consumes. */\nexport interface NimbleRawExtractData {\n /** Markdown rendering of the page (default). */\n markdown?: string;\n /** Raw HTML of the page. */\n html?: string;\n /** Unique URLs found on the page. */\n links?: string[];\n}\n\nexport interface NimbleRawExtractResponse {\n url: string;\n status: string;\n status_code?: number;\n task_id: string;\n data: NimbleRawExtractData;\n warnings?: string[];\n}\n\nexport interface NimbleExtractClient {\n extract(params: NimbleExtractParams): Promise<NimbleRawExtractResponse>;\n}\n\n/** The normalized extract output returned to the model. */\nexport interface NimbleExtractOutput {\n /** The final URL (after redirects). */\n url: string;\n /** Task status reported by Nimble (e.g. `success`). */\n status: string;\n statusCode?: number;\n format: ExtractFormat;\n /** The extracted page content in the requested format, truncated. */\n content: string;\n /** Unique links found on the page, when available. */\n links?: string[];\n}\n","import type {\n ExtractFormat,\n NimbleExtractOutput,\n NimbleRawExtractResponse,\n NimbleRawSearchResponse,\n NimbleRawSearchResult,\n NimbleSearchOutput,\n NimbleSearchResultItem,\n NimbleSerpMetadata,\n} from './schemas';\n\nexport interface NormalizeOptions {\n query: string;\n maxContentLength: number;\n}\n\n/** SERP focus (v1 `general`) carries position + entity_type; WSA does not. */\nfunction isSerpMetadata(\n metadata: NimbleRawSearchResult['metadata'],\n): metadata is NimbleSerpMetadata {\n return 'position' in metadata;\n}\n\nfunction truncate(text: string, max: number): string {\n return text.length > max ? text.slice(0, max) : text;\n}\n\n/**\n * Map a raw `/v1/search` response into the package's normalized output shape.\n *\n * - `description` is the snippet (always present when the API returns one).\n * - `content` is the full page text, present only in `deep` depth (empty in\n * `lite`), truncated to `maxContentLength`.\n * - `position` / `entityType` come from SERP metadata; for WSA results\n * `position` falls back to the array index and `entityType` is omitted.\n * - Results without a URL are dropped (defensive).\n * - `response.answer` is never surfaced in v1.\n */\nexport function normalizeSearchResponse(\n response: NimbleRawSearchResponse,\n options: NormalizeOptions,\n): NimbleSearchOutput {\n const results: NimbleSearchResultItem[] = [];\n\n response.results.forEach((raw, index) => {\n if (!raw.url) return;\n\n const item: NimbleSearchResultItem = {\n title: raw.title ?? '',\n url: raw.url,\n };\n if (raw.description) item.description = raw.description;\n if (raw.content && raw.content.trim().length > 0) {\n item.content = truncate(raw.content, options.maxContentLength);\n }\n\n if (isSerpMetadata(raw.metadata)) {\n item.position = raw.metadata.position;\n item.entityType = raw.metadata.entity_type;\n } else {\n item.position = index + 1;\n }\n\n results.push(item);\n });\n\n return {\n query: options.query,\n requestId: response.request_id,\n totalResults: response.total_results,\n results,\n };\n}\n\nexport interface NormalizeExtractOptions {\n format: ExtractFormat;\n maxContentLength: number;\n}\n\n/**\n * Map a raw `/v1/extract` response into the package's normalized extract shape.\n *\n * - `content` is `data.markdown` (default) or `data.html`, falling back to the\n * other when the requested one is empty, truncated to `maxContentLength`.\n * - `format` reflects the rendering actually returned, which may differ from\n * the requested format when the fallback is used.\n * - `links` is surfaced when present; everything else (browser actions, network\n * captures, screenshots) is intentionally dropped.\n */\nexport function normalizeExtractResponse(\n response: NimbleRawExtractResponse,\n options: NormalizeExtractOptions,\n): NimbleExtractOutput {\n const data = response.data;\n const otherFormat: ExtractFormat = options.format === 'html' ? 'markdown' : 'html';\n const primary = options.format === 'html' ? data.html : data.markdown;\n const fallback = options.format === 'html' ? data.markdown : data.html;\n\n // Report the format that actually populated `content`. We request one\n // rendering, but if the API returns it empty we fall back to the other — and\n // the model must be told which format it received, not the one we asked for.\n const usedFormat: ExtractFormat =\n primary && primary.length > 0\n ? options.format\n : fallback && fallback.length > 0\n ? otherFormat\n : options.format;\n const content = truncate(primary || fallback || '', options.maxContentLength);\n\n const out: NimbleExtractOutput = {\n url: response.url,\n status: response.status,\n format: usedFormat,\n content,\n };\n if (typeof response.status_code === 'number') out.statusCode = response.status_code;\n if (data.links && data.links.length > 0) out.links = data.links;\n return out;\n}\n","/**\n * Thrown when the tool is invoked without a resolvable API key (no `apiKey`\n * config and no `NIMBLE_API_KEY` in the environment) and no injected client.\n * Raised at execute time, not at factory-construction time, so the tool can be\n * constructed in environments without a key (e.g. unit tests, type-checking).\n */\nexport class NimbleConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'NimbleConfigError';\n }\n}\n\n/**\n * Wraps an error surfaced by the Nimble client / API during a search call,\n * preserving the HTTP status when available. The AI SDK surfaces a thrown\n * tool error back to the model as a tool-call failure.\n */\nexport class NimbleSearchError extends Error {\n readonly status?: number;\n\n constructor(message: string, options?: { status?: number; cause?: unknown }) {\n super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);\n this.name = 'NimbleSearchError';\n this.status = options?.status;\n }\n}\n\n/**\n * Wraps an error surfaced by the Nimble client / API during an extract call,\n * preserving the HTTP status when available.\n */\nexport class NimbleExtractError extends Error {\n readonly status?: number;\n\n constructor(message: string, options?: { status?: number; cause?: unknown }) {\n super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);\n this.name = 'NimbleExtractError';\n this.status = options?.status;\n }\n}\n","import { tool } from 'ai';\nimport { Nimble } from '@nimble-way/nimble-js';\nimport { nimbleSearchInputSchema } from './schemas';\nimport type {\n NimbleSearchClient,\n NimbleSearchOutput,\n NimbleSearchParams,\n NimbleSearchToolConfig,\n} from './schemas';\nimport { normalizeSearchResponse } from './normalize';\nimport { NimbleConfigError, NimbleSearchError } from './errors';\n\n/** v1 factory defaults. `focus` is fixed to `general` and not user-exposed. */\nexport const NIMBLE_SEARCH_DEFAULTS = {\n maxResults: 5,\n maxResultsCap: 10,\n searchDepth: 'lite',\n country: 'US',\n locale: 'en',\n maxContentLength: 10_000,\n focus: 'general',\n} as const;\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max);\n}\n\nfunction readStatus(err: unknown): number | undefined {\n if (typeof err === 'object' && err !== null && 'status' in err) {\n const status = (err as { status?: unknown }).status;\n return typeof status === 'number' ? status : undefined;\n }\n return undefined;\n}\n\nfunction toSearchError(err: unknown): NimbleSearchError {\n if (err instanceof NimbleSearchError) return err;\n const message = err instanceof Error ? err.message : String(err);\n return new NimbleSearchError(`Nimble search failed: ${message}`, {\n status: readStatus(err),\n cause: err,\n });\n}\n\nfunction resolveClient(config: NimbleSearchToolConfig): NimbleSearchClient {\n if (config.client) return config.client;\n const apiKey = config.apiKey ?? process.env.NIMBLE_API_KEY;\n if (!apiKey) {\n throw new NimbleConfigError(\n 'Missing Nimble API key: set NIMBLE_API_KEY or pass { apiKey } to nimbleSearch().',\n );\n }\n return new Nimble({ apiKey }) as unknown as NimbleSearchClient;\n}\n\n/**\n * Create a ready-made Vercel AI SDK web-search tool backed by Nimble Search\n * (`@nimble-way/nimble-js` → `POST /v1/search`).\n *\n * The model only chooses `{ query, maxResults? }`; all policy (depth, focus,\n * region, caps) is fixed by `config` — mirroring the Exa `webSearch()` shape.\n *\n * @example\n * ```ts\n * import { generateText } from 'ai';\n * import { nimbleSearch } from '@nimble-way/ai-sdk';\n *\n * const { text } = await generateText({\n * model: 'anthropic/claude-sonnet-4.6',\n * prompt: 'What are the latest Nimble release notes?',\n * tools: { webSearch: nimbleSearch({ searchDepth: 'lite', maxResults: 5 }) },\n * });\n * ```\n */\nexport function nimbleSearch(config: NimbleSearchToolConfig = {}) {\n const maxResults = config.maxResults ?? NIMBLE_SEARCH_DEFAULTS.maxResults;\n const maxResultsCap = config.maxResultsCap ?? NIMBLE_SEARCH_DEFAULTS.maxResultsCap;\n const searchDepth = config.searchDepth ?? NIMBLE_SEARCH_DEFAULTS.searchDepth;\n const country = config.country ?? NIMBLE_SEARCH_DEFAULTS.country;\n const locale = config.locale ?? NIMBLE_SEARCH_DEFAULTS.locale;\n const maxContentLength = config.maxContentLength ?? NIMBLE_SEARCH_DEFAULTS.maxContentLength;\n\n return tool({\n description:\n 'Search the web with Nimble and return ranked results (title, url, ' +\n 'snippet, and page content) for answering questions about current or ' +\n 'factual information.',\n inputSchema: nimbleSearchInputSchema,\n execute: async (input): Promise<NimbleSearchOutput> => {\n const client = resolveClient(config);\n\n const params: NimbleSearchParams = {\n query: input.query,\n max_results: clamp(input.maxResults ?? maxResults, 1, maxResultsCap),\n search_depth: searchDepth,\n focus: NIMBLE_SEARCH_DEFAULTS.focus, // fixed 'general' in v1\n country,\n locale,\n };\n\n let raw;\n try {\n raw = await client.search(params);\n } catch (err) {\n throw toSearchError(err);\n }\n\n return normalizeSearchResponse(raw, {\n query: input.query,\n maxContentLength,\n });\n },\n });\n}\n","import { tool } from 'ai';\nimport { Nimble } from '@nimble-way/nimble-js';\nimport { nimbleExtractInputSchema } from './schemas';\nimport type {\n ExtractFormat,\n NimbleExtractClient,\n NimbleExtractOutput,\n NimbleExtractParams,\n NimbleExtractToolConfig,\n} from './schemas';\nimport { normalizeExtractResponse } from './normalize';\nimport { NimbleConfigError, NimbleExtractError } from './errors';\n\n/** v1 extract defaults. */\nexport const NIMBLE_EXTRACT_DEFAULTS: { format: ExtractFormat; maxContentLength: number } = {\n format: 'markdown',\n maxContentLength: 50_000,\n};\n\nfunction readStatus(err: unknown): number | undefined {\n if (typeof err === 'object' && err !== null && 'status' in err) {\n const status = (err as { status?: unknown }).status;\n return typeof status === 'number' ? status : undefined;\n }\n return undefined;\n}\n\nfunction toExtractError(err: unknown): NimbleExtractError {\n if (err instanceof NimbleExtractError) return err;\n const message = err instanceof Error ? err.message : String(err);\n return new NimbleExtractError(`Nimble extract failed: ${message}`, {\n status: readStatus(err),\n cause: err,\n });\n}\n\nfunction resolveClient(config: NimbleExtractToolConfig): NimbleExtractClient {\n if (config.client) return config.client;\n const apiKey = config.apiKey ?? process.env.NIMBLE_API_KEY;\n if (!apiKey) {\n throw new NimbleConfigError(\n 'Missing Nimble API key: set NIMBLE_API_KEY or pass { apiKey } to nimbleExtract().',\n );\n }\n return new Nimble({ apiKey }) as unknown as NimbleExtractClient;\n}\n\n/**\n * Create a Vercel AI SDK tool that extracts clean, readable content from a web\n * page with Nimble (`@nimble-way/nimble-js` → `POST /v1/extract`).\n *\n * The model only chooses `{ url }`; format, region, and length cap are fixed by\n * `config`. Mirrors {@link nimbleSearch}'s ergonomics.\n *\n * @example\n * ```ts\n * import { generateText } from 'ai';\n * import { nimbleExtract } from '@nimble-way/ai-sdk';\n *\n * const { text } = await generateText({\n * model: 'openai/gpt-4o-mini',\n * prompt: 'Summarize https://nimbleway.com',\n * tools: { extract: nimbleExtract() },\n * });\n * ```\n */\nexport function nimbleExtract(config: NimbleExtractToolConfig = {}) {\n const format = config.format ?? NIMBLE_EXTRACT_DEFAULTS.format;\n const maxContentLength = config.maxContentLength ?? NIMBLE_EXTRACT_DEFAULTS.maxContentLength;\n const country = config.country;\n\n return tool({\n description:\n 'Fetch a web page by URL with Nimble and return its clean, readable content ' +\n '(markdown or HTML) — use this to read, quote, or summarize a specific page.',\n inputSchema: nimbleExtractInputSchema,\n execute: async (input): Promise<NimbleExtractOutput> => {\n const client = resolveClient(config);\n\n // Request both renderings (preferred format first) plus links. A rendering\n // is only produced when requested via `formats`, so asking for both lets an\n // empty primary fall back to the other; normalizeExtractResponse reports\n // whichever rendering actually populated the content.\n const params: NimbleExtractParams = {\n url: input.url,\n formats:\n format === 'html' ? ['html', 'markdown', 'links'] : ['markdown', 'html', 'links'],\n };\n if (country) params.country = country;\n // `main_content` returns the cleaned article body rather than the full page.\n if (format === 'markdown') params.markdown_backend = 'main_content';\n\n let raw;\n try {\n raw = await client.extract(params);\n } catch (err) {\n throw toExtractError(err);\n }\n\n return normalizeExtractResponse(raw, { format, maxContentLength });\n },\n });\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -111,6 +111,75 @@ interface NimbleSearchOutput {
|
|
|
111
111
|
totalResults?: number;
|
|
112
112
|
results: NimbleSearchResultItem[];
|
|
113
113
|
}
|
|
114
|
+
/** Output format for extracted page content. */
|
|
115
|
+
type ExtractFormat = 'markdown' | 'html';
|
|
116
|
+
/**
|
|
117
|
+
* The extract tool input the model fills in: just the URL to read. All policy
|
|
118
|
+
* (format, region, length cap) is fixed by the developer via the factory config.
|
|
119
|
+
*/
|
|
120
|
+
declare const nimbleExtractInputSchema: z.ZodObject<{
|
|
121
|
+
url: z.ZodString;
|
|
122
|
+
}, "strip", z.ZodTypeAny, {
|
|
123
|
+
url: string;
|
|
124
|
+
}, {
|
|
125
|
+
url: string;
|
|
126
|
+
}>;
|
|
127
|
+
type NimbleExtractInput = z.infer<typeof nimbleExtractInputSchema>;
|
|
128
|
+
/** Developer-facing factory config for the extract tool. */
|
|
129
|
+
interface NimbleExtractToolConfig {
|
|
130
|
+
/** Nimble API key. Defaults to `process.env.NIMBLE_API_KEY`. */
|
|
131
|
+
apiKey?: string;
|
|
132
|
+
/** Inject a pre-built / mock Nimble client (tests, advanced users). */
|
|
133
|
+
client?: NimbleExtractClient;
|
|
134
|
+
/** Content format. Default `markdown`. */
|
|
135
|
+
format?: ExtractFormat;
|
|
136
|
+
/** ISO country for geolocation / proxy selection. */
|
|
137
|
+
country?: string;
|
|
138
|
+
/** Truncate the extracted content to this many characters. Default 50_000. */
|
|
139
|
+
maxContentLength?: number;
|
|
140
|
+
}
|
|
141
|
+
/** Params this package sends to the SDK's `client.extract()`. */
|
|
142
|
+
interface NimbleExtractParams {
|
|
143
|
+
url: string;
|
|
144
|
+
country?: string;
|
|
145
|
+
/** Which renderings to request; `data.<format>` is populated per entry. */
|
|
146
|
+
formats?: Array<'html' | 'markdown' | 'links'>;
|
|
147
|
+
/** Refines Markdown extraction; `main_content` yields the cleaned article. */
|
|
148
|
+
markdown_backend?: 'full_page' | 'main_content';
|
|
149
|
+
}
|
|
150
|
+
/** Structural surface of the SDK extract response data this package consumes. */
|
|
151
|
+
interface NimbleRawExtractData {
|
|
152
|
+
/** Markdown rendering of the page (default). */
|
|
153
|
+
markdown?: string;
|
|
154
|
+
/** Raw HTML of the page. */
|
|
155
|
+
html?: string;
|
|
156
|
+
/** Unique URLs found on the page. */
|
|
157
|
+
links?: string[];
|
|
158
|
+
}
|
|
159
|
+
interface NimbleRawExtractResponse {
|
|
160
|
+
url: string;
|
|
161
|
+
status: string;
|
|
162
|
+
status_code?: number;
|
|
163
|
+
task_id: string;
|
|
164
|
+
data: NimbleRawExtractData;
|
|
165
|
+
warnings?: string[];
|
|
166
|
+
}
|
|
167
|
+
interface NimbleExtractClient {
|
|
168
|
+
extract(params: NimbleExtractParams): Promise<NimbleRawExtractResponse>;
|
|
169
|
+
}
|
|
170
|
+
/** The normalized extract output returned to the model. */
|
|
171
|
+
interface NimbleExtractOutput {
|
|
172
|
+
/** The final URL (after redirects). */
|
|
173
|
+
url: string;
|
|
174
|
+
/** Task status reported by Nimble (e.g. `success`). */
|
|
175
|
+
status: string;
|
|
176
|
+
statusCode?: number;
|
|
177
|
+
format: ExtractFormat;
|
|
178
|
+
/** The extracted page content in the requested format, truncated. */
|
|
179
|
+
content: string;
|
|
180
|
+
/** Unique links found on the page, when available. */
|
|
181
|
+
links?: string[];
|
|
182
|
+
}
|
|
114
183
|
|
|
115
184
|
/** v1 factory defaults. `focus` is fixed to `general` and not user-exposed. */
|
|
116
185
|
declare const NIMBLE_SEARCH_DEFAULTS: {
|
|
@@ -146,6 +215,34 @@ declare function nimbleSearch(config?: NimbleSearchToolConfig): ai.Tool<{
|
|
|
146
215
|
maxResults?: number | undefined;
|
|
147
216
|
}, NimbleSearchOutput>;
|
|
148
217
|
|
|
218
|
+
/** v1 extract defaults. */
|
|
219
|
+
declare const NIMBLE_EXTRACT_DEFAULTS: {
|
|
220
|
+
format: ExtractFormat;
|
|
221
|
+
maxContentLength: number;
|
|
222
|
+
};
|
|
223
|
+
/**
|
|
224
|
+
* Create a Vercel AI SDK tool that extracts clean, readable content from a web
|
|
225
|
+
* page with Nimble (`@nimble-way/nimble-js` → `POST /v1/extract`).
|
|
226
|
+
*
|
|
227
|
+
* The model only chooses `{ url }`; format, region, and length cap are fixed by
|
|
228
|
+
* `config`. Mirrors {@link nimbleSearch}'s ergonomics.
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* ```ts
|
|
232
|
+
* import { generateText } from 'ai';
|
|
233
|
+
* import { nimbleExtract } from '@nimble-way/ai-sdk';
|
|
234
|
+
*
|
|
235
|
+
* const { text } = await generateText({
|
|
236
|
+
* model: 'openai/gpt-4o-mini',
|
|
237
|
+
* prompt: 'Summarize https://nimbleway.com',
|
|
238
|
+
* tools: { extract: nimbleExtract() },
|
|
239
|
+
* });
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
242
|
+
declare function nimbleExtract(config?: NimbleExtractToolConfig): ai.Tool<{
|
|
243
|
+
url: string;
|
|
244
|
+
}, NimbleExtractOutput>;
|
|
245
|
+
|
|
149
246
|
interface NormalizeOptions {
|
|
150
247
|
query: string;
|
|
151
248
|
maxContentLength: number;
|
|
@@ -162,6 +259,21 @@ interface NormalizeOptions {
|
|
|
162
259
|
* - `response.answer` is never surfaced in v1.
|
|
163
260
|
*/
|
|
164
261
|
declare function normalizeSearchResponse(response: NimbleRawSearchResponse, options: NormalizeOptions): NimbleSearchOutput;
|
|
262
|
+
interface NormalizeExtractOptions {
|
|
263
|
+
format: ExtractFormat;
|
|
264
|
+
maxContentLength: number;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Map a raw `/v1/extract` response into the package's normalized extract shape.
|
|
268
|
+
*
|
|
269
|
+
* - `content` is `data.markdown` (default) or `data.html`, falling back to the
|
|
270
|
+
* other when the requested one is empty, truncated to `maxContentLength`.
|
|
271
|
+
* - `format` reflects the rendering actually returned, which may differ from
|
|
272
|
+
* the requested format when the fallback is used.
|
|
273
|
+
* - `links` is surfaced when present; everything else (browser actions, network
|
|
274
|
+
* captures, screenshots) is intentionally dropped.
|
|
275
|
+
*/
|
|
276
|
+
declare function normalizeExtractResponse(response: NimbleRawExtractResponse, options: NormalizeExtractOptions): NimbleExtractOutput;
|
|
165
277
|
|
|
166
278
|
/**
|
|
167
279
|
* Thrown when the tool is invoked without a resolvable API key (no `apiKey`
|
|
@@ -184,5 +296,16 @@ declare class NimbleSearchError extends Error {
|
|
|
184
296
|
cause?: unknown;
|
|
185
297
|
});
|
|
186
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Wraps an error surfaced by the Nimble client / API during an extract call,
|
|
301
|
+
* preserving the HTTP status when available.
|
|
302
|
+
*/
|
|
303
|
+
declare class NimbleExtractError extends Error {
|
|
304
|
+
readonly status?: number;
|
|
305
|
+
constructor(message: string, options?: {
|
|
306
|
+
status?: number;
|
|
307
|
+
cause?: unknown;
|
|
308
|
+
});
|
|
309
|
+
}
|
|
187
310
|
|
|
188
|
-
export { NIMBLE_SEARCH_DEFAULTS, NimbleConfigError, type NimbleRawSearchResponse, type NimbleRawSearchResult, type NimbleSearchClient, NimbleSearchError, type NimbleSearchInput, type NimbleSearchOutput, type NimbleSearchParams, type NimbleSearchResultItem, type NimbleSearchToolConfig, type NimbleSerpMetadata, type NimbleWsaMetadata, type NormalizeOptions, type SearchDepth, nimbleSearch, nimbleSearchInputSchema, normalizeSearchResponse };
|
|
311
|
+
export { type ExtractFormat, NIMBLE_EXTRACT_DEFAULTS, NIMBLE_SEARCH_DEFAULTS, NimbleConfigError, type NimbleExtractClient, NimbleExtractError, type NimbleExtractInput, type NimbleExtractOutput, type NimbleExtractParams, type NimbleExtractToolConfig, type NimbleRawExtractData, type NimbleRawExtractResponse, type NimbleRawSearchResponse, type NimbleRawSearchResult, type NimbleSearchClient, NimbleSearchError, type NimbleSearchInput, type NimbleSearchOutput, type NimbleSearchParams, type NimbleSearchResultItem, type NimbleSearchToolConfig, type NimbleSerpMetadata, type NimbleWsaMetadata, type NormalizeExtractOptions, type NormalizeOptions, type SearchDepth, nimbleExtract, nimbleExtractInputSchema, nimbleSearch, nimbleSearchInputSchema, normalizeExtractResponse, normalizeSearchResponse };
|
package/dist/index.d.ts
CHANGED
|
@@ -111,6 +111,75 @@ interface NimbleSearchOutput {
|
|
|
111
111
|
totalResults?: number;
|
|
112
112
|
results: NimbleSearchResultItem[];
|
|
113
113
|
}
|
|
114
|
+
/** Output format for extracted page content. */
|
|
115
|
+
type ExtractFormat = 'markdown' | 'html';
|
|
116
|
+
/**
|
|
117
|
+
* The extract tool input the model fills in: just the URL to read. All policy
|
|
118
|
+
* (format, region, length cap) is fixed by the developer via the factory config.
|
|
119
|
+
*/
|
|
120
|
+
declare const nimbleExtractInputSchema: z.ZodObject<{
|
|
121
|
+
url: z.ZodString;
|
|
122
|
+
}, "strip", z.ZodTypeAny, {
|
|
123
|
+
url: string;
|
|
124
|
+
}, {
|
|
125
|
+
url: string;
|
|
126
|
+
}>;
|
|
127
|
+
type NimbleExtractInput = z.infer<typeof nimbleExtractInputSchema>;
|
|
128
|
+
/** Developer-facing factory config for the extract tool. */
|
|
129
|
+
interface NimbleExtractToolConfig {
|
|
130
|
+
/** Nimble API key. Defaults to `process.env.NIMBLE_API_KEY`. */
|
|
131
|
+
apiKey?: string;
|
|
132
|
+
/** Inject a pre-built / mock Nimble client (tests, advanced users). */
|
|
133
|
+
client?: NimbleExtractClient;
|
|
134
|
+
/** Content format. Default `markdown`. */
|
|
135
|
+
format?: ExtractFormat;
|
|
136
|
+
/** ISO country for geolocation / proxy selection. */
|
|
137
|
+
country?: string;
|
|
138
|
+
/** Truncate the extracted content to this many characters. Default 50_000. */
|
|
139
|
+
maxContentLength?: number;
|
|
140
|
+
}
|
|
141
|
+
/** Params this package sends to the SDK's `client.extract()`. */
|
|
142
|
+
interface NimbleExtractParams {
|
|
143
|
+
url: string;
|
|
144
|
+
country?: string;
|
|
145
|
+
/** Which renderings to request; `data.<format>` is populated per entry. */
|
|
146
|
+
formats?: Array<'html' | 'markdown' | 'links'>;
|
|
147
|
+
/** Refines Markdown extraction; `main_content` yields the cleaned article. */
|
|
148
|
+
markdown_backend?: 'full_page' | 'main_content';
|
|
149
|
+
}
|
|
150
|
+
/** Structural surface of the SDK extract response data this package consumes. */
|
|
151
|
+
interface NimbleRawExtractData {
|
|
152
|
+
/** Markdown rendering of the page (default). */
|
|
153
|
+
markdown?: string;
|
|
154
|
+
/** Raw HTML of the page. */
|
|
155
|
+
html?: string;
|
|
156
|
+
/** Unique URLs found on the page. */
|
|
157
|
+
links?: string[];
|
|
158
|
+
}
|
|
159
|
+
interface NimbleRawExtractResponse {
|
|
160
|
+
url: string;
|
|
161
|
+
status: string;
|
|
162
|
+
status_code?: number;
|
|
163
|
+
task_id: string;
|
|
164
|
+
data: NimbleRawExtractData;
|
|
165
|
+
warnings?: string[];
|
|
166
|
+
}
|
|
167
|
+
interface NimbleExtractClient {
|
|
168
|
+
extract(params: NimbleExtractParams): Promise<NimbleRawExtractResponse>;
|
|
169
|
+
}
|
|
170
|
+
/** The normalized extract output returned to the model. */
|
|
171
|
+
interface NimbleExtractOutput {
|
|
172
|
+
/** The final URL (after redirects). */
|
|
173
|
+
url: string;
|
|
174
|
+
/** Task status reported by Nimble (e.g. `success`). */
|
|
175
|
+
status: string;
|
|
176
|
+
statusCode?: number;
|
|
177
|
+
format: ExtractFormat;
|
|
178
|
+
/** The extracted page content in the requested format, truncated. */
|
|
179
|
+
content: string;
|
|
180
|
+
/** Unique links found on the page, when available. */
|
|
181
|
+
links?: string[];
|
|
182
|
+
}
|
|
114
183
|
|
|
115
184
|
/** v1 factory defaults. `focus` is fixed to `general` and not user-exposed. */
|
|
116
185
|
declare const NIMBLE_SEARCH_DEFAULTS: {
|
|
@@ -146,6 +215,34 @@ declare function nimbleSearch(config?: NimbleSearchToolConfig): ai.Tool<{
|
|
|
146
215
|
maxResults?: number | undefined;
|
|
147
216
|
}, NimbleSearchOutput>;
|
|
148
217
|
|
|
218
|
+
/** v1 extract defaults. */
|
|
219
|
+
declare const NIMBLE_EXTRACT_DEFAULTS: {
|
|
220
|
+
format: ExtractFormat;
|
|
221
|
+
maxContentLength: number;
|
|
222
|
+
};
|
|
223
|
+
/**
|
|
224
|
+
* Create a Vercel AI SDK tool that extracts clean, readable content from a web
|
|
225
|
+
* page with Nimble (`@nimble-way/nimble-js` → `POST /v1/extract`).
|
|
226
|
+
*
|
|
227
|
+
* The model only chooses `{ url }`; format, region, and length cap are fixed by
|
|
228
|
+
* `config`. Mirrors {@link nimbleSearch}'s ergonomics.
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* ```ts
|
|
232
|
+
* import { generateText } from 'ai';
|
|
233
|
+
* import { nimbleExtract } from '@nimble-way/ai-sdk';
|
|
234
|
+
*
|
|
235
|
+
* const { text } = await generateText({
|
|
236
|
+
* model: 'openai/gpt-4o-mini',
|
|
237
|
+
* prompt: 'Summarize https://nimbleway.com',
|
|
238
|
+
* tools: { extract: nimbleExtract() },
|
|
239
|
+
* });
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
242
|
+
declare function nimbleExtract(config?: NimbleExtractToolConfig): ai.Tool<{
|
|
243
|
+
url: string;
|
|
244
|
+
}, NimbleExtractOutput>;
|
|
245
|
+
|
|
149
246
|
interface NormalizeOptions {
|
|
150
247
|
query: string;
|
|
151
248
|
maxContentLength: number;
|
|
@@ -162,6 +259,21 @@ interface NormalizeOptions {
|
|
|
162
259
|
* - `response.answer` is never surfaced in v1.
|
|
163
260
|
*/
|
|
164
261
|
declare function normalizeSearchResponse(response: NimbleRawSearchResponse, options: NormalizeOptions): NimbleSearchOutput;
|
|
262
|
+
interface NormalizeExtractOptions {
|
|
263
|
+
format: ExtractFormat;
|
|
264
|
+
maxContentLength: number;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Map a raw `/v1/extract` response into the package's normalized extract shape.
|
|
268
|
+
*
|
|
269
|
+
* - `content` is `data.markdown` (default) or `data.html`, falling back to the
|
|
270
|
+
* other when the requested one is empty, truncated to `maxContentLength`.
|
|
271
|
+
* - `format` reflects the rendering actually returned, which may differ from
|
|
272
|
+
* the requested format when the fallback is used.
|
|
273
|
+
* - `links` is surfaced when present; everything else (browser actions, network
|
|
274
|
+
* captures, screenshots) is intentionally dropped.
|
|
275
|
+
*/
|
|
276
|
+
declare function normalizeExtractResponse(response: NimbleRawExtractResponse, options: NormalizeExtractOptions): NimbleExtractOutput;
|
|
165
277
|
|
|
166
278
|
/**
|
|
167
279
|
* Thrown when the tool is invoked without a resolvable API key (no `apiKey`
|
|
@@ -184,5 +296,16 @@ declare class NimbleSearchError extends Error {
|
|
|
184
296
|
cause?: unknown;
|
|
185
297
|
});
|
|
186
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Wraps an error surfaced by the Nimble client / API during an extract call,
|
|
301
|
+
* preserving the HTTP status when available.
|
|
302
|
+
*/
|
|
303
|
+
declare class NimbleExtractError extends Error {
|
|
304
|
+
readonly status?: number;
|
|
305
|
+
constructor(message: string, options?: {
|
|
306
|
+
status?: number;
|
|
307
|
+
cause?: unknown;
|
|
308
|
+
});
|
|
309
|
+
}
|
|
187
310
|
|
|
188
|
-
export { NIMBLE_SEARCH_DEFAULTS, NimbleConfigError, type NimbleRawSearchResponse, type NimbleRawSearchResult, type NimbleSearchClient, NimbleSearchError, type NimbleSearchInput, type NimbleSearchOutput, type NimbleSearchParams, type NimbleSearchResultItem, type NimbleSearchToolConfig, type NimbleSerpMetadata, type NimbleWsaMetadata, type NormalizeOptions, type SearchDepth, nimbleSearch, nimbleSearchInputSchema, normalizeSearchResponse };
|
|
311
|
+
export { type ExtractFormat, NIMBLE_EXTRACT_DEFAULTS, NIMBLE_SEARCH_DEFAULTS, NimbleConfigError, type NimbleExtractClient, NimbleExtractError, type NimbleExtractInput, type NimbleExtractOutput, type NimbleExtractParams, type NimbleExtractToolConfig, type NimbleRawExtractData, type NimbleRawExtractResponse, type NimbleRawSearchResponse, type NimbleRawSearchResult, type NimbleSearchClient, NimbleSearchError, type NimbleSearchInput, type NimbleSearchOutput, type NimbleSearchParams, type NimbleSearchResultItem, type NimbleSearchToolConfig, type NimbleSerpMetadata, type NimbleWsaMetadata, type NormalizeExtractOptions, type NormalizeOptions, type SearchDepth, nimbleExtract, nimbleExtractInputSchema, nimbleSearch, nimbleSearchInputSchema, normalizeExtractResponse, normalizeSearchResponse };
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,9 @@ var nimbleSearchInputSchema = z.object({
|
|
|
7
7
|
query: z.string().min(1).describe("The web search query."),
|
|
8
8
|
maxResults: z.number().int().positive().optional().describe("How many results to return (clamped to the developer-configured cap).")
|
|
9
9
|
});
|
|
10
|
+
var nimbleExtractInputSchema = z.object({
|
|
11
|
+
url: z.string().url().describe("The URL of the web page to extract clean content from.")
|
|
12
|
+
});
|
|
10
13
|
|
|
11
14
|
// src/normalize.ts
|
|
12
15
|
function isSerpMetadata(metadata) {
|
|
@@ -42,6 +45,23 @@ function normalizeSearchResponse(response, options) {
|
|
|
42
45
|
results
|
|
43
46
|
};
|
|
44
47
|
}
|
|
48
|
+
function normalizeExtractResponse(response, options) {
|
|
49
|
+
const data = response.data;
|
|
50
|
+
const otherFormat = options.format === "html" ? "markdown" : "html";
|
|
51
|
+
const primary = options.format === "html" ? data.html : data.markdown;
|
|
52
|
+
const fallback = options.format === "html" ? data.markdown : data.html;
|
|
53
|
+
const usedFormat = primary && primary.length > 0 ? options.format : fallback && fallback.length > 0 ? otherFormat : options.format;
|
|
54
|
+
const content = truncate(primary || fallback || "", options.maxContentLength);
|
|
55
|
+
const out = {
|
|
56
|
+
url: response.url,
|
|
57
|
+
status: response.status,
|
|
58
|
+
format: usedFormat,
|
|
59
|
+
content
|
|
60
|
+
};
|
|
61
|
+
if (typeof response.status_code === "number") out.statusCode = response.status_code;
|
|
62
|
+
if (data.links && data.links.length > 0) out.links = data.links;
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
45
65
|
|
|
46
66
|
// src/errors.ts
|
|
47
67
|
var NimbleConfigError = class extends Error {
|
|
@@ -58,6 +78,14 @@ var NimbleSearchError = class extends Error {
|
|
|
58
78
|
this.status = options?.status;
|
|
59
79
|
}
|
|
60
80
|
};
|
|
81
|
+
var NimbleExtractError = class extends Error {
|
|
82
|
+
status;
|
|
83
|
+
constructor(message, options) {
|
|
84
|
+
super(message, options?.cause !== void 0 ? { cause: options.cause } : void 0);
|
|
85
|
+
this.name = "NimbleExtractError";
|
|
86
|
+
this.status = options?.status;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
61
89
|
|
|
62
90
|
// src/nimble-search.ts
|
|
63
91
|
var NIMBLE_SEARCH_DEFAULTS = {
|
|
@@ -131,7 +159,61 @@ function nimbleSearch(config = {}) {
|
|
|
131
159
|
}
|
|
132
160
|
});
|
|
133
161
|
}
|
|
162
|
+
var NIMBLE_EXTRACT_DEFAULTS = {
|
|
163
|
+
format: "markdown",
|
|
164
|
+
maxContentLength: 5e4
|
|
165
|
+
};
|
|
166
|
+
function readStatus2(err) {
|
|
167
|
+
if (typeof err === "object" && err !== null && "status" in err) {
|
|
168
|
+
const status = err.status;
|
|
169
|
+
return typeof status === "number" ? status : void 0;
|
|
170
|
+
}
|
|
171
|
+
return void 0;
|
|
172
|
+
}
|
|
173
|
+
function toExtractError(err) {
|
|
174
|
+
if (err instanceof NimbleExtractError) return err;
|
|
175
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
176
|
+
return new NimbleExtractError(`Nimble extract failed: ${message}`, {
|
|
177
|
+
status: readStatus2(err),
|
|
178
|
+
cause: err
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function resolveClient2(config) {
|
|
182
|
+
if (config.client) return config.client;
|
|
183
|
+
const apiKey = config.apiKey ?? process.env.NIMBLE_API_KEY;
|
|
184
|
+
if (!apiKey) {
|
|
185
|
+
throw new NimbleConfigError(
|
|
186
|
+
"Missing Nimble API key: set NIMBLE_API_KEY or pass { apiKey } to nimbleExtract()."
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return new Nimble({ apiKey });
|
|
190
|
+
}
|
|
191
|
+
function nimbleExtract(config = {}) {
|
|
192
|
+
const format = config.format ?? NIMBLE_EXTRACT_DEFAULTS.format;
|
|
193
|
+
const maxContentLength = config.maxContentLength ?? NIMBLE_EXTRACT_DEFAULTS.maxContentLength;
|
|
194
|
+
const country = config.country;
|
|
195
|
+
return tool({
|
|
196
|
+
description: "Fetch a web page by URL with Nimble and return its clean, readable content (markdown or HTML) \u2014 use this to read, quote, or summarize a specific page.",
|
|
197
|
+
inputSchema: nimbleExtractInputSchema,
|
|
198
|
+
execute: async (input) => {
|
|
199
|
+
const client = resolveClient2(config);
|
|
200
|
+
const params = {
|
|
201
|
+
url: input.url,
|
|
202
|
+
formats: format === "html" ? ["html", "markdown", "links"] : ["markdown", "html", "links"]
|
|
203
|
+
};
|
|
204
|
+
if (country) params.country = country;
|
|
205
|
+
if (format === "markdown") params.markdown_backend = "main_content";
|
|
206
|
+
let raw;
|
|
207
|
+
try {
|
|
208
|
+
raw = await client.extract(params);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
throw toExtractError(err);
|
|
211
|
+
}
|
|
212
|
+
return normalizeExtractResponse(raw, { format, maxContentLength });
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
134
216
|
|
|
135
|
-
export { NIMBLE_SEARCH_DEFAULTS, NimbleConfigError, NimbleSearchError, nimbleSearch, nimbleSearchInputSchema, normalizeSearchResponse };
|
|
217
|
+
export { NIMBLE_EXTRACT_DEFAULTS, NIMBLE_SEARCH_DEFAULTS, NimbleConfigError, NimbleExtractError, NimbleSearchError, nimbleExtract, nimbleExtractInputSchema, nimbleSearch, nimbleSearchInputSchema, normalizeExtractResponse, normalizeSearchResponse };
|
|
136
218
|
//# sourceMappingURL=index.js.map
|
|
137
219
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/schemas.ts","../src/normalize.ts","../src/errors.ts","../src/nimble-search.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { z } from 'zod';\n\n/**\n * The tool input the model fills in. Kept deliberately small: the model only\n * chooses the query and (optionally) how many results it wants. All policy\n * (depth, focus, region, caps) is fixed by the developer via the factory\n * config, not by the model.\n */\nexport const nimbleSearchInputSchema = z.object({\n query: z.string().min(1).describe('The web search query.'),\n maxResults: z\n .number()\n .int()\n .positive()\n .optional()\n .describe('How many results to return (clamped to the developer-configured cap).'),\n});\n\nexport type NimbleSearchInput = z.infer<typeof nimbleSearchInputSchema>;\n\n/**\n * v1 exposes only the two non-enterprise depths. `fast` is enterprise-gated and\n * intentionally not offered here.\n */\nexport type SearchDepth = 'lite' | 'deep';\n\n/**\n * Developer-facing factory config. `focus` is fixed to `general` in v1 and is\n * not exposed. `include_answer` and `search_depth: 'fast'` are intentionally\n * absent (enterprise / unverified-entitlement surface).\n */\nexport interface NimbleSearchToolConfig {\n /** Nimble API key. Defaults to `process.env.NIMBLE_API_KEY`. */\n apiKey?: string;\n /** Inject a pre-built / mock Nimble client (tests, advanced users). */\n client?: NimbleSearchClient;\n /** Default number of results when the model doesn't specify. Default 5. */\n maxResults?: number;\n /** Hard upper bound on results, regardless of model request. Default 10. */\n maxResultsCap?: number;\n /** Search depth. Default `lite`. */\n searchDepth?: SearchDepth;\n /** ISO country for result localization. Default `US`. */\n country?: string;\n /** Locale for result localization. Default `en`. */\n locale?: string;\n /** Truncate each result's body to this many characters. Default 10_000. */\n maxContentLength?: number;\n}\n\n/**\n * Structural surface of `@nimble-way/nimble-js`'s `client.search()` that this\n * package relies on. Declared structurally so the scaffold typechecks without\n * pinning to the SDK's generated type names, and so tests can inject a mock.\n *\n * Phase B: reconcile field casing/names against\n * `sdks/nimble-js/checkout/src/` after `./tools/sync-sdk.sh nimble-js`.\n */\nexport interface NimbleSearchParams {\n query: string;\n max_results?: number;\n search_depth?: SearchDepth;\n focus?: string;\n country?: string;\n locale?: string;\n}\n\n/** Metadata for SERP-based results (general/news/location focus). */\nexport interface NimbleSerpMetadata {\n country: string;\n entity_type: string;\n locale: string;\n position: number;\n driver?: string | null;\n}\n\n/** Metadata for WSA-based results (shopping/social/geo focus). */\nexport interface NimbleWsaMetadata {\n agent_name: string;\n}\n\nexport interface NimbleRawSearchResult {\n /** Full page text in `deep`; may be empty in `lite`. */\n content: string;\n description: string;\n title: string;\n url: string;\n /** SERP focus (v1 `general`) yields {@link NimbleSerpMetadata}. */\n metadata: NimbleSerpMetadata | NimbleWsaMetadata;\n /** Platform-specific extras (price, publish_date, …); omitted when none. */\n additional_data?: Record<string, unknown> | null;\n}\n\nexport interface NimbleRawSearchResponse {\n request_id: string;\n results: NimbleRawSearchResult[];\n total_results: number;\n /** Intentionally never surfaced in v1 (include_answer is off). */\n answer?: string | null;\n}\n\nexport interface NimbleSearchClient {\n search(params: NimbleSearchParams): Promise<NimbleRawSearchResponse>;\n}\n\n/** A single normalized result item returned to the model. */\nexport interface NimbleSearchResultItem {\n title: string;\n url: string;\n description?: string;\n content?: string;\n position?: number;\n entityType?: string;\n}\n\n/** The normalized tool output. `answer` is intentionally omitted in v1. */\nexport interface NimbleSearchOutput {\n query: string;\n requestId?: string;\n totalResults?: number;\n results: NimbleSearchResultItem[];\n}\n","import type {\n NimbleRawSearchResponse,\n NimbleRawSearchResult,\n NimbleSearchOutput,\n NimbleSearchResultItem,\n NimbleSerpMetadata,\n} from './schemas';\n\nexport interface NormalizeOptions {\n query: string;\n maxContentLength: number;\n}\n\n/** SERP focus (v1 `general`) carries position + entity_type; WSA does not. */\nfunction isSerpMetadata(\n metadata: NimbleRawSearchResult['metadata'],\n): metadata is NimbleSerpMetadata {\n return 'position' in metadata;\n}\n\nfunction truncate(text: string, max: number): string {\n return text.length > max ? text.slice(0, max) : text;\n}\n\n/**\n * Map a raw `/v1/search` response into the package's normalized output shape.\n *\n * - `description` is the snippet (always present when the API returns one).\n * - `content` is the full page text, present only in `deep` depth (empty in\n * `lite`), truncated to `maxContentLength`.\n * - `position` / `entityType` come from SERP metadata; for WSA results\n * `position` falls back to the array index and `entityType` is omitted.\n * - Results without a URL are dropped (defensive).\n * - `response.answer` is never surfaced in v1.\n */\nexport function normalizeSearchResponse(\n response: NimbleRawSearchResponse,\n options: NormalizeOptions,\n): NimbleSearchOutput {\n const results: NimbleSearchResultItem[] = [];\n\n response.results.forEach((raw, index) => {\n if (!raw.url) return;\n\n const item: NimbleSearchResultItem = {\n title: raw.title ?? '',\n url: raw.url,\n };\n if (raw.description) item.description = raw.description;\n if (raw.content && raw.content.trim().length > 0) {\n item.content = truncate(raw.content, options.maxContentLength);\n }\n\n if (isSerpMetadata(raw.metadata)) {\n item.position = raw.metadata.position;\n item.entityType = raw.metadata.entity_type;\n } else {\n item.position = index + 1;\n }\n\n results.push(item);\n });\n\n return {\n query: options.query,\n requestId: response.request_id,\n totalResults: response.total_results,\n results,\n };\n}\n","/**\n * Thrown when the tool is invoked without a resolvable API key (no `apiKey`\n * config and no `NIMBLE_API_KEY` in the environment) and no injected client.\n * Raised at execute time, not at factory-construction time, so the tool can be\n * constructed in environments without a key (e.g. unit tests, type-checking).\n */\nexport class NimbleConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'NimbleConfigError';\n }\n}\n\n/**\n * Wraps an error surfaced by the Nimble client / API during a search call,\n * preserving the HTTP status when available. The AI SDK surfaces a thrown\n * tool error back to the model as a tool-call failure.\n */\nexport class NimbleSearchError extends Error {\n readonly status?: number;\n\n constructor(message: string, options?: { status?: number; cause?: unknown }) {\n super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);\n this.name = 'NimbleSearchError';\n this.status = options?.status;\n }\n}\n","import { tool } from 'ai';\nimport { Nimble } from '@nimble-way/nimble-js';\nimport { nimbleSearchInputSchema } from './schemas';\nimport type {\n NimbleSearchClient,\n NimbleSearchOutput,\n NimbleSearchParams,\n NimbleSearchToolConfig,\n} from './schemas';\nimport { normalizeSearchResponse } from './normalize';\nimport { NimbleConfigError, NimbleSearchError } from './errors';\n\n/** v1 factory defaults. `focus` is fixed to `general` and not user-exposed. */\nexport const NIMBLE_SEARCH_DEFAULTS = {\n maxResults: 5,\n maxResultsCap: 10,\n searchDepth: 'lite',\n country: 'US',\n locale: 'en',\n maxContentLength: 10_000,\n focus: 'general',\n} as const;\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max);\n}\n\nfunction readStatus(err: unknown): number | undefined {\n if (typeof err === 'object' && err !== null && 'status' in err) {\n const status = (err as { status?: unknown }).status;\n return typeof status === 'number' ? status : undefined;\n }\n return undefined;\n}\n\nfunction toSearchError(err: unknown): NimbleSearchError {\n if (err instanceof NimbleSearchError) return err;\n const message = err instanceof Error ? err.message : String(err);\n return new NimbleSearchError(`Nimble search failed: ${message}`, {\n status: readStatus(err),\n cause: err,\n });\n}\n\nfunction resolveClient(config: NimbleSearchToolConfig): NimbleSearchClient {\n if (config.client) return config.client;\n const apiKey = config.apiKey ?? process.env.NIMBLE_API_KEY;\n if (!apiKey) {\n throw new NimbleConfigError(\n 'Missing Nimble API key: set NIMBLE_API_KEY or pass { apiKey } to nimbleSearch().',\n );\n }\n return new Nimble({ apiKey }) as unknown as NimbleSearchClient;\n}\n\n/**\n * Create a ready-made Vercel AI SDK web-search tool backed by Nimble Search\n * (`@nimble-way/nimble-js` → `POST /v1/search`).\n *\n * The model only chooses `{ query, maxResults? }`; all policy (depth, focus,\n * region, caps) is fixed by `config` — mirroring the Exa `webSearch()` shape.\n *\n * @example\n * ```ts\n * import { generateText } from 'ai';\n * import { nimbleSearch } from '@nimble-way/ai-sdk';\n *\n * const { text } = await generateText({\n * model: 'anthropic/claude-sonnet-4.6',\n * prompt: 'What are the latest Nimble release notes?',\n * tools: { webSearch: nimbleSearch({ searchDepth: 'lite', maxResults: 5 }) },\n * });\n * ```\n */\nexport function nimbleSearch(config: NimbleSearchToolConfig = {}) {\n const maxResults = config.maxResults ?? NIMBLE_SEARCH_DEFAULTS.maxResults;\n const maxResultsCap = config.maxResultsCap ?? NIMBLE_SEARCH_DEFAULTS.maxResultsCap;\n const searchDepth = config.searchDepth ?? NIMBLE_SEARCH_DEFAULTS.searchDepth;\n const country = config.country ?? NIMBLE_SEARCH_DEFAULTS.country;\n const locale = config.locale ?? NIMBLE_SEARCH_DEFAULTS.locale;\n const maxContentLength = config.maxContentLength ?? NIMBLE_SEARCH_DEFAULTS.maxContentLength;\n\n return tool({\n description:\n 'Search the web with Nimble and return ranked results (title, url, ' +\n 'snippet, and page content) for answering questions about current or ' +\n 'factual information.',\n inputSchema: nimbleSearchInputSchema,\n execute: async (input): Promise<NimbleSearchOutput> => {\n const client = resolveClient(config);\n\n const params: NimbleSearchParams = {\n query: input.query,\n max_results: clamp(input.maxResults ?? maxResults, 1, maxResultsCap),\n search_depth: searchDepth,\n focus: NIMBLE_SEARCH_DEFAULTS.focus, // fixed 'general' in v1\n country,\n locale,\n };\n\n let raw;\n try {\n raw = await client.search(params);\n } catch (err) {\n throw toSearchError(err);\n }\n\n return normalizeSearchResponse(raw, {\n query: input.query,\n maxContentLength,\n });\n },\n });\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/schemas.ts","../src/normalize.ts","../src/errors.ts","../src/nimble-search.ts","../src/nimble-extract.ts"],"names":["readStatus","resolveClient","Nimble","tool"],"mappings":";;;;;AAQO,IAAM,uBAAA,GAA0B,EAAE,MAAA,CAAO;AAAA,EAC9C,KAAA,EAAO,EAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA,CAAE,SAAS,uBAAuB,CAAA;AAAA,EACzD,UAAA,EAAY,CAAA,CACT,MAAA,EAAO,CACP,GAAA,EAAI,CACJ,QAAA,EAAS,CACT,QAAA,EAAS,CACT,QAAA,CAAS,uEAAuE;AACrF,CAAC;AAoHM,IAAM,wBAAA,GAA2B,EAAE,MAAA,CAAO;AAAA,EAC/C,KAAK,CAAA,CAAE,MAAA,GAAS,GAAA,EAAI,CAAE,SAAS,wDAAwD;AACzF,CAAC;;;ACrHD,SAAS,eACP,QAAA,EACgC;AAChC,EAAA,OAAO,UAAA,IAAc,QAAA;AACvB;AAEA,SAAS,QAAA,CAAS,MAAc,GAAA,EAAqB;AACnD,EAAA,OAAO,KAAK,MAAA,GAAS,GAAA,GAAM,KAAK,KAAA,CAAM,CAAA,EAAG,GAAG,CAAA,GAAI,IAAA;AAClD;AAaO,SAAS,uBAAA,CACd,UACA,OAAA,EACoB;AACpB,EAAA,MAAM,UAAoC,EAAC;AAE3C,EAAA,QAAA,CAAS,OAAA,CAAQ,OAAA,CAAQ,CAAC,GAAA,EAAK,KAAA,KAAU;AACvC,IAAA,IAAI,CAAC,IAAI,GAAA,EAAK;AAEd,IAAA,MAAM,IAAA,GAA+B;AAAA,MACnC,KAAA,EAAO,IAAI,KAAA,IAAS,EAAA;AAAA,MACpB,KAAK,GAAA,CAAI;AAAA,KACX;AACA,IAAA,IAAI,GAAA,CAAI,WAAA,EAAa,IAAA,CAAK,WAAA,GAAc,GAAA,CAAI,WAAA;AAC5C,IAAA,IAAI,IAAI,OAAA,IAAW,GAAA,CAAI,QAAQ,IAAA,EAAK,CAAE,SAAS,CAAA,EAAG;AAChD,MAAA,IAAA,CAAK,OAAA,GAAU,QAAA,CAAS,GAAA,CAAI,OAAA,EAAS,QAAQ,gBAAgB,CAAA;AAAA,IAC/D;AAEA,IAAA,IAAI,cAAA,CAAe,GAAA,CAAI,QAAQ,CAAA,EAAG;AAChC,MAAA,IAAA,CAAK,QAAA,GAAW,IAAI,QAAA,CAAS,QAAA;AAC7B,MAAA,IAAA,CAAK,UAAA,GAAa,IAAI,QAAA,CAAS,WAAA;AAAA,IACjC,CAAA,MAAO;AACL,MAAA,IAAA,CAAK,WAAW,KAAA,GAAQ,CAAA;AAAA,IAC1B;AAEA,IAAA,OAAA,CAAQ,KAAK,IAAI,CAAA;AAAA,EACnB,CAAC,CAAA;AAED,EAAA,OAAO;AAAA,IACL,OAAO,OAAA,CAAQ,KAAA;AAAA,IACf,WAAW,QAAA,CAAS,UAAA;AAAA,IACpB,cAAc,QAAA,CAAS,aAAA;AAAA,IACvB;AAAA,GACF;AACF;AAiBO,SAAS,wBAAA,CACd,UACA,OAAA,EACqB;AACrB,EAAA,MAAM,OAAO,QAAA,CAAS,IAAA;AACtB,EAAA,MAAM,WAAA,GAA6B,OAAA,CAAQ,MAAA,KAAW,MAAA,GAAS,UAAA,GAAa,MAAA;AAC5E,EAAA,MAAM,UAAU,OAAA,CAAQ,MAAA,KAAW,MAAA,GAAS,IAAA,CAAK,OAAO,IAAA,CAAK,QAAA;AAC7D,EAAA,MAAM,WAAW,OAAA,CAAQ,MAAA,KAAW,MAAA,GAAS,IAAA,CAAK,WAAW,IAAA,CAAK,IAAA;AAKlE,EAAA,MAAM,UAAA,GACJ,OAAA,IAAW,OAAA,CAAQ,MAAA,GAAS,CAAA,GACxB,OAAA,CAAQ,MAAA,GACR,QAAA,IAAY,QAAA,CAAS,MAAA,GAAS,CAAA,GAC5B,WAAA,GACA,OAAA,CAAQ,MAAA;AAChB,EAAA,MAAM,UAAU,QAAA,CAAS,OAAA,IAAW,QAAA,IAAY,EAAA,EAAI,QAAQ,gBAAgB,CAAA;AAE5E,EAAA,MAAM,GAAA,GAA2B;AAAA,IAC/B,KAAK,QAAA,CAAS,GAAA;AAAA,IACd,QAAQ,QAAA,CAAS,MAAA;AAAA,IACjB,MAAA,EAAQ,UAAA;AAAA,IACR;AAAA,GACF;AACA,EAAA,IAAI,OAAO,QAAA,CAAS,WAAA,KAAgB,QAAA,EAAU,GAAA,CAAI,aAAa,QAAA,CAAS,WAAA;AACxE,EAAA,IAAI,IAAA,CAAK,SAAS,IAAA,CAAK,KAAA,CAAM,SAAS,CAAA,EAAG,GAAA,CAAI,QAAQ,IAAA,CAAK,KAAA;AAC1D,EAAA,OAAO,GAAA;AACT;;;AChHO,IAAM,iBAAA,GAAN,cAAgC,KAAA,CAAM;AAAA,EAC3C,YAAY,OAAA,EAAiB;AAC3B,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,mBAAA;AAAA,EACd;AACF;AAOO,IAAM,iBAAA,GAAN,cAAgC,KAAA,CAAM;AAAA,EAClC,MAAA;AAAA,EAET,WAAA,CAAY,SAAiB,OAAA,EAAgD;AAC3E,IAAA,KAAA,CAAM,OAAA,EAAS,SAAS,KAAA,KAAU,MAAA,GAAY,EAAE,KAAA,EAAO,OAAA,CAAQ,KAAA,EAAM,GAAI,MAAS,CAAA;AAClF,IAAA,IAAA,CAAK,IAAA,GAAO,mBAAA;AACZ,IAAA,IAAA,CAAK,SAAS,OAAA,EAAS,MAAA;AAAA,EACzB;AACF;AAMO,IAAM,kBAAA,GAAN,cAAiC,KAAA,CAAM;AAAA,EACnC,MAAA;AAAA,EAET,WAAA,CAAY,SAAiB,OAAA,EAAgD;AAC3E,IAAA,KAAA,CAAM,OAAA,EAAS,SAAS,KAAA,KAAU,MAAA,GAAY,EAAE,KAAA,EAAO,OAAA,CAAQ,KAAA,EAAM,GAAI,MAAS,CAAA;AAClF,IAAA,IAAA,CAAK,IAAA,GAAO,oBAAA;AACZ,IAAA,IAAA,CAAK,SAAS,OAAA,EAAS,MAAA;AAAA,EACzB;AACF;;;AC3BO,IAAM,sBAAA,GAAyB;AAAA,EACpC,UAAA,EAAY,CAAA;AAAA,EACZ,aAAA,EAAe,EAAA;AAAA,EACf,WAAA,EAAa,MAAA;AAAA,EACb,OAAA,EAAS,IAAA;AAAA,EACT,MAAA,EAAQ,IAAA;AAAA,EACR,gBAAA,EAAkB,GAAA;AAAA,EAClB,KAAA,EAAO;AACT;AAEA,SAAS,KAAA,CAAM,KAAA,EAAe,GAAA,EAAa,GAAA,EAAqB;AAC9D,EAAA,OAAO,KAAK,GAAA,CAAI,IAAA,CAAK,IAAI,KAAA,EAAO,GAAG,GAAG,GAAG,CAAA;AAC3C;AAEA,SAAS,WAAW,GAAA,EAAkC;AACpD,EAAA,IAAI,OAAO,GAAA,KAAQ,QAAA,IAAY,GAAA,KAAQ,IAAA,IAAQ,YAAY,GAAA,EAAK;AAC9D,IAAA,MAAM,SAAU,GAAA,CAA6B,MAAA;AAC7C,IAAA,OAAO,OAAO,MAAA,KAAW,QAAA,GAAW,MAAA,GAAS,MAAA;AAAA,EAC/C;AACA,EAAA,OAAO,MAAA;AACT;AAEA,SAAS,cAAc,GAAA,EAAiC;AACtD,EAAA,IAAI,GAAA,YAAe,mBAAmB,OAAO,GAAA;AAC7C,EAAA,MAAM,UAAU,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG,CAAA;AAC/D,EAAA,OAAO,IAAI,iBAAA,CAAkB,CAAA,sBAAA,EAAyB,OAAO,CAAA,CAAA,EAAI;AAAA,IAC/D,MAAA,EAAQ,WAAW,GAAG,CAAA;AAAA,IACtB,KAAA,EAAO;AAAA,GACR,CAAA;AACH;AAEA,SAAS,cAAc,MAAA,EAAoD;AACzE,EAAA,IAAI,MAAA,CAAO,MAAA,EAAQ,OAAO,MAAA,CAAO,MAAA;AACjC,EAAA,MAAM,MAAA,GAAS,MAAA,CAAO,MAAA,IAAU,OAAA,CAAQ,GAAA,CAAI,cAAA;AAC5C,EAAA,IAAI,CAAC,MAAA,EAAQ;AACX,IAAA,MAAM,IAAI,iBAAA;AAAA,MACR;AAAA,KACF;AAAA,EACF;AACA,EAAA,OAAO,IAAI,MAAA,CAAO,EAAE,MAAA,EAAQ,CAAA;AAC9B;AAqBO,SAAS,YAAA,CAAa,MAAA,GAAiC,EAAC,EAAG;AAChE,EAAA,MAAM,UAAA,GAAa,MAAA,CAAO,UAAA,IAAc,sBAAA,CAAuB,UAAA;AAC/D,EAAA,MAAM,aAAA,GAAgB,MAAA,CAAO,aAAA,IAAiB,sBAAA,CAAuB,aAAA;AACrE,EAAA,MAAM,WAAA,GAAc,MAAA,CAAO,WAAA,IAAe,sBAAA,CAAuB,WAAA;AACjE,EAAA,MAAM,OAAA,GAAU,MAAA,CAAO,OAAA,IAAW,sBAAA,CAAuB,OAAA;AACzD,EAAA,MAAM,MAAA,GAAS,MAAA,CAAO,MAAA,IAAU,sBAAA,CAAuB,MAAA;AACvD,EAAA,MAAM,gBAAA,GAAmB,MAAA,CAAO,gBAAA,IAAoB,sBAAA,CAAuB,gBAAA;AAE3E,EAAA,OAAO,IAAA,CAAK;AAAA,IACV,WAAA,EACE,4JAAA;AAAA,IAGF,WAAA,EAAa,uBAAA;AAAA,IACb,OAAA,EAAS,OAAO,KAAA,KAAuC;AACrD,MAAA,MAAM,MAAA,GAAS,cAAc,MAAM,CAAA;AAEnC,MAAA,MAAM,MAAA,GAA6B;AAAA,QACjC,OAAO,KAAA,CAAM,KAAA;AAAA,QACb,aAAa,KAAA,CAAM,KAAA,CAAM,UAAA,IAAc,UAAA,EAAY,GAAG,aAAa,CAAA;AAAA,QACnE,YAAA,EAAc,WAAA;AAAA,QACd,OAAO,sBAAA,CAAuB,KAAA;AAAA;AAAA,QAC9B,OAAA;AAAA,QACA;AAAA,OACF;AAEA,MAAA,IAAI,GAAA;AACJ,MAAA,IAAI;AACF,QAAA,GAAA,GAAM,MAAM,MAAA,CAAO,MAAA,CAAO,MAAM,CAAA;AAAA,MAClC,SAAS,GAAA,EAAK;AACZ,QAAA,MAAM,cAAc,GAAG,CAAA;AAAA,MACzB;AAEA,MAAA,OAAO,wBAAwB,GAAA,EAAK;AAAA,QAClC,OAAO,KAAA,CAAM,KAAA;AAAA,QACb;AAAA,OACD,CAAA;AAAA,IACH;AAAA,GACD,CAAA;AACH;ACnGO,IAAM,uBAAA,GAA+E;AAAA,EAC1F,MAAA,EAAQ,UAAA;AAAA,EACR,gBAAA,EAAkB;AACpB;AAEA,SAASA,YAAW,GAAA,EAAkC;AACpD,EAAA,IAAI,OAAO,GAAA,KAAQ,QAAA,IAAY,GAAA,KAAQ,IAAA,IAAQ,YAAY,GAAA,EAAK;AAC9D,IAAA,MAAM,SAAU,GAAA,CAA6B,MAAA;AAC7C,IAAA,OAAO,OAAO,MAAA,KAAW,QAAA,GAAW,MAAA,GAAS,MAAA;AAAA,EAC/C;AACA,EAAA,OAAO,MAAA;AACT;AAEA,SAAS,eAAe,GAAA,EAAkC;AACxD,EAAA,IAAI,GAAA,YAAe,oBAAoB,OAAO,GAAA;AAC9C,EAAA,MAAM,UAAU,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG,CAAA;AAC/D,EAAA,OAAO,IAAI,kBAAA,CAAmB,CAAA,uBAAA,EAA0B,OAAO,CAAA,CAAA,EAAI;AAAA,IACjE,MAAA,EAAQA,YAAW,GAAG,CAAA;AAAA,IACtB,KAAA,EAAO;AAAA,GACR,CAAA;AACH;AAEA,SAASC,eAAc,MAAA,EAAsD;AAC3E,EAAA,IAAI,MAAA,CAAO,MAAA,EAAQ,OAAO,MAAA,CAAO,MAAA;AACjC,EAAA,MAAM,MAAA,GAAS,MAAA,CAAO,MAAA,IAAU,OAAA,CAAQ,GAAA,CAAI,cAAA;AAC5C,EAAA,IAAI,CAAC,MAAA,EAAQ;AACX,IAAA,MAAM,IAAI,iBAAA;AAAA,MACR;AAAA,KACF;AAAA,EACF;AACA,EAAA,OAAO,IAAIC,MAAAA,CAAO,EAAE,MAAA,EAAQ,CAAA;AAC9B;AAqBO,SAAS,aAAA,CAAc,MAAA,GAAkC,EAAC,EAAG;AAClE,EAAA,MAAM,MAAA,GAAS,MAAA,CAAO,MAAA,IAAU,uBAAA,CAAwB,MAAA;AACxD,EAAA,MAAM,gBAAA,GAAmB,MAAA,CAAO,gBAAA,IAAoB,uBAAA,CAAwB,gBAAA;AAC5E,EAAA,MAAM,UAAU,MAAA,CAAO,OAAA;AAEvB,EAAA,OAAOC,IAAAA,CAAK;AAAA,IACV,WAAA,EACE,6JAAA;AAAA,IAEF,WAAA,EAAa,wBAAA;AAAA,IACb,OAAA,EAAS,OAAO,KAAA,KAAwC;AACtD,MAAA,MAAM,MAAA,GAASF,eAAc,MAAM,CAAA;AAMnC,MAAA,MAAM,MAAA,GAA8B;AAAA,QAClC,KAAK,KAAA,CAAM,GAAA;AAAA,QACX,OAAA,EACE,MAAA,KAAW,MAAA,GAAS,CAAC,MAAA,EAAQ,UAAA,EAAY,OAAO,CAAA,GAAI,CAAC,UAAA,EAAY,MAAA,EAAQ,OAAO;AAAA,OACpF;AACA,MAAA,IAAI,OAAA,SAAgB,OAAA,GAAU,OAAA;AAE9B,MAAA,IAAI,MAAA,KAAW,UAAA,EAAY,MAAA,CAAO,gBAAA,GAAmB,cAAA;AAErD,MAAA,IAAI,GAAA;AACJ,MAAA,IAAI;AACF,QAAA,GAAA,GAAM,MAAM,MAAA,CAAO,OAAA,CAAQ,MAAM,CAAA;AAAA,MACnC,SAAS,GAAA,EAAK;AACZ,QAAA,MAAM,eAAe,GAAG,CAAA;AAAA,MAC1B;AAEA,MAAA,OAAO,wBAAA,CAAyB,GAAA,EAAK,EAAE,MAAA,EAAQ,kBAAkB,CAAA;AAAA,IACnE;AAAA,GACD,CAAA;AACH","file":"index.js","sourcesContent":["import { z } from 'zod';\n\n/**\n * The tool input the model fills in. Kept deliberately small: the model only\n * chooses the query and (optionally) how many results it wants. All policy\n * (depth, focus, region, caps) is fixed by the developer via the factory\n * config, not by the model.\n */\nexport const nimbleSearchInputSchema = z.object({\n query: z.string().min(1).describe('The web search query.'),\n maxResults: z\n .number()\n .int()\n .positive()\n .optional()\n .describe('How many results to return (clamped to the developer-configured cap).'),\n});\n\nexport type NimbleSearchInput = z.infer<typeof nimbleSearchInputSchema>;\n\n/**\n * v1 exposes only the two non-enterprise depths. `fast` is enterprise-gated and\n * intentionally not offered here.\n */\nexport type SearchDepth = 'lite' | 'deep';\n\n/**\n * Developer-facing factory config. `focus` is fixed to `general` in v1 and is\n * not exposed. `include_answer` and `search_depth: 'fast'` are intentionally\n * absent (enterprise / unverified-entitlement surface).\n */\nexport interface NimbleSearchToolConfig {\n /** Nimble API key. Defaults to `process.env.NIMBLE_API_KEY`. */\n apiKey?: string;\n /** Inject a pre-built / mock Nimble client (tests, advanced users). */\n client?: NimbleSearchClient;\n /** Default number of results when the model doesn't specify. Default 5. */\n maxResults?: number;\n /** Hard upper bound on results, regardless of model request. Default 10. */\n maxResultsCap?: number;\n /** Search depth. Default `lite`. */\n searchDepth?: SearchDepth;\n /** ISO country for result localization. Default `US`. */\n country?: string;\n /** Locale for result localization. Default `en`. */\n locale?: string;\n /** Truncate each result's body to this many characters. Default 10_000. */\n maxContentLength?: number;\n}\n\n/**\n * Structural surface of `@nimble-way/nimble-js`'s `client.search()` that this\n * package relies on. Declared structurally so the scaffold typechecks without\n * pinning to the SDK's generated type names, and so tests can inject a mock.\n *\n * Phase B: reconcile field casing/names against\n * `sdks/nimble-js/checkout/src/` after `./tools/sync-sdk.sh nimble-js`.\n */\nexport interface NimbleSearchParams {\n query: string;\n max_results?: number;\n search_depth?: SearchDepth;\n focus?: string;\n country?: string;\n locale?: string;\n}\n\n/** Metadata for SERP-based results (general/news/location focus). */\nexport interface NimbleSerpMetadata {\n country: string;\n entity_type: string;\n locale: string;\n position: number;\n driver?: string | null;\n}\n\n/** Metadata for WSA-based results (shopping/social/geo focus). */\nexport interface NimbleWsaMetadata {\n agent_name: string;\n}\n\nexport interface NimbleRawSearchResult {\n /** Full page text in `deep`; may be empty in `lite`. */\n content: string;\n description: string;\n title: string;\n url: string;\n /** SERP focus (v1 `general`) yields {@link NimbleSerpMetadata}. */\n metadata: NimbleSerpMetadata | NimbleWsaMetadata;\n /** Platform-specific extras (price, publish_date, …); omitted when none. */\n additional_data?: Record<string, unknown> | null;\n}\n\nexport interface NimbleRawSearchResponse {\n request_id: string;\n results: NimbleRawSearchResult[];\n total_results: number;\n /** Intentionally never surfaced in v1 (include_answer is off). */\n answer?: string | null;\n}\n\nexport interface NimbleSearchClient {\n search(params: NimbleSearchParams): Promise<NimbleRawSearchResponse>;\n}\n\n/** A single normalized result item returned to the model. */\nexport interface NimbleSearchResultItem {\n title: string;\n url: string;\n description?: string;\n content?: string;\n position?: number;\n entityType?: string;\n}\n\n/** The normalized tool output. `answer` is intentionally omitted in v1. */\nexport interface NimbleSearchOutput {\n query: string;\n requestId?: string;\n totalResults?: number;\n results: NimbleSearchResultItem[];\n}\n\n// ── Extract ────────────────────────────────────────────────────────────────\n\n/** Output format for extracted page content. */\nexport type ExtractFormat = 'markdown' | 'html';\n\n/**\n * The extract tool input the model fills in: just the URL to read. All policy\n * (format, region, length cap) is fixed by the developer via the factory config.\n */\nexport const nimbleExtractInputSchema = z.object({\n url: z.string().url().describe('The URL of the web page to extract clean content from.'),\n});\n\nexport type NimbleExtractInput = z.infer<typeof nimbleExtractInputSchema>;\n\n/** Developer-facing factory config for the extract tool. */\nexport interface NimbleExtractToolConfig {\n /** Nimble API key. Defaults to `process.env.NIMBLE_API_KEY`. */\n apiKey?: string;\n /** Inject a pre-built / mock Nimble client (tests, advanced users). */\n client?: NimbleExtractClient;\n /** Content format. Default `markdown`. */\n format?: ExtractFormat;\n /** ISO country for geolocation / proxy selection. */\n country?: string;\n /** Truncate the extracted content to this many characters. Default 50_000. */\n maxContentLength?: number;\n}\n\n/** Params this package sends to the SDK's `client.extract()`. */\nexport interface NimbleExtractParams {\n url: string;\n country?: string;\n /** Which renderings to request; `data.<format>` is populated per entry. */\n formats?: Array<'html' | 'markdown' | 'links'>;\n /** Refines Markdown extraction; `main_content` yields the cleaned article. */\n markdown_backend?: 'full_page' | 'main_content';\n}\n\n/** Structural surface of the SDK extract response data this package consumes. */\nexport interface NimbleRawExtractData {\n /** Markdown rendering of the page (default). */\n markdown?: string;\n /** Raw HTML of the page. */\n html?: string;\n /** Unique URLs found on the page. */\n links?: string[];\n}\n\nexport interface NimbleRawExtractResponse {\n url: string;\n status: string;\n status_code?: number;\n task_id: string;\n data: NimbleRawExtractData;\n warnings?: string[];\n}\n\nexport interface NimbleExtractClient {\n extract(params: NimbleExtractParams): Promise<NimbleRawExtractResponse>;\n}\n\n/** The normalized extract output returned to the model. */\nexport interface NimbleExtractOutput {\n /** The final URL (after redirects). */\n url: string;\n /** Task status reported by Nimble (e.g. `success`). */\n status: string;\n statusCode?: number;\n format: ExtractFormat;\n /** The extracted page content in the requested format, truncated. */\n content: string;\n /** Unique links found on the page, when available. */\n links?: string[];\n}\n","import type {\n ExtractFormat,\n NimbleExtractOutput,\n NimbleRawExtractResponse,\n NimbleRawSearchResponse,\n NimbleRawSearchResult,\n NimbleSearchOutput,\n NimbleSearchResultItem,\n NimbleSerpMetadata,\n} from './schemas';\n\nexport interface NormalizeOptions {\n query: string;\n maxContentLength: number;\n}\n\n/** SERP focus (v1 `general`) carries position + entity_type; WSA does not. */\nfunction isSerpMetadata(\n metadata: NimbleRawSearchResult['metadata'],\n): metadata is NimbleSerpMetadata {\n return 'position' in metadata;\n}\n\nfunction truncate(text: string, max: number): string {\n return text.length > max ? text.slice(0, max) : text;\n}\n\n/**\n * Map a raw `/v1/search` response into the package's normalized output shape.\n *\n * - `description` is the snippet (always present when the API returns one).\n * - `content` is the full page text, present only in `deep` depth (empty in\n * `lite`), truncated to `maxContentLength`.\n * - `position` / `entityType` come from SERP metadata; for WSA results\n * `position` falls back to the array index and `entityType` is omitted.\n * - Results without a URL are dropped (defensive).\n * - `response.answer` is never surfaced in v1.\n */\nexport function normalizeSearchResponse(\n response: NimbleRawSearchResponse,\n options: NormalizeOptions,\n): NimbleSearchOutput {\n const results: NimbleSearchResultItem[] = [];\n\n response.results.forEach((raw, index) => {\n if (!raw.url) return;\n\n const item: NimbleSearchResultItem = {\n title: raw.title ?? '',\n url: raw.url,\n };\n if (raw.description) item.description = raw.description;\n if (raw.content && raw.content.trim().length > 0) {\n item.content = truncate(raw.content, options.maxContentLength);\n }\n\n if (isSerpMetadata(raw.metadata)) {\n item.position = raw.metadata.position;\n item.entityType = raw.metadata.entity_type;\n } else {\n item.position = index + 1;\n }\n\n results.push(item);\n });\n\n return {\n query: options.query,\n requestId: response.request_id,\n totalResults: response.total_results,\n results,\n };\n}\n\nexport interface NormalizeExtractOptions {\n format: ExtractFormat;\n maxContentLength: number;\n}\n\n/**\n * Map a raw `/v1/extract` response into the package's normalized extract shape.\n *\n * - `content` is `data.markdown` (default) or `data.html`, falling back to the\n * other when the requested one is empty, truncated to `maxContentLength`.\n * - `format` reflects the rendering actually returned, which may differ from\n * the requested format when the fallback is used.\n * - `links` is surfaced when present; everything else (browser actions, network\n * captures, screenshots) is intentionally dropped.\n */\nexport function normalizeExtractResponse(\n response: NimbleRawExtractResponse,\n options: NormalizeExtractOptions,\n): NimbleExtractOutput {\n const data = response.data;\n const otherFormat: ExtractFormat = options.format === 'html' ? 'markdown' : 'html';\n const primary = options.format === 'html' ? data.html : data.markdown;\n const fallback = options.format === 'html' ? data.markdown : data.html;\n\n // Report the format that actually populated `content`. We request one\n // rendering, but if the API returns it empty we fall back to the other — and\n // the model must be told which format it received, not the one we asked for.\n const usedFormat: ExtractFormat =\n primary && primary.length > 0\n ? options.format\n : fallback && fallback.length > 0\n ? otherFormat\n : options.format;\n const content = truncate(primary || fallback || '', options.maxContentLength);\n\n const out: NimbleExtractOutput = {\n url: response.url,\n status: response.status,\n format: usedFormat,\n content,\n };\n if (typeof response.status_code === 'number') out.statusCode = response.status_code;\n if (data.links && data.links.length > 0) out.links = data.links;\n return out;\n}\n","/**\n * Thrown when the tool is invoked without a resolvable API key (no `apiKey`\n * config and no `NIMBLE_API_KEY` in the environment) and no injected client.\n * Raised at execute time, not at factory-construction time, so the tool can be\n * constructed in environments without a key (e.g. unit tests, type-checking).\n */\nexport class NimbleConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'NimbleConfigError';\n }\n}\n\n/**\n * Wraps an error surfaced by the Nimble client / API during a search call,\n * preserving the HTTP status when available. The AI SDK surfaces a thrown\n * tool error back to the model as a tool-call failure.\n */\nexport class NimbleSearchError extends Error {\n readonly status?: number;\n\n constructor(message: string, options?: { status?: number; cause?: unknown }) {\n super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);\n this.name = 'NimbleSearchError';\n this.status = options?.status;\n }\n}\n\n/**\n * Wraps an error surfaced by the Nimble client / API during an extract call,\n * preserving the HTTP status when available.\n */\nexport class NimbleExtractError extends Error {\n readonly status?: number;\n\n constructor(message: string, options?: { status?: number; cause?: unknown }) {\n super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);\n this.name = 'NimbleExtractError';\n this.status = options?.status;\n }\n}\n","import { tool } from 'ai';\nimport { Nimble } from '@nimble-way/nimble-js';\nimport { nimbleSearchInputSchema } from './schemas';\nimport type {\n NimbleSearchClient,\n NimbleSearchOutput,\n NimbleSearchParams,\n NimbleSearchToolConfig,\n} from './schemas';\nimport { normalizeSearchResponse } from './normalize';\nimport { NimbleConfigError, NimbleSearchError } from './errors';\n\n/** v1 factory defaults. `focus` is fixed to `general` and not user-exposed. */\nexport const NIMBLE_SEARCH_DEFAULTS = {\n maxResults: 5,\n maxResultsCap: 10,\n searchDepth: 'lite',\n country: 'US',\n locale: 'en',\n maxContentLength: 10_000,\n focus: 'general',\n} as const;\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max);\n}\n\nfunction readStatus(err: unknown): number | undefined {\n if (typeof err === 'object' && err !== null && 'status' in err) {\n const status = (err as { status?: unknown }).status;\n return typeof status === 'number' ? status : undefined;\n }\n return undefined;\n}\n\nfunction toSearchError(err: unknown): NimbleSearchError {\n if (err instanceof NimbleSearchError) return err;\n const message = err instanceof Error ? err.message : String(err);\n return new NimbleSearchError(`Nimble search failed: ${message}`, {\n status: readStatus(err),\n cause: err,\n });\n}\n\nfunction resolveClient(config: NimbleSearchToolConfig): NimbleSearchClient {\n if (config.client) return config.client;\n const apiKey = config.apiKey ?? process.env.NIMBLE_API_KEY;\n if (!apiKey) {\n throw new NimbleConfigError(\n 'Missing Nimble API key: set NIMBLE_API_KEY or pass { apiKey } to nimbleSearch().',\n );\n }\n return new Nimble({ apiKey }) as unknown as NimbleSearchClient;\n}\n\n/**\n * Create a ready-made Vercel AI SDK web-search tool backed by Nimble Search\n * (`@nimble-way/nimble-js` → `POST /v1/search`).\n *\n * The model only chooses `{ query, maxResults? }`; all policy (depth, focus,\n * region, caps) is fixed by `config` — mirroring the Exa `webSearch()` shape.\n *\n * @example\n * ```ts\n * import { generateText } from 'ai';\n * import { nimbleSearch } from '@nimble-way/ai-sdk';\n *\n * const { text } = await generateText({\n * model: 'anthropic/claude-sonnet-4.6',\n * prompt: 'What are the latest Nimble release notes?',\n * tools: { webSearch: nimbleSearch({ searchDepth: 'lite', maxResults: 5 }) },\n * });\n * ```\n */\nexport function nimbleSearch(config: NimbleSearchToolConfig = {}) {\n const maxResults = config.maxResults ?? NIMBLE_SEARCH_DEFAULTS.maxResults;\n const maxResultsCap = config.maxResultsCap ?? NIMBLE_SEARCH_DEFAULTS.maxResultsCap;\n const searchDepth = config.searchDepth ?? NIMBLE_SEARCH_DEFAULTS.searchDepth;\n const country = config.country ?? NIMBLE_SEARCH_DEFAULTS.country;\n const locale = config.locale ?? NIMBLE_SEARCH_DEFAULTS.locale;\n const maxContentLength = config.maxContentLength ?? NIMBLE_SEARCH_DEFAULTS.maxContentLength;\n\n return tool({\n description:\n 'Search the web with Nimble and return ranked results (title, url, ' +\n 'snippet, and page content) for answering questions about current or ' +\n 'factual information.',\n inputSchema: nimbleSearchInputSchema,\n execute: async (input): Promise<NimbleSearchOutput> => {\n const client = resolveClient(config);\n\n const params: NimbleSearchParams = {\n query: input.query,\n max_results: clamp(input.maxResults ?? maxResults, 1, maxResultsCap),\n search_depth: searchDepth,\n focus: NIMBLE_SEARCH_DEFAULTS.focus, // fixed 'general' in v1\n country,\n locale,\n };\n\n let raw;\n try {\n raw = await client.search(params);\n } catch (err) {\n throw toSearchError(err);\n }\n\n return normalizeSearchResponse(raw, {\n query: input.query,\n maxContentLength,\n });\n },\n });\n}\n","import { tool } from 'ai';\nimport { Nimble } from '@nimble-way/nimble-js';\nimport { nimbleExtractInputSchema } from './schemas';\nimport type {\n ExtractFormat,\n NimbleExtractClient,\n NimbleExtractOutput,\n NimbleExtractParams,\n NimbleExtractToolConfig,\n} from './schemas';\nimport { normalizeExtractResponse } from './normalize';\nimport { NimbleConfigError, NimbleExtractError } from './errors';\n\n/** v1 extract defaults. */\nexport const NIMBLE_EXTRACT_DEFAULTS: { format: ExtractFormat; maxContentLength: number } = {\n format: 'markdown',\n maxContentLength: 50_000,\n};\n\nfunction readStatus(err: unknown): number | undefined {\n if (typeof err === 'object' && err !== null && 'status' in err) {\n const status = (err as { status?: unknown }).status;\n return typeof status === 'number' ? status : undefined;\n }\n return undefined;\n}\n\nfunction toExtractError(err: unknown): NimbleExtractError {\n if (err instanceof NimbleExtractError) return err;\n const message = err instanceof Error ? err.message : String(err);\n return new NimbleExtractError(`Nimble extract failed: ${message}`, {\n status: readStatus(err),\n cause: err,\n });\n}\n\nfunction resolveClient(config: NimbleExtractToolConfig): NimbleExtractClient {\n if (config.client) return config.client;\n const apiKey = config.apiKey ?? process.env.NIMBLE_API_KEY;\n if (!apiKey) {\n throw new NimbleConfigError(\n 'Missing Nimble API key: set NIMBLE_API_KEY or pass { apiKey } to nimbleExtract().',\n );\n }\n return new Nimble({ apiKey }) as unknown as NimbleExtractClient;\n}\n\n/**\n * Create a Vercel AI SDK tool that extracts clean, readable content from a web\n * page with Nimble (`@nimble-way/nimble-js` → `POST /v1/extract`).\n *\n * The model only chooses `{ url }`; format, region, and length cap are fixed by\n * `config`. Mirrors {@link nimbleSearch}'s ergonomics.\n *\n * @example\n * ```ts\n * import { generateText } from 'ai';\n * import { nimbleExtract } from '@nimble-way/ai-sdk';\n *\n * const { text } = await generateText({\n * model: 'openai/gpt-4o-mini',\n * prompt: 'Summarize https://nimbleway.com',\n * tools: { extract: nimbleExtract() },\n * });\n * ```\n */\nexport function nimbleExtract(config: NimbleExtractToolConfig = {}) {\n const format = config.format ?? NIMBLE_EXTRACT_DEFAULTS.format;\n const maxContentLength = config.maxContentLength ?? NIMBLE_EXTRACT_DEFAULTS.maxContentLength;\n const country = config.country;\n\n return tool({\n description:\n 'Fetch a web page by URL with Nimble and return its clean, readable content ' +\n '(markdown or HTML) — use this to read, quote, or summarize a specific page.',\n inputSchema: nimbleExtractInputSchema,\n execute: async (input): Promise<NimbleExtractOutput> => {\n const client = resolveClient(config);\n\n // Request both renderings (preferred format first) plus links. A rendering\n // is only produced when requested via `formats`, so asking for both lets an\n // empty primary fall back to the other; normalizeExtractResponse reports\n // whichever rendering actually populated the content.\n const params: NimbleExtractParams = {\n url: input.url,\n formats:\n format === 'html' ? ['html', 'markdown', 'links'] : ['markdown', 'html', 'links'],\n };\n if (country) params.country = country;\n // `main_content` returns the cleaned article body rather than the full page.\n if (format === 'markdown') params.markdown_backend = 'main_content';\n\n let raw;\n try {\n raw = await client.extract(params);\n } catch (err) {\n throw toExtractError(err);\n }\n\n return normalizeExtractResponse(raw, { format, maxContentLength });\n },\n });\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nimble-way/ai-sdk",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "Nimble Web Search as
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Nimble Web Search and Extract as ready-made tools for the Vercel AI SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"author": "Nimble",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"ai",
|
|
21
21
|
"web-search",
|
|
22
22
|
"search",
|
|
23
|
+
"extract",
|
|
23
24
|
"tool",
|
|
24
25
|
"llm",
|
|
25
26
|
"agent"
|