@oceanum/oceanumlab 4.6.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.
Files changed (83) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +116 -0
  3. package/lib/DatameshUI.d.ts +5 -0
  4. package/lib/DatameshUI.js +8 -0
  5. package/lib/DatameshUI.js.map +1 -0
  6. package/lib/DatameshWidget.d.ts +59 -0
  7. package/lib/DatameshWidget.js +573 -0
  8. package/lib/DatameshWidget.js.map +1 -0
  9. package/lib/DatasourceItem.d.ts +12 -0
  10. package/lib/DatasourceItem.js +47 -0
  11. package/lib/DatasourceItem.js.map +1 -0
  12. package/lib/aiLoop.d.ts +60 -0
  13. package/lib/aiLoop.js +85 -0
  14. package/lib/aiLoop.js.map +1 -0
  15. package/lib/chatRouter.d.ts +64 -0
  16. package/lib/chatRouter.js +225 -0
  17. package/lib/chatRouter.js.map +1 -0
  18. package/lib/constants.d.ts +14 -0
  19. package/lib/constants.js +15 -0
  20. package/lib/constants.js.map +1 -0
  21. package/lib/conversationPin.d.ts +68 -0
  22. package/lib/conversationPin.js +120 -0
  23. package/lib/conversationPin.js.map +1 -0
  24. package/lib/handler.d.ts +8 -0
  25. package/lib/handler.js +36 -0
  26. package/lib/handler.js.map +1 -0
  27. package/lib/index.d.ts +16 -0
  28. package/lib/index.js +273 -0
  29. package/lib/index.js.map +1 -0
  30. package/lib/kernelHandoff.d.ts +61 -0
  31. package/lib/kernelHandoff.js +236 -0
  32. package/lib/kernelHandoff.js.map +1 -0
  33. package/lib/notebookContext.d.ts +48 -0
  34. package/lib/notebookContext.js +118 -0
  35. package/lib/notebookContext.js.map +1 -0
  36. package/lib/notebookHost.d.ts +6 -0
  37. package/lib/notebookHost.js +72 -0
  38. package/lib/notebookHost.js.map +1 -0
  39. package/lib/notebookRun.d.ts +37 -0
  40. package/lib/notebookRun.js +70 -0
  41. package/lib/notebookRun.js.map +1 -0
  42. package/lib/progress.d.ts +43 -0
  43. package/lib/progress.js +103 -0
  44. package/lib/progress.js.map +1 -0
  45. package/lib/sse.d.ts +26 -0
  46. package/lib/sse.js +112 -0
  47. package/lib/sse.js.map +1 -0
  48. package/package.json +151 -0
  49. package/schema/datamesh-connect.json +35 -0
  50. package/src/DatameshUI.tsx +15 -0
  51. package/src/DatameshWidget.tsx +909 -0
  52. package/src/DatasourceItem.tsx +124 -0
  53. package/src/__tests__/aiLoop.spec.ts +304 -0
  54. package/src/__tests__/chatRouter.spec.ts +315 -0
  55. package/src/__tests__/conversationPin.spec.ts +236 -0
  56. package/src/__tests__/kernelHandoff.spec.ts +106 -0
  57. package/src/__tests__/notebookContext.spec.ts +97 -0
  58. package/src/__tests__/notebookHost.spec.ts +195 -0
  59. package/src/__tests__/notebookRun.spec.ts +95 -0
  60. package/src/__tests__/notebookSnapshot.spec.ts +62 -0
  61. package/src/__tests__/oceanumlab.spec.ts +9 -0
  62. package/src/__tests__/sse.spec.ts +304 -0
  63. package/src/aiLoop.ts +140 -0
  64. package/src/chatRouter.ts +329 -0
  65. package/src/constants.ts +16 -0
  66. package/src/conversationPin.ts +148 -0
  67. package/src/global.d.ts +5 -0
  68. package/src/handler.ts +46 -0
  69. package/src/index.ts +327 -0
  70. package/src/kernelHandoff.ts +284 -0
  71. package/src/notebookContext.ts +144 -0
  72. package/src/notebookHost.ts +97 -0
  73. package/src/notebookRun.ts +104 -0
  74. package/src/progress.ts +121 -0
  75. package/src/sse.ts +127 -0
  76. package/src/svg.d.ts +4 -0
  77. package/style/base.css +5 -0
  78. package/style/icons/oceanum-dark.svg +1 -0
  79. package/style/icons/oceanum-light.svg +1 -0
  80. package/style/icons/oceanum.png +0 -0
  81. package/style/icons/oceanum.svg +1 -0
  82. package/style/index.css +477 -0
  83. package/style/index.js +1 -0
@@ -0,0 +1,124 @@
1
+ import React, { useState } from 'react';
2
+ import { caretDownIcon, caretRightIcon } from '@jupyterlab/ui-components';
3
+ import { ArrowBendDownRight } from '@phosphor-icons/react';
4
+
5
+ import '../style/index.css';
6
+ import { IDatasource } from './DatameshWidget';
7
+
8
+ export interface IDatasourceItemProps {
9
+ datasource: IDatasource;
10
+ insertDatasource: (datasource: IDatasource) => void;
11
+ onMouseDown: (
12
+ e: React.MouseEvent<HTMLSpanElement, MouseEvent>,
13
+ datasource: IDatasource
14
+ ) => void;
15
+ }
16
+
17
+ /**
18
+ * A React component for expandable containers.
19
+ */
20
+
21
+ export const DatasourceItem: React.FC<IDatasourceItemProps> = ({
22
+ datasource,
23
+ insertDatasource,
24
+ onMouseDown
25
+ }) => {
26
+ const [expanded, setExpandedValue] = useState(false);
27
+ const [isDragging, setIsDragging] = useState(false);
28
+
29
+ const handleToggleExpand = (): void => {
30
+ setExpandedValue(!expanded);
31
+ };
32
+
33
+ const startDrag = (e: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
34
+ setIsDragging(true);
35
+ onMouseDown(e, datasource);
36
+ };
37
+
38
+ return (
39
+ <div className={expanded ? 'expanded' : ''}>
40
+ <div key={datasource.id} className="datasource-item-title">
41
+ <button
42
+ title={expanded ? 'Hide Details' : 'Show Details'}
43
+ onClick={handleToggleExpand}
44
+ className="datasource-item-expand"
45
+ >
46
+ {expanded ? (
47
+ <caretDownIcon.react
48
+ tag="span"
49
+ elementPosition="center"
50
+ width="20px"
51
+ />
52
+ ) : (
53
+ <caretRightIcon.react
54
+ tag="span"
55
+ elementPosition="center"
56
+ width="20px"
57
+ />
58
+ )}
59
+ </button>
60
+ <span
61
+ title={datasource.description}
62
+ className={
63
+ isDragging
64
+ ? 'datasource-item-name'
65
+ : 'datasource-item-name draggable'
66
+ }
67
+ onClick={handleToggleExpand}
68
+ onMouseDown={startDrag}
69
+ onMouseUp={() => setIsDragging(false)}
70
+ >
71
+ {datasource.description}
72
+ </span>
73
+
74
+ <button
75
+ title="Insert code to load datasource"
76
+ onClick={handleToggleExpand}
77
+ className="datasource-item-action"
78
+ >
79
+ <ArrowBendDownRight
80
+ size={18}
81
+ onClick={e => insertDatasource(datasource)}
82
+ />
83
+ </button>
84
+ </div>
85
+ {expanded && (
86
+ <div className={'datasource-item-details'}>
87
+ <div className="datasource-query-field">
88
+ Datasource id: <span>{datasource.datasource}</span>
89
+ </div>
90
+ {datasource.geofilter && (
91
+ <div className="datasource-query-field">
92
+ Geofilter type:<span>{datasource.geofilter.type}</span>
93
+ </div>
94
+ )}
95
+ {datasource.variables && (
96
+ <div className="datasource-query-field">
97
+ Variables:<span>{datasource.variables.join(',')}</span>
98
+ </div>
99
+ )}
100
+ {datasource.timefilter && (
101
+ <div className="datasource-query-field">
102
+ Timefilter:<span>{datasource.timefilter.times[0]}</span>
103
+ <span>to</span>
104
+ <span>{datasource.timefilter.times[1]}</span>
105
+ </div>
106
+ )}
107
+ {datasource.spatialref && (
108
+ <div className="datasource-query-field">
109
+ Spatial ref:<span>{datasource.spatialref}</span>
110
+ </div>
111
+ )}
112
+ <div>
113
+ <a
114
+ href={`https://oceanum.io/datasets/${datasource.datasource}`}
115
+ target="_blank"
116
+ >
117
+ Datasource details
118
+ </a>
119
+ </div>
120
+ </div>
121
+ )}
122
+ </div>
123
+ );
124
+ };
@@ -0,0 +1,304 @@
1
+ import { runChatLoop, STOPPED, LoopDeps } from '../aiLoop';
2
+ import type { OceanumResponse } from '../chatRouter';
3
+ import type { ObservedRun } from '../notebookRun';
4
+
5
+ const code = (content: string) => ({ type: 'code' as const, content });
6
+ const reply = (message: string, ...blocks: OceanumResponse['blocks']) => ({
7
+ message,
8
+ blocks
9
+ });
10
+
11
+ /**
12
+ * A fake notebook: `place` records what it was asked to place and reports one
13
+ * ok run per code block, unless a script says a given code should fail.
14
+ */
15
+ function fakeDeps(
16
+ first: OceanumResponse,
17
+ observations: OceanumResponse[],
18
+ failing: string[] = []
19
+ ) {
20
+ const placed: { blocks: string[]; autoRun: boolean; replace: boolean }[] = [];
21
+ const observed: ObservedRun[][] = [];
22
+ const pending = [...observations];
23
+
24
+ const deps: LoopDeps = {
25
+ route: async () => ({ response: first, hasCodeCellSelected: true }),
26
+ observe: async (_p, _h, runs) => {
27
+ observed.push(runs.map(r => ({ ...r })));
28
+ const next = pending.shift();
29
+ if (!next) {
30
+ throw new Error('observe called more times than scripted');
31
+ }
32
+ return next;
33
+ },
34
+ place: async (response, opts) => {
35
+ placed.push({
36
+ blocks: response.blocks.map(b => b.content),
37
+ autoRun: opts.autoRun,
38
+ replace: opts.replaceCodeCell
39
+ });
40
+ const runs: ObservedRun[] = opts.autoRun
41
+ ? response.blocks
42
+ .filter(b => b.type === 'code')
43
+ .map(b => ({
44
+ code: b.content,
45
+ status: failing.includes(b.content) ? 'error' : 'ok',
46
+ stdout: `ran ${b.content}`,
47
+ error: failing.includes(b.content) ? 'Boom' : null,
48
+ message: response.message
49
+ }))
50
+ : [];
51
+ return { message: response.message, runs };
52
+ }
53
+ };
54
+ return { deps, placed, observed };
55
+ }
56
+
57
+ const opts = (over: Partial<Parameters<typeof runChatLoop>[3]> = {}) => ({
58
+ autoRunCode: false,
59
+ iterate: false,
60
+ maxRounds: 5,
61
+ signal: new AbortController().signal,
62
+ ...over
63
+ });
64
+
65
+ describe('runChatLoop: the three workflows', () => {
66
+ it('workflow 3: places blocks and does not run them', async () => {
67
+ const { deps, placed, observed } = fakeDeps(
68
+ reply('Here.', code('a()')),
69
+ []
70
+ );
71
+
72
+ const text = await runChatLoop('q', [], deps, opts());
73
+
74
+ expect(text).toBe('Here.');
75
+ expect(placed).toEqual([
76
+ { blocks: ['a()'], autoRun: false, replace: true }
77
+ ]);
78
+ expect(observed).toEqual([]);
79
+ });
80
+
81
+ it('workflow 1: places and runs, but never observes', async () => {
82
+ const { deps, placed, observed } = fakeDeps(
83
+ reply('Here.', code('a()')),
84
+ []
85
+ );
86
+
87
+ await runChatLoop('q', [], deps, opts({ autoRunCode: true }));
88
+
89
+ expect(placed[0].autoRun).toBe(true);
90
+ expect(observed).toEqual([]);
91
+ });
92
+
93
+ it('workflow 2: runs, observes, places the follow-up, and stops when no code comes back', async () => {
94
+ const { deps, placed, observed } = fakeDeps(
95
+ reply('Step one.', code('a()')),
96
+ [reply('Step two.', code('b()')), reply('Done: the peak is in January.')]
97
+ );
98
+
99
+ const text = await runChatLoop(
100
+ 'q',
101
+ [],
102
+ deps,
103
+ opts({ autoRunCode: true, iterate: true })
104
+ );
105
+
106
+ expect(placed.map(p => p.blocks)).toEqual([['a()'], ['b()'], []]);
107
+ // Every observation carries EVERY run so far, with the agent's message
108
+ // attached to the code it explained.
109
+ expect(observed.map(o => o.map(r => r.code))).toEqual([
110
+ ['a()'],
111
+ ['a()', 'b()']
112
+ ]);
113
+ expect(observed[1][0].message).toBe('Step one.');
114
+ expect(observed[1][1].message).toBe('Step two.');
115
+ // The chain reads as its steps, not just the last one.
116
+ expect(text).toBe(
117
+ 'Step one.\n\nStep two.\n\nDone: the peak is in January.'
118
+ );
119
+ });
120
+
121
+ it('iterate without autoRun is inert', async () => {
122
+ const { deps, observed } = fakeDeps(reply('Here.', code('a()')), []);
123
+
124
+ await runChatLoop('q', [], deps, opts({ iterate: true }));
125
+
126
+ expect(observed).toEqual([]);
127
+ });
128
+
129
+ it('a failed run is observed so the agent can repair it', async () => {
130
+ const { deps, observed } = fakeDeps(
131
+ reply('Try.', code('bad()')),
132
+ [reply('Fixed.', code('good()')), reply('Done.')],
133
+ ['bad()']
134
+ );
135
+
136
+ await runChatLoop(
137
+ 'q',
138
+ [],
139
+ deps,
140
+ opts({ autoRunCode: true, iterate: true })
141
+ );
142
+
143
+ expect(observed[0][0]).toMatchObject({
144
+ code: 'bad()',
145
+ status: 'error',
146
+ error: 'Boom'
147
+ });
148
+ expect(observed[1][1]).toMatchObject({ code: 'good()', status: 'ok' });
149
+ });
150
+
151
+ it('a cell that could not run at all ends the loop without observing', async () => {
152
+ const { deps, observed } = fakeDeps(reply('Try.', code('a()')), [
153
+ reply('Again.', code('b()'))
154
+ ]);
155
+ const place = deps.place;
156
+ deps.place = async (response, o) => ({
157
+ ...(await place(response, o)),
158
+ halted: true
159
+ });
160
+
161
+ const text = await runChatLoop(
162
+ 'q',
163
+ [],
164
+ deps,
165
+ opts({ autoRunCode: true, iterate: true })
166
+ );
167
+
168
+ expect(observed).toEqual([]);
169
+ expect(text).toBe('Try.');
170
+ });
171
+ });
172
+
173
+ describe('runChatLoop: limits', () => {
174
+ it('stops at maxRounds even if the agent keeps sending code', async () => {
175
+ const forever = Array.from({ length: 10 }, (_, i) =>
176
+ reply(`More ${i}.`, code(`s${i}()`))
177
+ );
178
+ const { deps, observed } = fakeDeps(reply('Start.', code('s()')), forever);
179
+
180
+ await runChatLoop(
181
+ 'q',
182
+ [],
183
+ deps,
184
+ opts({ autoRunCode: true, iterate: true, maxRounds: 3 })
185
+ );
186
+
187
+ // The cap is on observe requests, checked after each one so the server's
188
+ // own at-cap explanation turn is always requested: 3 observes, then the
189
+ // round they produced is placed and the loop ends.
190
+ expect(observed).toHaveLength(3);
191
+ });
192
+
193
+ it('a code-free answer (what the server sends at its cap) ends the loop', async () => {
194
+ const { deps, observed } = fakeDeps(reply('Start.', code('s()')), [
195
+ reply('Capped: here is what happened.')
196
+ ]);
197
+
198
+ const text = await runChatLoop(
199
+ 'q',
200
+ [],
201
+ deps,
202
+ opts({ autoRunCode: true, iterate: true, maxRounds: 1 })
203
+ );
204
+
205
+ expect(observed).toHaveLength(1);
206
+ expect(text).toBe('Start.\n\nCapped: here is what happened.');
207
+ });
208
+
209
+ it('Stop between rounds halts the loop and says so', async () => {
210
+ const controller = new AbortController();
211
+ const { deps, observed } = fakeDeps(reply('Start.', code('s()')), [
212
+ reply('More.', code('t()'))
213
+ ]);
214
+ // Abort as soon as the first placement happens.
215
+ const original = deps.place;
216
+ deps.place = async (r, o) => {
217
+ const out = await original(r, o);
218
+ controller.abort();
219
+ return out;
220
+ };
221
+
222
+ const text = await runChatLoop(
223
+ 'q',
224
+ [],
225
+ deps,
226
+ opts({ autoRunCode: true, iterate: true, signal: controller.signal })
227
+ );
228
+
229
+ expect(observed).toEqual([]);
230
+ expect(text).toBe(`Start.\n\n${STOPPED}`);
231
+ });
232
+
233
+ it('Stop while an observe request is in flight keeps what was already placed', async () => {
234
+ const controller = new AbortController();
235
+ const { deps, placed } = fakeDeps(reply('Step one.', code('a()')), []);
236
+ // The real router rejects with the fetch AbortError once Stop is pressed.
237
+ deps.observe = async () => {
238
+ controller.abort();
239
+ throw new DOMException('The user aborted a request.', 'AbortError');
240
+ };
241
+
242
+ const text = await runChatLoop(
243
+ 'q',
244
+ [],
245
+ deps,
246
+ opts({ autoRunCode: true, iterate: true, signal: controller.signal })
247
+ );
248
+
249
+ expect(placed).toHaveLength(1);
250
+ expect(text).toBe(`Step one.\n\n${STOPPED}`);
251
+ });
252
+
253
+ it('an observe failure that is not a Stop still propagates', async () => {
254
+ const { deps } = fakeDeps(reply('Step one.', code('a()')), []);
255
+ deps.observe = async () => {
256
+ throw new Error('Backend error: 500');
257
+ };
258
+
259
+ await expect(
260
+ runChatLoop('q', [], deps, opts({ autoRunCode: true, iterate: true }))
261
+ ).rejects.toThrow('Backend error: 500');
262
+ });
263
+
264
+ it('Stop during the first request returns Stopped without placing anything', async () => {
265
+ const controller = new AbortController();
266
+ const { deps, placed } = fakeDeps(reply('Start.', code('s()')), []);
267
+ deps.route = async () => {
268
+ controller.abort();
269
+ return {
270
+ response: reply('Start.', code('s()')),
271
+ hasCodeCellSelected: false
272
+ };
273
+ };
274
+
275
+ const text = await runChatLoop(
276
+ 'q',
277
+ [],
278
+ deps,
279
+ opts({ signal: controller.signal })
280
+ );
281
+
282
+ expect(text).toBe(STOPPED);
283
+ expect(placed).toEqual([]);
284
+ });
285
+
286
+ it('Stop that rejects the first request returns Stopped, not an error', async () => {
287
+ const controller = new AbortController();
288
+ const { deps, placed } = fakeDeps(reply('Start.', code('s()')), []);
289
+ deps.route = async () => {
290
+ controller.abort();
291
+ throw new DOMException('The user aborted a request.', 'AbortError');
292
+ };
293
+
294
+ const text = await runChatLoop(
295
+ 'q',
296
+ [],
297
+ deps,
298
+ opts({ signal: controller.signal })
299
+ );
300
+
301
+ expect(text).toBe(STOPPED);
302
+ expect(placed).toEqual([]);
303
+ });
304
+ });