@jataware/beaker-client 2.0.0-b.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/src/render.ts ADDED
@@ -0,0 +1,121 @@
1
+ import { MimeModel, RenderMimeRegistry} from '@jupyterlab/rendermime';
2
+ import { Sanitizer } from '@jupyterlab/apputils';
3
+ import { standardRendererFactories, IRenderMime } from '@jupyterlab/rendermime';
4
+ import { PartialJSONObject } from '@lumino/coreutils';
5
+
6
+
7
+ export interface IBeakerRendererOptions {
8
+ renderers: ReadonlyArray<IMimeRenderer>;
9
+ }
10
+
11
+ export type MimetypeString = "text/plain" | "text/html" | string;
12
+
13
+ /**
14
+ *
15
+ */
16
+ export interface IMimeBundle {
17
+ [mimetype: MimetypeString]: PartialJSONObject;
18
+ }
19
+
20
+ export interface IMimeRenderer<OutputType = HTMLElement> {
21
+ rank: number;
22
+ mimetypes: MimetypeString[];
23
+ render: (mimeType: MimetypeString, data: PartialJSONObject, metadata: PartialJSONObject) => OutputType;
24
+ }
25
+
26
+ export class MimeRenderer implements IMimeRenderer<HTMLElement> {
27
+
28
+ public rank: number = 100;
29
+ public mimetypes: string[] = [];
30
+
31
+ public render(mimeType: MimetypeString, data: PartialJSONObject, metadata: PartialJSONObject): HTMLElement {
32
+
33
+ return new HTMLElement();
34
+ };
35
+
36
+ }
37
+
38
+ export class JupyterMimeRenderer extends MimeRenderer {
39
+
40
+ constructor(factory: IRenderMime.IRendererFactory) {
41
+ super();
42
+ this._factory = factory
43
+ this.rank = factory.defaultRank || 100;
44
+ this.mimetypes = [...factory.mimeTypes];
45
+ }
46
+
47
+ public render(mimeType: MimetypeString, data: PartialJSONObject, metadata?: PartialJSONObject): HTMLElement {
48
+ const renderer = this._factory.createRenderer({
49
+ mimeType,
50
+ resolver: null,
51
+ sanitizer: new Sanitizer(),
52
+ linkHandler: null,
53
+ latexTypesetter: null,
54
+ markdownParser: null,
55
+ translator: undefined,
56
+
57
+ });
58
+ const model = new MimeModel({
59
+ trusted: true,
60
+ data: {[mimeType]: data},
61
+ metadata: metadata,
62
+ });
63
+ renderer.renderModel(model);
64
+ return renderer.node;
65
+ }
66
+ private _factory: IRenderMime.IRendererFactory;
67
+ }
68
+
69
+ export class BeakerRenderer {
70
+
71
+ constructor(options?: IBeakerRendererOptions) {
72
+ this._renderers = {}
73
+ for (const factory of standardRendererFactories) {
74
+ const renderer = new JupyterMimeRenderer(factory);
75
+ this.addRenderer(renderer);
76
+ }
77
+ for (const renderer of options?.renderers || []) {
78
+ this.addRenderer(renderer);
79
+ }
80
+ }
81
+
82
+ public addRenderer(renderer: IMimeRenderer) {
83
+ for (const mimetype of renderer.mimetypes) {
84
+ if (!Object.keys(this._renderers).includes(mimetype)) {
85
+ this._renderers[mimetype] = renderer;
86
+ }
87
+ else {
88
+ const prev_renderer = this._renderers[mimetype];
89
+ if (renderer.rank <= prev_renderer.rank) {
90
+ this._renderers[mimetype] = renderer;
91
+ }
92
+ }
93
+ }
94
+ }
95
+
96
+ public get rankedMimetypes(): MimetypeString[] {
97
+ const mimetypes = Object.keys(this._renderers);
98
+ mimetypes.sort((a, b) => this._renderers[a].rank - this._renderers[b].rank);
99
+ return mimetypes;
100
+ }
101
+
102
+ public render(mimeType: MimetypeString, data: PartialJSONObject, metadata?: PartialJSONObject): any {
103
+ const renderer = this._renderers[mimeType];
104
+ if (renderer) {
105
+ return renderer.render(mimeType, data, metadata || {});
106
+ }
107
+ }
108
+
109
+ public renderMimeBundle(bundle: IMimeBundle, metadata?: PartialJSONObject): {[key: MimetypeString]: HTMLElement} {
110
+ return Object.fromEntries(Object.entries(bundle).map(([mimeType, content]) => {
111
+ return [mimeType, this.render(mimeType, content, metadata)];
112
+ }));
113
+ }
114
+
115
+ public rankedMimetypesInBundle(bundle: IMimeBundle): MimetypeString[] {
116
+ const result = this.rankedMimetypes.filter((mime) => bundle && Object.keys(bundle).includes(mime))
117
+ return result;
118
+ }
119
+
120
+ private _renderers: {[key: string]: IMimeRenderer};
121
+ }
package/src/session.ts ADDED
@@ -0,0 +1,504 @@
1
+ import { SessionContext } from '@jupyterlab/apputils';
2
+ import { ServerConnection } from '@jupyterlab/services/lib/serverconnection';
3
+ import { IKernelConnection} from '@jupyterlab/services/lib/kernel/kernel';
4
+ import { ServiceManager } from '@jupyterlab/services';
5
+ import * as messages from '@jupyterlab/services/lib/kernel/messages';
6
+ import { JSONObject } from '@lumino/coreutils';
7
+ import { v4 as uuidv4 } from 'uuid';
8
+ import { Slot, Signal } from '@lumino/signaling';
9
+ import { ConnectionStatus as JupyterConnectionStatus, IAnyMessageArgs } from '@jupyterlab/services/lib/kernel/kernel';
10
+
11
+ import { createMessageId, IBeakerAvailableContexts, IBeakerFuture, IActiveContextInfo } from './util';
12
+ import { BeakerNotebook, IBeakerShellMessage, IBeakerAnyMessage, BeakerRawCell, BeakerCodeCell, BeakerMarkdownCell, BeakerQueryCell, IBeakerIOPubMessage } from './notebook';
13
+ import { BeakerHistory } from './history';
14
+ import { BeakerRenderer, IBeakerRendererOptions } from './render';
15
+
16
+
17
+ export interface IBeakerSessionOptions {
18
+ settings: any;
19
+ name: string;
20
+ kernelName?: string;
21
+ sessionId?: string;
22
+ rendererOptions?: IBeakerRendererOptions;
23
+ messageHandler?: Slot<any, any>;
24
+ context?: {
25
+ slug: string,
26
+ payload: any,
27
+ }
28
+ };
29
+
30
+ export type JupyterKernelStatus = messages.Status;
31
+ export type BeakerKernelStatus = JupyterKernelStatus | 'connected' | 'connecting' | 'reconnecting' | 'disconnected';
32
+
33
+ /**
34
+ * Main class for connecting to and working with a Beaker kernel.
35
+ */
36
+ export class BeakerSession {
37
+
38
+ constructor(options?: IBeakerSessionOptions) {
39
+ this._sessionId = options?.sessionId ?? uuidv4();
40
+ this._sessionOptions = options;
41
+ this._serverSettings = ServerConnection.makeSettings(options?.settings);
42
+ this._services = new ServiceManager({
43
+ serverSettings: this._serverSettings,
44
+ });
45
+ this._hasBeenConnected = false;
46
+ this._services.connectionFailure.connect(this._connectionFailureHandler);
47
+ this._renderer = new BeakerRenderer(options?.rendererOptions);
48
+ this._history = new BeakerHistory(this._sessionId);
49
+
50
+ this.notebook = new BeakerNotebook();
51
+ this._initialized = new Promise(async (resolve, reject) => {
52
+ this._services.ready.then(async () => {
53
+ await this.initialize(options);
54
+ if (options?.context) {
55
+ const active_context = await this.activeContext();
56
+ if (
57
+ active_context.slug !== options.context.slug
58
+ || JSON.stringify(active_context.config) !== JSON.stringify(options.context.payload)
59
+ ) {
60
+ await this.setContext({
61
+ context: options.context.slug,
62
+ context_info: options.context.payload,
63
+ });
64
+ }
65
+ }
66
+ });
67
+ resolve();
68
+ });
69
+ }
70
+
71
+ /**
72
+ * Internal initialization logic once all the services are up and ready.
73
+ */
74
+ private async initialize(options?: IBeakerSessionOptions) {
75
+
76
+ // Create (or reuse existing) a session Context
77
+ this._sessionContext = new SessionContext({
78
+ sessionManager: this._services.sessions,
79
+ specsManager: this._services.kernelspecs,
80
+ name: options?.name,
81
+ path: options?.sessionId,
82
+ kernelPreference: options?.kernelName ? {
83
+ name: options?.kernelName,
84
+ } : undefined,
85
+ });
86
+
87
+ // Track all messages from kernels. The disconnect on newValue is in case the kernel connection is reused, to
88
+ // not set up duplicate handlers.
89
+ this._sessionContext.kernelChanged.connect((sender, {oldValue, newValue, name}) => {
90
+ oldValue?.anyMessage.disconnect(this._sessionMessageHandler, this);
91
+ newValue?.anyMessage.disconnect(this._sessionMessageHandler, this);
92
+ newValue?.anyMessage.connect(this._sessionMessageHandler, this);
93
+ if (newValue) {
94
+ this._lastKernelModel = newValue?.model;
95
+ this._prevClientId = newValue?.clientId;
96
+ }
97
+ });
98
+
99
+ this._sessionContext.connectionStatusChanged.connect((sender, status) => {
100
+ if (!this._hasBeenConnected && status == "connected") {
101
+ this._hasBeenConnected = true;
102
+ }
103
+ });
104
+
105
+ // Initialize the session
106
+ await this._sessionContext.initialize();
107
+ await this._sessionContext.ready;
108
+ if (options?.messageHandler) {
109
+ this._messageHandler = options.messageHandler;
110
+ this._sessionContext.iopubMessage.connect(this._messageHandler);
111
+ }
112
+ else {
113
+ this._messageHandler = undefined;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Low-level method for reconnecting to the kernel.
119
+ *
120
+ */
121
+ public async reconnect() {
122
+ (this._sessionContext.connectionStatusChanged as Signal<SessionContext, JupyterConnectionStatus>).emit("connecting");
123
+ this._sessionContext.dispose;
124
+ await this.initialize(this._sessionOptions)
125
+ return this._sessionContext;
126
+ }
127
+
128
+ /**
129
+ * Low-level method for sending a message to the Beaker kernel over the "shell" channel.
130
+ *
131
+ * @param messageType - The message type, as passed in `msg.header.msg_type`
132
+ * @param content - Any JSON-encodable payload to be included with the message
133
+ * @param messageId - (Optional) Pre-defined id for the message. One will be generated if not provided.
134
+ */
135
+ public sendBeakerMessage(
136
+ messageType: string,
137
+ content: JSONObject,
138
+ messageId?: string,
139
+ metadata?: {[key: string]: any},
140
+ ): IBeakerFuture {
141
+ if (!messageId) {
142
+ messageId = createMessageId(messageType);
143
+ }
144
+ if (!this.kernel) {
145
+ throw Error("Unable to send message. Not connected to kernel.");
146
+ }
147
+ const msg: IBeakerShellMessage = messages.createMessage<IBeakerShellMessage>({
148
+ session: this.session.session?.id || this.kernel?.clientId,
149
+ channel: "shell",
150
+ msgType: messageType,
151
+ content: content,
152
+ msgId: messageId,
153
+ metadata: metadata,
154
+ });
155
+
156
+ const future: IBeakerFuture<messages.IShellMessage, messages.IShellMessage> = this.kernel.sendShellMessage(
157
+ msg,
158
+ true,
159
+ true
160
+ );
161
+ future.msgId = messageId;
162
+ return future;
163
+ }
164
+
165
+ /**
166
+ * Handler for session-specific messages from the Beaker kernel.
167
+ * This handler will be evoked for all IOPub messages, but should ignore all messages that are not session-specific.
168
+ *
169
+ * @param _sessionContext - The session Context related to the incoming message
170
+ * @param msg - The incoming IOPub message
171
+ */
172
+ private _sessionMessageHandler(kernel: IKernelConnection, {msg, direction}: {msg: IBeakerAnyMessage, direction: IAnyMessageArgs["direction"]}) {
173
+ if (msg.header.msg_type === "context_setup_response" || msg.header.msg_type === "context_info_response") {
174
+ if (msg.header.msg_type === "context_setup_response") {
175
+ this._sessionInfo = msg.content;
176
+ }
177
+ else if (msg.header.msg_type === "context_info_response") {
178
+ this._sessionInfo = msg.content.info;
179
+ }
180
+ this.notebook.setSubkernelInfo(this._sessionInfo);
181
+ }
182
+ else if (msg.header.msg_type === "kernel_info_reply") {
183
+ this._kernelInfo = msg.content;
184
+ }
185
+ else if (msg.header.msg_type === "notebook_state_request") {
186
+ const outboundMsg: IBeakerShellMessage = messages.createMessage<IBeakerShellMessage>({
187
+ session: kernel.clientId,
188
+ channel: "shell",
189
+ msgType: "notebook_state_response",
190
+ content: this.notebook.toIPynb(),
191
+ msgId: msg.header.msg_id + "_reply",
192
+ metadata: {},
193
+ });
194
+ kernel.sendShellMessage(
195
+ outboundMsg,
196
+ false,
197
+ true
198
+ );
199
+ }
200
+ }
201
+
202
+ private _connectionFailureHandler(serviceManager: ServiceManager, error: Error) {
203
+
204
+ console.log("CONNECTION ERROR:", typeof(error));
205
+ console.log({cause: error.cause, message: error.message, name: error.name, stack: error.stack});
206
+ console.error(error);
207
+ console.error()
208
+ }
209
+
210
+ /**
211
+ * Returns a promise, that once resolved provides all Beaker contexts available in the session.
212
+ */
213
+ public async availableContexts(): Promise<IBeakerAvailableContexts> {
214
+ return new Promise(async (resolve) => {
215
+ const url = `${this._serverSettings.baseUrl}contexts`;
216
+ const response = await fetch(`${this._serverSettings.baseUrl}contexts`);
217
+ const data = <IBeakerAvailableContexts>(await response.json());
218
+ resolve(data);
219
+ });
220
+ }
221
+
222
+ /**
223
+ * Returns a promise that once resolved provides detailed information about the active context.
224
+ */
225
+ public async activeContext(): Promise<IActiveContextInfo> {
226
+ return new Promise(async (resolve, reject) => {
227
+ await this.sessionReady;
228
+ const future = this.sendBeakerMessage(
229
+ "context_info_request",
230
+ {}
231
+ );
232
+ if (future !== undefined) {
233
+ future.onIOPub = async (msg: any) => {
234
+ if (msg.header.msg_type === "context_info_response") {
235
+ resolve({
236
+ ...msg.content,
237
+ kernelInfo: await this.kernel?.info,
238
+ });
239
+ }
240
+ }
241
+ await future.done;
242
+ }
243
+ reject({});
244
+ });
245
+ }
246
+
247
+ public async setContext(contextPayload: any): Promise<IActiveContextInfo> {
248
+ return new Promise(async (resolve, reject) => {
249
+ const setupResult = this.sendBeakerMessage(
250
+ "context_setup_request",
251
+ contextPayload
252
+ );
253
+ if (setupResult) {
254
+ await setupResult.done;
255
+ const contextInfo = setupResult.msg.content as IActiveContextInfo;
256
+ const kernelInfo = await this.kernel?.requestKernelInfo();
257
+
258
+ resolve({
259
+ ...contextInfo,
260
+ kernelInfo: kernelInfo?.content,
261
+ })
262
+ }
263
+ reject({});
264
+ });
265
+ }
266
+
267
+ /**
268
+ * Executes a Beaker Action, handling all of the message
269
+ *
270
+ * The usual IBeakerFuture response handlers can be applied to the returned future to do act upon the responses.
271
+ *
272
+ * @param actionName - Name of the action to execute
273
+ * @param payload - Payload to pass along with the action
274
+ * @param messageId - (Optional) Id for request message. If not provided, will be generated automatically.
275
+ * @returns - A future
276
+ */
277
+ public executeAction(actionName: string, payload: JSONObject, messageId?: string): IBeakerFuture {
278
+ const requestType = `${actionName}_request`;
279
+ const responseType = `${actionName}_response`;
280
+ const messageFuture = this.sendBeakerMessage(
281
+ requestType,
282
+ payload,
283
+ messageId,
284
+ )
285
+ if (messageFuture) {
286
+ const responseHandler = async (msg: IBeakerIOPubMessage): Promise<boolean> => {
287
+ if (msg.header.msg_type === responseType && messageFuture.onResponse) {
288
+ await messageFuture.onResponse(msg);
289
+ }
290
+ else if (msg.header.msg_type === "code_cell") {
291
+ const nb = this.notebook;
292
+ const codeCell = new BeakerCodeCell({
293
+ cell_type: "code",
294
+ source: msg.content.code,
295
+ metadata: {},
296
+ outputs: [],
297
+ });
298
+ nb.addCell(codeCell);
299
+ }
300
+ return true;
301
+ }
302
+ messageFuture.registerMessageHook(responseHandler)
303
+ }
304
+ return messageFuture;
305
+ }
306
+
307
+ /**
308
+ * Interrupt the kernel activity, stopping execution in both the beaker LLM ReAct loop and the subkernel as needed.
309
+ * See
310
+ *
311
+ * @returns - A future
312
+ */
313
+ public interrupt(): Promise<any> {
314
+ if (this.session.session?.kernel) {
315
+ return this.session.session.kernel.interrupt();
316
+ }
317
+ else {
318
+ return new Promise(() => {});
319
+ }
320
+ }
321
+
322
+
323
+ /**
324
+ *
325
+ * @param source - The `code` contents of the cell.
326
+ * @param metadata - (Optional) Any metadata to be associated with the cell.
327
+ * @param outputs - (Optional) Any outputs that should be included/displayed.
328
+ * @returns - A reference to the generated cell
329
+ */
330
+ public addCodeCell(source: string, metadata={}, outputs=[]) {
331
+ const cell = new BeakerCodeCell({
332
+ cell_type: "code",
333
+ source,
334
+ metadata,
335
+ outputs,
336
+ });
337
+ this.notebook.addCell(cell);
338
+ return cell;
339
+ }
340
+
341
+ /**
342
+ * Convenience method for adding a MarkdownCell to the notebook
343
+ *
344
+ * @param source - The raw markdown encoded text that should be rendered upon execute
345
+ * @param metadata - (Optional) Any metadata to be associated with the cell.
346
+ * @returns - A reference to the generated cell
347
+ */
348
+ public addMarkdownCell(source: string, metadata={}) {
349
+ const cell = new BeakerMarkdownCell({
350
+ cell_type: "markdown",
351
+ source,
352
+ metadata,
353
+ });
354
+ this.notebook.addCell(cell);
355
+ return cell;
356
+ }
357
+
358
+ /**
359
+ * Convenience method for adding a RawCell to the notebook
360
+ *
361
+ * @param source - The raw contents to be included in the raw cell.
362
+ * @param metadata - (Optional) Any metadata to be associated with the cell.
363
+ * @returns - A reference to the generated cell
364
+ */
365
+ public addRawCell(source:string, metadata={}) {
366
+ const cell = new BeakerRawCell({
367
+ cell_type: "raw",
368
+ source,
369
+ metadata,
370
+ });
371
+ this.notebook.addCell(cell);
372
+ return cell;
373
+ }
374
+
375
+ /**
376
+ * Convenience method for adding a QueryCell to the notebook
377
+ *
378
+ * @param source - The contents of the query for the LLM as a plain string
379
+ * @param metadata - (Optional) Any metadata to be associated with the cell.
380
+ * @returns - A reference to the generated cell
381
+ */
382
+ public addQueryCell(source: string, metadata={}) {
383
+ const cell = new BeakerQueryCell({
384
+ cell_type: "query",
385
+ source,
386
+ metadata,
387
+ });
388
+ this.notebook.addCell(cell);
389
+ return cell;
390
+ };
391
+
392
+ /**
393
+ * Populates the sessions notebook with the provided notebook json
394
+ *
395
+ * @param notebookJSONObject - The json representation of a notebook, as found inside an .ipynb file
396
+ */
397
+ public loadNotebook(notebookJSONObject: object) {
398
+ this.notebook.loadFromIPynb(notebookJSONObject);
399
+ }
400
+
401
+ /**
402
+ * Completely resets the session, clearing the notebook and history, and restarting the fresh kernel so it is in a fresh state.
403
+ */
404
+ public reset() {
405
+ // Remove cells via splice to ensure reactivity
406
+ this.notebook.cells.splice(0, this.notebook.cells.length);
407
+ this._history.clear();
408
+ this._sessionContext.restartKernel();
409
+ }
410
+
411
+ /**
412
+ * A promise that resolves once everything the session requires are also ready.
413
+ */
414
+ get sessionReady(): Promise<void> {
415
+ return new Promise(async (resolve) => {
416
+ await this._services.ready;
417
+ await this._sessionContext.ready;
418
+ await this._initialized;
419
+ resolve();
420
+ })
421
+ }
422
+
423
+ /**
424
+ * A reference to the underlying Jupyter SessionContext for this Beaker Session.
425
+ */
426
+ get session(): SessionContext {
427
+ return this._sessionContext;
428
+ }
429
+
430
+ /**
431
+ * Returns the status of the Beaker kernel
432
+ */
433
+ get status(): BeakerKernelStatus {
434
+ const connectionStatus = this.kernel?.connectionStatus;
435
+ const kernelStatus = this.kernel?.status;
436
+
437
+ // Prioritize kernel status over connection status if connected.
438
+ if (connectionStatus === "connected" && kernelStatus) {
439
+ return kernelStatus;
440
+ }
441
+ // Return "reconnecting" if we've been connected and lost connection.
442
+ else if (connectionStatus === "connecting" && this._hasBeenConnected) {
443
+ return "reconnecting";
444
+ }
445
+ else if (connectionStatus) {
446
+ return connectionStatus || "unknown";
447
+ }
448
+ else {
449
+ return "disconnected";
450
+ }
451
+ }
452
+
453
+ /**
454
+ * A reference to the Jupyter KernelConnection object for this session.
455
+ */
456
+ get kernel(): IKernelConnection | null {
457
+ return this._sessionContext?.session?.kernel || null;
458
+ }
459
+
460
+ get kernelInfo() {
461
+ return this._kernelInfo;
462
+ }
463
+
464
+ get lastKernelModel() {
465
+ return this._lastKernelModel;
466
+ }
467
+
468
+ get prevClientId() {
469
+ return this._prevClientId;
470
+ }
471
+
472
+ /**
473
+ * A reference to the Jupyter ServiceManager which contains all of the services for this session.
474
+ */
475
+ get services(): ServiceManager {
476
+ return this._services;
477
+ }
478
+
479
+ get renderer(): BeakerRenderer {
480
+ return this._renderer;
481
+ }
482
+
483
+ get sessionId(): string {
484
+ return this._sessionContext.path;
485
+ }
486
+
487
+ private _initialized: Promise<void>;
488
+ private _sessionId: string;
489
+ private _sessionOptions?: IBeakerSessionOptions;
490
+ private _services: ServiceManager;
491
+ private _serverSettings: ServerConnection.ISettings;
492
+ private _sessionContext!: SessionContext;
493
+ private _messageHandler?: Slot<SessionContext, messages.IIOPubMessage>;
494
+ private _history: BeakerHistory;
495
+ private _sessionInfo: any;
496
+ private _renderer: BeakerRenderer;
497
+ private _kernelInfo: any;
498
+ private _lastKernelModel?: any;
499
+ private _prevClientId?: string;
500
+ private _hasBeenConnected: boolean;
501
+
502
+ public notebook: BeakerNotebook;
503
+
504
+ }