@blackdrome/open-deepresearch 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/LICENSE +17 -0
- package/README.md +133 -0
- package/dist/index.cjs +928 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +193 -0
- package/dist/index.d.ts +193 -0
- package/dist/index.js +889 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,889 @@
|
|
|
1
|
+
// src/constraints.ts
|
|
2
|
+
var FORUM_DOMAIN_MAP = [
|
|
3
|
+
{ pattern: /\breddit\b|\br\/[a-z0-9_]+/i, domain: "reddit.com", hint: "reddit" },
|
|
4
|
+
{ pattern: /\bstack\s*overflow\b/i, domain: "stackoverflow.com", hint: "stackoverflow" },
|
|
5
|
+
{ pattern: /\bstack\s*exchange\b|\bstackexchange\b/i, domain: "stackexchange.com", hint: "stackexchange" },
|
|
6
|
+
{ pattern: /\bhacker\s*news\b|\bhn\b/i, domain: "news.ycombinator.com", hint: "hackernews" },
|
|
7
|
+
{ pattern: /\bquora\b/i, domain: "quora.com", hint: "quora" },
|
|
8
|
+
{ pattern: /\bgithub\s+issues\b/i, domain: "github.com", hint: "github" }
|
|
9
|
+
];
|
|
10
|
+
var STRICT_FORUM_PATTERN = /\b(?:only|just|strictly|specifically)\b.*\b(reddit|stack\s*overflow|stack\s*exchange|stackexchange|hacker\s*news|quora|github)\b/i;
|
|
11
|
+
var FORUM_PREPOSITION_PATTERN = /\b(?:from|on|in)\s+(reddit|stack\s*overflow|stack\s*exchange|stackexchange|hacker\s*news|quora|github)\b/i;
|
|
12
|
+
var normalizeDomain = (value) => value.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/^www\./, "");
|
|
13
|
+
var unique = (items) => Array.from(new Set(items.filter(Boolean).map(normalizeDomain)));
|
|
14
|
+
var extractSiteTokens = (query) => {
|
|
15
|
+
const matches = Array.from(query.matchAll(/site:([a-z0-9.-]+)/gi));
|
|
16
|
+
return unique(matches.map((entry) => entry[1]));
|
|
17
|
+
};
|
|
18
|
+
var extractSearchConstraints = (query) => {
|
|
19
|
+
const q = query || "";
|
|
20
|
+
const explicitSites = extractSiteTokens(q);
|
|
21
|
+
const preferredDomains = [];
|
|
22
|
+
const forumHints = [];
|
|
23
|
+
for (const forum of FORUM_DOMAIN_MAP) {
|
|
24
|
+
if (!forum.pattern.test(q)) continue;
|
|
25
|
+
preferredDomains.push(forum.domain);
|
|
26
|
+
forumHints.push(forum.hint);
|
|
27
|
+
}
|
|
28
|
+
const enforceByLanguage = STRICT_FORUM_PATTERN.test(q) || FORUM_PREPOSITION_PATTERN.test(q);
|
|
29
|
+
const enforceRequiredDomains = explicitSites.length > 0 || preferredDomains.length > 0 && enforceByLanguage;
|
|
30
|
+
const requiredDomains = enforceRequiredDomains ? explicitSites.length > 0 ? explicitSites : preferredDomains : explicitSites;
|
|
31
|
+
return {
|
|
32
|
+
requiredDomains: unique(requiredDomains),
|
|
33
|
+
preferredDomains: unique(preferredDomains),
|
|
34
|
+
enforceRequiredDomains,
|
|
35
|
+
forumHints: Array.from(new Set(forumHints))
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
var domainMatches = (urlOrDomain, targetDomains) => {
|
|
39
|
+
if (!targetDomains.length) return true;
|
|
40
|
+
let host = normalizeDomain(urlOrDomain);
|
|
41
|
+
try {
|
|
42
|
+
host = normalizeDomain(new URL(urlOrDomain).hostname);
|
|
43
|
+
} catch {
|
|
44
|
+
host = normalizeDomain(urlOrDomain);
|
|
45
|
+
}
|
|
46
|
+
return targetDomains.some((targetRaw) => {
|
|
47
|
+
const target = normalizeDomain(targetRaw);
|
|
48
|
+
return host === target || host.endsWith(`.${target}`);
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
var mergeConstraints = (base, incoming) => {
|
|
52
|
+
const requiredDomains = unique([...base.requiredDomains || [], ...incoming?.requiredDomains || []]);
|
|
53
|
+
const preferredDomains = unique([...base.preferredDomains || [], ...incoming?.preferredDomains || []]);
|
|
54
|
+
const forumHints = Array.from(/* @__PURE__ */ new Set([...base.forumHints || [], ...incoming?.forumHints || []]));
|
|
55
|
+
const enforceRequiredDomains = Boolean(
|
|
56
|
+
incoming?.enforceRequiredDomains ?? (base.enforceRequiredDomains || requiredDomains.length > 0)
|
|
57
|
+
);
|
|
58
|
+
return {
|
|
59
|
+
requiredDomains,
|
|
60
|
+
preferredDomains,
|
|
61
|
+
forumHints,
|
|
62
|
+
enforceRequiredDomains
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// src/validators/truth-contract.ts
|
|
67
|
+
var buildTruthContractInstruction = (strictness) => {
|
|
68
|
+
const strictLine = strictness === "strict" ? "Do not present uncertain claims as facts. If uncertain, explicitly state uncertainty." : "Prefer high-confidence claims and note uncertainty briefly when needed.";
|
|
69
|
+
return [
|
|
70
|
+
"TRUTH CONTRACT:",
|
|
71
|
+
"- Lead with the direct answer in 1-2 sentences.",
|
|
72
|
+
"- Do not invent sources or facts.",
|
|
73
|
+
"- If web or dossier sources are provided, cite claims as [1], [2], etc.",
|
|
74
|
+
"- Include a short 'What is uncertain' note when confidence is limited.",
|
|
75
|
+
strictLine
|
|
76
|
+
].join("\n");
|
|
77
|
+
};
|
|
78
|
+
var evaluateTruthContract = (response, options) => {
|
|
79
|
+
const text = (response || "").trim();
|
|
80
|
+
const violations = [];
|
|
81
|
+
if (!text) {
|
|
82
|
+
return {
|
|
83
|
+
passed: false,
|
|
84
|
+
violations: ["Empty response"]
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const hasCitation = /\[\d+\]/.test(text);
|
|
88
|
+
if (options?.requiresCitations && !hasCitation) {
|
|
89
|
+
violations.push("Missing source citations in bracket format like [1]");
|
|
90
|
+
}
|
|
91
|
+
const hasUncertaintySection = /what is uncertain/i.test(text) || /uncertain|not enough evidence|insufficient data/i.test(text);
|
|
92
|
+
if (options?.strictness === "strict" && !hasUncertaintySection) {
|
|
93
|
+
violations.push("No explicit uncertainty note found in strict mode");
|
|
94
|
+
}
|
|
95
|
+
const hasOverconfidentPhrases = /guaranteed|always true|definitely 100%|undeniably/i.test(text);
|
|
96
|
+
if (hasOverconfidentPhrases) {
|
|
97
|
+
violations.push("Contains overconfident phrasing");
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
passed: violations.length === 0,
|
|
101
|
+
violations
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// src/utils/text.ts
|
|
106
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
107
|
+
"the",
|
|
108
|
+
"a",
|
|
109
|
+
"an",
|
|
110
|
+
"and",
|
|
111
|
+
"or",
|
|
112
|
+
"to",
|
|
113
|
+
"for",
|
|
114
|
+
"of",
|
|
115
|
+
"in",
|
|
116
|
+
"on",
|
|
117
|
+
"with",
|
|
118
|
+
"is",
|
|
119
|
+
"are",
|
|
120
|
+
"was",
|
|
121
|
+
"were",
|
|
122
|
+
"be",
|
|
123
|
+
"as",
|
|
124
|
+
"at",
|
|
125
|
+
"from",
|
|
126
|
+
"by",
|
|
127
|
+
"that",
|
|
128
|
+
"this",
|
|
129
|
+
"it",
|
|
130
|
+
"about",
|
|
131
|
+
"into",
|
|
132
|
+
"what",
|
|
133
|
+
"which",
|
|
134
|
+
"who",
|
|
135
|
+
"when",
|
|
136
|
+
"where",
|
|
137
|
+
"how"
|
|
138
|
+
]);
|
|
139
|
+
var NEGATION_TERMS = ["not", "never", "no", "without", "cannot", "can't", "didn't"];
|
|
140
|
+
var normalizeUrl = (url) => {
|
|
141
|
+
try {
|
|
142
|
+
const parsed = new URL(url);
|
|
143
|
+
parsed.hash = "";
|
|
144
|
+
["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"].forEach((key) => {
|
|
145
|
+
parsed.searchParams.delete(key);
|
|
146
|
+
});
|
|
147
|
+
return parsed.toString();
|
|
148
|
+
} catch {
|
|
149
|
+
return url.trim();
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
var domainOf = (url) => {
|
|
153
|
+
try {
|
|
154
|
+
return new URL(url).hostname.replace(/^www\./, "").toLowerCase();
|
|
155
|
+
} catch {
|
|
156
|
+
return url.trim().toLowerCase();
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
var toTokens = (text) => text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).map((token) => token.trim()).filter((token) => token.length > 2 && !STOPWORDS.has(token));
|
|
160
|
+
var lexicalScore = (query, text) => {
|
|
161
|
+
const queryTokens = toTokens(query);
|
|
162
|
+
if (!queryTokens.length) return 0;
|
|
163
|
+
const textTokens = new Set(toTokens(text));
|
|
164
|
+
let hits = 0;
|
|
165
|
+
for (const token of queryTokens) {
|
|
166
|
+
if (textTokens.has(token)) hits += 1;
|
|
167
|
+
}
|
|
168
|
+
return hits / queryTokens.length;
|
|
169
|
+
};
|
|
170
|
+
var jaccardSimilarity = (a, b) => {
|
|
171
|
+
const aTokens = new Set(toTokens(a));
|
|
172
|
+
const bTokens = new Set(toTokens(b));
|
|
173
|
+
if (!aTokens.size || !bTokens.size) return 0;
|
|
174
|
+
let intersection = 0;
|
|
175
|
+
for (const token of aTokens) {
|
|
176
|
+
if (bTokens.has(token)) intersection += 1;
|
|
177
|
+
}
|
|
178
|
+
const union = aTokens.size + bTokens.size - intersection;
|
|
179
|
+
return union === 0 ? 0 : intersection / union;
|
|
180
|
+
};
|
|
181
|
+
var sentenceCandidates = (snippet) => snippet.split(/(?<=[.!?])\s+/).map((sentence) => sentence.trim()).filter((sentence) => sentence.length >= 35).slice(0, 2);
|
|
182
|
+
var hasNegation = (text) => {
|
|
183
|
+
const lower = text.toLowerCase();
|
|
184
|
+
return NEGATION_TERMS.some((term) => lower.includes(term));
|
|
185
|
+
};
|
|
186
|
+
var extractNumericTokens = (text) => Array.from(text.matchAll(/\b\d+(?:\.\d+)?%?\b/g)).map((m) => m[0]);
|
|
187
|
+
var authorityScore = (domain) => {
|
|
188
|
+
if (domain.endsWith(".gov") || domain.endsWith(".edu")) return 1;
|
|
189
|
+
if (domain.includes("wikipedia.org")) return 0.9;
|
|
190
|
+
if (domain.includes("stackoverflow.com") || domain.includes("stackexchange.com")) return 0.85;
|
|
191
|
+
if (domain.includes("github.com")) return 0.8;
|
|
192
|
+
if (domain.includes("reddit.com")) return 0.72;
|
|
193
|
+
return 0.56;
|
|
194
|
+
};
|
|
195
|
+
var recencyScore = (text, url) => {
|
|
196
|
+
const year = (/* @__PURE__ */ new Date()).getFullYear();
|
|
197
|
+
const bag = `${text} ${url}`;
|
|
198
|
+
if (bag.includes(String(year))) return 1;
|
|
199
|
+
if (bag.includes(String(year - 1))) return 0.82;
|
|
200
|
+
if (bag.includes(String(year - 2))) return 0.65;
|
|
201
|
+
return 0.48;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// src/core/engine.ts
|
|
205
|
+
var DEPTH_CONFIG = {
|
|
206
|
+
standard: {
|
|
207
|
+
initialQueries: 5,
|
|
208
|
+
refinementQueries: 2,
|
|
209
|
+
sourceLimit: 12,
|
|
210
|
+
maxPerDomain: 3,
|
|
211
|
+
minDomainDiversity: 3
|
|
212
|
+
},
|
|
213
|
+
deep: {
|
|
214
|
+
initialQueries: 8,
|
|
215
|
+
refinementQueries: 4,
|
|
216
|
+
sourceLimit: 20,
|
|
217
|
+
maxPerDomain: 4,
|
|
218
|
+
minDomainDiversity: 4
|
|
219
|
+
},
|
|
220
|
+
extreme: {
|
|
221
|
+
initialQueries: 12,
|
|
222
|
+
refinementQueries: 6,
|
|
223
|
+
sourceLimit: 30,
|
|
224
|
+
maxPerDomain: 5,
|
|
225
|
+
minDomainDiversity: 5
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
var confidenceFromSupport = (citations, domains) => {
|
|
229
|
+
if (citations >= 3 && domains >= 2) return "high";
|
|
230
|
+
if (citations >= 2) return "medium";
|
|
231
|
+
return "low";
|
|
232
|
+
};
|
|
233
|
+
var OpenDeepResearchEngine = class {
|
|
234
|
+
constructor(config) {
|
|
235
|
+
this.config = config;
|
|
236
|
+
}
|
|
237
|
+
resultCache = /* @__PURE__ */ new Map();
|
|
238
|
+
emit(onProgress, stage, message, completed, total) {
|
|
239
|
+
onProgress?.({ stage, message, completed, total });
|
|
240
|
+
}
|
|
241
|
+
buildPlan(query, depth, constraints) {
|
|
242
|
+
const cfg = DEPTH_CONFIG[depth];
|
|
243
|
+
const seed = [
|
|
244
|
+
{ query, intent: "Core answer" },
|
|
245
|
+
{ query: `${query} latest updates facts`, intent: "Fresh context" },
|
|
246
|
+
{ query: `${query} benchmark comparison`, intent: "Comparative evidence" },
|
|
247
|
+
{ query: `${query} data statistics report`, intent: "Quantitative evidence" },
|
|
248
|
+
{ query: `${query} criticism limitations`, intent: "Counter evidence" },
|
|
249
|
+
{ query: `${query} implementation guide`, intent: "Practical implementation" },
|
|
250
|
+
{ query: `${query} official documentation`, intent: "Primary source" },
|
|
251
|
+
{ query: `${query} common mistakes pitfalls`, intent: "Failure patterns" },
|
|
252
|
+
{ query: `${query} timeline history`, intent: "Historical context" },
|
|
253
|
+
{ query: `${query} expert analysis`, intent: "Expert opinion" }
|
|
254
|
+
];
|
|
255
|
+
for (const domain of constraints.requiredDomains.slice(0, 4)) {
|
|
256
|
+
seed.unshift({
|
|
257
|
+
query: `${query} site:${domain}`,
|
|
258
|
+
intent: `Required domain evidence (${domain})`,
|
|
259
|
+
constrainedToDomain: domain
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
for (const domain of constraints.preferredDomains.slice(0, 3)) {
|
|
263
|
+
seed.unshift({
|
|
264
|
+
query: `${query} discussion site:${domain}`,
|
|
265
|
+
intent: `Preferred domain signal (${domain})`,
|
|
266
|
+
constrainedToDomain: domain
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
const unique2 = /* @__PURE__ */ new Map();
|
|
270
|
+
for (const row of seed) {
|
|
271
|
+
const key = row.query.toLowerCase().trim();
|
|
272
|
+
if (!unique2.has(key)) unique2.set(key, row);
|
|
273
|
+
}
|
|
274
|
+
return Array.from(unique2.values()).slice(0, cfg.initialQueries);
|
|
275
|
+
}
|
|
276
|
+
buildRefinementQueries(query, rankedSources, constraints, depth) {
|
|
277
|
+
const cfg = DEPTH_CONFIG[depth];
|
|
278
|
+
const diversity = new Set(rankedSources.map((source) => source.domain)).size;
|
|
279
|
+
const lowDiversity = diversity < cfg.minDomainDiversity;
|
|
280
|
+
const candidates = [
|
|
281
|
+
{ query: `${query} contradictory findings`, intent: "Conflict checks" },
|
|
282
|
+
{ query: `${query} independent validation`, intent: "Independent corroboration" },
|
|
283
|
+
{ query: `${query} known caveats`, intent: "Known caveats" },
|
|
284
|
+
{ query: `${query} recent updates this year`, intent: "Recency refresh" },
|
|
285
|
+
{ query: `${query} real world case study`, intent: "Case studies" }
|
|
286
|
+
];
|
|
287
|
+
if (lowDiversity) {
|
|
288
|
+
candidates.push({ query: `${query} alternative sources analysis`, intent: "Diversity expansion" });
|
|
289
|
+
}
|
|
290
|
+
for (const domain of constraints.requiredDomains.slice(0, 3)) {
|
|
291
|
+
candidates.push({
|
|
292
|
+
query: `${query} site:${domain}`,
|
|
293
|
+
intent: `Required domain refinement (${domain})`,
|
|
294
|
+
constrainedToDomain: domain
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
const unique2 = /* @__PURE__ */ new Map();
|
|
298
|
+
for (const row of candidates) {
|
|
299
|
+
const key = row.query.toLowerCase().trim();
|
|
300
|
+
if (!unique2.has(key)) unique2.set(key, row);
|
|
301
|
+
}
|
|
302
|
+
return Array.from(unique2.values()).slice(0, cfg.refinementQueries);
|
|
303
|
+
}
|
|
304
|
+
async retrieveWithCache(query, constraints) {
|
|
305
|
+
const cacheKey = [
|
|
306
|
+
query.toLowerCase().trim(),
|
|
307
|
+
constraints.requiredDomains.join(","),
|
|
308
|
+
constraints.preferredDomains.join(","),
|
|
309
|
+
String(constraints.enforceRequiredDomains)
|
|
310
|
+
].join("|");
|
|
311
|
+
const cached = this.resultCache.get(cacheKey);
|
|
312
|
+
if (cached) return cached;
|
|
313
|
+
const results = await this.config.searchAdapter.search(query, {
|
|
314
|
+
sites: constraints.requiredDomains,
|
|
315
|
+
preferredSites: constraints.preferredDomains,
|
|
316
|
+
enforceSites: constraints.enforceRequiredDomains,
|
|
317
|
+
forumHints: constraints.forumHints
|
|
318
|
+
});
|
|
319
|
+
const filtered = results.filter((result) => result.url && result.url !== "#" && result.title !== "Search Error");
|
|
320
|
+
this.resultCache.set(cacheKey, filtered);
|
|
321
|
+
return filtered;
|
|
322
|
+
}
|
|
323
|
+
rank(question, rows, depth, constraints) {
|
|
324
|
+
const cfg = DEPTH_CONFIG[depth];
|
|
325
|
+
const deduped = /* @__PURE__ */ new Set();
|
|
326
|
+
const scored = [];
|
|
327
|
+
for (const row of rows) {
|
|
328
|
+
const url = normalizeUrl(row.result.url);
|
|
329
|
+
if (!url || deduped.has(url)) continue;
|
|
330
|
+
deduped.add(url);
|
|
331
|
+
const domain = domainOf(url);
|
|
332
|
+
if (constraints.enforceRequiredDomains && constraints.requiredDomains.length > 0) {
|
|
333
|
+
if (!domainMatches(domain, constraints.requiredDomains)) continue;
|
|
334
|
+
}
|
|
335
|
+
const preferredBoost = constraints.preferredDomains.length > 0 && domainMatches(domain, constraints.preferredDomains) ? 1 : 0;
|
|
336
|
+
const constrainedBoost = row.constrainedToDomain && domainMatches(domain, [row.constrainedToDomain]) ? 1 : 0;
|
|
337
|
+
const score = lexicalScore(question, `${row.result.title} ${row.result.snippet}`) * 0.45 + lexicalScore(row.plannedQuery, `${row.result.title} ${row.result.snippet}`) * 0.15 + authorityScore(domain) * 0.15 + recencyScore(`${row.result.title} ${row.result.snippet}`, url) * 0.1 + preferredBoost * 0.1 + constrainedBoost * 0.05;
|
|
338
|
+
scored.push({
|
|
339
|
+
id: 0,
|
|
340
|
+
title: row.result.title,
|
|
341
|
+
snippet: row.result.snippet,
|
|
342
|
+
url,
|
|
343
|
+
domain,
|
|
344
|
+
score,
|
|
345
|
+
retrievedFrom: row.plannedQuery
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
scored.sort((a, b) => b.score - a.score);
|
|
349
|
+
const diversified = [];
|
|
350
|
+
const domainCounts = /* @__PURE__ */ new Map();
|
|
351
|
+
for (const source of scored) {
|
|
352
|
+
const seenFromDomain = domainCounts.get(source.domain) || 0;
|
|
353
|
+
if (seenFromDomain >= cfg.maxPerDomain) continue;
|
|
354
|
+
diversified.push(source);
|
|
355
|
+
domainCounts.set(source.domain, seenFromDomain + 1);
|
|
356
|
+
if (diversified.length >= cfg.sourceLimit) break;
|
|
357
|
+
}
|
|
358
|
+
return diversified.map((entry, index) => ({ ...entry, id: index + 1 }));
|
|
359
|
+
}
|
|
360
|
+
verify(rankedSources) {
|
|
361
|
+
const clusters = [];
|
|
362
|
+
for (const source of rankedSources) {
|
|
363
|
+
const candidates = sentenceCandidates(source.snippet || source.title);
|
|
364
|
+
if (!candidates.length && source.title) candidates.push(source.title);
|
|
365
|
+
for (const sentence of candidates) {
|
|
366
|
+
let attached = false;
|
|
367
|
+
for (const cluster of clusters) {
|
|
368
|
+
if (jaccardSimilarity(cluster.claim, sentence) >= 0.66) {
|
|
369
|
+
cluster.citationIds.add(source.id);
|
|
370
|
+
cluster.domains.add(source.domain);
|
|
371
|
+
attached = true;
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (!attached) {
|
|
376
|
+
clusters.push({
|
|
377
|
+
claim: sentence,
|
|
378
|
+
citationIds: /* @__PURE__ */ new Set([source.id]),
|
|
379
|
+
domains: /* @__PURE__ */ new Set([source.domain])
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const claims = clusters.map((cluster) => {
|
|
385
|
+
const citationIds = Array.from(cluster.citationIds).sort((a, b) => a - b);
|
|
386
|
+
const domains = Array.from(cluster.domains);
|
|
387
|
+
return {
|
|
388
|
+
claim: cluster.claim,
|
|
389
|
+
citationIds,
|
|
390
|
+
domains,
|
|
391
|
+
confidence: confidenceFromSupport(citationIds.length, domains.length)
|
|
392
|
+
};
|
|
393
|
+
}).sort((a, b) => {
|
|
394
|
+
const scoreA = a.citationIds.length * 10 + (a.confidence === "high" ? 3 : a.confidence === "medium" ? 2 : 1);
|
|
395
|
+
const scoreB = b.citationIds.length * 10 + (b.confidence === "high" ? 3 : b.confidence === "medium" ? 2 : 1);
|
|
396
|
+
return scoreB - scoreA;
|
|
397
|
+
}).slice(0, 20);
|
|
398
|
+
const contradictions = [];
|
|
399
|
+
for (let i = 0; i < claims.length; i += 1) {
|
|
400
|
+
for (let j = i + 1; j < claims.length; j += 1) {
|
|
401
|
+
const left = claims[i];
|
|
402
|
+
const right = claims[j];
|
|
403
|
+
if (jaccardSimilarity(left.claim, right.claim) < 0.78) continue;
|
|
404
|
+
const negationConflict = hasNegation(left.claim) !== hasNegation(right.claim);
|
|
405
|
+
const leftNumbers = extractNumericTokens(left.claim);
|
|
406
|
+
const rightNumbers = extractNumericTokens(right.claim);
|
|
407
|
+
const numericConflict = leftNumbers.length > 0 && rightNumbers.length > 0 && leftNumbers.join("|") !== rightNumbers.join("|");
|
|
408
|
+
if (negationConflict || numericConflict) {
|
|
409
|
+
contradictions.push(
|
|
410
|
+
`Potential conflict between [${left.citationIds[0]}] and [${right.citationIds[0]}] on similar claim wording.`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const unresolvedQuestions = claims.filter((claim) => claim.confidence === "low").slice(0, 4).map((claim) => `Need stronger corroboration for: ${claim.claim}`);
|
|
416
|
+
return {
|
|
417
|
+
claims,
|
|
418
|
+
contradictions: Array.from(new Set(contradictions)).slice(0, 6),
|
|
419
|
+
unresolvedQuestions
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
buildPromptContext(params) {
|
|
423
|
+
const constraintsLine = params.constraints.requiredDomains.length ? `Required domains: ${params.constraints.requiredDomains.join(", ")} (strict=${String(
|
|
424
|
+
params.constraints.enforceRequiredDomains
|
|
425
|
+
)})` : params.constraints.preferredDomains.length ? `Preferred domains: ${params.constraints.preferredDomains.join(", ")}` : "No strict domain constraints requested.";
|
|
426
|
+
const sourcePayload = params.rankedSources.map((source) => ({
|
|
427
|
+
id: source.id,
|
|
428
|
+
title: source.title,
|
|
429
|
+
snippet: source.snippet,
|
|
430
|
+
url: source.url,
|
|
431
|
+
domain: source.domain,
|
|
432
|
+
score: Number(source.score.toFixed(4)),
|
|
433
|
+
retrievedFrom: source.retrievedFrom
|
|
434
|
+
}));
|
|
435
|
+
const claimPayload = params.claims.map((claim) => ({
|
|
436
|
+
claim: claim.claim,
|
|
437
|
+
confidence: claim.confidence,
|
|
438
|
+
citations: claim.citationIds,
|
|
439
|
+
domains: claim.domains
|
|
440
|
+
}));
|
|
441
|
+
const contextSection = (params.contextBlocks || []).filter(Boolean);
|
|
442
|
+
return [
|
|
443
|
+
"UPAI OPEN DEEPRESEARCH DOSSIER",
|
|
444
|
+
`Question: ${params.query}`,
|
|
445
|
+
`Depth: ${params.depth}`,
|
|
446
|
+
`Retrieval rounds: ${params.retrievalRounds}`,
|
|
447
|
+
constraintsLine,
|
|
448
|
+
"",
|
|
449
|
+
...contextSection.length ? ["Additional context:", ...contextSection, ""] : [],
|
|
450
|
+
"Research plan:",
|
|
451
|
+
...params.plan.map((entry, index) => `${index + 1}. ${entry.intent}: ${entry.query}`),
|
|
452
|
+
"",
|
|
453
|
+
"Claim graph:",
|
|
454
|
+
"```json",
|
|
455
|
+
JSON.stringify(claimPayload, null, 2),
|
|
456
|
+
"```",
|
|
457
|
+
"",
|
|
458
|
+
"Evidence table:",
|
|
459
|
+
"```json",
|
|
460
|
+
JSON.stringify(sourcePayload, null, 2),
|
|
461
|
+
"```",
|
|
462
|
+
"",
|
|
463
|
+
"Contradictions:",
|
|
464
|
+
params.contradictions.length ? params.contradictions.join("\n") : "None detected.",
|
|
465
|
+
"",
|
|
466
|
+
"Unresolved questions:",
|
|
467
|
+
params.unresolvedQuestions.length ? params.unresolvedQuestions.join("\n") : "None",
|
|
468
|
+
"",
|
|
469
|
+
"Answer requirements:",
|
|
470
|
+
"1) Start with a direct answer in 1-2 sentences.",
|
|
471
|
+
"2) Use only evidence supported by citations from this dossier.",
|
|
472
|
+
"3) Cite evidence as [1], [2], ... with bracket IDs.",
|
|
473
|
+
"4) Add a short 'What is uncertain' section.",
|
|
474
|
+
"5) If contradictions exist, explicitly mention them.",
|
|
475
|
+
"6) Respect required/preferred domain constraints in framing.",
|
|
476
|
+
"",
|
|
477
|
+
buildTruthContractInstruction("strict")
|
|
478
|
+
].join("\n");
|
|
479
|
+
}
|
|
480
|
+
async synthesize(promptContext, providerHint) {
|
|
481
|
+
const order = [];
|
|
482
|
+
if (providerHint) order.push(providerHint);
|
|
483
|
+
if (!order.includes(this.config.defaultProvider)) order.push(this.config.defaultProvider);
|
|
484
|
+
for (const key of ["openrouter", "nim", "fallback"]) {
|
|
485
|
+
if (!order.includes(key)) order.push(key);
|
|
486
|
+
}
|
|
487
|
+
const errors = [];
|
|
488
|
+
for (const provider of order) {
|
|
489
|
+
const adapter = this.config.llmAdapters[provider];
|
|
490
|
+
if (!adapter) continue;
|
|
491
|
+
try {
|
|
492
|
+
const answer = await adapter.complete(`${promptContext}
|
|
493
|
+
|
|
494
|
+
Now produce the final answer.`);
|
|
495
|
+
if (answer.trim()) {
|
|
496
|
+
return { provider, answer };
|
|
497
|
+
}
|
|
498
|
+
} catch (error) {
|
|
499
|
+
errors.push(`${provider}: ${error instanceof Error ? error.message : String(error)}`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
throw new Error(`All synthesis providers failed. ${errors.join(" | ")}`);
|
|
503
|
+
}
|
|
504
|
+
async run(query, options) {
|
|
505
|
+
const depth = options?.depth || "deep";
|
|
506
|
+
const onProgress = options?.onProgress;
|
|
507
|
+
const cfg = DEPTH_CONFIG[depth];
|
|
508
|
+
const inferred = extractSearchConstraints(query);
|
|
509
|
+
const constraints = mergeConstraints(inferred, options?.constraints);
|
|
510
|
+
this.emit(onProgress, "planning", "Planning multi-angle research queries...", 1, 6);
|
|
511
|
+
const plan = this.buildPlan(query, depth, constraints);
|
|
512
|
+
const gathered = [];
|
|
513
|
+
this.emit(onProgress, "retrieving", "Running retrieval round 1...", 2, 6);
|
|
514
|
+
for (let index = 0; index < plan.length; index += 1) {
|
|
515
|
+
const step = plan[index];
|
|
516
|
+
this.emit(onProgress, "retrieving", `Query ${index + 1}/${plan.length}: ${step.intent}`, 2, 6);
|
|
517
|
+
const results = await this.retrieveWithCache(step.query, constraints);
|
|
518
|
+
for (const result of results) {
|
|
519
|
+
gathered.push({ plannedQuery: step.query, constrainedToDomain: step.constrainedToDomain, result });
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
let retrievalRounds = 1;
|
|
523
|
+
let rankedSources = this.rank(query, gathered, depth, constraints);
|
|
524
|
+
const diversity = new Set(rankedSources.map((source) => source.domain)).size;
|
|
525
|
+
const needsRefinement = depth !== "standard" && (rankedSources.length < Math.floor(cfg.sourceLimit * 0.7) || diversity < cfg.minDomainDiversity);
|
|
526
|
+
if (needsRefinement) {
|
|
527
|
+
retrievalRounds += 1;
|
|
528
|
+
const refinementPlan = this.buildRefinementQueries(query, rankedSources, constraints, depth);
|
|
529
|
+
this.emit(onProgress, "retrieving", "Running adaptive refinement queries...", 2, 6);
|
|
530
|
+
for (let index = 0; index < refinementPlan.length; index += 1) {
|
|
531
|
+
const step = refinementPlan[index];
|
|
532
|
+
this.emit(onProgress, "retrieving", `Refinement ${index + 1}/${refinementPlan.length}: ${step.intent}`, 2, 6);
|
|
533
|
+
const results = await this.retrieveWithCache(step.query, constraints);
|
|
534
|
+
for (const result of results) {
|
|
535
|
+
gathered.push({ plannedQuery: step.query, constrainedToDomain: step.constrainedToDomain, result });
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
rankedSources = this.rank(query, gathered, depth, constraints);
|
|
539
|
+
}
|
|
540
|
+
this.emit(onProgress, "ranking", "Ranking and diversifying evidence...", 3, 6);
|
|
541
|
+
this.emit(onProgress, "verifying", "Building claim graph and contradiction checks...", 4, 6);
|
|
542
|
+
const { claims, contradictions, unresolvedQuestions } = this.verify(rankedSources);
|
|
543
|
+
this.emit(onProgress, "synthesizing", "Synthesizing final answer with citations...", 5, 6);
|
|
544
|
+
const promptContext = this.buildPromptContext({
|
|
545
|
+
query,
|
|
546
|
+
depth,
|
|
547
|
+
plan,
|
|
548
|
+
retrievalRounds,
|
|
549
|
+
constraints,
|
|
550
|
+
claims,
|
|
551
|
+
contradictions,
|
|
552
|
+
unresolvedQuestions,
|
|
553
|
+
rankedSources,
|
|
554
|
+
contextBlocks: options?.contextBlocks
|
|
555
|
+
});
|
|
556
|
+
const synthesis = await this.synthesize(promptContext, options?.providerHint);
|
|
557
|
+
let finalAnswer = synthesis.answer;
|
|
558
|
+
this.emit(onProgress, "critiquing", "Running citation/uncertainty critique pass...", 6, 6);
|
|
559
|
+
const critique = evaluateTruthContract(finalAnswer, {
|
|
560
|
+
requiresCitations: rankedSources.length > 0,
|
|
561
|
+
strictness: "strict"
|
|
562
|
+
});
|
|
563
|
+
if (!critique.passed) {
|
|
564
|
+
const repairPrompt = [
|
|
565
|
+
promptContext,
|
|
566
|
+
"",
|
|
567
|
+
"Your previous answer violated the response contract.",
|
|
568
|
+
`Violations: ${critique.violations.join("; ")}`,
|
|
569
|
+
"Rewrite the answer to satisfy all requirements.",
|
|
570
|
+
"",
|
|
571
|
+
"Previous answer:",
|
|
572
|
+
finalAnswer
|
|
573
|
+
].join("\n");
|
|
574
|
+
const adapter = this.config.llmAdapters[synthesis.provider];
|
|
575
|
+
if (adapter) {
|
|
576
|
+
try {
|
|
577
|
+
const repaired = await adapter.complete(repairPrompt);
|
|
578
|
+
if (repaired.trim()) finalAnswer = repaired;
|
|
579
|
+
} catch {
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
const sourcesForMessage = rankedSources.map((source) => ({
|
|
584
|
+
title: source.title,
|
|
585
|
+
snippet: source.snippet,
|
|
586
|
+
url: source.url
|
|
587
|
+
}));
|
|
588
|
+
return {
|
|
589
|
+
query,
|
|
590
|
+
depth,
|
|
591
|
+
plan,
|
|
592
|
+
retrievalRounds,
|
|
593
|
+
rankedSources,
|
|
594
|
+
claims,
|
|
595
|
+
contradictions,
|
|
596
|
+
unresolvedQuestions,
|
|
597
|
+
promptContext,
|
|
598
|
+
finalAnswer,
|
|
599
|
+
synthesis: {
|
|
600
|
+
provider: synthesis.provider,
|
|
601
|
+
critiqueViolations: critique.violations
|
|
602
|
+
},
|
|
603
|
+
constraints,
|
|
604
|
+
sourcesForMessage
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
// src/adapters/llm/openrouter.ts
|
|
610
|
+
var OpenRouterAdapter = class {
|
|
611
|
+
constructor(config) {
|
|
612
|
+
this.config = config;
|
|
613
|
+
this.endpoint = config.endpoint || "https://openrouter.ai/api/v1/chat/completions";
|
|
614
|
+
this.timeoutMs = config.timeoutMs || 6e4;
|
|
615
|
+
this.siteUrl = config.siteUrl || "https://github.com/blackdrome/upai-open-deepresearch";
|
|
616
|
+
this.siteName = config.siteName || "UPAI Open DeepResearch";
|
|
617
|
+
}
|
|
618
|
+
provider = "openrouter";
|
|
619
|
+
endpoint;
|
|
620
|
+
timeoutMs;
|
|
621
|
+
siteUrl;
|
|
622
|
+
siteName;
|
|
623
|
+
async complete(prompt) {
|
|
624
|
+
if (!this.config.apiKey) {
|
|
625
|
+
throw new Error("OpenRouter API key is required.");
|
|
626
|
+
}
|
|
627
|
+
const controller = new AbortController();
|
|
628
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
629
|
+
try {
|
|
630
|
+
const response = await fetch(this.endpoint, {
|
|
631
|
+
method: "POST",
|
|
632
|
+
headers: {
|
|
633
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
634
|
+
"Content-Type": "application/json",
|
|
635
|
+
"HTTP-Referer": this.siteUrl,
|
|
636
|
+
"X-Title": this.siteName
|
|
637
|
+
},
|
|
638
|
+
body: JSON.stringify({
|
|
639
|
+
model: this.config.model,
|
|
640
|
+
messages: [
|
|
641
|
+
{
|
|
642
|
+
role: "system",
|
|
643
|
+
content: "You are a grounded research synthesis model. Keep claims source-bound and concise."
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
role: "user",
|
|
647
|
+
content: prompt
|
|
648
|
+
}
|
|
649
|
+
],
|
|
650
|
+
stream: false,
|
|
651
|
+
temperature: 0.2
|
|
652
|
+
}),
|
|
653
|
+
signal: controller.signal
|
|
654
|
+
});
|
|
655
|
+
const raw = await response.text();
|
|
656
|
+
let payload = null;
|
|
657
|
+
try {
|
|
658
|
+
payload = JSON.parse(raw);
|
|
659
|
+
} catch {
|
|
660
|
+
payload = null;
|
|
661
|
+
}
|
|
662
|
+
if (!response.ok) {
|
|
663
|
+
throw new Error(payload?.error?.message || raw || `OpenRouter request failed (${response.status})`);
|
|
664
|
+
}
|
|
665
|
+
const text = payload?.choices?.[0]?.message?.content?.trim();
|
|
666
|
+
if (!text) {
|
|
667
|
+
throw new Error("OpenRouter returned an empty completion.");
|
|
668
|
+
}
|
|
669
|
+
return text;
|
|
670
|
+
} finally {
|
|
671
|
+
clearTimeout(timeout);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
// src/adapters/llm/nim.ts
|
|
677
|
+
var normalizeNimError = (status, raw, parsed) => {
|
|
678
|
+
const text = parsed?.error?.message || parsed?.message || parsed?.detail || raw || `NIM request failed (${status})`;
|
|
679
|
+
const lowered = text.toLowerCase();
|
|
680
|
+
if (lowered.includes("not found for account") || /function\s+'[^']+'\s*:\s*not found/i.test(text)) {
|
|
681
|
+
return "Selected NVIDIA NIM model is unavailable for this account.";
|
|
682
|
+
}
|
|
683
|
+
if (status === 401 || lowered.includes("authentication") || lowered.includes("invalid api key")) {
|
|
684
|
+
return "NVIDIA NIM authentication failed. Verify your API key.";
|
|
685
|
+
}
|
|
686
|
+
if (status === 429 || lowered.includes("rate limit")) {
|
|
687
|
+
return "NVIDIA NIM rate limit reached. Retry later or switch model.";
|
|
688
|
+
}
|
|
689
|
+
if (status === 408 || lowered.includes("timed out") || lowered.includes("timeout")) {
|
|
690
|
+
return "NVIDIA NIM timed out. Try a faster model or reduce response size.";
|
|
691
|
+
}
|
|
692
|
+
return text;
|
|
693
|
+
};
|
|
694
|
+
var NimAdapter = class {
|
|
695
|
+
constructor(config) {
|
|
696
|
+
this.config = config;
|
|
697
|
+
this.endpoint = config.endpoint || "https://integrate.api.nvidia.com/v1/chat/completions";
|
|
698
|
+
this.timeoutMs = config.timeoutMs || 7e4;
|
|
699
|
+
this.maxTokens = config.maxTokens || 1100;
|
|
700
|
+
this.temperature = config.temperature ?? 0.2;
|
|
701
|
+
}
|
|
702
|
+
provider = "nim";
|
|
703
|
+
endpoint;
|
|
704
|
+
timeoutMs;
|
|
705
|
+
maxTokens;
|
|
706
|
+
temperature;
|
|
707
|
+
async complete(prompt) {
|
|
708
|
+
if (!this.config.apiKey) {
|
|
709
|
+
throw new Error("NVIDIA NIM API key is required.");
|
|
710
|
+
}
|
|
711
|
+
const controller = new AbortController();
|
|
712
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
713
|
+
try {
|
|
714
|
+
const response = await fetch(this.endpoint, {
|
|
715
|
+
method: "POST",
|
|
716
|
+
headers: {
|
|
717
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
718
|
+
"Content-Type": "application/json"
|
|
719
|
+
},
|
|
720
|
+
body: JSON.stringify({
|
|
721
|
+
model: this.config.model,
|
|
722
|
+
messages: [{ role: "user", content: prompt }],
|
|
723
|
+
max_tokens: this.maxTokens,
|
|
724
|
+
temperature: this.temperature,
|
|
725
|
+
stream: false
|
|
726
|
+
}),
|
|
727
|
+
signal: controller.signal
|
|
728
|
+
});
|
|
729
|
+
const raw = await response.text();
|
|
730
|
+
let payload = null;
|
|
731
|
+
try {
|
|
732
|
+
payload = JSON.parse(raw);
|
|
733
|
+
} catch {
|
|
734
|
+
payload = null;
|
|
735
|
+
}
|
|
736
|
+
if (!response.ok) {
|
|
737
|
+
throw new Error(normalizeNimError(response.status, raw, payload));
|
|
738
|
+
}
|
|
739
|
+
const content = payload?.choices?.[0]?.message?.content;
|
|
740
|
+
const text = typeof content === "string" ? content : Array.isArray(content) ? content.map((entry) => entry.text || "").join("\n").trim() : "";
|
|
741
|
+
if (!text) {
|
|
742
|
+
throw new Error("NVIDIA NIM returned an empty completion.");
|
|
743
|
+
}
|
|
744
|
+
return text;
|
|
745
|
+
} catch (error) {
|
|
746
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
747
|
+
throw new Error("NVIDIA NIM request timed out.");
|
|
748
|
+
}
|
|
749
|
+
throw error;
|
|
750
|
+
} finally {
|
|
751
|
+
clearTimeout(timeout);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
// src/adapters/llm/function-adapter.ts
|
|
757
|
+
var FunctionLlmAdapter = class {
|
|
758
|
+
constructor(config) {
|
|
759
|
+
this.config = config;
|
|
760
|
+
this.provider = config.provider;
|
|
761
|
+
}
|
|
762
|
+
provider;
|
|
763
|
+
complete(prompt) {
|
|
764
|
+
return this.config.complete(prompt);
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
// src/adapters/search/http-json.ts
|
|
769
|
+
var sanitizeResults = (input) => input.filter((item) => item.url && item.url !== "#" && item.title).map((item) => ({
|
|
770
|
+
title: item.title,
|
|
771
|
+
snippet: item.snippet || "",
|
|
772
|
+
url: item.url
|
|
773
|
+
}));
|
|
774
|
+
var HttpJsonSearchAdapter = class {
|
|
775
|
+
constructor(config) {
|
|
776
|
+
this.config = config;
|
|
777
|
+
this.method = config.method || "POST";
|
|
778
|
+
this.timeoutMs = config.timeoutMs || 2e4;
|
|
779
|
+
this.apiKeyHeader = config.apiKeyHeader || "Authorization";
|
|
780
|
+
}
|
|
781
|
+
method;
|
|
782
|
+
timeoutMs;
|
|
783
|
+
apiKeyHeader;
|
|
784
|
+
async search(query, options) {
|
|
785
|
+
const controller = new AbortController();
|
|
786
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
787
|
+
try {
|
|
788
|
+
const headers = {
|
|
789
|
+
"Content-Type": "application/json",
|
|
790
|
+
...this.config.extraHeaders || {}
|
|
791
|
+
};
|
|
792
|
+
if (this.config.apiKey) {
|
|
793
|
+
headers[this.apiKeyHeader] = this.apiKeyHeader.toLowerCase() === "authorization" ? `Bearer ${this.config.apiKey}` : this.config.apiKey;
|
|
794
|
+
}
|
|
795
|
+
const payload = {
|
|
796
|
+
query,
|
|
797
|
+
...this.config.staticPayload,
|
|
798
|
+
sites: options.sites || [],
|
|
799
|
+
preferredSites: options.preferredSites || [],
|
|
800
|
+
enforceSites: Boolean(options.enforceSites),
|
|
801
|
+
forumHints: options.forumHints || []
|
|
802
|
+
};
|
|
803
|
+
let response;
|
|
804
|
+
if (this.method === "GET") {
|
|
805
|
+
const url = new URL(this.config.endpoint);
|
|
806
|
+
url.searchParams.set("query", query);
|
|
807
|
+
response = await fetch(url.toString(), {
|
|
808
|
+
method: "GET",
|
|
809
|
+
headers,
|
|
810
|
+
signal: controller.signal
|
|
811
|
+
});
|
|
812
|
+
} else {
|
|
813
|
+
response = await fetch(this.config.endpoint, {
|
|
814
|
+
method: "POST",
|
|
815
|
+
headers,
|
|
816
|
+
body: JSON.stringify(payload),
|
|
817
|
+
signal: controller.signal
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
const raw = await response.text();
|
|
821
|
+
let parsed = null;
|
|
822
|
+
try {
|
|
823
|
+
parsed = JSON.parse(raw);
|
|
824
|
+
} catch {
|
|
825
|
+
parsed = null;
|
|
826
|
+
}
|
|
827
|
+
if (!response.ok) {
|
|
828
|
+
throw new Error(parsed?.error || raw || `Search adapter request failed (${response.status})`);
|
|
829
|
+
}
|
|
830
|
+
const byResults = Array.isArray(parsed?.results) ? parsed?.results : [];
|
|
831
|
+
if (byResults.length > 0) {
|
|
832
|
+
return sanitizeResults(byResults);
|
|
833
|
+
}
|
|
834
|
+
const byItems = Array.isArray(parsed?.items) ? parsed.items.map((item) => ({
|
|
835
|
+
title: item.title || "Untitled",
|
|
836
|
+
snippet: item.snippet || "",
|
|
837
|
+
url: item.url || item.link || "#"
|
|
838
|
+
})) : [];
|
|
839
|
+
return sanitizeResults(byItems);
|
|
840
|
+
} catch (error) {
|
|
841
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
842
|
+
throw new Error("Search adapter timed out.");
|
|
843
|
+
}
|
|
844
|
+
throw error;
|
|
845
|
+
} finally {
|
|
846
|
+
clearTimeout(timeout);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
// src/index.ts
|
|
852
|
+
var createOpenDeepResearchEngine = (options) => {
|
|
853
|
+
const llmAdapters = {};
|
|
854
|
+
if (options.openRouter) {
|
|
855
|
+
llmAdapters.openrouter = new OpenRouterAdapter(options.openRouter);
|
|
856
|
+
}
|
|
857
|
+
if (options.nim) {
|
|
858
|
+
llmAdapters.nim = new NimAdapter(options.nim);
|
|
859
|
+
}
|
|
860
|
+
if (options.fallbackComplete) {
|
|
861
|
+
llmAdapters.fallback = new FunctionLlmAdapter({
|
|
862
|
+
provider: "fallback",
|
|
863
|
+
complete: options.fallbackComplete
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
const defaultProvider = options.defaultProvider || "openrouter";
|
|
867
|
+
const config = {
|
|
868
|
+
searchAdapter: options.searchAdapter,
|
|
869
|
+
llmAdapters,
|
|
870
|
+
defaultProvider
|
|
871
|
+
};
|
|
872
|
+
return new OpenDeepResearchEngine(config);
|
|
873
|
+
};
|
|
874
|
+
export {
|
|
875
|
+
FunctionLlmAdapter,
|
|
876
|
+
HttpJsonSearchAdapter,
|
|
877
|
+
NimAdapter,
|
|
878
|
+
OpenDeepResearchEngine,
|
|
879
|
+
OpenRouterAdapter,
|
|
880
|
+
buildTruthContractInstruction,
|
|
881
|
+
createOpenDeepResearchEngine,
|
|
882
|
+
domainMatches,
|
|
883
|
+
evaluateTruthContract,
|
|
884
|
+
extractSearchConstraints,
|
|
885
|
+
extractSiteTokens,
|
|
886
|
+
mergeConstraints,
|
|
887
|
+
normalizeDomain
|
|
888
|
+
};
|
|
889
|
+
//# sourceMappingURL=index.js.map
|