@littlebigbrain/mcp 0.10.0 → 0.12.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 +33 -18
- package/dist/files-tool.d.ts +43 -0
- package/dist/files-tool.js +183 -0
- package/dist/query-tools.d.ts +89 -0
- package/dist/query-tools.js +208 -0
- package/dist/question.d.ts +9 -7
- package/dist/question.js +112 -61
- package/dist/tool-contracts.js +164 -13
- package/dist/tools.js +472 -10
- package/package.json +2 -2
package/dist/tools.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { metadataPage } from "./metadata-pages.js";
|
|
3
3
|
import { answerQuestion, } from "./question.js";
|
|
4
|
+
import { commitAt, compareQuery, describeGraph, findNames, } from "./query-tools.js";
|
|
4
5
|
import { registerRdfTool } from "./rdf-tool.js";
|
|
6
|
+
import { registerFilesTool } from "./files-tool.js";
|
|
5
7
|
import { queryTiming } from "./query-observer.js";
|
|
6
8
|
import { DESTRUCTIVE, IDEMPOTENT_WRITE, MUTATING, READ_ONLY, configureInputSchema, configureWireSchema, detailSchema, graphScope, inspectInputSchema, inspectWireSchema, jsonObjectSchema, queryInputSchema, queryWireSchema, searchFeedbackSchema, yearMonthSchema, } from "./tool-contracts.js";
|
|
7
9
|
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";
|
|
8
10
|
export function registerLbbTools(server, client, options = {}) {
|
|
9
11
|
registerRdfTool(server, client);
|
|
12
|
+
registerFilesTool(server, client);
|
|
10
13
|
server.registerTool("lbb_inspect", {
|
|
11
|
-
description: "Read graph context and exact graph facts. Actions: guide, graphs, publication, ontology, ontology_conformance, schema, ontology_search, metadata, entity, ontology_suggestions. graphs works before bootstrap; ontology_suggestions lists the ontology change suggestions that wait for review (or were decided). 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 attributes and current relationships from the RDF read. For a node's past values, run SPARQL with as_of_commit_seq through lbb_query. Use lbb_query with SPARQL property paths for precise path selection.",
|
|
14
|
+
description: "Read graph context and exact graph facts. Actions: guide, graphs, publication, ontology, ontology_conformance, schema, ontology_search, metadata, entity, ontology_suggestions. graphs works before bootstrap; ontology_suggestions lists the ontology change suggestions that wait for review (or were decided); fact_count is how many graph facts (triplets and entity property rows) accepting one commits, and identity_count how many identities it links. 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 attributes and current relationships from the RDF read. For a node's past values, run SPARQL with as_of_commit_seq through lbb_query. Use lbb_query with SPARQL property paths for precise path selection.",
|
|
12
15
|
inputSchema: inspectWireSchema,
|
|
13
16
|
annotations: READ_ONLY,
|
|
14
17
|
}, (rawArgs) => {
|
|
@@ -63,7 +66,7 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
63
66
|
});
|
|
64
67
|
});
|
|
65
68
|
server.registerTool("lbb_query", {
|
|
66
|
-
description: "Analytical and expert reads. Modes: question (a question in plain words), 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. Use mode=question when you have a question in plain words and no SPARQL query: the server
|
|
69
|
+
description: "Analytical and expert reads. Modes: question (a question in plain words), 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, and four tools for writing your own queries: names, describe, commit_at, compare. names: before a query that names an entity (a person, a company), find its IRI with text=<the question or the names>; use the first candidate, not a CONTAINS match on the name. describe: before a query on classes or properties you have not seen, read them with question=<the question> (or classes/properties as IRIs): how many sampled instances hold each property (a filter on a rare one returns few rows), the values of small classes such as stages, examples and schema statements. commit_at: for a question about a date, find the commit of date=YYYY-MM-DD (or moment=RFC 3339), then run mode=sparql as_of_commit_seq=<it>. compare: for what changed between two points, run one SELECT at before and after (default the latest) with key=[the entity variable]; added, removed and changed come with totals and pages (next). Use mode=question when you have a question in plain words and no SPARQL query: the server picks the kind of question (route), runs queries in a bounded loop, reads their rows, and answers in plain words (about 8 s). The result holds the answer with citations and a chart hint (kind, x, y: columns of the rows), the steps, the route, and the query and rows the answer stands on. Continue or correct that query with mode=sparql. Each question uses model tokens and counts toward a daily limit of the stack. SPARQL is the query language; search finds what the words describe. SPARQL text can also search by meaning inside the query with ?x <https://littlebigbrain.com/search#similarTo> \"words\", so the other patterns filter and join the hits in one query; the result's search field reports the plan. 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.",
|
|
67
70
|
inputSchema: queryWireSchema,
|
|
68
71
|
annotations: READ_ONLY,
|
|
69
72
|
}, (rawArgs, extra) => {
|
|
@@ -300,6 +303,29 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
300
303
|
}
|
|
301
304
|
})();
|
|
302
305
|
}
|
|
306
|
+
if (args.mode === "names" ||
|
|
307
|
+
args.mode === "describe" ||
|
|
308
|
+
args.mode === "commit_at" ||
|
|
309
|
+
args.mode === "compare") {
|
|
310
|
+
const toolArgs = args;
|
|
311
|
+
return (async () => {
|
|
312
|
+
try {
|
|
313
|
+
switch (args.mode) {
|
|
314
|
+
case "names":
|
|
315
|
+
return await findNames(client, toolArgs);
|
|
316
|
+
case "describe":
|
|
317
|
+
return await describeGraph(client, toolArgs);
|
|
318
|
+
case "commit_at":
|
|
319
|
+
return await commitAt(client, toolArgs);
|
|
320
|
+
default:
|
|
321
|
+
return await compareQuery(client, toolArgs);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
return errorResult(await enrichError(client, error));
|
|
326
|
+
}
|
|
327
|
+
})();
|
|
328
|
+
}
|
|
303
329
|
if (args.mode === "search") {
|
|
304
330
|
const searchArgs = args;
|
|
305
331
|
// The caller's detail shapes the hits like every other mode: compact
|
|
@@ -476,8 +502,426 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
476
502
|
}
|
|
477
503
|
return scoped(client, graph).embeddings.delete(name);
|
|
478
504
|
}));
|
|
505
|
+
// Fit from text: fit sources declared on classes, like embeddings.
|
|
506
|
+
const fitSetup = {
|
|
507
|
+
class: z
|
|
508
|
+
.string()
|
|
509
|
+
.optional()
|
|
510
|
+
.describe("preview/declare/dry_run: the class IRI whose instances hold the text."),
|
|
511
|
+
from: z
|
|
512
|
+
.array(z.string())
|
|
513
|
+
.optional()
|
|
514
|
+
.describe("preview/declare/dry_run: the fields that hold the text, as embedding paths (transcript, label, about/label, <https://…>). Omit for the long text facts of the class."),
|
|
515
|
+
exclude: z
|
|
516
|
+
.array(z.string())
|
|
517
|
+
.optional()
|
|
518
|
+
.describe("preview/declare/dry_run: fields to drop from the automatic choice."),
|
|
519
|
+
context: z
|
|
520
|
+
.string()
|
|
521
|
+
.optional()
|
|
522
|
+
.describe('preview/declare/dry_run: one sentence about the text, e.g. "interviews with employees about their work processes" (at most 500 characters).'),
|
|
523
|
+
};
|
|
524
|
+
const fitDeclaration = (args) => {
|
|
525
|
+
if (!args.class)
|
|
526
|
+
throw new Error("this action requires class");
|
|
527
|
+
return {
|
|
528
|
+
class: args.class,
|
|
529
|
+
...(args.name ? { name: args.name } : {}),
|
|
530
|
+
...(args.from ? { from: args.from } : {}),
|
|
531
|
+
...(args.exclude ? { exclude: args.exclude } : {}),
|
|
532
|
+
...(args.context ? { context: args.context } : {}),
|
|
533
|
+
};
|
|
534
|
+
};
|
|
535
|
+
server.registerTool("lbb_fit_sources", {
|
|
536
|
+
description: "Fit from text, read only: fit sources declared on classes. A fit source names the fields of a class that hold text (transcripts, documents); the server reads every instance, proposes ontology changes (classes, properties, relations) with verbatim quotes, checks them, and files them as ontology suggestions with origin id fit:<name>. list shows each source's status (progress, lag, instances read, proposals kept and dropped, suggestions per status, this month's spend, last error) and the stack's monthly budget; get shows one; preview shows the fields, every candidate field of the class and the text of sample instances, calls no model and stores nothing. Declare, refresh or dry-run with lbb_fit_sources_manage; list the suggestions with lbb_inspect action=ontology_suggestions.",
|
|
537
|
+
inputSchema: {
|
|
538
|
+
action: z.enum(["list", "get", "preview"]),
|
|
539
|
+
name: z.string().optional().describe("get: the fit source name."),
|
|
540
|
+
...fitSetup,
|
|
541
|
+
sample: z.number().int().positive().max(20).optional(),
|
|
542
|
+
iris: z
|
|
543
|
+
.array(z.string())
|
|
544
|
+
.optional()
|
|
545
|
+
.describe("preview: the instances to show instead of a sample."),
|
|
546
|
+
detail: detailSchema,
|
|
547
|
+
...graphScope,
|
|
548
|
+
},
|
|
549
|
+
annotations: READ_ONLY,
|
|
550
|
+
}, (args) => run(client, `lbb_fit_sources.${args.action}`, args.detail, () => {
|
|
551
|
+
const fit = scoped(client, args.graph).ontology.fitSources;
|
|
552
|
+
switch (args.action) {
|
|
553
|
+
case "list":
|
|
554
|
+
return fit.list();
|
|
555
|
+
case "get":
|
|
556
|
+
if (!args.name)
|
|
557
|
+
throw new Error("get requires name");
|
|
558
|
+
return fit.get(args.name);
|
|
559
|
+
case "preview":
|
|
560
|
+
return fit.preview({
|
|
561
|
+
...fitDeclaration(args),
|
|
562
|
+
...(args.sample ? { sample: args.sample } : {}),
|
|
563
|
+
...(args.iris ? { iris: args.iris } : {}),
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}));
|
|
567
|
+
server.registerTool("lbb_fit_sources_manage", {
|
|
568
|
+
description: "Declare, dry-run or refresh a fit source (fit from text). Run lbb_fit_sources action=preview first and show the user the text the fit reads. dry_run runs the models on the first part of up to 3 instances (at most 60 s; truncated when one did not finish) and returns every proposal with its quote, its support score and what the checks made of it; it files nothing but spends model budget. declare sets the class, the fields and the context; the job then reads every instance and files suggestions (an existing source changes only what you name). refresh asks the job to run now and returns the status at once; the job also runs by itself after each published commit, so poll lbb_fit_sources action=get for progress. Nothing changes the ontology until a person accepts a suggestion.",
|
|
569
|
+
inputSchema: {
|
|
570
|
+
action: z.enum(["declare", "dry_run", "refresh"]),
|
|
571
|
+
name: z
|
|
572
|
+
.string()
|
|
573
|
+
.optional()
|
|
574
|
+
.describe("The fit source name (refresh: required)."),
|
|
575
|
+
...fitSetup,
|
|
576
|
+
detail: detailSchema,
|
|
577
|
+
...graphScope,
|
|
578
|
+
},
|
|
579
|
+
annotations: MUTATING,
|
|
580
|
+
}, (args) => run(client, `lbb_fit_sources_manage.${args.action}`, args.detail, () => {
|
|
581
|
+
const fit = scoped(client, args.graph).ontology.fitSources;
|
|
582
|
+
switch (args.action) {
|
|
583
|
+
case "declare":
|
|
584
|
+
return fit.declare(fitDeclaration(args));
|
|
585
|
+
case "dry_run":
|
|
586
|
+
return fit.preview({ ...fitDeclaration(args), propose: true });
|
|
587
|
+
case "refresh":
|
|
588
|
+
if (!args.name)
|
|
589
|
+
throw new Error("refresh requires name");
|
|
590
|
+
return fit.refresh(args.name);
|
|
591
|
+
}
|
|
592
|
+
}));
|
|
593
|
+
server.registerTool("lbb_fit_sources_delete", {
|
|
594
|
+
description: "Delete a fit source: the fit stops reading its class. The suggestions it filed stay. Ask the user first; confirm must repeat the name.",
|
|
595
|
+
inputSchema: {
|
|
596
|
+
name: z.string().describe("The fit source name."),
|
|
597
|
+
confirm: z.string().describe("The same name again, to confirm."),
|
|
598
|
+
detail: detailSchema,
|
|
599
|
+
...graphScope,
|
|
600
|
+
},
|
|
601
|
+
annotations: DESTRUCTIVE,
|
|
602
|
+
}, ({ name, confirm, detail, graph }) => run(client, "lbb_fit_sources_delete", detail, () => {
|
|
603
|
+
if (confirm !== name) {
|
|
604
|
+
throw new Error("confirm must repeat the fit source name");
|
|
605
|
+
}
|
|
606
|
+
return scoped(client, graph).ontology.fitSources.delete(name);
|
|
607
|
+
}));
|
|
608
|
+
// Starter workflows and triggered workflows: one API over everything that
|
|
609
|
+
// runs when a class of the graph changes. search.embed and ontology.fit
|
|
610
|
+
// are the embeddings and fit sources above, through the same stored
|
|
611
|
+
// documents.
|
|
612
|
+
const workflowSetup = {
|
|
613
|
+
starter: z
|
|
614
|
+
.string()
|
|
615
|
+
.optional()
|
|
616
|
+
.describe("The starter workflow's catalog id from action=starters, e.g. search.embed (embeddings on a class), ontology.fit (fit from text) or workflow (a developer workflow: your worker's workflow gets the instances). get/preview/use/refresh/pause/resume: required."),
|
|
617
|
+
class: z
|
|
618
|
+
.string()
|
|
619
|
+
.optional()
|
|
620
|
+
.describe("preview/use: the class whose instances the workflow watches (an IRI, or a name the ontology knows). Required unless the starter workflow's watch is fixed."),
|
|
621
|
+
fields: z
|
|
622
|
+
.array(z.string())
|
|
623
|
+
.optional()
|
|
624
|
+
.describe("preview/use: the fields whose change triggers the workflow, as embedding paths (transcript, label, about/label, <https://…>). Omit for the starter workflow's own choice."),
|
|
625
|
+
exclude: z
|
|
626
|
+
.array(z.string())
|
|
627
|
+
.optional()
|
|
628
|
+
.describe("preview/use: fields to drop from the automatic choice."),
|
|
629
|
+
params: jsonObjectSchema
|
|
630
|
+
.optional()
|
|
631
|
+
.describe('preview/use: the starter workflow\'s own settings, as listed in its catalog entry: search.embed takes model, dim and title; ontology.fit takes context (one sentence about the text, e.g. {"context": "interviews with employees"}); workflow takes none.'),
|
|
632
|
+
workflow: z
|
|
633
|
+
.object({
|
|
634
|
+
workflow_type: z
|
|
635
|
+
.string()
|
|
636
|
+
.describe("The workflow type your worker serves, e.g. word-count."),
|
|
637
|
+
version: z.string().describe("Its version, e.g. v1."),
|
|
638
|
+
})
|
|
639
|
+
.strict()
|
|
640
|
+
.optional()
|
|
641
|
+
.describe("preview/use, developer workflow (starter=workflow) only, required there: the workflow of your worker that receives the messages. Creating it creates the instance starter.<name>."),
|
|
642
|
+
owns: z
|
|
643
|
+
.object({
|
|
644
|
+
classes: z.array(z.string()).max(64).optional(),
|
|
645
|
+
relations: z.array(z.string()).max(64).optional(),
|
|
646
|
+
properties: z.array(z.string()).max(64).optional(),
|
|
647
|
+
})
|
|
648
|
+
.strict()
|
|
649
|
+
.optional()
|
|
650
|
+
.describe("preview/use, developer workflow only: what your code writes. classes it creates instances of, relations it writes as out-edges of the watched instances, properties it writes on them. Owned outputs never trigger the workflow itself."),
|
|
651
|
+
batch: z
|
|
652
|
+
.number()
|
|
653
|
+
.int()
|
|
654
|
+
.min(1)
|
|
655
|
+
.max(64)
|
|
656
|
+
.optional()
|
|
657
|
+
.describe("preview/use, developer workflow only: instances per message, 1 to 64 (default 16)."),
|
|
658
|
+
};
|
|
659
|
+
const workflowRequest = (args) => {
|
|
660
|
+
if (!args.starter)
|
|
661
|
+
throw new Error("this action requires starter");
|
|
662
|
+
if (!args.class && (args.fields || args.exclude)) {
|
|
663
|
+
throw new Error("fields and exclude need class");
|
|
664
|
+
}
|
|
665
|
+
return {
|
|
666
|
+
starter: args.starter,
|
|
667
|
+
...(args.name ? { name: args.name } : {}),
|
|
668
|
+
...(args.class
|
|
669
|
+
? {
|
|
670
|
+
watch: {
|
|
671
|
+
class: args.class,
|
|
672
|
+
...(args.fields ? { fields: args.fields } : {}),
|
|
673
|
+
...(args.exclude ? { exclude: args.exclude } : {}),
|
|
674
|
+
},
|
|
675
|
+
}
|
|
676
|
+
: {}),
|
|
677
|
+
...(args.params ? { params: args.params } : {}),
|
|
678
|
+
...(args.gate !== undefined ? { gate: { below: args.gate } } : {}),
|
|
679
|
+
...(args.budget_usd_per_month !== undefined
|
|
680
|
+
? { budget_usd_per_month: args.budget_usd_per_month }
|
|
681
|
+
: {}),
|
|
682
|
+
...(args.workflow ? { workflow: args.workflow } : {}),
|
|
683
|
+
...(args.owns ? { owns: args.owns } : {}),
|
|
684
|
+
...(args.batch !== undefined ? { batch: args.batch } : {}),
|
|
685
|
+
};
|
|
686
|
+
};
|
|
687
|
+
const workflowRef = (args) => {
|
|
688
|
+
if (!args.starter || !args.name) {
|
|
689
|
+
throw new Error("this action requires starter and name");
|
|
690
|
+
}
|
|
691
|
+
return { starter: args.starter, name: args.name };
|
|
692
|
+
};
|
|
693
|
+
server.registerTool("lbb_workflows", {
|
|
694
|
+
description: "Starter workflows and triggered workflows, read only. A starter workflow is a template; a triggered workflow is a workflow created from one on the graph. It watches a class: when a published commit adds or changes instances of the class (or changes the watched fields), the workflow reads them and does its work. search.embed keeps an embedding of the class (the same as lbb_embeddings); ontology.fit reads text fields and files ontology suggestions (the same as lbb_fit_sources); workflow (a developer workflow) sends the new and changed instances to a workflow of the developer's worker as trigger messages, and that code writes the facts the workflow owns. documents.parse reads uploaded PDF files into pages with line boxes, documents.link writes DocEntry and DocMention instances and links each mention to its entry, and infer.llm fills chosen properties from text, each value with a quote; read the files and their pages with lbb_files. starters lists the starter workflows this server offers with their group, settings and whether each can run here. list shows every triggered workflow of the graph with its state (backfilling, ready, paused, failed, unavailable), lag, counters, this month's spend and last error; get shows one (starter and name) with its own status in details (for workflow: the messages sent, the instances waiting, the message in flight and whether it is paused). preview shows what creating one would do (the resolved watch, the instance count and the workflow's own preview; for workflow, the first messages it would send), calls no model and stores nothing. Message workflows of the workflow engine are not listed here. Create, refresh, pause or resume with lbb_workflows_manage; delete with lbb_workflows_delete.",
|
|
695
|
+
inputSchema: {
|
|
696
|
+
action: z.enum(["starters", "list", "get", "preview"]),
|
|
697
|
+
name: z
|
|
698
|
+
.string()
|
|
699
|
+
.optional()
|
|
700
|
+
.describe("get: the triggered workflow's name. preview: the name a new workflow would get."),
|
|
701
|
+
...workflowSetup,
|
|
702
|
+
sample: z
|
|
703
|
+
.number()
|
|
704
|
+
.int()
|
|
705
|
+
.positive()
|
|
706
|
+
.max(50)
|
|
707
|
+
.optional()
|
|
708
|
+
.describe("preview: how many sample instances to show."),
|
|
709
|
+
iris: z
|
|
710
|
+
.array(z.string())
|
|
711
|
+
.optional()
|
|
712
|
+
.describe("preview: the instances to show instead of a sample."),
|
|
713
|
+
detail: detailSchema,
|
|
714
|
+
...graphScope,
|
|
715
|
+
},
|
|
716
|
+
annotations: READ_ONLY,
|
|
717
|
+
}, (args) => run(client, `lbb_workflows.${args.action}`, args.detail, () => {
|
|
718
|
+
const workflows = scoped(client, args.graph).workflows;
|
|
719
|
+
switch (args.action) {
|
|
720
|
+
case "starters":
|
|
721
|
+
return workflows.starters.list();
|
|
722
|
+
case "list":
|
|
723
|
+
return workflows.triggered.list();
|
|
724
|
+
case "get":
|
|
725
|
+
return workflows.triggered.get(workflowRef(args));
|
|
726
|
+
case "preview":
|
|
727
|
+
return workflows.triggered.preview({
|
|
728
|
+
...workflowRequest(args),
|
|
729
|
+
...(args.sample ? { sample: args.sample } : {}),
|
|
730
|
+
...(args.iris ? { iris: args.iris } : {}),
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
}));
|
|
734
|
+
server.registerTool("lbb_workflows_manage", {
|
|
735
|
+
description: "Create a workflow from a starter workflow, or refresh, pause or resume a triggered workflow. Run lbb_workflows action=preview first and show the user the watch and the instance count. use creates a workflow from a starter workflow of action=starters, under a name (default: the class's local name), with its watch (class, fields) and params; the workflow then reads every instance of the class and follows each published commit. A developer workflow (starter=workflow) also takes workflow (workflow_type and version of the developer's worker), owns and batch; it needs a server with the workflow engine. use again with the same starter and name changes only what you name, and the same request again changes nothing. A workflow that calls models (ontology.fit, documents.link, infer.llm) spends model budget as it reads. refresh asks the workflow to run now and returns at once with queued; poll lbb_workflows action=get for progress. pause stops a workflow (a developer workflow, documents.parse, documents.link or infer.llm) from reading and sending; resume lets it catch up. search.embed and ontology.fit cannot be paused.",
|
|
736
|
+
inputSchema: {
|
|
737
|
+
action: z.enum(["use", "refresh", "pause", "resume"]),
|
|
738
|
+
name: z
|
|
739
|
+
.string()
|
|
740
|
+
.optional()
|
|
741
|
+
.describe("The triggered workflow's name, [a-z0-9][a-z0-9-]{0,62} (refresh, pause, resume: required; use: omit for the class's local name)."),
|
|
742
|
+
...workflowSetup,
|
|
743
|
+
gate: z
|
|
744
|
+
.number()
|
|
745
|
+
.min(0)
|
|
746
|
+
.max(1)
|
|
747
|
+
.optional()
|
|
748
|
+
.describe("use: outputs below this confidence go to review instead of the graph, for a workflow with a review gate."),
|
|
749
|
+
budget_usd_per_month: z
|
|
750
|
+
.number()
|
|
751
|
+
.nonnegative()
|
|
752
|
+
.optional()
|
|
753
|
+
.describe("use: a monthly model budget in USD for this workflow, below the stack's own."),
|
|
754
|
+
detail: detailSchema,
|
|
755
|
+
...graphScope,
|
|
756
|
+
},
|
|
757
|
+
annotations: MUTATING,
|
|
758
|
+
}, (args) => run(client, `lbb_workflows_manage.${args.action}`, args.detail, () => {
|
|
759
|
+
const workflows = scoped(client, args.graph).workflows;
|
|
760
|
+
switch (args.action) {
|
|
761
|
+
case "use":
|
|
762
|
+
return workflows.starters.use(workflowRequest(args));
|
|
763
|
+
case "refresh":
|
|
764
|
+
return workflows.triggered.refresh(workflowRef(args));
|
|
765
|
+
case "pause":
|
|
766
|
+
return workflows.triggered.pause(workflowRef(args), true);
|
|
767
|
+
case "resume":
|
|
768
|
+
return workflows.triggered.pause(workflowRef(args), false);
|
|
769
|
+
}
|
|
770
|
+
}));
|
|
771
|
+
server.registerTool("lbb_workflows_delete", {
|
|
772
|
+
description: "Delete a triggered workflow: it stops following its class, and what it wrote stays. Deleting search.embed deletes the embedding and its vectors, so its class stops being searchable; the suggestions an ontology.fit filed stay; a developer workflow leaves its workflow instance and the facts its code wrote. Ask the user first; confirm must repeat the name.",
|
|
773
|
+
inputSchema: {
|
|
774
|
+
starter: z
|
|
775
|
+
.string()
|
|
776
|
+
.describe("The starter workflow's catalog id, e.g. search.embed."),
|
|
777
|
+
name: z.string().describe("The triggered workflow's name."),
|
|
778
|
+
confirm: z.string().describe("The same name again, to confirm."),
|
|
779
|
+
detail: detailSchema,
|
|
780
|
+
...graphScope,
|
|
781
|
+
},
|
|
782
|
+
annotations: DESTRUCTIVE,
|
|
783
|
+
}, ({ starter, name, confirm, detail, graph }) => run(client, "lbb_workflows_delete", detail, () => {
|
|
784
|
+
if (confirm !== name) {
|
|
785
|
+
throw new Error("confirm must repeat the workflow name");
|
|
786
|
+
}
|
|
787
|
+
return scoped(client, graph).workflows.triggered.delete({
|
|
788
|
+
starter,
|
|
789
|
+
name,
|
|
790
|
+
});
|
|
791
|
+
}));
|
|
792
|
+
const modelUse = z
|
|
793
|
+
.enum(["ask", "route", "rerank", "fit", "label"])
|
|
794
|
+
.describe("A use of a model: ask = question answers, route = query routing, rerank = search rerank, fit = ontology fit, label = eval labels.");
|
|
795
|
+
server.registerTool("lbb_model_choice", {
|
|
796
|
+
description: "Model choice, read only: which model each use of a model on the graph runs, and trials that test other models against the graph's ground truth (the checked calls: a person's review, else the judge's verdict). options shows per use the model it runs now (current), LBB's model (default), a switch (switched), its checked calls, and the models a trial can test with their efforts, prices and whether the server holds their key; available=false with a reason when trials cannot run. trials lists the trials, newest first, each with its report: calls compared, both models' right share, mean score (0 to 1), cost per call and median time, the difference with its 95% interval (delta, ci_low, ci_high), outcome (too_few, better, same, worse) and qualifies (meets the bar: at least 20 calls, at most 5 points worse at the low end, no more failures, cheaper or better). trial reads one trial with its compared calls; trial_call reads one call with both answers and the ground truth. switches lists the switched uses. Start, stop, switch or revert with lbb_model_choice_manage.",
|
|
797
|
+
inputSchema: {
|
|
798
|
+
action: z.enum([
|
|
799
|
+
"options",
|
|
800
|
+
"trials",
|
|
801
|
+
"trial",
|
|
802
|
+
"trial_call",
|
|
803
|
+
"switches",
|
|
804
|
+
]),
|
|
805
|
+
trial_id: z
|
|
806
|
+
.string()
|
|
807
|
+
.optional()
|
|
808
|
+
.describe("trial / trial_call: the trial id."),
|
|
809
|
+
call_id: z
|
|
810
|
+
.string()
|
|
811
|
+
.optional()
|
|
812
|
+
.describe("trial_call: a call id from the trial's calls."),
|
|
813
|
+
job: modelUse
|
|
814
|
+
.optional()
|
|
815
|
+
.describe("trials: only the trials of this use."),
|
|
816
|
+
limit: z
|
|
817
|
+
.number()
|
|
818
|
+
.int()
|
|
819
|
+
.positive()
|
|
820
|
+
.max(50)
|
|
821
|
+
.optional()
|
|
822
|
+
.describe("trials: at most this many, newest first (default 20)."),
|
|
823
|
+
detail: detailSchema,
|
|
824
|
+
...graphScope,
|
|
825
|
+
},
|
|
826
|
+
annotations: READ_ONLY,
|
|
827
|
+
}, (args) => run(client, `lbb_model_choice.${args.action}`, args.detail, () => {
|
|
828
|
+
const models = scoped(client, args.graph).models;
|
|
829
|
+
switch (args.action) {
|
|
830
|
+
case "options":
|
|
831
|
+
return models.trials.options();
|
|
832
|
+
case "trials":
|
|
833
|
+
return models.trials.list({ job: args.job, limit: args.limit });
|
|
834
|
+
case "trial":
|
|
835
|
+
if (!args.trial_id)
|
|
836
|
+
throw new Error("trial requires trial_id");
|
|
837
|
+
return models.trials.get(args.trial_id);
|
|
838
|
+
case "trial_call":
|
|
839
|
+
if (!args.trial_id || !args.call_id) {
|
|
840
|
+
throw new Error("trial_call requires trial_id and call_id");
|
|
841
|
+
}
|
|
842
|
+
return models.trials.call(args.trial_id, args.call_id);
|
|
843
|
+
case "switches":
|
|
844
|
+
return models.switches.list();
|
|
845
|
+
}
|
|
846
|
+
}));
|
|
847
|
+
server.registerTool("lbb_model_choice_manage", {
|
|
848
|
+
description: "Model choice, writes. start tests a candidate model on the uses in jobs (without jobs, every use it can do that has checked calls): the server answers those checked calls again with the candidate and scores it and the model in use against the same ground truth. It changes no model and spends at most the trial budget ($3.00 by default) of model and judge cost; an open trial of the same use and candidate comes back as it is. Pick candidates from lbb_model_choice action=options (provider anthropic with an effort, or typesafe jev-latest). A trial runs by itself: poll lbb_model_choice action=trial until its status is no longer running. stop ends a trial; it keeps what it compared. switch makes the trial's use run its candidate on this graph from the next call on; only a trial that qualifies and compared with the model in use switches (ask to a Claude model, route to a Jev model). Show the user the trial's report (score, difference and interval, cost per call, time) and switch only what the user confirmed. revert puts the use back on LBB's model.",
|
|
849
|
+
inputSchema: {
|
|
850
|
+
action: z.enum(["start", "stop", "switch", "revert"]),
|
|
851
|
+
provider: z
|
|
852
|
+
.enum(["anthropic", "typesafe"])
|
|
853
|
+
.optional()
|
|
854
|
+
.describe("start: the candidate's provider."),
|
|
855
|
+
model: z
|
|
856
|
+
.string()
|
|
857
|
+
.optional()
|
|
858
|
+
.describe("start: the candidate model id, e.g. claude-haiku-5-5 or jev-latest."),
|
|
859
|
+
effort: z
|
|
860
|
+
.enum(["low", "medium", "high", "xhigh", "max"])
|
|
861
|
+
.optional()
|
|
862
|
+
.describe("start: the effort of a Claude candidate."),
|
|
863
|
+
jobs: z
|
|
864
|
+
.array(modelUse)
|
|
865
|
+
.optional()
|
|
866
|
+
.describe("start: the uses to test it on."),
|
|
867
|
+
target: z
|
|
868
|
+
.number()
|
|
869
|
+
.int()
|
|
870
|
+
.positive()
|
|
871
|
+
.max(100)
|
|
872
|
+
.optional()
|
|
873
|
+
.describe("start: the calls to compare (default 40)."),
|
|
874
|
+
days: z
|
|
875
|
+
.number()
|
|
876
|
+
.int()
|
|
877
|
+
.positive()
|
|
878
|
+
.max(30)
|
|
879
|
+
.optional()
|
|
880
|
+
.describe("start: the days the trial takes new checks (default 14)."),
|
|
881
|
+
trial_id: z
|
|
882
|
+
.string()
|
|
883
|
+
.optional()
|
|
884
|
+
.describe("stop / switch: the trial id."),
|
|
885
|
+
job: modelUse
|
|
886
|
+
.optional()
|
|
887
|
+
.describe("revert: the use to put back on LBB's model."),
|
|
888
|
+
detail: detailSchema,
|
|
889
|
+
...graphScope,
|
|
890
|
+
},
|
|
891
|
+
annotations: MUTATING,
|
|
892
|
+
}, (args) => run(client, `lbb_model_choice_manage.${args.action}`, args.detail, () => {
|
|
893
|
+
const models = scoped(client, args.graph).models;
|
|
894
|
+
switch (args.action) {
|
|
895
|
+
case "start":
|
|
896
|
+
if (!args.provider || !args.model) {
|
|
897
|
+
throw new Error("start requires provider and model");
|
|
898
|
+
}
|
|
899
|
+
return models.trials.create({
|
|
900
|
+
candidate: {
|
|
901
|
+
provider: args.provider,
|
|
902
|
+
model: args.model,
|
|
903
|
+
...(args.effort ? { effort: args.effort } : {}),
|
|
904
|
+
},
|
|
905
|
+
...(args.jobs ? { jobs: args.jobs } : {}),
|
|
906
|
+
...(args.target ? { target: args.target } : {}),
|
|
907
|
+
...(args.days ? { days: args.days } : {}),
|
|
908
|
+
});
|
|
909
|
+
case "stop":
|
|
910
|
+
if (!args.trial_id)
|
|
911
|
+
throw new Error("stop requires trial_id");
|
|
912
|
+
return models.trials.stop(args.trial_id);
|
|
913
|
+
case "switch":
|
|
914
|
+
if (!args.trial_id)
|
|
915
|
+
throw new Error("switch requires trial_id");
|
|
916
|
+
return models.switches.create({ trial: args.trial_id });
|
|
917
|
+
case "revert":
|
|
918
|
+
if (!args.job)
|
|
919
|
+
throw new Error("revert requires job");
|
|
920
|
+
return models.switches.revert(args.job);
|
|
921
|
+
}
|
|
922
|
+
}));
|
|
479
923
|
server.registerTool("lbb_evals", {
|
|
480
|
-
description: "Managed evals: the
|
|
924
|
+
description: "Managed evals: ground truth for the questions an app asks and the searches it runs. A query run with `request` (lbb_query, also mode=question) records a trace: the question, the query that answered it, the type of that query (`query_type`: sparql, hybrid = search by meaning with conditions, search = search by meaning alone) and one item per result (hit or row); trace reads it back with the item ids. label has two forms. On a question's trace, valid alone judges the whole answer: valid=true makes the trace's query the golden query of the question; valid=false with sparql gives the right query, which becomes the golden query; valid=false alone marks the answer wrong. item + valid (or items) judges one result, a citation of the answer: the ground truth of the hits of a search by meaning, and a result marked wrong must not come back for any type. golden freezes a stored query or a search (every result it returns now is relevant). run checks every golden at the current commit: a question is asked again through the query rewriter, then the type of the query it wrote is compared with the expected type, its rows with the rows of the golden query at the same commit (`query_check`), and its results with the judged results; a search is done again; 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. Model checks: a judge model checks a sample of the model calls LBB makes for the graph (rerank, route, ask = the answer of a question, rewrite, fit, propose, label). checks_summary reads a month per job and model (checks, score, right, partly, wrong, reviews) and the judge's agreement with people; checks lists the month's checks, newest first, with the judge's verdict, score and reason and the ground truth (`truth`). review_check records a person's review of one check (call_id): agree=true keeps the judge's verdict; agree=false with verdict (and an optional score, reference and note) corrects it. The review becomes the call's ground truth, so show the check to the user and review only what the user confirmed.",
|
|
481
925
|
inputSchema: {
|
|
482
926
|
action: z.enum([
|
|
483
927
|
"summary",
|
|
@@ -507,7 +951,7 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
507
951
|
valid: z
|
|
508
952
|
.boolean()
|
|
509
953
|
.optional()
|
|
510
|
-
.describe("label: true = the result answers the request (thumbs up), false = it does not."),
|
|
954
|
+
.describe("label: true = the result answers the request (thumbs up), false = it does not. Without item or items, on a question's trace: the whole answer is right (true) or wrong (false)."),
|
|
511
955
|
items: z
|
|
512
956
|
.array(z
|
|
513
957
|
.object({
|
|
@@ -529,7 +973,7 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
529
973
|
sparql: z
|
|
530
974
|
.string()
|
|
531
975
|
.optional()
|
|
532
|
-
.describe("golden: the query to freeze (the search text for surface=search)."),
|
|
976
|
+
.describe("golden: the query to freeze (the search text for surface=search). label: with valid=false and no item, the right query for the question; the server checks and runs it once."),
|
|
533
977
|
surface: z
|
|
534
978
|
.enum(["sparql", "search"])
|
|
535
979
|
.optional()
|
|
@@ -566,7 +1010,15 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
566
1010
|
.optional()
|
|
567
1011
|
.describe("checks_summary / checks: the month, yyyy-mm (UTC). Defaults to the current month."),
|
|
568
1012
|
job: z
|
|
569
|
-
.enum([
|
|
1013
|
+
.enum([
|
|
1014
|
+
"rerank",
|
|
1015
|
+
"route",
|
|
1016
|
+
"ask",
|
|
1017
|
+
"rewrite",
|
|
1018
|
+
"fit",
|
|
1019
|
+
"propose",
|
|
1020
|
+
"label",
|
|
1021
|
+
])
|
|
570
1022
|
.optional()
|
|
571
1023
|
.describe("checks: only the checks of one job."),
|
|
572
1024
|
verdict: z
|
|
@@ -616,13 +1068,19 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
616
1068
|
case "label":
|
|
617
1069
|
if (!trace_id)
|
|
618
1070
|
throw new Error("label requires trace_id");
|
|
619
|
-
if (!item && !items?.length)
|
|
620
|
-
throw new Error("label requires item (with valid) or items");
|
|
621
1071
|
if (item && valid === undefined)
|
|
622
1072
|
throw new Error("label requires valid with item");
|
|
1073
|
+
if (!item && !items?.length && valid === undefined)
|
|
1074
|
+
throw new Error("label requires item (with valid), items, or valid alone for the answer of a question");
|
|
1075
|
+
if (sparql !== undefined &&
|
|
1076
|
+
(item || items?.length || valid !== false))
|
|
1077
|
+
throw new Error("label takes sparql (the right query) only with valid=false and no item");
|
|
623
1078
|
return target.evals.label(trace_id, {
|
|
624
1079
|
...(item ? { item, valid } : {}),
|
|
625
1080
|
...(items?.length ? { items } : {}),
|
|
1081
|
+
// The answer of a question as a whole.
|
|
1082
|
+
...(!item && !items?.length ? { valid } : {}),
|
|
1083
|
+
...(sparql !== undefined ? { sparql } : {}),
|
|
626
1084
|
by,
|
|
627
1085
|
note,
|
|
628
1086
|
});
|
|
@@ -805,7 +1263,7 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
805
1263
|
});
|
|
806
1264
|
}));
|
|
807
1265
|
server.registerTool("lbb_configure", {
|
|
808
|
-
description: "Manage native schema metadata. Actions: define_ontology (friendly spec with super_types), evolve_ontology (ordered edits including add_super_types), list_starters (the base ontologies crm, documents and work, each with its status on the graph: absent, partial or applied, what applying adds, and conflicts), apply_starter (add what the graph lacks of a starter in one ontology version; a relation the graph has is widened; refused with starter_conflict when the graph holds a term differently; dry_run previews), publish_schema (SHACL activation), suggest_ontology_change (file a change for a person to review instead of applying it; prefer it when the graph's owner reviews ontology changes, and list the result with lbb_inspect action=ontology_suggestions), get_rewrite_profile and set_rewrite_profile (the graph's notes and up to 20 worked question-to-SPARQL examples that lbb_query mode=question reads for every question; pass the version you read as expected_version). define, evolve, publish and set_rewrite_profile support dry_run previews. Definition/import here extracts native metadata; it does NOT store the complete RDF/OWL document as queryable graph facts. Use lbb_rdf import for full OWL and lbb_rdf update for additive INSERT DATA revisions; RDF deletions are unsupported. Publish_schema accepts unchanged ontology plus shapes; use define/evolve for native ontology changes. Publication enqueues durable conformance; a preview does not validate the whole graph.",
|
|
1266
|
+
description: "Manage native schema metadata. Actions: define_ontology (friendly spec with super_types), evolve_ontology (ordered edits including add_super_types), list_starters (the base ontologies crm, documents and work, each with its status on the graph: absent, partial or applied, what applying adds, and conflicts), apply_starter (add what the graph lacks of a starter in one ontology version; a relation the graph has is widened; refused with starter_conflict when the graph holds a term differently; dry_run previews), publish_schema (SHACL activation), suggest_ontology_change (file a change for a person to review instead of applying it; prefer it when the graph's owner reviews ontology changes, and list the result with lbb_inspect action=ontology_suggestions; facts files graph facts for review, which accepting commits after the change, and change may be empty when facts is set), get_rewrite_profile and set_rewrite_profile (the graph's notes and up to 20 worked question-to-SPARQL examples that lbb_query mode=question reads for every question; pass the version you read as expected_version). define, evolve, publish and set_rewrite_profile support dry_run previews. Definition/import here extracts native metadata; it does NOT store the complete RDF/OWL document as queryable graph facts. Use lbb_rdf import for full OWL and lbb_rdf update for additive INSERT DATA revisions; RDF deletions are unsupported. Publish_schema accepts unchanged ontology plus shapes; use define/evolve for native ontology changes. Publication enqueues durable conformance; a preview does not validate the whole graph.",
|
|
809
1267
|
inputSchema: configureWireSchema,
|
|
810
1268
|
annotations: MUTATING,
|
|
811
1269
|
}, (rawArgs) => {
|
|
@@ -826,10 +1284,14 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
826
1284
|
}
|
|
827
1285
|
if (args.action === "suggest_ontology_change") {
|
|
828
1286
|
const agent = args.agent ?? "mcp agent";
|
|
1287
|
+
const change = args.change ?? [];
|
|
1288
|
+
if (!change.length && !args.facts)
|
|
1289
|
+
throw new Error("suggest_ontology_change needs change, facts, or both");
|
|
829
1290
|
return scoped(client, args.graph).ontology.suggestions.create({
|
|
830
1291
|
title: args.title,
|
|
831
1292
|
rationale: args.rationale ?? "",
|
|
832
|
-
change
|
|
1293
|
+
change,
|
|
1294
|
+
...(args.facts ? { facts: args.facts } : {}),
|
|
833
1295
|
origin: { kind: "agent", id: agent, label: agent },
|
|
834
1296
|
...(args.anchor ? { anchor: args.anchor } : {}),
|
|
835
1297
|
...(args.key ? { key: args.key } : {}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@littlebigbrain/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "MCP server for little big brain: search, graph queries, and data import",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"test:coverage": "npm run clean:test && tsc -p tsconfig.test.json && node --test --experimental-test-coverage --test-coverage-include=test-dist/*.js --test-coverage-exclude=test-dist/*.test.js --test-coverage-exclude=test-dist/stdio.js --test-coverage-exclude=test-dist/test-support.js --test-coverage-lines=90 --test-coverage-branches=70 --test-coverage-functions=85 \"test-dist/**/*.test.js\""
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@littlebigbrain/client": "^0.
|
|
55
|
+
"@littlebigbrain/client": "^0.23.0",
|
|
56
56
|
"@modelcontextprotocol/sdk": "^1",
|
|
57
57
|
"zod": "^3"
|
|
58
58
|
},
|