@littlebigbrain/mcp 0.10.0 → 0.11.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 +29 -18
- 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 +94 -11
- package/dist/tools.js +279 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -114,23 +114,29 @@ An agent without a SPARQL query can pass the question in plain words:
|
|
|
114
114
|
}
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
-
The server
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
A question
|
|
131
|
-
`
|
|
132
|
-
|
|
133
|
-
|
|
117
|
+
The server answers in plain words (`POST /v1/query/ask`). A router model picks
|
|
118
|
+
the kind of question. A reasoning model runs queries in a bounded loop, reads
|
|
119
|
+
their rows, and answers. The result holds `answer` (`text`, `citations`, the
|
|
120
|
+
IRIs the answer names, and `chart`, how to draw the rows, when the server
|
|
121
|
+
gave one), `steps`, the route, and the query and rows the answer stands on. `next` continues that query with `mode: "sparql"`. A
|
|
122
|
+
question takes about 8 s. Each question uses model tokens and counts toward a
|
|
123
|
+
daily limit of the stack, so the tool does not retry a failed call.
|
|
124
|
+
|
|
125
|
+
The server finds the names in the question in the graph and lists them in
|
|
126
|
+
`linked`. When the question is about records you already know, pass their
|
|
127
|
+
IRIs in `anchor` (at most 10): the queries use them directly. A question
|
|
128
|
+
about a date reads the last commit written by the end of that day; pass
|
|
129
|
+
`timeline` (`[{date, as_of_commit_seq, label?}]`) when the commits stand for
|
|
130
|
+
other dates. A question that asks what changed returns `history.added`,
|
|
131
|
+
`history.removed` and `history.changed`, with `history.totals`.
|
|
132
|
+
|
|
133
|
+
When the tool call carries a `_meta.progressToken`, the server sends a
|
|
134
|
+
progress notification for each step, for example `Route: lookup (router,
|
|
135
|
+
0.92)` or `Step 2, sparql SELECT ?db: 1 row`. The result is the same. Cancel
|
|
136
|
+
the call to stop the server's work.
|
|
137
|
+
|
|
138
|
+
To write the queries yourself, use `names`, `describe`, `commit_at` and
|
|
139
|
+
`compare`, then `mode: "sparql"`.
|
|
134
140
|
|
|
135
141
|
For your own data, start with `lbb_inspect` using `action: "guide"` or
|
|
136
142
|
`action: "ontology"`. Imported Resource Description Framework (RDF) data keeps
|
|
@@ -141,14 +147,19 @@ its original identifiers; inspect it with SPARQL when choosing query predicates.
|
|
|
141
147
|
| Tool | Purpose |
|
|
142
148
|
| --- | --- |
|
|
143
149
|
| `lbb_inspect` | Read the schema, graph status, and entities. |
|
|
144
|
-
| `lbb_query` | Ask a question in plain words, run SPARQL queries, search by meaning (also inside a SPARQL query with `search:similarTo`), or request summary statistics. |
|
|
150
|
+
| `lbb_query` | Ask a question in plain words, run SPARQL queries, search by meaning (also inside a SPARQL query with `search:similarTo`), or request summary statistics. Four modes help an agent write its own queries: `names` (the IRIs of the names in a text), `describe` (the classes and properties a question needs, with how many instances hold each property and the values of small classes), `commit_at` (the commit of a date) and `compare` (one query at two points, the rows paired by entity). |
|
|
145
151
|
| `lbb_rdf` | Import RDF documents or add facts with SPARQL `INSERT DATA`. |
|
|
146
152
|
| `lbb_embeddings` | Inspect search setup and preview the text to embed. |
|
|
147
153
|
| `lbb_embeddings_manage` | Set up or refresh embeddings, or change the embedding model. |
|
|
148
154
|
| `lbb_embeddings_delete` | Delete an embedding and its stored vectors. |
|
|
155
|
+
| `lbb_fit_sources` | Inspect fit from text and preview the text the fit reads. |
|
|
156
|
+
| `lbb_fit_sources_manage` | Declare, dry-run or refresh a fit source. |
|
|
157
|
+
| `lbb_fit_sources_delete` | Delete a fit source; its suggestions stay. |
|
|
149
158
|
| `lbb_commit` | Write or retract JSON facts, or record search feedback. |
|
|
150
159
|
| `lbb_configure` | Define record types and relationships, list and apply ontology starters (CRM, documents, work), publish validation rules, or keep the graph's notes and worked examples for questions in plain words (`get_rewrite_profile`, `set_rewrite_profile`). |
|
|
151
160
|
| `lbb_evals` | Label query results and check whether later queries return the expected answers. Read the model checks of a graph, and agree with or correct a check that the user confirmed. |
|
|
161
|
+
| `lbb_model_choice` | Read which model each use runs (question answers, query routing, search rerank, ontology fit, eval labels), and the trials that test other models against the graph's ground truth. |
|
|
162
|
+
| `lbb_model_choice_manage` | Start or stop a trial, switch a use to a tested model that meets the bar, or revert to LBB's model. |
|
|
152
163
|
| `lbb_models` | Compare retrieval settings, read model training datasets, or read a month of managed-model activity. |
|
|
153
164
|
|
|
154
165
|
See [search by meaning](https://docs.littlebigbrain.com/guides/search-by-meaning/)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { LbbClient } from "@littlebigbrain/client";
|
|
2
|
+
import { type Detail } from "./tool-contracts.js";
|
|
3
|
+
/** One value of a row, as SPARQL 1.1 Query Results JSON writes it. */
|
|
4
|
+
export interface Term {
|
|
5
|
+
type: string;
|
|
6
|
+
value: string;
|
|
7
|
+
datatype?: string | null;
|
|
8
|
+
"xml:lang"?: string | null;
|
|
9
|
+
}
|
|
10
|
+
/** A row as `{ variable: lexical value }`. */
|
|
11
|
+
export declare function lexicalRow(row: Record<string, Term>): Record<string, string>;
|
|
12
|
+
/** Entries of each list a page shows, by detail. */
|
|
13
|
+
export declare function listLimit(detail: Detail): number;
|
|
14
|
+
export interface NamesArgs {
|
|
15
|
+
text: string;
|
|
16
|
+
limit?: number;
|
|
17
|
+
detail?: string;
|
|
18
|
+
graph?: string;
|
|
19
|
+
}
|
|
20
|
+
/** `lbb_query mode=names`: the entities the text names, per name. */
|
|
21
|
+
export declare function findNames(client: LbbClient, args: NamesArgs): Promise<{
|
|
22
|
+
content: {
|
|
23
|
+
type: "text";
|
|
24
|
+
text: string;
|
|
25
|
+
}[];
|
|
26
|
+
structuredContent: Record<string, unknown>;
|
|
27
|
+
}>;
|
|
28
|
+
export interface DescribeArgs {
|
|
29
|
+
question?: string;
|
|
30
|
+
classes?: string[];
|
|
31
|
+
properties?: string[];
|
|
32
|
+
detail?: string;
|
|
33
|
+
graph?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* `lbb_query mode=describe`: the parts of the graph a question needs. The
|
|
37
|
+
* compact and standard answers are the server's text; full adds the same
|
|
38
|
+
* description as JSON.
|
|
39
|
+
*/
|
|
40
|
+
export declare function describeGraph(client: LbbClient, args: DescribeArgs): Promise<{
|
|
41
|
+
content: {
|
|
42
|
+
type: "text";
|
|
43
|
+
text: string;
|
|
44
|
+
}[];
|
|
45
|
+
structuredContent: Record<string, unknown>;
|
|
46
|
+
}>;
|
|
47
|
+
export interface CommitAtArgs {
|
|
48
|
+
date?: string;
|
|
49
|
+
moment?: string;
|
|
50
|
+
graph?: string;
|
|
51
|
+
}
|
|
52
|
+
/** `lbb_query mode=commit_at`: the commit of a date or a moment. */
|
|
53
|
+
export declare function commitAt(client: LbbClient, args: CommitAtArgs): Promise<{
|
|
54
|
+
content: {
|
|
55
|
+
type: "text";
|
|
56
|
+
text: string;
|
|
57
|
+
}[];
|
|
58
|
+
structuredContent: Record<string, unknown>;
|
|
59
|
+
}>;
|
|
60
|
+
interface ComparePoint {
|
|
61
|
+
as_of_commit_seq?: number;
|
|
62
|
+
date?: string;
|
|
63
|
+
moment?: string;
|
|
64
|
+
}
|
|
65
|
+
export interface CompareArgs {
|
|
66
|
+
query: string;
|
|
67
|
+
before: ComparePoint;
|
|
68
|
+
after?: ComparePoint;
|
|
69
|
+
key?: string[];
|
|
70
|
+
entailment?: "none" | "subclass" | "rdfs" | "owl";
|
|
71
|
+
max_rows?: number;
|
|
72
|
+
limit?: number;
|
|
73
|
+
compare_cursor?: string;
|
|
74
|
+
detail?: string;
|
|
75
|
+
graph?: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* `lbb_query mode=compare`: one SELECT at two points, the rows paired by
|
|
79
|
+
* `key`. Each list shows one page with its total; `next` continues with the
|
|
80
|
+
* server's cursor.
|
|
81
|
+
*/
|
|
82
|
+
export declare function compareQuery(client: LbbClient, args: CompareArgs): Promise<{
|
|
83
|
+
content: {
|
|
84
|
+
type: "text";
|
|
85
|
+
text: string;
|
|
86
|
+
}[];
|
|
87
|
+
structuredContent: Record<string, unknown>;
|
|
88
|
+
}>;
|
|
89
|
+
export {};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { HARD_OUTPUT_CHARS } from "./tool-contracts.js";
|
|
2
|
+
import { compactLimits, normalizeDetail, scoped, toolResult, truncateValue, } from "./tool-runtime.js";
|
|
3
|
+
/** A row as `{ variable: lexical value }`. */
|
|
4
|
+
export function lexicalRow(row) {
|
|
5
|
+
return Object.fromEntries(Object.entries(row).map(([name, term]) => [name, term.value]));
|
|
6
|
+
}
|
|
7
|
+
/** Entries of each list a page shows, by detail. */
|
|
8
|
+
export function listLimit(detail) {
|
|
9
|
+
switch (detail) {
|
|
10
|
+
case "full":
|
|
11
|
+
return 500;
|
|
12
|
+
case "standard":
|
|
13
|
+
return 100;
|
|
14
|
+
default:
|
|
15
|
+
return 20;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Long strings cut by detail; every entry stays, so no cursor skips one. */
|
|
19
|
+
function boundStrings(value, detail) {
|
|
20
|
+
return truncateValue(value, {
|
|
21
|
+
maxItems: Number.MAX_SAFE_INTEGER,
|
|
22
|
+
maxString: compactLimits(detail).maxString,
|
|
23
|
+
}, { truncated: false });
|
|
24
|
+
}
|
|
25
|
+
/** A tool result that must fit the MCP output budget whole. */
|
|
26
|
+
function fitted(value, narrow) {
|
|
27
|
+
const text = JSON.stringify(value, null, 2);
|
|
28
|
+
if (Buffer.byteLength(text, "utf8") > HARD_OUTPUT_CHARS) {
|
|
29
|
+
throw new Error(`The answer exceeds the MCP output budget. ${narrow}`);
|
|
30
|
+
}
|
|
31
|
+
return toolResult(value);
|
|
32
|
+
}
|
|
33
|
+
/** `lbb_query mode=names`: the entities the text names, per name. */
|
|
34
|
+
export async function findNames(client, args) {
|
|
35
|
+
const target = scoped(client, args.graph);
|
|
36
|
+
const response = await target.request("POST", "/v1/query/names", {
|
|
37
|
+
body: {
|
|
38
|
+
text: args.text,
|
|
39
|
+
...(args.limit !== undefined ? { limit: args.limit } : {}),
|
|
40
|
+
},
|
|
41
|
+
retry: true,
|
|
42
|
+
});
|
|
43
|
+
const names = [];
|
|
44
|
+
for (const { text, ...candidate } of response.candidates) {
|
|
45
|
+
const last = names[names.length - 1];
|
|
46
|
+
if (last && last.text === text)
|
|
47
|
+
last.candidates.push(candidate);
|
|
48
|
+
else
|
|
49
|
+
names.push({ text, candidates: [candidate] });
|
|
50
|
+
}
|
|
51
|
+
const notes = [];
|
|
52
|
+
if (!response.index_ready) {
|
|
53
|
+
notes.push("The graph's name index is still building: ask again in a few seconds.");
|
|
54
|
+
}
|
|
55
|
+
else if (names.length === 0) {
|
|
56
|
+
notes.push('No name of the text matched an entity. Match the name in SPARQL instead, without case and on a short distinctive part, for example FILTER(CONTAINS(LCASE(STR(?label)), "quellm")).');
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
notes.push("Use the first candidate of each name unless its class does not fit the question; put its IRI in the query (as a subject, an object or in VALUES) instead of matching the name.");
|
|
60
|
+
}
|
|
61
|
+
const count = response.candidates.length;
|
|
62
|
+
return fitted({
|
|
63
|
+
summary: `lbb_query.names: ${names.length} ${names.length === 1 ? "name" : "names"}, ${count} ${count === 1 ? "candidate" : "candidates"}`,
|
|
64
|
+
data: {
|
|
65
|
+
names,
|
|
66
|
+
index_ready: response.index_ready,
|
|
67
|
+
commit_seq: response.commit_seq,
|
|
68
|
+
},
|
|
69
|
+
notes,
|
|
70
|
+
}, "Pass a shorter text.");
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* `lbb_query mode=describe`: the parts of the graph a question needs. The
|
|
74
|
+
* compact and standard answers are the server's text; full adds the same
|
|
75
|
+
* description as JSON.
|
|
76
|
+
*/
|
|
77
|
+
export async function describeGraph(client, args) {
|
|
78
|
+
const detail = normalizeDetail(args.detail);
|
|
79
|
+
const target = scoped(client, args.graph);
|
|
80
|
+
const response = await target.request("POST", "/v1/query/describe", {
|
|
81
|
+
body: {
|
|
82
|
+
...(args.question !== undefined ? { question: args.question } : {}),
|
|
83
|
+
...(args.classes?.length ? { classes: args.classes } : {}),
|
|
84
|
+
...(args.properties?.length ? { properties: args.properties } : {}),
|
|
85
|
+
},
|
|
86
|
+
retry: true,
|
|
87
|
+
});
|
|
88
|
+
const notes = [];
|
|
89
|
+
if (response.partial) {
|
|
90
|
+
notes.push("The server is still sampling the graph: coverage and the values of small classes can be missing. Ask again in a minute for the whole description.");
|
|
91
|
+
}
|
|
92
|
+
if (response.unknown?.length) {
|
|
93
|
+
notes.push(`The graph has no class or property ${response.unknown.join(", ")}.`);
|
|
94
|
+
}
|
|
95
|
+
notes.push("A property that few sampled instances hold (n of m) is rare: a filter on it returns few rows. Use the listed values of small classes as they are written.");
|
|
96
|
+
const data = {
|
|
97
|
+
commit_seq: response.commit_seq,
|
|
98
|
+
partial: response.partial,
|
|
99
|
+
text: response.text,
|
|
100
|
+
...(response.unknown?.length ? { unknown: response.unknown } : {}),
|
|
101
|
+
};
|
|
102
|
+
if (detail === "full") {
|
|
103
|
+
data.classes = response.classes;
|
|
104
|
+
data.properties = response.properties;
|
|
105
|
+
data.statements = response.statements;
|
|
106
|
+
data.prefixes = response.prefixes;
|
|
107
|
+
}
|
|
108
|
+
return fitted({
|
|
109
|
+
summary: `lbb_query.describe: ${response.classes.length} classes, ${response.properties.length} properties`,
|
|
110
|
+
data,
|
|
111
|
+
notes,
|
|
112
|
+
}, "Name fewer classes, or use detail=standard.");
|
|
113
|
+
}
|
|
114
|
+
/** `lbb_query mode=commit_at`: the commit of a date or a moment. */
|
|
115
|
+
export async function commitAt(client, args) {
|
|
116
|
+
if ((args.date === undefined) === (args.moment === undefined)) {
|
|
117
|
+
throw new Error("commit_at needs exactly one of date and moment");
|
|
118
|
+
}
|
|
119
|
+
const target = scoped(client, args.graph);
|
|
120
|
+
const response = await target.request("GET", "/v1/graph/commit-at", {
|
|
121
|
+
query: { date: args.date, moment: args.moment },
|
|
122
|
+
retry: true,
|
|
123
|
+
});
|
|
124
|
+
const notes = [];
|
|
125
|
+
if (typeof response.as_of_commit_seq === "number") {
|
|
126
|
+
notes.push(`Read the graph as it was then with lbb_query mode=sparql as_of_commit_seq=${response.as_of_commit_seq}, or compare two points with lbb_query mode=compare.`);
|
|
127
|
+
}
|
|
128
|
+
if (response.note)
|
|
129
|
+
notes.push(response.note);
|
|
130
|
+
return toolResult({
|
|
131
|
+
summary: typeof response.as_of_commit_seq === "number"
|
|
132
|
+
? `lbb_query.commit_at: commit ${response.as_of_commit_seq}`
|
|
133
|
+
: "lbb_query.commit_at: no commit",
|
|
134
|
+
data: response,
|
|
135
|
+
notes,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* `lbb_query mode=compare`: one SELECT at two points, the rows paired by
|
|
140
|
+
* `key`. Each list shows one page with its total; `next` continues with the
|
|
141
|
+
* server's cursor.
|
|
142
|
+
*/
|
|
143
|
+
export async function compareQuery(client, args) {
|
|
144
|
+
const detail = normalizeDetail(args.detail);
|
|
145
|
+
const target = scoped(client, args.graph);
|
|
146
|
+
const limit = args.limit ?? listLimit(detail);
|
|
147
|
+
const response = await target.request("POST", "/v1/query/compare", {
|
|
148
|
+
body: {
|
|
149
|
+
query: args.query,
|
|
150
|
+
before: args.before,
|
|
151
|
+
...(args.after ? { after: args.after } : {}),
|
|
152
|
+
...(args.key?.length ? { key: args.key } : {}),
|
|
153
|
+
...(args.entailment ? { entailment: args.entailment } : {}),
|
|
154
|
+
...(args.max_rows !== undefined ? { max_rows: args.max_rows } : {}),
|
|
155
|
+
limit,
|
|
156
|
+
...(args.compare_cursor ? { cursor: args.compare_cursor } : {}),
|
|
157
|
+
},
|
|
158
|
+
retry: true,
|
|
159
|
+
query: { consistency: target.defaultConsistency },
|
|
160
|
+
});
|
|
161
|
+
const notes = [...(response.notes ?? [])];
|
|
162
|
+
const totals = response.totals;
|
|
163
|
+
const shown = {
|
|
164
|
+
added: response.added.length,
|
|
165
|
+
removed: response.removed.length,
|
|
166
|
+
changed: response.changed.length,
|
|
167
|
+
};
|
|
168
|
+
const cut = Object.keys(shown).filter((name) => response.offset + shown[name] < totals[name]);
|
|
169
|
+
if (cut.length) {
|
|
170
|
+
notes.push(`This page shows entries ${response.offset + 1} to ${response.offset + limit} of each list (${cut.map((name) => `${name}: ${totals[name]}`).join(", ")}). Call again with the arguments in next for the next page.`);
|
|
171
|
+
}
|
|
172
|
+
const data = boundStrings({
|
|
173
|
+
before: response.before,
|
|
174
|
+
after: response.after,
|
|
175
|
+
key: response.key ?? [],
|
|
176
|
+
totals,
|
|
177
|
+
added: response.added.map(lexicalRow),
|
|
178
|
+
removed: response.removed.map(lexicalRow),
|
|
179
|
+
changed: response.changed.map((change) => ({
|
|
180
|
+
key: lexicalRow(change.key),
|
|
181
|
+
before: change.before.map(lexicalRow),
|
|
182
|
+
after: change.after.map(lexicalRow),
|
|
183
|
+
})),
|
|
184
|
+
offset: response.offset,
|
|
185
|
+
...(response.truncated ? { truncated: true } : {}),
|
|
186
|
+
}, detail);
|
|
187
|
+
const next = response.next_cursor
|
|
188
|
+
? {
|
|
189
|
+
mode: "compare",
|
|
190
|
+
query: args.query,
|
|
191
|
+
before: args.before,
|
|
192
|
+
...(args.after ? { after: args.after } : {}),
|
|
193
|
+
...(args.key?.length ? { key: args.key } : {}),
|
|
194
|
+
...(args.entailment ? { entailment: args.entailment } : {}),
|
|
195
|
+
...(args.max_rows !== undefined ? { max_rows: args.max_rows } : {}),
|
|
196
|
+
limit,
|
|
197
|
+
compare_cursor: response.next_cursor,
|
|
198
|
+
...(args.detail ? { detail: args.detail } : {}),
|
|
199
|
+
...(args.graph ? { graph: args.graph } : {}),
|
|
200
|
+
}
|
|
201
|
+
: undefined;
|
|
202
|
+
return fitted({
|
|
203
|
+
summary: `lbb_query.compare: added ${totals.added}, removed ${totals.removed}, changed ${totals.changed}${response.truncated ? " [not complete: a point was not read whole]" : ""}`,
|
|
204
|
+
data,
|
|
205
|
+
...(next ? { next } : {}),
|
|
206
|
+
notes,
|
|
207
|
+
}, "Pass a smaller limit, or project fewer variables.");
|
|
208
|
+
}
|
package/dist/question.d.ts
CHANGED
|
@@ -11,7 +11,6 @@ export interface QuestionArgs {
|
|
|
11
11
|
context?: string;
|
|
12
12
|
route?: QuestionRoute;
|
|
13
13
|
limit?: number;
|
|
14
|
-
run?: boolean;
|
|
15
14
|
anchor?: string[];
|
|
16
15
|
timeline?: TimelinePoint[];
|
|
17
16
|
detail?: string;
|
|
@@ -26,7 +25,7 @@ export interface QuestionProgress {
|
|
|
26
25
|
signal?: AbortSignal;
|
|
27
26
|
report(progress: number, message: string): Promise<void>;
|
|
28
27
|
}
|
|
29
|
-
/** One event of a streamed
|
|
28
|
+
/** One event of a streamed question, as this tool reads it. */
|
|
30
29
|
type StreamEvent = {
|
|
31
30
|
event: string;
|
|
32
31
|
data: unknown;
|
|
@@ -34,11 +33,14 @@ type StreamEvent = {
|
|
|
34
33
|
/** The progress message of a stream event, in short plain words. */
|
|
35
34
|
export declare function progressMessage(event: StreamEvent): string | undefined;
|
|
36
35
|
/**
|
|
37
|
-
* `lbb_query mode=question`: the server
|
|
38
|
-
* query
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
36
|
+
* `lbb_query mode=question`: the server answers the question in plain words
|
|
37
|
+
* (`POST /v1/query/ask`). Its model runs queries in a bounded loop and
|
|
38
|
+
* reads their rows. The result holds the answer, its citations and chart
|
|
39
|
+
* hint, the steps, and the rows of the query the answer stands on, bounded
|
|
40
|
+
* as `mode=sparql`
|
|
41
|
+
* bounds them; `next` continues that query with `mode=sparql`. With
|
|
42
|
+
* `progress`, the server streams its steps and each step is reported; the
|
|
43
|
+
* result is the same.
|
|
42
44
|
*/
|
|
43
45
|
export declare function answerQuestion(client: LbbClient, args: QuestionArgs, textFormat?: "compact" | "pretty", progress?: QuestionProgress): Promise<{
|
|
44
46
|
content: {
|
package/dist/question.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { lexicalRow, listLimit } from "./query-tools.js";
|
|
1
2
|
import { QUESTION_MAX_ROWS } from "./tool-contracts.js";
|
|
2
3
|
import { defaultRowLimit, normalizeDetail, queryEnvelope, queryToolResult, rowPageFrom, rowPageNext, scoped, } from "./tool-runtime.js";
|
|
3
|
-
const REPAIR_ERROR_CHARS = 200;
|
|
4
4
|
/** The progress message of a stream event, in short plain words. */
|
|
5
5
|
export function progressMessage(event) {
|
|
6
6
|
const data = (event.data ?? {});
|
|
@@ -13,41 +13,35 @@ export function progressMessage(event) {
|
|
|
13
13
|
: "";
|
|
14
14
|
return `Route: ${String(data.kind)} (${String(data.by)}${confidence})`;
|
|
15
15
|
}
|
|
16
|
-
case "
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
?
|
|
21
|
-
:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
case "rows": {
|
|
25
|
-
const count = Number(data.count);
|
|
26
|
-
return `${count} ${count === 1 ? "row" : "rows"} in ${String(data.ms)} ms`;
|
|
27
|
-
}
|
|
28
|
-
case "repair": {
|
|
29
|
-
const error = String(data.error ?? "");
|
|
30
|
-
const shown = error.length > REPAIR_ERROR_CHARS
|
|
31
|
-
? `${error.slice(0, REPAIR_ERROR_CHARS - 1)}…`
|
|
32
|
-
: error;
|
|
33
|
-
return `Correcting the query: ${shown}`;
|
|
16
|
+
case "step": {
|
|
17
|
+
// A compare step counts the entries that differ, not rows.
|
|
18
|
+
const unit = data.tool === "compare" ? "difference" : "row";
|
|
19
|
+
const rows = typeof data.rows === "number"
|
|
20
|
+
? `: ${data.rows} ${unit}${data.rows === 1 ? "" : "s"}`
|
|
21
|
+
: "";
|
|
22
|
+
const outcome = data.ok === false ? ": failed" : rows;
|
|
23
|
+
return `Step ${String(data.n)}, ${String(data.tool)} ${String(data.input ?? "")}${outcome}`;
|
|
34
24
|
}
|
|
25
|
+
case "answer":
|
|
26
|
+
return "Answered";
|
|
35
27
|
default:
|
|
36
28
|
return undefined;
|
|
37
29
|
}
|
|
38
30
|
}
|
|
39
31
|
/**
|
|
40
|
-
* The
|
|
32
|
+
* The question as a stream of progress events. Each event becomes one
|
|
41
33
|
* progress notification; the `done` event is the same response as the call
|
|
42
|
-
* without a stream. A client release without `query.
|
|
34
|
+
* without a stream. A client release without `query.askStream` answers
|
|
43
35
|
* without progress.
|
|
44
36
|
*/
|
|
45
|
-
async function
|
|
37
|
+
async function streamedAnswer(target, body, progress) {
|
|
46
38
|
const query = target.query;
|
|
47
|
-
if (typeof query.
|
|
39
|
+
if (typeof query.askStream !== "function")
|
|
48
40
|
return undefined;
|
|
41
|
+
const { question, ...options } = body;
|
|
49
42
|
let step = 0;
|
|
50
|
-
for await (const event of query.
|
|
43
|
+
for await (const event of query.askStream.call(target.query, question, {
|
|
44
|
+
...options,
|
|
51
45
|
signal: progress.signal,
|
|
52
46
|
})) {
|
|
53
47
|
if (event.event === "done")
|
|
@@ -59,30 +53,50 @@ async function streamedRewrite(target, body, progress) {
|
|
|
59
53
|
// A lost notification never fails the question.
|
|
60
54
|
await progress.report(step, message).catch(() => undefined);
|
|
61
55
|
}
|
|
62
|
-
throw new Error("the
|
|
56
|
+
throw new Error("the answer stream ended before its done event");
|
|
63
57
|
}
|
|
64
|
-
/**
|
|
65
|
-
function
|
|
66
|
-
return
|
|
58
|
+
/** The totals of a comparison: the server's, else the lengths of its lists. */
|
|
59
|
+
function historyTotals(history) {
|
|
60
|
+
return (history.totals ?? {
|
|
61
|
+
added: history.added?.length ?? 0,
|
|
62
|
+
removed: history.removed?.length ?? 0,
|
|
63
|
+
changed: history.changed?.length ?? 0,
|
|
64
|
+
});
|
|
67
65
|
}
|
|
68
66
|
/**
|
|
69
67
|
* The history the tool returns: where the question read the graph, and for
|
|
70
|
-
* a comparison the rows that differ as lexical values
|
|
71
|
-
*
|
|
72
|
-
*
|
|
68
|
+
* a comparison the rows that differ as lexical values, with the total of
|
|
69
|
+
* each list. Each list shows at most `limit` entries; `shown` says how many
|
|
70
|
+
* when a list is cut.
|
|
73
71
|
*/
|
|
74
|
-
function historyView(history) {
|
|
72
|
+
function historyView(history, limit) {
|
|
75
73
|
const view = { ...history };
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
74
|
+
if (!history.added && !history.removed && !history.changed)
|
|
75
|
+
return view;
|
|
76
|
+
const totals = historyTotals(history);
|
|
77
|
+
view.totals = totals;
|
|
78
|
+
const shown = {};
|
|
79
|
+
const show = (name, list, map) => {
|
|
80
|
+
if (!list)
|
|
81
|
+
return;
|
|
82
|
+
const kept = list.slice(0, limit);
|
|
83
|
+
view[name] = kept.map(map);
|
|
84
|
+
if (kept.length < (totals[name] ?? list.length))
|
|
85
|
+
shown[name] = kept.length;
|
|
86
|
+
};
|
|
87
|
+
show("added", history.added, lexicalRow);
|
|
88
|
+
show("removed", history.removed, lexicalRow);
|
|
89
|
+
show("changed", history.changed, (change) => ({
|
|
90
|
+
key: lexicalRow(change.key),
|
|
91
|
+
before: change.before.map(lexicalRow),
|
|
92
|
+
after: change.after.map(lexicalRow),
|
|
93
|
+
}));
|
|
94
|
+
if (Object.keys(shown).length)
|
|
95
|
+
view.shown = shown;
|
|
82
96
|
return view;
|
|
83
97
|
}
|
|
84
98
|
/** What the agent should know about the history of the answer. */
|
|
85
|
-
function historyNotes(history) {
|
|
99
|
+
function historyNotes(history, limit, query) {
|
|
86
100
|
if (!history)
|
|
87
101
|
return [];
|
|
88
102
|
const notes = [];
|
|
@@ -100,36 +114,55 @@ function historyNotes(history) {
|
|
|
100
114
|
else if (history.as_of_date) {
|
|
101
115
|
notes.push(`The server could not place ${history.as_of_date} (see error). Pass timeline=[{date, as_of_commit_seq}] to name the commit of each date, or run the query with lbb_query mode=sparql as_of_commit_seq=<commit>.`);
|
|
102
116
|
}
|
|
103
|
-
if (history.compare && history.added && point) {
|
|
104
|
-
|
|
117
|
+
if (history.compare && (history.added || history.changed) && point) {
|
|
118
|
+
const totals = historyTotals(history);
|
|
119
|
+
// The compared query reads the later point.
|
|
120
|
+
const later = typeof query?.as_of_commit_seq === "number"
|
|
121
|
+
? query.as_of_commit_seq
|
|
122
|
+
: undefined;
|
|
123
|
+
const keyed = history.key?.length
|
|
124
|
+
? `paired by ${history.key.map((v) => `?${v}`).join(", ")}: history.added and history.removed hold the entities at one point only, history.changed the entities whose values differ`
|
|
125
|
+
: "history.added and history.removed hold the rows that differ (a changed value is a removed row and an added row)";
|
|
126
|
+
notes.push(`A comparison from ${point} to ${later === undefined ? "the latest commit" : `commit ${later}`}; ${keyed}. Totals: added ${totals.added}, removed ${totals.removed}, changed ${totals.changed}.`);
|
|
127
|
+
const cut = ["added", "removed", "changed"].filter((name) => totals[name] > limit);
|
|
128
|
+
if (cut.length && query) {
|
|
129
|
+
const after = later === undefined ? "" : ` after={"as_of_commit_seq": ${later}}`;
|
|
130
|
+
const key = history.key?.length
|
|
131
|
+
? ` key=${JSON.stringify(history.key)}`
|
|
132
|
+
: "";
|
|
133
|
+
notes.push(`Each list shows at most ${limit} entries (${cut.map((name) => `${name}: ${totals[name]}`).join(", ")}; see history.shown). Read them all, page by page, with lbb_query mode=compare query=<the query> before={"as_of_commit_seq": ${history.as_of_commit_seq}}${after}${key}.`);
|
|
134
|
+
}
|
|
135
|
+
if (history.truncated) {
|
|
136
|
+
notes.push("The difference is not complete: a point was not read whole, or a list held more than 500 entries on the server. Use lbb_query mode=compare for the whole difference.");
|
|
137
|
+
}
|
|
105
138
|
}
|
|
106
139
|
return notes;
|
|
107
140
|
}
|
|
108
141
|
/**
|
|
109
|
-
* `lbb_query mode=question`: the server
|
|
110
|
-
* query
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
142
|
+
* `lbb_query mode=question`: the server answers the question in plain words
|
|
143
|
+
* (`POST /v1/query/ask`). Its model runs queries in a bounded loop and
|
|
144
|
+
* reads their rows. The result holds the answer, its citations and chart
|
|
145
|
+
* hint, the steps, and the rows of the query the answer stands on, bounded
|
|
146
|
+
* as `mode=sparql`
|
|
147
|
+
* bounds them; `next` continues that query with `mode=sparql`. With
|
|
148
|
+
* `progress`, the server streams its steps and each step is reported; the
|
|
149
|
+
* result is the same.
|
|
114
150
|
*/
|
|
115
151
|
export async function answerQuestion(client, args, textFormat, progress) {
|
|
116
152
|
const detail = normalizeDetail(args.detail);
|
|
117
|
-
const run = args.run ?? true;
|
|
118
153
|
const rowLimit = Math.min(args.limit ?? defaultRowLimit(detail), QUESTION_MAX_ROWS);
|
|
119
154
|
const target = scoped(client, args.graph);
|
|
120
155
|
const body = {
|
|
121
156
|
question: args.question,
|
|
122
157
|
...(args.context !== undefined ? { context: args.context } : {}),
|
|
123
158
|
...(args.route !== undefined ? { route: args.route } : {}),
|
|
124
|
-
|
|
125
|
-
...(run ? { limit: rowLimit } : {}),
|
|
159
|
+
limit: rowLimit,
|
|
126
160
|
...(args.anchor?.length ? { anchor: args.anchor } : {}),
|
|
127
161
|
...(args.timeline?.length ? { timeline: args.timeline } : {}),
|
|
128
162
|
};
|
|
129
|
-
// Each call uses model tokens,
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
(await target.request("POST", "/v1/query/rewrite", {
|
|
163
|
+
// Each call uses model tokens, so a failed call is not retried.
|
|
164
|
+
const response = (progress ? await streamedAnswer(target, body, progress) : undefined) ??
|
|
165
|
+
(await target.request("POST", "/v1/query/ask", {
|
|
133
166
|
body,
|
|
134
167
|
retry: false,
|
|
135
168
|
query: { consistency: target.defaultConsistency },
|
|
@@ -137,6 +170,20 @@ export async function answerQuestion(client, args, textFormat, progress) {
|
|
|
137
170
|
const { route, query, result } = response;
|
|
138
171
|
const traceId = result?.trace_id ?? null;
|
|
139
172
|
const answer = {
|
|
173
|
+
answer: response.answer
|
|
174
|
+
? {
|
|
175
|
+
text: response.answer.text,
|
|
176
|
+
citations: response.answer.citations ?? [],
|
|
177
|
+
...(response.answer.chart ? { chart: response.answer.chart } : {}),
|
|
178
|
+
}
|
|
179
|
+
: null,
|
|
180
|
+
steps: (response.steps ?? []).map((step) => ({
|
|
181
|
+
n: step.n,
|
|
182
|
+
tool: step.tool,
|
|
183
|
+
ok: step.ok,
|
|
184
|
+
...(typeof step.rows === "number" ? { rows: step.rows } : {}),
|
|
185
|
+
...(step.error ? { error: step.error } : {}),
|
|
186
|
+
})),
|
|
140
187
|
route: { kind: route.kind, confidence: route.confidence, by: route.by },
|
|
141
188
|
rationale: response.rationale,
|
|
142
189
|
sparql: query?.sparql ?? null,
|
|
@@ -144,7 +191,9 @@ export async function answerQuestion(client, args, textFormat, progress) {
|
|
|
144
191
|
...(typeof query?.as_of_commit_seq === "number"
|
|
145
192
|
? { as_of_commit_seq: query.as_of_commit_seq }
|
|
146
193
|
: {}),
|
|
147
|
-
...(response.history
|
|
194
|
+
...(response.history
|
|
195
|
+
? { history: historyView(response.history, listLimit(detail)) }
|
|
196
|
+
: {}),
|
|
148
197
|
error: response.error ?? null,
|
|
149
198
|
trace_id: traceId,
|
|
150
199
|
attempts: response.attempts,
|
|
@@ -160,9 +209,9 @@ export async function answerQuestion(client, args, textFormat, progress) {
|
|
|
160
209
|
if (traceId) {
|
|
161
210
|
notes.push(`eval trace ${traceId}: to label rows, read their item ids with lbb_evals action=trace trace_id=${traceId}, then call lbb_evals action=label trace_id=${traceId} item=<id> valid=true|false (or items=[…]).`);
|
|
162
211
|
}
|
|
163
|
-
notes.push(...historyNotes(response.history));
|
|
164
|
-
if (response.
|
|
165
|
-
notes.push("The
|
|
212
|
+
notes.push(...historyNotes(response.history, listLimit(detail), query));
|
|
213
|
+
if (!response.answer) {
|
|
214
|
+
notes.push("The answer loop stopped before it answered (see error); the rows are the best ones it read. Continue with lbb_query mode=sparql, or ask again with more context.");
|
|
166
215
|
}
|
|
167
216
|
const label = `lbb_query.question: route ${route.kind} (${route.by}, confidence ${route.confidence.toFixed(2)})`;
|
|
168
217
|
const data = result
|
|
@@ -186,13 +235,15 @@ export async function answerQuestion(client, args, textFormat, progress) {
|
|
|
186
235
|
};
|
|
187
236
|
return queryToolResult(queryEnvelope(label, { ...answer, ...data }, detail, rowPage, rowPageNext(cursorBase, rowPage), cursorBase, { textFormat, notes }), textFormat);
|
|
188
237
|
}
|
|
189
|
-
// No rows to page:
|
|
238
|
+
// No rows to page: an ASK answer, a comparison, or no query at all. The
|
|
190
239
|
// query text is returned whole.
|
|
191
240
|
const summary = data
|
|
192
241
|
? `${label}: ran the query`
|
|
193
|
-
:
|
|
194
|
-
? `${label}:
|
|
195
|
-
:
|
|
242
|
+
: response.history?.compare
|
|
243
|
+
? `${label}: compared two points`
|
|
244
|
+
: query
|
|
245
|
+
? `${label}: no rows`
|
|
246
|
+
: `${label}: no query`;
|
|
196
247
|
return queryToolResult({
|
|
197
248
|
summary,
|
|
198
249
|
data: { ...answer, ...(data ?? {}) },
|
package/dist/tool-contracts.js
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
export const DEFAULT_DETAIL = "compact";
|
|
3
3
|
export const HARD_OUTPUT_CHARS = 80_000;
|
|
4
4
|
export const MAX_QUERY_ROW_LIMIT = 5_000;
|
|
5
|
-
/** The most rows `POST /v1/query/
|
|
5
|
+
/** The most rows each query of `POST /v1/query/ask` returns. */
|
|
6
6
|
export const QUESTION_MAX_ROWS = 1_000;
|
|
7
7
|
/** Entity IRIs one question may anchor, and the characters of one. */
|
|
8
8
|
export const QUESTION_MAX_ANCHORS = 10;
|
|
@@ -405,6 +405,17 @@ export const SPARQL_IRI_GUIDE = 'IRI scheme: relations are <https://littlebigbra
|
|
|
405
405
|
// Search by meaning runs inside SPARQL text as a pattern, so an agent can
|
|
406
406
|
// filter and join the hits in one query instead of a search then a lookup.
|
|
407
407
|
export const SPARQL_SEARCH_GUIDE = 'Search by meaning inside the query (the graph needs an embedding; see lbb_embeddings action=list): PREFIX search: <https://littlebigbrain.com/search#> SELECT ?x ?score WHERE { ?x search:similarTo "card payments" ; search:score ?score . ?x <https://littlebigbrain.com/r/calls> ?y } ORDER BY DESC(?score) LIMIT 5. The other patterns filter and join the hits. One search:similarTo per query, with a variable subject, in the main group (not inside OPTIONAL, UNION, MINUS or a subquery). The object is text, an entity IRI (records like that one) or a vector literal. search:top N sets the hits (1 to 1000; default the query LIMIT, then 10) and search:embedding "name" searches one embedding. FILTER(?score > 0.8) keeps close hits. The result\'s search field reports the plan (nearest, filter_first or search_first), the hits and complete. For a question in words, add search:rerank true ; search:relevance ?r and ORDER BY DESC(?r): the rerank model (Jev) reads the best hits with the text, keeps the top that answer it and binds its answer (0 to 1); search.rerank reports the status. Only a text query reranks.';
|
|
408
|
+
/** One point of `lbb_query mode=compare`: a commit, a date or a moment. */
|
|
409
|
+
const comparePointSchema = z
|
|
410
|
+
.object({
|
|
411
|
+
as_of_commit_seq: z.number().int().min(0).optional(),
|
|
412
|
+
date: z
|
|
413
|
+
.string()
|
|
414
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
415
|
+
.optional(),
|
|
416
|
+
moment: z.string().optional(),
|
|
417
|
+
})
|
|
418
|
+
.strict();
|
|
408
419
|
export const queryInputSchema = z.discriminatedUnion("mode", [
|
|
409
420
|
z
|
|
410
421
|
.object({
|
|
@@ -447,7 +458,7 @@ export const queryInputSchema = z.discriminatedUnion("mode", [
|
|
|
447
458
|
query: z
|
|
448
459
|
.string()
|
|
449
460
|
.optional()
|
|
450
|
-
.describe(`SPARQL 1.1 query text (SELECT or ASK). Valid-time as_of is unsupported; use as_of_commit_seq for a retained commit snapshot. ${SPARQL_IRI_GUIDE} Example: SELECT ?service ?db WHERE { ?service <https://littlebigbrain.com/r/writes_to> ?db } LIMIT 10. ${SPARQL_SEARCH_GUIDE}
|
|
461
|
+
.describe(`SPARQL 1.1 query text (SELECT or ASK). Valid-time as_of is unsupported; use as_of_commit_seq for a retained commit snapshot. ${SPARQL_IRI_GUIDE} Example: SELECT ?service ?db WHERE { ?service <https://littlebigbrain.com/r/writes_to> ?db } LIMIT 10. ${SPARQL_SEARCH_GUIDE} compare: the SELECT to run at both points; leave out LIMIT.`),
|
|
451
462
|
// Kept for an actionable error when an older connector sends this field.
|
|
452
463
|
as_of: z
|
|
453
464
|
.string()
|
|
@@ -474,7 +485,7 @@ export const queryInputSchema = z.discriminatedUnion("mode", [
|
|
|
474
485
|
question: z
|
|
475
486
|
.string()
|
|
476
487
|
.min(1)
|
|
477
|
-
.describe("question: the question in plain words (required), 1 to 4,000 characters. The server
|
|
488
|
+
.describe("question: the question in plain words (required), 1 to 4,000 characters. The server's model runs queries on this graph, reads their rows, and answers. describe: the question whose classes and properties to describe."),
|
|
478
489
|
context: z
|
|
479
490
|
.string()
|
|
480
491
|
.optional()
|
|
@@ -489,23 +500,19 @@ export const queryInputSchema = z.discriminatedUnion("mode", [
|
|
|
489
500
|
"unanswerable",
|
|
490
501
|
])
|
|
491
502
|
.optional()
|
|
492
|
-
.describe("question: the kind of
|
|
503
|
+
.describe("question: the kind of question, when you know it. Omit it and the router model picks it."),
|
|
493
504
|
limit: z
|
|
494
505
|
.number()
|
|
495
506
|
.int()
|
|
496
507
|
.min(1)
|
|
497
508
|
.max(QUESTION_MAX_ROWS)
|
|
498
509
|
.optional()
|
|
499
|
-
.describe("question: rows the
|
|
500
|
-
run: z
|
|
501
|
-
.boolean()
|
|
502
|
-
.optional()
|
|
503
|
-
.describe("question: run the query and return its rows. Defaults to true; false returns the query only."),
|
|
510
|
+
.describe("question: rows each query of the server's loop returns, 1 to 1,000. Defaults by detail: compact=20, standard=100, full=1000. names: candidates per name, 1 to 10 (default 5). compare: entries of each list per page, 1 to 1,000 (defaults by detail: 20, 100, 500)."),
|
|
504
511
|
anchor: z
|
|
505
512
|
.array(z.string().min(1).max(QUESTION_MAX_ANCHOR_CHARS))
|
|
506
513
|
.max(QUESTION_MAX_ANCHORS)
|
|
507
514
|
.optional()
|
|
508
|
-
.describe("question: entity IRIs the question is about, at most 10 (for example the record the user has open). The server reads each one, and
|
|
515
|
+
.describe("question: entity IRIs the question is about, at most 10 (for example the record the user has open). The server reads each one, and its queries use these IRIs directly instead of matching their names."),
|
|
509
516
|
timeline: z
|
|
510
517
|
.array(z
|
|
511
518
|
.object({
|
|
@@ -541,7 +548,7 @@ export const queryInputSchema = z.discriminatedUnion("mode", [
|
|
|
541
548
|
text: z
|
|
542
549
|
.string()
|
|
543
550
|
.optional()
|
|
544
|
-
.describe("
|
|
551
|
+
.describe("search: the query text (required); embedded with the graph's model. names: the question, or the names, to find (required)."),
|
|
545
552
|
top_k: z.number().int().positive().max(200).optional(),
|
|
546
553
|
probe: z
|
|
547
554
|
.number()
|
|
@@ -605,6 +612,82 @@ export const queryInputSchema = z.discriminatedUnion("mode", [
|
|
|
605
612
|
...readScope,
|
|
606
613
|
})
|
|
607
614
|
.strict(),
|
|
615
|
+
z
|
|
616
|
+
.object({
|
|
617
|
+
mode: z.literal("names"),
|
|
618
|
+
text: z
|
|
619
|
+
.string()
|
|
620
|
+
.min(1)
|
|
621
|
+
.max(4_000)
|
|
622
|
+
.describe("names: the question, or the names, to find (required)."),
|
|
623
|
+
limit: z.number().int().min(1).max(10).optional(),
|
|
624
|
+
...readScope,
|
|
625
|
+
})
|
|
626
|
+
.strict(),
|
|
627
|
+
z
|
|
628
|
+
.object({
|
|
629
|
+
mode: z.literal("describe"),
|
|
630
|
+
question: z.string().max(4_000).optional(),
|
|
631
|
+
classes: z
|
|
632
|
+
.array(z.string().min(1))
|
|
633
|
+
.max(20)
|
|
634
|
+
.optional()
|
|
635
|
+
.describe("describe: class IRIs to describe, at most 20."),
|
|
636
|
+
properties: z
|
|
637
|
+
.array(z.string().min(1))
|
|
638
|
+
.max(50)
|
|
639
|
+
.optional()
|
|
640
|
+
.describe("describe: property IRIs to describe, at most 50."),
|
|
641
|
+
...readScope,
|
|
642
|
+
})
|
|
643
|
+
.strict(),
|
|
644
|
+
z
|
|
645
|
+
.object({
|
|
646
|
+
mode: z.literal("commit_at"),
|
|
647
|
+
date: z
|
|
648
|
+
.string()
|
|
649
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
650
|
+
.optional()
|
|
651
|
+
.describe("commit_at: YYYY-MM-DD; the last commit written by the end of that day (UTC)."),
|
|
652
|
+
moment: z
|
|
653
|
+
.string()
|
|
654
|
+
.optional()
|
|
655
|
+
.describe("commit_at: RFC 3339, e.g. 2026-06-18T12:00:00Z; the last commit written at or before it."),
|
|
656
|
+
...readScope,
|
|
657
|
+
})
|
|
658
|
+
.strict(),
|
|
659
|
+
z
|
|
660
|
+
.object({
|
|
661
|
+
mode: z.literal("compare"),
|
|
662
|
+
query: z
|
|
663
|
+
.string()
|
|
664
|
+
.min(1)
|
|
665
|
+
.describe("compare: the SELECT to run at both points. Leave out LIMIT: the server reads up to max_rows rows a point, page by page."),
|
|
666
|
+
before: comparePointSchema.describe('compare: the earlier point (required): {"as_of_commit_seq": n}, {"date": "YYYY-MM-DD"} or {"moment": "RFC 3339"}.'),
|
|
667
|
+
after: comparePointSchema
|
|
668
|
+
.optional()
|
|
669
|
+
.describe("compare: the later point; defaults to the latest commit."),
|
|
670
|
+
key: z
|
|
671
|
+
.array(z.string().min(1))
|
|
672
|
+
.max(8)
|
|
673
|
+
.optional()
|
|
674
|
+
.describe('compare: the variables that identify a row\'s entity, e.g. ["contact"]. With a key the rows are paired: added and removed hold the entities at one point only, changed the entities whose values differ. Without one whole rows are compared.'),
|
|
675
|
+
entailment: z.enum(["none", "subclass", "rdfs", "owl"]).optional(),
|
|
676
|
+
max_rows: z
|
|
677
|
+
.number()
|
|
678
|
+
.int()
|
|
679
|
+
.min(1)
|
|
680
|
+
.max(50_000)
|
|
681
|
+
.optional()
|
|
682
|
+
.describe("compare: rows to read per point, 1 to 50,000 (default 20,000). Past it the result says truncated."),
|
|
683
|
+
limit: z.number().int().min(1).max(1_000).optional(),
|
|
684
|
+
compare_cursor: z
|
|
685
|
+
.string()
|
|
686
|
+
.optional()
|
|
687
|
+
.describe("compare: the cursor of the page before, from next; pass it with the same arguments."),
|
|
688
|
+
...readScope,
|
|
689
|
+
})
|
|
690
|
+
.strict(),
|
|
608
691
|
]);
|
|
609
692
|
export const configureInputSchema = z.discriminatedUnion("action", [
|
|
610
693
|
z
|
package/dist/tools.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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";
|
|
5
6
|
import { queryTiming } from "./query-observer.js";
|
|
6
7
|
import { DESTRUCTIVE, IDEMPOTENT_WRITE, MUTATING, READ_ONLY, configureInputSchema, configureWireSchema, detailSchema, graphScope, inspectInputSchema, inspectWireSchema, jsonObjectSchema, queryInputSchema, queryWireSchema, searchFeedbackSchema, yearMonthSchema, } from "./tool-contracts.js";
|
|
@@ -63,7 +64,7 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
63
64
|
});
|
|
64
65
|
});
|
|
65
66
|
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
|
|
67
|
+
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
68
|
inputSchema: queryWireSchema,
|
|
68
69
|
annotations: READ_ONLY,
|
|
69
70
|
}, (rawArgs, extra) => {
|
|
@@ -300,6 +301,29 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
300
301
|
}
|
|
301
302
|
})();
|
|
302
303
|
}
|
|
304
|
+
if (args.mode === "names" ||
|
|
305
|
+
args.mode === "describe" ||
|
|
306
|
+
args.mode === "commit_at" ||
|
|
307
|
+
args.mode === "compare") {
|
|
308
|
+
const toolArgs = args;
|
|
309
|
+
return (async () => {
|
|
310
|
+
try {
|
|
311
|
+
switch (args.mode) {
|
|
312
|
+
case "names":
|
|
313
|
+
return await findNames(client, toolArgs);
|
|
314
|
+
case "describe":
|
|
315
|
+
return await describeGraph(client, toolArgs);
|
|
316
|
+
case "commit_at":
|
|
317
|
+
return await commitAt(client, toolArgs);
|
|
318
|
+
default:
|
|
319
|
+
return await compareQuery(client, toolArgs);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
return errorResult(await enrichError(client, error));
|
|
324
|
+
}
|
|
325
|
+
})();
|
|
326
|
+
}
|
|
303
327
|
if (args.mode === "search") {
|
|
304
328
|
const searchArgs = args;
|
|
305
329
|
// The caller's detail shapes the hits like every other mode: compact
|
|
@@ -476,8 +500,242 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
476
500
|
}
|
|
477
501
|
return scoped(client, graph).embeddings.delete(name);
|
|
478
502
|
}));
|
|
503
|
+
// Fit from text: fit sources declared on classes, like embeddings.
|
|
504
|
+
const fitSetup = {
|
|
505
|
+
class: z
|
|
506
|
+
.string()
|
|
507
|
+
.optional()
|
|
508
|
+
.describe("preview/declare/dry_run: the class IRI whose instances hold the text."),
|
|
509
|
+
from: z
|
|
510
|
+
.array(z.string())
|
|
511
|
+
.optional()
|
|
512
|
+
.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."),
|
|
513
|
+
exclude: z
|
|
514
|
+
.array(z.string())
|
|
515
|
+
.optional()
|
|
516
|
+
.describe("preview/declare/dry_run: fields to drop from the automatic choice."),
|
|
517
|
+
context: z
|
|
518
|
+
.string()
|
|
519
|
+
.optional()
|
|
520
|
+
.describe('preview/declare/dry_run: one sentence about the text, e.g. "interviews with employees about their work processes" (at most 500 characters).'),
|
|
521
|
+
};
|
|
522
|
+
const fitDeclaration = (args) => {
|
|
523
|
+
if (!args.class)
|
|
524
|
+
throw new Error("this action requires class");
|
|
525
|
+
return {
|
|
526
|
+
class: args.class,
|
|
527
|
+
...(args.name ? { name: args.name } : {}),
|
|
528
|
+
...(args.from ? { from: args.from } : {}),
|
|
529
|
+
...(args.exclude ? { exclude: args.exclude } : {}),
|
|
530
|
+
...(args.context ? { context: args.context } : {}),
|
|
531
|
+
};
|
|
532
|
+
};
|
|
533
|
+
server.registerTool("lbb_fit_sources", {
|
|
534
|
+
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.",
|
|
535
|
+
inputSchema: {
|
|
536
|
+
action: z.enum(["list", "get", "preview"]),
|
|
537
|
+
name: z.string().optional().describe("get: the fit source name."),
|
|
538
|
+
...fitSetup,
|
|
539
|
+
sample: z.number().int().positive().max(20).optional(),
|
|
540
|
+
iris: z
|
|
541
|
+
.array(z.string())
|
|
542
|
+
.optional()
|
|
543
|
+
.describe("preview: the instances to show instead of a sample."),
|
|
544
|
+
detail: detailSchema,
|
|
545
|
+
...graphScope,
|
|
546
|
+
},
|
|
547
|
+
annotations: READ_ONLY,
|
|
548
|
+
}, (args) => run(client, `lbb_fit_sources.${args.action}`, args.detail, () => {
|
|
549
|
+
const fit = scoped(client, args.graph).ontology.fitSources;
|
|
550
|
+
switch (args.action) {
|
|
551
|
+
case "list":
|
|
552
|
+
return fit.list();
|
|
553
|
+
case "get":
|
|
554
|
+
if (!args.name)
|
|
555
|
+
throw new Error("get requires name");
|
|
556
|
+
return fit.get(args.name);
|
|
557
|
+
case "preview":
|
|
558
|
+
return fit.preview({
|
|
559
|
+
...fitDeclaration(args),
|
|
560
|
+
...(args.sample ? { sample: args.sample } : {}),
|
|
561
|
+
...(args.iris ? { iris: args.iris } : {}),
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}));
|
|
565
|
+
server.registerTool("lbb_fit_sources_manage", {
|
|
566
|
+
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.",
|
|
567
|
+
inputSchema: {
|
|
568
|
+
action: z.enum(["declare", "dry_run", "refresh"]),
|
|
569
|
+
name: z
|
|
570
|
+
.string()
|
|
571
|
+
.optional()
|
|
572
|
+
.describe("The fit source name (refresh: required)."),
|
|
573
|
+
...fitSetup,
|
|
574
|
+
detail: detailSchema,
|
|
575
|
+
...graphScope,
|
|
576
|
+
},
|
|
577
|
+
annotations: MUTATING,
|
|
578
|
+
}, (args) => run(client, `lbb_fit_sources_manage.${args.action}`, args.detail, () => {
|
|
579
|
+
const fit = scoped(client, args.graph).ontology.fitSources;
|
|
580
|
+
switch (args.action) {
|
|
581
|
+
case "declare":
|
|
582
|
+
return fit.declare(fitDeclaration(args));
|
|
583
|
+
case "dry_run":
|
|
584
|
+
return fit.preview({ ...fitDeclaration(args), propose: true });
|
|
585
|
+
case "refresh":
|
|
586
|
+
if (!args.name)
|
|
587
|
+
throw new Error("refresh requires name");
|
|
588
|
+
return fit.refresh(args.name);
|
|
589
|
+
}
|
|
590
|
+
}));
|
|
591
|
+
server.registerTool("lbb_fit_sources_delete", {
|
|
592
|
+
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.",
|
|
593
|
+
inputSchema: {
|
|
594
|
+
name: z.string().describe("The fit source name."),
|
|
595
|
+
confirm: z.string().describe("The same name again, to confirm."),
|
|
596
|
+
detail: detailSchema,
|
|
597
|
+
...graphScope,
|
|
598
|
+
},
|
|
599
|
+
annotations: DESTRUCTIVE,
|
|
600
|
+
}, ({ name, confirm, detail, graph }) => run(client, "lbb_fit_sources_delete", detail, () => {
|
|
601
|
+
if (confirm !== name) {
|
|
602
|
+
throw new Error("confirm must repeat the fit source name");
|
|
603
|
+
}
|
|
604
|
+
return scoped(client, graph).ontology.fitSources.delete(name);
|
|
605
|
+
}));
|
|
606
|
+
const modelUse = z
|
|
607
|
+
.enum(["ask", "route", "rerank", "fit", "label"])
|
|
608
|
+
.describe("A use of a model: ask = question answers, route = query routing, rerank = search rerank, fit = ontology fit, label = eval labels.");
|
|
609
|
+
server.registerTool("lbb_model_choice", {
|
|
610
|
+
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.",
|
|
611
|
+
inputSchema: {
|
|
612
|
+
action: z.enum([
|
|
613
|
+
"options",
|
|
614
|
+
"trials",
|
|
615
|
+
"trial",
|
|
616
|
+
"trial_call",
|
|
617
|
+
"switches",
|
|
618
|
+
]),
|
|
619
|
+
trial_id: z
|
|
620
|
+
.string()
|
|
621
|
+
.optional()
|
|
622
|
+
.describe("trial / trial_call: the trial id."),
|
|
623
|
+
call_id: z
|
|
624
|
+
.string()
|
|
625
|
+
.optional()
|
|
626
|
+
.describe("trial_call: a call id from the trial's calls."),
|
|
627
|
+
job: modelUse
|
|
628
|
+
.optional()
|
|
629
|
+
.describe("trials: only the trials of this use."),
|
|
630
|
+
limit: z
|
|
631
|
+
.number()
|
|
632
|
+
.int()
|
|
633
|
+
.positive()
|
|
634
|
+
.max(50)
|
|
635
|
+
.optional()
|
|
636
|
+
.describe("trials: at most this many, newest first (default 20)."),
|
|
637
|
+
detail: detailSchema,
|
|
638
|
+
...graphScope,
|
|
639
|
+
},
|
|
640
|
+
annotations: READ_ONLY,
|
|
641
|
+
}, (args) => run(client, `lbb_model_choice.${args.action}`, args.detail, () => {
|
|
642
|
+
const models = scoped(client, args.graph).models;
|
|
643
|
+
switch (args.action) {
|
|
644
|
+
case "options":
|
|
645
|
+
return models.trials.options();
|
|
646
|
+
case "trials":
|
|
647
|
+
return models.trials.list({ job: args.job, limit: args.limit });
|
|
648
|
+
case "trial":
|
|
649
|
+
if (!args.trial_id)
|
|
650
|
+
throw new Error("trial requires trial_id");
|
|
651
|
+
return models.trials.get(args.trial_id);
|
|
652
|
+
case "trial_call":
|
|
653
|
+
if (!args.trial_id || !args.call_id) {
|
|
654
|
+
throw new Error("trial_call requires trial_id and call_id");
|
|
655
|
+
}
|
|
656
|
+
return models.trials.call(args.trial_id, args.call_id);
|
|
657
|
+
case "switches":
|
|
658
|
+
return models.switches.list();
|
|
659
|
+
}
|
|
660
|
+
}));
|
|
661
|
+
server.registerTool("lbb_model_choice_manage", {
|
|
662
|
+
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.",
|
|
663
|
+
inputSchema: {
|
|
664
|
+
action: z.enum(["start", "stop", "switch", "revert"]),
|
|
665
|
+
provider: z
|
|
666
|
+
.enum(["anthropic", "typesafe"])
|
|
667
|
+
.optional()
|
|
668
|
+
.describe("start: the candidate's provider."),
|
|
669
|
+
model: z
|
|
670
|
+
.string()
|
|
671
|
+
.optional()
|
|
672
|
+
.describe("start: the candidate model id, e.g. claude-haiku-5-5 or jev-latest."),
|
|
673
|
+
effort: z
|
|
674
|
+
.enum(["low", "medium", "high", "xhigh", "max"])
|
|
675
|
+
.optional()
|
|
676
|
+
.describe("start: the effort of a Claude candidate."),
|
|
677
|
+
jobs: z
|
|
678
|
+
.array(modelUse)
|
|
679
|
+
.optional()
|
|
680
|
+
.describe("start: the uses to test it on."),
|
|
681
|
+
target: z
|
|
682
|
+
.number()
|
|
683
|
+
.int()
|
|
684
|
+
.positive()
|
|
685
|
+
.max(100)
|
|
686
|
+
.optional()
|
|
687
|
+
.describe("start: the calls to compare (default 40)."),
|
|
688
|
+
days: z
|
|
689
|
+
.number()
|
|
690
|
+
.int()
|
|
691
|
+
.positive()
|
|
692
|
+
.max(30)
|
|
693
|
+
.optional()
|
|
694
|
+
.describe("start: the days the trial takes new checks (default 14)."),
|
|
695
|
+
trial_id: z
|
|
696
|
+
.string()
|
|
697
|
+
.optional()
|
|
698
|
+
.describe("stop / switch: the trial id."),
|
|
699
|
+
job: modelUse
|
|
700
|
+
.optional()
|
|
701
|
+
.describe("revert: the use to put back on LBB's model."),
|
|
702
|
+
detail: detailSchema,
|
|
703
|
+
...graphScope,
|
|
704
|
+
},
|
|
705
|
+
annotations: MUTATING,
|
|
706
|
+
}, (args) => run(client, `lbb_model_choice_manage.${args.action}`, args.detail, () => {
|
|
707
|
+
const models = scoped(client, args.graph).models;
|
|
708
|
+
switch (args.action) {
|
|
709
|
+
case "start":
|
|
710
|
+
if (!args.provider || !args.model) {
|
|
711
|
+
throw new Error("start requires provider and model");
|
|
712
|
+
}
|
|
713
|
+
return models.trials.create({
|
|
714
|
+
candidate: {
|
|
715
|
+
provider: args.provider,
|
|
716
|
+
model: args.model,
|
|
717
|
+
...(args.effort ? { effort: args.effort } : {}),
|
|
718
|
+
},
|
|
719
|
+
...(args.jobs ? { jobs: args.jobs } : {}),
|
|
720
|
+
...(args.target ? { target: args.target } : {}),
|
|
721
|
+
...(args.days ? { days: args.days } : {}),
|
|
722
|
+
});
|
|
723
|
+
case "stop":
|
|
724
|
+
if (!args.trial_id)
|
|
725
|
+
throw new Error("stop requires trial_id");
|
|
726
|
+
return models.trials.stop(args.trial_id);
|
|
727
|
+
case "switch":
|
|
728
|
+
if (!args.trial_id)
|
|
729
|
+
throw new Error("switch requires trial_id");
|
|
730
|
+
return models.switches.create({ trial: args.trial_id });
|
|
731
|
+
case "revert":
|
|
732
|
+
if (!args.job)
|
|
733
|
+
throw new Error("revert requires job");
|
|
734
|
+
return models.switches.revert(args.job);
|
|
735
|
+
}
|
|
736
|
+
}));
|
|
479
737
|
server.registerTool("lbb_evals", {
|
|
480
|
-
description: "Managed evals: the
|
|
738
|
+
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
739
|
inputSchema: {
|
|
482
740
|
action: z.enum([
|
|
483
741
|
"summary",
|
|
@@ -507,7 +765,7 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
507
765
|
valid: z
|
|
508
766
|
.boolean()
|
|
509
767
|
.optional()
|
|
510
|
-
.describe("label: true = the result answers the request (thumbs up), false = it does not."),
|
|
768
|
+
.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
769
|
items: z
|
|
512
770
|
.array(z
|
|
513
771
|
.object({
|
|
@@ -529,7 +787,7 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
529
787
|
sparql: z
|
|
530
788
|
.string()
|
|
531
789
|
.optional()
|
|
532
|
-
.describe("golden: the query to freeze (the search text for surface=search)."),
|
|
790
|
+
.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
791
|
surface: z
|
|
534
792
|
.enum(["sparql", "search"])
|
|
535
793
|
.optional()
|
|
@@ -566,7 +824,15 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
566
824
|
.optional()
|
|
567
825
|
.describe("checks_summary / checks: the month, yyyy-mm (UTC). Defaults to the current month."),
|
|
568
826
|
job: z
|
|
569
|
-
.enum([
|
|
827
|
+
.enum([
|
|
828
|
+
"rerank",
|
|
829
|
+
"route",
|
|
830
|
+
"ask",
|
|
831
|
+
"rewrite",
|
|
832
|
+
"fit",
|
|
833
|
+
"propose",
|
|
834
|
+
"label",
|
|
835
|
+
])
|
|
570
836
|
.optional()
|
|
571
837
|
.describe("checks: only the checks of one job."),
|
|
572
838
|
verdict: z
|
|
@@ -616,13 +882,19 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
616
882
|
case "label":
|
|
617
883
|
if (!trace_id)
|
|
618
884
|
throw new Error("label requires trace_id");
|
|
619
|
-
if (!item && !items?.length)
|
|
620
|
-
throw new Error("label requires item (with valid) or items");
|
|
621
885
|
if (item && valid === undefined)
|
|
622
886
|
throw new Error("label requires valid with item");
|
|
887
|
+
if (!item && !items?.length && valid === undefined)
|
|
888
|
+
throw new Error("label requires item (with valid), items, or valid alone for the answer of a question");
|
|
889
|
+
if (sparql !== undefined &&
|
|
890
|
+
(item || items?.length || valid !== false))
|
|
891
|
+
throw new Error("label takes sparql (the right query) only with valid=false and no item");
|
|
623
892
|
return target.evals.label(trace_id, {
|
|
624
893
|
...(item ? { item, valid } : {}),
|
|
625
894
|
...(items?.length ? { items } : {}),
|
|
895
|
+
// The answer of a question as a whole.
|
|
896
|
+
...(!item && !items?.length ? { valid } : {}),
|
|
897
|
+
...(sparql !== undefined ? { sparql } : {}),
|
|
626
898
|
by,
|
|
627
899
|
note,
|
|
628
900
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@littlebigbrain/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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.22.0",
|
|
56
56
|
"@modelcontextprotocol/sdk": "^1",
|
|
57
57
|
"zod": "^3"
|
|
58
58
|
},
|