llm_meta_widget 0.6.1 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eea94229b42018fa2725d28578dd49970e9d0d3b2bc9bf095b9ac8fced1dcf80
4
- data.tar.gz: 1b3c95b8632aa46d0bb6e8b338fae6ae2166356912629294e68db480e9124abf
3
+ metadata.gz: 8c33302ad656efdecb39d20c27c5a9a04aa888b1ac297c4d6554ddd59c3d0a2a
4
+ data.tar.gz: 423222703ab4ca5511498829df6dd259b16d82a54f8377014e175707e256d916
5
5
  SHA512:
6
- metadata.gz: 7b6a4f6ac17d8601544fdb3ca94396b3482e66e13f3d5ba765c6d5839b608b17e692af5d7df16b51008a39ad527229193a8ce8d44345eb92feb98c31550f96b5
7
- data.tar.gz: d8785730a6294fbfe926353c07ed3737add9ea8577a81cfc55471d020cfef51c641fdc735d5297a6701ab9cefe4b5a295df74c75897c84bac0e5f2c30e92f6d9
6
+ metadata.gz: 3db09535d1fb922382bb85a267c69b941ac52accfe4e8c11297d73fb0a6642820adf6501b8508016a1a675436cd854562ffdea1d8b32bfcfcb1cd2c27b127640
7
+ data.tar.gz: 4499b65309c28d304439e267d7d4d1d5e2cf620a531f79b61011f1ef873ed7c9e6477a6b4e4337a9defb2330c1277862fbb8e0866d25e8bd936ff8559d890dc2
data/README.md CHANGED
@@ -79,6 +79,65 @@ Once that works, the widget can converse but cannot *do* anything. To let the
79
79
  LLM act on your page or call your own services, declare tools using any of the
80
80
  **three tool classes** below.
81
81
 
82
+ ## Any host: the custom element
83
+
84
+ The widget is a custom element in a single self-contained ES module, so a host
85
+ that is not Rails needs no gem, no template engine and no asset pipeline — two
86
+ lines of HTML:
87
+
88
+ ```html
89
+ <script type="module" src="https://cdn.jsdelivr.net/npm/@aibranch/llm-meta-widget@0.7"></script>
90
+ <llm-meta-widget llm-url="https://your-hub.example"
91
+ model="qwen3-8-27b-fast"
92
+ greeting="Hi — ask me anything about this page."></llm-meta-widget>
93
+ ```
94
+
95
+ Nothing else is needed: the stylesheets and the markdown renderer are bundled
96
+ in, and the element injects its own styles. The host serves no CSS and no JS.
97
+
98
+ Three ways to get that one file, in descending order of convenience:
99
+
100
+ - **the CDN**, as above — `@aibranch/llm-meta-widget` on npm, no path needed
101
+ because the package's `main` is the bundle;
102
+ - **the gem**, which serves the identical file at
103
+ `/llm_meta_widget_assets/llm-meta-widget.js` for Rails hosts, and is what the
104
+ `llm_meta_widget` helper points at;
105
+ - **self-hosted** — copy it out of the package or the gem and serve it as a
106
+ static asset, if you would rather not depend on a CDN.
107
+
108
+ The two Rails-specific pieces are gone from the host's side of the contract.
109
+ What is NOT gone is the host page's own contract, because those pieces belong to
110
+ the host: the `#ai-actions` JSON block, `window.aiState` and `window.aiActions`
111
+ are declared exactly as they are under Rails (see the three tool classes below).
112
+
113
+ **Attributes** map one-to-one onto the helper's keyword options. Three are not
114
+ obvious and are the ones that bite:
115
+
116
+ | attribute | notes |
117
+ |---|---|
118
+ | `llm-url`, `model` | required |
119
+ | `tool-hub-url` | omit for no Class 1; `""` is the same as omitting |
120
+ | `llm-provider` | `llm_meta_server` (default) or `ollama` |
121
+ | `api-key-uuid`, `greeting`, `max-rounds` | as the helper options |
122
+ | `actions-schema-id`, `state-global`, `actions-global`, `remote-tools-schema-id` | as the helper options |
123
+ | `models`, `hub-tools` | comma-separated. Omitted **or empty** means no allowlist — an allowlist permitting nothing is never what anyone meant |
124
+ | `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 |
125
+ | `well-known-urls` | **tri-state**, because an attribute cannot express nil-versus-empty: omitted = auto-discover same-origin `/.well-known/mcp.json`; `""` = discovery off; `"a,b"` = fetch those |
126
+
127
+ That last one is the quiet failure to watch for: expecting discovery off and
128
+ getting a same-origin fetch looks like nothing at all, except a 404 in the
129
+ console.
130
+
131
+ One widget per page. The panel uses fixed element ids, so a second
132
+ `<llm-meta-widget>` is ignored with a console warning rather than fighting the
133
+ first over every lookup.
134
+
135
+ **Building it.** `npm run build` bundles `element.js`, `config.js`,
136
+ `orchestrator.js`, the vendored `marked` and both stylesheets with esbuild. The
137
+ result is committed and shipped in the gem, because a gem cannot run a build
138
+ step on install — and `npm test` fails if the committed bundle has drifted from
139
+ its sources.
140
+
82
141
  ## Three tool classes
83
142
 
84
143
  The widget classifies every tool_call the LLM emits by name and dispatches to one of three execution paths. Each class has a different declaration, different visibility to the LLM, and different execution semantics.
@@ -90,7 +149,10 @@ Declared inline on the same view as the widget. Runs as JavaScript in the browse
90
149
  **Declaration** — three script blocks on the view:
91
150
 
92
151
  ```html
93
- <!-- (a) Tool schemas the LLM sees. JSON Schema (draft-2020-12 subset). -->
152
+ <!-- (a) Tool schemas the LLM sees. JSON Schema vocabulary — in practice
153
+ type, properties, required, description, items, enum. No $schema is
154
+ declared and nothing here validates one: the object is forwarded to
155
+ the provider as-is, and each provider accepts its own subset. -->
94
156
  <script type="application/json" id="ai-actions">
95
157
  [
96
158
  { "name": "add_dictionaries",
@@ -219,7 +281,7 @@ To lock the widget to the fixed `model:` prop and disable Class-1 tools entirely
219
281
  | `tool_hub_url:` | `nil` | An llm_meta_server whose registered MCP tools to offer. Absent = none (page actions and your own `.well-known` MCP are unaffected) |
220
282
  | `model:` | required | Initial model (also the fallback when picker is disabled) |
221
283
  | `api_key_uuid:` | `"ollama-local"` | Hub API-key uuid to invoke |
222
- | `orchestrator_path:` | `"/llm_meta_widget_assets/orchestrator.js"` | Served by the gem's engine; rarely overridden |
284
+ | `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 |
223
285
  | `actions_schema_id:` | `"ai-actions"` | DOM id of the Class-3 schema block |
224
286
  | `state_global:` | `"aiState"` | Global window object holding Class-3 state readers |
225
287
  | `actions_global:` | `"aiActions"` | Global window object holding Class-3 implementations |
@@ -0,0 +1,79 @@
1
+ // Attribute reading for <llm-meta-widget>.
2
+ //
3
+ // Separate from element.js so it can be unit-tested without a DOM and without
4
+ // the bundler: it takes anything with hasAttribute/getAttribute, so the tests
5
+ // pass a plain object. The panel's own logic has never had unit tests — this is
6
+ // the half of the custom-element move that can have them, so it does.
7
+ //
8
+ // Three cases are not obvious, and are the ones adopters get wrong:
9
+ //
10
+ // * The pickers default to TRUE, so they cannot be HTML boolean attributes
11
+ // (presence-means-true would be backwards — you would have to add an
12
+ // attribute to get the default). They are value attributes: disable with
13
+ // enable-tool-picker="false". Any other value, including "", is true.
14
+ // * well-known-urls is TRI-state, because the Ruby option distinguished nil
15
+ // (auto-discover same-origin /.well-known/mcp.json) from [] (discovery off)
16
+ // from an explicit list, and attributes cannot express nil versus empty:
17
+ // absent -> null (auto-discover)
18
+ // "" -> [] (off)
19
+ // "a,b" -> ["a","b"]
20
+ // * models / hub-tools treat "" as absent rather than as an empty allowlist,
21
+ // because an allowlist that permits nothing is never what anyone meant.
22
+ //
23
+ // Key names are the UPPER_CASE ones the panel logic already used, so that logic
24
+ // moved across untouched.
25
+
26
+ export const DEFAULTS = Object.freeze({
27
+ API_KEY_UUID: "ollama-local",
28
+ ACTIONS_SCHEMA_ID: "ai-actions",
29
+ STATE_GLOBAL: "aiState",
30
+ ACTIONS_GLOBAL: "aiActions",
31
+ REMOTE_TOOLS_SCHEMA_ID: "remote-mcp-tools",
32
+ LLM_PROVIDER: "llm_meta_server",
33
+ MAX_ROUNDS: 3,
34
+ });
35
+
36
+ const splitList = (v) => v.split(",").map((s) => s.trim()).filter(Boolean);
37
+
38
+ export function readConfig(el) {
39
+ const raw = (n) => (el && el.hasAttribute(n) ? el.getAttribute(n) : null);
40
+ const str = (n, d) => { const v = raw(n); return v === null ? d : v; };
41
+ const bool = (n, d) => { const v = raw(n); return v === null ? d : v !== "false"; };
42
+ const int = (n, d) => {
43
+ const v = raw(n);
44
+ if (v === null) return d;
45
+ const i = parseInt(v, 10);
46
+ return Number.isNaN(i) ? d : i;
47
+ };
48
+ const list = (n) => {
49
+ const v = raw(n);
50
+ return v === null || v.trim() === "" ? null : splitList(v);
51
+ };
52
+
53
+ const toolHub = str("tool-hub-url", null) || null; // "" collapses to null
54
+ const wellKnown = raw("well-known-urls");
55
+
56
+ return {
57
+ LLM_BASE: str("llm-url", null),
58
+ TOOL_HUB_BASE: toolHub,
59
+ API_KEY_UUID: str("api-key-uuid", DEFAULTS.API_KEY_UUID),
60
+ MODEL: str("model", null),
61
+ ACTIONS_SCHEMA_ID: str("actions-schema-id", DEFAULTS.ACTIONS_SCHEMA_ID),
62
+ GREETING: str("greeting", null),
63
+ STATE_GLOBAL: str("state-global", DEFAULTS.STATE_GLOBAL),
64
+ ACTIONS_GLOBAL: str("actions-global", DEFAULTS.ACTIONS_GLOBAL),
65
+ REMOTE_TOOLS_SCHEMA_ID: str("remote-tools-schema-id", DEFAULTS.REMOTE_TOOLS_SCHEMA_ID),
66
+ MAX_ROUNDS: int("max-rounds", DEFAULTS.MAX_ROUNDS),
67
+ WELL_KNOWN_URLS: wellKnown === null
68
+ ? null
69
+ : (wellKnown.trim() === "" ? [] : splitList(wellKnown)),
70
+ LLM_PROVIDER: str("llm-provider", DEFAULTS.LLM_PROVIDER),
71
+ ENABLE_MODEL_PICKER: bool("enable-model-picker", true),
72
+ // Class 1 tools are registered on a hub, so the picker needs one — which is
73
+ // independent of who answers the chat. Page actions and the host's own
74
+ // .well-known MCP work regardless.
75
+ ENABLE_TOOL_PICKER: bool("enable-tool-picker", true) && toolHub !== null,
76
+ MODEL_ALLOWLIST: list("models"),
77
+ HUB_TOOLS_ALLOWLIST: list("hub-tools"),
78
+ };
79
+ }