webfox 4.10.0 → 4.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +2 -12
  2. package/dist/{adapter-UIFBJKKF.js → adapter-44ZBIK47.js} +4 -6
  3. package/dist/{adapter-HTMXAJTU.js → adapter-57MELLYI.js} +2 -8
  4. package/dist/{adapter-VMMS4YQG.js → adapter-BZO5UEI6.js} +3 -15
  5. package/dist/{adapter-OK2PTP6R.js → adapter-D4EXZEDL.js} +16 -48
  6. package/dist/{adapter-G3I7JEO5.js → adapter-DJCTEEOA.js} +3 -5
  7. package/dist/{adapter-764OQFKY.js → adapter-DLNMW34W.js} +8 -35
  8. package/dist/{adapter-HRI5ZMGK.js → adapter-GFXYSHKP.js} +9 -43
  9. package/dist/{adapter-2UVAARTV.js → adapter-GJOUFE2U.js} +1 -1
  10. package/dist/{adapter-C2VZHFUB.js → adapter-MZ7USB6Q.js} +3 -4
  11. package/dist/adapter-ODDFBGJG.js +167 -0
  12. package/dist/{adapter-K6AMSUCR.js → adapter-QQZFHUOP.js} +14 -37
  13. package/dist/{adapter-AUYLTDPM.js → adapter-RR55EZAW.js} +1 -1
  14. package/dist/{adapter-JJSMYZXG.js → adapter-SZMVKAMC.js} +1 -1
  15. package/dist/{adapter-6EZAIE65.js → adapter-VXT4C6RZ.js} +16 -66
  16. package/dist/{adapter-L6BFKWGX.js → adapter-WXEVTCM3.js} +7 -14
  17. package/dist/{adapter-ZYDUANQL.js → adapter-XSQX3GAI.js} +13 -5
  18. package/dist/{adapter-EIDSGLOP.js → adapter-XU2APKPC.js} +3 -3
  19. package/dist/{adapter-A5WYGGZI.js → adapter-YVZ4YBK5.js} +8 -16
  20. package/dist/chunk-6RQGO727.js +71 -0
  21. package/dist/{chunk-TGULKHB5.js → chunk-KMUCUJVN.js} +3 -3
  22. package/dist/{chunk-EF4VMDZL.js → chunk-LDVDVA4Z.js} +1 -4
  23. package/dist/{chunk-57OOEOXI.js → chunk-NENL6L6N.js} +1 -0
  24. package/dist/{chunk-5FP7KNMC.js → chunk-PVNWACMT.js} +132 -18
  25. package/dist/{chunk-YZXM4U2C.js → chunk-TYJ6RHKB.js} +10 -0
  26. package/dist/{chunk-3WBE6UEW.js → chunk-YILOS4E7.js} +23 -9
  27. package/dist/cli.js +5 -5
  28. package/dist/config.schema.json +187 -0
  29. package/dist/contents.d.ts +5 -0
  30. package/dist/document-schema.d.ts +17 -5
  31. package/dist/domain.d.ts +14 -2
  32. package/dist/durable.js +5 -5
  33. package/dist/index.js +4 -4
  34. package/dist/pi.js +33 -14
  35. package/dist/providers/contract.d.ts +11 -1
  36. package/dist/providers/registry.d.ts +1 -0
  37. package/dist/providers/sofya/adapter.d.ts +8 -0
  38. package/dist/providers/sofya/definition.d.ts +1 -0
  39. package/dist/providers/sofya/types.d.ts +4 -0
  40. package/dist/render.d.ts +8 -2
  41. package/docs/provider.md +27 -0
  42. package/examples/custom/README.md +9 -3
  43. package/package.json +1 -1
  44. package/dist/chunk-BTWECET5.js +0 -31
package/README.md CHANGED
@@ -11,18 +11,6 @@ Search the web, extract pages, get grounded answers, and run deep research with
11
11
  one command: `web`. Choose your providers, bring your API keys, and use the same
12
12
  configuration in your terminal, TypeScript applications, and pi.
13
13
 
14
- > [!IMPORTANT]
15
- > **Coming from pi-web-providers?** Webfox continues the project as **v4.0.0**.
16
- > Replace the old Pi package:
17
- >
18
- > ```sh
19
- > pi remove npm:pi-web-providers
20
- > pi install npm:webfox
21
- > ```
22
- >
23
- > Recreate your provider defaults in the [Webfox configuration](./docs/reference.md#configuration)
24
- > and restart pi. The old `~/.pi/agent/web-providers.json` settings aren't imported.
25
-
26
14
  ## 🚀 Installation
27
15
 
28
16
  - **Terminal:** Install the `web` command with [Homebrew](https://github.com/mavam/homebrew-tap):
@@ -152,6 +140,7 @@ Use different providers for different tasks:
152
140
  | [Perplexity] | ✔︎ | | ✔︎ | ✔︎ |
153
141
  | [SerpBase] | ✔︎ | | | |
154
142
  | [Serper] | ✔︎ | | | |
143
+ | [Sofya] | ✔︎ | ✔︎ | ✔︎ | ✔︎ |
155
144
  | [Tavily] | ✔︎ | ✔︎ | | |
156
145
  | [Valyu] | ✔︎ | ✔︎ | ✔︎ | ✔︎ |
157
146
  | [You.com] | ✔︎ | | | |
@@ -170,6 +159,7 @@ Use different providers for different tasks:
170
159
  [Perplexity]: ./docs/provider.md#perplexity
171
160
  [SerpBase]: ./docs/provider.md#serpbase
172
161
  [Serper]: ./docs/provider.md#serper
162
+ [Sofya]: ./docs/provider.md#sofya
173
163
  [Tavily]: ./docs/provider.md#tavily
174
164
  [Valyu]: ./docs/provider.md#valyu
175
165
  [You.com]: ./docs/provider.md#youcom
@@ -2,8 +2,9 @@ import {
2
2
  trimSnippet
3
3
  } from "./chunk-FSTCLBSF.js";
4
4
  import {
5
+ omittedContents,
5
6
  orderedContents
6
- } from "./chunk-YZXM4U2C.js";
7
+ } from "./chunk-TYJ6RHKB.js";
7
8
  import "./chunk-U4BLULLV.js";
8
9
 
9
10
  // src/providers/tavily/adapter.ts
@@ -20,7 +21,7 @@ var tavilyImplementation = {
20
21
  return {
21
22
  provider: "tavily",
22
23
  results: response.results.slice(0, maxResults).map((result) => ({
23
- title: result.title || result.url || "Untitled",
24
+ title: result.title,
24
25
  url: result.url || "",
25
26
  snippet: trimSnippet(result.content ?? result.rawContent),
26
27
  score: typeof result.score === "number" ? result.score : void 0,
@@ -57,10 +58,7 @@ var tavilyImplementation = {
57
58
  error: failedResult.error || "Content extraction failed."
58
59
  };
59
60
  }
60
- return {
61
- url,
62
- error: "No content returned for this URL."
63
- };
61
+ return omittedContents(url);
64
62
  })
65
63
  };
66
64
  }
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-FSTCLBSF.js";
5
5
  import {
6
6
  orderedContents
7
- } from "./chunk-YZXM4U2C.js";
7
+ } from "./chunk-TYJ6RHKB.js";
8
8
  import {
9
9
  asWebfoxError,
10
10
  httpError
@@ -36,7 +36,7 @@ async function searchOllama(query, maxResults, config, context) {
36
36
  return {
37
37
  provider: "ollama",
38
38
  results: results.slice(0, clampMaxResults(maxResults)).map((result) => ({
39
- title: result.title || result.url || "Untitled",
39
+ title: result.title,
40
40
  url: result.url ?? "",
41
41
  snippet: trimSnippet(result.content)
42
42
  }))
@@ -69,12 +69,6 @@ async function fetchOllamaContents(urls, config, context) {
69
69
  }
70
70
  const data = await response.json();
71
71
  const content = normalizeContentText(data.content);
72
- if (!content) {
73
- return {
74
- url,
75
- error: "No content returned for this URL."
76
- };
77
- }
78
72
  const metadata = buildFetchMetadata(data);
79
73
  return {
80
74
  url,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  executeAsyncResearch
3
- } from "./chunk-EF4VMDZL.js";
3
+ } from "./chunk-LDVDVA4Z.js";
4
4
  import "./chunk-I2UIACY3.js";
5
5
  import "./chunk-WCTOYZVB.js";
6
6
  import {
@@ -315,23 +315,11 @@ function ensureCompletedResponse(response, operation) {
315
315
  throw new Error(`${operation} did not complete (status: ${status})`);
316
316
  }
317
317
  function formatResponseOutput(response, operation) {
318
- const lines = [];
319
- lines.push(
320
- response.output_text?.trim() || `OpenAI ${operation} completed without textual output.`
321
- );
322
318
  const citations = extractUrlCitations(response);
323
- if (citations.length > 0) {
324
- lines.push("");
325
- lines.push("Sources:");
326
- for (const [index, citation] of citations.entries()) {
327
- lines.push(`${index + 1}. ${citation.title}`);
328
- lines.push(` ${citation.url}`);
329
- }
330
- }
331
319
  return {
332
320
  provider: "openai",
333
- text: lines.join("\n").trimEnd(),
334
- itemCount: citations.length,
321
+ text: response.output_text ?? "",
322
+ sources: citations.map(({ title, url }) => ({ title, url })),
335
323
  metadata: {
336
324
  responseId: response.id,
337
325
  model: response.model,
@@ -38,7 +38,6 @@ var perplexityImplementation = {
38
38
  config,
39
39
  context,
40
40
  DEFAULT_ANSWER_MODEL,
41
- "Answer",
42
41
  options
43
42
  );
44
43
  },
@@ -48,12 +47,11 @@ var perplexityImplementation = {
48
47
  config,
49
48
  context,
50
49
  DEFAULT_RESEARCH_MODEL,
51
- "Research",
52
50
  options
53
51
  );
54
52
  }
55
53
  };
56
- async function runSilentForegroundChatTool(input, config, context, fallbackModel, label, options) {
54
+ async function runSilentForegroundChatTool(input, config, context, fallbackModel, options) {
57
55
  const client = createClient(config);
58
56
  const request = {
59
57
  ...options ?? {},
@@ -67,23 +65,13 @@ async function runSilentForegroundChatTool(input, config, context, fallbackModel
67
65
  );
68
66
  const content = extractMessageText(response.choices[0]?.message?.content);
69
67
  const sources = dedupeSources(extractSources(response));
70
- const lines = [];
71
- lines.push(content || `No ${label.toLowerCase()} returned.`);
72
- if (sources.length > 0) {
73
- lines.push("");
74
- lines.push("Sources:");
75
- for (const [index, source] of sources.entries()) {
76
- lines.push(`${index + 1}. ${source.title}`);
77
- lines.push(` ${source.url}`);
78
- }
79
- }
80
68
  return {
81
69
  provider: "perplexity",
82
- text: lines.join("\n").trimEnd(),
83
- itemCount: sources.length
70
+ text: content,
71
+ sources
84
72
  };
85
73
  }
86
- async function runStreamingForegroundChatTool(input, config, context, fallbackModel, label, options) {
74
+ async function runStreamingForegroundChatTool(input, config, context, fallbackModel, options) {
87
75
  const client = createClient(config);
88
76
  const request = {
89
77
  ...options ?? {},
@@ -106,21 +94,11 @@ async function runStreamingForegroundChatTool(input, config, context, fallbackMo
106
94
  }
107
95
  sources.push(...extractSources(chunk));
108
96
  }
109
- const finalText = partialText.trim() || extractMessageText(lastChunk?.choices?.[0]?.message?.content) || `No ${label.toLowerCase()} returned.`;
110
- const dedupedSources = dedupeSources(sources);
111
- const lines = [finalText];
112
- if (dedupedSources.length > 0) {
113
- lines.push("");
114
- lines.push("Sources:");
115
- for (const [index, source] of dedupedSources.entries()) {
116
- lines.push(`${index + 1}. ${source.title}`);
117
- lines.push(` ${source.url}`);
118
- }
119
- }
97
+ const finalText = partialText.trim() || extractMessageText(lastChunk?.choices?.[0]?.message?.content);
120
98
  return {
121
99
  provider: "perplexity",
122
- text: lines.join("\n").trimEnd(),
123
- itemCount: dedupedSources.length
100
+ text: finalText,
101
+ sources: dedupeSources(sources)
124
102
  };
125
103
  }
126
104
  function createClient(config) {
@@ -182,32 +160,22 @@ function extractDeltaText(content) {
182
160
  }
183
161
  function dedupeSources(sources) {
184
162
  const seen = /* @__PURE__ */ new Set();
185
- const unique = [];
186
- for (const source of sources) {
187
- const title = source.title.trim() || source.url.trim() || "Untitled";
188
- const url = source.url.trim();
189
- if (!url) continue;
190
- const key = `${title.toLowerCase()}::${url.toLowerCase()}`;
191
- if (seen.has(key)) continue;
163
+ return sources.filter((source) => {
164
+ const key = `${source.title?.trim().toLowerCase() ?? ""}::${source.url?.trim().toLowerCase()}`;
165
+ if (seen.has(key)) return false;
192
166
  seen.add(key);
193
- unique.push({ title, url });
194
- }
195
- return unique;
167
+ return true;
168
+ });
196
169
  }
197
170
  function extractSources(response) {
198
- const searchResults = response.search_results?.flatMap((result) => {
199
- const url = result.url?.trim() ?? "";
200
- if (!url) {
201
- return [];
202
- }
203
- return [{ title: result.title?.trim() ?? url, url }];
204
- }) ?? [];
171
+ const searchResults = response.search_results?.flatMap(
172
+ (result) => result.url?.trim() ? [{ title: result.title ?? void 0, url: result.url }] : []
173
+ ) ?? [];
205
174
  if (searchResults.length > 0) {
206
175
  return searchResults;
207
176
  }
208
177
  return response.citations?.flatMap((citation) => {
209
- const url = citation?.trim() ?? "";
210
- return url ? [{ title: url, url }] : [];
178
+ return citation?.trim() ? [{ url: citation }] : [];
211
179
  }) ?? [];
212
180
  }
213
181
  function buildRequestOptions(context) {
@@ -3,8 +3,9 @@ import {
3
3
  trimSnippet
4
4
  } from "./chunk-FSTCLBSF.js";
5
5
  import {
6
+ omittedContents,
6
7
  orderedContents
7
- } from "./chunk-YZXM4U2C.js";
8
+ } from "./chunk-TYJ6RHKB.js";
8
9
  import "./chunk-U4BLULLV.js";
9
10
 
10
11
  // src/providers/parallel/adapter.ts
@@ -56,10 +57,7 @@ var parallelImplementation = {
56
57
  return error ? {
57
58
  url,
58
59
  error: formatJson(error)
59
- } : {
60
- url,
61
- error: "No content returned for this URL."
62
- };
60
+ } : omittedContents(url);
63
61
  })
64
62
  };
65
63
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  executeAsyncResearch
3
- } from "./chunk-EF4VMDZL.js";
3
+ } from "./chunk-LDVDVA4Z.js";
4
4
  import "./chunk-I2UIACY3.js";
5
5
  import {
6
6
  sleep,
@@ -11,8 +11,9 @@ import {
11
11
  trimSnippet
12
12
  } from "./chunk-FSTCLBSF.js";
13
13
  import {
14
+ omittedContents,
14
15
  orderedContents
15
- } from "./chunk-YZXM4U2C.js";
16
+ } from "./chunk-TYJ6RHKB.js";
16
17
  import "./chunk-U4BLULLV.js";
17
18
 
18
19
  // src/providers/valyu/adapter.ts
@@ -73,10 +74,7 @@ var valyuImplementation = {
73
74
  answers: urls.map((url) => {
74
75
  const result = resultsByUrl.get(url) ?? (urls.length === 1 && finalResponse.results?.length === 1 ? finalResponse.results[0] : void 0);
75
76
  if (!result) {
76
- return {
77
- url,
78
- error: "No content returned for this URL."
79
- };
77
+ return omittedContents(url);
80
78
  }
81
79
  if (result.status === "failed" || result.status === "not_found") {
82
80
  return {
@@ -105,22 +103,10 @@ var valyuImplementation = {
105
103
  "error" in response && typeof response.error === "string" ? response.error : "answer failed"
106
104
  );
107
105
  }
108
- const lines = [];
109
- const contents = typeof response.contents === "string" ? response.contents : formatJson(response.contents);
110
- lines.push(contents);
111
- const sources = response.search_results ?? [];
112
- if (sources.length > 0) {
113
- lines.push("");
114
- lines.push("Sources:");
115
- for (const [index, result] of sources.entries()) {
116
- lines.push(`${index + 1}. ${result.title}`);
117
- lines.push(` ${result.url}`);
118
- }
119
- }
120
106
  return {
121
107
  provider: "valyu",
122
- text: lines.join("\n").trimEnd(),
123
- itemCount: sources.length
108
+ text: typeof response.contents === "string" ? response.contents : response.contents == null ? "" : formatJson(response.contents),
109
+ sources: response.search_results ?? []
124
110
  };
125
111
  },
126
112
  async research(input, config, context, options) {
@@ -155,25 +141,12 @@ var valyuImplementation = {
155
141
  throw new Error(result.error || "deep research failed");
156
142
  }
157
143
  if (result.status === "completed") {
158
- const lines = [];
159
- lines.push(
160
- typeof result.output === "string" ? result.output : result.output ? formatJson(result.output) : "Valyu deep research completed without textual output."
161
- );
162
- const sources = result.sources ?? [];
163
- if (sources.length > 0) {
164
- lines.push("");
165
- lines.push("Sources:");
166
- for (const [index, source] of sources.entries()) {
167
- lines.push(`${index + 1}. ${source.title}`);
168
- lines.push(` ${source.url}`);
169
- }
170
- }
171
144
  return {
172
145
  status: "completed",
173
146
  output: {
174
147
  provider: "valyu",
175
- text: lines.join("\n").trimEnd(),
176
- itemCount: sources.length
148
+ text: typeof result.output === "string" ? result.output : result.output ? formatJson(result.output) : "",
149
+ sources: result.sources ?? []
177
150
  }
178
151
  };
179
152
  }
@@ -5,11 +5,10 @@ import {
5
5
  trimSnippet
6
6
  } from "./chunk-FSTCLBSF.js";
7
7
  import {
8
+ omittedContents,
8
9
  orderedContents
9
- } from "./chunk-YZXM4U2C.js";
10
- import {
11
- WebfoxError
12
- } from "./chunk-U4BLULLV.js";
10
+ } from "./chunk-TYJ6RHKB.js";
11
+ import "./chunk-U4BLULLV.js";
13
12
 
14
13
  // src/providers/exa/adapter.ts
15
14
  var DEFAULT_BASE_URL = "https://api.exa.ai";
@@ -28,7 +27,7 @@ var exaImplementation = {
28
27
  return {
29
28
  provider: "exa",
30
29
  results: (response.results ?? []).slice(0, maxResults).map((result) => ({
31
- title: String(result.title ?? result.url ?? "Untitled"),
30
+ title: typeof result.title === "string" ? result.title : void 0,
32
31
  url: String(result.url ?? ""),
33
32
  snippet: trimSnippet(
34
33
  typeof result.text === "string" ? result.text : Array.isArray(result.highlights) ? result.highlights.join(" ") : typeof result.summary === "string" ? result.summary : ""
@@ -50,10 +49,7 @@ var exaImplementation = {
50
49
  answers: urls.map((url, index) => {
51
50
  const result = results[index];
52
51
  if (!result) {
53
- return {
54
- url,
55
- error: "No content returned for this URL."
56
- };
52
+ return omittedContents(url);
57
53
  }
58
54
  return {
59
55
  url: typeof result.url === "string" ? result.url : url,
@@ -79,25 +75,10 @@ var exaImplementation = {
79
75
  },
80
76
  context
81
77
  );
82
- const lines = [];
83
- lines.push(
84
- typeof response.answer === "string" ? response.answer : formatJson(response.answer)
85
- );
86
- const citations = response.citations ?? [];
87
- if (citations.length > 0) {
88
- lines.push("");
89
- lines.push("Sources:");
90
- for (const [index, citation] of citations.entries()) {
91
- lines.push(
92
- `${index + 1}. ${String(citation.title ?? citation.url ?? "Untitled")}`
93
- );
94
- lines.push(` ${String(citation.url ?? "")}`);
95
- }
96
- }
97
78
  return {
98
79
  provider: "exa",
99
- text: lines.join("\n").trimEnd(),
100
- itemCount: citations.length
80
+ text: typeof response.answer === "string" ? response.answer : response.answer == null ? "" : formatJson(response.answer),
81
+ sources: response.citations ?? []
101
82
  };
102
83
  },
103
84
  async research(input, config, context, options) {
@@ -117,25 +98,10 @@ var exaImplementation = {
117
98
  context
118
99
  );
119
100
  const content = result.output?.content;
120
- if (typeof content !== "string" || !content.trim())
121
- throw new WebfoxError(
122
- "PROVIDER_FAILURE",
123
- "Exa returned no research report."
124
- );
125
- const sources = result.results ?? [];
126
- const lines = [content.trim()];
127
- if (sources.length) {
128
- lines.push("", "Sources:");
129
- for (const [index, source] of sources.entries())
130
- lines.push(
131
- `${index + 1}. ${source.title || source.url}`,
132
- ` ${source.url}`
133
- );
134
- }
135
101
  return {
136
102
  provider: "exa",
137
- text: lines.join("\n"),
138
- itemCount: sources.length
103
+ text: typeof content === "string" ? content : "",
104
+ sources: result.results ?? []
139
105
  };
140
106
  }
141
107
  };
@@ -67,7 +67,7 @@ function joinUrl(baseUrl) {
67
67
  function collectResults(results, section, searchMetadata) {
68
68
  return array(results).map((entry) => asRecord(entry)).filter((entry) => Object.keys(entry).length > 0).map((entry) => {
69
69
  const url = string(entry.url) ?? "";
70
- const title = string(entry.title) ?? string(entry.name) ?? (url || "Untitled");
70
+ const title = string(entry.title) ?? string(entry.name);
71
71
  return {
72
72
  title,
73
73
  url,
@@ -334,10 +334,9 @@ function toSearchResult(entry, searchContext, mode) {
334
334
  record.query,
335
335
  record.value,
336
336
  user?.name,
337
- formatReviewTitle(record, user),
338
- resultUrl
339
- ) ?? "Untitled";
340
- const url = resultUrl || (mode === "autocomplete" ? toGoogleSearchUrl(title) : "");
337
+ formatReviewTitle(record, user)
338
+ );
339
+ const url = resultUrl || (mode === "autocomplete" && title ? toGoogleSearchUrl(title) : "");
341
340
  const snippet = trimSnippet(
342
341
  firstNonEmptyString(
343
342
  record.snippet,
@@ -0,0 +1,167 @@
1
+ import {
2
+ joinUrl,
3
+ requestJson,
4
+ trimSnippet
5
+ } from "./chunk-FSTCLBSF.js";
6
+ import {
7
+ orderedContents
8
+ } from "./chunk-TYJ6RHKB.js";
9
+ import {
10
+ WebfoxError,
11
+ asWebfoxError
12
+ } from "./chunk-U4BLULLV.js";
13
+
14
+ // src/providers/sofya/adapter.ts
15
+ var DEFAULT_BASE_URL = "https://sofya.co";
16
+ var MAX_RESULTS = 20;
17
+ var adapter = {
18
+ async search(request, config, context) {
19
+ const maxResults = Math.max(
20
+ 1,
21
+ Math.min(MAX_RESULTS, Math.trunc(request.maxResults || 0))
22
+ );
23
+ const payload = await sofyaRequest(config, context, "v1/search", {
24
+ ...asRecord(request.options),
25
+ query: request.query,
26
+ max_results: maxResults
27
+ });
28
+ const entries = payload.results;
29
+ if (!Array.isArray(entries)) throw invalidResponse("search");
30
+ const results = entries.map(toSearchResult).filter((result) => result !== void 0);
31
+ return { provider: "sofya", results: results.slice(0, maxResults) };
32
+ },
33
+ async contents(request, config, context) {
34
+ const answers = await Promise.all(
35
+ request.urls.map(async (url) => {
36
+ try {
37
+ const payload = await sofyaRequest(config, context, "v1/fetch", {
38
+ ...asRecord(request.options),
39
+ urls: [url]
40
+ });
41
+ const result = Array.isArray(payload.results) ? asRecord(payload.results[0]) : void 0;
42
+ if (!result) throw invalidResponse("fetch");
43
+ if (result.success === false) {
44
+ return { url, error: text(result.error) ?? "Fetch failed." };
45
+ }
46
+ const {
47
+ url: finalUrl,
48
+ content,
49
+ success: _s,
50
+ error: _e,
51
+ ...rest
52
+ } = result;
53
+ const metadata = Object.fromEntries(
54
+ Object.entries(rest).filter(
55
+ ([, value]) => value !== null && value !== ""
56
+ )
57
+ );
58
+ return {
59
+ url: text(finalUrl) ?? url,
60
+ content: typeof content === "string" ? content : "",
61
+ ...Object.keys(metadata).length > 0 ? { metadata } : {}
62
+ };
63
+ } catch (error) {
64
+ return { url, error: asWebfoxError(error).toJSON() };
65
+ }
66
+ })
67
+ );
68
+ return orderedContents({ provider: "sofya", answers });
69
+ },
70
+ async answer(request, config, context) {
71
+ const payload = await sofyaRequest(config, context, "v1/search", {
72
+ ...asRecord(request.options),
73
+ query: request.query,
74
+ include_answer: true
75
+ });
76
+ const sources = (Array.isArray(payload.results) ? payload.results : []).map(toSearchResult).filter((result) => result !== void 0).map(({ title, url }) => ({ title, url }));
77
+ return output(payload.answer, sources, queryMetadata(payload));
78
+ },
79
+ async research(request, config, context) {
80
+ context.onProgress?.("Sofya is researching and writing the report.");
81
+ const payload = await sofyaRequest(config, context, "v1/research", {
82
+ ...asRecord(request.options),
83
+ query: request.input
84
+ });
85
+ const sources = (Array.isArray(payload.sources) ? payload.sources : []).map(asRecord).filter((source) => source !== void 0).map((source) => ({ title: text(source.title), url: text(source.url) }));
86
+ return output(
87
+ payload.report,
88
+ sources,
89
+ Array.isArray(payload.sub_queries) ? { sub_queries: payload.sub_queries } : {}
90
+ );
91
+ }
92
+ };
93
+ async function sofyaRequest(config, context, path, body) {
94
+ const apiKey = config.credentials?.api;
95
+ if (!apiKey) throw new Error("Sofya is missing an API key");
96
+ const payload = asRecord(
97
+ await requestJson(
98
+ joinUrl(config.baseUrl ?? DEFAULT_BASE_URL, path),
99
+ {
100
+ method: "POST",
101
+ headers: { Authorization: `Bearer ${apiKey}` },
102
+ body,
103
+ signal: context.signal
104
+ },
105
+ describeError
106
+ )
107
+ );
108
+ if (!payload) throw invalidResponse(path.replace("v1/", ""));
109
+ return payload;
110
+ }
111
+ function output(body, sources, metadata) {
112
+ return {
113
+ provider: "sofya",
114
+ text: typeof body === "string" ? body : "",
115
+ sources,
116
+ ...Object.keys(metadata).length > 0 ? { metadata } : {}
117
+ };
118
+ }
119
+ function queryMetadata(payload) {
120
+ return Object.fromEntries(
121
+ ["altered_query", "relaxed_query"].flatMap((key) => {
122
+ const value = text(payload[key]);
123
+ return value ? [[key, value]] : [];
124
+ })
125
+ );
126
+ }
127
+ function invalidResponse(endpoint) {
128
+ return new WebfoxError(
129
+ "PROVIDER_FAILURE",
130
+ `Sofya returned an invalid ${endpoint} response.`,
131
+ { retryable: false }
132
+ );
133
+ }
134
+ function describeError(payload, response) {
135
+ const record = asRecord(payload);
136
+ const detail = text(record?.detail) ?? (typeof payload === "string" ? text(payload) : void 0) ?? (record ? JSON.stringify(record.detail ?? record) : void 0);
137
+ const trimmed = detail?.trim().slice(0, 500);
138
+ return `Sofya request failed (HTTP ${response.status})${trimmed ? `: ${trimmed}` : "."}`;
139
+ }
140
+ function toSearchResult(entry) {
141
+ const record = asRecord(entry);
142
+ const url = text(record?.url);
143
+ if (!record || !url) return void 0;
144
+ const { title, url: _url, content, description, fetched, ...rest } = record;
145
+ const metadata = Object.fromEntries(
146
+ Object.entries(rest).filter(
147
+ ([, value]) => value !== null && !(Array.isArray(value) && value.length === 0) && !(asRecord(value) && Object.keys(value).length === 0)
148
+ )
149
+ );
150
+ if (fetched === true && text(content)) metadata.content = content;
151
+ const excerpt = text(description) ?? text(content);
152
+ return {
153
+ title: text(title),
154
+ url,
155
+ snippet: excerpt ? trimSnippet(excerpt) : "",
156
+ ...Object.keys(metadata).length > 0 ? { metadata } : {}
157
+ };
158
+ }
159
+ function asRecord(value) {
160
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
161
+ }
162
+ function text(value) {
163
+ return typeof value === "string" && value.trim() ? value : void 0;
164
+ }
165
+ export {
166
+ adapter
167
+ };