@open-mercato/web-research 0.7.1-develop.7194.1.ab4fc81f82
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/.turbo/turbo-build.log +2 -0
- package/AGENTS.md +67 -0
- package/build.mjs +10 -0
- package/dist/contract/adapter.js +13 -0
- package/dist/contract/adapter.js.map +7 -0
- package/dist/contract/errors.js +28 -0
- package/dist/contract/errors.js.map +7 -0
- package/dist/contract/http.js +7 -0
- package/dist/contract/http.js.map +7 -0
- package/dist/contract/index.js +9 -0
- package/dist/contract/index.js.map +7 -0
- package/dist/contract/outcomes.js +58 -0
- package/dist/contract/outcomes.js.map +7 -0
- package/dist/contract/policy.js +91 -0
- package/dist/contract/policy.js.map +7 -0
- package/dist/contract/results.js +13 -0
- package/dist/contract/results.js.map +7 -0
- package/dist/contract/steps.js +1 -0
- package/dist/contract/steps.js.map +7 -0
- package/dist/contract/version.js +11 -0
- package/dist/contract/version.js.map +7 -0
- package/dist/engine/cache.js +29 -0
- package/dist/engine/cache.js.map +7 -0
- package/dist/engine/deadline.js +54 -0
- package/dist/engine/deadline.js.map +7 -0
- package/dist/engine/engine.js +481 -0
- package/dist/engine/engine.js.map +7 -0
- package/dist/engine/index.js +20 -0
- package/dist/engine/index.js.map +7 -0
- package/dist/engine/runAdapter.js +57 -0
- package/dist/engine/runAdapter.js.map +7 -0
- package/dist/engine/types.js +1 -0
- package/dist/engine/types.js.map +7 -0
- package/dist/extract/classify.js +51 -0
- package/dist/extract/classify.js.map +7 -0
- package/dist/extract/content.js +104 -0
- package/dist/extract/content.js.map +7 -0
- package/dist/extract/entities.js +95 -0
- package/dist/extract/entities.js.map +7 -0
- package/dist/extract/index.js +24 -0
- package/dist/extract/index.js.map +7 -0
- package/dist/extract/text.js +114 -0
- package/dist/extract/text.js.map +7 -0
- package/dist/extract/tokenizer.js +141 -0
- package/dist/extract/tokenizer.js.map +7 -0
- package/dist/fusion/canonicalize.js +88 -0
- package/dist/fusion/canonicalize.js.map +7 -0
- package/dist/fusion/fuse.js +88 -0
- package/dist/fusion/fuse.js.map +7 -0
- package/dist/fusion/index.js +11 -0
- package/dist/fusion/index.js.map +7 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +7 -0
- package/dist/loader/describe.js +92 -0
- package/dist/loader/describe.js.map +7 -0
- package/dist/loader/index.js +31 -0
- package/dist/loader/index.js.map +7 -0
- package/dist/loader/manifest.js +32 -0
- package/dist/loader/manifest.js.map +7 -0
- package/dist/loader/registry.js +98 -0
- package/dist/loader/registry.js.map +7 -0
- package/dist/net/client.js +298 -0
- package/dist/net/client.js.map +7 -0
- package/dist/net/ssrf.js +165 -0
- package/dist/net/ssrf.js.map +7 -0
- package/dist/net/url.js +9 -0
- package/dist/net/url.js.map +7 -0
- package/dist/testing/contract.js +118 -0
- package/dist/testing/contract.js.map +7 -0
- package/dist/testing/fakeAdapter.js +44 -0
- package/dist/testing/fakeAdapter.js.map +7 -0
- package/dist/testing/index.js +15 -0
- package/dist/testing/index.js.map +7 -0
- package/jest.config.cjs +24 -0
- package/package.json +65 -0
- package/src/contract/adapter.ts +100 -0
- package/src/contract/errors.ts +48 -0
- package/src/contract/http.ts +46 -0
- package/src/contract/index.ts +8 -0
- package/src/contract/outcomes.ts +91 -0
- package/src/contract/policy.ts +157 -0
- package/src/contract/results.ts +79 -0
- package/src/contract/steps.ts +45 -0
- package/src/contract/version.ts +25 -0
- package/src/engine/__tests__/engine.test.ts +597 -0
- package/src/engine/cache.ts +46 -0
- package/src/engine/deadline.ts +65 -0
- package/src/engine/engine.ts +633 -0
- package/src/engine/index.ts +22 -0
- package/src/engine/runAdapter.ts +89 -0
- package/src/engine/types.ts +137 -0
- package/src/extract/__tests__/content.test.ts +81 -0
- package/src/extract/__tests__/text.test.ts +61 -0
- package/src/extract/__tests__/tokenizer.test.ts +85 -0
- package/src/extract/classify.ts +81 -0
- package/src/extract/content.ts +136 -0
- package/src/extract/entities.ts +98 -0
- package/src/extract/index.ts +11 -0
- package/src/extract/text.ts +124 -0
- package/src/extract/tokenizer.ts +197 -0
- package/src/fusion/__tests__/fusion.test.ts +198 -0
- package/src/fusion/canonicalize.ts +119 -0
- package/src/fusion/fuse.ts +145 -0
- package/src/fusion/index.ts +8 -0
- package/src/index.ts +15 -0
- package/src/loader/__tests__/describe.test.ts +119 -0
- package/src/loader/__tests__/registry.test.ts +134 -0
- package/src/loader/describe.ts +145 -0
- package/src/loader/index.ts +25 -0
- package/src/loader/manifest.ts +44 -0
- package/src/loader/registry.ts +156 -0
- package/src/net/__tests__/client.test.ts +273 -0
- package/src/net/__tests__/ssrf.test.ts +165 -0
- package/src/net/client.ts +434 -0
- package/src/net/ssrf.ts +235 -0
- package/src/net/url.ts +13 -0
- package/src/testing/contract.ts +172 -0
- package/src/testing/fakeAdapter.ts +69 -0
- package/src/testing/index.ts +9 -0
- package/tsconfig.json +10 -0
- package/watch.mjs +7 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export type SettleMode = 'race' | 'quorum' | 'exhaustive'
|
|
4
|
+
|
|
5
|
+
export type AdapterPolicyEntry = {
|
|
6
|
+
readonly id: string
|
|
7
|
+
readonly enabled: boolean
|
|
8
|
+
readonly order: number
|
|
9
|
+
/** Relative pull in the fusion step; higher trusts this adapter's ranking more. */
|
|
10
|
+
readonly weight: number
|
|
11
|
+
/**
|
|
12
|
+
* Overrides `adapterTimeoutMs` for this adapter alone. Latency is not uniform
|
|
13
|
+
* across kinds: a SERP read finishes in about a second, while a model doing its
|
|
14
|
+
* own multi-step web search takes tens of seconds. One shared budget has to be
|
|
15
|
+
* either too tight for the model or too slack for everything else.
|
|
16
|
+
*/
|
|
17
|
+
readonly timeoutMs?: number
|
|
18
|
+
/**
|
|
19
|
+
* Ceiling on how often this adapter may be called, per tenant, per hour.
|
|
20
|
+
*
|
|
21
|
+
* The engine does not enforce this — it is per-request and has no memory. The
|
|
22
|
+
* host counts calls and simply stops enabling the adapter once the ceiling is
|
|
23
|
+
* reached, which is why the knob lives on the policy rather than in the engine.
|
|
24
|
+
* Undefined means no ceiling.
|
|
25
|
+
*/
|
|
26
|
+
readonly maxCallsPerHour?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ContentPolicy = {
|
|
30
|
+
readonly enabledByDefault: boolean
|
|
31
|
+
readonly maxPages: number
|
|
32
|
+
readonly maxBytesPerPage: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type SearchPolicy = {
|
|
36
|
+
readonly settleMode: SettleMode
|
|
37
|
+
readonly concurrency: number
|
|
38
|
+
readonly minResults: number
|
|
39
|
+
readonly minAdapters: number
|
|
40
|
+
readonly minConfidence: number
|
|
41
|
+
readonly softDeadlineMs: number
|
|
42
|
+
readonly hardDeadlineMs: number
|
|
43
|
+
readonly adapterTimeoutMs: number
|
|
44
|
+
readonly maxPerDomain: number
|
|
45
|
+
readonly cacheTtlMs: number
|
|
46
|
+
/**
|
|
47
|
+
* Adapter run when every wave is exhausted and the fused set is still short,
|
|
48
|
+
* even if it is disabled in the normal order. This is what makes "the engine
|
|
49
|
+
* always returns something" true rather than aspirational.
|
|
50
|
+
*/
|
|
51
|
+
readonly lastResort: string | null
|
|
52
|
+
readonly escalateToBrowser: boolean
|
|
53
|
+
readonly content: ContentPolicy
|
|
54
|
+
readonly adapters: readonly AdapterPolicyEntry[]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Budget for `model-native`, the adapter enabled by default. It runs the model's
|
|
59
|
+
* own multi-step web search, which measures at roughly 30s end to end, so the
|
|
60
|
+
* generic adapter budget would time it out on every single run and the shipped
|
|
61
|
+
* configuration could never return anything.
|
|
62
|
+
*/
|
|
63
|
+
export const MODEL_ADAPTER_TIMEOUT_MS = 45_000
|
|
64
|
+
|
|
65
|
+
export const DEFAULT_POLICY: SearchPolicy = {
|
|
66
|
+
settleMode: 'quorum',
|
|
67
|
+
concurrency: 2,
|
|
68
|
+
minResults: 5,
|
|
69
|
+
minAdapters: 1,
|
|
70
|
+
minConfidence: 0,
|
|
71
|
+
// The soft deadline only binds once results exist, so a fast adapter still
|
|
72
|
+
// settles the run in about a second; this ceiling is what a lone slow adapter
|
|
73
|
+
// is allowed to use, not what a normal search costs.
|
|
74
|
+
softDeadlineMs: 6_000,
|
|
75
|
+
hardDeadlineMs: 60_000,
|
|
76
|
+
adapterTimeoutMs: 8_000,
|
|
77
|
+
maxPerDomain: 3,
|
|
78
|
+
cacheTtlMs: 900_000,
|
|
79
|
+
lastResort: 'model-native',
|
|
80
|
+
escalateToBrowser: true,
|
|
81
|
+
content: { enabledByDefault: false, maxPages: 3, maxBytesPerPage: 128 * 1024 },
|
|
82
|
+
adapters: [],
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const adapterPolicyEntrySchema = z.object({
|
|
86
|
+
id: z.string().min(1),
|
|
87
|
+
enabled: z.boolean().optional(),
|
|
88
|
+
order: z.number().int().min(0).optional(),
|
|
89
|
+
weight: z.number().min(0).max(10).optional(),
|
|
90
|
+
timeoutMs: z.number().int().min(250).max(120_000).optional(),
|
|
91
|
+
maxCallsPerHour: z.number().int().min(1).max(1_000_000).optional(),
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
export const searchPolicySchema = z.object({
|
|
95
|
+
settleMode: z.enum(['race', 'quorum', 'exhaustive']).optional(),
|
|
96
|
+
concurrency: z.number().int().min(1).max(16).optional(),
|
|
97
|
+
minResults: z.number().int().min(1).max(50).optional(),
|
|
98
|
+
minAdapters: z.number().int().min(1).max(16).optional(),
|
|
99
|
+
minConfidence: z.number().min(0).max(1).optional(),
|
|
100
|
+
softDeadlineMs: z.number().int().min(250).max(120_000).optional(),
|
|
101
|
+
hardDeadlineMs: z.number().int().min(500).max(180_000).optional(),
|
|
102
|
+
adapterTimeoutMs: z.number().int().min(250).max(120_000).optional(),
|
|
103
|
+
maxPerDomain: z.number().int().min(1).max(50).optional(),
|
|
104
|
+
cacheTtlMs: z.number().int().min(0).max(86_400_000).optional(),
|
|
105
|
+
lastResort: z.string().min(1).nullable().optional(),
|
|
106
|
+
escalateToBrowser: z.boolean().optional(),
|
|
107
|
+
content: z
|
|
108
|
+
.object({
|
|
109
|
+
enabledByDefault: z.boolean().optional(),
|
|
110
|
+
maxPages: z.number().int().min(0).max(20).optional(),
|
|
111
|
+
maxBytesPerPage: z.number().int().min(1024).max(4 * 1024 * 1024).optional(),
|
|
112
|
+
})
|
|
113
|
+
.optional(),
|
|
114
|
+
adapters: z.array(adapterPolicyEntrySchema).optional(),
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
export type SearchPolicyInput = z.infer<typeof searchPolicySchema>
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Merges a partial policy over the defaults. Deadlines are reconciled rather
|
|
121
|
+
* than trusted: a soft deadline past the hard one would make the scheduler wait
|
|
122
|
+
* for a barrier it can never reach.
|
|
123
|
+
*/
|
|
124
|
+
export function resolvePolicy(input: SearchPolicyInput | null | undefined): SearchPolicy {
|
|
125
|
+
const patch = input ?? {}
|
|
126
|
+
const hardDeadlineMs = patch.hardDeadlineMs ?? DEFAULT_POLICY.hardDeadlineMs
|
|
127
|
+
const softDeadlineMs = Math.min(patch.softDeadlineMs ?? DEFAULT_POLICY.softDeadlineMs, hardDeadlineMs)
|
|
128
|
+
return {
|
|
129
|
+
settleMode: patch.settleMode ?? DEFAULT_POLICY.settleMode,
|
|
130
|
+
concurrency: patch.concurrency ?? DEFAULT_POLICY.concurrency,
|
|
131
|
+
minResults: patch.minResults ?? DEFAULT_POLICY.minResults,
|
|
132
|
+
minAdapters: patch.minAdapters ?? DEFAULT_POLICY.minAdapters,
|
|
133
|
+
minConfidence: patch.minConfidence ?? DEFAULT_POLICY.minConfidence,
|
|
134
|
+
softDeadlineMs,
|
|
135
|
+
hardDeadlineMs,
|
|
136
|
+
adapterTimeoutMs: Math.min(patch.adapterTimeoutMs ?? DEFAULT_POLICY.adapterTimeoutMs, hardDeadlineMs),
|
|
137
|
+
maxPerDomain: patch.maxPerDomain ?? DEFAULT_POLICY.maxPerDomain,
|
|
138
|
+
cacheTtlMs: patch.cacheTtlMs ?? DEFAULT_POLICY.cacheTtlMs,
|
|
139
|
+
lastResort: patch.lastResort === undefined ? DEFAULT_POLICY.lastResort : patch.lastResort,
|
|
140
|
+
escalateToBrowser: patch.escalateToBrowser ?? DEFAULT_POLICY.escalateToBrowser,
|
|
141
|
+
content: {
|
|
142
|
+
enabledByDefault: patch.content?.enabledByDefault ?? DEFAULT_POLICY.content.enabledByDefault,
|
|
143
|
+
maxPages: patch.content?.maxPages ?? DEFAULT_POLICY.content.maxPages,
|
|
144
|
+
maxBytesPerPage: patch.content?.maxBytesPerPage ?? DEFAULT_POLICY.content.maxBytesPerPage,
|
|
145
|
+
},
|
|
146
|
+
adapters: (patch.adapters ?? []).map((entry, index) => ({
|
|
147
|
+
id: entry.id,
|
|
148
|
+
enabled: entry.enabled ?? true,
|
|
149
|
+
order: entry.order ?? index,
|
|
150
|
+
weight: entry.weight ?? 1,
|
|
151
|
+
// Clamped to the hard deadline for the same reason the shared budget is:
|
|
152
|
+
// a per-adapter timeout past it can never be reached.
|
|
153
|
+
...(entry.timeoutMs === undefined ? {} : { timeoutMs: Math.min(entry.timeoutMs, hardDeadlineMs) }),
|
|
154
|
+
...(entry.maxCallsPerHour === undefined ? {} : { maxCallsPerHour: entry.maxCallsPerHour }),
|
|
155
|
+
})),
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export type Freshness = 'day' | 'week' | 'month' | 'year'
|
|
4
|
+
|
|
5
|
+
/** Normalized request handed to adapters. The engine resolves defaults first. */
|
|
6
|
+
export type SearchRequest = {
|
|
7
|
+
readonly query: string
|
|
8
|
+
readonly limit: number
|
|
9
|
+
readonly locale?: string
|
|
10
|
+
readonly freshness?: Freshness
|
|
11
|
+
readonly site?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* What an adapter returns per hit. Everything but `url` is optional because
|
|
16
|
+
* sources differ wildly — an LLM citation list has no snippet, a SERP has no
|
|
17
|
+
* content, a scraping API has both.
|
|
18
|
+
*/
|
|
19
|
+
export type RawResult = {
|
|
20
|
+
readonly url: string
|
|
21
|
+
readonly title?: string | null
|
|
22
|
+
readonly snippet?: string | null
|
|
23
|
+
readonly content?: string | null
|
|
24
|
+
readonly publishedAt?: string | null
|
|
25
|
+
/** Adapter-native relevance, used only to break ties within one adapter. */
|
|
26
|
+
readonly score?: number | null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ResultProvenance = {
|
|
30
|
+
readonly adapterId: string
|
|
31
|
+
readonly rank: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A hit after canonicalization, dedup and cross-adapter fusion. */
|
|
35
|
+
export type SearchResult = {
|
|
36
|
+
readonly url: string
|
|
37
|
+
readonly title: string
|
|
38
|
+
readonly snippet: string | null
|
|
39
|
+
readonly content: string | null
|
|
40
|
+
readonly publishedAt: string | null
|
|
41
|
+
readonly confidence: number
|
|
42
|
+
readonly sources: readonly ResultProvenance[]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type RenderMode = 'auto' | 'never' | 'always'
|
|
46
|
+
|
|
47
|
+
export type FetchRequest = {
|
|
48
|
+
readonly url: string
|
|
49
|
+
readonly maxBytes?: number
|
|
50
|
+
readonly render?: RenderMode
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Why an HTTP read was judged insufficient. Drives browser escalation, and is
|
|
55
|
+
* reported back so the decision is auditable rather than invisible.
|
|
56
|
+
*/
|
|
57
|
+
export type PageVerdict = 'ok' | 'js-shell' | 'blocked' | 'empty'
|
|
58
|
+
|
|
59
|
+
export type FetchedPage = {
|
|
60
|
+
readonly url: string
|
|
61
|
+
readonly title: string | null
|
|
62
|
+
readonly text: string
|
|
63
|
+
readonly contentType: string | null
|
|
64
|
+
readonly status: number
|
|
65
|
+
readonly truncated: boolean
|
|
66
|
+
readonly renderedWith: 'http' | 'browser'
|
|
67
|
+
readonly escalatedBecause?: PageVerdict
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const searchRequestSchema = z.object({
|
|
71
|
+
query: z.string().trim().min(1).max(500),
|
|
72
|
+
limit: z.number().int().min(1).max(50).optional(),
|
|
73
|
+
locale: z.string().min(2).max(16).optional(),
|
|
74
|
+
freshness: z.enum(['day', 'week', 'month', 'year']).optional(),
|
|
75
|
+
site: z.string().min(1).max(253).optional(),
|
|
76
|
+
includeContent: z.boolean().optional(),
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
export type SearchRequestInput = z.infer<typeof searchRequestSchema>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type SearchStepPhase = 'plan' | 'adapter' | 'fetch' | 'fusion' | 'done'
|
|
2
|
+
|
|
3
|
+
export type SearchStepEvent =
|
|
4
|
+
| 'queued'
|
|
5
|
+
| 'started'
|
|
6
|
+
| 'ok'
|
|
7
|
+
| 'empty'
|
|
8
|
+
| 'blocked'
|
|
9
|
+
| 'timeout'
|
|
10
|
+
| 'unavailable'
|
|
11
|
+
| 'error'
|
|
12
|
+
| 'cancelled'
|
|
13
|
+
| 'escalated'
|
|
14
|
+
| 'cached'
|
|
15
|
+
| 'fused'
|
|
16
|
+
| 'completed'
|
|
17
|
+
|
|
18
|
+
export type SearchStepMetrics = {
|
|
19
|
+
readonly latencyMs?: number
|
|
20
|
+
readonly resultCount?: number
|
|
21
|
+
readonly bytes?: number
|
|
22
|
+
readonly httpStatus?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* One narratable unit of engine work. Emitted live and persisted, so `detail`
|
|
27
|
+
* must be human-readable and free of secrets — it reaches an operator's screen.
|
|
28
|
+
*/
|
|
29
|
+
export type SearchStep = {
|
|
30
|
+
readonly seq: number
|
|
31
|
+
readonly at: string
|
|
32
|
+
readonly phase: SearchStepPhase
|
|
33
|
+
readonly event: SearchStepEvent
|
|
34
|
+
readonly adapterId?: string
|
|
35
|
+
readonly detail?: string
|
|
36
|
+
readonly metrics?: SearchStepMetrics
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type StepReporter = (
|
|
40
|
+
event: SearchStepEvent,
|
|
41
|
+
detail?: string,
|
|
42
|
+
metrics?: SearchStepMetrics,
|
|
43
|
+
) => void
|
|
44
|
+
|
|
45
|
+
export type StepSink = (step: SearchStep) => void
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter contract version. Adapter packages declare the version they were built
|
|
3
|
+
* against; the loader refuses to instantiate a mismatched major rather than let a
|
|
4
|
+
* stale adapter fail in an unpredictable place at request time.
|
|
5
|
+
*/
|
|
6
|
+
export const CONTRACT_VERSION = 1
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Oldest adapter contract this engine still accepts.
|
|
10
|
+
*
|
|
11
|
+
* An exact-match check makes every bump a flag day: a purely additive change
|
|
12
|
+
* would disable every third-party adapter at once, with nothing wrong with any
|
|
13
|
+
* of them. A range lets an additive bump raise `CONTRACT_VERSION` while leaving
|
|
14
|
+
* this behind, and a genuinely breaking one raise both together.
|
|
15
|
+
*/
|
|
16
|
+
export const MIN_SUPPORTED_CONTRACT_VERSION = 1
|
|
17
|
+
|
|
18
|
+
export function isSupportedContractVersion(version: unknown): version is number {
|
|
19
|
+
return (
|
|
20
|
+
typeof version === 'number' &&
|
|
21
|
+
Number.isInteger(version) &&
|
|
22
|
+
version >= MIN_SUPPORTED_CONTRACT_VERSION &&
|
|
23
|
+
version <= CONTRACT_VERSION
|
|
24
|
+
)
|
|
25
|
+
}
|