@littlebigbrain/mcp 0.5.1 → 0.6.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/README.md +119 -126
- package/dist/http-server.d.ts +0 -1
- package/dist/http-server.js +0 -1
- package/dist/index.d.ts +1 -0
- package/dist/metadata-pages.d.ts +0 -2
- package/dist/metadata-pages.js +2 -12
- package/dist/query-observer.d.ts +33 -0
- package/dist/query-observer.js +95 -0
- package/dist/rdf-tool.js +2 -3
- package/dist/server.d.ts +2 -1
- package/dist/server.js +2 -2
- package/dist/stdio.js +1 -2
- package/dist/tool-contracts.js +67 -6
- package/dist/tool-runtime.js +54 -56
- package/dist/tools.d.ts +2 -1
- package/dist/tools.js +339 -131
- package/package.json +3 -3
package/dist/tools.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { metadataPage } from "./metadata-pages.js";
|
|
3
3
|
import { registerRdfTool } from "./rdf-tool.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
4
|
+
import { queryTiming } from "./query-observer.js";
|
|
5
|
+
import { DESTRUCTIVE, IDEMPOTENT_WRITE, MUTATING, READ_ONLY, configureInputSchema, configureWireSchema, detailSchema, graphScope, inspectInputSchema, inspectWireSchema, jsonObjectSchema, queryInputSchema, queryWireSchema, searchFeedbackSchema, } from "./tool-contracts.js";
|
|
6
|
+
import { analyze, assertCursorScope, contentHashKey, decodeQueryCursor, effectiveRowLimit, enrichError, errorResult, guide, normalizeDetail, normalizeLbbIris, ontologyDefineBody, queryCommitPin, queryEnvelope, queryToolResult, requireString, rowPageFrom, rowPageNext, run, scoped, stableJson, toolResult, } from "./tool-runtime.js";
|
|
7
|
+
export function registerLbbTools(server, client, options = {}) {
|
|
7
8
|
registerRdfTool(server, client);
|
|
8
9
|
server.registerTool("lbb_inspect", {
|
|
9
10
|
description: "Read graph context and exact graph facts. Actions: guide, graphs, publication, ontology, ontology_conformance, schema, ontology_search, metadata, entity, state, history, transitions, why. graphs works before bootstrap; publication reports whether writes are queryable. ontology and schema return complete entries with page_size, section and cursor; follow next until absent. schema reads active native ontology/SHACL metadata without running validation. Query asserted RDF/OWL axioms separately with lbb_query. ontology_conformance serves the durable report referenced by the pinned published root. entity returns one node's metadata, scalar attributes, bounded Base-backed edge neighborhood, history, and observations. Use lbb_query with SPARQL property paths for precise path selection.",
|
|
@@ -20,7 +21,7 @@ export function registerLbbTools(server, client) {
|
|
|
20
21
|
.catch(async (error) => errorResult(await enrichError(client, error)));
|
|
21
22
|
}
|
|
22
23
|
return run(client, `lbb_inspect.${args.action}`, args.detail, () => {
|
|
23
|
-
const target = scoped(client, args.graph
|
|
24
|
+
const target = scoped(client, args.graph);
|
|
24
25
|
switch (args.action) {
|
|
25
26
|
case "guide":
|
|
26
27
|
return guide(target);
|
|
@@ -94,7 +95,7 @@ export function registerLbbTools(server, client) {
|
|
|
94
95
|
});
|
|
95
96
|
});
|
|
96
97
|
server.registerTool("lbb_query", {
|
|
97
|
-
description: "Analytical and expert reads. Modes: structured (SPARQL-subset JSON body), sparql (SPARQL text), analyze. SPARQL is the
|
|
98
|
+
description: "Analytical and expert reads. Modes: structured (SPARQL-subset JSON body), sparql (SPARQL text), search (instances by meaning over every searchable class; filter narrows by class and relationship; every hit checked against the graph), analyze. SPARQL is the query language; search finds what the words describe. To plan a search: lbb_embeddings action=list, then SPARQL for a class's relationships on a sample, then mode=search with explain=true to check the resolved filter before the real search (lbb_inspect action=guide has the queries). Relations are <https://littlebigbrain.com/r/NAME> and types <https://littlebigbrain.com/class/NAME> (both lowercased); entities are content-addressed, so anchor a named one by its rdfs:label rather than building its IRI. Structured and text queries pin one published watermark for the request.",
|
|
98
99
|
inputSchema: queryWireSchema,
|
|
99
100
|
annotations: READ_ONLY,
|
|
100
101
|
}, (rawArgs) => {
|
|
@@ -109,7 +110,7 @@ export function registerLbbTools(server, client) {
|
|
|
109
110
|
if (cursor && cursor.mode !== args.mode) {
|
|
110
111
|
throw new Error(`cursor is for ${cursor.mode}, not ${args.mode}`);
|
|
111
112
|
}
|
|
112
|
-
assertCursorScope({ graph: args.graph
|
|
113
|
+
assertCursorScope({ graph: args.graph }, cursor);
|
|
113
114
|
if (cursor &&
|
|
114
115
|
args.row_limit !== undefined &&
|
|
115
116
|
args.row_limit !== cursor.row_limit) {
|
|
@@ -118,9 +119,39 @@ export function registerLbbTools(server, client) {
|
|
|
118
119
|
const detail = normalizeDetail(args.detail ?? cursor?.detail);
|
|
119
120
|
const rowLimit = effectiveRowLimit(detail, args.row_limit ?? cursor?.row_limit);
|
|
120
121
|
const graph = cursor?.graph ?? args.graph;
|
|
121
|
-
const branch = cursor?.branch ?? args.branch;
|
|
122
122
|
const offset = cursor?.offset ?? 0;
|
|
123
|
-
const target = scoped(client, graph
|
|
123
|
+
const target = scoped(client, graph);
|
|
124
|
+
const timing = queryTiming(options, {
|
|
125
|
+
mode: args.mode,
|
|
126
|
+
continuation: cursor !== undefined,
|
|
127
|
+
row_limit: rowLimit,
|
|
128
|
+
offset,
|
|
129
|
+
});
|
|
130
|
+
const pin = (requested) => !cursor && requested === undefined
|
|
131
|
+
? timing.measureAsync("query_pin_metadata", () => queryCommitPin(target, requested, cursor))
|
|
132
|
+
: queryCommitPin(target, requested, cursor);
|
|
133
|
+
const buildEnvelope = (value, rowPage, next, cursorBase, notes) => timing.measure("query_envelope", () => {
|
|
134
|
+
timing.counts({ received_rows: rowPage?.returned });
|
|
135
|
+
const result = queryEnvelope(`lbb_query.${args.mode}`, value, detail, rowPage, next, cursorBase, { textFormat: options.queryTextFormat, notes });
|
|
136
|
+
if (options.timing) {
|
|
137
|
+
const nextCursor = result.next?.cursor;
|
|
138
|
+
timing.counts({
|
|
139
|
+
returned_rows: rowPageFrom(result)?.returned,
|
|
140
|
+
cursor_bytes: typeof nextCursor === "string"
|
|
141
|
+
? Buffer.byteLength(nextCursor, "utf8")
|
|
142
|
+
: 0,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
});
|
|
147
|
+
const render = (value) => timing.measure("query_render", () => {
|
|
148
|
+
const result = queryToolResult(value, options.queryTextFormat);
|
|
149
|
+
if (options.timing)
|
|
150
|
+
timing.counts({
|
|
151
|
+
text_bytes: Buffer.byteLength(result.content[0].text, "utf8"),
|
|
152
|
+
});
|
|
153
|
+
return result;
|
|
154
|
+
});
|
|
124
155
|
for (const key of [
|
|
125
156
|
"entailment",
|
|
126
157
|
"consistency",
|
|
@@ -174,7 +205,7 @@ export function registerLbbTools(server, client) {
|
|
|
174
205
|
args.as_of_commit_seq !== cursor.as_of_commit_seq) {
|
|
175
206
|
throw new Error("cursor as_of_commit_seq does not match the supplied as_of_commit_seq argument");
|
|
176
207
|
}
|
|
177
|
-
const asOfCommitSeq = await
|
|
208
|
+
const asOfCommitSeq = await pin(requestedCommitSeq);
|
|
178
209
|
const request = {
|
|
179
210
|
...body,
|
|
180
211
|
limit: rowLimit,
|
|
@@ -189,16 +220,15 @@ export function registerLbbTools(server, client) {
|
|
|
189
220
|
request.combinators.length > 0) {
|
|
190
221
|
throw new Error("`combinators` (UNION/OPTIONAL/MINUS/EXISTS) is no longer accepted by structured mode; the analytics route was removed. Express the same query as SPARQL text with mode=sparql.");
|
|
191
222
|
}
|
|
192
|
-
const response = await target.sparql(request, {
|
|
223
|
+
const response = await timing.measureAsync("query_http_total", () => target.sparql(request, {
|
|
193
224
|
consistency,
|
|
194
225
|
minIndexedSeq,
|
|
195
|
-
});
|
|
226
|
+
}));
|
|
196
227
|
const rowPage = rowPageFrom(response);
|
|
197
228
|
const cursorBase = {
|
|
198
229
|
v: 1,
|
|
199
230
|
mode: "structured",
|
|
200
231
|
graph,
|
|
201
|
-
branch,
|
|
202
232
|
detail,
|
|
203
233
|
row_limit: rowLimit,
|
|
204
234
|
body,
|
|
@@ -207,7 +237,7 @@ export function registerLbbTools(server, client) {
|
|
|
207
237
|
as_of_commit_seq: asOfCommitSeq,
|
|
208
238
|
};
|
|
209
239
|
const next = rowPageNext(cursorBase, rowPage);
|
|
210
|
-
return
|
|
240
|
+
return render(buildEnvelope(response, rowPage, next, cursorBase));
|
|
211
241
|
}
|
|
212
242
|
// Canonicalize little big brain relation/class/property IRI local-name case up
|
|
213
243
|
// front, then use the normalized text everywhere (mismatch check,
|
|
@@ -229,22 +259,29 @@ export function registerLbbTools(server, client) {
|
|
|
229
259
|
args.as_of_commit_seq !== cursor.as_of_commit_seq) {
|
|
230
260
|
throw new Error("cursor as_of_commit_seq does not match the supplied as_of_commit_seq argument");
|
|
231
261
|
}
|
|
232
|
-
const asOfCommitSeq = await
|
|
262
|
+
const asOfCommitSeq = await pin(args.as_of_commit_seq);
|
|
233
263
|
const entailment = cursor?.entailment ?? args.entailment ?? "none";
|
|
234
|
-
const response = await target.sparqlText({
|
|
264
|
+
const response = await timing.measureAsync("query_http_total", () => target.sparqlText({
|
|
235
265
|
query,
|
|
236
266
|
entailment,
|
|
237
267
|
as_of_commit_seq: asOfCommitSeq ?? null,
|
|
238
268
|
limit: rowLimit,
|
|
239
269
|
offset,
|
|
240
|
-
|
|
241
|
-
|
|
270
|
+
// Managed evals: the first page records a trace; a
|
|
271
|
+
// continuation page re-reads the same rows and must not.
|
|
272
|
+
...(cursor === undefined && args.request
|
|
273
|
+
? { request: args.request }
|
|
274
|
+
: {}),
|
|
275
|
+
}, { consistency, minIndexedSeq }));
|
|
276
|
+
if (response.trace_id) {
|
|
277
|
+
notes.push(`eval trace ${response.trace_id}: to label rows, read their item ids with lbb_evals action=trace trace_id=${response.trace_id}, then call lbb_evals action=label trace_id=${response.trace_id} item=<id> valid=true|false (or items=[…]).`);
|
|
278
|
+
}
|
|
279
|
+
const data = timing.measure("query_results_parse", () => JSON.parse(response.results));
|
|
242
280
|
const rowPage = rowPageFrom(response);
|
|
243
281
|
const cursorBase = {
|
|
244
282
|
v: 1,
|
|
245
283
|
mode: "sparql",
|
|
246
284
|
graph,
|
|
247
|
-
branch,
|
|
248
285
|
detail,
|
|
249
286
|
row_limit: rowLimit,
|
|
250
287
|
query,
|
|
@@ -254,16 +291,32 @@ export function registerLbbTools(server, client) {
|
|
|
254
291
|
as_of_commit_seq: asOfCommitSeq,
|
|
255
292
|
};
|
|
256
293
|
const next = rowPageNext(cursorBase, rowPage);
|
|
257
|
-
const sparqlEnvelope =
|
|
258
|
-
return
|
|
294
|
+
const sparqlEnvelope = buildEnvelope(data, rowPage, next, cursorBase, notes);
|
|
295
|
+
return render(sparqlEnvelope);
|
|
259
296
|
}
|
|
260
297
|
catch (error) {
|
|
261
298
|
return errorResult(await enrichError(client, error));
|
|
262
299
|
}
|
|
263
300
|
})();
|
|
264
301
|
}
|
|
302
|
+
if (args.mode === "search") {
|
|
303
|
+
const searchArgs = args;
|
|
304
|
+
return run(client, "lbb_query.search", undefined, async () => {
|
|
305
|
+
const target = scoped(client, searchArgs.graph);
|
|
306
|
+
return target.embeddings.search({
|
|
307
|
+
embedding: searchArgs.embedding,
|
|
308
|
+
text: searchArgs.text,
|
|
309
|
+
top_k: searchArgs.top_k,
|
|
310
|
+
probe: searchArgs.probe,
|
|
311
|
+
include: searchArgs.include,
|
|
312
|
+
request: searchArgs.request,
|
|
313
|
+
filter: searchArgs.filter,
|
|
314
|
+
explain: searchArgs.explain,
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
}
|
|
265
318
|
return run(client, `lbb_query.${args.mode}`, args.detail, async () => {
|
|
266
|
-
const target = scoped(client, args.graph
|
|
319
|
+
const target = scoped(client, args.graph);
|
|
267
320
|
return analyze(target, {
|
|
268
321
|
metric: args.metric,
|
|
269
322
|
chart: args.chart,
|
|
@@ -277,13 +330,7 @@ export function registerLbbTools(server, client) {
|
|
|
277
330
|
server.registerTool("lbb_models", {
|
|
278
331
|
description: "Read model-training inputs or compare retrieval configurations over one pinned published snapshot. shadow_eval takes the API ShadowEvalRequest body; dataset actions return bounded training examples at an optional signal split.",
|
|
279
332
|
inputSchema: {
|
|
280
|
-
action: z.enum([
|
|
281
|
-
"shadow_eval",
|
|
282
|
-
"planner_dataset",
|
|
283
|
-
"planner_preference_dataset",
|
|
284
|
-
"suggest_dataset",
|
|
285
|
-
"extractor_dataset",
|
|
286
|
-
]),
|
|
333
|
+
action: z.enum(["shadow_eval", "suggest_dataset", "extractor_dataset"]),
|
|
287
334
|
body: jsonObjectSchema.optional(),
|
|
288
335
|
limit: z.number().int().positive().optional(),
|
|
289
336
|
split_seq: z.number().int().nonnegative().optional(),
|
|
@@ -291,26 +338,270 @@ export function registerLbbTools(server, client) {
|
|
|
291
338
|
...graphScope,
|
|
292
339
|
},
|
|
293
340
|
annotations: READ_ONLY,
|
|
294
|
-
}, ({ action, body, limit, split_seq, detail, graph
|
|
295
|
-
const target = scoped(client, graph
|
|
341
|
+
}, ({ action, body, limit, split_seq, detail, graph }) => run(client, `lbb_models.${action}`, detail, () => {
|
|
342
|
+
const target = scoped(client, graph);
|
|
296
343
|
switch (action) {
|
|
297
344
|
case "shadow_eval":
|
|
298
345
|
if (!body)
|
|
299
346
|
throw new Error("shadow_eval requires body");
|
|
300
347
|
return target.shadowEval(body);
|
|
301
|
-
case "planner_dataset":
|
|
302
|
-
return target.plannerDataset({ limit, splitSeq: split_seq });
|
|
303
|
-
case "planner_preference_dataset":
|
|
304
|
-
return target.plannerPreferenceDataset({
|
|
305
|
-
limit,
|
|
306
|
-
splitSeq: split_seq,
|
|
307
|
-
});
|
|
308
348
|
case "suggest_dataset":
|
|
309
349
|
return target.suggestDataset({ limit, splitSeq: split_seq });
|
|
310
350
|
case "extractor_dataset":
|
|
311
351
|
return target.extractorDataset({ limit, splitSeq: split_seq });
|
|
312
352
|
}
|
|
313
353
|
}));
|
|
354
|
+
const embeddingSetup = {
|
|
355
|
+
class: z.string().optional().describe("preview/declare: the class IRI."),
|
|
356
|
+
from: z
|
|
357
|
+
.array(z.string())
|
|
358
|
+
.optional()
|
|
359
|
+
.describe('preview/declare: the fields, e.g. ["label", "description", "calls/label"]. Omit for the automatic choice; on an existing embedding, omit to keep its fields.'),
|
|
360
|
+
exclude: z
|
|
361
|
+
.array(z.string())
|
|
362
|
+
.optional()
|
|
363
|
+
.describe("preview/declare: fields to drop from the automatic choice."),
|
|
364
|
+
title: z
|
|
365
|
+
.string()
|
|
366
|
+
.optional()
|
|
367
|
+
.describe('preview/declare: the field that names each hit, e.g. "display_name". Omit for the ontology\'s name property (skos:prefLabel, a property declared rdfs:subPropertyOf rdfs:label, else rdfs:label); on an existing embedding, omit to keep its name. preview reports title_source and, when the labels read as keys, title_suggestion.'),
|
|
368
|
+
model: z
|
|
369
|
+
.string()
|
|
370
|
+
.optional()
|
|
371
|
+
.describe("model: the graph's new model. preview/declare: omit it; a graph has one model (the graph's, or the platform's default for the first embedding)."),
|
|
372
|
+
dim: z.number().int().positive().optional(),
|
|
373
|
+
};
|
|
374
|
+
const recipeOf = (args) => {
|
|
375
|
+
if (!args.class)
|
|
376
|
+
throw new Error("this action requires class");
|
|
377
|
+
return {
|
|
378
|
+
class: args.class,
|
|
379
|
+
...(args.name ? { name: args.name } : {}),
|
|
380
|
+
...(args.from ? { from: args.from } : {}),
|
|
381
|
+
...(args.exclude ? { exclude: args.exclude } : {}),
|
|
382
|
+
...(args.title ? { title: args.title } : {}),
|
|
383
|
+
...(args.model ? { model: args.model } : {}),
|
|
384
|
+
...(args.dim ? { dim: args.dim } : {}),
|
|
385
|
+
};
|
|
386
|
+
};
|
|
387
|
+
server.registerTool("lbb_embeddings", {
|
|
388
|
+
description: "Search setup, read only: embeddings declared on classes (an embedding on a class covers its subclasses; one model per graph). list shows each embedding's status (serving and building version, backfill progress, lag) and the graph's model; get shows one; preview shows every candidate fact of a class with its coverage and examples, and the exact text of sample instances, and stores nothing. Declare, refresh, or change the model with lbb_embeddings_manage; search with lbb_query mode=search. list is the first step of planning a search (lbb_inspect action=guide has the recipe).",
|
|
389
|
+
inputSchema: {
|
|
390
|
+
action: z.enum(["list", "get", "preview"]),
|
|
391
|
+
name: z.string().optional().describe("get: the embedding name."),
|
|
392
|
+
...embeddingSetup,
|
|
393
|
+
sample: z.number().int().positive().max(50).optional(),
|
|
394
|
+
iris: z
|
|
395
|
+
.array(z.string())
|
|
396
|
+
.optional()
|
|
397
|
+
.describe("preview: the instances to show instead of a sample."),
|
|
398
|
+
detail: detailSchema,
|
|
399
|
+
...graphScope,
|
|
400
|
+
},
|
|
401
|
+
annotations: READ_ONLY,
|
|
402
|
+
}, (args) => run(client, `lbb_embeddings.${args.action}`, args.detail, () => {
|
|
403
|
+
const target = scoped(client, args.graph);
|
|
404
|
+
switch (args.action) {
|
|
405
|
+
case "list":
|
|
406
|
+
return target.embeddings.list();
|
|
407
|
+
case "get":
|
|
408
|
+
if (!args.name)
|
|
409
|
+
throw new Error("get requires name");
|
|
410
|
+
return target.embeddings.get(args.name);
|
|
411
|
+
case "preview":
|
|
412
|
+
return target.embeddings.preview({
|
|
413
|
+
...recipeOf(args),
|
|
414
|
+
...(args.sample ? { sample: args.sample } : {}),
|
|
415
|
+
...(args.iris ? { iris: args.iris } : {}),
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}));
|
|
419
|
+
server.registerTool("lbb_embeddings_manage", {
|
|
420
|
+
description: "Declare or refresh an embedding, or change the graph's model. Run lbb_embeddings action=preview first and show the user the text it will embed. declare sets the class, the fields (one-hop property paths such as label, description, calls/label; omit for an automatic choice); a new embedding takes the graph's model, and another model is refused. On an existing embedding it changes only what you name, and a changed recipe builds a new version while the old one serves. model moves every embedding of the graph to `model`: each builds a new version, and the graph switches when all are ready (a search never mixes two models). refresh runs one step of the embed job now (it also runs by itself after each published commit).",
|
|
421
|
+
inputSchema: {
|
|
422
|
+
action: z.enum(["declare", "refresh", "model"]),
|
|
423
|
+
name: z
|
|
424
|
+
.string()
|
|
425
|
+
.optional()
|
|
426
|
+
.describe("The embedding name (refresh: required)."),
|
|
427
|
+
...embeddingSetup,
|
|
428
|
+
detail: detailSchema,
|
|
429
|
+
...graphScope,
|
|
430
|
+
},
|
|
431
|
+
annotations: MUTATING,
|
|
432
|
+
}, (args) => run(client, `lbb_embeddings_manage.${args.action}`, args.detail, () => {
|
|
433
|
+
const target = scoped(client, args.graph);
|
|
434
|
+
switch (args.action) {
|
|
435
|
+
case "declare":
|
|
436
|
+
return target.embeddings.declare(recipeOf(args));
|
|
437
|
+
case "refresh":
|
|
438
|
+
if (!args.name)
|
|
439
|
+
throw new Error("refresh requires name");
|
|
440
|
+
return target.embeddings.refresh(args.name);
|
|
441
|
+
case "model":
|
|
442
|
+
if (!args.model)
|
|
443
|
+
throw new Error("model requires model");
|
|
444
|
+
return target.embeddings.setModel({
|
|
445
|
+
model: args.model,
|
|
446
|
+
...(args.dim ? { dim: args.dim } : {}),
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}));
|
|
450
|
+
server.registerTool("lbb_embeddings_delete", {
|
|
451
|
+
description: "Delete an embedding: its name and its vectors (index-gc removes the stored runs). Its class stops being searchable until it is declared and built again. Ask the user first; confirm must repeat the name.",
|
|
452
|
+
inputSchema: {
|
|
453
|
+
name: z.string().describe("The embedding name."),
|
|
454
|
+
confirm: z.string().describe("The same name again, to confirm."),
|
|
455
|
+
detail: detailSchema,
|
|
456
|
+
...graphScope,
|
|
457
|
+
},
|
|
458
|
+
annotations: DESTRUCTIVE,
|
|
459
|
+
}, ({ name, confirm, detail, graph }) => run(client, "lbb_embeddings_delete", detail, () => {
|
|
460
|
+
if (confirm !== name) {
|
|
461
|
+
throw new Error("confirm must repeat the embedding name");
|
|
462
|
+
}
|
|
463
|
+
return scoped(client, graph).embeddings.delete(name);
|
|
464
|
+
}));
|
|
465
|
+
server.registerTool("lbb_evals", {
|
|
466
|
+
description: "Managed evals: the thumbs up / thumbs down of the graph, per result. A query run with `request` (lbb_query) records a trace with one item per result (hit or row); trace reads it back with the item ids. label marks one result (item + valid) or several (items) relevant or not; the labels become the golden's ground truth. golden freezes a query (every result it returns now is relevant). run replays every golden at the current commit: pass when every relevant result is back and no wrong one is; new results open a review trace. judge lets the platform's judge model (the hosted frontier model) label unlabeled results. summary, traces, goldens, results, and settings read state.",
|
|
467
|
+
inputSchema: {
|
|
468
|
+
action: z.enum([
|
|
469
|
+
"summary",
|
|
470
|
+
"traces",
|
|
471
|
+
"trace",
|
|
472
|
+
"label",
|
|
473
|
+
"judge",
|
|
474
|
+
"goldens",
|
|
475
|
+
"golden",
|
|
476
|
+
"accept",
|
|
477
|
+
"delete",
|
|
478
|
+
"run",
|
|
479
|
+
"results",
|
|
480
|
+
"settings",
|
|
481
|
+
]),
|
|
482
|
+
trace_id: z
|
|
483
|
+
.string()
|
|
484
|
+
.optional()
|
|
485
|
+
.describe("trace / label / judge: the trace id from lbb_query."),
|
|
486
|
+
item: z
|
|
487
|
+
.string()
|
|
488
|
+
.optional()
|
|
489
|
+
.describe("label: the result id (a hit's `id`, or an item id from trace) to label with `valid`."),
|
|
490
|
+
valid: z
|
|
491
|
+
.boolean()
|
|
492
|
+
.optional()
|
|
493
|
+
.describe("label: true = the result answers the request (thumbs up), false = it does not."),
|
|
494
|
+
items: z
|
|
495
|
+
.array(z
|
|
496
|
+
.object({
|
|
497
|
+
id: z.string(),
|
|
498
|
+
valid: z.boolean(),
|
|
499
|
+
note: z.string().optional(),
|
|
500
|
+
})
|
|
501
|
+
.strict())
|
|
502
|
+
.optional()
|
|
503
|
+
.describe("label: several results at once."),
|
|
504
|
+
by: z
|
|
505
|
+
.string()
|
|
506
|
+
.optional()
|
|
507
|
+
.describe("label: who labels (an agent name)."),
|
|
508
|
+
note: z.string().optional(),
|
|
509
|
+
sparql: z
|
|
510
|
+
.string()
|
|
511
|
+
.optional()
|
|
512
|
+
.describe("golden: the query to freeze (the search text for surface=search)."),
|
|
513
|
+
surface: z
|
|
514
|
+
.enum(["sparql", "search"])
|
|
515
|
+
.optional()
|
|
516
|
+
.describe("golden: which surface the query runs on (default sparql)."),
|
|
517
|
+
embedding: z
|
|
518
|
+
.string()
|
|
519
|
+
.optional()
|
|
520
|
+
.describe("golden: the embedding, for surface=search."),
|
|
521
|
+
top_k: z
|
|
522
|
+
.number()
|
|
523
|
+
.int()
|
|
524
|
+
.positive()
|
|
525
|
+
.max(200)
|
|
526
|
+
.optional()
|
|
527
|
+
.describe("golden: the top_k of a search golden."),
|
|
528
|
+
request: z
|
|
529
|
+
.string()
|
|
530
|
+
.optional()
|
|
531
|
+
.describe("golden: the user's words the query answers."),
|
|
532
|
+
golden_id: z
|
|
533
|
+
.string()
|
|
534
|
+
.optional()
|
|
535
|
+
.describe("accept / delete: the golden id."),
|
|
536
|
+
limit: z.number().int().positive().optional(),
|
|
537
|
+
unlabeled: z
|
|
538
|
+
.boolean()
|
|
539
|
+
.optional()
|
|
540
|
+
.describe("traces: only traces without a label."),
|
|
541
|
+
consistency: z
|
|
542
|
+
.enum(["strong", "eventual"])
|
|
543
|
+
.optional()
|
|
544
|
+
.describe("run / accept: strong reads the head, eventual the last published commit."),
|
|
545
|
+
detail: detailSchema,
|
|
546
|
+
...graphScope,
|
|
547
|
+
},
|
|
548
|
+
annotations: MUTATING,
|
|
549
|
+
}, ({ action, trace_id, item, valid, items, by, note, sparql, surface, embedding, top_k, request, golden_id, limit, unlabeled, consistency, detail, graph, }) => run(client, `lbb_evals.${action}`, detail, () => {
|
|
550
|
+
const target = scoped(client, graph);
|
|
551
|
+
switch (action) {
|
|
552
|
+
case "summary":
|
|
553
|
+
return target.evals.summary();
|
|
554
|
+
case "traces":
|
|
555
|
+
return target.evals.traces({ limit, unlabeled });
|
|
556
|
+
case "trace":
|
|
557
|
+
if (!trace_id)
|
|
558
|
+
throw new Error("trace requires trace_id");
|
|
559
|
+
return target.evals.trace(trace_id);
|
|
560
|
+
case "label":
|
|
561
|
+
if (!trace_id)
|
|
562
|
+
throw new Error("label requires trace_id");
|
|
563
|
+
if (!item && !items?.length)
|
|
564
|
+
throw new Error("label requires item (with valid) or items");
|
|
565
|
+
if (item && valid === undefined)
|
|
566
|
+
throw new Error("label requires valid with item");
|
|
567
|
+
return target.evals.label(trace_id, {
|
|
568
|
+
...(item ? { item, valid } : {}),
|
|
569
|
+
...(items?.length ? { items } : {}),
|
|
570
|
+
by,
|
|
571
|
+
note,
|
|
572
|
+
});
|
|
573
|
+
case "judge":
|
|
574
|
+
return target.evals.judge({ traceId: trace_id, limit });
|
|
575
|
+
case "goldens":
|
|
576
|
+
return target.evals.goldens();
|
|
577
|
+
case "golden":
|
|
578
|
+
if (!sparql)
|
|
579
|
+
throw new Error("golden requires sparql");
|
|
580
|
+
if (surface === "search" && !embedding)
|
|
581
|
+
throw new Error("a search golden requires embedding");
|
|
582
|
+
return target.evals.createGolden({
|
|
583
|
+
sparql,
|
|
584
|
+
request,
|
|
585
|
+
...(surface ? { surface } : {}),
|
|
586
|
+
...(embedding ? { embedding } : {}),
|
|
587
|
+
...(top_k ? { top_k } : {}),
|
|
588
|
+
});
|
|
589
|
+
case "accept":
|
|
590
|
+
if (!golden_id)
|
|
591
|
+
throw new Error("accept requires golden_id");
|
|
592
|
+
return target.evals.acceptGolden(golden_id, { consistency });
|
|
593
|
+
case "delete":
|
|
594
|
+
if (!golden_id)
|
|
595
|
+
throw new Error("delete requires golden_id");
|
|
596
|
+
return target.evals.deleteGolden(golden_id);
|
|
597
|
+
case "run":
|
|
598
|
+
return target.evals.run({ consistency });
|
|
599
|
+
case "results":
|
|
600
|
+
return target.evals.results({ limit });
|
|
601
|
+
case "settings":
|
|
602
|
+
return target.evals.settings();
|
|
603
|
+
}
|
|
604
|
+
}));
|
|
314
605
|
server.registerTool("lbb_commit", {
|
|
315
606
|
description: "Write graph facts, retract them, or label ranked results. mode=facts writes triplets/embeddings/properties; mode=retract removes a wrongly-added fact (by edge or by entity) without a full reset; mode=search_feedback stores query/result relevance labels (Feedback grades: 3=ideal/good, 1=partial, 0=bad; include query, search_id when available, target, rank, score). Explicit idempotency_key wins; when omitted, MCP derives a stable content hash so content-identical retries dedupe. Facts mode defaults edge_idempotency to append; pass skip_unchanged for re-runnable backfills.",
|
|
316
607
|
inputSchema: {
|
|
@@ -377,7 +668,7 @@ export function registerLbbTools(server, client) {
|
|
|
377
668
|
...graphScope,
|
|
378
669
|
},
|
|
379
670
|
annotations: IDEMPOTENT_WRITE,
|
|
380
|
-
}, ({ idempotency_key, mode, triplets, entity_embeddings, entity_properties, search_feedback, dry_run, observed_at, edge_idempotency, retract_edges, retract_entities, graph,
|
|
671
|
+
}, ({ idempotency_key, mode, triplets, entity_embeddings, entity_properties, search_feedback, dry_run, observed_at, edge_idempotency, retract_edges, retract_entities, graph, }) => run(client, "lbb_commit", "standard", () => {
|
|
381
672
|
const commitMode = mode ??
|
|
382
673
|
(search_feedback
|
|
383
674
|
? "search_feedback"
|
|
@@ -393,8 +684,8 @@ export function registerLbbTools(server, client) {
|
|
|
393
684
|
throw new Error("lbb_commit mode=retract requires retract_edges or retract_entities");
|
|
394
685
|
}
|
|
395
686
|
const key = idempotency_key ??
|
|
396
|
-
contentHashKey({ graph
|
|
397
|
-
return scoped(client, graph
|
|
687
|
+
contentHashKey({ graph }, { mode: "retract", edges, entities });
|
|
688
|
+
return scoped(client, graph).retract({ edges, entities }, {
|
|
398
689
|
idempotencyKey: key,
|
|
399
690
|
});
|
|
400
691
|
}
|
|
@@ -402,8 +693,8 @@ export function registerLbbTools(server, client) {
|
|
|
402
693
|
if (!search_feedback)
|
|
403
694
|
throw new Error("lbb_commit mode=search_feedback requires search_feedback");
|
|
404
695
|
const key = idempotency_key ??
|
|
405
|
-
contentHashKey({ graph
|
|
406
|
-
return scoped(client, graph
|
|
696
|
+
contentHashKey({ graph }, { mode: "search_feedback", search_feedback });
|
|
697
|
+
return scoped(client, graph).searchFeedback(search_feedback, { idempotencyKey: key });
|
|
407
698
|
}
|
|
408
699
|
if (search_feedback) {
|
|
409
700
|
throw new Error("lbb_commit facts mode cannot include search_feedback");
|
|
@@ -420,10 +711,10 @@ export function registerLbbTools(server, client) {
|
|
|
420
711
|
payload.entity_properties.length === 0) {
|
|
421
712
|
throw new Error("lbb_commit requires at least one triplet, entity embedding, or entity property");
|
|
422
713
|
}
|
|
423
|
-
const key = idempotency_key ?? contentHashKey({ graph
|
|
714
|
+
const key = idempotency_key ?? contentHashKey({ graph }, payload);
|
|
424
715
|
if (dry_run)
|
|
425
|
-
return scoped(client, graph
|
|
426
|
-
return scoped(client, graph
|
|
716
|
+
return scoped(client, graph).commitDryRun(payload);
|
|
717
|
+
return scoped(client, graph).commit(payload, {
|
|
427
718
|
idempotencyKey: key,
|
|
428
719
|
});
|
|
429
720
|
}));
|
|
@@ -438,9 +729,7 @@ export function registerLbbTools(server, client) {
|
|
|
438
729
|
const args = parsed.data;
|
|
439
730
|
return run(client, `lbb_configure.${args.action}`, "standard", () => {
|
|
440
731
|
if (args.action === "define_ontology") {
|
|
441
|
-
return client
|
|
442
|
-
.withScope({ graph: args.graph, branch: args.branch })
|
|
443
|
-
.ontologyDefine(ontologyDefineBody({
|
|
732
|
+
return client.withScope({ graph: args.graph }).ontologyDefine(ontologyDefineBody({
|
|
444
733
|
entity_types: args.entity_types,
|
|
445
734
|
relations: args.relations,
|
|
446
735
|
source: args.source,
|
|
@@ -450,7 +739,7 @@ export function registerLbbTools(server, client) {
|
|
|
450
739
|
}));
|
|
451
740
|
}
|
|
452
741
|
if (args.action === "evolve_ontology") {
|
|
453
|
-
return scoped(client, args.graph
|
|
742
|
+
return scoped(client, args.graph).ontology.evolve({
|
|
454
743
|
ops: args.ops,
|
|
455
744
|
allow_data_conflicts: args.allow_data_conflicts ?? false,
|
|
456
745
|
}, { dryRun: args.dry_run });
|
|
@@ -458,7 +747,7 @@ export function registerLbbTools(server, client) {
|
|
|
458
747
|
if (args.shapes === undefined) {
|
|
459
748
|
throw new Error("publish_schema requires a SHACL shapes source; use define_ontology or evolve_ontology for native metadata changes");
|
|
460
749
|
}
|
|
461
|
-
return scoped(client, args.graph
|
|
750
|
+
return scoped(client, args.graph).schema.publish({
|
|
462
751
|
ontology: args.ontology,
|
|
463
752
|
shapes: args.shapes,
|
|
464
753
|
desired_mode: args.desired_mode,
|
|
@@ -466,85 +755,4 @@ export function registerLbbTools(server, client) {
|
|
|
466
755
|
}, { dryRun: args.dry_run });
|
|
467
756
|
});
|
|
468
757
|
});
|
|
469
|
-
server.registerTool("lbb_branch", {
|
|
470
|
-
description: "Branch lifecycle. Actions: create (fork a new branch off from_branch — the tool's `branch` argument names the NEW branch) and merge (validate-then-merge: replay from_branch's post-fork commits onto the scoped target branch — its fork parent — as ONE commit with event ids preserved; SHACL-validates the would-be merged state first and refuses with the report on violations; a fact superseded on the target after the fork wins over the branch's version, reported as a supersedure_race conflict; delete_source consumes the merged branch).",
|
|
471
|
-
inputSchema: {
|
|
472
|
-
action: z
|
|
473
|
-
.enum(["create", "merge"])
|
|
474
|
-
.describe("create = fork a new branch; merge = replay a child branch onto its fork parent"),
|
|
475
|
-
from_branch: z
|
|
476
|
-
.string()
|
|
477
|
-
.describe("create: the branch to fork from; merge: the child branch whose commits are replayed"),
|
|
478
|
-
validate: z
|
|
479
|
-
.boolean()
|
|
480
|
-
.optional()
|
|
481
|
-
.describe("merge only: refuse on SHACL violations of the would-be merged state (default true)"),
|
|
482
|
-
delete_source: z
|
|
483
|
-
.boolean()
|
|
484
|
-
.optional()
|
|
485
|
-
.describe("merge only: delete every object under the merged branch after success"),
|
|
486
|
-
...graphScope,
|
|
487
|
-
},
|
|
488
|
-
annotations: MUTATING,
|
|
489
|
-
}, ({ action, from_branch, validate, delete_source, graph, branch }) => run(client, `lbb_branch.${action}`, "standard", () => {
|
|
490
|
-
const target = scoped(client, graph, branch);
|
|
491
|
-
if (action === "create")
|
|
492
|
-
return target.createBranch({ from_branch });
|
|
493
|
-
return target.mergeBranch({
|
|
494
|
-
from_branch,
|
|
495
|
-
validate: validate ?? true,
|
|
496
|
-
delete_source: delete_source ?? false,
|
|
497
|
-
});
|
|
498
|
-
}));
|
|
499
|
-
server.registerTool("lbb_observe", {
|
|
500
|
-
description: "Remember a conversation: store the turns verbatim as an EPISODE evidence entity, then anchor + gate the supplied facts on an observe branch (LLM extraction cannot poison the main graph). Facts with both endpoints already in the graph are anchored; unanchored facts need confidence >= 0.8 to mint new entities, else they come back needs_review. auto_merge merges the branch onto the scoped branch when SHACL validation is clean (the validate-then-merge). Server flag-gated (--enable-observe). This build takes caller-extracted facts (each with a structured triplet); bare statements come back needs_review.",
|
|
501
|
-
inputSchema: {
|
|
502
|
-
session_id: z
|
|
503
|
-
.string()
|
|
504
|
-
.describe("Caller's conversation id (drives the default observe branch name)"),
|
|
505
|
-
turns: z
|
|
506
|
-
.array(z.object({
|
|
507
|
-
role: z.string().describe("user | assistant | tool"),
|
|
508
|
-
content: z.string(),
|
|
509
|
-
name: z.string().optional(),
|
|
510
|
-
ts: z.string().optional().describe("RFC 3339 timestamp"),
|
|
511
|
-
}))
|
|
512
|
-
.min(1)
|
|
513
|
-
.describe("The conversation slice to remember (stored verbatim)"),
|
|
514
|
-
source: z
|
|
515
|
-
.string()
|
|
516
|
-
.optional()
|
|
517
|
-
.describe("Source label, e.g. support-bot"),
|
|
518
|
-
facts: z
|
|
519
|
-
.array(z.object({
|
|
520
|
-
fact: z.string().describe("Natural-language statement"),
|
|
521
|
-
confidence: z.number().optional().describe("0..1 (default 0.9)"),
|
|
522
|
-
triplet: jsonObjectSchema
|
|
523
|
-
.optional()
|
|
524
|
-
.describe("Structured form {source:{type,name}, relation, target:{type,name}} — required for the fact to commit"),
|
|
525
|
-
}))
|
|
526
|
-
.optional()
|
|
527
|
-
.describe("Caller-extracted candidate facts; omit with extract:false to store the episode only"),
|
|
528
|
-
extract: z
|
|
529
|
-
.boolean()
|
|
530
|
-
.optional()
|
|
531
|
-
.describe("false = store the episode only (default true)"),
|
|
532
|
-
observe_branch: z
|
|
533
|
-
.string()
|
|
534
|
-
.optional()
|
|
535
|
-
.describe("Branch for the facts (default observe-<hash12(session_id)>)"),
|
|
536
|
-
auto_merge: z
|
|
537
|
-
.boolean()
|
|
538
|
-
.optional()
|
|
539
|
-
.describe("Merge onto the scoped branch when validation is clean"),
|
|
540
|
-
...graphScope,
|
|
541
|
-
},
|
|
542
|
-
annotations: MUTATING,
|
|
543
|
-
}, ({ session_id, turns, source, facts, extract, observe_branch, auto_merge, graph, branch, }) => run(client, "lbb_observe", "standard", () => scoped(client, graph, branch).observe({
|
|
544
|
-
episode: { turns, session_id, source },
|
|
545
|
-
extract: extract ?? true,
|
|
546
|
-
extraction: { byo_completion: (facts ?? []) },
|
|
547
|
-
branch: observe_branch,
|
|
548
|
-
auto_merge: auto_merge ?? false,
|
|
549
|
-
})));
|
|
550
758
|
}
|