llm_meta_widget 0.9.0 → 0.9.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 56c8bf50409745cc9a6cc372bdf40c8e36bd546fce37eee67c6dbce16157bedd
4
- data.tar.gz: 2506d6fc96004902b4618ff7a759a1139a13bae69ca4b272e6841326d55397ee
3
+ metadata.gz: f6dba4c1cd88e26bde9f0179009ae2f47cdd07a30aae6035e619f0ca5b83d32e
4
+ data.tar.gz: 95cca5b804adab95a89dfbdf23b7624cb43304462d4cf6c72a605a7ebef170de
5
5
  SHA512:
6
- metadata.gz: d9bcd580647795a610c6c7612a36e2fe60fbd9962d1842fd03c07702933d9b7cbfa1850f969b25dec0a719387b33f227729da747d7702d45d2be953ba3efaa14
7
- data.tar.gz: 31c03d36d61ceece0ae0e4ea4b93fa9a78ed2bea461b6ff155f9e9ac9f8e6d2f589b2f7a8efc348258323574b3e1e6d8cea9cbd07b25ec937936f235d797ffb1
6
+ metadata.gz: d2c499f1d83a689e52fff3fabb0cc08f359c4d34571295085718f3fa5b8c13ccf565f6f47c9ae73b210f1bea350581e7e1c4ae47ac27a759116364518fce2bd2
7
+ data.tar.gz: bb45042dcae5849cf88750d8d6235416fda8e531f0852ed454405c59c4358ff3c31137b78349b6b87be8fc490c3235de62b5b9bebd4f0a61cea790c0cf0925ba
data/README.md CHANGED
@@ -216,6 +216,7 @@ easy to get wrong:
216
216
  | `llm-provider` | `llm_meta_server` (default) or `ollama` |
217
217
  | `api-key-uuid`, `greeting`, `max-rounds` | as the helper options |
218
218
  | `actions-schema-id`, `state-global`, `actions-global`, `remote-tools-schema-id` | as the helper options |
219
+ | `page-card-id`, `prompts-global`, `resources-global` | as the helper options |
219
220
  | `models`, `hub-tools` | comma-separated. Omitted **or empty** means no allowlist — an allowlist permitting nothing is never what anyone meant |
220
221
  | `enable-model-picker`, `enable-tool-picker` | **value attributes, not boolean attributes.** They default to true, so presence cannot mean true. Disable with `enable-tool-picker="false"`; any other value is true |
221
222
  | `well-known-urls` | **Three different states**, because an attribute cannot tell "not set" from "set to empty": omitted = auto-discover same-origin `/.well-known/mcp.json`; `""` = discovery off; `"a,b"` = fetch those |
@@ -401,6 +402,9 @@ translate directly.
401
402
  | `api_key_uuid:` | `"ollama-local"` | Hub API-key uuid to invoke |
402
403
  | `element_path:` | `"/llm_meta_widget_assets/llm-meta-widget.js"` | The bundled element the page loads. Served by the gem's engine; override to load it from elsewhere |
403
404
  | `actions_schema_id:` | `"ai-actions"` | DOM id of the Class-3 schema block |
405
+ | `page_card_id:` | `"ai-page"` | DOM id of the page card (see "The page card"). Its tools replace the `actions_schema_id` block |
406
+ | `prompts_global:` | `"aiPrompts"` | Global object holding page prompt handlers, keyed by prompt name |
407
+ | `resources_global:` | `"aiResources"` | Global object holding page resource readers, keyed by resource URI |
404
408
  | `state_global:` | `"aiState"` | Global window object holding Class-3 state readers, each `{ description, read }` (see the breaking change in 0.8.0) |
405
409
  | `actions_global:` | `"aiActions"` | Global window object holding Class-3 implementations |
406
410
  | `remote_tools_schema_id:` | `"remote-mcp-tools"` | DOM id of a JSON block listing Class-1 tools (`{id, name, description, input_schema}`) to start with. They seed the picker rather than bypassing it — shown ticked, and the visitor may untick them. Read once, at boot |
@@ -412,6 +416,59 @@ translate directly.
412
416
  | `models:` | `nil` | Model-name allowlist; `nil` = all anon-available |
413
417
  | `hub_tools:` | `nil` | MCP-server-name allowlist; `nil` = all anon-public |
414
418
 
419
+ ## The page card
420
+
421
+ Since 0.9.0 a page can declare what it offers in one block of JSON, instead of
422
+ using a separate channel for each kind of thing. The block uses the same envelope
423
+ an MCP server puts in its Server Card, so the page and the server describe
424
+ themselves the same way.
425
+
426
+ ```html
427
+ <script type="application/json" id="ai-page">
428
+ { "_meta": { "io.modelcontextprotocol/static-primitives": {
429
+ "tools": [ { "name": "set_text", "description": "…", "inputSchema": { } } ],
430
+ "prompts": [ { "name": "annotate", "description": "Annotate this page" } ],
431
+ "resources": [ { "uri": "page:text", "name": "Text", "description": "the text to annotate",
432
+ "volatility": "volatile" } ]
433
+ } } }
434
+ </script>
435
+ ```
436
+
437
+ The card says what exists. Three global objects say what it does, and each one is
438
+ keyed by the identifier that the matching MCP operation takes:
439
+
440
+ | declared in the card | implemented in | keyed by |
441
+ |---|---|---|
442
+ | `tools` | `window.aiActions` | tool `name`, as `tools/call` takes |
443
+ | `prompts` | `window.aiPrompts` | prompt `name`, as `prompts/get` takes |
444
+ | `resources` | `window.aiResources` | resource `uri`, as `resources/read` takes |
445
+
446
+ ```js
447
+ window.aiActions = { set_text: (args) => { /* change the page */ } }
448
+ window.aiPrompts = { annotate: (args) => "Annotate this: " + args.text }
449
+ window.aiResources = { "page:text": () => document.querySelector("#text").value }
450
+ ```
451
+
452
+ A prompt handler may return a plain string or the full `messages` array that
453
+ `prompts/get` returns. A resource reader may return a string or any value that
454
+ can be turned into JSON.
455
+
456
+ **Resources carry two optional hints.** `volatility` is `"volatile"` when the
457
+ value changes between turns, so the widget reads it again every turn; the default
458
+ is `"stable"`, which is read once and reused. `autoAttach: false` keeps a resource
459
+ out of the turn unless something asks for it. A `size` in bytes lets the widget
460
+ decide whether the value fits its budget before reading it.
461
+
462
+ **The `page:` scheme is experimental.** It is not a registered URI scheme, and
463
+ nothing outside this widget gives it meaning. It is used because the protocol
464
+ asks for a URI, and because `https://` is reserved for resources a client can
465
+ fetch from the web by itself — which page resources are not. The form is
466
+ `page:<name>` with no path, because a page has only one namespace. Expect this to
467
+ change.
468
+
469
+ **The card is optional.** A page that declares nothing keeps working exactly as
470
+ before, and a page that declares no `tools` still uses its `ai-actions` block.
471
+
415
472
  ## Surviving navigation
416
473
 
417
474
  A page action that navigates — submitting a form, following a link — no longer
@@ -24,7 +24,8 @@ import { runChatLoop, fetchMcpManifest, listMcpPrompts, getMcpPrompt,
24
24
  promptButtonProps, promptArgumentSummary, fetchOllamaModels,
25
25
  createConversationStore, conversationKeyFor,
26
26
  readStaticPrimitivesCard, getPagePrompt,
27
- planPageResources, pageResourceLinesForTurn } from "./orchestrator.js";
27
+ planPageResources, pageResourceLinesForTurn,
28
+ promptArgState } from "./orchestrator.js";
28
29
  import { marked } from "./marked.esm.js";
29
30
  import { readConfig } from "./config.js";
30
31
  import conversationCss from "../../stylesheets/llm_meta_widget/conversation.css";
@@ -685,6 +686,15 @@ function boot(cfg) {
685
686
  // submits — so it travels the exact path a typed message does.
686
687
 
687
688
 
689
+ // Where a prompt's declared arguments are filled from. A card-only page keeps
690
+ // its state in aiResources under `page:<name>` URIs, so the names the resolver
691
+ // looks up live there, not in aiState.
692
+ function argState() {
693
+ return promptArgState({ entries: pageCard.resources,
694
+ resources: window[RESOURCES_GLOBAL] || {},
695
+ state: window[STATE_GLOBAL] || {} });
696
+ }
697
+
688
698
  function renderPromptButtons() {
689
699
  if (!promptsEl) return;
690
700
  promptsEl.textContent = "";
@@ -749,7 +759,7 @@ function boot(cfg) {
749
759
  (hostWidePrompts.length ? ", or start with one of these:" : ".");
750
760
  box.appendChild(hello);
751
761
 
752
- var state = window[STATE_GLOBAL] || {};
762
+ var state = argState();
753
763
  hostWidePrompts.forEach(function(prompt) {
754
764
  var props = promptButtonProps(prompt);
755
765
  var card = document.createElement("div");
@@ -792,7 +802,7 @@ function boot(cfg) {
792
802
  }
793
803
 
794
804
  async function runPromptTemplate(prompt, button) {
795
- var resolved = resolvePromptArguments(prompt, window[STATE_GLOBAL] || {});
805
+ var resolved = resolvePromptArguments(prompt, argState());
796
806
  var args = resolved.args;
797
807
  var missing = resolved.missing;
798
808
 
@@ -737,7 +737,20 @@ async function resourceLinesForTurn({ plan, cached, endpoint, read, budgetBytes
737
737
  }
738
738
  return { lines: resourceContextLines(cached), cached };
739
739
  }
740
+ var PAGE_URI_PREFIX = "page:";
740
741
  var PROMPT_ARG_ALIASES = { dictionaries: "selected_dictionaries" };
742
+ function promptArgState({ entries = [], resources = {}, state = {} } = {}) {
743
+ const merged = Object.assign({}, state);
744
+ for (const entry of entries) {
745
+ const uri = entry && entry.uri;
746
+ if (!uri || uri.indexOf(PAGE_URI_PREFIX) !== 0) continue;
747
+ const name = uri.slice(PAGE_URI_PREFIX.length);
748
+ const reader = resources[uri];
749
+ if (!name || merged[name] || typeof reader !== "function") continue;
750
+ merged[name] = { description: entry.description || "", read: reader };
751
+ }
752
+ return merged;
753
+ }
741
754
  function promptArgFromState(argName, state) {
742
755
  const entry = state && state[argName] || state && state[PROMPT_ARG_ALIASES[argName]];
743
756
  const reader = entry && typeof entry.read === "function" ? entry.read : null;
@@ -4162,6 +4175,13 @@ function boot(cfg) {
4162
4175
  renderPromptButtons();
4163
4176
  renderWelcome();
4164
4177
  })();
4178
+ function argState() {
4179
+ return promptArgState({
4180
+ entries: pageCard.resources,
4181
+ resources: window[RESOURCES_GLOBAL] || {},
4182
+ state: window[STATE_GLOBAL] || {}
4183
+ });
4184
+ }
4165
4185
  function renderPromptButtons() {
4166
4186
  if (!promptsEl) return;
4167
4187
  promptsEl.textContent = "";
@@ -4213,7 +4233,7 @@ function boot(cfg) {
4213
4233
  hello.className = "lmw-welcome-hello";
4214
4234
  hello.textContent = GREETING || "Hi \u2014 I can work this page for you. Tell me what you need in your own words" + (hostWidePrompts.length ? ", or start with one of these:" : ".");
4215
4235
  box.appendChild(hello);
4216
- var state = window[STATE_GLOBAL] || {};
4236
+ var state = argState();
4217
4237
  hostWidePrompts.forEach(function(prompt) {
4218
4238
  var props = promptButtonProps(prompt);
4219
4239
  var card = document.createElement("div");
@@ -4249,7 +4269,7 @@ function boot(cfg) {
4249
4269
  historyEl.appendChild(box);
4250
4270
  }
4251
4271
  async function runPromptTemplate(prompt, button) {
4252
- var resolved = resolvePromptArguments(prompt, window[STATE_GLOBAL] || {});
4272
+ var resolved = resolvePromptArguments(prompt, argState());
4253
4273
  var args = resolved.args;
4254
4274
  var missing = resolved.missing;
4255
4275
  if (missing.length) {
@@ -1129,8 +1129,35 @@ export async function resourceLinesForTurn({ plan, cached, endpoint, read, budge
1129
1129
  // (window.aiState) by name. `dictionaries` is read from a
1130
1130
  // `selected_dictionaries` reader: the page selects a LIST, and the prompt
1131
1131
  // declares the CSV shape the text_annotation tool already takes.
1132
+ // Page resources are addressed `page:<name>`: a custom scheme because the
1133
+ // protocol asks for a URI and `https:` is reserved for what a client can fetch
1134
+ // itself, and with no path because a page has one namespace.
1135
+ const PAGE_URI_PREFIX = "page:"
1136
+
1132
1137
  const PROMPT_ARG_ALIASES = { dictionaries: "selected_dictionaries" }
1133
1138
 
1139
+ // Prompt arguments are declared by plain name (`text`); page resources are
1140
+ // addressed by URI (`page:text`). So on a page that declares its state as
1141
+ // resources and keeps no `aiState`, the resolver below finds nothing under the
1142
+ // names it looks up and every argument arrives empty — the visitor is asked to
1143
+ // retype what is already on screen. This presents those resources in the shape
1144
+ // the resolver consumes, keyed by the name inside the URI.
1145
+ //
1146
+ // An explicit `aiState` entry wins: a page running both channels has said what
1147
+ // it wants under that name.
1148
+ export function promptArgState({ entries = [], resources = {}, state = {} } = {}) {
1149
+ const merged = Object.assign({}, state)
1150
+ for (const entry of entries) {
1151
+ const uri = entry && entry.uri
1152
+ if (!uri || uri.indexOf(PAGE_URI_PREFIX) !== 0) continue
1153
+ const name = uri.slice(PAGE_URI_PREFIX.length)
1154
+ const reader = resources[uri]
1155
+ if (!name || merged[name] || typeof reader !== "function") continue
1156
+ merged[name] = { description: entry.description || "", read: reader }
1157
+ }
1158
+ return merged
1159
+ }
1160
+
1134
1161
  export function promptArgFromState(argName, state) {
1135
1162
  // Same object shape the system-prompt annex consumes: { description, read }.
1136
1163
  // The bare-function form was removed in 0.8.0, so an entry without `read` is
@@ -1,3 +1,3 @@
1
1
  module LlmMetaWidget
2
- VERSION = "0.9.0"
2
+ VERSION = "0.9.1"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm_meta_widget
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - jdkim