signalk-ai-bridge 0.1.0-beta.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.
- package/README.md +142 -0
- package/index.cjs +239 -0
- package/lib/ai-service.cjs +519 -0
- package/lib/bridge-service.cjs +246 -0
- package/package.json +48 -0
- package/public/assets/AppPanel-Bzd3495q.js +521 -0
- package/public/assets/hostInit-CQgwjAFf.js +4 -0
- package/public/assets/icons/icon-72x72.svg +6 -0
- package/public/assets/index-BHbVnIkY.js +580 -0
- package/public/assets/index-Czk6bIdV.js +16466 -0
- package/public/assets/index-zyKFBttD.js +259 -0
- package/public/assets/localSharedImportMap-sNvgyfj-.js +77 -0
- package/public/assets/preload-helper-CKlQz3_F.js +79 -0
- package/public/assets/signalk_ai_bridge__loadShare__react__loadShare__.js-B4V8rfyn.js +101 -0
- package/public/assets/signalk_ai_bridge__loadShare__react__loadShare__.js_commonjs-proxy-DUh5J9xL.js +34 -0
- package/public/assets/signalk_ai_bridge__loadShare__react_mf_2_dom__loadShare__.js-CQkl_yUA.js +73 -0
- package/public/assets/signalk_ai_bridge__loadShare__react_mf_2_dom__loadShare__.js_commonjs-proxy-CIQqSJv_.js +6 -0
- package/public/assets/virtualExposes-Daupo2Ou.js +60 -0
- package/public/esmRemoteEntry.js +4312 -0
- package/public/index.html +21 -0
- package/public/remoteEntry.js +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# signalk-ai-bridge
|
|
2
|
+
|
|
3
|
+
`signalk-ai-bridge` is a Signal K plugin that adds an `Ask AI` panel to the Signal K web UI.
|
|
4
|
+
|
|
5
|
+
It lets you send selected Signal K vessel data to a local Ollama model such as Gemma, then read the response directly in the browser.
|
|
6
|
+
|
|
7
|
+
## Experimental Plugin
|
|
8
|
+
|
|
9
|
+
This is an experimental study plugin.
|
|
10
|
+
|
|
11
|
+
It is intended for testing, evaluation, and local experimentation with AI-assisted vessel summaries inside Signal K. It should not be treated as a safety-critical navigation system, an authoritative decision-maker, or a production-hardened marine control feature.
|
|
12
|
+
|
|
13
|
+
## What It Is
|
|
14
|
+
|
|
15
|
+
This plugin is a bridge between:
|
|
16
|
+
|
|
17
|
+
- Signal K vessel data
|
|
18
|
+
- a local Ollama AI model
|
|
19
|
+
- a simple web UI inside Signal K
|
|
20
|
+
|
|
21
|
+
It is meant for local, operator-facing use. You choose which Signal K paths are shared with the AI, write a question in plain language, and the plugin sends that question plus the selected vessel context to Ollama.
|
|
22
|
+
|
|
23
|
+
## What It Does
|
|
24
|
+
|
|
25
|
+
With this plugin you can:
|
|
26
|
+
|
|
27
|
+
- ask for a vessel-state summary in plain language
|
|
28
|
+
- send selected Signal K paths to AI instead of the full data tree
|
|
29
|
+
- review the AI response in a readable panel
|
|
30
|
+
- see a history of previous AI requests
|
|
31
|
+
- inspect the actual request that was sent to the model
|
|
32
|
+
- check whether Ollama and the configured model are available
|
|
33
|
+
|
|
34
|
+
## What You Need
|
|
35
|
+
|
|
36
|
+
- a running Signal K server
|
|
37
|
+
- this plugin installed in Signal K
|
|
38
|
+
- a running Ollama server
|
|
39
|
+
- a locally available Ollama model, for example `gemma4:e2b`
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
1. Start Ollama.
|
|
44
|
+
2. Make sure the model you want to use is available.
|
|
45
|
+
3. Open the plugin configuration in Signal K.
|
|
46
|
+
4. Set the Ollama URL and model name.
|
|
47
|
+
5. Choose which Signal K paths should be sent to AI.
|
|
48
|
+
6. Open the plugin web UI and press `Ask AI`.
|
|
49
|
+
|
|
50
|
+
## Ollama With Docker Compose
|
|
51
|
+
|
|
52
|
+
If you do not already have Ollama running, you can use the included compose file:
|
|
53
|
+
|
|
54
|
+
[`docker-compose.gemma.yml`](https://github.com/KEGustafsson/signalk-ai-bridge/blob/main/docker-compose.gemma.yml)
|
|
55
|
+
|
|
56
|
+
Start it with:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
docker compose -f docker-compose.gemma.yml up -d
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This compose setup already pulls `gemma4:e2b` during startup, so you do not need to run a separate `ollama pull` command.
|
|
63
|
+
|
|
64
|
+
If Signal K runs on the host, the default Ollama URL `http://localhost:11434` is usually correct.
|
|
65
|
+
|
|
66
|
+
If Signal K runs in another container, use an address reachable from that container, for example `http://ollama:11434` on a shared Docker network.
|
|
67
|
+
|
|
68
|
+
## Normal Use
|
|
69
|
+
|
|
70
|
+
In the web UI you will see:
|
|
71
|
+
|
|
72
|
+
- `Signal K`: login state and vessel self ID
|
|
73
|
+
- `Ollama / Gemma`: backend URL, model, AI status, and timeout
|
|
74
|
+
- `AI Path Selection`: which Signal K paths are currently sent to AI
|
|
75
|
+
- `AI Response`: the latest answer from the model
|
|
76
|
+
- `Ask AI History`: previous prompts and results
|
|
77
|
+
|
|
78
|
+
If AI is unavailable, the web UI also shows a help link that opens the Ollama setup instructions.
|
|
79
|
+
|
|
80
|
+
## Important Plugin Settings
|
|
81
|
+
|
|
82
|
+
These are the settings most users will care about:
|
|
83
|
+
|
|
84
|
+
- `baseUrl`
|
|
85
|
+
Ollama server URL. Default: `http://localhost:11434`
|
|
86
|
+
|
|
87
|
+
- `model`
|
|
88
|
+
Ollama model name. Example: `gemma4:e2b`
|
|
89
|
+
|
|
90
|
+
- `aiDataPaths`
|
|
91
|
+
The Signal K self paths that will be sent to AI. You can use exact paths like `navigation.position` and simple wildcards like `navigation.*`
|
|
92
|
+
|
|
93
|
+
- `requestTimeoutMs`
|
|
94
|
+
How long the plugin waits for Ollama. Set `0` to disable the timeout
|
|
95
|
+
|
|
96
|
+
- `systemPrompt`
|
|
97
|
+
Extra instructions sent to the model before your question
|
|
98
|
+
|
|
99
|
+
- `temperature`
|
|
100
|
+
Lower values are more stable and literal. Higher values are more varied
|
|
101
|
+
|
|
102
|
+
- `topP`
|
|
103
|
+
Additional output randomness control
|
|
104
|
+
|
|
105
|
+
- `maxTokens`
|
|
106
|
+
The output/context budget forwarded to Ollama
|
|
107
|
+
|
|
108
|
+
## Notes About Model Names
|
|
109
|
+
|
|
110
|
+
The plugin defaults to the Gemma 4 family.
|
|
111
|
+
|
|
112
|
+
If you configure `gemma4` but Ollama only has a tagged variant installed, such as `gemma4:e2b`, the plugin will try to resolve and use the installed tagged model automatically.
|
|
113
|
+
|
|
114
|
+
If you already know the exact installed model name, configuring that exact name is the clearest option.
|
|
115
|
+
|
|
116
|
+
## Development
|
|
117
|
+
|
|
118
|
+
For local development:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
npm install
|
|
122
|
+
npm run dev
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Useful checks:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npm run test
|
|
129
|
+
npm run check
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
To remove generated build output:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npm run clean
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
To build the packaged web UI:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
npm run build
|
|
142
|
+
```
|
package/index.cjs
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
DEFAULT_AI_BASE_URL,
|
|
5
|
+
DEFAULT_AI_MODEL,
|
|
6
|
+
DEFAULT_SYSTEM_PROMPT,
|
|
7
|
+
getAiAvailability,
|
|
8
|
+
normalizeAiConfig,
|
|
9
|
+
queryAiModel,
|
|
10
|
+
readJsonBody
|
|
11
|
+
} = require('./lib/ai-service.cjs');
|
|
12
|
+
const { createBridgeService } = require('./lib/bridge-service.cjs');
|
|
13
|
+
|
|
14
|
+
module.exports = function createPlugin(app, dependencies = {}) {
|
|
15
|
+
let pluginOptions = {};
|
|
16
|
+
let routesRegistered = false;
|
|
17
|
+
const bridgeService = createBridgeService(app, dependencies);
|
|
18
|
+
|
|
19
|
+
function normalizeServerConfig(options = {}) {
|
|
20
|
+
const aiDataPaths = Array.isArray(options.aiDataPaths)
|
|
21
|
+
? options.aiDataPaths.map((item) => String(item || '').trim()).filter(Boolean)
|
|
22
|
+
: [];
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
aiDataPaths
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const getConfig = () => ({
|
|
30
|
+
...normalizeAiConfig(pluginOptions),
|
|
31
|
+
...normalizeServerConfig(pluginOptions)
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const schema = () => ({
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
enabled: {
|
|
38
|
+
type: 'boolean',
|
|
39
|
+
title: 'Enable AI pipeline',
|
|
40
|
+
default: true
|
|
41
|
+
},
|
|
42
|
+
baseUrl: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
title: 'Ollama host',
|
|
45
|
+
description:
|
|
46
|
+
'Ollama host URL. Leave blank to use AI_MODEL_URL or the default local Ollama server.',
|
|
47
|
+
default: DEFAULT_AI_BASE_URL
|
|
48
|
+
},
|
|
49
|
+
model: {
|
|
50
|
+
type: 'string',
|
|
51
|
+
title: 'AI model',
|
|
52
|
+
description: 'Ollama model name to send in chat requests.',
|
|
53
|
+
default: DEFAULT_AI_MODEL
|
|
54
|
+
},
|
|
55
|
+
systemPrompt: {
|
|
56
|
+
type: 'string',
|
|
57
|
+
title: 'System prompt',
|
|
58
|
+
description: 'Passed as a native Ollama system message before the operator request.',
|
|
59
|
+
default: DEFAULT_SYSTEM_PROMPT
|
|
60
|
+
},
|
|
61
|
+
requestTimeoutMs: {
|
|
62
|
+
type: 'integer',
|
|
63
|
+
title: 'Request timeout (ms)',
|
|
64
|
+
description: 'How long to wait for Ollama before failing. Set to 0 to disable the timeout.',
|
|
65
|
+
default: 120000,
|
|
66
|
+
minimum: 0,
|
|
67
|
+
maximum: 300000
|
|
68
|
+
},
|
|
69
|
+
temperature: {
|
|
70
|
+
type: 'number',
|
|
71
|
+
title: 'Temperature',
|
|
72
|
+
default: 0.2,
|
|
73
|
+
minimum: 0,
|
|
74
|
+
maximum: 2
|
|
75
|
+
},
|
|
76
|
+
topP: {
|
|
77
|
+
type: 'number',
|
|
78
|
+
title: 'Top-p',
|
|
79
|
+
default: 0.95,
|
|
80
|
+
minimum: 0,
|
|
81
|
+
maximum: 1
|
|
82
|
+
},
|
|
83
|
+
maxTokens: {
|
|
84
|
+
type: 'integer',
|
|
85
|
+
title: 'Max output tokens',
|
|
86
|
+
default: 131072,
|
|
87
|
+
minimum: 64,
|
|
88
|
+
maximum: 131072
|
|
89
|
+
},
|
|
90
|
+
aiDataPaths: {
|
|
91
|
+
type: 'array',
|
|
92
|
+
title: 'AI data paths',
|
|
93
|
+
description:
|
|
94
|
+
'Signal K self paths to collect and send to AI. Exact paths and simple wildcards ending in .* are supported. You can type your own paths, for example navigation.position, navigation.*, environment.wind.speedApparent, or notifications.',
|
|
95
|
+
uniqueItems: true,
|
|
96
|
+
default: [
|
|
97
|
+
'navigation.position',
|
|
98
|
+
'navigation.speedOverGround',
|
|
99
|
+
'navigation.courseOverGroundTrue',
|
|
100
|
+
'notifications'
|
|
101
|
+
],
|
|
102
|
+
items: {
|
|
103
|
+
type: 'string',
|
|
104
|
+
title: 'Signal K path'
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const statusHandler = async (req, res) => {
|
|
111
|
+
try {
|
|
112
|
+
const config = getConfig();
|
|
113
|
+
const availability = await getAiAvailability(config, dependencies);
|
|
114
|
+
res.status(200).json({
|
|
115
|
+
enabled: config.enabled,
|
|
116
|
+
baseUrl: config.baseUrl,
|
|
117
|
+
model: config.model,
|
|
118
|
+
requestTimeoutMs: config.requestTimeoutMs,
|
|
119
|
+
maxTokens: config.maxTokens,
|
|
120
|
+
aiDataPaths: config.aiDataPaths,
|
|
121
|
+
signalKSelfId: typeof app.selfId === 'string' ? app.selfId : undefined,
|
|
122
|
+
aiAvailable: availability.available,
|
|
123
|
+
ollamaReachable: availability.backendReachable,
|
|
124
|
+
modelAvailable: availability.modelAvailable,
|
|
125
|
+
resolvedModel: availability.resolvedModel,
|
|
126
|
+
availabilityMessage: availability.message
|
|
127
|
+
});
|
|
128
|
+
} catch (error) {
|
|
129
|
+
res.status(500).json({
|
|
130
|
+
error: {
|
|
131
|
+
code: 'unknown',
|
|
132
|
+
message: error instanceof Error ? error.message : 'Unknown AI status failure.'
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const queryHandler = async (req, res) => {
|
|
139
|
+
try {
|
|
140
|
+
const config = getConfig();
|
|
141
|
+
const body = await readJsonBody(req);
|
|
142
|
+
const payload = await bridgeService.buildAiPayload(body, config);
|
|
143
|
+
const result = await queryAiModel(payload, config, dependencies);
|
|
144
|
+
res.status(200).json(result);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
const statusCode =
|
|
147
|
+
typeof error === 'object' &&
|
|
148
|
+
error !== null &&
|
|
149
|
+
'statusCode' in error &&
|
|
150
|
+
typeof error.statusCode === 'number'
|
|
151
|
+
? error.statusCode
|
|
152
|
+
: error && error.code === 'unauthorized'
|
|
153
|
+
? 401
|
|
154
|
+
: error && error.code === 'validation-failed'
|
|
155
|
+
? 400
|
|
156
|
+
: error && error.code === 'disabled'
|
|
157
|
+
? 503
|
|
158
|
+
: error && error.code === 'timeout'
|
|
159
|
+
? 504
|
|
160
|
+
: 502;
|
|
161
|
+
|
|
162
|
+
res.status(statusCode).json({
|
|
163
|
+
error: {
|
|
164
|
+
code:
|
|
165
|
+
typeof error === 'object' && error !== null && 'code' in error && typeof error.code === 'string'
|
|
166
|
+
? error.code
|
|
167
|
+
: 'unknown',
|
|
168
|
+
message: error instanceof Error ? error.message : 'Unknown AI backend failure.'
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const bridgeExecuteHandler = async (req, res) => {
|
|
175
|
+
try {
|
|
176
|
+
const config = getConfig();
|
|
177
|
+
const body = await readJsonBody(req);
|
|
178
|
+
const result = await bridgeService.executeTool(body, config);
|
|
179
|
+
res.status(200).json(result);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
const statusCode =
|
|
182
|
+
typeof error === 'object' &&
|
|
183
|
+
error !== null &&
|
|
184
|
+
'statusCode' in error &&
|
|
185
|
+
typeof error.statusCode === 'number'
|
|
186
|
+
? error.statusCode
|
|
187
|
+
: error && error.code === 'unauthorized'
|
|
188
|
+
? 401
|
|
189
|
+
: error && error.code === 'validation-failed'
|
|
190
|
+
? 400
|
|
191
|
+
: 500;
|
|
192
|
+
|
|
193
|
+
res.status(statusCode).json({
|
|
194
|
+
error: {
|
|
195
|
+
code:
|
|
196
|
+
typeof error === 'object' && error !== null && 'code' in error && typeof error.code === 'string'
|
|
197
|
+
? error.code
|
|
198
|
+
: 'unknown',
|
|
199
|
+
message: error instanceof Error ? error.message : 'Unknown bridge failure.'
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
id: 'signalk-ai-bridge',
|
|
207
|
+
name: 'AI Bridge',
|
|
208
|
+
description: 'Signal K Ask AI plugin with embedded web UI for Ollama and Gemma.',
|
|
209
|
+
schema,
|
|
210
|
+
start: (options = {}) => {
|
|
211
|
+
pluginOptions = options;
|
|
212
|
+
bridgeService.reset();
|
|
213
|
+
const config = getConfig();
|
|
214
|
+
if (typeof app.setPluginStatus === 'function') {
|
|
215
|
+
app.setPluginStatus(
|
|
216
|
+
config.enabled
|
|
217
|
+
? `AI Bridge ready: ${config.model} via ${config.baseUrl}`
|
|
218
|
+
: 'AI Bridge webapp assets available. AI pipeline disabled.'
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
registerWithRouter: (router) => {
|
|
223
|
+
if (routesRegistered) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
router.get('/ai/status', statusHandler);
|
|
227
|
+
router.post('/ai/query', queryHandler);
|
|
228
|
+
router.post('/bridge/execute', bridgeExecuteHandler);
|
|
229
|
+
routesRegistered = true;
|
|
230
|
+
},
|
|
231
|
+
stop: () => {
|
|
232
|
+
if (typeof app.setPluginStatus === 'function') {
|
|
233
|
+
app.setPluginStatus('AI Bridge stopped.');
|
|
234
|
+
}
|
|
235
|
+
bridgeService.reset();
|
|
236
|
+
pluginOptions = {};
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
};
|