llm_meta_widget 0.6.0 → 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 +4 -4
- data/README.md +64 -2
- data/app/assets/javascripts/llm_meta_widget/config.js +79 -0
- data/app/assets/javascripts/llm_meta_widget/element.js +1133 -0
- data/app/assets/javascripts/llm_meta_widget/llm-meta-widget.js +4429 -0
- data/app/assets/javascripts/llm_meta_widget/orchestrator.js +3 -3
- data/app/assets/stylesheets/llm_meta_widget/panel.css +363 -0
- data/app/controllers/llm_meta_widget/assets_controller.rb +11 -0
- data/app/helpers/llm_meta_widget/widget_helper.rb +34 -5
- data/app/views/llm_meta_widget/_chat_panel.html.erb +39 -1435
- data/config/routes.rb +8 -0
- data/lib/llm_meta_widget/version.rb +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8c33302ad656efdecb39d20c27c5a9a04aa888b1ac297c4d6554ddd59c3d0a2a
|
|
4
|
+
data.tar.gz: 423222703ab4ca5511498829df6dd259b16d82a54f8377014e175707e256d916
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
| `
|
|
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
|
+
}
|