@ggui-ai/negotiator 0.1.0-rc.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/LICENSE +201 -0
- package/README.md +49 -0
- package/dist/contract-hash.d.ts +54 -0
- package/dist/contract-hash.d.ts.map +1 -0
- package/dist/contract-hash.js +96 -0
- package/dist/contract-validators.d.ts +171 -0
- package/dist/contract-validators.d.ts.map +1 -0
- package/dist/contract-validators.js +478 -0
- package/dist/decision-input.d.ts +48 -0
- package/dist/decision-input.d.ts.map +1 -0
- package/dist/decision-input.js +14 -0
- package/dist/decision.d.ts +54 -0
- package/dist/decision.d.ts.map +1 -0
- package/dist/decision.js +500 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/intent.d.ts +22 -0
- package/dist/intent.d.ts.map +1 -0
- package/dist/intent.js +28 -0
- package/dist/llm-caller.d.ts +70 -0
- package/dist/llm-caller.d.ts.map +1 -0
- package/dist/llm-caller.js +38 -0
- package/dist/llm-rerank.d.ts +101 -0
- package/dist/llm-rerank.d.ts.map +1 -0
- package/dist/llm-rerank.js +178 -0
- package/dist/negotiate.d.ts +141 -0
- package/dist/negotiate.d.ts.map +1 -0
- package/dist/negotiate.js +161 -0
- package/dist/normalize-schema.d.ts +22 -0
- package/dist/normalize-schema.d.ts.map +1 -0
- package/dist/normalize-schema.js +191 -0
- package/dist/pure.d.ts +30 -0
- package/dist/pure.d.ts.map +1 -0
- package/dist/pure.js +43 -0
- package/dist/rag-search.d.ts +73 -0
- package/dist/rag-search.d.ts.map +1 -0
- package/dist/rag-search.js +192 -0
- package/dist/rerank-eval/pairs.d.ts +28 -0
- package/dist/rerank-eval/pairs.d.ts.map +1 -0
- package/dist/rerank-eval/pairs.js +531 -0
- package/dist/rerank-eval/run-probe-cli.d.ts +3 -0
- package/dist/rerank-eval/run-probe-cli.d.ts.map +1 -0
- package/dist/rerank-eval/run-probe-cli.js +146 -0
- package/dist/rerank-eval/run-probe.d.ts +68 -0
- package/dist/rerank-eval/run-probe.d.ts.map +1 -0
- package/dist/rerank-eval/run-probe.js +113 -0
- package/dist/session.d.ts +42 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +21 -0
- package/dist/suggestion.d.ts +38 -0
- package/dist/suggestion.d.ts.map +1 -0
- package/dist/suggestion.js +47 -0
- package/dist/synth-bench/corpus.d.ts +106 -0
- package/dist/synth-bench/corpus.d.ts.map +1 -0
- package/dist/synth-bench/corpus.js +994 -0
- package/dist/synth-bench/run-bench-cli.d.ts +3 -0
- package/dist/synth-bench/run-bench-cli.d.ts.map +1 -0
- package/dist/synth-bench/run-bench-cli.js +181 -0
- package/dist/synth-bench/run-bench.d.ts +101 -0
- package/dist/synth-bench/run-bench.d.ts.map +1 -0
- package/dist/synth-bench/run-bench.js +374 -0
- package/dist/synthesize-contract.d.ts +131 -0
- package/dist/synthesize-contract.d.ts.map +1 -0
- package/dist/synthesize-contract.js +948 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +13 -0
- package/package.json +74 -0
- package/src/contract-hash.ts +102 -0
- package/src/contract-validators.ts +604 -0
- package/src/decision-input.ts +49 -0
- package/src/decision.ts +581 -0
- package/src/index.ts +63 -0
- package/src/intent.ts +37 -0
- package/src/llm-caller.ts +82 -0
- package/src/llm-rerank.ts +280 -0
- package/src/negotiate.ts +312 -0
- package/src/normalize-schema.ts +193 -0
- package/src/pure.ts +46 -0
- package/src/rag-search.ts +274 -0
- package/src/rerank-eval/pairs.ts +624 -0
- package/src/rerank-eval/run-probe-cli.ts +197 -0
- package/src/rerank-eval/run-probe.ts +198 -0
- package/src/session.ts +41 -0
- package/src/suggestion.ts +73 -0
- package/src/synth-bench/corpus.ts +1126 -0
- package/src/synth-bench/run-bench-cli.ts +237 -0
- package/src/synth-bench/run-bench.ts +525 -0
- package/src/synthesize-contract.ts +1161 -0
- package/src/types.ts +31 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic repair of the invalid JSON Schema `type` spellings an
|
|
3
|
+
* LLM occasionally emits in a synthesized contract. Runs inside
|
|
4
|
+
* `buildContract` (before the `dataContractSchema` validation gate) so
|
|
5
|
+
* the dominant synth-decline class — a `type` value outside the seven
|
|
6
|
+
* JSON Schema primitives — is structurally eliminated rather than left
|
|
7
|
+
* to a repair retry.
|
|
8
|
+
*
|
|
9
|
+
* Pure, recursive, total: a schema that is already valid passes
|
|
10
|
+
* through unchanged — every mapping fires only on a value NOT in the
|
|
11
|
+
* valid set. Recursion descends only into sub-schema positions, so
|
|
12
|
+
* data-value positions (`default`, `const`, `enum`, `examples`) are
|
|
13
|
+
* left verbatim — a `default` of `{type: "list"}` is user data, not a
|
|
14
|
+
* schema, and must not be rewritten.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** The seven JSON Schema primitive type names. */
|
|
18
|
+
const VALID_TYPES = new Set([
|
|
19
|
+
'string',
|
|
20
|
+
'number',
|
|
21
|
+
'integer',
|
|
22
|
+
'boolean',
|
|
23
|
+
'array',
|
|
24
|
+
'object',
|
|
25
|
+
'null',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
/** Unambiguous non-canonical type spellings → canonical primitive. */
|
|
29
|
+
const TYPE_ALIASES: Record<string, string> = {
|
|
30
|
+
list: 'array',
|
|
31
|
+
tuple: 'array',
|
|
32
|
+
sequence: 'array',
|
|
33
|
+
dict: 'object',
|
|
34
|
+
map: 'object',
|
|
35
|
+
record: 'object',
|
|
36
|
+
hash: 'object',
|
|
37
|
+
str: 'string',
|
|
38
|
+
text: 'string',
|
|
39
|
+
char: 'string',
|
|
40
|
+
int: 'integer',
|
|
41
|
+
long: 'integer',
|
|
42
|
+
short: 'integer',
|
|
43
|
+
float: 'number',
|
|
44
|
+
double: 'number',
|
|
45
|
+
decimal: 'number',
|
|
46
|
+
num: 'number',
|
|
47
|
+
numeric: 'number',
|
|
48
|
+
bool: 'boolean',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Type words that mean "no constraint" — the `type` key is dropped. */
|
|
52
|
+
const DROP_TYPES = new Set(['any', 'unknown', 'mixed', 'void']);
|
|
53
|
+
|
|
54
|
+
/** Object keys whose value is itself a single schema. */
|
|
55
|
+
const SCHEMA_VALUED_KEYS = new Set([
|
|
56
|
+
'items',
|
|
57
|
+
'additionalProperties',
|
|
58
|
+
'not',
|
|
59
|
+
'contains',
|
|
60
|
+
'propertyNames',
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
/** Object keys whose value is a map of name → schema. */
|
|
64
|
+
const SCHEMA_MAP_KEYS = new Set([
|
|
65
|
+
'properties',
|
|
66
|
+
'patternProperties',
|
|
67
|
+
'$defs',
|
|
68
|
+
'definitions',
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
/** Object keys whose value is an array of schemas. */
|
|
72
|
+
const SCHEMA_ARRAY_KEYS = new Set([
|
|
73
|
+
'allOf',
|
|
74
|
+
'anyOf',
|
|
75
|
+
'oneOf',
|
|
76
|
+
'prefixItems',
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Infer the base type of an `enum`-constrained field from its value
|
|
81
|
+
* list. Mixed / empty / non-array → `string` (the safe default, and
|
|
82
|
+
* the overwhelmingly common enum shape).
|
|
83
|
+
*/
|
|
84
|
+
function inferEnumBaseType(enumSibling: unknown): string {
|
|
85
|
+
if (!Array.isArray(enumSibling) || enumSibling.length === 0) {
|
|
86
|
+
return 'string';
|
|
87
|
+
}
|
|
88
|
+
if (enumSibling.every((v) => typeof v === 'string')) return 'string';
|
|
89
|
+
if (enumSibling.every((v) => typeof v === 'number')) return 'number';
|
|
90
|
+
if (enumSibling.every((v) => typeof v === 'boolean')) return 'boolean';
|
|
91
|
+
return 'string';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Map one `type` value to a canonical primitive, or `undefined` when
|
|
96
|
+
* the `type` key should be dropped entirely (an absent `type` is valid
|
|
97
|
+
* JSON Schema — it simply imposes no constraint).
|
|
98
|
+
*
|
|
99
|
+
* `enumSibling` is the schema's `enum` array, used to pick the base
|
|
100
|
+
* type when the LLM wrote the (invalid) `type: "enum"`.
|
|
101
|
+
*/
|
|
102
|
+
function normalizeTypeValue(
|
|
103
|
+
value: unknown,
|
|
104
|
+
enumSibling: unknown,
|
|
105
|
+
): string | undefined {
|
|
106
|
+
if (typeof value === 'string') {
|
|
107
|
+
const lower = value.toLowerCase();
|
|
108
|
+
if (VALID_TYPES.has(lower)) return lower;
|
|
109
|
+
if (lower === 'enum') return inferEnumBaseType(enumSibling);
|
|
110
|
+
if (DROP_TYPES.has(lower)) return undefined;
|
|
111
|
+
const alias = TYPE_ALIASES[lower];
|
|
112
|
+
if (alias !== undefined) return alias;
|
|
113
|
+
// Unrecognized garbage — drop the constraint rather than guess.
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
if (Array.isArray(value)) {
|
|
117
|
+
// Union type (`["string", "null"]`). The protocol contract schema
|
|
118
|
+
// accepts only a single string; recover the first valid non-null
|
|
119
|
+
// member, dropping the nullable arm.
|
|
120
|
+
for (const member of value) {
|
|
121
|
+
if (typeof member === 'string') {
|
|
122
|
+
const norm = normalizeTypeValue(member, enumSibling);
|
|
123
|
+
if (norm !== undefined && norm !== 'null') return norm;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return 'string';
|
|
127
|
+
}
|
|
128
|
+
// `type` was a number / object / boolean — meaningless; drop it.
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Normalize a map of name → schema (e.g. `properties`). */
|
|
133
|
+
function normalizeSchemaMap(value: unknown): unknown {
|
|
134
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
const out: Record<string, unknown> = {};
|
|
138
|
+
for (const [name, sub] of Object.entries(value as Record<string, unknown>)) {
|
|
139
|
+
out[name] = coerceToSchema(sub);
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Coerce a value found in a schema-expecting position into a schema
|
|
146
|
+
* object. An LLM sometimes writes the shorthand `items: "number"`
|
|
147
|
+
* where JSON Schema requires `items: {type: "number"}` — expand it
|
|
148
|
+
* (`"number"` → `{type: "number"}`, an unrecognized string → `{}`,
|
|
149
|
+
* the unconstrained schema). A boolean (`additionalProperties: false`)
|
|
150
|
+
* is a valid schema-position value and passes through; anything else
|
|
151
|
+
* recurses through {@link normalizeSchema}.
|
|
152
|
+
*/
|
|
153
|
+
function coerceToSchema(value: unknown): unknown {
|
|
154
|
+
if (typeof value === 'string') {
|
|
155
|
+
const t = normalizeTypeValue(value, undefined);
|
|
156
|
+
return t === undefined ? {} : { type: t };
|
|
157
|
+
}
|
|
158
|
+
return normalizeSchema(value);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Recursively normalize a JSON Schema, repairing invalid `type`
|
|
163
|
+
* values. Non-object input (booleans, primitives) passes through —
|
|
164
|
+
* `additionalProperties: false` and the like are valid as-is.
|
|
165
|
+
*/
|
|
166
|
+
export function normalizeSchema(schema: unknown): unknown {
|
|
167
|
+
if (typeof schema !== 'object' || schema === null || Array.isArray(schema)) {
|
|
168
|
+
return schema;
|
|
169
|
+
}
|
|
170
|
+
const obj = schema as Record<string, unknown>;
|
|
171
|
+
const out: Record<string, unknown> = {};
|
|
172
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
173
|
+
if (key === 'type') {
|
|
174
|
+
const fixed = normalizeTypeValue(value, obj['enum']);
|
|
175
|
+
if (fixed !== undefined) out[key] = fixed;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (SCHEMA_VALUED_KEYS.has(key)) {
|
|
179
|
+
out[key] = coerceToSchema(value);
|
|
180
|
+
} else if (SCHEMA_MAP_KEYS.has(key)) {
|
|
181
|
+
out[key] = normalizeSchemaMap(value);
|
|
182
|
+
} else if (SCHEMA_ARRAY_KEYS.has(key)) {
|
|
183
|
+
out[key] = Array.isArray(value)
|
|
184
|
+
? value.map((s) => coerceToSchema(s))
|
|
185
|
+
: value;
|
|
186
|
+
} else {
|
|
187
|
+
// Data-value position (`default`, `const`, `enum`, `examples`,
|
|
188
|
+
// `required`, `description`, …) — leave verbatim.
|
|
189
|
+
out[key] = value;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
package/src/pure.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers — zero I/O, zero AWS dependencies.
|
|
3
|
+
*
|
|
4
|
+
* - `inferInteractionMode(text)` — keyword-priority mapper from free
|
|
5
|
+
* text (category / pattern / prompt) to a categorical interaction
|
|
6
|
+
* label. Used for telemetry / RAG hinting only; NOT part of the
|
|
7
|
+
* `DataContract` wire surface. Deterministic; default `'display'`.
|
|
8
|
+
* - `inferJsonSchemaType(tsType)` — TypeScript-style type-string
|
|
9
|
+
* mapper to the JSON Schema type union. Deterministic.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Categorical interaction-mode label used by `inferInteractionMode` for
|
|
14
|
+
* telemetry / RAG hinting. NOT a `DataContract` field — the four typed
|
|
15
|
+
* specs (propsSpec / actionSpec / contextSpec / streamSpec) describe
|
|
16
|
+
* the wire surface exhaustively, so interaction mode is a derived
|
|
17
|
+
* heuristic, never part of the contract.
|
|
18
|
+
*/
|
|
19
|
+
export type InteractionMode = 'display' | 'collect' | 'converse' | 'broadcast' | 'flow';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Infer interaction mode from any text (category, pattern, or prompt).
|
|
23
|
+
* Consolidated keyword matcher — all call sites use the same function.
|
|
24
|
+
*
|
|
25
|
+
* Used by rag-search and external benchmark runners for routing /
|
|
26
|
+
* telemetry. Deterministic; no I/O. Matches the first rule in priority
|
|
27
|
+
* order, defaulting to 'display'.
|
|
28
|
+
*/
|
|
29
|
+
export function inferInteractionMode(text: string): InteractionMode {
|
|
30
|
+
const t = text.toLowerCase();
|
|
31
|
+
if (t.includes('form') || t.includes('survey') || t.includes('input') || t.includes('collect') || t.includes('sign up') || t.includes('contact')) return 'collect';
|
|
32
|
+
if (t.includes('chat') || t.includes('conversation') || t.includes('messaging') || t.includes('talk')) return 'converse';
|
|
33
|
+
if (t.includes('dashboard') || t.includes('monitor') || t.includes('live') || t.includes('feed') || t.includes('real-time')) return 'broadcast';
|
|
34
|
+
if (t.includes('wizard') || t.includes('onboarding') || t.includes('checkout') || t.includes('stepper') || t.includes('multi-step') || t.includes('flow') || t.includes('step')) return 'flow';
|
|
35
|
+
return 'display';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Map TypeScript-style type strings to JSON Schema types. */
|
|
39
|
+
export function inferJsonSchemaType(tsType: string): 'string' | 'number' | 'boolean' | 'array' | 'object' {
|
|
40
|
+
const t = tsType.toLowerCase();
|
|
41
|
+
if (t.includes('string')) return 'string';
|
|
42
|
+
if (t.includes('number') || t.includes('int') || t.includes('float')) return 'number';
|
|
43
|
+
if (t.includes('boolean') || t.includes('bool')) return 'boolean';
|
|
44
|
+
if (t.includes('array') || t.includes('[]')) return 'array';
|
|
45
|
+
return 'object';
|
|
46
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RAG Search — embedding-similarity blueprint retrieval over the
|
|
3
|
+
* public `@ggui-ai/mcp-server-core` storage seam.
|
|
4
|
+
*
|
|
5
|
+
* Given agent `prompt` + a tenant `scope` (typically appId, or the
|
|
6
|
+
* literal `"shared"` for the global catalog), this helper:
|
|
7
|
+
*
|
|
8
|
+
* 1. Embeds the prompt via {@link EmbeddingProvider.embed}.
|
|
9
|
+
* 2. Queries the {@link VectorStore} for the top-K nearest neighbors
|
|
10
|
+
* (server-side cosine, scope-partitioned — no cross-tenant leak).
|
|
11
|
+
* 3. Filters by minimum score, biases private (per-app registered
|
|
12
|
+
* UIs) over shared (generated pool) at the same score band, and
|
|
13
|
+
* projects each hit into a {@link NegotiatorOption}.
|
|
14
|
+
*
|
|
15
|
+
* Two pipeline paths emerge from the confidence band:
|
|
16
|
+
* - **High confidence** (`score >= HIGH_CONFIDENCE_THRESHOLD`): the
|
|
17
|
+
* hit is marked `exact`. Upstream callers (V3 `negotiate()`) can
|
|
18
|
+
* short-circuit the decision LLM entirely.
|
|
19
|
+
* - **Medium confidence** (`score >= RETRIEVAL_MIN_SCORE` and below
|
|
20
|
+
* the exact threshold): marked `partial`. Upstream passes these to
|
|
21
|
+
* the decision LLM as candidates.
|
|
22
|
+
*
|
|
23
|
+
* ### Public-seam contract
|
|
24
|
+
*
|
|
25
|
+
* This function reads only the public `VectorStore` surface (scalar
|
|
26
|
+
* metadata). Consumers that still hold the rich `EmbeddingStorage`
|
|
27
|
+
* shape bridge via
|
|
28
|
+
* `@ggui-cloud/aws-adapters.embeddingStorageToVectorStore`, which
|
|
29
|
+
* encodes array-valued fields (`props`, `callbacks`, `sourceTools`)
|
|
30
|
+
* as JSON strings inside `metadata`. Any `VectorStore` implementation
|
|
31
|
+
* that stores fresh writes through `writeRagVector` (see
|
|
32
|
+
* `mcp-servers/ggui-protocol/src/adapters/vector-store.ts`) uses the
|
|
33
|
+
* same encoding — so existing caches, fresh writes, and community
|
|
34
|
+
* `VectorStore` backends all collide on the same retrieval key.
|
|
35
|
+
*
|
|
36
|
+
* ### Scope
|
|
37
|
+
*
|
|
38
|
+
* No AWS bindings, no LLM dependency. Pure composition over the two
|
|
39
|
+
* public seams. The one-LLM-call `makeDecision()` step is separate.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import type {
|
|
43
|
+
EmbeddingProvider,
|
|
44
|
+
VectorSearchResult,
|
|
45
|
+
VectorStore,
|
|
46
|
+
} from '@ggui-ai/mcp-server-core';
|
|
47
|
+
import type { DataContract, JsonValue } from '@ggui-ai/protocol';
|
|
48
|
+
import { inferJsonSchemaType } from './pure.js';
|
|
49
|
+
import type { NegotiatorOption } from './types.js';
|
|
50
|
+
|
|
51
|
+
/** RAG search result with per-stage timing. */
|
|
52
|
+
export interface RagSearchResult {
|
|
53
|
+
options: NegotiatorOption[];
|
|
54
|
+
embeddingLatencyMs: number;
|
|
55
|
+
searchLatencyMs: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Dependencies — both public seams from `@ggui-ai/mcp-server-core`. */
|
|
59
|
+
export interface RagSearchDeps {
|
|
60
|
+
embedding: EmbeddingProvider;
|
|
61
|
+
vectors: VectorStore;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface RagSearchInput {
|
|
65
|
+
/** Natural-language query text. Embedded as-is. */
|
|
66
|
+
prompt: string;
|
|
67
|
+
/**
|
|
68
|
+
* Scope / tenant partition. Typically `appId` for per-app indexes
|
|
69
|
+
* or the literal `"shared"` for the global catalog. Passed straight
|
|
70
|
+
* through to `VectorStore.query`; the scope is the tenant boundary,
|
|
71
|
+
* so a query never crosses into another tenant's index.
|
|
72
|
+
*/
|
|
73
|
+
scope: string;
|
|
74
|
+
/** Override the k-NN cut-off (default 10). */
|
|
75
|
+
maxCandidates?: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Minimum cosine similarity to be considered a retrieval candidate. */
|
|
79
|
+
const RETRIEVAL_MIN_SCORE = 0.15;
|
|
80
|
+
|
|
81
|
+
/** Cosine similarity above this → exact match (skip LLM). */
|
|
82
|
+
const HIGH_CONFIDENCE_THRESHOLD = 0.45;
|
|
83
|
+
|
|
84
|
+
/** Default top-K for the k-NN query. */
|
|
85
|
+
const DEFAULT_MAX_CANDIDATES = 10;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Search the vector index for blueprints matching `prompt` within
|
|
89
|
+
* `scope`. See the module docstring for the confidence-band pipeline
|
|
90
|
+
* and encoding contract.
|
|
91
|
+
*/
|
|
92
|
+
export async function ragSearch(
|
|
93
|
+
deps: RagSearchDeps,
|
|
94
|
+
input: RagSearchInput,
|
|
95
|
+
): Promise<RagSearchResult> {
|
|
96
|
+
const topK = input.maxCandidates ?? DEFAULT_MAX_CANDIDATES;
|
|
97
|
+
|
|
98
|
+
// Stage 1: embed the query text
|
|
99
|
+
const embedStart = Date.now();
|
|
100
|
+
const queryEmbedding = await deps.embedding.embed(input.prompt);
|
|
101
|
+
const embeddingLatencyMs = Date.now() - embedStart;
|
|
102
|
+
|
|
103
|
+
// Stage 2: nearest-neighbor query
|
|
104
|
+
const searchStart = Date.now();
|
|
105
|
+
const results = await deps.vectors.query(input.scope, queryEmbedding, topK);
|
|
106
|
+
const searchLatencyMs = Date.now() - searchStart;
|
|
107
|
+
|
|
108
|
+
// Filter + sort: private (registered UIs) always beats shared (generated)
|
|
109
|
+
// at the same score level; then descending score.
|
|
110
|
+
const candidates = results
|
|
111
|
+
.filter((r) => r.score >= RETRIEVAL_MIN_SCORE)
|
|
112
|
+
.sort((a, b) => {
|
|
113
|
+
const aPrivate = readPoolSource(a.metadata) === 'private' ? 1 : 0;
|
|
114
|
+
const bPrivate = readPoolSource(b.metadata) === 'private' ? 1 : 0;
|
|
115
|
+
if (aPrivate !== bPrivate) return bPrivate - aPrivate;
|
|
116
|
+
return b.score - a.score;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (candidates.length === 0) {
|
|
120
|
+
return { options: [], embeddingLatencyMs, searchLatencyMs };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const seenContracts = new Set<string>();
|
|
124
|
+
const options: NegotiatorOption[] = [];
|
|
125
|
+
|
|
126
|
+
for (const match of candidates) {
|
|
127
|
+
const option = buildOption(match);
|
|
128
|
+
if (!option) continue;
|
|
129
|
+
if (seenContracts.has(option.contractKey)) continue;
|
|
130
|
+
seenContracts.add(option.contractKey);
|
|
131
|
+
options.push(option.value);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { options, embeddingLatencyMs, searchLatencyMs };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Project a public {@link VectorSearchResult} hit into a
|
|
139
|
+
* {@link NegotiatorOption}, together with the dedup key used to
|
|
140
|
+
* collapse hits that describe the same semantic contract.
|
|
141
|
+
*/
|
|
142
|
+
function buildOption(
|
|
143
|
+
match: VectorSearchResult,
|
|
144
|
+
): { value: NegotiatorOption; contractKey: string } | undefined {
|
|
145
|
+
const isExact = match.score >= HIGH_CONFIDENCE_THRESHOLD;
|
|
146
|
+
const verdict = isExact ? 'exact' : 'partial';
|
|
147
|
+
|
|
148
|
+
const blueprintHash = match.key;
|
|
149
|
+
// Registered (private) blueprints already carry a `p_` prefix in
|
|
150
|
+
// their hash; generated pool entries do not — prefix them as `c_`
|
|
151
|
+
// to match the legacy option ID format that downstream consumers
|
|
152
|
+
// (decision prompt, cache layer) have been reading since V2.
|
|
153
|
+
const blueprintId = blueprintHash.startsWith('p_')
|
|
154
|
+
? blueprintHash
|
|
155
|
+
: `c_${blueprintHash}`;
|
|
156
|
+
|
|
157
|
+
const prompt = readString(match.metadata.prompt, '');
|
|
158
|
+
const intent = readString(match.metadata.intent, '');
|
|
159
|
+
const category = readString(match.metadata.category, '');
|
|
160
|
+
const contractHash = readString(match.metadata.contractHash, '');
|
|
161
|
+
const poolSource = readPoolSource(match.metadata);
|
|
162
|
+
const featured = Boolean(match.metadata.featured);
|
|
163
|
+
const props = readProps(match.metadata.props);
|
|
164
|
+
|
|
165
|
+
const contract = buildContract({ prompt, intent, category, props });
|
|
166
|
+
|
|
167
|
+
// Dedup: prefer matching on semantic intent; fall back to prop
|
|
168
|
+
// signature so two blueprints with identical intents but different
|
|
169
|
+
// prop shapes don't collapse together.
|
|
170
|
+
const contractKey =
|
|
171
|
+
intent !== ''
|
|
172
|
+
? intent
|
|
173
|
+
: JSON.stringify(Object.keys(contract.propsSpec?.properties ?? {}).sort());
|
|
174
|
+
|
|
175
|
+
const option: NegotiatorOption = {
|
|
176
|
+
id: `rag_${blueprintId.slice(-8)}`,
|
|
177
|
+
type: 'blueprint',
|
|
178
|
+
blueprintId,
|
|
179
|
+
pattern: category,
|
|
180
|
+
description: `${prompt} (similarity: ${Math.round(match.score * 100)}%, ${verdict})`,
|
|
181
|
+
pros: [
|
|
182
|
+
isExact
|
|
183
|
+
? 'Exact blueprint match — instant render'
|
|
184
|
+
: 'Semantically matched to your request',
|
|
185
|
+
...(featured ? ['Featured blueprint — curated quality'] : []),
|
|
186
|
+
],
|
|
187
|
+
cons: [
|
|
188
|
+
...(isExact ? [] : ['Partial match — may need adjustments']),
|
|
189
|
+
...(!isExact ? ['Fixed layout — limited customization'] : []),
|
|
190
|
+
],
|
|
191
|
+
renderTime: 'instant',
|
|
192
|
+
contract,
|
|
193
|
+
...(contractHash !== '' ? { contractHash } : {}),
|
|
194
|
+
...(poolSource !== undefined ? { poolSource } : {}),
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
return { value: option, contractKey };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Prop schema shape as encoded by `embeddingStorageToVectorStore`. */
|
|
201
|
+
interface PropSpec {
|
|
202
|
+
name: string;
|
|
203
|
+
type: string;
|
|
204
|
+
required: boolean;
|
|
205
|
+
description: string;
|
|
206
|
+
example?: unknown;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function buildContract(args: {
|
|
210
|
+
prompt: string;
|
|
211
|
+
intent: string;
|
|
212
|
+
category: string;
|
|
213
|
+
props: PropSpec[];
|
|
214
|
+
}): DataContract {
|
|
215
|
+
// `intent` is not a contract field. The `prompt` and `intent` fields
|
|
216
|
+
// stay on the args because callers still use them for RAG keys +
|
|
217
|
+
// dedup; the returned contract carries structural shape only.
|
|
218
|
+
const { props } = args;
|
|
219
|
+
return {
|
|
220
|
+
propsSpec: {
|
|
221
|
+
properties: Object.fromEntries(
|
|
222
|
+
props.map((p) => [
|
|
223
|
+
p.name,
|
|
224
|
+
{
|
|
225
|
+
description: p.description,
|
|
226
|
+
schema: { type: inferJsonSchemaType(p.type) },
|
|
227
|
+
required: p.required,
|
|
228
|
+
...(p.example !== undefined
|
|
229
|
+
? { example: p.example as JsonValue }
|
|
230
|
+
: {}),
|
|
231
|
+
},
|
|
232
|
+
]),
|
|
233
|
+
),
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function readString(
|
|
239
|
+
value: string | number | boolean | null | undefined,
|
|
240
|
+
fallback: string,
|
|
241
|
+
): string {
|
|
242
|
+
return typeof value === 'string' ? value : fallback;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function readPoolSource(
|
|
246
|
+
metadata: Record<string, string | number | boolean | null>,
|
|
247
|
+
): 'shared' | 'private' | undefined {
|
|
248
|
+
const v = metadata.poolSource;
|
|
249
|
+
return v === 'shared' || v === 'private' ? v : undefined;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function readProps(
|
|
253
|
+
value: string | number | boolean | null | undefined,
|
|
254
|
+
): PropSpec[] {
|
|
255
|
+
if (typeof value !== 'string' || value.length === 0) return [];
|
|
256
|
+
try {
|
|
257
|
+
const parsed = JSON.parse(value) as unknown;
|
|
258
|
+
if (!Array.isArray(parsed)) return [];
|
|
259
|
+
return parsed.filter(isPropSpec);
|
|
260
|
+
} catch {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function isPropSpec(value: unknown): value is PropSpec {
|
|
266
|
+
if (value === null || typeof value !== 'object') return false;
|
|
267
|
+
const v = value as Record<string, unknown>;
|
|
268
|
+
return (
|
|
269
|
+
typeof v.name === 'string' &&
|
|
270
|
+
typeof v.type === 'string' &&
|
|
271
|
+
typeof v.required === 'boolean' &&
|
|
272
|
+
typeof v.description === 'string'
|
|
273
|
+
);
|
|
274
|
+
}
|