@zenera/rag 1.1.5 → 1.1.8
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/README.md +68 -6
- package/dist/command.js +41 -674
- package/dist/common/embedder.d.ts +3 -0
- package/dist/common/embedder.js +64 -0
- package/dist/common/locate.d.ts +18 -0
- package/dist/common/locate.js +155 -0
- package/dist/common/manifest.d.ts +50 -0
- package/dist/common/manifest.js +62 -0
- package/dist/{schema → common}/match.d.ts +4 -0
- package/dist/{schema → common}/match.js +7 -0
- package/dist/common/progress.d.ts +57 -0
- package/dist/common/progress.js +155 -0
- package/dist/common/prose.d.ts +13 -0
- package/dist/common/prose.js +56 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +6 -3
- package/dist/schema/build.js +6 -2
- package/dist/schema/command.d.ts +3 -0
- package/dist/schema/command.js +819 -0
- package/dist/schema/files.d.ts +5 -27
- package/dist/schema/files.js +9 -29
- package/dist/schema/lookup.d.ts +12 -2
- package/dist/schema/lookup.js +29 -2
- package/dist/{present.d.ts → schema/present.d.ts} +5 -5
- package/dist/{present.js → schema/present.js} +2 -2
- package/dist/{query.d.ts → schema/query.d.ts} +1 -1
- package/dist/schema/readme.d.ts +6 -0
- package/dist/schema/readme.js +122 -0
- package/dist/schema/render.d.ts +8 -0
- package/dist/schema/render.js +12 -2
- package/dist/{repl.d.ts → schema/repl.d.ts} +1 -1
- package/dist/schema/search.js +2 -1
- package/dist/schema/tools.d.ts +3 -1
- package/dist/schema/tools.js +152 -19
- package/dist/schema/trace.d.ts +52 -0
- package/dist/schema/trace.js +144 -0
- package/package.json +3 -3
- package/dist/schema/progress.d.ts +0 -26
- package/dist/schema/progress.js +0 -316
- /package/dist/{query.js → schema/query.js} +0 -0
- /package/dist/{repl.js → schema/repl.js} +0 -0
package/dist/schema/tools.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { tool } from '@zenera/neo';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { loose, matcher, PatternError } from "../common/match.js";
|
|
3
|
+
import { FORMATS, isFormat, present } from "./present.js";
|
|
4
|
+
import { isEmpty, parseQuery, QueryError } from "./query.js";
|
|
4
5
|
import { toTypeScript } from "./hydrate.js";
|
|
5
6
|
import { fields, grepNodes, listNodes, propertyCount } from "./lookup.js";
|
|
6
|
-
import {
|
|
7
|
+
import { sourceTag } from "./render.js";
|
|
7
8
|
import { stitch } from "./subgraph.js";
|
|
9
|
+
import { chainOf, traceNodes } from "./trace.js";
|
|
8
10
|
// ---------------------------------------------------------------------------
|
|
9
11
|
// The same index, given to an agent
|
|
10
12
|
//
|
|
11
|
-
//
|
|
12
|
-
// is the way in when the question is vague; the other
|
|
13
|
+
// Six tools over one engine, and only one of them ranks anything. `search_api`
|
|
14
|
+
// is the way in when the question is vague; the other five are exact, because
|
|
13
15
|
// a model that has been told "no results" by a vector search has learned
|
|
14
16
|
// nothing — a ranking returns the top of a list, so an empty answer and an
|
|
15
17
|
// absent thing look identical.
|
|
@@ -19,6 +21,12 @@ import { stitch } from "./subgraph.js";
|
|
|
19
21
|
// model does not need to be reminded what a password is — it needs the list of
|
|
20
22
|
// types that have one. `grep_api` is the same instinct widened: every literal
|
|
21
23
|
// occurrence, counted in full, so "it is not there" can actually be concluded.
|
|
24
|
+
//
|
|
25
|
+
// `trace_api` is the other direction entirely. Having found a field, the next
|
|
26
|
+
// question is always which call carries it. `search_api` stitches part of the
|
|
27
|
+
// way there, but only between the nodes that ranked — and the operation and
|
|
28
|
+
// the field usually share no word at all, which is why the edge between them
|
|
29
|
+
// was built. `trace_api` follows that edge instead of ranking anything.
|
|
22
30
|
// ---------------------------------------------------------------------------
|
|
23
31
|
const GROUP = 'schema';
|
|
24
32
|
/** Kept small on purpose: a tool result is prompt, and the model asked for one thing. */
|
|
@@ -28,9 +36,17 @@ const MAX_CANDIDATES = 25;
|
|
|
28
36
|
/** A listing is lines rather than subgraphs, so it can afford more of them. */
|
|
29
37
|
const DEFAULT_ROWS = 50;
|
|
30
38
|
const MAX_ROWS = 200;
|
|
39
|
+
/** A trace is a paragraph per node, so fewer of them, and fewer routes each. */
|
|
40
|
+
const DEFAULT_TRACES = 5;
|
|
41
|
+
const MAX_TRACES = 25;
|
|
42
|
+
const DEFAULT_ROUTES = 10;
|
|
31
43
|
export function schemaTools(index, options = {}) {
|
|
32
44
|
const fallback = options.format ?? 'text';
|
|
33
45
|
const docs = options.docs ?? true;
|
|
46
|
+
// With one document there is nothing to disambiguate and naming it on every
|
|
47
|
+
// line is prompt spent saying the same word; with several it is the only
|
|
48
|
+
// way to tell two revisions of one API apart.
|
|
49
|
+
const source = options.source ?? index.manifest.sources.length > 1;
|
|
34
50
|
const searchApi = tool({
|
|
35
51
|
name: 'search_api',
|
|
36
52
|
group: GROUP,
|
|
@@ -107,7 +123,10 @@ export function schemaTools(index, options = {}) {
|
|
|
107
123
|
found: result.subgraphs.length,
|
|
108
124
|
ids: result.subgraphs.flatMap((s) => s.hits),
|
|
109
125
|
truncated: result.subgraphs.some((s) => s.truncated),
|
|
110
|
-
api: await present(index, result.subgraphs, chosen(format, fallback), {
|
|
126
|
+
api: await present(index, result.subgraphs, chosen(format, fallback), {
|
|
127
|
+
docs,
|
|
128
|
+
source,
|
|
129
|
+
}),
|
|
111
130
|
};
|
|
112
131
|
},
|
|
113
132
|
});
|
|
@@ -209,12 +228,24 @@ export function schemaTools(index, options = {}) {
|
|
|
209
228
|
description: 'Match the name. A plain word matches anywhere in it; use * and ? ' +
|
|
210
229
|
'to match the whole name, e.g. "*Password*".',
|
|
211
230
|
},
|
|
212
|
-
path: {
|
|
231
|
+
path: {
|
|
232
|
+
type: 'string',
|
|
233
|
+
description: 'Match the route, e.g. "/users*". Keeps operations and their ' +
|
|
234
|
+
'parameters; a schema sits on no one route, so it is left out.',
|
|
235
|
+
},
|
|
236
|
+
regex: {
|
|
237
|
+
type: 'boolean',
|
|
238
|
+
description: 'Read `name` and `path` as regular expressions instead.',
|
|
239
|
+
},
|
|
213
240
|
method_type: {
|
|
214
241
|
type: 'string',
|
|
215
242
|
enum: ['read_only', 'read_write', 'any'],
|
|
216
243
|
},
|
|
217
244
|
direction: { type: 'string', enum: ['input', 'output', 'any'] },
|
|
245
|
+
source: {
|
|
246
|
+
type: 'string',
|
|
247
|
+
description: 'Only this document, when the index holds more than one.',
|
|
248
|
+
},
|
|
218
249
|
limit: {
|
|
219
250
|
type: 'integer',
|
|
220
251
|
description: `Rows to return. Default ${DEFAULT_ROWS}.`,
|
|
@@ -222,7 +253,7 @@ export function schemaTools(index, options = {}) {
|
|
|
222
253
|
},
|
|
223
254
|
additionalProperties: false,
|
|
224
255
|
},
|
|
225
|
-
execute: async ({ kind, name, path, method_type, direction, limit }) => {
|
|
256
|
+
execute: async ({ kind, name, path, regex, method_type, direction, source: only, limit, }) => {
|
|
226
257
|
const subject = SUBJECTS[kind ?? 'methods'];
|
|
227
258
|
if (!subject) {
|
|
228
259
|
return { error: `cannot list "${kind}"`, hint: 'kind is methods, types or fields' };
|
|
@@ -231,8 +262,9 @@ export function schemaTools(index, options = {}) {
|
|
|
231
262
|
try {
|
|
232
263
|
result = listNodes(index.graph, {
|
|
233
264
|
kind: subject,
|
|
234
|
-
name: name ? [loose(name)] : undefined,
|
|
235
|
-
path: path ? [loose(path)] : undefined,
|
|
265
|
+
name: name ? [loose(name, { regex })] : undefined,
|
|
266
|
+
path: path ? [loose(path, { regex })] : undefined,
|
|
267
|
+
source: only,
|
|
236
268
|
methodType: enumerated(method_type),
|
|
237
269
|
direction: enumerated(direction),
|
|
238
270
|
limit: Math.min(limit ?? DEFAULT_ROWS, MAX_ROWS),
|
|
@@ -244,7 +276,7 @@ export function schemaTools(index, options = {}) {
|
|
|
244
276
|
return {
|
|
245
277
|
found: result.found,
|
|
246
278
|
truncated: result.truncated,
|
|
247
|
-
[PLURALS[subject]]: result.rows.map((r) => line(index.graph, subject, r)),
|
|
279
|
+
[PLURALS[subject]]: result.rows.map((r) => line(index.graph, subject, r, source)),
|
|
248
280
|
};
|
|
249
281
|
},
|
|
250
282
|
});
|
|
@@ -255,7 +287,8 @@ export function schemaTools(index, options = {}) {
|
|
|
255
287
|
'operations, schemas and fields alike. No embeddings and no ranking, so ' +
|
|
256
288
|
'nothing is missed for being an unusual word or an odd spelling. This is the ' +
|
|
257
289
|
'tool for "does X exist anywhere", and for checking that a search which ' +
|
|
258
|
-
'returned nothing really means there is nothing.'
|
|
290
|
+
'returned nothing really means there is nothing. Narrow it with `path` or ' +
|
|
291
|
+
'`name` when the word is common and only one corner of the API is meant.',
|
|
259
292
|
parameters: {
|
|
260
293
|
type: 'object',
|
|
261
294
|
properties: {
|
|
@@ -268,6 +301,18 @@ export function schemaTools(index, options = {}) {
|
|
|
268
301
|
description: 'Read the pattern as a regular expression instead.',
|
|
269
302
|
},
|
|
270
303
|
kind: { type: 'string', enum: ['method', 'type', 'property'] },
|
|
304
|
+
name: {
|
|
305
|
+
type: 'string',
|
|
306
|
+
description: 'Only nodes whose own name matches this. * and ? allowed.',
|
|
307
|
+
},
|
|
308
|
+
path: {
|
|
309
|
+
type: 'string',
|
|
310
|
+
description: 'Only what sits on a matching route, e.g. "/users*".',
|
|
311
|
+
},
|
|
312
|
+
source: {
|
|
313
|
+
type: 'string',
|
|
314
|
+
description: 'Only this document, when the index holds more than one.',
|
|
315
|
+
},
|
|
271
316
|
limit: {
|
|
272
317
|
type: 'integer',
|
|
273
318
|
description: `Matches to return. Default ${DEFAULT_ROWS}. \`found\` always counts them all.`,
|
|
@@ -276,11 +321,14 @@ export function schemaTools(index, options = {}) {
|
|
|
276
321
|
required: ['pattern'],
|
|
277
322
|
additionalProperties: false,
|
|
278
323
|
},
|
|
279
|
-
execute: async ({ pattern, regex, kind, limit }) => {
|
|
324
|
+
execute: async ({ pattern, regex, kind, name, path, source: only, limit }) => {
|
|
280
325
|
let result;
|
|
281
326
|
try {
|
|
282
327
|
result = grepNodes(index.graph, matcher(pattern, { regex }), {
|
|
283
328
|
kinds: kind ? [kind] : undefined,
|
|
329
|
+
name: name ? [loose(name)] : undefined,
|
|
330
|
+
path: path ? [loose(path)] : undefined,
|
|
331
|
+
source: only,
|
|
284
332
|
limit: Math.min(limit ?? DEFAULT_ROWS, MAX_ROWS),
|
|
285
333
|
});
|
|
286
334
|
}
|
|
@@ -296,11 +344,95 @@ export function schemaTools(index, options = {}) {
|
|
|
296
344
|
return {
|
|
297
345
|
found: result.found,
|
|
298
346
|
truncated: result.truncated,
|
|
299
|
-
matches: result.matches.map((m) => ({
|
|
347
|
+
matches: result.matches.map((m) => ({
|
|
348
|
+
id: m.id,
|
|
349
|
+
...(source ? { source: m.attributes.source } : {}),
|
|
350
|
+
text: m.text,
|
|
351
|
+
})),
|
|
352
|
+
};
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
const traceApi = tool({
|
|
356
|
+
name: 'trace_api',
|
|
357
|
+
group: GROUP,
|
|
358
|
+
description: 'Answers "which calls can reach this?" for a field or a schema: walks up the ' +
|
|
359
|
+
'graph from everything of that name to the operations that accept or return it, ' +
|
|
360
|
+
'and gives the chain in between. Use it whenever a field has been found and the ' +
|
|
361
|
+
'endpoint to call is what is actually wanted — searching for the operation will ' +
|
|
362
|
+
'not work, because a call almost never repeats the name of a field nested inside ' +
|
|
363
|
+
'its body. An empty answer means nothing in the API carries it.',
|
|
364
|
+
parameters: {
|
|
365
|
+
type: 'object',
|
|
366
|
+
properties: {
|
|
367
|
+
of: {
|
|
368
|
+
type: 'string',
|
|
369
|
+
description: 'The field or schema name to trace up from, or a node id such as ' +
|
|
370
|
+
'"Type:User". A plain word matches anywhere in the name; * and ? ' +
|
|
371
|
+
'match the whole of it.',
|
|
372
|
+
},
|
|
373
|
+
kind: {
|
|
374
|
+
type: 'string',
|
|
375
|
+
enum: ['type', 'property'],
|
|
376
|
+
description: 'Only trace from schemas, or only from fields.',
|
|
377
|
+
},
|
|
378
|
+
direction: {
|
|
379
|
+
type: 'string',
|
|
380
|
+
enum: ['input', 'output', 'any'],
|
|
381
|
+
description: 'Keep only the calls that accept it, or that return it.',
|
|
382
|
+
},
|
|
383
|
+
source: {
|
|
384
|
+
type: 'string',
|
|
385
|
+
description: 'Only this document, when the index holds more than one.',
|
|
386
|
+
},
|
|
387
|
+
limit: {
|
|
388
|
+
type: 'integer',
|
|
389
|
+
description: `Nodes to trace from. Default ${DEFAULT_TRACES}.`,
|
|
390
|
+
},
|
|
391
|
+
routes: {
|
|
392
|
+
type: 'integer',
|
|
393
|
+
description: `Operations per node. Default ${DEFAULT_ROUTES}.`,
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
required: ['of'],
|
|
397
|
+
additionalProperties: false,
|
|
398
|
+
},
|
|
399
|
+
execute: async ({ of: wanted, kind, direction, source: only, limit, routes }) => {
|
|
400
|
+
let result;
|
|
401
|
+
try {
|
|
402
|
+
result = traceNodes(index.graph, {
|
|
403
|
+
ids: index.graph.hasNode(wanted) ? [wanted] : undefined,
|
|
404
|
+
kinds: kind ? [kind] : undefined,
|
|
405
|
+
name: index.graph.hasNode(wanted) ? undefined : [loose(wanted)],
|
|
406
|
+
source: only,
|
|
407
|
+
limit: Math.min(limit ?? DEFAULT_TRACES, MAX_TRACES),
|
|
408
|
+
maxRoutes: Math.min(routes ?? DEFAULT_ROUTES, MAX_ROWS),
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
catch (err) {
|
|
412
|
+
return { error: err instanceof PatternError ? err.message : String(err) };
|
|
413
|
+
}
|
|
414
|
+
if (result.found === 0) {
|
|
415
|
+
return {
|
|
416
|
+
found: 0,
|
|
417
|
+
hint: `nothing in the API is called "${wanted}" — try grep_api for it`,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
const side = enumerated(direction);
|
|
421
|
+
return {
|
|
422
|
+
found: result.found,
|
|
423
|
+
truncated: result.truncated,
|
|
424
|
+
traced: result.traces.map((t) => ({
|
|
425
|
+
id: t.id,
|
|
426
|
+
reached_by: t.found,
|
|
427
|
+
operations: t.routes
|
|
428
|
+
.filter((r) => !side || r.direction === side || r.direction === 'both')
|
|
429
|
+
.map((r) => `${r.attributes.httpMethod} ${r.attributes.path} ` +
|
|
430
|
+
`${r.attributes.name} (${r.direction})${source ? ` ${sourceTag(r.attributes.source)}` : ''} ${chainOf(index.graph, t.id, r)}`),
|
|
431
|
+
})),
|
|
300
432
|
};
|
|
301
433
|
},
|
|
302
434
|
});
|
|
303
|
-
return [searchApi, describeTypes, findTypesWithProperty, listApi, grepApi];
|
|
435
|
+
return [searchApi, describeTypes, findTypesWithProperty, listApi, grepApi, traceApi];
|
|
304
436
|
}
|
|
305
437
|
// ---------------------------------------------------------------------------
|
|
306
438
|
const SUBJECTS = {
|
|
@@ -315,16 +447,17 @@ const PLURALS = {
|
|
|
315
447
|
property: 'properties',
|
|
316
448
|
};
|
|
317
449
|
/** One row, as the line a model reads rather than an object it has to walk. */
|
|
318
|
-
function line(graph, kind, row) {
|
|
450
|
+
function line(graph, kind, row, source = false) {
|
|
451
|
+
const from = source ? ` ${sourceTag(row.source)}` : '';
|
|
319
452
|
if (kind === 'method') {
|
|
320
|
-
return `${row.httpMethod} ${row.path} ${row.name}${row.doc ? ` — ${row.doc}` : ''}`;
|
|
453
|
+
return `${row.httpMethod} ${row.path} ${row.name}${from}${row.doc ? ` — ${row.doc}` : ''}`;
|
|
321
454
|
}
|
|
322
455
|
if (kind === 'type') {
|
|
323
456
|
const side = row.direction === 'none' ? '' : ` (${row.direction})`;
|
|
324
|
-
return `${row.name}${side} ${fields(propertyCount(graph, row.id))}${row.doc ? ` — ${row.doc}` : ''}`;
|
|
457
|
+
return `${row.name}${side} ${fields(propertyCount(graph, row.id))}${from}${row.doc ? ` — ${row.doc}` : ''}`;
|
|
325
458
|
}
|
|
326
459
|
const owner = row.parent ? `${row.parent}.` : '';
|
|
327
|
-
return `${owner}${row.name}${row.required ? '' : '?'}: ${row.signature || 'unknown'}`;
|
|
460
|
+
return `${owner}${row.name}${row.required ? '' : '?'}: ${row.signature || 'unknown'}${from}`;
|
|
328
461
|
}
|
|
329
462
|
const enumerated = (value) => value && value !== 'any' ? value : undefined;
|
|
330
463
|
function list(description) {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Matcher } from '../common/match.ts';
|
|
2
|
+
import type { ApiGraph, NodeAttrs, NodeKind } from './graph.ts';
|
|
3
|
+
export type Side = 'input' | 'output' | 'both';
|
|
4
|
+
export interface Route {
|
|
5
|
+
/** the operation node */
|
|
6
|
+
id: string;
|
|
7
|
+
attributes: NodeAttrs;
|
|
8
|
+
/** whether the call accepts what was traced, returns it, or both */
|
|
9
|
+
direction: Side;
|
|
10
|
+
/** the nodes in between, nearest the operation first */
|
|
11
|
+
via: string[];
|
|
12
|
+
hops: number;
|
|
13
|
+
}
|
|
14
|
+
export interface Trace {
|
|
15
|
+
id: string;
|
|
16
|
+
attributes: NodeAttrs;
|
|
17
|
+
/** how many operations reach it, whatever was kept */
|
|
18
|
+
found: number;
|
|
19
|
+
routes: Route[];
|
|
20
|
+
truncated: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface Traced {
|
|
23
|
+
/** how many nodes were traced, whatever was kept */
|
|
24
|
+
found: number;
|
|
25
|
+
traces: Trace[];
|
|
26
|
+
truncated: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface TraceFilter {
|
|
29
|
+
/** node ids to start from, taken as given */
|
|
30
|
+
ids?: readonly string[];
|
|
31
|
+
/** which kinds a `name` may match; all three when unset */
|
|
32
|
+
kinds?: readonly NodeKind[];
|
|
33
|
+
name?: readonly Matcher[];
|
|
34
|
+
source?: string;
|
|
35
|
+
/** starting nodes kept */
|
|
36
|
+
limit?: number;
|
|
37
|
+
/** routes kept per starting node */
|
|
38
|
+
maxRoutes?: number;
|
|
39
|
+
maxHops?: number;
|
|
40
|
+
}
|
|
41
|
+
/** How far containment is read backwards before a route is too indirect to mean anything. */
|
|
42
|
+
export declare const DEFAULT_TRACE_HOPS = 8;
|
|
43
|
+
export declare function traceNodes(graph: ApiGraph, filter: TraceFilter): Traced;
|
|
44
|
+
export declare function traceOne(graph: ApiGraph, id: string, filter?: TraceFilter): Trace;
|
|
45
|
+
/**
|
|
46
|
+
* The chain from an operation down to the node that was traced, as one line.
|
|
47
|
+
* A field is written onto the type above it (`User.email`) and a parameter is
|
|
48
|
+
* marked (`?page_size`), so the shape of the call can be read off the route
|
|
49
|
+
* without going and looking any of it up.
|
|
50
|
+
*/
|
|
51
|
+
export declare function chainOf(graph: ApiGraph, start: string, route: Route): string;
|
|
52
|
+
//# sourceMappingURL=trace.d.ts.map
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { listNodes } from "./lookup.js";
|
|
2
|
+
/** How far containment is read backwards before a route is too indirect to mean anything. */
|
|
3
|
+
export const DEFAULT_TRACE_HOPS = 8;
|
|
4
|
+
/** A walk is bounded; a document can always be larger than the one this was written against. */
|
|
5
|
+
const MAX_VISITS = 20_000;
|
|
6
|
+
/**
|
|
7
|
+
* What a bare pattern matches. Operations are left out on purpose: they are
|
|
8
|
+
* where a trace ends, so starting one at an operationId that happens to share
|
|
9
|
+
* a word adds a row saying only that it found itself. Naming a `Method:` id
|
|
10
|
+
* outright still works.
|
|
11
|
+
*/
|
|
12
|
+
const KINDS = ['type', 'property'];
|
|
13
|
+
/** The edges an operation holds its payload by. Reaching one of these is arriving. */
|
|
14
|
+
const ENTRY = new Set(['TAKES_INPUT', 'RETURNS_OUTPUT', 'HAS_PARAM']);
|
|
15
|
+
/** Containment, read backwards: a type to the fields that hold it, a field to its owner. */
|
|
16
|
+
const UPWARD = new Set(['HAS_PROPERTY', 'OF_TYPE', 'COMPOSES', 'ITEM_OF']);
|
|
17
|
+
export function traceNodes(graph, filter) {
|
|
18
|
+
const starts = select(graph, filter);
|
|
19
|
+
const kept = filter.limit && filter.limit < starts.length ? starts.slice(0, filter.limit) : starts;
|
|
20
|
+
return {
|
|
21
|
+
found: starts.length,
|
|
22
|
+
truncated: kept.length < starts.length,
|
|
23
|
+
traces: kept.map((id) => traceOne(graph, id, filter)),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function traceOne(graph, id, filter = {}) {
|
|
27
|
+
const attributes = graph.getNodeAttributes(id);
|
|
28
|
+
// An operation is already where a trace ends; it reaches itself and nothing above it.
|
|
29
|
+
const routes = attributes.kind === 'method'
|
|
30
|
+
? [{ id, attributes, direction: 'both', via: [], hops: 0 }]
|
|
31
|
+
: climb(graph, id, filter.maxHops ?? DEFAULT_TRACE_HOPS);
|
|
32
|
+
const limit = filter.maxRoutes;
|
|
33
|
+
return {
|
|
34
|
+
id,
|
|
35
|
+
attributes,
|
|
36
|
+
found: routes.length,
|
|
37
|
+
routes: limit && limit < routes.length ? routes.slice(0, limit) : routes,
|
|
38
|
+
truncated: Boolean(limit && limit < routes.length),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The chain from an operation down to the node that was traced, as one line.
|
|
43
|
+
* A field is written onto the type above it (`User.email`) and a parameter is
|
|
44
|
+
* marked (`?page_size`), so the shape of the call can be read off the route
|
|
45
|
+
* without going and looking any of it up.
|
|
46
|
+
*/
|
|
47
|
+
export function chainOf(graph, start, route) {
|
|
48
|
+
let out = '';
|
|
49
|
+
for (const id of [...route.via, start]) {
|
|
50
|
+
const label = labelOf(graph, id);
|
|
51
|
+
out += out === '' || label.startsWith('.') ? label : ` → ${label}`;
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
function labelOf(graph, id) {
|
|
57
|
+
const a = graph.getNodeAttributes(id);
|
|
58
|
+
if (a.kind !== 'property') {
|
|
59
|
+
return a.name;
|
|
60
|
+
}
|
|
61
|
+
// A parameter belongs to a call rather than to a type, so it cannot be
|
|
62
|
+
// written as a field of the thing before it.
|
|
63
|
+
return id.includes('#') ? `?${a.name}` : `.${a.name}`;
|
|
64
|
+
}
|
|
65
|
+
function select(graph, filter) {
|
|
66
|
+
const out = new Set((filter.ids ?? []).filter((id) => graph.hasNode(id)));
|
|
67
|
+
if (filter.name) {
|
|
68
|
+
for (const kind of filter.kinds ?? KINDS) {
|
|
69
|
+
for (const row of listNodes(graph, { kind, name: filter.name, source: filter.source })
|
|
70
|
+
.rows) {
|
|
71
|
+
out.add(row.id);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return [...out].sort((a, b) => a.localeCompare(b));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Breadth-first up containment, recording an operation the moment one is
|
|
79
|
+
* reached. Breadth-first is what makes the answer the *shortest* way in: a
|
|
80
|
+
* type held by a wrapper held by a request body should be reported through
|
|
81
|
+
* the body, not through whichever branch happened to be walked first.
|
|
82
|
+
*/
|
|
83
|
+
function climb(graph, start, maxHops) {
|
|
84
|
+
const previous = new Map();
|
|
85
|
+
const seen = new Set([start]);
|
|
86
|
+
const found = new Map();
|
|
87
|
+
let frontier = [start];
|
|
88
|
+
for (let hop = 0; hop <= maxHops && frontier.length > 0 && seen.size < MAX_VISITS; hop++) {
|
|
89
|
+
const next = [];
|
|
90
|
+
for (const node of frontier) {
|
|
91
|
+
for (const edge of graph.inEdges(node)) {
|
|
92
|
+
const relation = graph.getEdgeAttribute(edge, 'relation');
|
|
93
|
+
const from = graph.source(edge);
|
|
94
|
+
if (ENTRY.has(relation)) {
|
|
95
|
+
arrive(found, {
|
|
96
|
+
id: from,
|
|
97
|
+
attributes: graph.getNodeAttributes(from),
|
|
98
|
+
direction: sideOf(relation),
|
|
99
|
+
via: upward(previous, start, node),
|
|
100
|
+
hops: hop + 1,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
else if (UPWARD.has(relation) && !seen.has(from)) {
|
|
104
|
+
seen.add(from);
|
|
105
|
+
previous.set(from, node);
|
|
106
|
+
next.push(from);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
frontier = next;
|
|
111
|
+
}
|
|
112
|
+
return [...found.values()].sort((a, b) => a.hops - b.hops ||
|
|
113
|
+
a.attributes.path.localeCompare(b.attributes.path) ||
|
|
114
|
+
a.attributes.httpMethod.localeCompare(b.attributes.httpMethod));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* One operation, once. A call that both accepts and returns the same type is
|
|
118
|
+
* two edges and one answer, and the honest word for that answer is `both`.
|
|
119
|
+
*/
|
|
120
|
+
function arrive(found, route) {
|
|
121
|
+
const existing = found.get(route.id);
|
|
122
|
+
if (!existing) {
|
|
123
|
+
found.set(route.id, route);
|
|
124
|
+
}
|
|
125
|
+
else if (existing.direction !== route.direction) {
|
|
126
|
+
existing.direction = 'both';
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const sideOf = (relation) => (relation === 'RETURNS_OUTPUT' ? 'output' : 'input');
|
|
130
|
+
/** The nodes walked through, from the one holding the entry edge back down towards the start. */
|
|
131
|
+
function upward(previous, start, from) {
|
|
132
|
+
const out = [];
|
|
133
|
+
let at = from;
|
|
134
|
+
while (at !== start) {
|
|
135
|
+
out.push(at);
|
|
136
|
+
const below = previous.get(at);
|
|
137
|
+
if (below === undefined) {
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
at = below;
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=trace.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenera/rag",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.8",
|
|
4
4
|
"description": "Retrieval over API descriptions: openapi/swagger documents as a searchable graph.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"@apidevtools/swagger-parser": "^12.0.0",
|
|
54
54
|
"@lancedb/lancedb": "^0.38.0",
|
|
55
55
|
"graphology": "^0.26.0",
|
|
56
|
-
"@zenera/cli": "^1.1.
|
|
57
|
-
"@zenera/neo": "^1.1.
|
|
56
|
+
"@zenera/cli": "^1.1.8",
|
|
57
|
+
"@zenera/neo": "^1.1.8"
|
|
58
58
|
}
|
|
59
59
|
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { Counts, Manifest } from './files.ts';
|
|
2
|
-
export declare const LOCK_FILE = ".lock";
|
|
3
|
-
export declare const README_FILE = "README.md";
|
|
4
|
-
export type Phase = 'reading' | 'graph' | 'embedding' | 'writing';
|
|
5
|
-
export interface BuildPlan {
|
|
6
|
-
/** where the index goes */
|
|
7
|
-
dir: string;
|
|
8
|
-
files: readonly string[];
|
|
9
|
-
/** the embedding reference as it was typed */
|
|
10
|
-
embedding: string;
|
|
11
|
-
indexer: string;
|
|
12
|
-
}
|
|
13
|
-
export interface Journal {
|
|
14
|
-
phase(name: Phase): void;
|
|
15
|
-
/** what the documents turned out to hold, once they have been read */
|
|
16
|
-
read(counts: Counts): void;
|
|
17
|
-
progress(done: number, total: number): void;
|
|
18
|
-
finish(manifest: Manifest): void;
|
|
19
|
-
fail(reason: unknown): void;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Takes the directory, or refuses it. Two builds writing one index would
|
|
23
|
-
* interleave their LanceDB writes and leave a store neither of them describes.
|
|
24
|
-
*/
|
|
25
|
-
export declare function beginBuild(plan: BuildPlan): Journal;
|
|
26
|
-
//# sourceMappingURL=progress.d.ts.map
|