@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.
@@ -0,0 +1,949 @@
1
+ import * as nbformat from '@jupyterlab/nbformat';
2
+ import * as messages from '@jupyterlab/services/lib/kernel/messages';
3
+ import { JSONObject, PartialJSONValue, PartialJSONObject } from '@lumino/coreutils';
4
+ import { IShellFuture } from '@jupyterlab/services/lib/kernel/kernel';
5
+ import { v4 as uuidv4 } from 'uuid';
6
+
7
+ import { BeakerSession } from './session';
8
+ import { IBeakerFuture, BeakerCellFuture, BeakerCellFutures } from './util';
9
+
10
+
11
+ export interface IBeakerHeader extends messages.IHeader<messages.MessageType> {
12
+ msg_type: any;
13
+ }
14
+
15
+ export interface IBeakerShellMessage extends messages.IShellMessage {
16
+ header: IBeakerHeader;
17
+ channel: "shell";
18
+ content: any;
19
+ parent_header: any;
20
+ }
21
+
22
+ export interface IBeakerIOPubMessage extends messages.IIOPubMessage {
23
+ header: IBeakerHeader;
24
+ channel: "iopub";
25
+ content: any;
26
+ }
27
+
28
+ export interface IBeakerAnyMessage extends messages.IMessage {
29
+ header: IBeakerHeader;
30
+ content: any;
31
+ }
32
+
33
+ export type BeakerCellStatus = messages.Status | "awaiting_input";
34
+
35
+ export type BeakerCellExecutionStatus = IBeakerCellExecutionStatusPlain | IBeakerCellExecutionStatusPending | IBeakerCellExecutionStatusOk | IBeakerCellExecutionStatusError;
36
+
37
+ export interface IBeakerCellExecutionStatusPlain {
38
+ status: "none" | "modified" | "abort";
39
+ }
40
+
41
+ export interface IBeakerCellExecutionStatusPending {
42
+ status: "pending";
43
+ checkpoint_index?: number;
44
+ }
45
+
46
+ export interface IBeakerCellExecutionStatusOk {
47
+ status: "ok"
48
+ checkpoint_index?: number;
49
+ }
50
+ export interface IBeakerCellExecutionStatusError {
51
+ status: "error";
52
+ ename: string;
53
+ evalue: string;
54
+ traceback: string[];
55
+ }
56
+
57
+ export type BeakerCellType = nbformat.CellType | string | 'query';
58
+
59
+ export class BeakerBaseCell implements nbformat.IBaseCell {
60
+ // Override index type to allow methods to be defined on the class
61
+ static IPYNB_KEYS = ["cell_type", "source", "metadata", "id", "attachments", "outputs", "execution_count"];
62
+
63
+ [key: string]: any;
64
+ id: string = BeakerBaseCell.generateId();
65
+ declare cell_type: BeakerCellType;
66
+ metadata: Partial<nbformat.ICellMetadata> = {};
67
+ source: nbformat.MultilineString = "";
68
+ status: BeakerCellStatus = "idle";
69
+ children: BeakerBaseCell[] = [];
70
+
71
+ constructor(content: Partial<nbformat.IBaseCell>) {
72
+ Object.assign(this, content)
73
+ if (content.id === undefined) {
74
+ this.id = BeakerBaseCell.generateId();
75
+ }
76
+ }
77
+
78
+ static generateId(): string {
79
+ return uuidv4();
80
+ }
81
+
82
+ public fromJSON(obj: any) {
83
+ Object.keys(obj).forEach((key) => {
84
+ this[key] = obj[key];
85
+ })
86
+ }
87
+
88
+ public toJSON() {
89
+ return {...this};
90
+ }
91
+
92
+ public fromIPynb(obj: any) {
93
+ Object.keys(obj).forEach((key) => {
94
+ this[key] = obj[key];
95
+ })
96
+ }
97
+
98
+ public toIPynb(): nbformat.IBaseCell|nbformat.IBaseCell[] {
99
+ const output: JSONObject = {};
100
+ for (const key of Object.keys(this)) {
101
+ if (BeakerBaseCell.IPYNB_KEYS.includes(key)) {
102
+ output[key] = this[key];
103
+ }
104
+ }
105
+ return output as nbformat.IBaseCell;
106
+ }
107
+ }
108
+
109
+ // Simple payload events
110
+
111
+ type BeakerQueryTextEventType =
112
+ | "response"
113
+ | "user_question"
114
+ | "user_answer"
115
+ | "abort";
116
+
117
+ export interface IBeakerQueryTextEvent extends PartialJSONObject {
118
+ type: BeakerQueryTextEventType;
119
+ content: string;
120
+ };
121
+
122
+ // Specific-payload types
123
+
124
+ type BeakerQueryCellEventType = "code_cell";
125
+
126
+ export interface IBeakerQueryCellEvent extends PartialJSONObject {
127
+ type: BeakerQueryCellEventType;
128
+ content: {
129
+ cell_id: string;
130
+ parent_id: string;
131
+ metadata: PartialJSONObject;
132
+ };
133
+ };
134
+
135
+ type BeakerQueryErrorEventType = "error";
136
+
137
+ export interface IBeakerQueryErrorEvent extends PartialJSONObject {
138
+ type: BeakerQueryErrorEventType;
139
+ content: {
140
+ ename: string;
141
+ evalue: string;
142
+ traceback: string[];
143
+ };
144
+ }
145
+
146
+ export type BeakerToolCallState =
147
+ | "pending"
148
+ | "running"
149
+ | "done"
150
+ | "error"
151
+ | "cancelled";
152
+
153
+ export interface IBeakerToolCall extends PartialJSONObject {
154
+ tool_call_id: string;
155
+ tool_name: string;
156
+ tool_input: any;
157
+ state: BeakerToolCallState;
158
+ started_at?: string;
159
+ ended_at?: string;
160
+ output_preview?: string;
161
+ output_truncated?: boolean;
162
+ error?: {
163
+ ename: string;
164
+ evalue: string;
165
+ traceback?: string[];
166
+ };
167
+ }
168
+
169
+ type BeakerQueryThoughtType = "thought";
170
+ export interface IBeakerQueryThoughtEvent extends PartialJSONObject {
171
+ type: BeakerQueryThoughtType;
172
+ content: {
173
+ thought: string;
174
+ thought_id: string;
175
+ tool_calls: IBeakerToolCall[];
176
+ };
177
+ }
178
+
179
+ // umbrella-type for events
180
+
181
+ export type BeakerQueryEventType =
182
+ | BeakerQueryTextEventType
183
+ | BeakerQueryCellEventType
184
+ | BeakerQueryErrorEventType
185
+ | BeakerQueryThoughtType
186
+ ;
187
+
188
+ export type BeakerQueryEvent =
189
+ | IBeakerQueryTextEvent
190
+ | IBeakerQueryCellEvent
191
+ | IBeakerQueryErrorEvent
192
+ | IBeakerQueryThoughtEvent
193
+ ;
194
+
195
+
196
+ export interface IQueryCell extends nbformat.IBaseCell {
197
+ cell_type: 'query';
198
+ events: BeakerQueryEvent[];
199
+ }
200
+
201
+ export class BeakerRawCell extends BeakerBaseCell implements nbformat.IRawCell {
202
+ cell_type: "raw" = "raw";
203
+ attachments?: nbformat.IAttachments;
204
+ declare metadata: Partial<nbformat.IRawCellMetadata>;
205
+
206
+ constructor(content: nbformat.ICell) {
207
+ super(content);
208
+ Object.assign(this, content)
209
+ }
210
+
211
+ public execute(session: BeakerSession): IBeakerFuture | null {
212
+ return null
213
+ };
214
+ }
215
+
216
+ export class BeakerCodeCell extends BeakerBaseCell implements nbformat.ICodeCell {
217
+ cell_type: "code" = "code";
218
+ outputs: nbformat.IOutput[] = [];
219
+ execution_count: nbformat.ExecutionCount = null;
220
+ declare metadata: Partial<nbformat.ICodeCellMetadata>;
221
+ last_execution?: BeakerCellExecutionStatus = {status: "none"};
222
+ busy?: boolean = false;
223
+
224
+ constructor(content: Partial<nbformat.ICell>) {
225
+ super({ ...content});
226
+ Object.assign(this, content)
227
+ }
228
+
229
+ public execute(session: BeakerSession, syntheticFuture?: BeakerCellFuture): IBeakerFuture | null {
230
+ this.busy = true;
231
+
232
+ var future: BeakerCellFuture | IShellFuture;
233
+ this.outputs.splice(0, this.outputs.length);
234
+ if (syntheticFuture !== undefined) {
235
+ future = syntheticFuture;
236
+ }
237
+ else {
238
+ future = session.sendBeakerMessage(
239
+ "execute_request",
240
+ {
241
+ code: this.source,
242
+ silent: false,
243
+ store_history: true,
244
+ user_expressions: {},
245
+ allow_stdin: true,
246
+ stop_on_error: false,
247
+ }
248
+ );
249
+ future.onIOPub = (msg: IBeakerIOPubMessage) => BeakerCellFutures.handleIOPub(msg, this);
250
+ future.onReply = (msg: IBeakerShellMessage) => BeakerCellFutures.handleReply(msg, this);
251
+ }
252
+ return future;
253
+ }
254
+
255
+ public rollback(session: BeakerSession): IBeakerFuture | null {
256
+ if (this?.last_execution?.status === "ok") {
257
+ const future = session.executeAction(
258
+ "rollback",
259
+ {
260
+ checkpoint_index: this.last_execution.checkpoint_index || null
261
+ }
262
+ );
263
+ // Treat rolling back like an execution as it may fail
264
+ this.busy = true;
265
+
266
+ // Clear outputs and children when rollback completes
267
+ future.done.then((reply_msg: messages.IExecuteReplyMsg) => {
268
+ if (reply_msg.content.status === "ok") {
269
+ this.busy = false;
270
+ this.last_execution = {"status": "none"};
271
+ this.execution_count = null;
272
+ this.outputs.splice(0, this.outputs.length);
273
+ this.children.splice(0, this.children.length);
274
+ }
275
+ else if (reply_msg.content.status === "error") {
276
+ this.last_execution = reply_msg.content;
277
+ this.outputs.push({...reply_msg.content, output_type: "error"})
278
+ }
279
+ });
280
+ return future;
281
+ }
282
+ return null;
283
+ };
284
+
285
+ public reset_execution_state(): void {
286
+ this.last_execution = {status: "modified"};
287
+ }
288
+
289
+ public toIPynb(): nbformat.ICodeCell {
290
+ const result: nbformat.ICodeCell = {
291
+ ...(super.toIPynb() as nbformat.IBaseCell),
292
+ cell_type: "code",
293
+ outputs: this.outputs.map(
294
+ (output) => {
295
+ return {
296
+ ...output,
297
+ transient: undefined,
298
+ }
299
+
300
+ }
301
+ ),
302
+ execution_count: this.execution_count,
303
+ };
304
+ return result;
305
+ }
306
+ }
307
+
308
+ export class BeakerMarkdownCell extends BeakerBaseCell implements nbformat.IMarkdownCell {
309
+ static IPYNB_KEYS = ["cell_type", "source", "metadata", "id", "attachments"];
310
+ cell_type: "markdown" = "markdown";
311
+ attachments?: nbformat.IAttachments;
312
+
313
+ constructor(content: Partial<nbformat.ICell>) {
314
+ super({ ...content});
315
+ Object.assign(this, content)
316
+ }
317
+
318
+
319
+ public execute(session: BeakerSession): IBeakerFuture | null {
320
+ // TODO: Replace this with code to render markdown.
321
+ return null;
322
+ };
323
+
324
+ public toIPynb(): nbformat.IBaseCell|nbformat.IBaseCell[] {
325
+ const output: JSONObject = {};
326
+ for (const key of Object.keys(this)) {
327
+ if (BeakerMarkdownCell.IPYNB_KEYS.includes(key)) {
328
+ output[key] = this[key];
329
+ }
330
+ }
331
+ return output as nbformat.IBaseCell;
332
+ }
333
+ }
334
+
335
+ export class BeakerQueryCell extends BeakerBaseCell implements IQueryCell {
336
+ cell_type: "query" = "query";
337
+ events: BeakerQueryEvent[] = [];
338
+ _current_input_request_message?: messages.IInputRequestMsg;
339
+ _toolCallIndex: Map<string, {eventIndex: number; slot: number}> = new Map();
340
+
341
+ constructor(content: Partial<nbformat.ICell>) {
342
+ super({cell_type: 'query', ...content});
343
+ Object.assign(this, content)
344
+ }
345
+
346
+ public execute(session: BeakerSession, includeNotebookState: boolean = true): IBeakerFuture | null {
347
+ this.events.splice(0, this.events.length);
348
+ this.children.splice(0, this.children.length);
349
+ this._toolCallIndex.clear();
350
+
351
+ const handleIOPub = async (msg: IBeakerIOPubMessage) => {
352
+ const msg_type = msg.header.msg_type;
353
+ const content = msg.content;
354
+ if (msg_type === "status") {
355
+ this.status = content.execution_state;
356
+ }
357
+ else if (msg_type === "llm_thought") {
358
+ const tool_calls: IBeakerToolCall[] = (content.tool_calls || []).map(
359
+ (tc: any) => ({
360
+ tool_call_id: tc.tool_call_id,
361
+ tool_name: tc.tool_name,
362
+ tool_input: tc.tool_input,
363
+ state: (tc.state as BeakerToolCallState) || "pending",
364
+ })
365
+ );
366
+ if (!content.thought && tool_calls.length === 0) {
367
+ return;
368
+ }
369
+ const event: IBeakerQueryThoughtEvent = {
370
+ type: "thought",
371
+ content: {
372
+ thought: content.thought || "",
373
+ thought_id: content.thought_id,
374
+ tool_calls,
375
+ }
376
+ };
377
+ this.events.push(event);
378
+ // Index by events-array position rather than by raw object reference.
379
+ // Reactive consumers (Vue) only observe mutations made through the
380
+ // proxy — i.e. through `this.events[idx]` — so we re-fetch the event
381
+ // via index when applying updates.
382
+ const eventIndex = this.events.length - 1;
383
+ tool_calls.forEach((tc, slot) => {
384
+ if (tc.tool_call_id) {
385
+ this._toolCallIndex.set(tc.tool_call_id, {eventIndex, slot});
386
+ }
387
+ });
388
+ }
389
+ else if (msg_type === "tool_call_update") {
390
+ const entry = this._toolCallIndex.get(content.tool_call_id);
391
+ if (entry === undefined) {
392
+ return;
393
+ }
394
+ const {eventIndex, slot} = entry;
395
+ const event = this.events[eventIndex] as IBeakerQueryThoughtEvent;
396
+ if (!event || event.type !== "thought") {
397
+ return;
398
+ }
399
+ const existing = event.content.tool_calls[slot];
400
+ const {tool_call_id: _id, ...updateFields} = content;
401
+ // Mutate via the proxy-wrapped event so Vue picks up the change.
402
+ event.content.tool_calls = [
403
+ ...event.content.tool_calls.slice(0, slot),
404
+ {...existing, ...updateFields},
405
+ ...event.content.tool_calls.slice(slot + 1),
406
+ ];
407
+ }
408
+ else if (msg_type === "llm_response" && content.name === "response_text") {
409
+ this.events.push({
410
+ type: "response",
411
+ content: content.text,
412
+ });
413
+ }
414
+ else if (msg_type === "code_cell") {
415
+ const nb = session.notebook;
416
+ const codeCell = new BeakerCodeCell({
417
+ cell_type: "code",
418
+ source: content.code,
419
+ metadata: {
420
+ parent_cell: this.id,
421
+ },
422
+ outputs: [],
423
+ });
424
+
425
+ const queryCellIndex = nb.cells.findIndex((cell) => (cell.id === this.id));
426
+ if (queryCellIndex >= 0) {
427
+ session.notebook.cells.splice(queryCellIndex + 1, 0, codeCell);
428
+ }
429
+ else {
430
+ nb.addCell(codeCell);
431
+ }
432
+ }
433
+ else if (msg_type === "add_child_codecell") {
434
+ const autoexecute = content.autoexecute;
435
+ const codeCell = new BeakerCodeCell({
436
+ cell_type: "code",
437
+ source: content.code,
438
+ metadata: {
439
+ parent_cell: this.id,
440
+ },
441
+ outputs: [],
442
+ busy: true,
443
+ });
444
+ codeCell.last_execution = {
445
+ status: "pending",
446
+ checkpoint_index: content.checkpoint_index,
447
+ }
448
+ // cell is stored in events - we point the children field to the same object
449
+ // to make selection and execution work
450
+ this.events.push({
451
+ type: "code_cell",
452
+ content: {
453
+ parent_id: this.id,
454
+ cell_id: codeCell.id,
455
+ metadata: {}
456
+ }
457
+ });
458
+ this.children.push(codeCell);
459
+
460
+ const reactiveCell = this.children[this.children.length - 1] as BeakerCodeCell;
461
+ if (autoexecute && content.execute_request_msg) {
462
+ const executeRequest = content.execute_request_msg;
463
+ const future = new BeakerCellFuture(
464
+ executeRequest, // Message that is presumably sent
465
+ true, // expectReply
466
+ true, // disposeOnDone
467
+ session.kernel, // kernel
468
+ reactiveCell, // relatedCodecell
469
+ );
470
+ }
471
+ }
472
+ else if (msg_type == "update_child_codecell") {
473
+ const cellId = content.id;
474
+ const cell = this.children.find((value) => (value.id === cellId));
475
+ if (cell === undefined || cell === null) {
476
+ console.log("Can't find cell");
477
+ throw Error("Cell to be updated not found ")
478
+ }
479
+
480
+ cell.execution_count = content.execution_count;
481
+ let status : BeakerCellExecutionStatus = {
482
+ status: content.execution_status
483
+ }
484
+
485
+ if (content.execution_status === "error") {
486
+ const error_details = {
487
+ ename: msg.content.ename,
488
+ evalue: msg.content.evalue,
489
+ traceback: msg.content.traceback,
490
+ };
491
+ cell.outputs.push({
492
+ output_type: "error",
493
+ ...error_details,
494
+ });
495
+ status = {...status, ...error_details};
496
+ }
497
+ else if (status.status === "ok") {
498
+ status = {...status, checkpoint_index: content.checkpoint_index};
499
+ cell.outputs.push({
500
+ output_type: "execute_result",
501
+ data: {
502
+ "text/plain": content.execution_return
503
+ }
504
+ });
505
+ }
506
+ cell.last_execution = status;
507
+ }
508
+ };
509
+
510
+ const handleStdin = async (msg: messages.IStdinMessage) => {
511
+ if (messages.isInputRequestMsg(msg)) {
512
+ this.events.push({
513
+ type: "user_question",
514
+ content: msg.content.prompt,
515
+ });
516
+ this.status = "awaiting_input";
517
+ this._current_input_request_message = msg;
518
+ }
519
+ }
520
+
521
+ const handleReply = async (msg: messages.IExecuteReplyMsg) => {
522
+ if (msg.content.status === "ok") {
523
+ this.last_execution = {
524
+ status: "ok",
525
+ };
526
+ }
527
+ else if (msg.content.status === "error") {
528
+ const error_details = {
529
+ ename: msg.content.ename,
530
+ evalue: msg.content.evalue,
531
+ traceback: msg.content.traceback,
532
+ };
533
+ this.last_execution = {
534
+ status: "error",
535
+ ...error_details
536
+ };
537
+ this.events.push({
538
+ type: "error",
539
+ content: {...msg.content}
540
+ });
541
+ }
542
+ else if (msg.content.status === "abort") {
543
+ this.last_execution = {
544
+ status: "abort",
545
+ };
546
+ this.events.push({
547
+ type: "abort",
548
+ content: "Request aborted",
549
+ });
550
+ }
551
+ };
552
+
553
+ var metadata: PartialJSONObject = {};
554
+
555
+ if (includeNotebookState) {
556
+ const notebookState = session.notebook.toIPynb();
557
+ notebookState.cells = notebookState.cells.filter(
558
+ // Skip this cell and any children of this cell
559
+ cell => cell.id !== this.id && cell.metadata?.parent_cell !== this.id
560
+ );
561
+ metadata["notebook_state"] = notebookState
562
+ }
563
+
564
+ const future = session.sendBeakerMessage(
565
+ "llm_request",
566
+ {
567
+ request: this.source
568
+ },
569
+ undefined,
570
+ metadata,
571
+ );
572
+ future.onIOPub = handleIOPub;
573
+ future.onStdin = handleStdin;
574
+ future.onReply = handleReply;
575
+ return future;
576
+ };
577
+
578
+ public respond(response: string, session: BeakerSession) {
579
+ // Only handle if we're awaiting input
580
+ if (this.status !== "awaiting_input" || !this._current_input_request_message) {
581
+ return;
582
+ }
583
+
584
+ this.events.push({
585
+ type: "user_answer",
586
+ content: response,
587
+ });
588
+
589
+ // Send the reply to the kernel
590
+ session.kernel?.sendInputReply(
591
+ {
592
+ "value": response,
593
+ "status": "ok",
594
+ },
595
+ this._current_input_request_message.header,
596
+ );
597
+
598
+ //cleanup
599
+ this.status = "busy";
600
+ this._current_input_request_message = undefined;
601
+ }
602
+
603
+ public toMultipleCells(): [BeakerMarkdownCell, ...BeakerBaseCell[]] {
604
+ // break apart a query cell into a series of cells so that code is interspersed with markdown cells
605
+ // and that in a jupyter notebook, loading and saving handles the redundant cells / query cell mapping
606
+
607
+ // Transient classes for organizing strings.
608
+ class JoinableMarkdown extends String {
609
+ public prefix: string = "";
610
+ public suffix: string = "";
611
+ }
612
+ class IndentedMarkdown extends JoinableMarkdown {
613
+ public prefix = `
614
+ <div style="display: flex; width: 100%;">
615
+ <div style="padding: 0.2rem; margin: 0.2rem; margin-left: 4rem;">\n\n`;
616
+ public suffix = `</div></div>\n\n`;
617
+ }
618
+ class AgentMarkdown extends JoinableMarkdown {
619
+ public prefix = `
620
+ ### Agent:
621
+ <div style="display: flex; width: 100%;">
622
+ <div style="padding: 0.2rem; margin: 0.2rem; margin-left: 4rem;">\n\n`;
623
+ public suffix = `</div></div>\n\n`;
624
+ }
625
+ class FinalResponseMarkdown extends AgentMarkdown {}
626
+ class UserMarkdown extends JoinableMarkdown {
627
+ public prefix = `
628
+ ### User:
629
+ <div style="display: flex; width: 100%;">
630
+ <div style="padding: 0.2rem; margin: 0.2rem; margin-left: 4rem;">\n\n`;
631
+ public suffix = `</div></div>\n\n`;
632
+ }
633
+
634
+ // only tag first cell with all metadata to reconstruct, and let the rest get dropped on fromIPynb
635
+ const parentMetadata = {
636
+ ...this.metadata,
637
+ beaker_cell_type: "query",
638
+ prompt: this.source,
639
+ events: this.events,
640
+ };
641
+
642
+ const eventElements: (JoinableMarkdown|IndentedMarkdown|BeakerBaseCell)[] = this.events.map((event) => {
643
+ if (event.type === "thought") {
644
+ const lines: string[] = [];
645
+ if (event.content.thought) {
646
+ lines.push(`${event.content.thought} `);
647
+ }
648
+ for (const tc of event.content.tool_calls || []) {
649
+ const argsBlob = JSON.stringify(tc.tool_input ?? {});
650
+ const stateLabel = tc.state ? ` [${tc.state}]` : "";
651
+ lines.push(`- \`${tc.tool_name}\`${stateLabel} — \`${argsBlob}\``);
652
+ if (tc.output_preview) {
653
+ const previewSuffix = tc.output_truncated ? "…" : "";
654
+ lines.push(` - output: \`${tc.output_preview}${previewSuffix}\``);
655
+ }
656
+ if (tc.error) {
657
+ lines.push(` - error: \`${tc.error.ename}: ${tc.error.evalue}\``);
658
+ }
659
+ }
660
+ return new AgentMarkdown(`${lines.join("\n")}\n`);
661
+ }
662
+ else if (event.type === "user_question") {
663
+ return new AgentMarkdown(`**Agent Question:** \n> ${event.content}\n`);
664
+ }
665
+ else if (event.type === "user_answer") {
666
+ return new UserMarkdown(`**User Response:** \n> ${event.content}\n`);
667
+ }
668
+ else if (event.type === "code_cell") {
669
+ const childCell = this.children.find(child => child.id === event.content.cell_id);
670
+ if (childCell === undefined) {
671
+ throw `Failed to find child cell ${event.content.cell_id} in children`
672
+ }
673
+ childCell.metadata.beaker_child_of = this.id;
674
+ childCell.metadata.beakerQueryCellChild = true;
675
+ return childCell;
676
+ }
677
+ else if (event.type === "response") {
678
+ var output: string;
679
+ if (typeof event.content === "string") {
680
+ const lines = event.content.split("\n")
681
+ .map(line => (/^\s*$/.test(line) ? "" : `${line}`))
682
+ // add an extra ## to shrink agent markdown output headers under notebook "agent" header
683
+ .map(line => /^#+\s+/.test(line) ? `###${line}` : line);
684
+ output = `\n${lines.join("\n")}`;
685
+ }
686
+ else {
687
+ output = `\n${event.content}`;
688
+ }
689
+ return new FinalResponseMarkdown(output);
690
+ }
691
+ else if (event.type === "abort") {
692
+ return new JoinableMarkdown(event.content);
693
+ }
694
+ else if (event.type === "error") {
695
+ return new BeakerMarkdownCell({
696
+ cell_type: 'markdown',
697
+ id: uuidv4(),
698
+ source: [JSON.stringify(event.content, undefined, 2)],
699
+ metadata: { ...parentMetadata, parentQueryCell: true, beakerQueryCellChild: true, },
700
+ })
701
+ }
702
+ else {
703
+ return new JoinableMarkdown(`Error: Unhandled query event type "${event.type}".`)
704
+ }
705
+ });
706
+
707
+ let outputElements: (nbformat.IBaseCell|JoinableMarkdown)[] = [
708
+ // new JoinableMarkdown('### User:'),
709
+ new UserMarkdown(`${this.source}`),
710
+ ...eventElements,
711
+ ];
712
+
713
+ let lastSeenElement = null;
714
+ let cell = new BeakerMarkdownCell(
715
+ {
716
+ cell_type: "markdown",
717
+ id: this.id,
718
+ source: [],
719
+ // contains all parent metadata
720
+ metadata: { ...parentMetadata, parentQueryCell: true },
721
+ }
722
+ );
723
+ const outputCells: IBeakerCell[] = [
724
+ cell,
725
+ ];
726
+ for (let element of outputElements) {
727
+ if (element instanceof JoinableMarkdown) {
728
+ if ((lastSeenElement === null && element.prefix)) {
729
+ if (element.prefix) {
730
+ (cell.source as string[]).push(element.prefix);
731
+ }
732
+ }
733
+ if ((lastSeenElement === null) || (lastSeenElement.constructor === element.constructor)) {
734
+ (cell.source as string[]).push(element as unknown as string)
735
+ }
736
+ else if (!(lastSeenElement instanceof JoinableMarkdown)) {
737
+ cell = new BeakerMarkdownCell({
738
+ cell_type: "markdown",
739
+ id: uuidv4(),
740
+ source: [],
741
+ metadata: { skipWhenLoading: true, beakerQueryCellChild: true },
742
+ })
743
+ if (element instanceof FinalResponseMarkdown) {
744
+ cell.metadata.finalResponse = true;
745
+ }
746
+ if (element.prefix) {
747
+ (cell.source as string[]).push(element.prefix);
748
+ }
749
+ (cell.source as string[]).push(element as unknown as string);
750
+ outputCells.push(cell);
751
+ }
752
+ else {
753
+ if (lastSeenElement.suffix) {
754
+ (cell.source as string[]).push(lastSeenElement.suffix);
755
+ }
756
+ if (element.prefix) {
757
+ (cell.source as string[]).push(element.prefix);
758
+ }
759
+ (cell.source as string[]).push(element as unknown as string);
760
+ }
761
+ }
762
+ else {
763
+ if (lastSeenElement instanceof JoinableMarkdown && lastSeenElement.suffix) {
764
+ (cell.source as string[]).push(lastSeenElement.suffix);
765
+ }
766
+ outputCells.push(element);
767
+ }
768
+ lastSeenElement = element;
769
+ }
770
+ return outputCells as [BeakerMarkdownCell, ...BeakerBaseCell[]];
771
+ }
772
+
773
+ // public fromJSON(obj: any) {
774
+ // if (this.metadata.beaker_cell_type !== "query") {
775
+ // throw TypeError("Cell is trying to be parsed as a query cell, but doesn't have the required metadata.")
776
+ // }
777
+ // this.source = obj.metadata.prompt;
778
+ // this.events = obj.metadata.events;
779
+ // }
780
+
781
+ // public toJSON() {
782
+ // return this.toMarkdownCell();
783
+ // }
784
+
785
+ public fromIPynb(obj: any) {
786
+ if (this.metadata.beaker_cell_type !== "query") {
787
+ throw TypeError("Cell is trying to be parsed as a query cell, but doesn't have the required metadata.")
788
+ }
789
+ this.source = obj.metadata.prompt;
790
+ this.events = obj.metadata.events;
791
+
792
+ }
793
+
794
+ public toIPynb(): [nbformat.IMarkdownCell, ...nbformat.IBaseCell[]] {
795
+ return this.toMultipleCells().flatMap(cell => cell.toIPynb()) as [nbformat.IMarkdownCell, ...nbformat.IBaseCell[]]
796
+ }
797
+ }
798
+
799
+ export type IBeakerCell = BeakerCodeCell | BeakerMarkdownCell | BeakerRawCell | nbformat.IUnrecognizedCell;
800
+
801
+ export class BeakerNotebookContent implements nbformat.INotebookContent {
802
+
803
+ [key: string]: PartialJSONValue;
804
+ nbformat: number = 4;
805
+ nbformat_minor: number = 5;
806
+ metadata: nbformat.INotebookMetadata = {};
807
+ cells: BeakerBaseCell[] = [];
808
+ }
809
+
810
+ export class BeakerNotebook {
811
+
812
+ constructor() {
813
+
814
+ this.content = {
815
+ nbformat: 4,
816
+ nbformat_minor: 5,
817
+ cells: [],
818
+ metadata: {
819
+ "kernelspec": {
820
+ "display_name": "Beaker Kernel",
821
+ "name": "beaker",
822
+ "language": "beaker",
823
+ },
824
+ "language_info": this.subkernelInfo,
825
+ },
826
+ }
827
+
828
+ // Using a Proxy to get around issues with reactivity in getters/setters
829
+ this.cells = new Proxy(this.content.cells, {});
830
+ }
831
+
832
+ public toJSON() {
833
+ this.content.metadata.language_info = this.subkernelInfo;
834
+ return {...this.content};
835
+ }
836
+
837
+ public fromJSON(obj: any) {
838
+ Object.keys(obj).forEach((key) => {
839
+ this.content[key] = obj[key];
840
+ })
841
+ this.cells = new Proxy(this.content.cells, {});
842
+ }
843
+
844
+ public loadFromIPynb(obj: any) {
845
+ this.content.nbformat = obj.nbformat;
846
+ this.content.nbformat_minor = obj.nbformat_minor;
847
+ const cellList = obj.cells
848
+ .map((cell: IBeakerCell) => {
849
+ if (cell.cell_type === "raw") {
850
+ return new BeakerRawCell(cell);
851
+ }
852
+ else if (cell.cell_type === "code") {
853
+ return new BeakerCodeCell(cell);
854
+ }
855
+ else if (cell.metadata.beaker_cell_type === "query") {
856
+ return new BeakerQueryCell({
857
+ cell_type: "query",
858
+ id: cell.id,
859
+ source: cell.metadata.prompt as nbformat.MultilineString,
860
+ events: cell.metadata.events,
861
+ metadata: cell.metadata,
862
+ });
863
+ }
864
+ else if (cell.cell_type === "markdown") {
865
+ if (cell.metadata?.skipWhenLoading === true) {
866
+ return;
867
+ }
868
+ return new BeakerMarkdownCell(cell);
869
+ }
870
+ else {
871
+ return new BeakerRawCell(cell);
872
+ }
873
+ })
874
+ .filter((cell: BeakerBaseCell | undefined) => cell);
875
+
876
+ let cellMap: {[uuid: string]: BeakerBaseCell} = {};
877
+ cellList.forEach((cell: BeakerBaseCell) => {
878
+ cellMap[cell.id] = cell;
879
+ });
880
+
881
+ let i = 0;
882
+ while (i < cellList.length) {
883
+ const cell = cellList[i];
884
+ if (typeof(cell.metadata.beaker_child_of) !== "undefined") {
885
+ cellMap[cell.metadata.beaker_child_of].children.push(cellList.splice(i, 1)[0]);
886
+ }
887
+ else {
888
+ i++;
889
+ }
890
+ }
891
+
892
+ this.cells.splice(
893
+ 0,
894
+ this.cells.length,
895
+ ...cellList
896
+ );
897
+ this.content.metadata = obj.metadata;
898
+ }
899
+
900
+ public toIPynb() {
901
+ this.content.metadata.language_info = this.subkernelInfo;
902
+ return {
903
+ nbformat: this.content.nbformat,
904
+ nbformat_minor: this.content.nbformat_minor,
905
+ cells: this.content.cells.flatMap((cell: BeakerBaseCell): nbformat.IBaseCell | nbformat.IBaseCell[] => cell.toIPynb()),
906
+ metadata: this.content.metadata,
907
+ }
908
+ }
909
+
910
+ public addCell(cell: IBeakerCell | nbformat.ICell) {
911
+ this.cells.push(cell);
912
+ }
913
+
914
+ public insertCell(cell: IBeakerCell | nbformat.ICell, position: number) {
915
+ this.cells.splice(position, 0, cell);
916
+ }
917
+
918
+ public removeCell(index: number) {
919
+ this.cells.splice(index, 1);
920
+ }
921
+
922
+ public cutCell(index: number) {
923
+ const removedValues = this.cells.splice(index, 1);
924
+ if (removedValues.length > 0) {
925
+ return removedValues[0];
926
+ }
927
+ return null;
928
+ }
929
+
930
+ public moveCell(fromIndex: number, toIndex: number) {
931
+ if (fromIndex !== toIndex) {
932
+ const cell = this.cutCell(fromIndex);
933
+ if (cell) {
934
+ this.insertCell(cell, toIndex);
935
+ }
936
+ }
937
+ }
938
+
939
+ public setSubkernelInfo(sessionInfo: any) {
940
+ this.subkernelInfo = {
941
+ "name": sessionInfo.subkernel,
942
+ "display_name": sessionInfo.language,
943
+ }
944
+ };
945
+
946
+ public content: BeakerNotebookContent;
947
+ public cells: IBeakerCell[];
948
+ private subkernelInfo?: nbformat.ILanguageInfoMetadata;
949
+ }