@zenera/rag 1.1.4 → 1.1.6
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 +77 -4
- package/dist/command.js +334 -24
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/schema/entities.d.ts +2 -0
- package/dist/schema/entities.js +2 -1
- package/dist/schema/files.d.ts +7 -0
- package/dist/schema/files.js +18 -1
- package/dist/schema/locate.d.ts +20 -0
- package/dist/schema/locate.js +141 -0
- package/dist/schema/lookup.d.ts +54 -0
- package/dist/schema/lookup.js +110 -0
- package/dist/schema/match.d.ts +40 -0
- package/dist/schema/match.js +92 -0
- package/dist/schema/render.d.ts +8 -0
- package/dist/schema/render.js +12 -2
- package/dist/schema/subgraph.d.ts +6 -0
- package/dist/schema/subgraph.js +27 -0
- package/dist/schema/tools.d.ts +2 -0
- package/dist/schema/tools.js +183 -29
- package/package.json +3 -3
package/dist/schema/tools.js
CHANGED
|
@@ -2,25 +2,40 @@ import { tool } from '@zenera/neo';
|
|
|
2
2
|
import { FORMATS, isFormat, present } from "../present.js";
|
|
3
3
|
import { isEmpty, parseQuery, QueryError } from "../query.js";
|
|
4
4
|
import { toTypeScript } from "./hydrate.js";
|
|
5
|
+
import { fields, grepNodes, listNodes, propertyCount } from "./lookup.js";
|
|
6
|
+
import { loose, matcher, PatternError } from "./match.js";
|
|
7
|
+
import { sourceTag } from "./render.js";
|
|
5
8
|
import { stitch } from "./subgraph.js";
|
|
6
9
|
// ---------------------------------------------------------------------------
|
|
7
10
|
// The same index, given to an agent
|
|
8
11
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
12
|
+
// Five tools over one engine, and only one of them ranks anything. `search_api`
|
|
13
|
+
// is the way in when the question is vague; the other four are exact, because
|
|
14
|
+
// a model that has been told "no results" by a vector search has learned
|
|
15
|
+
// nothing — a ranking returns the top of a list, so an empty answer and an
|
|
16
|
+
// absent thing look identical.
|
|
17
|
+
//
|
|
18
|
+
// `find_types_with_property` is for the moment after the compiler says
|
|
19
|
+
// `'password' does not exist in type 'PublicUserProfile'`. At that point the
|
|
20
|
+
// model does not need to be reminded what a password is — it needs the list of
|
|
21
|
+
// types that have one. `grep_api` is the same instinct widened: every literal
|
|
22
|
+
// occurrence, counted in full, so "it is not there" can actually be concluded.
|
|
15
23
|
// ---------------------------------------------------------------------------
|
|
16
24
|
const GROUP = 'schema';
|
|
17
25
|
/** Kept small on purpose: a tool result is prompt, and the model asked for one thing. */
|
|
18
26
|
const DEFAULT_LIMIT = 4;
|
|
19
27
|
const DEFAULT_MAX_NODES = 60;
|
|
20
28
|
const MAX_CANDIDATES = 25;
|
|
29
|
+
/** A listing is lines rather than subgraphs, so it can afford more of them. */
|
|
30
|
+
const DEFAULT_ROWS = 50;
|
|
31
|
+
const MAX_ROWS = 200;
|
|
21
32
|
export function schemaTools(index, options = {}) {
|
|
22
33
|
const fallback = options.format ?? 'text';
|
|
23
34
|
const docs = options.docs ?? true;
|
|
35
|
+
// With one document there is nothing to disambiguate and naming it on every
|
|
36
|
+
// line is prompt spent saying the same word; with several it is the only
|
|
37
|
+
// way to tell two revisions of one API apart.
|
|
38
|
+
const source = options.source ?? index.manifest.sources.length > 1;
|
|
24
39
|
const searchApi = tool({
|
|
25
40
|
name: 'search_api',
|
|
26
41
|
group: GROUP,
|
|
@@ -97,7 +112,10 @@ export function schemaTools(index, options = {}) {
|
|
|
97
112
|
found: result.subgraphs.length,
|
|
98
113
|
ids: result.subgraphs.flatMap((s) => s.hits),
|
|
99
114
|
truncated: result.subgraphs.some((s) => s.truncated),
|
|
100
|
-
api: await present(index, result.subgraphs, chosen(format, fallback), {
|
|
115
|
+
api: await present(index, result.subgraphs, chosen(format, fallback), {
|
|
116
|
+
docs,
|
|
117
|
+
source,
|
|
118
|
+
}),
|
|
101
119
|
};
|
|
102
120
|
},
|
|
103
121
|
});
|
|
@@ -178,43 +196,179 @@ export function schemaTools(index, options = {}) {
|
|
|
178
196
|
: { found: candidates.length, candidates: candidates.slice(0, MAX_CANDIDATES) };
|
|
179
197
|
},
|
|
180
198
|
});
|
|
181
|
-
const
|
|
182
|
-
name: '
|
|
199
|
+
const listApi = tool({
|
|
200
|
+
name: 'list_api',
|
|
183
201
|
group: GROUP,
|
|
184
|
-
description: 'Lists operations by
|
|
185
|
-
'
|
|
202
|
+
description: 'Lists operations, schemas or fields by name, with no searching and no ranking. ' +
|
|
203
|
+
'The answer is complete: every match is counted, so `found` tells you how many ' +
|
|
204
|
+
'exist even when the list was shortened. Use it to see the shape of the API ' +
|
|
205
|
+
'before deciding what to ask for, and to settle whether something exists at all — ' +
|
|
206
|
+
'search can only ever return its best guesses, so it cannot answer that.',
|
|
186
207
|
parameters: {
|
|
187
208
|
type: 'object',
|
|
188
209
|
properties: {
|
|
189
|
-
|
|
210
|
+
kind: {
|
|
211
|
+
type: 'string',
|
|
212
|
+
enum: ['methods', 'types', 'properties'],
|
|
213
|
+
description: 'What to list. Default methods.',
|
|
214
|
+
},
|
|
215
|
+
name: {
|
|
216
|
+
type: 'string',
|
|
217
|
+
description: 'Match the name. A plain word matches anywhere in it; use * and ? ' +
|
|
218
|
+
'to match the whole name, e.g. "*Password*".',
|
|
219
|
+
},
|
|
220
|
+
path: {
|
|
221
|
+
type: 'string',
|
|
222
|
+
description: 'Match the route, e.g. "/users*". Keeps operations and their ' +
|
|
223
|
+
'parameters; a schema sits on no one route, so it is left out.',
|
|
224
|
+
},
|
|
225
|
+
regex: {
|
|
226
|
+
type: 'boolean',
|
|
227
|
+
description: 'Read `name` and `path` as regular expressions instead.',
|
|
228
|
+
},
|
|
190
229
|
method_type: {
|
|
191
230
|
type: 'string',
|
|
192
231
|
enum: ['read_only', 'read_write', 'any'],
|
|
193
232
|
},
|
|
233
|
+
direction: { type: 'string', enum: ['input', 'output', 'any'] },
|
|
234
|
+
source: {
|
|
235
|
+
type: 'string',
|
|
236
|
+
description: 'Only this document, when the index holds more than one.',
|
|
237
|
+
},
|
|
238
|
+
limit: {
|
|
239
|
+
type: 'integer',
|
|
240
|
+
description: `Rows to return. Default ${DEFAULT_ROWS}.`,
|
|
241
|
+
},
|
|
194
242
|
},
|
|
195
243
|
additionalProperties: false,
|
|
196
244
|
},
|
|
197
|
-
execute: async ({
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
245
|
+
execute: async ({ kind, name, path, regex, method_type, direction, source: only, limit, }) => {
|
|
246
|
+
const subject = SUBJECTS[kind ?? 'methods'];
|
|
247
|
+
if (!subject) {
|
|
248
|
+
return { error: `cannot list "${kind}"`, hint: 'kind is methods, types or fields' };
|
|
249
|
+
}
|
|
250
|
+
let result;
|
|
251
|
+
try {
|
|
252
|
+
result = listNodes(index.graph, {
|
|
253
|
+
kind: subject,
|
|
254
|
+
name: name ? [loose(name, { regex })] : undefined,
|
|
255
|
+
path: path ? [loose(path, { regex })] : undefined,
|
|
256
|
+
source: only,
|
|
257
|
+
methodType: enumerated(method_type),
|
|
258
|
+
direction: enumerated(direction),
|
|
259
|
+
limit: Math.min(limit ?? DEFAULT_ROWS, MAX_ROWS),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
return { error: err instanceof PatternError ? err.message : String(err) };
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
found: result.found,
|
|
267
|
+
truncated: result.truncated,
|
|
268
|
+
[PLURALS[subject]]: result.rows.map((r) => line(index.graph, subject, r, source)),
|
|
269
|
+
};
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
const grepApi = tool({
|
|
273
|
+
name: 'grep_api',
|
|
274
|
+
group: GROUP,
|
|
275
|
+
description: 'Finds every literal occurrence of a string across the whole API description — ' +
|
|
276
|
+
'operations, schemas and fields alike. No embeddings and no ranking, so ' +
|
|
277
|
+
'nothing is missed for being an unusual word or an odd spelling. This is the ' +
|
|
278
|
+
'tool for "does X exist anywhere", and for checking that a search which ' +
|
|
279
|
+
'returned nothing really means there is nothing. Narrow it with `path` or ' +
|
|
280
|
+
'`name` when the word is common and only one corner of the API is meant.',
|
|
281
|
+
parameters: {
|
|
282
|
+
type: 'object',
|
|
283
|
+
properties: {
|
|
284
|
+
pattern: {
|
|
285
|
+
type: 'string',
|
|
286
|
+
description: 'The text to find. Matched anywhere, ignoring case.',
|
|
287
|
+
},
|
|
288
|
+
regex: {
|
|
289
|
+
type: 'boolean',
|
|
290
|
+
description: 'Read the pattern as a regular expression instead.',
|
|
291
|
+
},
|
|
292
|
+
kind: { type: 'string', enum: ['method', 'type', 'property'] },
|
|
293
|
+
name: {
|
|
294
|
+
type: 'string',
|
|
295
|
+
description: 'Only nodes whose own name matches this. * and ? allowed.',
|
|
296
|
+
},
|
|
297
|
+
path: {
|
|
298
|
+
type: 'string',
|
|
299
|
+
description: 'Only what sits on a matching route, e.g. "/users*".',
|
|
300
|
+
},
|
|
301
|
+
source: {
|
|
302
|
+
type: 'string',
|
|
303
|
+
description: 'Only this document, when the index holds more than one.',
|
|
304
|
+
},
|
|
305
|
+
limit: {
|
|
306
|
+
type: 'integer',
|
|
307
|
+
description: `Matches to return. Default ${DEFAULT_ROWS}. \`found\` always counts them all.`,
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
required: ['pattern'],
|
|
311
|
+
additionalProperties: false,
|
|
312
|
+
},
|
|
313
|
+
execute: async ({ pattern, regex, kind, name, path, source: only, limit }) => {
|
|
314
|
+
let result;
|
|
315
|
+
try {
|
|
316
|
+
result = grepNodes(index.graph, matcher(pattern, { regex }), {
|
|
317
|
+
kinds: kind ? [kind] : undefined,
|
|
318
|
+
name: name ? [loose(name)] : undefined,
|
|
319
|
+
path: path ? [loose(path)] : undefined,
|
|
320
|
+
source: only,
|
|
321
|
+
limit: Math.min(limit ?? DEFAULT_ROWS, MAX_ROWS),
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
return { error: err instanceof PatternError ? err.message : String(err) };
|
|
326
|
+
}
|
|
327
|
+
if (result.found === 0) {
|
|
328
|
+
return {
|
|
329
|
+
found: 0,
|
|
330
|
+
hint: 'nothing in the description contains it — it is not there under this name',
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
found: result.found,
|
|
335
|
+
truncated: result.truncated,
|
|
336
|
+
matches: result.matches.map((m) => ({
|
|
337
|
+
id: m.id,
|
|
338
|
+
...(source ? { source: m.attributes.source } : {}),
|
|
339
|
+
text: m.text,
|
|
340
|
+
})),
|
|
341
|
+
};
|
|
213
342
|
},
|
|
214
343
|
});
|
|
215
|
-
return [searchApi, describeTypes, findTypesWithProperty,
|
|
344
|
+
return [searchApi, describeTypes, findTypesWithProperty, listApi, grepApi];
|
|
216
345
|
}
|
|
217
346
|
// ---------------------------------------------------------------------------
|
|
347
|
+
const SUBJECTS = {
|
|
348
|
+
methods: 'method',
|
|
349
|
+
types: 'type',
|
|
350
|
+
properties: 'property',
|
|
351
|
+
fields: 'property',
|
|
352
|
+
};
|
|
353
|
+
const PLURALS = {
|
|
354
|
+
method: 'methods',
|
|
355
|
+
type: 'types',
|
|
356
|
+
property: 'properties',
|
|
357
|
+
};
|
|
358
|
+
/** One row, as the line a model reads rather than an object it has to walk. */
|
|
359
|
+
function line(graph, kind, row, source = false) {
|
|
360
|
+
const from = source ? ` ${sourceTag(row.source)}` : '';
|
|
361
|
+
if (kind === 'method') {
|
|
362
|
+
return `${row.httpMethod} ${row.path} ${row.name}${from}${row.doc ? ` — ${row.doc}` : ''}`;
|
|
363
|
+
}
|
|
364
|
+
if (kind === 'type') {
|
|
365
|
+
const side = row.direction === 'none' ? '' : ` (${row.direction})`;
|
|
366
|
+
return `${row.name}${side} ${fields(propertyCount(graph, row.id))}${from}${row.doc ? ` — ${row.doc}` : ''}`;
|
|
367
|
+
}
|
|
368
|
+
const owner = row.parent ? `${row.parent}.` : '';
|
|
369
|
+
return `${owner}${row.name}${row.required ? '' : '?'}: ${row.signature || 'unknown'}${from}`;
|
|
370
|
+
}
|
|
371
|
+
const enumerated = (value) => value && value !== 'any' ? value : undefined;
|
|
218
372
|
function list(description) {
|
|
219
373
|
return { type: 'array', items: { type: 'string' }, description };
|
|
220
374
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenera/rag",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
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.6",
|
|
57
|
+
"@zenera/neo": "^1.1.6"
|
|
58
58
|
}
|
|
59
59
|
}
|