llm_meta_widget 0.8.6 → 0.9.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.
- checksums.yaml +4 -4
- data/app/assets/javascripts/llm_meta_widget/config.js +6 -0
- data/app/assets/javascripts/llm_meta_widget/element.js +56 -6
- data/app/assets/javascripts/llm_meta_widget/llm-meta-widget.js +122 -7
- data/app/assets/javascripts/llm_meta_widget/orchestrator.js +146 -2
- data/app/helpers/llm_meta_widget/widget_helper.rb +10 -0
- data/app/views/llm_meta_widget/_chat_panel.html.erb +3 -0
- data/lib/llm_meta_widget/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 56c8bf50409745cc9a6cc372bdf40c8e36bd546fce37eee67c6dbce16157bedd
|
|
4
|
+
data.tar.gz: 2506d6fc96004902b4618ff7a759a1139a13bae69ca4b272e6841326d55397ee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d9bcd580647795a610c6c7612a36e2fe60fbd9962d1842fd03c07702933d9b7cbfa1850f969b25dec0a719387b33f227729da747d7702d45d2be953ba3efaa14
|
|
7
|
+
data.tar.gz: 31c03d36d61ceece0ae0e4ea4b93fa9a78ed2bea461b6ff155f9e9ac9f8e6d2f589b2f7a8efc348258323574b3e1e6d8cea9cbd07b25ec937936f235d797ffb1
|
|
@@ -26,8 +26,11 @@
|
|
|
26
26
|
export const DEFAULTS = Object.freeze({
|
|
27
27
|
API_KEY_UUID: "ollama-local",
|
|
28
28
|
ACTIONS_SCHEMA_ID: "ai-actions",
|
|
29
|
+
PAGE_CARD_ID: "ai-page",
|
|
29
30
|
STATE_GLOBAL: "aiState",
|
|
30
31
|
ACTIONS_GLOBAL: "aiActions",
|
|
32
|
+
PROMPTS_GLOBAL: "aiPrompts",
|
|
33
|
+
RESOURCES_GLOBAL: "aiResources",
|
|
31
34
|
REMOTE_TOOLS_SCHEMA_ID: "remote-mcp-tools",
|
|
32
35
|
LLM_PROVIDER: "llm_meta_server",
|
|
33
36
|
MAX_ROUNDS: 3,
|
|
@@ -59,9 +62,12 @@ export function readConfig(el) {
|
|
|
59
62
|
API_KEY_UUID: str("api-key-uuid", DEFAULTS.API_KEY_UUID),
|
|
60
63
|
MODEL: str("model", null),
|
|
61
64
|
ACTIONS_SCHEMA_ID: str("actions-schema-id", DEFAULTS.ACTIONS_SCHEMA_ID),
|
|
65
|
+
PAGE_CARD_ID: str("page-card-id", DEFAULTS.PAGE_CARD_ID),
|
|
62
66
|
GREETING: str("greeting", null),
|
|
63
67
|
STATE_GLOBAL: str("state-global", DEFAULTS.STATE_GLOBAL),
|
|
64
68
|
ACTIONS_GLOBAL: str("actions-global", DEFAULTS.ACTIONS_GLOBAL),
|
|
69
|
+
PROMPTS_GLOBAL: str("prompts-global", DEFAULTS.PROMPTS_GLOBAL),
|
|
70
|
+
RESOURCES_GLOBAL: str("resources-global", DEFAULTS.RESOURCES_GLOBAL),
|
|
65
71
|
REMOTE_TOOLS_SCHEMA_ID: str("remote-tools-schema-id", DEFAULTS.REMOTE_TOOLS_SCHEMA_ID),
|
|
66
72
|
MAX_ROUNDS: int("max-rounds", DEFAULTS.MAX_ROUNDS),
|
|
67
73
|
WELL_KNOWN_URLS: wellKnown === null
|
|
@@ -22,7 +22,9 @@ import { runChatLoop, fetchMcpManifest, listMcpPrompts, getMcpPrompt,
|
|
|
22
22
|
listMcpResources, readMcpResource, promptMessagesToText,
|
|
23
23
|
loadHostResource, resourceLinesForTurn, resolvePromptArguments,
|
|
24
24
|
promptButtonProps, promptArgumentSummary, fetchOllamaModels,
|
|
25
|
-
createConversationStore, conversationKeyFor
|
|
25
|
+
createConversationStore, conversationKeyFor,
|
|
26
|
+
readStaticPrimitivesCard, getPagePrompt,
|
|
27
|
+
planPageResources, pageResourceLinesForTurn } from "./orchestrator.js";
|
|
26
28
|
import { marked } from "./marked.esm.js";
|
|
27
29
|
import { readConfig } from "./config.js";
|
|
28
30
|
import conversationCss from "../../stylesheets/llm_meta_widget/conversation.css";
|
|
@@ -132,9 +134,12 @@ function boot(cfg) {
|
|
|
132
134
|
var API_KEY_UUID = cfg.API_KEY_UUID;
|
|
133
135
|
var MODEL = cfg.MODEL;
|
|
134
136
|
var ACTIONS_SCHEMA_ID = cfg.ACTIONS_SCHEMA_ID;
|
|
137
|
+
var PAGE_CARD_ID = cfg.PAGE_CARD_ID;
|
|
135
138
|
var GREETING = cfg.GREETING;
|
|
136
139
|
var STATE_GLOBAL = cfg.STATE_GLOBAL;
|
|
137
140
|
var ACTIONS_GLOBAL = cfg.ACTIONS_GLOBAL;
|
|
141
|
+
var PROMPTS_GLOBAL = cfg.PROMPTS_GLOBAL;
|
|
142
|
+
var RESOURCES_GLOBAL = cfg.RESOURCES_GLOBAL;
|
|
138
143
|
var REMOTE_TOOLS_SCHEMA_ID = cfg.REMOTE_TOOLS_SCHEMA_ID;
|
|
139
144
|
var MAX_ROUNDS = cfg.MAX_ROUNDS;
|
|
140
145
|
var WELL_KNOWN_URLS = cfg.WELL_KNOWN_URLS;
|
|
@@ -295,8 +300,17 @@ function boot(cfg) {
|
|
|
295
300
|
model: MODEL
|
|
296
301
|
});
|
|
297
302
|
}
|
|
303
|
+
// The page card is the same static-primitives envelope a server puts in
|
|
304
|
+
// its Server Card, delivered in the document. Its `tools` REPLACE the
|
|
305
|
+
// older #ai-actions schema tag rather than merging with it: two sources
|
|
306
|
+
// for one list means a reader has to work out which the page meant, and a
|
|
307
|
+
// handler in window.aiActions answers to a name either way.
|
|
308
|
+
var pageCardEl = document.getElementById(PAGE_CARD_ID);
|
|
309
|
+
var pageCard = readStaticPrimitivesCard(pageCardEl ? pageCardEl.textContent : null);
|
|
298
310
|
var actionsSchemaEl = document.getElementById(ACTIONS_SCHEMA_ID);
|
|
299
|
-
var localTools =
|
|
311
|
+
var localTools = pageCard.tools.length
|
|
312
|
+
? pageCard.tools
|
|
313
|
+
: (actionsSchemaEl ? JSON.parse(actionsSchemaEl.textContent) : []);
|
|
300
314
|
var remoteToolsEl = document.getElementById(REMOTE_TOOLS_SCHEMA_ID);
|
|
301
315
|
// remoteTools is a mutable live list: starts from the schema-provided
|
|
302
316
|
// baseline (if any), then the level-1 picker adds/removes entries as
|
|
@@ -576,7 +590,12 @@ function boot(cfg) {
|
|
|
576
590
|
// Fetch well-known MCP manifests at boot. Auto-discovers same origin
|
|
577
591
|
// if WELL_KNOWN_URLS is null; empty array disables entirely.
|
|
578
592
|
var hostWideTools = [];
|
|
579
|
-
|
|
593
|
+
// The page's own prompts sit in the same list as the hub's: to a visitor
|
|
594
|
+
// they are the same affordance. `source` is what routes prompts/get —
|
|
595
|
+
// a page prompt has no endpoint to ask.
|
|
596
|
+
var hostWidePrompts = pageCard.prompts.map(function(p) {
|
|
597
|
+
return Object.assign({}, p, { source: "page" });
|
|
598
|
+
});
|
|
580
599
|
var resourceContext = null; // payload of the host's reference resource
|
|
581
600
|
var resourcePlan = null; // the pre-flight decision, kept so Clear can re-arm the gate
|
|
582
601
|
|
|
@@ -585,11 +604,30 @@ function boot(cfg) {
|
|
|
585
604
|
// is 218 dictionaries / ~35KB / ~10k tokens.
|
|
586
605
|
var RESOURCE_BUDGET_BYTES = 8000;
|
|
587
606
|
|
|
607
|
+
// The page's own resources, planned once from the card. Unlike the hub's
|
|
608
|
+
// single reference resource these are a list: a page declares one per
|
|
609
|
+
// thing it knows, each with its own description.
|
|
610
|
+
var pageResourcePlans = planPageResources({
|
|
611
|
+
entries: pageCard.resources,
|
|
612
|
+
budgetBytes: RESOURCE_BUDGET_BYTES,
|
|
613
|
+
onSkip: function(plan) {
|
|
614
|
+
console.info("[llm_meta_widget] not attaching " + plan.uri +
|
|
615
|
+
" (" + plan.reason + ", size=" + plan.size + ")");
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
var pageResourceCache = {};
|
|
619
|
+
|
|
588
620
|
|
|
589
621
|
// Before anything asynchronous: if this page was navigated away from and
|
|
590
622
|
// back, the visitor should see their conversation immediately.
|
|
591
623
|
restoreConversation();
|
|
592
624
|
|
|
625
|
+
// Looked up BEFORE the discovery IIFE, not after it. That block calls
|
|
626
|
+
// renderPromptButtons(), and with no well-known URLs to fetch it has no
|
|
627
|
+
// await at all — so it ran while this was still undefined and the render
|
|
628
|
+
// bailed at its own guard. A page that declares its own prompts is
|
|
629
|
+
// exactly the case with nothing to await.
|
|
630
|
+
var promptsEl = root.querySelector(".lmw-prompts");
|
|
593
631
|
var wellKnownReady = (async function() {
|
|
594
632
|
var urls = WELL_KNOWN_URLS === null
|
|
595
633
|
? [ window.location.origin + "/.well-known/mcp.json" ]
|
|
@@ -646,7 +684,6 @@ function boot(cfg) {
|
|
|
646
684
|
// the message (prompts/get), then drops the text into the textarea and
|
|
647
685
|
// submits — so it travels the exact path a typed message does.
|
|
648
686
|
|
|
649
|
-
var promptsEl = root.querySelector(".lmw-prompts");
|
|
650
687
|
|
|
651
688
|
function renderPromptButtons() {
|
|
652
689
|
if (!promptsEl) return;
|
|
@@ -769,7 +806,9 @@ function boot(cfg) {
|
|
|
769
806
|
|
|
770
807
|
button.disabled = true;
|
|
771
808
|
try {
|
|
772
|
-
var result =
|
|
809
|
+
var result = prompt.source === "page"
|
|
810
|
+
? await getPagePrompt({ name: prompt.name, args: args, prompts: window[PROMPTS_GLOBAL] })
|
|
811
|
+
: await getMcpPrompt({ endpoint: prompt.endpoint, name: prompt.name, args: args });
|
|
773
812
|
var text = promptMessagesToText(result);
|
|
774
813
|
if (!text) { appendTurn("error", "The server returned an empty prompt."); return; }
|
|
775
814
|
inputEl.value = text;
|
|
@@ -1054,7 +1093,18 @@ function boot(cfg) {
|
|
|
1054
1093
|
budgetBytes: RESOURCE_BUDGET_BYTES
|
|
1055
1094
|
});
|
|
1056
1095
|
resourceContext = turn.cached;
|
|
1057
|
-
|
|
1096
|
+
|
|
1097
|
+
// The page's own resources ride in the same per-turn slot as the hub's.
|
|
1098
|
+
// Volatile ones are re-read here, which is the whole point: a form field
|
|
1099
|
+
// read at boot is already stale by the time anyone sends a message.
|
|
1100
|
+
var page = await pageResourceLinesForTurn({
|
|
1101
|
+
plans: pageResourcePlans,
|
|
1102
|
+
resources: window[RESOURCES_GLOBAL],
|
|
1103
|
+
budgetBytes: RESOURCE_BUDGET_BYTES,
|
|
1104
|
+
cache: pageResourceCache
|
|
1105
|
+
});
|
|
1106
|
+
pageResourceCache = page.cache;
|
|
1107
|
+
return turn.lines.concat(page.lines);
|
|
1058
1108
|
}
|
|
1059
1109
|
|
|
1060
1110
|
// Per-turn AbortController — lets the Clear button (or a new submit)
|
|
@@ -483,6 +483,17 @@ async function readMcpResource({ endpoint, uri, signal }) {
|
|
|
483
483
|
label: `readMcpResource(${uri})`
|
|
484
484
|
});
|
|
485
485
|
}
|
|
486
|
+
async function getPagePrompt({ name, args, prompts }) {
|
|
487
|
+
const handler = prompts && prompts[name];
|
|
488
|
+
if (typeof handler !== "function") {
|
|
489
|
+
throw new Error('the page declares prompt "' + name + '" but no handler answers to that name');
|
|
490
|
+
}
|
|
491
|
+
const result = await handler(args || {});
|
|
492
|
+
if (typeof result === "string") {
|
|
493
|
+
return { messages: [{ role: "user", content: { type: "text", text: result } }] };
|
|
494
|
+
}
|
|
495
|
+
return result || { messages: [] };
|
|
496
|
+
}
|
|
486
497
|
function promptMessagesToText(result) {
|
|
487
498
|
return (result?.messages || []).map((m) => {
|
|
488
499
|
const c = m.content;
|
|
@@ -549,8 +560,40 @@ function parseSseFrame(raw) {
|
|
|
549
560
|
return { name, data };
|
|
550
561
|
}
|
|
551
562
|
var STATIC_PRIMITIVES_META = "io.modelcontextprotocol/static-primitives";
|
|
563
|
+
function readStaticPrimitivesCard(text, warn = console.warn) {
|
|
564
|
+
const empty = { tools: [], prompts: [], resources: [] };
|
|
565
|
+
if (typeof text !== "string" || text.trim() === "") return empty;
|
|
566
|
+
let doc;
|
|
567
|
+
try {
|
|
568
|
+
doc = JSON.parse(text);
|
|
569
|
+
} catch (e) {
|
|
570
|
+
warn("[llm-meta-widget] page card is not valid JSON; ignoring it:", e.message);
|
|
571
|
+
return empty;
|
|
572
|
+
}
|
|
573
|
+
const ext = doc && doc._meta && doc._meta[STATIC_PRIMITIVES_META] || null;
|
|
574
|
+
if (!ext) {
|
|
575
|
+
warn(`[llm-meta-widget] page card has no _meta["${STATIC_PRIMITIVES_META}"]; ignoring it`);
|
|
576
|
+
return empty;
|
|
577
|
+
}
|
|
578
|
+
const named = (arr, kind) => (Array.isArray(arr) ? arr : []).filter((e) => {
|
|
579
|
+
const ok = e && typeof e.name === "string" && e.name.trim() !== "";
|
|
580
|
+
if (!ok) warn(`[llm-meta-widget] page card: dropping a ${kind} with no name`);
|
|
581
|
+
return ok;
|
|
582
|
+
});
|
|
583
|
+
return {
|
|
584
|
+
tools: named(ext.tools, "tool"),
|
|
585
|
+
prompts: named(ext.prompts, "prompt"),
|
|
586
|
+
// Resources are addressed by uri, not name — resources/read takes the uri.
|
|
587
|
+
resources: (Array.isArray(ext.resources) ? ext.resources : []).filter((e) => {
|
|
588
|
+
const ok = e && typeof e.uri === "string" && e.uri.trim() !== "";
|
|
589
|
+
if (!ok) warn("[llm-meta-widget] page card: dropping a resource with no uri");
|
|
590
|
+
return ok;
|
|
591
|
+
})
|
|
592
|
+
};
|
|
593
|
+
}
|
|
552
594
|
var VOLATILITIES = ["stable", "volatile"];
|
|
553
595
|
var usableSize = (n) => typeof n === "number" && isFinite(n) ? n : null;
|
|
596
|
+
var pickDeclared = (own, fromMeta) => own === void 0 ? fromMeta : own;
|
|
554
597
|
function resourceHints(entry) {
|
|
555
598
|
const meta = entry && entry._meta && entry._meta[STATIC_PRIMITIVES_META] || {};
|
|
556
599
|
return {
|
|
@@ -566,8 +609,13 @@ function resourceHints(entry) {
|
|
|
566
609
|
get sizeBytes() {
|
|
567
610
|
return this.size;
|
|
568
611
|
},
|
|
569
|
-
|
|
570
|
-
|
|
612
|
+
// Same rule as `size`: the entry's own field wins, `_meta` is the fallback.
|
|
613
|
+
// SEP-0000 defines these ON the Resource object; a server puts them in
|
|
614
|
+
// `_meta` only because the extension is not merged yet. A PAGE card's
|
|
615
|
+
// entries already sit inside the extension block, so for them the fields
|
|
616
|
+
// are simply top-level — reading both is what lets one planner serve both.
|
|
617
|
+
volatility: VOLATILITIES.indexOf(pickDeclared(entry && entry.volatility, meta.volatility)) === -1 ? "stable" : pickDeclared(entry && entry.volatility, meta.volatility),
|
|
618
|
+
autoAttach: typeof pickDeclared(entry && entry.autoAttach, meta.autoAttach) === "boolean" ? pickDeclared(entry && entry.autoAttach, meta.autoAttach) : true
|
|
571
619
|
};
|
|
572
620
|
}
|
|
573
621
|
function planResourceAttachment(entry, budgetBytes) {
|
|
@@ -611,6 +659,45 @@ async function readResourceContext({ endpoint, uri, name, read, budgetBytes }) {
|
|
|
611
659
|
return null;
|
|
612
660
|
}
|
|
613
661
|
}
|
|
662
|
+
function planPageResources({ entries, budgetBytes, onSkip }) {
|
|
663
|
+
return (entries || []).map((entry) => {
|
|
664
|
+
const plan = planResourceAttachment(entry, budgetBytes);
|
|
665
|
+
plan.name = entry.name || entry.uri;
|
|
666
|
+
plan.description = entry.description || "";
|
|
667
|
+
if (!plan.fetch && onSkip) onSkip(plan);
|
|
668
|
+
return plan;
|
|
669
|
+
}).filter((plan) => plan.fetch);
|
|
670
|
+
}
|
|
671
|
+
function pageResourceLine(plan, text) {
|
|
672
|
+
return "- " + plan.name + (plan.description ? " (" + plan.description + ")" : "") + ": " + text;
|
|
673
|
+
}
|
|
674
|
+
async function pageResourceLinesForTurn({ plans, resources, budgetBytes, cache = {} }) {
|
|
675
|
+
const lines = [];
|
|
676
|
+
for (const plan of plans || []) {
|
|
677
|
+
let text = plan.volatility === "volatile" ? null : cache[plan.uri];
|
|
678
|
+
if (text === null || text === void 0) {
|
|
679
|
+
text = await readPageResource({ uri: plan.uri, resources, budgetBytes });
|
|
680
|
+
if (plan.volatility !== "volatile" && text !== null) cache[plan.uri] = text;
|
|
681
|
+
}
|
|
682
|
+
if (text !== null) lines.push(pageResourceLine(plan, text));
|
|
683
|
+
}
|
|
684
|
+
return { lines, cache };
|
|
685
|
+
}
|
|
686
|
+
async function readPageResource({ uri, resources, budgetBytes }) {
|
|
687
|
+
const handler = resources && resources[uri];
|
|
688
|
+
if (typeof handler !== "function") {
|
|
689
|
+
console.warn('[llm-meta-widget] the page declares resource "' + uri + '" but no handler answers to that uri');
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
try {
|
|
693
|
+
const value = await handler();
|
|
694
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
695
|
+
return trimResourceText(text === void 0 ? "" : text, budgetBytes);
|
|
696
|
+
} catch (e) {
|
|
697
|
+
console.warn('[llm-meta-widget] reading "' + uri + '" failed:', e && e.message);
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
614
701
|
async function loadHostResource({ endpoint, budgetBytes, list: list2, read, onSkip }) {
|
|
615
702
|
const resources = await list2({ endpoint });
|
|
616
703
|
const candidate = (resources || []).filter((r) => (r.mimeType || "") === "application/json")[0];
|
|
@@ -2904,8 +2991,11 @@ var lexer = _Lexer.lex;
|
|
|
2904
2991
|
var DEFAULTS = Object.freeze({
|
|
2905
2992
|
API_KEY_UUID: "ollama-local",
|
|
2906
2993
|
ACTIONS_SCHEMA_ID: "ai-actions",
|
|
2994
|
+
PAGE_CARD_ID: "ai-page",
|
|
2907
2995
|
STATE_GLOBAL: "aiState",
|
|
2908
2996
|
ACTIONS_GLOBAL: "aiActions",
|
|
2997
|
+
PROMPTS_GLOBAL: "aiPrompts",
|
|
2998
|
+
RESOURCES_GLOBAL: "aiResources",
|
|
2909
2999
|
REMOTE_TOOLS_SCHEMA_ID: "remote-mcp-tools",
|
|
2910
3000
|
LLM_PROVIDER: "llm_meta_server",
|
|
2911
3001
|
MAX_ROUNDS: 3
|
|
@@ -2939,9 +3029,12 @@ function readConfig(el) {
|
|
|
2939
3029
|
API_KEY_UUID: str("api-key-uuid", DEFAULTS.API_KEY_UUID),
|
|
2940
3030
|
MODEL: str("model", null),
|
|
2941
3031
|
ACTIONS_SCHEMA_ID: str("actions-schema-id", DEFAULTS.ACTIONS_SCHEMA_ID),
|
|
3032
|
+
PAGE_CARD_ID: str("page-card-id", DEFAULTS.PAGE_CARD_ID),
|
|
2942
3033
|
GREETING: str("greeting", null),
|
|
2943
3034
|
STATE_GLOBAL: str("state-global", DEFAULTS.STATE_GLOBAL),
|
|
2944
3035
|
ACTIONS_GLOBAL: str("actions-global", DEFAULTS.ACTIONS_GLOBAL),
|
|
3036
|
+
PROMPTS_GLOBAL: str("prompts-global", DEFAULTS.PROMPTS_GLOBAL),
|
|
3037
|
+
RESOURCES_GLOBAL: str("resources-global", DEFAULTS.RESOURCES_GLOBAL),
|
|
2945
3038
|
REMOTE_TOOLS_SCHEMA_ID: str("remote-tools-schema-id", DEFAULTS.REMOTE_TOOLS_SCHEMA_ID),
|
|
2946
3039
|
MAX_ROUNDS: int("max-rounds", DEFAULTS.MAX_ROUNDS),
|
|
2947
3040
|
WELL_KNOWN_URLS: wellKnown === null ? null : wellKnown.trim() === "" ? [] : splitList(wellKnown),
|
|
@@ -3658,9 +3751,12 @@ function boot(cfg) {
|
|
|
3658
3751
|
var API_KEY_UUID = cfg.API_KEY_UUID;
|
|
3659
3752
|
var MODEL = cfg.MODEL;
|
|
3660
3753
|
var ACTIONS_SCHEMA_ID = cfg.ACTIONS_SCHEMA_ID;
|
|
3754
|
+
var PAGE_CARD_ID = cfg.PAGE_CARD_ID;
|
|
3661
3755
|
var GREETING = cfg.GREETING;
|
|
3662
3756
|
var STATE_GLOBAL = cfg.STATE_GLOBAL;
|
|
3663
3757
|
var ACTIONS_GLOBAL = cfg.ACTIONS_GLOBAL;
|
|
3758
|
+
var PROMPTS_GLOBAL = cfg.PROMPTS_GLOBAL;
|
|
3759
|
+
var RESOURCES_GLOBAL = cfg.RESOURCES_GLOBAL;
|
|
3664
3760
|
var REMOTE_TOOLS_SCHEMA_ID = cfg.REMOTE_TOOLS_SCHEMA_ID;
|
|
3665
3761
|
var MAX_ROUNDS = cfg.MAX_ROUNDS;
|
|
3666
3762
|
var WELL_KNOWN_URLS = cfg.WELL_KNOWN_URLS;
|
|
@@ -3791,8 +3887,10 @@ function boot(cfg) {
|
|
|
3791
3887
|
model: MODEL
|
|
3792
3888
|
});
|
|
3793
3889
|
}
|
|
3890
|
+
var pageCardEl = document.getElementById(PAGE_CARD_ID);
|
|
3891
|
+
var pageCard = readStaticPrimitivesCard(pageCardEl ? pageCardEl.textContent : null);
|
|
3794
3892
|
var actionsSchemaEl = document.getElementById(ACTIONS_SCHEMA_ID);
|
|
3795
|
-
var localTools = actionsSchemaEl ? JSON.parse(actionsSchemaEl.textContent) : [];
|
|
3893
|
+
var localTools = pageCard.tools.length ? pageCard.tools : actionsSchemaEl ? JSON.parse(actionsSchemaEl.textContent) : [];
|
|
3796
3894
|
var remoteToolsEl = document.getElementById(REMOTE_TOOLS_SCHEMA_ID);
|
|
3797
3895
|
var baselineRemoteTools = remoteToolsEl ? JSON.parse(remoteToolsEl.textContent) : [];
|
|
3798
3896
|
var remoteTools = baselineRemoteTools.slice();
|
|
@@ -4015,11 +4113,22 @@ function boot(cfg) {
|
|
|
4015
4113
|
}
|
|
4016
4114
|
}
|
|
4017
4115
|
var hostWideTools = [];
|
|
4018
|
-
var hostWidePrompts =
|
|
4116
|
+
var hostWidePrompts = pageCard.prompts.map(function(p) {
|
|
4117
|
+
return Object.assign({}, p, { source: "page" });
|
|
4118
|
+
});
|
|
4019
4119
|
var resourceContext = null;
|
|
4020
4120
|
var resourcePlan = null;
|
|
4021
4121
|
var RESOURCE_BUDGET_BYTES = 8e3;
|
|
4122
|
+
var pageResourcePlans = planPageResources({
|
|
4123
|
+
entries: pageCard.resources,
|
|
4124
|
+
budgetBytes: RESOURCE_BUDGET_BYTES,
|
|
4125
|
+
onSkip: function(plan) {
|
|
4126
|
+
console.info("[llm_meta_widget] not attaching " + plan.uri + " (" + plan.reason + ", size=" + plan.size + ")");
|
|
4127
|
+
}
|
|
4128
|
+
});
|
|
4129
|
+
var pageResourceCache = {};
|
|
4022
4130
|
restoreConversation();
|
|
4131
|
+
var promptsEl = root.querySelector(".lmw-prompts");
|
|
4023
4132
|
var wellKnownReady = (async function() {
|
|
4024
4133
|
var urls = WELL_KNOWN_URLS === null ? [window.location.origin + "/.well-known/mcp.json"] : WELL_KNOWN_URLS;
|
|
4025
4134
|
for (var i = 0; i < urls.length; i++) {
|
|
@@ -4053,7 +4162,6 @@ function boot(cfg) {
|
|
|
4053
4162
|
renderPromptButtons();
|
|
4054
4163
|
renderWelcome();
|
|
4055
4164
|
})();
|
|
4056
|
-
var promptsEl = root.querySelector(".lmw-prompts");
|
|
4057
4165
|
function renderPromptButtons() {
|
|
4058
4166
|
if (!promptsEl) return;
|
|
4059
4167
|
promptsEl.textContent = "";
|
|
@@ -4150,7 +4258,7 @@ function boot(cfg) {
|
|
|
4150
4258
|
}
|
|
4151
4259
|
button.disabled = true;
|
|
4152
4260
|
try {
|
|
4153
|
-
var result = await getMcpPrompt({ endpoint: prompt.endpoint, name: prompt.name, args });
|
|
4261
|
+
var result = prompt.source === "page" ? await getPagePrompt({ name: prompt.name, args, prompts: window[PROMPTS_GLOBAL] }) : await getMcpPrompt({ endpoint: prompt.endpoint, name: prompt.name, args });
|
|
4154
4262
|
var text = promptMessagesToText(result);
|
|
4155
4263
|
if (!text) {
|
|
4156
4264
|
appendTurn("error", "The server returned an empty prompt.");
|
|
@@ -4364,7 +4472,14 @@ function boot(cfg) {
|
|
|
4364
4472
|
budgetBytes: RESOURCE_BUDGET_BYTES
|
|
4365
4473
|
});
|
|
4366
4474
|
resourceContext = turn.cached;
|
|
4367
|
-
|
|
4475
|
+
var page = await pageResourceLinesForTurn({
|
|
4476
|
+
plans: pageResourcePlans,
|
|
4477
|
+
resources: window[RESOURCES_GLOBAL],
|
|
4478
|
+
budgetBytes: RESOURCE_BUDGET_BYTES,
|
|
4479
|
+
cache: pageResourceCache
|
|
4480
|
+
});
|
|
4481
|
+
pageResourceCache = page.cache;
|
|
4482
|
+
return turn.lines.concat(page.lines);
|
|
4368
4483
|
}
|
|
4369
4484
|
var currentAbort = null;
|
|
4370
4485
|
clearBtn.addEventListener("click", function() {
|
|
@@ -730,6 +730,28 @@ export async function readMcpResource({ endpoint, uri, signal }) {
|
|
|
730
730
|
// The spec allows `content` to be an object or an array of content blocks;
|
|
731
731
|
// anything non-text is dropped, which is worth knowing — a prompt carrying an
|
|
732
732
|
// image would silently lose it on this path.
|
|
733
|
+
// A prompt the PAGE declared, resolved against its handler map. This is
|
|
734
|
+
// prompts/get for a surface with no transport: the card carries the metadata
|
|
735
|
+
// (name, description, arguments — what prompts/list returns) and the handler
|
|
736
|
+
// returns the filled messages, exactly as a server's prompts/get would.
|
|
737
|
+
//
|
|
738
|
+
// Keyed by `name` because that is what prompts/get takes. Accepts a plain
|
|
739
|
+
// string as well as a messages array: a page prompt is usually one block of
|
|
740
|
+
// text, and making every host write the envelope by hand would be a tax with
|
|
741
|
+
// no benefit — promptMessagesToText flattens both to the same thing.
|
|
742
|
+
export async function getPagePrompt({ name, args, prompts }) {
|
|
743
|
+
const handler = prompts && prompts[name]
|
|
744
|
+
if (typeof handler !== "function") {
|
|
745
|
+
throw new Error('the page declares prompt "' + name + '" but no handler answers to that name')
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const result = await handler(args || {})
|
|
749
|
+
if (typeof result === "string") {
|
|
750
|
+
return { messages: [ { role: "user", content: { type: "text", text: result } } ] }
|
|
751
|
+
}
|
|
752
|
+
return result || { messages: [] }
|
|
753
|
+
}
|
|
754
|
+
|
|
733
755
|
export function promptMessagesToText(result) {
|
|
734
756
|
return (result?.messages || [])
|
|
735
757
|
.map((m) => {
|
|
@@ -840,9 +862,63 @@ function parseSseFrame(raw) {
|
|
|
840
862
|
// the first turn only.
|
|
841
863
|
export const STATIC_PRIMITIVES_META = "io.modelcontextprotocol/static-primitives"
|
|
842
864
|
|
|
865
|
+
// ---- page card --------------------------------------------------------
|
|
866
|
+
//
|
|
867
|
+
// The same `io.modelcontextprotocol/static-primitives` envelope a server puts
|
|
868
|
+
// in its Server Card, delivered in the DOCUMENT instead: one <script> of JSON
|
|
869
|
+
// declaring the page's own tools, prompts and resources. SEP-2575 already says
|
|
870
|
+
// the Server Card and server/discover are two deliveries of one content; a page
|
|
871
|
+
// is a third, with no transport at all.
|
|
872
|
+
//
|
|
873
|
+
// Parsing only. What a declaration MEANS is resolved elsewhere: tool handlers
|
|
874
|
+
// live in window.aiActions keyed by `name`, and reads will be keyed by the
|
|
875
|
+
// protocol's own identifier — `name` for prompts, `uri` for resources, because
|
|
876
|
+
// that is what prompts/get and resources/read take.
|
|
877
|
+
//
|
|
878
|
+
// Malformed input must never take the page down: a card is host-authored and a
|
|
879
|
+
// typo in it should cost that one entry, not the assistant. Everything
|
|
880
|
+
// unparseable returns empty arrays and says so on the console, which is how
|
|
881
|
+
// aiState already treats a reader missing its description.
|
|
882
|
+
export function readStaticPrimitivesCard(text, warn = console.warn) {
|
|
883
|
+
const empty = { tools: [], prompts: [], resources: [] }
|
|
884
|
+
if (typeof text !== "string" || text.trim() === "") return empty
|
|
885
|
+
|
|
886
|
+
let doc
|
|
887
|
+
try {
|
|
888
|
+
doc = JSON.parse(text)
|
|
889
|
+
} catch (e) {
|
|
890
|
+
warn("[llm-meta-widget] page card is not valid JSON; ignoring it:", e.message)
|
|
891
|
+
return empty
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const ext = (doc && doc._meta && doc._meta[STATIC_PRIMITIVES_META]) || null
|
|
895
|
+
if (!ext) {
|
|
896
|
+
warn(`[llm-meta-widget] page card has no _meta["${STATIC_PRIMITIVES_META}"]; ignoring it`)
|
|
897
|
+
return empty
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const named = (arr, kind) => (Array.isArray(arr) ? arr : []).filter((e) => {
|
|
901
|
+
const ok = e && typeof e.name === "string" && e.name.trim() !== ""
|
|
902
|
+
if (!ok) warn(`[llm-meta-widget] page card: dropping a ${kind} with no name`)
|
|
903
|
+
return ok
|
|
904
|
+
})
|
|
905
|
+
|
|
906
|
+
return {
|
|
907
|
+
tools: named(ext.tools, "tool"),
|
|
908
|
+
prompts: named(ext.prompts, "prompt"),
|
|
909
|
+
// Resources are addressed by uri, not name — resources/read takes the uri.
|
|
910
|
+
resources: (Array.isArray(ext.resources) ? ext.resources : []).filter((e) => {
|
|
911
|
+
const ok = e && typeof e.uri === "string" && e.uri.trim() !== ""
|
|
912
|
+
if (!ok) warn("[llm-meta-widget] page card: dropping a resource with no uri")
|
|
913
|
+
return ok
|
|
914
|
+
})
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
843
918
|
const VOLATILITIES = [ "stable", "volatile" ]
|
|
844
919
|
|
|
845
920
|
const usableSize = (n) => (typeof n === "number" && isFinite(n) ? n : null)
|
|
921
|
+
const pickDeclared = (own, fromMeta) => (own === undefined ? fromMeta : own)
|
|
846
922
|
|
|
847
923
|
export function resourceHints(entry) {
|
|
848
924
|
const meta = (entry && entry._meta && entry._meta[STATIC_PRIMITIVES_META]) || {}
|
|
@@ -857,8 +933,17 @@ export function resourceHints(entry) {
|
|
|
857
933
|
// it returns is public API. Dropped at 0.9.0 together with the
|
|
858
934
|
// `_meta.sizeBytes` fallback above — one deprecation, one bump.
|
|
859
935
|
get sizeBytes() { return this.size },
|
|
860
|
-
|
|
861
|
-
|
|
936
|
+
// Same rule as `size`: the entry's own field wins, `_meta` is the fallback.
|
|
937
|
+
// SEP-0000 defines these ON the Resource object; a server puts them in
|
|
938
|
+
// `_meta` only because the extension is not merged yet. A PAGE card's
|
|
939
|
+
// entries already sit inside the extension block, so for them the fields
|
|
940
|
+
// are simply top-level — reading both is what lets one planner serve both.
|
|
941
|
+
volatility: VOLATILITIES.indexOf(pickDeclared(entry && entry.volatility, meta.volatility)) === -1
|
|
942
|
+
? "stable"
|
|
943
|
+
: pickDeclared(entry && entry.volatility, meta.volatility),
|
|
944
|
+
autoAttach: typeof pickDeclared(entry && entry.autoAttach, meta.autoAttach) === "boolean"
|
|
945
|
+
? pickDeclared(entry && entry.autoAttach, meta.autoAttach)
|
|
946
|
+
: true
|
|
862
947
|
}
|
|
863
948
|
}
|
|
864
949
|
|
|
@@ -920,6 +1005,65 @@ async function readResourceContext({ endpoint, uri, name, read, budgetBytes }) {
|
|
|
920
1005
|
}
|
|
921
1006
|
}
|
|
922
1007
|
|
|
1008
|
+
// ---- page resources ----------------------------------------------------
|
|
1009
|
+
//
|
|
1010
|
+
// resources/read for a surface with no transport. The card lists them, a
|
|
1011
|
+
// handler map keyed by URI reads them — URI because that is what
|
|
1012
|
+
// resources/read takes, and because the spec defines it as the unique
|
|
1013
|
+
// identifier while `name` is only required, never unique.
|
|
1014
|
+
//
|
|
1015
|
+
// Unlike a server endpoint, a page declares SEVERAL: PubDictionaries has six
|
|
1016
|
+
// readers, each with its own description. So these are planned and rendered as
|
|
1017
|
+
// a list, not as the single reference resource loadHostResource assumes.
|
|
1018
|
+
export function planPageResources({ entries, budgetBytes, onSkip }) {
|
|
1019
|
+
return (entries || []).map((entry) => {
|
|
1020
|
+
const plan = planResourceAttachment(entry, budgetBytes)
|
|
1021
|
+
plan.name = entry.name || entry.uri
|
|
1022
|
+
plan.description = entry.description || ""
|
|
1023
|
+
if (!plan.fetch && onSkip) onSkip(plan)
|
|
1024
|
+
return plan
|
|
1025
|
+
}).filter((plan) => plan.fetch)
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// One compact line per resource — the same shape the page-state annex uses,
|
|
1029
|
+
// NOT the block form a hub resource gets. Six block-form resources would cost
|
|
1030
|
+
// three lines each and repeat a header the model does not need; how to render
|
|
1031
|
+
// attached context is the client's decision, which the spec leaves open.
|
|
1032
|
+
export function pageResourceLine(plan, text) {
|
|
1033
|
+
return "- " + plan.name + (plan.description ? " (" + plan.description + ")" : "") + ": " + text
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
export async function pageResourceLinesForTurn({ plans, resources, budgetBytes, cache = {} }) {
|
|
1037
|
+
const lines = []
|
|
1038
|
+
for (const plan of plans || []) {
|
|
1039
|
+
let text = plan.volatility === "volatile" ? null : cache[plan.uri]
|
|
1040
|
+
if (text === null || text === undefined) {
|
|
1041
|
+
text = await readPageResource({ uri: plan.uri, resources, budgetBytes })
|
|
1042
|
+
if (plan.volatility !== "volatile" && text !== null) cache[plan.uri] = text
|
|
1043
|
+
}
|
|
1044
|
+
if (text !== null) lines.push(pageResourceLine(plan, text))
|
|
1045
|
+
}
|
|
1046
|
+
return { lines, cache }
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
async function readPageResource({ uri, resources, budgetBytes }) {
|
|
1050
|
+
const handler = resources && resources[uri]
|
|
1051
|
+
if (typeof handler !== "function") {
|
|
1052
|
+
console.warn('[llm-meta-widget] the page declares resource "' + uri + '" but no handler answers to that uri')
|
|
1053
|
+
return null
|
|
1054
|
+
}
|
|
1055
|
+
try {
|
|
1056
|
+
const value = await handler()
|
|
1057
|
+
const text = typeof value === "string" ? value : JSON.stringify(value)
|
|
1058
|
+
return trimResourceText(text === undefined ? "" : text, budgetBytes)
|
|
1059
|
+
} catch (e) {
|
|
1060
|
+
// Optional context — one broken reader must not cost the turn, exactly as
|
|
1061
|
+
// a failed hub read does not.
|
|
1062
|
+
console.warn('[llm-meta-widget] reading "' + uri + '" failed:', e && e.message)
|
|
1063
|
+
return null
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
923
1067
|
// The discovery sequence for one endpoint's reference resource, kept here
|
|
924
1068
|
// rather than in the panel so the decision AND the call site are testable
|
|
925
1069
|
// together: a gate nothing consults is exactly the bug this shape prevents.
|
|
@@ -39,8 +39,18 @@ module LlmMetaWidget
|
|
|
39
39
|
# instead of this gem (same file, published to npm).
|
|
40
40
|
element_path: "/llm_meta_widget_assets/llm-meta-widget.js",
|
|
41
41
|
actions_schema_id: "ai-actions",
|
|
42
|
+
# The page card: the same static-primitives envelope a server puts in
|
|
43
|
+
# its Server Card, delivered in the document. Its `tools` replace the
|
|
44
|
+
# actions_schema_id block when the card declares any.
|
|
45
|
+
page_card_id: "ai-page",
|
|
42
46
|
state_global: "aiState",
|
|
43
47
|
actions_global: "aiActions",
|
|
48
|
+
# Page-declared prompts resolve their messages here, keyed by name —
|
|
49
|
+
# prompts/get for a surface with no transport.
|
|
50
|
+
prompts_global: "aiPrompts",
|
|
51
|
+
# Page-declared resources are read here, keyed by URI — resources/read
|
|
52
|
+
# for the same surface. URI, because that is what resources/read takes.
|
|
53
|
+
resources_global: "aiResources",
|
|
44
54
|
remote_tools_schema_id: "remote-mcp-tools",
|
|
45
55
|
# nil → widget auto-discovers same-origin /.well-known/mcp.json at boot;
|
|
46
56
|
# explicit array → fetch those URLs; empty array → disable entirely.
|
|
@@ -28,8 +28,11 @@
|
|
|
28
28
|
<%= tag_attr("greeting", greeting) %>
|
|
29
29
|
<%= tag_attr("max-rounds", max_rounds) %>
|
|
30
30
|
<%= tag_attr("actions-schema-id", actions_schema_id) %>
|
|
31
|
+
<%= tag_attr("page-card-id", page_card_id) %>
|
|
31
32
|
<%= tag_attr("state-global", state_global) %>
|
|
32
33
|
<%= tag_attr("actions-global", actions_global) %>
|
|
34
|
+
<%= tag_attr("prompts-global", prompts_global) %>
|
|
35
|
+
<%= tag_attr("resources-global", resources_global) %>
|
|
33
36
|
<%= tag_attr("remote-tools-schema-id", remote_tools_schema_id) %>
|
|
34
37
|
<%# Pickers default to true in the element, so only emit the disabling value. %>
|
|
35
38
|
<%= enable_model_picker ? "" : 'enable-model-picker="false"'.html_safe %>
|