@pipeshub-ai/mcp 2.3.3 → 2.4.1
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 +2 -2
- package/bin/mcp-server.js +328 -210
- package/bin/mcp-server.js.map +12 -12
- package/bin/pipeshub.js +16 -8
- package/bin/pipeshub.js.map +4 -4
- package/esm/cli/commands.d.ts +1 -1
- package/esm/cli/commands.d.ts.map +1 -1
- package/esm/cli/commands.js +4 -1
- package/esm/cli/commands.js.map +1 -1
- package/esm/cli/pipeshub.js +13 -6
- package/esm/cli/pipeshub.js.map +1 -1
- package/esm/mcp-server/instructions.d.ts +1 -1
- package/esm/mcp-server/instructions.d.ts.map +1 -1
- package/esm/mcp-server/instructions.js +9 -6
- package/esm/mcp-server/instructions.js.map +1 -1
- package/esm/mcp-server/tools/_helpers.d.ts +76 -0
- package/esm/mcp-server/tools/_helpers.d.ts.map +1 -1
- package/esm/mcp-server/tools/_helpers.js +151 -0
- package/esm/mcp-server/tools/_helpers.js.map +1 -1
- package/esm/mcp-server/tools/pipeshubChat.d.ts.map +1 -1
- package/esm/mcp-server/tools/pipeshubChat.js +25 -11
- package/esm/mcp-server/tools/pipeshubChat.js.map +1 -1
- package/esm/mcp-server/tools/pipeshubDownloadRecord.d.ts.map +1 -1
- package/esm/mcp-server/tools/pipeshubDownloadRecord.js +15 -12
- package/esm/mcp-server/tools/pipeshubDownloadRecord.js.map +1 -1
- package/esm/mcp-server/tools/pipeshubSearch.d.ts +1 -0
- package/esm/mcp-server/tools/pipeshubSearch.d.ts.map +1 -1
- package/esm/mcp-server/tools/pipeshubSearch.js +22 -13
- package/esm/mcp-server/tools/pipeshubSearch.js.map +1 -1
- package/esm/mcp-server/tools/pipeshubSources.d.ts.map +1 -1
- package/esm/mcp-server/tools/pipeshubSources.js +15 -27
- package/esm/mcp-server/tools/pipeshubSources.js.map +1 -1
- package/esm/tool-names.js +3 -3
- package/esm/tool-names.js.map +1 -1
- package/package.json +1 -1
- package/qm/sandbox/Dockerfile +1 -1
- package/qm/sandbox/skills/pipeshub/SKILL.md +1 -0
- package/src/cli/commands.ts +3 -0
- package/src/cli/pipeshub.ts +12 -6
- package/src/mcp-server/instructions.ts +9 -6
- package/src/mcp-server/tools/_helpers.ts +233 -0
- package/src/mcp-server/tools/pipeshubChat.ts +31 -10
- package/src/mcp-server/tools/pipeshubDownloadRecord.ts +15 -12
- package/src/mcp-server/tools/pipeshubSearch.ts +38 -13
- package/src/mcp-server/tools/pipeshubSources.ts +18 -25
- package/src/tool-names.ts +3 -3
|
@@ -8,6 +8,7 @@ import { Security } from "../../models/security.js";
|
|
|
8
8
|
import { PipeshubCore } from "../../core.js";
|
|
9
9
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
10
10
|
import { agentsListAgents } from "../../funcs/agentsListAgents.js";
|
|
11
|
+
import { knowledgeHubGetKnowledgeHubRootNodes } from "../../funcs/knowledgeHubGetKnowledgeHubRootNodes.js";
|
|
11
12
|
import {
|
|
12
13
|
AgentListEnvelope$zodSchema,
|
|
13
14
|
AgentSummary,
|
|
@@ -536,3 +537,235 @@ export function trimSearchHit(hit: any) {
|
|
|
536
537
|
pageNum: md.pageNum,
|
|
537
538
|
};
|
|
538
539
|
}
|
|
540
|
+
|
|
541
|
+
// ─── Sources and search scoping ──────────────────────────────────────────────
|
|
542
|
+
//
|
|
543
|
+
// Search and chat each take two scoping lists, and they are not interchangeable.
|
|
544
|
+
// `apps` holds connector ids; `kb` holds collection (knowledge base) ids. When
|
|
545
|
+
// `apps` is set and `kb` is empty, the backend drops every collection id from
|
|
546
|
+
// `apps` and searches no collection at all — so a collection id in the wrong
|
|
547
|
+
// list silently returns nothing. Both kinds are UUIDs, so only the source
|
|
548
|
+
// listing can tell them apart.
|
|
549
|
+
|
|
550
|
+
export type SourceKind = "knowledgeBase" | "connector";
|
|
551
|
+
|
|
552
|
+
export interface SourceItem {
|
|
553
|
+
id: string;
|
|
554
|
+
name: string | undefined;
|
|
555
|
+
kind: SourceKind;
|
|
556
|
+
connector: string | undefined;
|
|
557
|
+
hasChildren: boolean | undefined;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** A knowledge-hub root node as a source. Collections carry `connector: "KB"`. */
|
|
561
|
+
export function toSource(n: any): SourceItem {
|
|
562
|
+
return {
|
|
563
|
+
id: n?.id,
|
|
564
|
+
name: n?.name,
|
|
565
|
+
kind: n?.connector === "KB" ? "knowledgeBase" : "connector",
|
|
566
|
+
connector: n?.connector,
|
|
567
|
+
hasChildren: n?.hasChildren,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Fetch every root source (connectors and collections), paginating
|
|
573
|
+
* `GET /knowledgeBase/knowledge-hub/nodes` the same way `listAllAgents` does.
|
|
574
|
+
* A partial listing would let a misplaced id slip past the scope router, so
|
|
575
|
+
* this does not stop at the first page.
|
|
576
|
+
*
|
|
577
|
+
* `reason` is a short cause for callers that report the failure as a note
|
|
578
|
+
* rather than an error — `result` carries the full message, including an auth
|
|
579
|
+
* hint that would mislead inside a search that otherwise succeeded.
|
|
580
|
+
*/
|
|
581
|
+
export async function listAllSources(
|
|
582
|
+
client: PipeshubCore,
|
|
583
|
+
opts: {
|
|
584
|
+
signal?: AbortSignal | undefined;
|
|
585
|
+
maxPages?: number | undefined;
|
|
586
|
+
} = {},
|
|
587
|
+
): Promise<
|
|
588
|
+
| { ok: true; sources: SourceItem[]; truncated: boolean }
|
|
589
|
+
| { ok: false; result: CallToolResult; reason: string }
|
|
590
|
+
> {
|
|
591
|
+
const PAGE_SIZE = 200;
|
|
592
|
+
const maxPages = opts.maxPages ?? 5;
|
|
593
|
+
// Only set `signal` when present (exactOptionalPropertyTypes).
|
|
594
|
+
const reqOptions = opts.signal
|
|
595
|
+
? { fetchOptions: { signal: opts.signal } }
|
|
596
|
+
: {};
|
|
597
|
+
|
|
598
|
+
const all: SourceItem[] = [];
|
|
599
|
+
|
|
600
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
601
|
+
const [r] = await knowledgeHubGetKnowledgeHubRootNodes(client, {
|
|
602
|
+
page,
|
|
603
|
+
limit: PAGE_SIZE,
|
|
604
|
+
}, reqOptions).$inspect();
|
|
605
|
+
if (!r.ok) {
|
|
606
|
+
return {
|
|
607
|
+
ok: false,
|
|
608
|
+
result: errorResult(`sources: ${r.error.message}`),
|
|
609
|
+
reason: r.error.message,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const status = r.value.status;
|
|
614
|
+
const httpOk = r.value.ok;
|
|
615
|
+
const parsed = await readJson<{
|
|
616
|
+
items?: any[];
|
|
617
|
+
pagination?: { hasNext?: boolean | null } | null;
|
|
618
|
+
}>(r.value, "Knowledge base listing");
|
|
619
|
+
if (!parsed.ok) {
|
|
620
|
+
return {
|
|
621
|
+
ok: false,
|
|
622
|
+
result: parsed.result,
|
|
623
|
+
reason: httpOk ? "unreadable response" : `HTTP ${status}`,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const items = parsed.value.items ?? [];
|
|
628
|
+
all.push(...items.map(toSource));
|
|
629
|
+
|
|
630
|
+
const hasNext = parsed.value.pagination?.hasNext ?? (items.length >= PAGE_SIZE);
|
|
631
|
+
if (!hasNext || items.length === 0) {
|
|
632
|
+
return { ok: true, sources: all, truncated: false };
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Hit the page cap with more pages still available.
|
|
637
|
+
return { ok: true, sources: all, truncated: true };
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const dedupe = (ids: readonly string[]): string[] => [...new Set(ids)];
|
|
641
|
+
|
|
642
|
+
export interface SourceScope {
|
|
643
|
+
apps: string[];
|
|
644
|
+
kb: string[];
|
|
645
|
+
/** Collection ids that arrived in `apps` and were moved to `kb`. */
|
|
646
|
+
movedToKb: string[];
|
|
647
|
+
/** Connector ids that arrived in `kb` and were moved to `apps`. */
|
|
648
|
+
movedToApps: string[];
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Put each id in the list the backend reads it from, whichever list the
|
|
653
|
+
* caller used. `kinds` maps a source id to what the listing says it is. An id
|
|
654
|
+
* the listing does not know stays where the caller put it. Order is kept:
|
|
655
|
+
* ids from `apps` first, then ids from `kb`. Duplicates collapse.
|
|
656
|
+
*/
|
|
657
|
+
export function routeSourceScope(
|
|
658
|
+
apps: readonly string[] | undefined,
|
|
659
|
+
kb: readonly string[] | undefined,
|
|
660
|
+
kinds: ReadonlyMap<string, SourceKind>,
|
|
661
|
+
): SourceScope {
|
|
662
|
+
const appIds = dedupe(apps ?? []);
|
|
663
|
+
const kbIds = dedupe(kb ?? []);
|
|
664
|
+
const movedToKb = appIds.filter((id) => kinds.get(id) === "knowledgeBase");
|
|
665
|
+
const movedToApps = kbIds.filter((id) => kinds.get(id) === "connector");
|
|
666
|
+
return {
|
|
667
|
+
apps: dedupe([
|
|
668
|
+
...appIds.filter((id) => kinds.get(id) !== "knowledgeBase"),
|
|
669
|
+
...movedToApps,
|
|
670
|
+
]),
|
|
671
|
+
kb: dedupe([
|
|
672
|
+
...movedToKb,
|
|
673
|
+
...kbIds.filter((id) => kinds.get(id) !== "connector"),
|
|
674
|
+
]),
|
|
675
|
+
movedToKb,
|
|
676
|
+
movedToApps,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/** The `filters` body for `POST /search`; omitted when nothing is scoped. */
|
|
681
|
+
export function searchFilters(
|
|
682
|
+
scope: { apps: string[]; kb: string[] },
|
|
683
|
+
): { apps: string[]; kb: string[] } | undefined {
|
|
684
|
+
if (scope.apps.length === 0 && scope.kb.length === 0) return undefined;
|
|
685
|
+
return { apps: scope.apps, kb: scope.kb };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/** Notes telling the model what the router did, or could not do. */
|
|
689
|
+
export function sourceScopeNotes(input: {
|
|
690
|
+
movedToKb: string[];
|
|
691
|
+
movedToApps: string[];
|
|
692
|
+
lookupError?: string | undefined;
|
|
693
|
+
unlisted?: string[] | undefined;
|
|
694
|
+
}): string[] {
|
|
695
|
+
const notes: string[] = [];
|
|
696
|
+
if (input.movedToKb.length > 0) {
|
|
697
|
+
notes.push(
|
|
698
|
+
`Moved ${input.movedToKb.length} collection id(s) from apps to kb: `
|
|
699
|
+
+ `${input.movedToKb.join(", ")}. Collection ids belong in kb.`,
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
if (input.movedToApps.length > 0) {
|
|
703
|
+
notes.push(
|
|
704
|
+
`Moved ${input.movedToApps.length} connector id(s) from kb to apps: `
|
|
705
|
+
+ `${input.movedToApps.join(", ")}. Connector ids belong in apps.`,
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
if (input.lookupError) {
|
|
709
|
+
notes.push(
|
|
710
|
+
"Could not check the ids against the source list "
|
|
711
|
+
+ `(${input.lookupError}); sent them unchanged. If this finds nothing, `
|
|
712
|
+
+ "check that collection ids are in kb and connector ids in apps.",
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
if (input.unlisted && input.unlisted.length > 0) {
|
|
716
|
+
notes.push(
|
|
717
|
+
`Source list was incomplete; ${input.unlisted.join(", ")} were not `
|
|
718
|
+
+ "found and were sent unchanged.",
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
return notes;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Route `apps` / `kb` for one request. Every id in either list is checked
|
|
726
|
+
* against the source listing and sent in the list the backend reads it from:
|
|
727
|
+
* collection ids in `kb`, connector ids in `apps`. The lookup costs a request,
|
|
728
|
+
* so it runs only when something is scoped. A failed lookup sends the ids
|
|
729
|
+
* unchanged and says so in `notes`.
|
|
730
|
+
*/
|
|
731
|
+
export async function resolveSourceScope(
|
|
732
|
+
client: PipeshubCore,
|
|
733
|
+
apps: readonly string[] | undefined,
|
|
734
|
+
kb: readonly string[] | undefined,
|
|
735
|
+
opts: {
|
|
736
|
+
signal?: AbortSignal | undefined;
|
|
737
|
+
maxPages?: number | undefined;
|
|
738
|
+
} = {},
|
|
739
|
+
): Promise<{ scope: SourceScope; notes: string[] }> {
|
|
740
|
+
const unchecked = routeSourceScope(apps, kb, new Map());
|
|
741
|
+
if (unchecked.apps.length === 0 && unchecked.kb.length === 0) {
|
|
742
|
+
return { scope: unchecked, notes: [] };
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const listed = await listAllSources(client, opts);
|
|
746
|
+
if (!listed.ok) {
|
|
747
|
+
return {
|
|
748
|
+
scope: unchecked,
|
|
749
|
+
notes: sourceScopeNotes({
|
|
750
|
+
movedToKb: [],
|
|
751
|
+
movedToApps: [],
|
|
752
|
+
lookupError: listed.reason,
|
|
753
|
+
}),
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const kinds = new Map(listed.sources.map((s) => [s.id, s.kind] as const));
|
|
758
|
+
const scope = routeSourceScope(apps, kb, kinds);
|
|
759
|
+
const unlisted = listed.truncated
|
|
760
|
+
? [...scope.apps, ...scope.kb].filter((id) => !kinds.has(id))
|
|
761
|
+
: [];
|
|
762
|
+
return {
|
|
763
|
+
scope,
|
|
764
|
+
notes: sourceScopeNotes({
|
|
765
|
+
movedToKb: scope.movedToKb,
|
|
766
|
+
movedToApps: scope.movedToApps,
|
|
767
|
+
unlisted,
|
|
768
|
+
}),
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
|
|
@@ -25,19 +25,18 @@ import {
|
|
|
25
25
|
httpErrorResult,
|
|
26
26
|
iterateSSE,
|
|
27
27
|
jsonResult,
|
|
28
|
+
resolveSourceScope,
|
|
28
29
|
trimConversation,
|
|
29
30
|
} from "./_helpers.js";
|
|
30
31
|
|
|
31
32
|
const FiltersShape = z.object({
|
|
32
33
|
apps: z.array(z.string()).optional().describe(
|
|
33
|
-
"
|
|
34
|
-
+ "
|
|
35
|
-
+ "`knowledgeBase_<orgId>` id is still accepted on deployments that "
|
|
36
|
-
+ "predate per-KB sources. Empty / omitted means no app-side "
|
|
37
|
-
+ "restriction.",
|
|
34
|
+
"Connector ids to use. Get them from `pipeshub_sources`, where `kind` is "
|
|
35
|
+
+ "\"connector\". Collection ids go in `kb`, not here.",
|
|
38
36
|
),
|
|
39
37
|
kb: z.array(z.string()).optional().describe(
|
|
40
|
-
"
|
|
38
|
+
"Collection (knowledge base) ids to use. Get them from "
|
|
39
|
+
+ "`pipeshub_sources`, where `kind` is \"knowledgeBase\".",
|
|
41
40
|
),
|
|
42
41
|
}).optional();
|
|
43
42
|
|
|
@@ -52,8 +51,8 @@ const args = {
|
|
|
52
51
|
+ "prior messages.",
|
|
53
52
|
),
|
|
54
53
|
filters: FiltersShape.describe(
|
|
55
|
-
"
|
|
56
|
-
+ "
|
|
54
|
+
"Which sources the answer may use. Leave out to use all sources. Only "
|
|
55
|
+
+ "works on the FIRST turn; later turns keep the first turn's sources.",
|
|
57
56
|
),
|
|
58
57
|
modelKey: z.string().optional().describe(
|
|
59
58
|
"Model id to use (from `pipeshub_sources` `llmModels[*].modelKey`). "
|
|
@@ -156,6 +155,26 @@ cited document, take \`citations[*].recordId\` and call
|
|
|
156
155
|
: "internal_search";
|
|
157
156
|
let response: Response;
|
|
158
157
|
|
|
158
|
+
// Filters apply only when a conversation starts. Each id is sent in the
|
|
159
|
+
// list the backend reads it from (collection ids in `kb`, connector ids in
|
|
160
|
+
// `apps`). Both keys go out: on agent chat a missing key falls back to the
|
|
161
|
+
// agent's own sources. An explicit empty `{ apps: [], kb: [] }` passes
|
|
162
|
+
// through unchanged.
|
|
163
|
+
let filters = args.filters;
|
|
164
|
+
let notes: string[] = [];
|
|
165
|
+
if (!args.conversationId && filters) {
|
|
166
|
+
const resolved = await resolveSourceScope(
|
|
167
|
+
client,
|
|
168
|
+
filters.apps,
|
|
169
|
+
filters.kb,
|
|
170
|
+
{ signal: ctx.signal },
|
|
171
|
+
);
|
|
172
|
+
notes = resolved.notes;
|
|
173
|
+
if (resolved.scope.apps.length > 0 || resolved.scope.kb.length > 0) {
|
|
174
|
+
filters = { apps: resolved.scope.apps, kb: resolved.scope.kb };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
159
178
|
if (args.agentId) {
|
|
160
179
|
// `quick` is the only value the agent stream schemas accept, and it is
|
|
161
180
|
// required — so ignore whatever the caller passed rather than forwarding
|
|
@@ -182,7 +201,7 @@ cited document, take \`citations[*].recordId\` and call
|
|
|
182
201
|
agentKey: args.agentId,
|
|
183
202
|
body: {
|
|
184
203
|
query: args.query,
|
|
185
|
-
filters
|
|
204
|
+
filters,
|
|
186
205
|
modelKey: args.modelKey,
|
|
187
206
|
modelName: args.modelName,
|
|
188
207
|
modelFriendlyName: args.modelFriendlyName,
|
|
@@ -210,7 +229,7 @@ cited document, take \`citations[*].recordId\` and call
|
|
|
210
229
|
// Start a new (non-agent) conversation.
|
|
211
230
|
const [result] = await conversationsStreamConversation(client, {
|
|
212
231
|
query: args.query,
|
|
213
|
-
filters
|
|
232
|
+
filters,
|
|
214
233
|
modelKey: args.modelKey,
|
|
215
234
|
modelName: args.modelName,
|
|
216
235
|
modelFriendlyName: args.modelFriendlyName,
|
|
@@ -244,6 +263,7 @@ cited document, take \`citations[*].recordId\` and call
|
|
|
244
263
|
return jsonResult({
|
|
245
264
|
...trimConversation(state.conversation),
|
|
246
265
|
recordsUsed: state.recordsUsed,
|
|
266
|
+
...(notes.length > 0 ? { notes } : {}),
|
|
247
267
|
});
|
|
248
268
|
}
|
|
249
269
|
|
|
@@ -264,6 +284,7 @@ cited document, take \`citations[*].recordId\` and call
|
|
|
264
284
|
recordsUsed: state.recordsUsed,
|
|
265
285
|
warning: "Stream ended without a terminal RUN_FINISHED; answer is the "
|
|
266
286
|
+ "accumulated TEXT_MESSAGE_CONTENT and citations are unavailable.",
|
|
287
|
+
...(notes.length > 0 ? { notes } : {}),
|
|
267
288
|
});
|
|
268
289
|
}
|
|
269
290
|
|
|
@@ -10,26 +10,29 @@ const args = {
|
|
|
10
10
|
+ "citation (`citations[*].recordId`) or from a `pipeshub_search` hit.",
|
|
11
11
|
),
|
|
12
12
|
convertTo: z.string().optional().describe(
|
|
13
|
-
"
|
|
14
|
-
+ "
|
|
13
|
+
"The only conversion target connectors honour is `application/pdf` "
|
|
14
|
+
+ "(the MIME type, not `pdf`). A bare `pdf` is ignored and the "
|
|
15
|
+
+ "original file is returned with no error. Omit for the file as "
|
|
16
|
+
+ "stored. Does not parse the document — use "
|
|
17
|
+
+ "`pipeshub_get_record_content` `mode:\"content\"` for that.",
|
|
15
18
|
),
|
|
16
19
|
};
|
|
17
20
|
|
|
18
21
|
export const tool$pipeshubDownloadRecord: ToolDefinition<typeof args> = {
|
|
19
22
|
name: "pipeshub_download_record",
|
|
20
23
|
description:
|
|
21
|
-
`
|
|
24
|
+
`Download the file as stored for one record — not PipesHub's parsed
|
|
25
|
+
content, metadata header, or summary.
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
\`pipeshub_chat\` response → \`citations[*].recordId\`.
|
|
26
|
-
- A search result:
|
|
27
|
-
\`pipeshub_search\` response → \`hits[*].recordId\` /
|
|
28
|
-
\`uniqueRecords[*].recordId\`.
|
|
27
|
+
Use this when the user wants the file itself (download, attach, open).
|
|
28
|
+
Get \`recordId\` from a chat citation or a \`pipeshub_search\` hit.
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
Do not use this to read, summarize, or answer "what does this doc
|
|
31
|
+
say?" regardless of format. That is \`pipeshub_get_record_content\`
|
|
32
|
+
\`mode:"content"\`. Text formats come back inline; images, audio, and
|
|
33
|
+
binary as base64.
|
|
34
|
+
|
|
35
|
+
\`convertTo\` accepts only \`application/pdf\`; anything else is ignored.`,
|
|
33
36
|
scopes: ["read"],
|
|
34
37
|
annotations: {
|
|
35
38
|
title: "Download a document by record id",
|
|
@@ -2,19 +2,34 @@
|
|
|
2
2
|
import * as z from "zod";
|
|
3
3
|
import { semanticSearchSearch } from "../../funcs/semanticSearchSearch.js";
|
|
4
4
|
import { ToolDefinition } from "../tools.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
errorResult,
|
|
7
|
+
jsonResult,
|
|
8
|
+
readJson,
|
|
9
|
+
resolveSourceScope,
|
|
10
|
+
searchFilters,
|
|
11
|
+
trimSearchHit,
|
|
12
|
+
} from "./_helpers.js";
|
|
13
|
+
|
|
14
|
+
/** Matches the SDK request default (`src/models/semanticsearchrequest.ts`). */
|
|
15
|
+
const DEFAULT_LIMIT = 10;
|
|
6
16
|
|
|
7
17
|
const args = {
|
|
8
18
|
query: z.string().min(1).describe(
|
|
9
19
|
"Natural language query. Vector search across the org's indexed records.",
|
|
10
20
|
),
|
|
11
21
|
limit: z.number().int().min(1).max(100).optional().describe(
|
|
12
|
-
"
|
|
13
|
-
+ "
|
|
22
|
+
"Number of results. Default 10. Use 5–10 when you only need a "
|
|
23
|
+
+ "`recordId`.",
|
|
14
24
|
),
|
|
15
25
|
apps: z.array(z.string()).optional().describe(
|
|
16
|
-
"
|
|
17
|
-
+ "
|
|
26
|
+
"Connector ids to search (for example a Jira or Google Drive connection). "
|
|
27
|
+
+ "Get them from `pipeshub_sources`, where `kind` is \"connector\". "
|
|
28
|
+
+ "Collection ids go in `kb`, not here.",
|
|
29
|
+
),
|
|
30
|
+
kb: z.array(z.string()).optional().describe(
|
|
31
|
+
"Collection (knowledge base) ids to search. Get them from "
|
|
32
|
+
+ "`pipeshub_sources`, where `kind` is \"knowledgeBase\".",
|
|
18
33
|
),
|
|
19
34
|
};
|
|
20
35
|
|
|
@@ -48,13 +63,12 @@ not every record that matches. Never count them to answer "how many" /
|
|
|
48
63
|
"all" / "every"; navigate the record group instead, which reports its
|
|
49
64
|
real total.
|
|
50
65
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
Highest \`score\` first; multiple hits may share the same \`recordId\`
|
|
54
|
-
(different blocks of the same record).
|
|
66
|
+
By default it searches everything. To search only some sources, pass
|
|
67
|
+
connector ids in \`apps\` and collection ids in \`kb\`.
|
|
55
68
|
|
|
56
|
-
|
|
57
|
-
\`webUrl
|
|
69
|
+
Each hit is one matching passage, best match first:
|
|
70
|
+
\`{ recordId, recordName, score, snippet, mimeType, webUrl, ... }\`.
|
|
71
|
+
One record can appear in several hits. Link a record by its \`webUrl\`.`,
|
|
58
72
|
scopes: ["read"],
|
|
59
73
|
annotations: {
|
|
60
74
|
title: "Semantic search",
|
|
@@ -65,10 +79,20 @@ When presenting results to the user, link each record using its
|
|
|
65
79
|
},
|
|
66
80
|
args,
|
|
67
81
|
tool: async (client, args, ctx) => {
|
|
82
|
+
const limit = args.limit ?? DEFAULT_LIMIT;
|
|
83
|
+
// Each id is sent in the list the backend reads it from: a collection id
|
|
84
|
+
// in `apps` or a connector id in `kb` is dropped by the backend.
|
|
85
|
+
const { scope, notes } = await resolveSourceScope(
|
|
86
|
+
client,
|
|
87
|
+
args.apps,
|
|
88
|
+
args.kb,
|
|
89
|
+
{ signal: ctx.signal },
|
|
90
|
+
);
|
|
91
|
+
|
|
68
92
|
const [result] = await semanticSearchSearch(client, {
|
|
69
93
|
query: args.query,
|
|
70
|
-
limit
|
|
71
|
-
filters:
|
|
94
|
+
limit,
|
|
95
|
+
filters: searchFilters(scope),
|
|
72
96
|
}, { fetchOptions: { signal: ctx.signal } }).$inspect();
|
|
73
97
|
if (!result.ok) return errorResult(result.error.message);
|
|
74
98
|
|
|
@@ -94,6 +118,7 @@ When presenting results to the user, link each record using its
|
|
|
94
118
|
mimeType: r.mimeType,
|
|
95
119
|
webUrl: r.webUrl,
|
|
96
120
|
})),
|
|
121
|
+
...(notes.length > 0 ? { notes } : {}),
|
|
97
122
|
});
|
|
98
123
|
},
|
|
99
124
|
};
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
|
-
import { knowledgeHubGetKnowledgeHubRootNodes } from "../../funcs/knowledgeHubGetKnowledgeHubRootNodes.js";
|
|
3
2
|
import { aiModelsProvidersGetAvailableModelsByType } from "../../funcs/aiModelsProvidersGetAvailableModelsByType.js";
|
|
4
3
|
import { ToolDefinition } from "../tools.js";
|
|
5
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
errorResult,
|
|
6
|
+
jsonResult,
|
|
7
|
+
listAllSources,
|
|
8
|
+
readJson,
|
|
9
|
+
} from "./_helpers.js";
|
|
6
10
|
|
|
7
11
|
const args = {
|
|
8
12
|
include: z.array(z.enum(["sources", "llmModels", "embeddingModels"]))
|
|
@@ -20,20 +24,19 @@ export const tool$pipeshubSources: ToolDefinition<typeof args> = {
|
|
|
20
24
|
|
|
21
25
|
Returns up to three sections:
|
|
22
26
|
|
|
23
|
-
- \`sources\` —
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
\`
|
|
27
|
+
- \`sources\` — connectors (\`kind: "connector"\`) and collections
|
|
28
|
+
(\`kind: "knowledgeBase"\`). For \`pipeshub_search\` and
|
|
29
|
+
\`pipeshub_chat\`, put a connector \`id\` in \`apps\` and a collection
|
|
30
|
+
\`id\` in \`kb\`. \`sourcesTruncated: true\` means the list stopped at
|
|
31
|
+
1,000 sources.
|
|
27
32
|
- \`llmModels\` — chat / generation models. Each item's \`modelKey\`
|
|
28
|
-
is the value to pass on \`pipeshub_chat\`
|
|
29
|
-
\`
|
|
30
|
-
specific model.
|
|
33
|
+
is the value to pass on \`pipeshub_chat\` as \`modelKey\`. Pick
|
|
34
|
+
\`isDefault: true\` unless the user asks for a specific model.
|
|
31
35
|
- \`embeddingModels\` — vector embedding models (only fetched when
|
|
32
36
|
explicitly requested via \`include\`).
|
|
33
37
|
|
|
34
38
|
Call this once at the start of a session and cache the result —
|
|
35
|
-
sources and models change infrequently
|
|
36
|
-
are returned by default; pass \`include\` to override.`,
|
|
39
|
+
sources and models change infrequently.`,
|
|
37
40
|
scopes: ["read"],
|
|
38
41
|
annotations: {
|
|
39
42
|
title: "List PipesHub sources and AI models",
|
|
@@ -52,20 +55,10 @@ are returned by default; pass \`include\` to override.`,
|
|
|
52
55
|
const result: Record<string, unknown> = {};
|
|
53
56
|
|
|
54
57
|
if (want("sources")) {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (!r.ok) return errorResult(`sources: ${r.error.message}`);
|
|
60
|
-
const parsed = await readJson<{ items?: any[] }>(r.value, "Knowledge base listing");
|
|
61
|
-
if (!parsed.ok) return parsed.result;
|
|
62
|
-
result["sources"] = (parsed.value.items ?? []).map((n: any) => ({
|
|
63
|
-
id: n.id,
|
|
64
|
-
name: n.name,
|
|
65
|
-
kind: n.connector === "KB" ? "knowledgeBase" : "connector",
|
|
66
|
-
connector: n.connector,
|
|
67
|
-
hasChildren: n.hasChildren,
|
|
68
|
-
}));
|
|
58
|
+
const listed = await listAllSources(client, { signal: ctx.signal });
|
|
59
|
+
if (!listed.ok) return listed.result;
|
|
60
|
+
result["sources"] = listed.sources;
|
|
61
|
+
if (listed.truncated) result["sourcesTruncated"] = true;
|
|
69
62
|
}
|
|
70
63
|
|
|
71
64
|
for (
|
package/src/tool-names.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
export const toolNames: Array<{ name: string; description: string }>= [
|
|
3
3
|
{
|
|
4
4
|
"name": "pipeshub_sources",
|
|
5
|
-
"description": "Discover available chat sources and AI models in one call.\n\nReturns up to three sections:\n\n- `sources` —
|
|
5
|
+
"description": "Discover available chat sources and AI models in one call.\n\nReturns up to three sections:\n\n- `sources` — connectors (`kind: \"connector\"`) and collections\n (`kind: \"knowledgeBase\"`). For `pipeshub_search` and\n `pipeshub_chat`, put a connector `id` in `apps` and a collection\n `id` in `kb`. `sourcesTruncated: true` means the list stopped at\n 1,000 sources.\n- `llmModels` — chat / generation models. Each item's `modelKey`\n is the value to pass on `pipeshub_chat` as `modelKey`. Pick\n `isDefault: true` unless the user asks for a specific model.\n- `embeddingModels` — vector embedding models (only fetched when\n explicitly requested via `include`).\n\nCall this once at the start of a session and cache the result —\nsources and models change infrequently."
|
|
6
6
|
},
|
|
7
7
|
{
|
|
8
8
|
"name": "pipeshub_chat",
|
|
@@ -10,11 +10,11 @@ export const toolNames: Array<{ name: string; description: string }>= [
|
|
|
10
10
|
},
|
|
11
11
|
{
|
|
12
12
|
"name": "pipeshub_search",
|
|
13
|
-
"description": "Vector / semantic search across the org's indexed documents.\n\n**Use this when the user wants to LOCATE a document** — by name, topic,\nor a phrase to grep for — and to resolve it to a `recordId`. For\nopen-ended questions across many documents, use `pipeshub_chat`\ninstead, which does the retrieval internally and grounds the answer in\ncitations.\n\nTypical uses:\n- Resolve a doc name / topic into a `recordId` for\n `pipeshub_get_record_content` — step 1 of any full-document task\n (summarize, extract, review, \"what does the doc say?\").\n- Resolve a filename / phrase into a `recordId` for\n `pipeshub_download_record`.\n- Show the user a ranked list of matching files when they ask \"find /\n search for X\".\n\nNot for structural questions — what is under this epic, which pages are\nin this space, what links to this ticket. Ranking by content cannot show\nhow records relate; use `pipeshub_get_record_content` `mode:\"navigate\"`.\n\n**A ranked sample, never a complete list.** Hits are the top-scoring\nblocks from the best-matching records — not all blocks of any record, and\nnot every record that matches. Never count them to answer \"how many\" /\n\"all\" / \"every\"; navigate the record group instead, which reports its\nreal total.\n\
|
|
13
|
+
"description": "Vector / semantic search across the org's indexed documents.\n\n**Use this when the user wants to LOCATE a document** — by name, topic,\nor a phrase to grep for — and to resolve it to a `recordId`. For\nopen-ended questions across many documents, use `pipeshub_chat`\ninstead, which does the retrieval internally and grounds the answer in\ncitations.\n\nTypical uses:\n- Resolve a doc name / topic into a `recordId` for\n `pipeshub_get_record_content` — step 1 of any full-document task\n (summarize, extract, review, \"what does the doc say?\").\n- Resolve a filename / phrase into a `recordId` for\n `pipeshub_download_record`.\n- Show the user a ranked list of matching files when they ask \"find /\n search for X\".\n\nNot for structural questions — what is under this epic, which pages are\nin this space, what links to this ticket. Ranking by content cannot show\nhow records relate; use `pipeshub_get_record_content` `mode:\"navigate\"`.\n\n**A ranked sample, never a complete list.** Hits are the top-scoring\nblocks from the best-matching records — not all blocks of any record, and\nnot every record that matches. Never count them to answer \"how many\" /\n\"all\" / \"every\"; navigate the record group instead, which reports its\nreal total.\n\nBy default it searches everything. To search only some sources, pass\nconnector ids in `apps` and collection ids in `kb`.\n\nEach hit is one matching passage, best match first:\n`{ recordId, recordName, score, snippet, mimeType, webUrl, ... }`.\nOne record can appear in several hits. Link a record by its `webUrl`."
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
16
|
"name": "pipeshub_download_record",
|
|
17
|
-
"description": "
|
|
17
|
+
"description": "Download the file as stored for one record — not PipesHub's parsed\ncontent, metadata header, or summary.\n\nUse this when the user wants the file itself (download, attach, open).\nGet `recordId` from a chat citation or a `pipeshub_search` hit.\n\nDo not use this to read, summarize, or answer \"what does this doc\nsay?\" regardless of format. That is `pipeshub_get_record_content`\n`mode:\"content\"`. Text formats come back inline; images, audio, and\nbinary as base64.\n\n`convertTo` accepts only `application/pdf`; anything else is ignored."
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
20
|
"name": "pipeshub_get_record_content",
|