@zenera/rag 1.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/LICENSE +21 -0
- package/README.md +144 -0
- package/dist/command.d.ts +3 -0
- package/dist/command.js +436 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +16 -0
- package/dist/present.d.ts +17 -0
- package/dist/present.js +40 -0
- package/dist/query.d.ts +7 -0
- package/dist/query.js +88 -0
- package/dist/repl.d.ts +8 -0
- package/dist/repl.js +119 -0
- package/dist/schema/build.d.ts +28 -0
- package/dist/schema/build.js +79 -0
- package/dist/schema/entities.d.ts +18 -0
- package/dist/schema/entities.js +71 -0
- package/dist/schema/files.d.ts +74 -0
- package/dist/schema/files.js +79 -0
- package/dist/schema/graph.d.ts +48 -0
- package/dist/schema/graph.js +320 -0
- package/dist/schema/hydrate.d.ts +19 -0
- package/dist/schema/hydrate.js +182 -0
- package/dist/schema/render.d.ts +11 -0
- package/dist/schema/render.js +254 -0
- package/dist/schema/schema.d.ts +11 -0
- package/dist/schema/schema.js +189 -0
- package/dist/schema/search.d.ts +50 -0
- package/dist/schema/search.js +142 -0
- package/dist/schema/spec.d.ts +58 -0
- package/dist/schema/spec.js +309 -0
- package/dist/schema/store.d.ts +32 -0
- package/dist/schema/store.js +126 -0
- package/dist/schema/subgraph.d.ts +42 -0
- package/dist/schema/subgraph.js +272 -0
- package/dist/schema/tools.d.ts +10 -0
- package/dist/schema/tools.js +242 -0
- package/dist/schema/typescript.d.ts +22 -0
- package/dist/schema/typescript.js +246 -0
- package/package.json +59 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
export const DEFAULT_MAX_HOPS = 3;
|
|
2
|
+
export const DEFAULT_MAX_NODES = 200;
|
|
3
|
+
/** How many fields of one type are pulled in when the type itself was the hit. */
|
|
4
|
+
const FIELDS_PER_TYPE = 24;
|
|
5
|
+
/** How many when one field was the hit and the rest of the type is only context. */
|
|
6
|
+
const FIELDS_PER_OWNER = 8;
|
|
7
|
+
/** How far containment is read backwards, how many calls are enough, how wide it may get. */
|
|
8
|
+
const REACH_HOPS = 6;
|
|
9
|
+
const REACH_CALLERS = 3;
|
|
10
|
+
const REACH_VISITS = 512;
|
|
11
|
+
const OWNS = new Set(['HAS_PROPERTY', 'HAS_PARAM']);
|
|
12
|
+
const CALLS = new Set(['TAKES_INPUT', 'RETURNS_OUTPUT']);
|
|
13
|
+
const OF_TYPE = new Set(['OF_TYPE']);
|
|
14
|
+
const WITHIN = new Set(['COMPOSES', 'ITEM_OF']);
|
|
15
|
+
/** Containment read backwards: a type to the fields holding it, a field to its owner. */
|
|
16
|
+
const UPWARD = new Set([
|
|
17
|
+
'HAS_PROPERTY',
|
|
18
|
+
'HAS_PARAM',
|
|
19
|
+
'OF_TYPE',
|
|
20
|
+
'COMPOSES',
|
|
21
|
+
'ITEM_OF',
|
|
22
|
+
]);
|
|
23
|
+
/** Why a node is here. The budget is spent in this order, so fills go first. */
|
|
24
|
+
const LINK = 0;
|
|
25
|
+
const FILL = 1;
|
|
26
|
+
export function stitch(graph, seeds, options = {}) {
|
|
27
|
+
const maxHops = options.maxHops ?? DEFAULT_MAX_HOPS;
|
|
28
|
+
const maxNodes = options.maxNodes ?? DEFAULT_MAX_NODES;
|
|
29
|
+
const live = seeds.filter((s) => graph.hasNode(s.id));
|
|
30
|
+
if (live.length === 0) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
const scores = new Map();
|
|
34
|
+
for (const seed of live) {
|
|
35
|
+
scores.set(seed.id, (scores.get(seed.id) ?? 0) + seed.score);
|
|
36
|
+
}
|
|
37
|
+
const keep = new Map();
|
|
38
|
+
const put = (id, tier) => keep.set(id, Math.min(keep.get(id) ?? tier, tier));
|
|
39
|
+
for (const id of scores.keys()) {
|
|
40
|
+
put(id, LINK);
|
|
41
|
+
}
|
|
42
|
+
for (const seed of live) {
|
|
43
|
+
for (const node of anchor(graph, seed.id)) {
|
|
44
|
+
put(node.id, node.tier);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const node of connect(graph, live, maxHops)) {
|
|
48
|
+
put(node, LINK);
|
|
49
|
+
}
|
|
50
|
+
return components(graph, keep, scores).map((part) => materialize(graph, part, scores, keep, maxNodes));
|
|
51
|
+
}
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
/** The nodes that make one hit legible on its own. */
|
|
54
|
+
function anchor(graph, id) {
|
|
55
|
+
const kind = graph.getNodeAttribute(id, 'kind');
|
|
56
|
+
const out = [];
|
|
57
|
+
const link = (ids) => out.push(...ids.map((n) => ({ id: n, tier: LINK })));
|
|
58
|
+
const fill = (ids) => out.push(...ids.map((n) => ({ id: n, tier: FILL })));
|
|
59
|
+
if (kind === 'property') {
|
|
60
|
+
for (const owner of related(graph, id, 'in', OWNS)) {
|
|
61
|
+
link([owner, ...reach(graph, owner)]);
|
|
62
|
+
fill(related(graph, owner, 'out', OWNS).slice(0, FIELDS_PER_OWNER));
|
|
63
|
+
}
|
|
64
|
+
// What the field is *of* matters as much as what holds it.
|
|
65
|
+
link(related(graph, id, 'out', OF_TYPE));
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
if (kind === 'type') {
|
|
69
|
+
link(reach(graph, id));
|
|
70
|
+
link(related(graph, id, 'out', WITHIN));
|
|
71
|
+
fill(related(graph, id, 'out', OWNS).slice(0, FIELDS_PER_TYPE));
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
for (const target of related(graph, id, 'out', new Set([...OWNS, ...CALLS]))) {
|
|
75
|
+
link([target]);
|
|
76
|
+
// A parameter typed `Species` is part of reading the call, and is one
|
|
77
|
+
// hop further out than anything else here.
|
|
78
|
+
if (graph.getNodeAttribute(target, 'kind') === 'property') {
|
|
79
|
+
link(related(graph, target, 'out', OF_TYPE));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The way in, and only the way in: containment read backwards from a type until
|
|
86
|
+
* it lands on the operations carrying it, keeping the nodes on the route and
|
|
87
|
+
* nothing beside them. A shared `Money` is held by half the spec, so this stops
|
|
88
|
+
* at the first few calls rather than reporting all of them.
|
|
89
|
+
*/
|
|
90
|
+
function reach(graph, from) {
|
|
91
|
+
const previous = new Map([[from, from]]);
|
|
92
|
+
const out = [];
|
|
93
|
+
let frontier = [from];
|
|
94
|
+
let found = 0;
|
|
95
|
+
for (let hop = 0; hop < REACH_HOPS && frontier.length > 0 && found < REACH_CALLERS; hop++) {
|
|
96
|
+
const next = [];
|
|
97
|
+
for (const node of frontier) {
|
|
98
|
+
for (const caller of related(graph, node, 'in', CALLS)) {
|
|
99
|
+
if (previous.has(caller)) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
previous.set(caller, node);
|
|
103
|
+
out.push(...trace(previous, from, caller));
|
|
104
|
+
if (++found >= REACH_CALLERS) {
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (found >= REACH_CALLERS || previous.size >= REACH_VISITS) {
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
for (const up of related(graph, node, 'in', UPWARD)) {
|
|
112
|
+
if (!previous.has(up)) {
|
|
113
|
+
previous.set(up, node);
|
|
114
|
+
next.push(up);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
frontier = next;
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
function related(graph, id, side, relations) {
|
|
123
|
+
const edges = side === 'in' ? graph.inEdges(id) : graph.outEdges(id);
|
|
124
|
+
const out = [];
|
|
125
|
+
for (const edge of edges) {
|
|
126
|
+
if (relations.has(graph.getEdgeAttribute(edge, 'relation'))) {
|
|
127
|
+
out.push(side === 'in' ? graph.source(edge) : graph.target(edge));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return [...new Set(out)];
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The joins between hits. Only pairs from different query terms are tried:
|
|
134
|
+
* two properties found by the same phrase are already one thought, and paying
|
|
135
|
+
* for a path between them buys nothing but hops.
|
|
136
|
+
*/
|
|
137
|
+
function connect(graph, seeds, maxHops) {
|
|
138
|
+
const out = new Set();
|
|
139
|
+
for (let i = 0; i < seeds.length; i++) {
|
|
140
|
+
for (let j = i + 1; j < seeds.length; j++) {
|
|
141
|
+
const a = seeds[i];
|
|
142
|
+
const b = seeds[j];
|
|
143
|
+
if (a.term === b.term || a.id === b.id) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
for (const node of path(graph, a.id, b.id, maxHops) ?? []) {
|
|
147
|
+
out.add(node);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
/** Breadth-first, ignoring edge direction, giving up past `maxHops`. */
|
|
154
|
+
export function path(graph, from, to, maxHops) {
|
|
155
|
+
if (from === to) {
|
|
156
|
+
return [from];
|
|
157
|
+
}
|
|
158
|
+
const previous = new Map([[from, from]]);
|
|
159
|
+
let frontier = [from];
|
|
160
|
+
for (let hop = 0; hop < maxHops && frontier.length > 0; hop++) {
|
|
161
|
+
const next = [];
|
|
162
|
+
for (const node of frontier) {
|
|
163
|
+
for (const neighbor of graph.neighbors(node)) {
|
|
164
|
+
if (previous.has(neighbor)) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
previous.set(neighbor, node);
|
|
168
|
+
if (neighbor === to) {
|
|
169
|
+
return trace(previous, from, to);
|
|
170
|
+
}
|
|
171
|
+
next.push(neighbor);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
frontier = next;
|
|
175
|
+
}
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
function trace(previous, from, to) {
|
|
179
|
+
const out = [to];
|
|
180
|
+
while (out[0] !== from) {
|
|
181
|
+
out.unshift(previous.get(out[0]));
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
/** Connected pieces of the kept set, biggest score first. */
|
|
187
|
+
function components(graph, keep, scores) {
|
|
188
|
+
const seen = new Set();
|
|
189
|
+
const out = [];
|
|
190
|
+
for (const start of keep.keys()) {
|
|
191
|
+
if (seen.has(start)) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const part = new Set();
|
|
195
|
+
const stack = [start];
|
|
196
|
+
while (stack.length > 0) {
|
|
197
|
+
const node = stack.pop();
|
|
198
|
+
if (seen.has(node)) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
seen.add(node);
|
|
202
|
+
part.add(node);
|
|
203
|
+
for (const neighbor of graph.neighbors(node)) {
|
|
204
|
+
if (keep.has(neighbor) && !seen.has(neighbor)) {
|
|
205
|
+
stack.push(neighbor);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
out.push(part);
|
|
210
|
+
}
|
|
211
|
+
return out.sort((a, b) => total(b, scores) - total(a, scores));
|
|
212
|
+
}
|
|
213
|
+
function total(part, scores) {
|
|
214
|
+
let sum = 0;
|
|
215
|
+
for (const node of part) {
|
|
216
|
+
sum += scores.get(node) ?? 0;
|
|
217
|
+
}
|
|
218
|
+
return sum;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Turns one component into a result, spending the node budget on the hits
|
|
222
|
+
* first, then on what connects them to a call, and only then on the fields
|
|
223
|
+
* that came along for context — so what is dropped is always the part
|
|
224
|
+
* furthest from anything anyone asked about.
|
|
225
|
+
*/
|
|
226
|
+
function materialize(graph, part, scores, tiers, maxNodes) {
|
|
227
|
+
const hits = [...part].filter((id) => scores.has(id));
|
|
228
|
+
const kept = new Set(hits.slice(0, maxNodes));
|
|
229
|
+
const rest = [...part]
|
|
230
|
+
.filter((id) => !kept.has(id))
|
|
231
|
+
.sort((a, b) => (tiers.get(a) ?? FILL) - (tiers.get(b) ?? FILL) || a.localeCompare(b));
|
|
232
|
+
for (const id of rest) {
|
|
233
|
+
if (kept.size >= maxNodes) {
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
kept.add(id);
|
|
237
|
+
}
|
|
238
|
+
const nodes = [...kept]
|
|
239
|
+
.map((id) => ({
|
|
240
|
+
id,
|
|
241
|
+
kind: graph.getNodeAttribute(id, 'kind'),
|
|
242
|
+
attributes: graph.getNodeAttributes(id),
|
|
243
|
+
hit: scores.has(id),
|
|
244
|
+
score: scores.get(id) ?? 0,
|
|
245
|
+
}))
|
|
246
|
+
.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
247
|
+
const edges = [];
|
|
248
|
+
for (const id of kept) {
|
|
249
|
+
for (const edge of graph.outEdges(id)) {
|
|
250
|
+
const target = graph.target(edge);
|
|
251
|
+
if (!kept.has(target)) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const attrs = graph.getEdgeAttributes(edge);
|
|
255
|
+
edges.push({
|
|
256
|
+
source: id,
|
|
257
|
+
target,
|
|
258
|
+
relation: attrs.relation,
|
|
259
|
+
status: attrs.status,
|
|
260
|
+
in: attrs.in,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
nodes,
|
|
266
|
+
edges: edges.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target)),
|
|
267
|
+
hits: hits.filter((id) => kept.has(id)),
|
|
268
|
+
score: total(part, scores),
|
|
269
|
+
truncated: kept.size < part.size,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
//# sourceMappingURL=subgraph.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type AnyTool } from '@zenera/neo';
|
|
2
|
+
import { type Format } from '../present.ts';
|
|
3
|
+
import type { SchemaIndex } from './search.ts';
|
|
4
|
+
export interface SchemaToolOptions {
|
|
5
|
+
/** what `format` defaults to when the model does not say */
|
|
6
|
+
format?: Format;
|
|
7
|
+
docs?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function schemaTools<TCtx = unknown>(index: SchemaIndex, options?: SchemaToolOptions): AnyTool<TCtx>[];
|
|
10
|
+
//# sourceMappingURL=tools.d.ts.map
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { tool } from '@zenera/neo';
|
|
2
|
+
import { FORMATS, isFormat, present } from "../present.js";
|
|
3
|
+
import { isEmpty, parseQuery, QueryError } from "../query.js";
|
|
4
|
+
import { toTypeScript } from "./hydrate.js";
|
|
5
|
+
import { stitch } from "./subgraph.js";
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// The same index, given to an agent
|
|
8
|
+
//
|
|
9
|
+
// Four tools over one engine. Three of them search, and the fourth deliberately
|
|
10
|
+
// does not: `find_types_with_property` is a graph lookup, for the moment after
|
|
11
|
+
// the compiler says `'password' does not exist in type 'PublicUserProfile'`.
|
|
12
|
+
// At that point the model does not need to be reminded what a password is —
|
|
13
|
+
// it needs the list of types that have one, and an embedding of the word will
|
|
14
|
+
// only rank the guess it already made near the top again.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
const GROUP = 'schema';
|
|
17
|
+
/** Kept small on purpose: a tool result is prompt, and the model asked for one thing. */
|
|
18
|
+
const DEFAULT_LIMIT = 4;
|
|
19
|
+
const DEFAULT_MAX_NODES = 60;
|
|
20
|
+
const MAX_CANDIDATES = 25;
|
|
21
|
+
export function schemaTools(index, options = {}) {
|
|
22
|
+
const fallback = options.format ?? 'text';
|
|
23
|
+
const docs = options.docs ?? true;
|
|
24
|
+
const searchApi = tool({
|
|
25
|
+
name: 'search_api',
|
|
26
|
+
group: GROUP,
|
|
27
|
+
description: 'Searches the API description and answers with the connected piece of it that ' +
|
|
28
|
+
'matched: the operations, the schemas they carry and the fields inside them. ' +
|
|
29
|
+
'Put the intent in the field that matches what is wanted — a request field in ' +
|
|
30
|
+
'input_properties, a response field in output_properties — rather than putting ' +
|
|
31
|
+
'everything in `all`, which cannot filter. Pass the ids from a previous answer ' +
|
|
32
|
+
'in exclude_ids to be shown something new instead of the same thing again.',
|
|
33
|
+
parameters: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
all: list('Free search over operations, schemas and fields alike.'),
|
|
37
|
+
methods: list('What the call does, e.g. "reset a user password".'),
|
|
38
|
+
types: list('What the schema is, e.g. "a billing invoice".'),
|
|
39
|
+
input_types: list('Schemas a call accepts.'),
|
|
40
|
+
output_types: list('Schemas a call returns.'),
|
|
41
|
+
properties: list('Fields or parameters, either side.'),
|
|
42
|
+
input_properties: list('Fields in a request body, or query/path parameters.'),
|
|
43
|
+
output_properties: list('Fields in a response body.'),
|
|
44
|
+
direction: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
enum: ['input', 'output', 'any'],
|
|
47
|
+
description: 'Which side `types` and `properties` are read on.',
|
|
48
|
+
},
|
|
49
|
+
method_type: {
|
|
50
|
+
type: 'string',
|
|
51
|
+
enum: ['read_only', 'read_write', 'any'],
|
|
52
|
+
description: 'read_only is GET/HEAD/OPTIONS; read_write is everything else.',
|
|
53
|
+
},
|
|
54
|
+
exclude_ids: list('Node ids already seen, as printed in an earlier answer.'),
|
|
55
|
+
exclude_methods: list('Operation names to leave out.'),
|
|
56
|
+
exclude_types: list('Schema names to leave out.'),
|
|
57
|
+
exclude_properties: list('Field names to leave out.'),
|
|
58
|
+
limit: {
|
|
59
|
+
type: 'integer',
|
|
60
|
+
description: `Results per phrase. Default ${DEFAULT_LIMIT}.`,
|
|
61
|
+
},
|
|
62
|
+
max_nodes: {
|
|
63
|
+
type: 'integer',
|
|
64
|
+
description: `Largest answer, in nodes. Default ${DEFAULT_MAX_NODES}.`,
|
|
65
|
+
},
|
|
66
|
+
format: {
|
|
67
|
+
type: 'string',
|
|
68
|
+
enum: [...FORMATS],
|
|
69
|
+
description: `How to write it. Default ${fallback}. Use "ts" to get types to code against.`,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
additionalProperties: false,
|
|
73
|
+
},
|
|
74
|
+
execute: async (args) => {
|
|
75
|
+
const { format, ...rest } = args;
|
|
76
|
+
let query;
|
|
77
|
+
try {
|
|
78
|
+
query = parseQuery(rest);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
return {
|
|
82
|
+
error: err instanceof QueryError ? err.message : String(err),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (isEmpty(query)) {
|
|
86
|
+
return { error: 'nothing was asked for', hint: 'fill at least one search field' };
|
|
87
|
+
}
|
|
88
|
+
const result = await index.search({
|
|
89
|
+
limit: DEFAULT_LIMIT,
|
|
90
|
+
max_nodes: DEFAULT_MAX_NODES,
|
|
91
|
+
...query,
|
|
92
|
+
});
|
|
93
|
+
if (result.subgraphs.length === 0) {
|
|
94
|
+
return { found: 0, hint: 'try fewer words, or `all` instead of a narrower field' };
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
found: result.subgraphs.length,
|
|
98
|
+
ids: result.subgraphs.flatMap((s) => s.hits),
|
|
99
|
+
truncated: result.subgraphs.some((s) => s.truncated),
|
|
100
|
+
api: await present(index, result.subgraphs, chosen(format, fallback), { docs }),
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
const describeTypes = tool({
|
|
105
|
+
name: 'describe_types',
|
|
106
|
+
group: GROUP,
|
|
107
|
+
description: 'Prints named schemas as TypeScript declarations, together with everything they ' +
|
|
108
|
+
'refer to, so the result compiles on its own. Use it once search has named a ' +
|
|
109
|
+
'schema and the exact fields are what is needed.',
|
|
110
|
+
parameters: {
|
|
111
|
+
type: 'object',
|
|
112
|
+
properties: {
|
|
113
|
+
names: list('Schema names, e.g. ["ResetPasswordPayload"]. Not node ids.'),
|
|
114
|
+
only: list('Restrict every schema to these field names.'),
|
|
115
|
+
},
|
|
116
|
+
required: ['names'],
|
|
117
|
+
additionalProperties: false,
|
|
118
|
+
},
|
|
119
|
+
execute: async ({ names, only }) => {
|
|
120
|
+
const known = names.filter((name) => index.graph.hasNode(`Type:${name}`));
|
|
121
|
+
const missing = names.filter((name) => !known.includes(name));
|
|
122
|
+
if (known.length === 0) {
|
|
123
|
+
return {
|
|
124
|
+
error: `no such schema: ${names.join(', ')}`,
|
|
125
|
+
hint: 'search for it first',
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const schemas = await index.schemas();
|
|
129
|
+
const sub = subgraphOf(index, known.map((name) => `Type:${name}`));
|
|
130
|
+
const code = toTypeScript(sub, schemas, { docs, onlyHits: false });
|
|
131
|
+
return {
|
|
132
|
+
typescript: only?.length ? narrow(code, only) : code,
|
|
133
|
+
...(missing.length > 0 ? { missing } : {}),
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
const findTypesWithProperty = tool({
|
|
138
|
+
name: 'find_types_with_property',
|
|
139
|
+
group: GROUP,
|
|
140
|
+
description: 'Lists every schema that has a field of this name. Exact lookup, no searching: ' +
|
|
141
|
+
'reach for it when a field was put on the wrong type and the right one has to be ' +
|
|
142
|
+
'found, for instance after a compiler error saying the property does not exist.',
|
|
143
|
+
parameters: {
|
|
144
|
+
type: 'object',
|
|
145
|
+
properties: {
|
|
146
|
+
property: { type: 'string', description: 'The field name, e.g. "password".' },
|
|
147
|
+
direction: {
|
|
148
|
+
type: 'string',
|
|
149
|
+
enum: ['input', 'output', 'any'],
|
|
150
|
+
description: 'Only schemas used on this side of a call.',
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
required: ['property'],
|
|
154
|
+
additionalProperties: false,
|
|
155
|
+
},
|
|
156
|
+
execute: async ({ property, direction }) => {
|
|
157
|
+
const wanted = property.toLowerCase();
|
|
158
|
+
const candidates = [];
|
|
159
|
+
index.graph.forEachNode((id, a) => {
|
|
160
|
+
if (a.kind !== 'property' || a.name.toLowerCase() !== wanted || !a.parent) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const owner = index.graph.getNodeAttributes(`Type:${a.parent}`) ?? a;
|
|
164
|
+
if (direction && direction !== 'any' && !onSide(owner.direction, direction)) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
candidates.push({
|
|
168
|
+
type: a.parent,
|
|
169
|
+
id,
|
|
170
|
+
signature: a.signature,
|
|
171
|
+
required: a.required,
|
|
172
|
+
direction: owner.direction,
|
|
173
|
+
doc: a.doc || owner.doc,
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
return candidates.length === 0
|
|
177
|
+
? { found: 0, hint: 'try search_api with the field in input_properties' }
|
|
178
|
+
: { found: candidates.length, candidates: candidates.slice(0, MAX_CANDIDATES) };
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
const listMethods = tool({
|
|
182
|
+
name: 'list_methods',
|
|
183
|
+
group: GROUP,
|
|
184
|
+
description: 'Lists operations by path, with no searching. Use it to see the shape of the API ' +
|
|
185
|
+
'before deciding what to ask for.',
|
|
186
|
+
parameters: {
|
|
187
|
+
type: 'object',
|
|
188
|
+
properties: {
|
|
189
|
+
contains: { type: 'string', description: 'Only paths holding this text.' },
|
|
190
|
+
method_type: {
|
|
191
|
+
type: 'string',
|
|
192
|
+
enum: ['read_only', 'read_write', 'any'],
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
additionalProperties: false,
|
|
196
|
+
},
|
|
197
|
+
execute: async ({ contains, method_type }) => {
|
|
198
|
+
const needle = contains?.toLowerCase();
|
|
199
|
+
const rows = [];
|
|
200
|
+
index.graph.forEachNode((_id, a) => {
|
|
201
|
+
if (a.kind !== 'method') {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (needle && !a.path.toLowerCase().includes(needle)) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (method_type && method_type !== 'any' && a.methodType !== method_type) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
rows.push(`${a.httpMethod} ${a.path} ${a.name}${a.doc ? ` — ${a.doc}` : ''}`);
|
|
211
|
+
});
|
|
212
|
+
return { found: rows.length, methods: rows.sort() };
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
return [searchApi, describeTypes, findTypesWithProperty, listMethods];
|
|
216
|
+
}
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
function list(description) {
|
|
219
|
+
return { type: 'array', items: { type: 'string' }, description };
|
|
220
|
+
}
|
|
221
|
+
function chosen(format, fallback) {
|
|
222
|
+
return format && isFormat(format) ? format : fallback;
|
|
223
|
+
}
|
|
224
|
+
function onSide(direction, wanted) {
|
|
225
|
+
return direction === wanted || direction === 'both';
|
|
226
|
+
}
|
|
227
|
+
function subgraphOf(index, ids) {
|
|
228
|
+
const [first] = stitch(index.graph, ids.map((id) => ({ id, term: id, field: 'describe', score: 1 })), { maxNodes: DEFAULT_MAX_NODES * ids.length });
|
|
229
|
+
return first ?? { nodes: [], edges: [], hits: [], score: 0, truncated: false };
|
|
230
|
+
}
|
|
231
|
+
/** Keeps the declarations, drops the field lines nobody asked about. */
|
|
232
|
+
function narrow(code, only) {
|
|
233
|
+
const wanted = new Set(only);
|
|
234
|
+
return code
|
|
235
|
+
.split('\n')
|
|
236
|
+
.filter((line) => {
|
|
237
|
+
const field = /^\s{4}'?([A-Za-z0-9_$-]+)'?\??:/.exec(line);
|
|
238
|
+
return !field || wanted.has(field[1]);
|
|
239
|
+
})
|
|
240
|
+
.join('\n');
|
|
241
|
+
}
|
|
242
|
+
//# sourceMappingURL=tools.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Schema } from './schema.ts';
|
|
2
|
+
export interface PrintOptions {
|
|
3
|
+
/** emit `/** … *\/` above types and properties */
|
|
4
|
+
docs?: boolean;
|
|
5
|
+
/** truncate one comment to this many characters */
|
|
6
|
+
maxDoc?: number;
|
|
7
|
+
/** when given, only these properties of an object are printed */
|
|
8
|
+
only?: ReadonlySet<string>;
|
|
9
|
+
}
|
|
10
|
+
export declare class Printer {
|
|
11
|
+
#private;
|
|
12
|
+
constructor(types: Readonly<Record<string, Schema>>);
|
|
13
|
+
/** The TypeScript identifier a type id is printed as. */
|
|
14
|
+
identifier(id: string): string;
|
|
15
|
+
/** An inline type expression — what goes to the right of a colon. */
|
|
16
|
+
signature(schema: unknown, depth?: number): string;
|
|
17
|
+
/** A whole `export interface` / `export type` declaration for one id. */
|
|
18
|
+
declaration(id: string, options?: PrintOptions): string;
|
|
19
|
+
}
|
|
20
|
+
/** A key that is not a plain identifier has to be quoted. */
|
|
21
|
+
export declare function property(key: string): string;
|
|
22
|
+
//# sourceMappingURL=typescript.d.ts.map
|