@oceanum/oceanumlab 4.6.1 → 4.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.
- package/lib/DatameshUI.d.ts +10 -0
- package/lib/DatameshUI.js +20 -2
- package/lib/DatameshWidget.d.ts +11 -1
- package/lib/DatameshWidget.js +116 -43
- package/lib/aiBackend.d.ts +66 -0
- package/lib/aiBackend.js +101 -0
- package/lib/chatRouter.d.ts +5 -1
- package/lib/chatRouter.js +27 -9
- package/lib/constants.d.ts +1 -1
- package/lib/constants.js +1 -1
- package/lib/datameshUiUrl.d.ts +15 -0
- package/lib/datameshUiUrl.js +35 -0
- package/lib/httpUrl.d.ts +9 -0
- package/lib/httpUrl.js +20 -0
- package/lib/index.js +28 -7
- package/package.json +1 -1
- package/schema/datamesh-connect.json +12 -0
- package/src/DatameshUI.tsx +21 -2
- package/src/DatameshWidget.tsx +200 -54
- package/src/__tests__/DatameshWidget.spec.tsx +380 -0
- package/src/__tests__/aiBackend.spec.ts +272 -0
- package/src/__tests__/chatRouter.spec.ts +259 -17
- package/src/__tests__/datameshUiUrl.spec.ts +154 -0
- package/src/aiBackend.ts +150 -0
- package/src/chatRouter.ts +36 -8
- package/src/constants.ts +1 -1
- package/src/datameshUiUrl.ts +44 -0
- package/src/httpUrl.ts +23 -0
- package/src/index.ts +38 -10
package/src/chatRouter.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
aiBackendUrl,
|
|
4
|
+
canSignIn,
|
|
5
|
+
resolveAiCredential,
|
|
6
|
+
signInToken,
|
|
7
|
+
type AuthCommands
|
|
8
|
+
} from './aiBackend';
|
|
3
9
|
import type { Progress } from './progress';
|
|
4
10
|
import { readSse } from './sse';
|
|
5
11
|
import type { ObservedRun } from './notebookRun';
|
|
@@ -76,6 +82,8 @@ function asOceanumResponse(data: unknown): OceanumResponse {
|
|
|
76
82
|
|
|
77
83
|
export class ChatRouter {
|
|
78
84
|
/**
|
|
85
|
+
* @param _commands Where the host's Oceanum.io sign-in is asked for, when
|
|
86
|
+
* there is no pasted Datamesh token.
|
|
79
87
|
* @param _notebook What the conversation's notebook holds -- the one its
|
|
80
88
|
* answers are placed in -- or null if there is nothing to send. Asked on
|
|
81
89
|
* every request, so it follows the conversation rather than whichever tab
|
|
@@ -83,6 +91,7 @@ export class ChatRouter {
|
|
|
83
91
|
*/
|
|
84
92
|
constructor(
|
|
85
93
|
private _settings: ISettingRegistry.ISettings,
|
|
94
|
+
private _commands: AuthCommands,
|
|
86
95
|
private _notebook: () => Promise<INotebookSnapshot | null>
|
|
87
96
|
) {}
|
|
88
97
|
|
|
@@ -178,20 +187,30 @@ export class ChatRouter {
|
|
|
178
187
|
signal?: AbortSignal,
|
|
179
188
|
onProgress?: (progress: Progress) => void
|
|
180
189
|
): Promise<unknown> {
|
|
181
|
-
|
|
182
|
-
|
|
190
|
+
// Resolved per request, not once: the sign-in token refreshes, and the
|
|
191
|
+
// user can sign in, sign out or paste a token between two requests.
|
|
192
|
+
const credential = await resolveAiCredential(
|
|
193
|
+
this._settings.get('datameshToken').composite,
|
|
194
|
+
signInToken(this._commands)
|
|
195
|
+
);
|
|
196
|
+
if (!credential) {
|
|
183
197
|
throw new ChatRouterError(
|
|
184
|
-
|
|
198
|
+
canSignIn(this._commands)
|
|
199
|
+
? 'Not signed in. Sign in to Oceanum.io to use Oceanum AI, or set a Datamesh token in Settings → Oceanum.io.'
|
|
200
|
+
: 'Datamesh token not configured. Set your token in Settings → Oceanum.io.'
|
|
185
201
|
);
|
|
186
202
|
}
|
|
203
|
+
const backend = aiBackendUrl(
|
|
204
|
+
this._settings.get('aiBackendUrl').composite as string
|
|
205
|
+
);
|
|
187
206
|
|
|
188
207
|
let response: Response;
|
|
189
208
|
try {
|
|
190
|
-
response = await fetch(`${
|
|
209
|
+
response = await fetch(`${backend}${path}`, {
|
|
191
210
|
method: 'POST',
|
|
192
211
|
headers: {
|
|
193
212
|
'Content-Type': 'application/json',
|
|
194
|
-
|
|
213
|
+
...credential.headers,
|
|
195
214
|
// Ask for the streamed form only when someone is listening. The
|
|
196
215
|
// server requires the media type to be NAMED -- a wildcard never
|
|
197
216
|
// selects it -- so omitting this is what keeps the plain JSON path
|
|
@@ -208,12 +227,21 @@ export class ChatRouter {
|
|
|
208
227
|
throw err;
|
|
209
228
|
}
|
|
210
229
|
throw new ChatRouterError(
|
|
211
|
-
`Could not reach Oceanum AI backend at ${
|
|
230
|
+
`Could not reach Oceanum AI backend at ${backend}. Is it running?`
|
|
212
231
|
);
|
|
213
232
|
}
|
|
214
233
|
|
|
215
234
|
if (response.status === 401) {
|
|
216
|
-
|
|
235
|
+
let message = 'Invalid or expired Datamesh token.';
|
|
236
|
+
if (credential.source === 'sign-in') {
|
|
237
|
+
message =
|
|
238
|
+
'Your Oceanum.io sign-in was not accepted. Sign in to Oceanum.io again.';
|
|
239
|
+
} else if (canSignIn(this._commands)) {
|
|
240
|
+
// A pasted token wins over the sign-in, so say how to fall back to it.
|
|
241
|
+
message +=
|
|
242
|
+
' Update it, or clear it to use your Oceanum.io sign-in, in Settings → Oceanum.io.';
|
|
243
|
+
}
|
|
244
|
+
throw new ChatRouterError(message, 401);
|
|
217
245
|
}
|
|
218
246
|
|
|
219
247
|
if (!response.ok) {
|
package/src/constants.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* These values are compiled into the extension.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
/** URL of the Oceanum AI backend service */
|
|
6
|
+
/** Default URL of the Oceanum AI backend service (the `aiBackendUrl` setting) */
|
|
7
7
|
export const OCEANUM_AI_BACKEND_URL = 'https://ai.oceanum.io';
|
|
8
8
|
|
|
9
9
|
/** URL of the Datamesh UI embedded in the Datamesh panel */
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Datamesh UI panel's address, and which messages may come from it.
|
|
3
|
+
*
|
|
4
|
+
* The address is a setting (`datameshUiUrl`), so a deployment can point the
|
|
5
|
+
* panel at another Datamesh UI -- a dev one, say. A setting is typed in by
|
|
6
|
+
* hand, so anything that is not an http(s) URL falls back to the default
|
|
7
|
+
* rather than ending up in an iframe's `src`.
|
|
8
|
+
*/
|
|
9
|
+
import { DATAMESH_UI_SERVICE } from './constants';
|
|
10
|
+
import { validHttpUrl } from './httpUrl';
|
|
11
|
+
|
|
12
|
+
/** The Datamesh UI address to use for a `datameshUiUrl` setting value. */
|
|
13
|
+
export function validDatameshUiUrl(url: string | null | undefined): URL {
|
|
14
|
+
return validHttpUrl(url, DATAMESH_UI_SERVICE);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The panel's iframe `src`: the Datamesh UI in embed mode, which hides the
|
|
19
|
+
* Oceanum.io navigator. Any query the setting already has is kept.
|
|
20
|
+
*/
|
|
21
|
+
export function datameshUiSrc(url: string | null | undefined): string {
|
|
22
|
+
const src = validDatameshUiUrl(url);
|
|
23
|
+
src.searchParams.set('embed', '1');
|
|
24
|
+
return src.toString();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Whether a `message` event was posted by the Datamesh UI panel.
|
|
29
|
+
*
|
|
30
|
+
* Any page or frame can post to this window, so a message counts only when it
|
|
31
|
+
* comes from the panel's iframe (`frame`, its `contentWindow`; `null` when the
|
|
32
|
+
* panel is not open) and from the configured Datamesh UI's origin.
|
|
33
|
+
*/
|
|
34
|
+
export function isDatameshUiMessage(
|
|
35
|
+
event: Pick<MessageEvent, 'origin' | 'source'>,
|
|
36
|
+
url: string | null | undefined,
|
|
37
|
+
frame: Window | null
|
|
38
|
+
): boolean {
|
|
39
|
+
return (
|
|
40
|
+
!!frame &&
|
|
41
|
+
event.source === frame &&
|
|
42
|
+
event.origin === validDatameshUiUrl(url).origin
|
|
43
|
+
);
|
|
44
|
+
}
|
package/src/httpUrl.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An address typed into a setting, checked before anything uses it.
|
|
3
|
+
*
|
|
4
|
+
* A setting is typed in by hand, so anything that is not an http(s) URL falls
|
|
5
|
+
* back to the default rather than ending up in an iframe's `src` or a
|
|
6
|
+
* request's address.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** `value` as an http(s) URL, or `fallback` when it is empty or not one. */
|
|
10
|
+
export function validHttpUrl(
|
|
11
|
+
value: string | null | undefined,
|
|
12
|
+
fallback: string
|
|
13
|
+
): URL {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = new URL((value ?? '').trim());
|
|
16
|
+
if (parsed.protocol === 'https:' || parsed.protocol === 'http:') {
|
|
17
|
+
return parsed;
|
|
18
|
+
}
|
|
19
|
+
} catch {
|
|
20
|
+
// Empty or not a URL: the default below.
|
|
21
|
+
}
|
|
22
|
+
return new URL(fallback);
|
|
23
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import { ICommandPalette, Notification } from '@jupyterlab/apputils';
|
|
8
8
|
import { LabIcon } from '@jupyterlab/ui-components';
|
|
9
9
|
import { find } from '@lumino/algorithm';
|
|
10
|
+
import { Signal } from '@lumino/signaling';
|
|
10
11
|
import { Widget } from '@lumino/widgets';
|
|
11
12
|
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
12
13
|
import { IStateDB } from '@jupyterlab/statedb';
|
|
@@ -21,7 +22,11 @@ import { snapshotFromIpynb, snapshotOf } from './notebookContext';
|
|
|
21
22
|
import { notebookHost } from './notebookHost';
|
|
22
23
|
import { KernelHandoff } from './kernelHandoff';
|
|
23
24
|
import { runChatLoop, STOPPED } from './aiLoop';
|
|
24
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
DATAMESH_UI_SERVICE,
|
|
27
|
+
MAX_OBSERVE_ROUNDS,
|
|
28
|
+
OCEANUM_AI_BACKEND_URL
|
|
29
|
+
} from './constants';
|
|
25
30
|
|
|
26
31
|
import '../style/index.css';
|
|
27
32
|
|
|
@@ -57,6 +62,21 @@ export const datamesh_connect_extension: JupyterFrontEndPlugin<void> = {
|
|
|
57
62
|
) => {
|
|
58
63
|
console.log('Oceanum datamesh connect extension is loaded');
|
|
59
64
|
|
|
65
|
+
// The `datameshUiUrl` setting; the default until the settings load.
|
|
66
|
+
let datameshUiUrl = DATAMESH_UI_SERVICE;
|
|
67
|
+
// The `aiBackendUrl` and `datameshToken` settings for the AI chat; the
|
|
68
|
+
// defaults until the settings load.
|
|
69
|
+
let aiBackendUrl = OCEANUM_AI_BACKEND_URL;
|
|
70
|
+
let datameshTokenSetting = '';
|
|
71
|
+
// Tells the sidebar the settings above have changed.
|
|
72
|
+
const settingsChanged = new Signal<JupyterFrontEnd, void>(app);
|
|
73
|
+
|
|
74
|
+
const findDatameshUI = (): DatameshUI | undefined =>
|
|
75
|
+
find(
|
|
76
|
+
app.shell.widgets('main'),
|
|
77
|
+
(widget: Widget) => widget.id === 'datamesh-ui'
|
|
78
|
+
) as DatameshUI | undefined;
|
|
79
|
+
|
|
60
80
|
//Try to get the datamesh token from the settings
|
|
61
81
|
const updateSettings = (set: ISettingRegistry.ISettings) => {
|
|
62
82
|
const token = set.get('datameshToken');
|
|
@@ -68,6 +88,15 @@ export const datamesh_connect_extension: JupyterFrontEndPlugin<void> = {
|
|
|
68
88
|
}).then(res => console.log(res));
|
|
69
89
|
}
|
|
70
90
|
window.injectToken = set.get('injectToken').user as boolean;
|
|
91
|
+
aiBackendUrl = set.get('aiBackendUrl').composite as string;
|
|
92
|
+
datameshTokenSetting = set.get('datameshToken').composite as string;
|
|
93
|
+
// An open Datamesh UI panel follows a change of address.
|
|
94
|
+
datameshUiUrl = set.get('datameshUiUrl').composite as string;
|
|
95
|
+
const datameshUI = findDatameshUI();
|
|
96
|
+
if (datameshUI) {
|
|
97
|
+
datameshUI.url = datameshUiUrl;
|
|
98
|
+
}
|
|
99
|
+
settingsChanged.emit();
|
|
71
100
|
};
|
|
72
101
|
//Try to get the datamesh token from the envars
|
|
73
102
|
|
|
@@ -91,18 +120,12 @@ export const datamesh_connect_extension: JupyterFrontEndPlugin<void> = {
|
|
|
91
120
|
|
|
92
121
|
const openDatameshUI = (event: any): void => {
|
|
93
122
|
const widgetId = 'datamesh-ui';
|
|
94
|
-
|
|
95
|
-
app.shell.widgets('main'),
|
|
96
|
-
(widget: Widget, index: number) => {
|
|
97
|
-
return widget.id === widgetId;
|
|
98
|
-
}
|
|
99
|
-
);
|
|
100
|
-
if (openWidget) {
|
|
123
|
+
if (findDatameshUI()) {
|
|
101
124
|
app.shell.activateById(widgetId);
|
|
102
125
|
return;
|
|
103
126
|
}
|
|
104
127
|
|
|
105
|
-
const datameshUIWidget = new DatameshUI();
|
|
128
|
+
const datameshUIWidget = new DatameshUI(datameshUiUrl);
|
|
106
129
|
datameshUIWidget.title.label = 'Oceanum Datamesh';
|
|
107
130
|
datameshUIWidget.id = widgetId;
|
|
108
131
|
datameshUIWidget.title.closable = true;
|
|
@@ -116,6 +139,11 @@ export const datamesh_connect_extension: JupyterFrontEndPlugin<void> = {
|
|
|
116
139
|
name: 'Datamesh Connect',
|
|
117
140
|
icon: oceanumIcon,
|
|
118
141
|
openDatameshUI: openDatameshUI,
|
|
142
|
+
datameshUiUrl: () => datameshUiUrl,
|
|
143
|
+
datameshUiFrame: () => findDatameshUI()?.frame ?? null,
|
|
144
|
+
datameshToken: () => datameshTokenSetting,
|
|
145
|
+
aiBackendUrl: () => aiBackendUrl,
|
|
146
|
+
settingsChanged,
|
|
119
147
|
commands: app.commands,
|
|
120
148
|
getCurrentWidget
|
|
121
149
|
});
|
|
@@ -169,7 +197,7 @@ export const oceanum_ai_extension: JupyterFrontEndPlugin<void> = {
|
|
|
169
197
|
// The panel the current request read its selected cell from, if the
|
|
170
198
|
// notebook was open: a code answer may replace that cell only there.
|
|
171
199
|
let readFrom: NotebookPanel | null = null;
|
|
172
|
-
const router = new ChatRouter(settings, async () => {
|
|
200
|
+
const router = new ChatRouter(settings, app.commands, async () => {
|
|
173
201
|
const open = await pin.current();
|
|
174
202
|
readFrom = open;
|
|
175
203
|
if (open) {
|