llm_meta_widget 0.7.6 → 0.8.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: 1590b5b56f4d164501425b88c2fddd28a07a19a229e6d3a6737a00a1f78b2a31
4
- data.tar.gz: f8ba25f792c18736fd031757b18d8e2be7bdd85155f084e92f2cc683218a69c0
3
+ metadata.gz: 4f2938c7c0624169deea0320e4e0805c55555febd2eacbfb31be883dcb51b204
4
+ data.tar.gz: 4fc357cf41c9f420272babb65d9f85bf7eef3d7ea7f992de94f173ce0dba92df
5
5
  SHA512:
6
- metadata.gz: 7cb9aad1047fca745e0c50c47e00c3c3504aaa34a104d2662ce1b2818abed81cddfdc7437a272b99629f4c61afba8b81a49deec8f97aff1f28a53ab29058410a
7
- data.tar.gz: b5c75d48fc1e0583cf2e417e4f57f609742bf7342524f85a344c5ee89cb778db5b38f7703e15ebfc1d5f6a54d7b5bb2aab3c82765bbfadfc780104830712bd25
6
+ metadata.gz: c297b01c8588c9cdeafd6f58ba7b8f3c8898178c0110ed4d86403ee214e4fdb79023ed1622c037ba13ab4450d46e2bd1803391faf6f89187b208e67633ec73c7
7
+ data.tar.gz: da4ece5000dde3ce73b709a1648a01844d7c2b60ceef35c723e950ed2395dc85d018f24f2193f2f21a47eea5eeaf49c4b6c0f14c93d4717cfcdabf27896bd45c
data/CHANGELOG.md ADDED
@@ -0,0 +1,44 @@
1
+ # Changelog
2
+
3
+ This file starts at 0.8.0, the first release with a breaking change worth
4
+ calling out. Earlier releases are in the git history (`git log --oneline v0.7.6`).
5
+
6
+ ## 0.8.0
7
+
8
+ ### Breaking: state readers must declare a description
9
+
10
+ `window.aiState` entries are now objects, not bare functions:
11
+
12
+ ```js
13
+ window.aiState = {
14
+ text: {
15
+ description: "the text the user wants to annotate",
16
+ read: function() { return $("#text").val() || ""; }
17
+ }
18
+ };
19
+ ```
20
+
21
+ The bare-function form (`text: function() {…}`) is **removed**, with no fallback.
22
+ A key still using it is reported to the console by name, with the shape it should
23
+ have, and skipped — the page's other readers keep working.
24
+
25
+ **Why.** Every other channel the widget declares to the model already carries a
26
+ developer-authored description: Class 3 action schemas, Class 2 `.well-known`
27
+ tools, Class 1 hub-registered tools, static-primitives resources. State readers
28
+ were the exception — the model saw a bare key and inferred meaning from its name.
29
+ That holds for `text` and fails for `annotation_mode` or `pending_merges`.
30
+
31
+ **What to change.** Wrap each reader in `{ description, read }`. The description
32
+ is the place to tell the model what it must know about the value, not merely what
33
+ the value is — an enumeration it must choose from, a unit, a null convention.
34
+
35
+ The system prompt now renders one line per reader with the description inline:
36
+
37
+ ```
38
+ Current page state:
39
+ - text (the text the user wants to annotate): "The stomach was examined."
40
+ - selected_dictionaries (the dictionaries the user has chosen): ["uberon"]
41
+ ```
42
+
43
+ Prompt-argument pre-filling (`promptArgFromState`) reads the same shape, so a
44
+ page left on the old form also stops pre-filling prompt arguments.
data/README.md CHANGED
@@ -124,7 +124,7 @@ that is not Rails needs no gem, no template engine and no asset pipeline — two
124
124
  lines of HTML:
125
125
 
126
126
  ```html
127
- <script type="module" src="https://cdn.jsdelivr.net/npm/@aibranch/llm-meta-widget@0.7"></script>
127
+ <script type="module" src="https://cdn.jsdelivr.net/npm/@aibranch/llm-meta-widget@0.8"></script>
128
128
  <llm-meta-widget llm-url="https://your-meta-server.example"
129
129
  model="qwen3-6-35b-fast"
130
130
  greeting="Hi — ask me anything about this page."></llm-meta-widget>
@@ -135,6 +135,16 @@ instead of ERB. Both produce the same widget, so the advice in that section
135
135
  — CORS, a reachable `llm_url`, the hub's name for the model — applies here
136
136
  too.
137
137
 
138
+ **`@0.8` is a range, and it stops at 0.8.x on purpose.** You get patches without
139
+ touching your page, and you do not get a breaking release by surprise. The cost
140
+ is that moving to 0.9 is a deliberate edit: read [CHANGELOG.md](CHANGELOG.md)
141
+ first, because a major-or-minor bump here is where the page's own contract can
142
+ change — 0.8.0 changed the shape of `window.aiState`, and a page that followed
143
+ the new documentation while still loading `@0.7` would have had every state
144
+ value silently replaced by an error string. Rails hosts are protected from that
145
+ mismatch by the Gemfile constraint; a CDN embed has no such guard, so the
146
+ version in that URL is the guard.
147
+
138
148
  Nothing else is needed: the stylesheets and the markdown renderer are bundled
139
149
  in, and the element injects its own styles. The host serves no CSS and no JS.
140
150
 
@@ -211,10 +221,17 @@ Declared inline on the same view as the widget. Runs as JavaScript in the browse
211
221
 
212
222
  <!-- (b) State readers folded into the LLM's system prompt EVERY turn, -->
213
223
  <!-- so the LLM can answer from page state without a tool_call. -->
224
+ <!-- Each reader declares what it returns, like every other channel. -->
214
225
  <script>
215
226
  window.aiState = {
216
- text: function() { return $("#text").val() || ""; },
217
- selected_dictionaries: function() { return getSelected(); }
227
+ text: {
228
+ description: "the text the user wants to annotate",
229
+ read: function() { return $("#text").val() || ""; }
230
+ },
231
+ selected_dictionaries: {
232
+ description: "the dictionaries the user has chosen for annotation",
233
+ read: function() { return getSelected(); }
234
+ }
218
235
  };
219
236
  </script>
220
237
 
@@ -226,7 +243,17 @@ Declared inline on the same view as the widget. Runs as JavaScript in the browse
226
243
  </script>
227
244
  ```
228
245
 
229
- **What the LLM sees.** The `ai-actions` JSON is passed to the meta-server as `local_tools` for every turn. Alongside it, every reader in `window.aiState` is invoked (each turn) and the results are JSON-serialized into a `Current page state:` block appended to the system prompt — the LLM can answer from state directly instead of tool-calling for lookups.
246
+ **What the LLM sees.** The `ai-actions` JSON is passed to the meta-server as `local_tools` for every turn. Alongside it, every reader in `window.aiState` is invoked (each turn) and rendered into a `Current page state:` block appended to the system prompt, one line per reader, with its description beside its value:
247
+
248
+ ```
249
+ Current page state:
250
+ - text (the text the user wants to annotate): "The stomach was examined."
251
+ - selected_dictionaries (the dictionaries the user has chosen for annotation): ["uberon"]
252
+ ```
253
+
254
+ So the LLM can answer from state directly instead of tool-calling for lookups, and knows what each value *means* rather than guessing from the key.
255
+
256
+ **Breaking change in 0.8.0.** A reader must be `{ description, read }`. The bare-function form — `text: function() {…}` — is removed, with no fallback. A key that still uses it is reported to the console by name, with the shape it should have, and skipped; the page's other readers keep working. Descriptions are not decoration: every other channel the widget declares to the model already carries one (Class 3 tool schemas, Class 2 `.well-known` tools, Class 1 hub tools, resources), and state readers were the exception.
230
257
 
231
258
  **When the tool_call fires.** During the turn, as soon as the LLM emits the
232
259
  call. Since 0.4.0 the action's outcome — `{ok: true, applied: "<name>"}`, or
@@ -279,7 +306,7 @@ Declared out-of-band on the meta-server (via the hub's admin UI or `/user/:id/mc
279
306
 
280
307
  **Declaration.** No widget-side change. Register the server on the hub → flip `public: true` (visible to signed-in users) and optionally `public_to_anonymous: true` (visible to widget visitors without a login). See the meta-server's `Api::McpServersController`.
281
308
 
282
- **What the LLM sees.** Only tools from servers the visitor has **enabled via the tool picker** (see "Level-1 pickers" below). Nothing is auto-selected — the visitor opts in per session.
309
+ **What the LLM sees.** Only tools the visitor has **enabled via the tool picker** (see "Level-1 pickers" below) — nothing is auto-selected, the visitor opts in per session. A page can set the starting selection instead, with `remote_tools_schema_id:`: those tools arrive already ticked, and the visitor can still untick them.
283
310
 
284
311
  **When the tool_call fires.** Synchronously during the turn — widget POSTs to the hub's `/api/llm_api_keys/:uuid/models/:name/single_llm_calls` endpoint with `tool_ids: [...]`; the hub proxies to each MCP server and streams results back through SSE.
285
312
 
@@ -332,9 +359,9 @@ translate directly.
332
359
  | `api_key_uuid:` | `"ollama-local"` | Hub API-key uuid to invoke |
333
360
  | `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 |
334
361
  | `actions_schema_id:` | `"ai-actions"` | DOM id of the Class-3 schema block |
335
- | `state_global:` | `"aiState"` | Global window object holding Class-3 state readers |
362
+ | `state_global:` | `"aiState"` | Global window object holding Class-3 state readers, each `{ description, read }` (see the breaking change in 0.8.0) |
336
363
  | `actions_global:` | `"aiActions"` | Global window object holding Class-3 implementations |
337
- | `remote_tools_schema_id:` | `"remote-mcp-tools"` | Optional DOM id for pre-configured Class-1 tools (bypasses picker) |
364
+ | `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 |
338
365
  | `well_known_urls:` | `nil` | `nil` = auto-discover same-origin; explicit array = fetch those; `[]` = disable |
339
366
  | `greeting:` | `nil` | First thing a visitor sees when the panel opens, above the offered prompt templates. `nil` = a generic line |
340
367
  | `max_rounds:` | `3` | Cap on tool-call rounds per LLM turn. Page actions cost a round each since 0.4.0 — raise it for multi-step flows |
@@ -457,6 +484,10 @@ it most, since they must already know your form to press it.
457
484
  behind the custom-element move: the npm package and its CDN URL, why `main`,
458
485
  `exports` and `sideEffects` are set the way they are, and why the gem and the npm
459
486
  package must never drift in version.
487
+ - **A page you can run** — `examples/connection-test.html` checks every call the
488
+ widget makes to a hub, one at a time, then mounts the widget with a tool
489
+ already selected. Serve it from an origin the hub allows and open it: if
490
+ something in the chain is wrong, it names which step.
460
491
  - **Issues and questions** — <https://github.com/jdkim/llm_meta_widget/issues>
461
492
 
462
493
  ## License
@@ -954,15 +954,69 @@ function boot(cfg) {
954
954
  currentThinkingBody = null;
955
955
  }
956
956
 
957
- function currentPageState() {
958
- var reader = window[STATE_GLOBAL] || {};
959
- var out = {};
960
- Object.keys(reader).forEach(function(k) {
961
- try { out[k] = reader[k](); } catch (e) { out[k] = "<error: " + e.message + ">"; }
957
+ // Every declaration channel the widget hands the model carries a
958
+ // developer-authored description — Class 3 action schemas, Class 2
959
+ // .well-known tools, Class 1 hub tools, static-primitives resources. State
960
+ // readers used to be the exception: the model saw a bare key and had to
961
+ // infer meaning from its name, which holds for `text` and falls apart for
962
+ // `annotation_mode` or `pending_merges`. Each reader now declares what it
963
+ // returns, in the same shape as everything else.
964
+ //
965
+ // The bare-function form is GONE as of 0.8.0 — no fallback branch. A page
966
+ // that still uses it is told exactly what to change, per key, and that key
967
+ // is skipped; the rest of the page still works.
968
+ function stateReaders() {
969
+ var declared = window[STATE_GLOBAL] || {};
970
+ var out = [];
971
+ Object.keys(declared).forEach(function(key) {
972
+ var entry = declared[key];
973
+ var hasDescription = entry && typeof entry.description === "string" && entry.description.trim() !== "";
974
+ var hasRead = entry && typeof entry.read === "function";
975
+ if (!hasDescription || !hasRead) {
976
+ console.error(
977
+ "[llm-meta-widget] " + STATE_GLOBAL + "." + key + " is not a valid state reader. " +
978
+ "Expected { description: \"what this value is\", read: function () { … } }, got " +
979
+ (typeof entry === "function" ? "a bare function (the 0.7 form, removed in 0.8)" : describeValue(entry)) +
980
+ ". Skipping " + key + "."
981
+ );
982
+ return;
983
+ }
984
+ out.push({ key: key, description: entry.description, read: entry.read });
962
985
  });
963
986
  return out;
964
987
  }
965
988
 
989
+ function describeValue(v) {
990
+ if (v === null) return "null";
991
+ if (Array.isArray(v)) return "an array";
992
+ if (typeof v === "object") {
993
+ var missing = [];
994
+ if (typeof v.description !== "string" || v.description.trim() === "") missing.push("description");
995
+ if (typeof v.read !== "function") missing.push("read");
996
+ return "an object missing " + missing.join(" and ");
997
+ }
998
+ return typeof v;
999
+ }
1000
+
1001
+ function currentPageState() {
1002
+ return stateReaders().map(function(r) {
1003
+ var value;
1004
+ try { value = r.read(); } catch (e) { value = "<error: " + e.message + ">"; }
1005
+ return { key: r.key, description: r.description, value: value };
1006
+ });
1007
+ }
1008
+
1009
+ // One line per reader, with its description inline. A separate glossary
1010
+ // above the values would make the model match names across two lists; the
1011
+ // point is that it reads the meaning and the value together.
1012
+ function pageStateLines() {
1013
+ var readers = currentPageState();
1014
+ if (readers.length === 0) return [ "(this page declares no state)" ];
1015
+ return readers.map(function(r) {
1016
+ return "- " + r.key + " (" + r.description + "): " + JSON.stringify(r.value);
1017
+ });
1018
+ }
1019
+
966
1020
  function currentSystemPrompt(resourceLines) {
967
1021
  return [
968
1022
  "You are integrated into a web page as an AI assistant. You have tools available to change page state or fetch information.",
@@ -973,9 +1027,8 @@ function boot(cfg) {
973
1027
  "3. After a tool returns a result, use it: either take the next step the task needs, or — if the task is done — answer in plain text. Do not stop silently after a tool call.",
974
1028
  "4. NEVER repeat a call you have already made with the same or similar arguments — its result is already in the conversation history.",
975
1029
  "",
976
- "Current page state:",
977
- JSON.stringify(currentPageState(), null, 2)
978
- ].concat(resourceLines || []).join("\n");
1030
+ "Current page state:"
1031
+ ].concat(pageStateLines()).concat(resourceLines || []).join("\n");
979
1032
  }
980
1033
 
981
1034
  // Runs once per send, before the system prompt is built: a volatile
@@ -642,7 +642,8 @@ async function resourceLinesForTurn({ plan, cached, endpoint, read, budgetBytes
642
642
  }
643
643
  var PROMPT_ARG_ALIASES = { dictionaries: "selected_dictionaries" };
644
644
  function promptArgFromState(argName, state) {
645
- const reader = state && state[argName] || state && state[PROMPT_ARG_ALIASES[argName]];
645
+ const entry = state && state[argName] || state && state[PROMPT_ARG_ALIASES[argName]];
646
+ const reader = entry && typeof entry.read === "function" ? entry.read : null;
646
647
  if (typeof reader !== "function") return "";
647
648
  let value;
648
649
  try {
@@ -4266,17 +4267,51 @@ function boot(cfg) {
4266
4267
  currentThinkingBlock = null;
4267
4268
  currentThinkingBody = null;
4268
4269
  }
4270
+ function stateReaders() {
4271
+ var declared = window[STATE_GLOBAL] || {};
4272
+ var out = [];
4273
+ Object.keys(declared).forEach(function(key) {
4274
+ var entry = declared[key];
4275
+ var hasDescription = entry && typeof entry.description === "string" && entry.description.trim() !== "";
4276
+ var hasRead = entry && typeof entry.read === "function";
4277
+ if (!hasDescription || !hasRead) {
4278
+ console.error(
4279
+ "[llm-meta-widget] " + STATE_GLOBAL + "." + key + ' is not a valid state reader. Expected { description: "what this value is", read: function () { \u2026 } }, got ' + (typeof entry === "function" ? "a bare function (the 0.7 form, removed in 0.8)" : describeValue(entry)) + ". Skipping " + key + "."
4280
+ );
4281
+ return;
4282
+ }
4283
+ out.push({ key, description: entry.description, read: entry.read });
4284
+ });
4285
+ return out;
4286
+ }
4287
+ function describeValue(v) {
4288
+ if (v === null) return "null";
4289
+ if (Array.isArray(v)) return "an array";
4290
+ if (typeof v === "object") {
4291
+ var missing = [];
4292
+ if (typeof v.description !== "string" || v.description.trim() === "") missing.push("description");
4293
+ if (typeof v.read !== "function") missing.push("read");
4294
+ return "an object missing " + missing.join(" and ");
4295
+ }
4296
+ return typeof v;
4297
+ }
4269
4298
  function currentPageState() {
4270
- var reader = window[STATE_GLOBAL] || {};
4271
- var out = {};
4272
- Object.keys(reader).forEach(function(k) {
4299
+ return stateReaders().map(function(r) {
4300
+ var value;
4273
4301
  try {
4274
- out[k] = reader[k]();
4302
+ value = r.read();
4275
4303
  } catch (e) {
4276
- out[k] = "<error: " + e.message + ">";
4304
+ value = "<error: " + e.message + ">";
4277
4305
  }
4306
+ return { key: r.key, description: r.description, value };
4307
+ });
4308
+ }
4309
+ function pageStateLines() {
4310
+ var readers = currentPageState();
4311
+ if (readers.length === 0) return ["(this page declares no state)"];
4312
+ return readers.map(function(r) {
4313
+ return "- " + r.key + " (" + r.description + "): " + JSON.stringify(r.value);
4278
4314
  });
4279
- return out;
4280
4315
  }
4281
4316
  function currentSystemPrompt(resourceLines) {
4282
4317
  return [
@@ -4288,9 +4323,8 @@ function boot(cfg) {
4288
4323
  "3. After a tool returns a result, use it: either take the next step the task needs, or \u2014 if the task is done \u2014 answer in plain text. Do not stop silently after a tool call.",
4289
4324
  "4. NEVER repeat a call you have already made with the same or similar arguments \u2014 its result is already in the conversation history.",
4290
4325
  "",
4291
- "Current page state:",
4292
- JSON.stringify(currentPageState(), null, 2)
4293
- ].concat(resourceLines || []).join("\n");
4326
+ "Current page state:"
4327
+ ].concat(pageStateLines()).concat(resourceLines || []).join("\n");
4294
4328
  }
4295
4329
  async function resourceLinesForThisTurn() {
4296
4330
  var turn = await resourceLinesForTurn({
@@ -971,7 +971,12 @@ export async function resourceLinesForTurn({ plan, cached, endpoint, read, budge
971
971
  const PROMPT_ARG_ALIASES = { dictionaries: "selected_dictionaries" }
972
972
 
973
973
  export function promptArgFromState(argName, state) {
974
- const reader = (state && state[argName]) || (state && state[PROMPT_ARG_ALIASES[argName]])
974
+ // Same object shape the system-prompt annex consumes: { description, read }.
975
+ // The bare-function form was removed in 0.8.0, so an entry without `read` is
976
+ // simply not a reader — the prompt argument stays empty and the visitor fills
977
+ // it, which is the documented behaviour for state the page cannot supply.
978
+ const entry = (state && state[argName]) || (state && state[PROMPT_ARG_ALIASES[argName]])
979
+ const reader = entry && typeof entry.read === "function" ? entry.read : null
975
980
  if (typeof reader !== "function") return ""
976
981
  let value
977
982
  try {
@@ -1,3 +1,3 @@
1
1
  module LlmMetaWidget
2
- VERSION = "0.7.6"
2
+ VERSION = "0.8.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.7.6
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - jdkim
@@ -35,6 +35,7 @@ executables: []
35
35
  extensions: []
36
36
  extra_rdoc_files: []
37
37
  files:
38
+ - CHANGELOG.md
38
39
  - LICENSE
39
40
  - README.md
40
41
  - Rakefile