@gmickel/gno 0.22.6 → 0.24.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 +71 -1
- package/package.json +14 -2
- package/src/cli/program.ts +30 -0
- package/src/core/structured-query.ts +198 -0
- package/src/mcp/tools/query.ts +17 -3
- package/src/pipeline/query-modes.ts +17 -12
- package/src/sdk/client.ts +584 -0
- package/src/sdk/documents.ts +348 -0
- package/src/sdk/embed.ts +287 -0
- package/src/sdk/errors.ts +42 -0
- package/src/sdk/index.ts +51 -0
- package/src/sdk/types.ts +137 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/Ask.tsx +30 -2
- package/src/serve/public/pages/Search.tsx +47 -7
- package/src/serve/routes/api.ts +67 -14
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "lucide-react";
|
|
11
11
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
12
12
|
|
|
13
|
+
import { normalizeStructuredQueryInput } from "../../../core/structured-query";
|
|
13
14
|
import { Loader } from "../components/ai-elements/loader";
|
|
14
15
|
import {
|
|
15
16
|
Source,
|
|
@@ -199,6 +200,14 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
199
200
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
200
201
|
|
|
201
202
|
const hybridAvailable = capabilities?.hybrid ?? false;
|
|
203
|
+
const structuredQueryState = useMemo(
|
|
204
|
+
() => normalizeStructuredQueryInput(query, queryModes),
|
|
205
|
+
[query, queryModes]
|
|
206
|
+
);
|
|
207
|
+
const structuredQueryError =
|
|
208
|
+
query.trim().length > 0 && !structuredQueryState.ok
|
|
209
|
+
? structuredQueryState.error.message
|
|
210
|
+
: null;
|
|
202
211
|
|
|
203
212
|
useEffect(() => {
|
|
204
213
|
async function bootstrap(): Promise<void> {
|
|
@@ -270,6 +279,11 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
270
279
|
if (!query.trim()) {
|
|
271
280
|
return;
|
|
272
281
|
}
|
|
282
|
+
if (!structuredQueryState.ok) {
|
|
283
|
+
setQueryModeError(structuredQueryState.error.message);
|
|
284
|
+
setShowAdvanced(true);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
273
287
|
|
|
274
288
|
const entryId = crypto.randomUUID();
|
|
275
289
|
const currentQuery = query.trim();
|
|
@@ -361,6 +375,7 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
361
375
|
queryModes,
|
|
362
376
|
selectedCollection,
|
|
363
377
|
since,
|
|
378
|
+
structuredQueryState,
|
|
364
379
|
tagMode,
|
|
365
380
|
tagsInput,
|
|
366
381
|
thoroughness,
|
|
@@ -906,7 +921,7 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
906
921
|
onKeyDown={handleKeyDown}
|
|
907
922
|
placeholder={
|
|
908
923
|
answerAvailable
|
|
909
|
-
? "Ask a question about your documents..."
|
|
924
|
+
? "Ask a question about your documents... Use Shift+Enter for structured query documents"
|
|
910
925
|
: "AI answers not available"
|
|
911
926
|
}
|
|
912
927
|
ref={textareaRef}
|
|
@@ -915,13 +930,26 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
915
930
|
/>
|
|
916
931
|
<Button
|
|
917
932
|
className="absolute right-2 bottom-2"
|
|
918
|
-
disabled={
|
|
933
|
+
disabled={
|
|
934
|
+
!(query.trim() && answerAvailable) ||
|
|
935
|
+
Boolean(structuredQueryError)
|
|
936
|
+
}
|
|
919
937
|
size="icon-sm"
|
|
920
938
|
type="submit"
|
|
921
939
|
>
|
|
922
940
|
<CornerDownLeft className="size-4" />
|
|
923
941
|
</Button>
|
|
924
942
|
</div>
|
|
943
|
+
{structuredQueryError && (
|
|
944
|
+
<p className="text-destructive text-xs">{structuredQueryError}</p>
|
|
945
|
+
)}
|
|
946
|
+
{!structuredQueryError && (
|
|
947
|
+
<p className="text-muted-foreground/70 text-xs">
|
|
948
|
+
Press Enter to submit. Use Shift+Enter for multi-line structured
|
|
949
|
+
query documents with <code>term:</code>, <code>intent:</code>, and{" "}
|
|
950
|
+
<code>hyde:</code>.
|
|
951
|
+
</p>
|
|
952
|
+
)}
|
|
925
953
|
</form>
|
|
926
954
|
</footer>
|
|
927
955
|
</div>
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "lucide-react";
|
|
9
9
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
10
10
|
|
|
11
|
+
import { normalizeStructuredQueryInput } from "../../../core/structured-query";
|
|
11
12
|
import { Loader } from "../components/ai-elements/loader";
|
|
12
13
|
import { TagFacets } from "../components/TagFacets";
|
|
13
14
|
import {
|
|
@@ -30,6 +31,7 @@ import {
|
|
|
30
31
|
SelectTrigger,
|
|
31
32
|
SelectValue,
|
|
32
33
|
} from "../components/ui/select";
|
|
34
|
+
import { Textarea } from "../components/ui/textarea";
|
|
33
35
|
import { apiFetch } from "../hooks/use-api";
|
|
34
36
|
import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts";
|
|
35
37
|
import {
|
|
@@ -187,9 +189,20 @@ export default function Search({ navigate }: PageProps) {
|
|
|
187
189
|
const [showMobileTags, setShowMobileTags] = useState(false);
|
|
188
190
|
|
|
189
191
|
const hybridAvailable = capabilities?.hybrid ?? false;
|
|
192
|
+
const structuredQueryState = useMemo(
|
|
193
|
+
() => normalizeStructuredQueryInput(query, queryModes),
|
|
194
|
+
[query, queryModes]
|
|
195
|
+
);
|
|
196
|
+
const structuredQueryError =
|
|
197
|
+
query.trim().length > 0 && !structuredQueryState.ok
|
|
198
|
+
? structuredQueryState.error.message
|
|
199
|
+
: null;
|
|
190
200
|
const forceHybridForModes =
|
|
191
201
|
thoroughness === "fast" &&
|
|
192
|
-
(queryModes.length > 0 ||
|
|
202
|
+
(queryModes.length > 0 ||
|
|
203
|
+
intent.trim().length > 0 ||
|
|
204
|
+
(structuredQueryState.ok &&
|
|
205
|
+
structuredQueryState.value.usedStructuredQuerySyntax));
|
|
193
206
|
|
|
194
207
|
// Sync URL as filter state changes.
|
|
195
208
|
useEffect(() => {
|
|
@@ -294,6 +307,10 @@ export default function Search({ navigate }: PageProps) {
|
|
|
294
307
|
if (!query.trim()) {
|
|
295
308
|
return;
|
|
296
309
|
}
|
|
310
|
+
if (!structuredQueryState.ok) {
|
|
311
|
+
setError(structuredQueryState.error.message);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
297
314
|
|
|
298
315
|
setLoading(true);
|
|
299
316
|
setError(null);
|
|
@@ -302,7 +319,8 @@ export default function Search({ navigate }: PageProps) {
|
|
|
302
319
|
const useBm25 =
|
|
303
320
|
thoroughness === "fast" &&
|
|
304
321
|
queryModes.length === 0 &&
|
|
305
|
-
intent.trim().length === 0
|
|
322
|
+
intent.trim().length === 0 &&
|
|
323
|
+
!structuredQueryState.value.usedStructuredQuerySyntax;
|
|
306
324
|
const endpoint = useBm25 ? "/api/search" : "/api/query";
|
|
307
325
|
const body: Record<string, unknown> = {
|
|
308
326
|
query,
|
|
@@ -386,6 +404,7 @@ export default function Search({ navigate }: PageProps) {
|
|
|
386
404
|
queryModes,
|
|
387
405
|
selectedCollection,
|
|
388
406
|
since,
|
|
407
|
+
structuredQueryState,
|
|
389
408
|
tagMode,
|
|
390
409
|
thoroughness,
|
|
391
410
|
until,
|
|
@@ -448,6 +467,13 @@ export default function Search({ navigate }: PageProps) {
|
|
|
448
467
|
setQueryModeError(null);
|
|
449
468
|
};
|
|
450
469
|
|
|
470
|
+
const handleQueryKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
471
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
472
|
+
e.preventDefault();
|
|
473
|
+
void handleSearch();
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
|
|
451
477
|
return (
|
|
452
478
|
<div className="min-h-screen">
|
|
453
479
|
<header className="glass sticky top-0 z-10 border-border/50 border-b">
|
|
@@ -482,17 +508,19 @@ export default function Search({ navigate }: PageProps) {
|
|
|
482
508
|
<div className="pointer-events-none absolute -inset-[1px] rounded-lg bg-gradient-to-r from-primary/50 via-primary to-primary/50 opacity-0 blur-sm transition-opacity duration-300 group-focus-within:opacity-100" />
|
|
483
509
|
<div className="relative">
|
|
484
510
|
<SearchIcon className="absolute top-1/2 left-4 size-5 -translate-y-1/2 text-muted-foreground transition-colors duration-200 group-focus-within:text-primary" />
|
|
485
|
-
<
|
|
511
|
+
<Textarea
|
|
486
512
|
autoFocus
|
|
487
|
-
className="border-border/50 bg-card
|
|
513
|
+
className="min-h-[72px] resize-y border-border/50 bg-card pt-5 pr-20 pb-4 pl-12 text-base transition-all duration-200 focus:border-primary focus:bg-card/80 focus:shadow-[0_0_20px_-5px_hsl(var(--primary)/0.3)]"
|
|
488
514
|
onChange={(e) => setQuery(e.target.value)}
|
|
489
|
-
|
|
490
|
-
|
|
515
|
+
onKeyDown={handleQueryKeyDown}
|
|
516
|
+
placeholder="Search your documents... Use Shift+Enter for structured query documents"
|
|
491
517
|
value={query}
|
|
492
518
|
/>
|
|
493
519
|
<Button
|
|
494
520
|
className="absolute top-1/2 right-2 -translate-y-1/2"
|
|
495
|
-
disabled={
|
|
521
|
+
disabled={
|
|
522
|
+
loading || !query.trim() || Boolean(structuredQueryError)
|
|
523
|
+
}
|
|
496
524
|
size="sm"
|
|
497
525
|
type="submit"
|
|
498
526
|
>
|
|
@@ -501,6 +529,18 @@ export default function Search({ navigate }: PageProps) {
|
|
|
501
529
|
</div>
|
|
502
530
|
</div>
|
|
503
531
|
|
|
532
|
+
{structuredQueryError && (
|
|
533
|
+
<p className="text-destructive text-xs">
|
|
534
|
+
{structuredQueryError}
|
|
535
|
+
</p>
|
|
536
|
+
)}
|
|
537
|
+
|
|
538
|
+
<p className="text-muted-foreground/70 text-xs">
|
|
539
|
+
Press Enter to search. Use Shift+Enter for multi-line structured
|
|
540
|
+
query documents with <code>term:</code>, <code>intent:</code>,
|
|
541
|
+
and <code>hyde:</code>.
|
|
542
|
+
</p>
|
|
543
|
+
|
|
504
544
|
<div className="flex flex-wrap items-center gap-4">
|
|
505
545
|
<ThoroughnessSelector
|
|
506
546
|
disabled={
|
package/src/serve/routes/api.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type { EmbedScheduler } from "../embed-scheduler";
|
|
|
18
18
|
import { modelsPull } from "../../cli/commands/models/pull";
|
|
19
19
|
import { addCollection, removeCollection } from "../../collection";
|
|
20
20
|
import { atomicWrite } from "../../core/file-ops";
|
|
21
|
+
import { normalizeStructuredQueryInput } from "../../core/structured-query";
|
|
21
22
|
import {
|
|
22
23
|
normalizeTag,
|
|
23
24
|
parseAndValidateTagFilter,
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
processAnswerResult,
|
|
33
34
|
} from "../../pipeline/answer";
|
|
34
35
|
import { searchHybrid } from "../../pipeline/hybrid";
|
|
36
|
+
import { validateQueryModes } from "../../pipeline/query-modes";
|
|
35
37
|
import { searchBm25 } from "../../pipeline/search";
|
|
36
38
|
import { applyConfigChange } from "../config-sync";
|
|
37
39
|
import {
|
|
@@ -242,7 +244,38 @@ function parseQueryModesInput(value: unknown): {
|
|
|
242
244
|
queryModes.push({ mode, text: text.trim() });
|
|
243
245
|
}
|
|
244
246
|
|
|
245
|
-
|
|
247
|
+
const validated = validateQueryModes(queryModes);
|
|
248
|
+
if (!validated.ok) {
|
|
249
|
+
return {
|
|
250
|
+
error: errorResponse("VALIDATION", validated.error.message),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return { queryModes: validated.value };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function normalizeStructuredQueryBody(
|
|
258
|
+
query: string,
|
|
259
|
+
queryModes: QueryModeInput[] | undefined
|
|
260
|
+
): {
|
|
261
|
+
query?: string;
|
|
262
|
+
queryModes?: QueryModeInput[];
|
|
263
|
+
error?: Response;
|
|
264
|
+
} {
|
|
265
|
+
const normalized = normalizeStructuredQueryInput(query, queryModes ?? []);
|
|
266
|
+
if (!normalized.ok) {
|
|
267
|
+
return {
|
|
268
|
+
error: errorResponse("VALIDATION", normalized.error.message),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
query: normalized.value.query,
|
|
274
|
+
queryModes:
|
|
275
|
+
normalized.value.queryModes.length > 0
|
|
276
|
+
? normalized.value.queryModes
|
|
277
|
+
: undefined,
|
|
278
|
+
};
|
|
246
279
|
}
|
|
247
280
|
|
|
248
281
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -1137,8 +1170,8 @@ export async function handleSearch(
|
|
|
1137
1170
|
return errorResponse("VALIDATION", "Missing or invalid query");
|
|
1138
1171
|
}
|
|
1139
1172
|
|
|
1140
|
-
const
|
|
1141
|
-
if (!
|
|
1173
|
+
const rawQuery = body.query.trim();
|
|
1174
|
+
if (!rawQuery) {
|
|
1142
1175
|
return errorResponse("VALIDATION", "Query cannot be empty");
|
|
1143
1176
|
}
|
|
1144
1177
|
|
|
@@ -1237,7 +1270,7 @@ export async function handleSearch(
|
|
|
1237
1270
|
author,
|
|
1238
1271
|
};
|
|
1239
1272
|
|
|
1240
|
-
const result = await searchBm25(store,
|
|
1273
|
+
const result = await searchBm25(store, rawQuery, options);
|
|
1241
1274
|
|
|
1242
1275
|
if (!result.ok) {
|
|
1243
1276
|
return errorResponse("RUNTIME", result.error.message, 500);
|
|
@@ -1266,8 +1299,8 @@ export async function handleQuery(
|
|
|
1266
1299
|
return errorResponse("VALIDATION", "Missing or invalid query");
|
|
1267
1300
|
}
|
|
1268
1301
|
|
|
1269
|
-
const
|
|
1270
|
-
if (!
|
|
1302
|
+
const rawQuery = body.query.trim();
|
|
1303
|
+
if (!rawQuery) {
|
|
1271
1304
|
return errorResponse("VALIDATION", "Query cannot be empty");
|
|
1272
1305
|
}
|
|
1273
1306
|
|
|
@@ -1333,6 +1366,16 @@ export async function handleQuery(
|
|
|
1333
1366
|
return queryModesError;
|
|
1334
1367
|
}
|
|
1335
1368
|
|
|
1369
|
+
const {
|
|
1370
|
+
query,
|
|
1371
|
+
queryModes: normalizedQueryModes,
|
|
1372
|
+
error: structuredQueryError,
|
|
1373
|
+
} = normalizeStructuredQueryBody(rawQuery, queryModes);
|
|
1374
|
+
if (structuredQueryError) {
|
|
1375
|
+
return structuredQueryError;
|
|
1376
|
+
}
|
|
1377
|
+
const normalizedQuery = query ?? rawQuery;
|
|
1378
|
+
|
|
1336
1379
|
// Parse tag filters
|
|
1337
1380
|
let tagsAll: string[] | undefined;
|
|
1338
1381
|
let tagsAny: string[] | undefined;
|
|
@@ -1376,7 +1419,7 @@ export async function handleQuery(
|
|
|
1376
1419
|
genPort: ctx.genPort,
|
|
1377
1420
|
rerankPort: ctx.rerankPort,
|
|
1378
1421
|
},
|
|
1379
|
-
|
|
1422
|
+
normalizedQuery,
|
|
1380
1423
|
{
|
|
1381
1424
|
limit: Math.min(body.limit ?? 20, 50),
|
|
1382
1425
|
minScore: body.minScore,
|
|
@@ -1388,7 +1431,7 @@ export async function handleQuery(
|
|
|
1388
1431
|
? Math.min(body.candidateLimit, 100)
|
|
1389
1432
|
: undefined,
|
|
1390
1433
|
exclude,
|
|
1391
|
-
queryModes,
|
|
1434
|
+
queryModes: normalizedQueryModes,
|
|
1392
1435
|
noExpand: body.noExpand,
|
|
1393
1436
|
noRerank: body.noRerank,
|
|
1394
1437
|
tagsAll,
|
|
@@ -1427,8 +1470,8 @@ export async function handleAsk(
|
|
|
1427
1470
|
return errorResponse("VALIDATION", "Missing or invalid query");
|
|
1428
1471
|
}
|
|
1429
1472
|
|
|
1430
|
-
const
|
|
1431
|
-
if (!
|
|
1473
|
+
const rawQuery = body.query.trim();
|
|
1474
|
+
if (!rawQuery) {
|
|
1432
1475
|
return errorResponse("VALIDATION", "Query cannot be empty");
|
|
1433
1476
|
}
|
|
1434
1477
|
|
|
@@ -1486,6 +1529,16 @@ export async function handleAsk(
|
|
|
1486
1529
|
return queryModesError;
|
|
1487
1530
|
}
|
|
1488
1531
|
|
|
1532
|
+
const {
|
|
1533
|
+
query,
|
|
1534
|
+
queryModes: normalizedQueryModes,
|
|
1535
|
+
error: structuredQueryError,
|
|
1536
|
+
} = normalizeStructuredQueryBody(rawQuery, queryModes);
|
|
1537
|
+
if (structuredQueryError) {
|
|
1538
|
+
return structuredQueryError;
|
|
1539
|
+
}
|
|
1540
|
+
const normalizedQuery = query ?? rawQuery;
|
|
1541
|
+
|
|
1489
1542
|
if (body.tagsAll) {
|
|
1490
1543
|
try {
|
|
1491
1544
|
tagsAll = parseAndValidateTagFilter(body.tagsAll);
|
|
@@ -1528,7 +1581,7 @@ export async function handleAsk(
|
|
|
1528
1581
|
genPort: ctx.genPort,
|
|
1529
1582
|
rerankPort: ctx.rerankPort,
|
|
1530
1583
|
},
|
|
1531
|
-
|
|
1584
|
+
normalizedQuery,
|
|
1532
1585
|
{
|
|
1533
1586
|
limit,
|
|
1534
1587
|
collection: body.collection,
|
|
@@ -1541,7 +1594,7 @@ export async function handleAsk(
|
|
|
1541
1594
|
? Math.min(body.candidateLimit, 100)
|
|
1542
1595
|
: undefined,
|
|
1543
1596
|
exclude,
|
|
1544
|
-
queryModes,
|
|
1597
|
+
queryModes: normalizedQueryModes,
|
|
1545
1598
|
tagsAll,
|
|
1546
1599
|
tagsAny,
|
|
1547
1600
|
since: body.since,
|
|
@@ -1567,7 +1620,7 @@ export async function handleAsk(
|
|
|
1567
1620
|
const maxTokens = body.maxAnswerTokens ?? 512;
|
|
1568
1621
|
const rawResult = await generateGroundedAnswer(
|
|
1569
1622
|
{ genPort: ctx.genPort, store: ctx.store },
|
|
1570
|
-
|
|
1623
|
+
normalizedQuery,
|
|
1571
1624
|
results,
|
|
1572
1625
|
maxTokens
|
|
1573
1626
|
);
|
|
@@ -1582,7 +1635,7 @@ export async function handleAsk(
|
|
|
1582
1635
|
}
|
|
1583
1636
|
|
|
1584
1637
|
const askResult: AskResult = {
|
|
1585
|
-
query,
|
|
1638
|
+
query: normalizedQuery,
|
|
1586
1639
|
mode: searchResult.value.meta.vectorsUsed ? "hybrid" : "bm25_only",
|
|
1587
1640
|
queryLanguage: searchResult.value.meta.queryLanguage ?? "und",
|
|
1588
1641
|
answer,
|