llm_meta_widget 0.7.4 → 0.7.6

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: 3c9ff9a76f254cfd3096189703db3546a3089661a02de5848c98130732933e87
4
- data.tar.gz: 5091614c56ea8d2e31e26d32286114505c30cc26edd5b9f4b0e66b9a779f5805
3
+ metadata.gz: 1590b5b56f4d164501425b88c2fddd28a07a19a229e6d3a6737a00a1f78b2a31
4
+ data.tar.gz: f8ba25f792c18736fd031757b18d8e2be7bdd85155f084e92f2cc683218a69c0
5
5
  SHA512:
6
- metadata.gz: 94afd5f3ac2861a419b283e48c0fb49483ea0c128e1b3a68b0ab23b6da0b29d852274d7b4bba906826ee1f06196a5a5aa09f5209ef192b85341735962fed0e09
7
- data.tar.gz: f0faabb00220362e70218e8685bb30bb0d201ea9d8963772e60cb95d5f4a7fac2e42691cf1b75e404b14ce34f4148204c46d30768a983ffa7db597b22ceb1de6
6
+ metadata.gz: 7cb9aad1047fca745e0c50c47e00c3c3504aaa34a104d2662ce1b2818abed81cddfdc7437a272b99629f4c61afba8b81a49deec8f97aff1f28a53ab29058410a
7
+ data.tar.gz: b5c75d48fc1e0583cf2e417e4f57f609742bf7342524f85a344c5ee89cb778db5b38f7703e15ebfc1d5f6a54d7b5bb2aab3c82765bbfadfc780104830712bd25
data/README.md CHANGED
@@ -4,7 +4,7 @@ Embeddable browser chat widget for the [llm_meta](https://github.com/pubannotati
4
4
 
5
5
  Client-orchestrated: the widget fetches host-side action schemas + host-published `.well-known/mcp.json` manifests at boot, dispatches tool_calls locally (page-embedded actions) or directly to MCP endpoints (host-wide well-known), and consumes the meta-server's SSE `single_llm_calls` API. Ships as a custom element in one self-contained ES module — on npm for any host, and in a Rails gem whose helper serves the identical file.
6
6
 
7
- **No** Devise, DB migrations, ChatManager, or PromptNavigator. Adds only `rails >= 8.0` as a runtime dep — so hosts that haven't bumped to 8.1 can adopt it without a Rails upgrade.
7
+ **No** Devise, DB migrations, ChatManager, or PromptNavigator. The gem adds only `rails >= 8.0` as a runtime dep — so hosts that haven't bumped to 8.1 can adopt it without a Rails upgrade — and hosts that are not Rails at all skip the gem entirely.
8
8
 
9
9
  ## What you need first
10
10
 
@@ -22,13 +22,26 @@ answers the chat, or use no hub at all. Page actions and your own
22
22
  Nothing else is required: no database, no migrations, no JavaScript build
23
23
  step, no Node at runtime.
24
24
 
25
- | Requirement | Version |
26
- |---|---|
27
- | Ruby | >= 3.2 |
28
- | Rails | >= 8.0 (8.1 not required) |
29
- | An llm_meta_server **or** an Ollama | any |
25
+ | Requirement | Version | Needed when |
26
+ |---|---|---|
27
+ | A browser with custom elements + ES modules | any current | always |
28
+ | An llm_meta_server **or** an Ollama | any | always — something has to answer the chat |
29
+ | Ruby | >= 3.2 | only if you install the gem |
30
+ | Rails | >= 8.0 (8.1 not required) | only if you install the gem |
31
+
32
+ ## Two ways to add the widget — choose one
33
+
34
+ You do **not** need both. Pick the row that matches your app, then read only
35
+ that section.
36
+
37
+ | Your app | What you add | What you write |
38
+ |---|---|---|
39
+ | **Rails** | the gem, one line in your `Gemfile` | one helper call in a view. The helper writes the `<llm-meta-widget>` tag for you, so you never write that tag yourself |
40
+ | **Anything else** — Python, Go, PHP, plain HTML… | one `<script>` tag from the CDN | the `<llm-meta-widget>` tag yourself |
41
+
42
+ Both ways load the same file and run the same widget.
30
43
 
31
- ## Installation
44
+ ## Rails apps: install the gem
32
45
 
33
46
  ```ruby
34
47
  # Gemfile
@@ -44,7 +57,27 @@ deliberately skips `isolate_namespace`, so the helper is included into
44
57
  ActionView and the asset routes appear at the host's top level on their own.
45
58
  There are no migrations and no generators to run.
46
59
 
47
- ## Minimal working example
60
+ **Why install the gem?** The gem gives you the same widget as the CDN — the
61
+ same file, byte for byte. It does not add features. What it gives you is a
62
+ simpler way to deliver and configure the widget:
63
+
64
+ - **Your own server sends the file**, at
65
+ `/llm_meta_widget_assets/llm-meta-widget.js`. The page makes no request to
66
+ any other site. If the CDN is down, your page still works, and the widget
67
+ also works behind a firewall or on a network with no internet access.
68
+ - **Bundler manages the version**, like every other gem. You update it with
69
+ `bundle update`. With the CDN, the version is part of a URL that someone
70
+ has to remember to change.
71
+ - **You write Ruby, not HTML attributes.** The helper takes normal Ruby
72
+ values and converts them for you. That matters most where the attributes
73
+ are easy to get wrong: `nil` and `[]` mean different things for
74
+ `well_known_urls:`, the allowlists take arrays, and the pickers take real
75
+ `true`/`false`. Written by hand, all three must be encoded correctly as
76
+ text.
77
+ - **The defaults are set for you**, so your view names only the options you
78
+ want to change.
79
+
80
+ ## Rails apps: a minimal working example
48
81
 
49
82
  Put this on any view — a fresh `pages/demo.html.erb` is fine:
50
83
 
@@ -79,7 +112,12 @@ Once that works, the widget can converse but cannot *do* anything. To let the
79
112
  LLM act on your page or call your own services, declare tools using any of the
80
113
  **three tool classes** below.
81
114
 
82
- ## Any host: the custom element
115
+ ## Non-Rails hosts: write the element yourself
116
+
117
+ **If your app is Rails, you can skip this section.** The gem already does
118
+ everything described here: its helper writes this tag for you and its engine
119
+ sends this file. Read on if your app is not Rails — or if you use Rails but
120
+ prefer the CDN to the gem, which the `element_path:` option allows.
83
121
 
84
122
  The widget is a custom element in a single self-contained ES module, so a host
85
123
  that is not Rails needs no gem, no template engine and no asset pipeline — two
@@ -87,11 +125,16 @@ lines of HTML:
87
125
 
88
126
  ```html
89
127
  <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"
128
+ <llm-meta-widget llm-url="https://your-meta-server.example"
129
+ model="qwen3-6-35b-fast"
92
130
  greeting="Hi — ask me anything about this page."></llm-meta-widget>
93
131
  ```
94
132
 
133
+ This is the same configuration as the Rails example above, written in HTML
134
+ instead of ERB. Both produce the same widget, so the advice in that section
135
+ — CORS, a reachable `llm_url`, the hub's name for the model — applies here
136
+ too.
137
+
95
138
  Nothing else is needed: the stylesheets and the markdown renderer are bundled
96
139
  in, and the element injects its own styles. The host serves no CSS and no JS.
97
140
 
@@ -105,10 +148,11 @@ Three ways to get that one file, in descending order of convenience:
105
148
  - **self-hosted** — copy it out of the package or the gem and serve it as a
106
149
  static asset, if you would rather not depend on a CDN.
107
150
 
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).
151
+ Going without the gem removes the Rails pieces — the helper and the partial.
152
+ It does not remove your page's own contract, because those parts belong to the
153
+ page rather than to Rails: the `#ai-actions` JSON block, `window.aiState` and
154
+ `window.aiActions` are declared exactly as they are under Rails (see the three
155
+ tool classes below).
112
156
 
113
157
  **Attributes** map one-to-one onto the helper's keyword options. Three are not
114
158
  obvious and are the ones that bite:
@@ -248,7 +292,7 @@ By default the widget renders two pickers below the input textarea:
248
292
  - **Model dropdown** — populated from the hub's `GET /api/llms` (anon path returns Ollama-only, since the widget's LLM calls use `api_key_uuid: "ollama-local"`).
249
293
  - **Tool picker** — populated from the hub's `GET /api/mcp_servers` (anon path returns `public_to_anonymous: true` servers). Two-level UX: server bulk-toggle + individual tool checkboxes on expand.
250
294
 
251
- Both pickers require **CORS**: the meta-server must allow the host's origin on its `/api/*` resource. That allowlist is an environment variable, not code — `CORS_ORIGINS`, a comma-separated list — so adding a host is a deployment change and a restart, with no commit and nothing about one deployment's hosts published in the repo. Without CORS the fetch is silently blocked and the pickers stay empty.
295
+ Both pickers require **CORS**, the same way the chat itself does: the hub must list your page's origin in its `CORS_ORIGINS` environment variable for the `/api/*` resource. Without that the fetches are blocked silently and both pickers simply stay empty.
252
296
 
253
297
  To adjust picker behavior at the helper call site:
254
298
 
@@ -274,6 +318,11 @@ To lock the widget to the fixed `model:` prop and disable Class-1 tools entirely
274
318
 
275
319
  ## All helper options
276
320
 
321
+ These are the gem helper's keyword options. If you write the element by hand,
322
+ each one has an attribute of the same name in kebab-case — `llm_url:` becomes
323
+ `llm-url`, and so on. The attribute table above lists the few that do not
324
+ translate directly.
325
+
277
326
  | Option | Default | Purpose |
278
327
  |---|---|---|
279
328
  | `llm_url:` | required | Who answers the chat — an llm_meta_server, or an Ollama |
@@ -299,7 +299,11 @@ function boot(cfg) {
299
299
  // baseline (if any), then the level-1 picker adds/removes entries as
300
300
  // the visitor toggles server checkboxes. Passed to runChatLoop on
301
301
  // every submit so the current selection is respected turn-by-turn.
302
- var remoteTools = remoteToolsEl ? JSON.parse(remoteToolsEl.textContent) : [];
302
+ // Keep the baseline separately: refreshRemoteToolsFromPicker() rebuilds
303
+ // remoteTools wholesale, so without a pristine copy the pre-configured
304
+ // tools vanish the moment the picker first renders.
305
+ var baselineRemoteTools = remoteToolsEl ? JSON.parse(remoteToolsEl.textContent) : [];
306
+ var remoteTools = baselineRemoteTools.slice();
303
307
 
304
308
  // Level-1 picker caches. hubMcpServers[] is the list of anon-visible
305
309
  // servers fetched from GET /api/mcp_servers; each server has an
@@ -310,6 +314,10 @@ function boot(cfg) {
310
314
  // state (all-on / indeterminate / all-off).
311
315
  var hubMcpServers = [];
312
316
  var selectedToolIds = new Set();
317
+ // A pre-configured tool IS a selection: seed the set so the picker renders
318
+ // it checked and the visitor can turn it off, rather than the picker
319
+ // silently overriding the page.
320
+ baselineRemoteTools.forEach(function(t) { selectedToolIds.add(t.id); });
313
321
 
314
322
  function anyAllowedByAllowlist(name, allowlist) {
315
323
  return allowlist === null || allowlist.indexOf(name) >= 0;
@@ -319,9 +327,11 @@ function boot(cfg) {
319
327
  // Flatten every SELECTED tool (across all servers) into remoteTools,
320
328
  // adapted to runChatLoop's expected { id, name, description, input_schema }.
321
329
  var flat = [];
330
+ var seen = {};
322
331
  hubMcpServers.forEach(function(s) {
323
332
  (s.tools || []).forEach(function(t) {
324
333
  if (!selectedToolIds.has(t.id)) return;
334
+ seen[t.id] = true;
325
335
  flat.push({
326
336
  id: t.id,
327
337
  name: t.name,
@@ -330,6 +340,12 @@ function boot(cfg) {
330
340
  });
331
341
  });
332
342
  });
343
+ // A baseline tool whose server the picker never listed (not anon-visible,
344
+ // or the fetch failed) has no checkbox to be found through — keep it
345
+ // unless the visitor explicitly turned it off.
346
+ baselineRemoteTools.forEach(function(t) {
347
+ if (!seen[t.id] && selectedToolIds.has(t.id)) { flat.push(t); seen[t.id] = true; }
348
+ });
333
349
  remoteTools = flat;
334
350
  if (toolsCountEl) {
335
351
  toolsCountEl.textContent = flat.length;
@@ -3765,17 +3765,23 @@ function boot(cfg) {
3765
3765
  var actionsSchemaEl = document.getElementById(ACTIONS_SCHEMA_ID);
3766
3766
  var localTools = actionsSchemaEl ? JSON.parse(actionsSchemaEl.textContent) : [];
3767
3767
  var remoteToolsEl = document.getElementById(REMOTE_TOOLS_SCHEMA_ID);
3768
- var remoteTools = remoteToolsEl ? JSON.parse(remoteToolsEl.textContent) : [];
3768
+ var baselineRemoteTools = remoteToolsEl ? JSON.parse(remoteToolsEl.textContent) : [];
3769
+ var remoteTools = baselineRemoteTools.slice();
3769
3770
  var hubMcpServers = [];
3770
3771
  var selectedToolIds = /* @__PURE__ */ new Set();
3772
+ baselineRemoteTools.forEach(function(t) {
3773
+ selectedToolIds.add(t.id);
3774
+ });
3771
3775
  function anyAllowedByAllowlist(name, allowlist) {
3772
3776
  return allowlist === null || allowlist.indexOf(name) >= 0;
3773
3777
  }
3774
3778
  function refreshRemoteToolsFromPicker() {
3775
3779
  var flat = [];
3780
+ var seen = {};
3776
3781
  hubMcpServers.forEach(function(s) {
3777
3782
  (s.tools || []).forEach(function(t) {
3778
3783
  if (!selectedToolIds.has(t.id)) return;
3784
+ seen[t.id] = true;
3779
3785
  flat.push({
3780
3786
  id: t.id,
3781
3787
  name: t.name,
@@ -3784,6 +3790,12 @@ function boot(cfg) {
3784
3790
  });
3785
3791
  });
3786
3792
  });
3793
+ baselineRemoteTools.forEach(function(t) {
3794
+ if (!seen[t.id] && selectedToolIds.has(t.id)) {
3795
+ flat.push(t);
3796
+ seen[t.id] = true;
3797
+ }
3798
+ });
3787
3799
  remoteTools = flat;
3788
3800
  if (toolsCountEl) {
3789
3801
  toolsCountEl.textContent = flat.length;
@@ -1,3 +1,3 @@
1
1
  module LlmMetaWidget
2
- VERSION = "0.7.4"
2
+ VERSION = "0.7.6"
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.4
4
+ version: 0.7.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - jdkim