mszel commited on
Commit
993dfa7
·
2 Parent(s): d7a3caf b80a35a

Merge remote-tracking branch 'origin/main' into feature/image-search-new

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .github/workflows/test.yaml +1 -0
  2. Dockerfile +6 -2
  3. biome.json +5 -0
  4. examples/{AIMO → AIMO.lynxkite.json} +0 -0
  5. examples/{Airlines demo → Airlines demo.lynxkite.json} +0 -0
  6. examples/{Bio demo → Bio demo.lynxkite.json} +0 -0
  7. examples/{BioNeMo demo → BioNeMo demo.lynxkite.json} +0 -0
  8. examples/{LynxScribe RAG Chatbot.lynxkite.json → Generative drug screening.lynxkite.json} +520 -447
  9. examples/{Graph RAG → Graph RAG.lynxkite.json} +1 -9
  10. examples/{Image processing → Image processing.lynxkite.json} +0 -0
  11. examples/Model definition.lynxkite.json +614 -0
  12. examples/Model use.lynxkite.json +0 -0
  13. examples/{NetworkX demo → NetworkX demo.lynxkite.json} +0 -0
  14. examples/ODE-GNN experiment.lynxkite.json +466 -0
  15. examples/ODE-GNN.lynxkite.json +796 -0
  16. examples/{PyTorch demo → PyTorch demo.lynxkite.json} +0 -0
  17. examples/{RAG chatbot app → RAG chatbot app.lynxkite.json} +0 -0
  18. examples/Word2vec.lynxkite.json +0 -0
  19. examples/fake_data.py +16 -0
  20. examples/requirements.txt +1 -0
  21. examples/{sql → sql.lynxkite.json} +0 -0
  22. examples/uploads/plus-one-dataset.parquet +0 -0
  23. examples/word2vec.py +26 -0
  24. lynxkite-app/src/lynxkite_app/crdt.py +67 -18
  25. lynxkite-app/src/lynxkite_app/main.py +14 -6
  26. lynxkite-app/tests/test_main.py +16 -8
  27. lynxkite-app/web/eslint.config.js +1 -4
  28. lynxkite-app/web/index.html +0 -1
  29. lynxkite-app/web/package-lock.json +55 -0
  30. lynxkite-app/web/package.json +2 -0
  31. lynxkite-app/web/playwright.config.ts +2 -1
  32. lynxkite-app/web/src/Code.tsx +101 -0
  33. lynxkite-app/web/src/Directory.tsx +105 -108
  34. lynxkite-app/web/src/apiTypes.ts +4 -1
  35. lynxkite-app/web/src/code-theme.ts +38 -0
  36. lynxkite-app/web/src/index.css +39 -3
  37. lynxkite-app/web/src/main.tsx +2 -0
  38. lynxkite-app/web/src/workspace/NodeSearch.tsx +2 -7
  39. lynxkite-app/web/src/workspace/Workspace.tsx +20 -42
  40. lynxkite-app/web/src/workspace/nodes/GraphCreationNode.tsx +4 -16
  41. lynxkite-app/web/src/workspace/nodes/LynxKiteNode.tsx +6 -19
  42. lynxkite-app/web/src/workspace/nodes/NodeGroupParameter.tsx +5 -6
  43. lynxkite-app/web/src/workspace/nodes/NodeParameter.tsx +199 -19
  44. lynxkite-app/web/src/workspace/nodes/NodeWithImage.tsx +1 -3
  45. lynxkite-app/web/src/workspace/nodes/NodeWithParams.tsx +6 -11
  46. lynxkite-app/web/src/workspace/nodes/NodeWithTableView.tsx +22 -18
  47. lynxkite-app/web/src/workspace/nodes/NodeWithVisualization.tsx +4 -0
  48. lynxkite-app/web/tests/directory.spec.ts +0 -12
  49. lynxkite-app/web/tests/errors.spec.ts +1 -3
  50. lynxkite-app/web/tests/graph_creation.spec.ts +6 -15
.github/workflows/test.yaml CHANGED
@@ -81,6 +81,7 @@ jobs:
81
  - name: Run Playwright tests
82
  run: |
83
  cd lynxkite-app/web
 
84
  npm run test
85
 
86
  - uses: actions/upload-artifact@v4
 
81
  - name: Run Playwright tests
82
  run: |
83
  cd lynxkite-app/web
84
+ npm run build
85
  npm run test
86
 
87
  - uses: actions/upload-artifact@v4
Dockerfile CHANGED
@@ -5,12 +5,16 @@ USER node
5
  ENV HOME=/home/node PATH=/home/node/.local/bin:$PATH
6
  WORKDIR $HOME/app
7
  COPY --chown=node . $HOME/app
8
- RUN uv venv && uv pip install \
 
 
9
  -e lynxkite-core \
10
  -e lynxkite-app \
11
  -e lynxkite-graph-analytics \
12
  -e lynxkite-bio \
13
- -e lynxkite-pillow-example
 
 
14
  WORKDIR $HOME/app/examples
15
  ENV PORT=7860
16
  CMD ["uv", "run", "lynxkite"]
 
5
  ENV HOME=/home/node PATH=/home/node/.local/bin:$PATH
6
  WORKDIR $HOME/app
7
  COPY --chown=node . $HOME/app
8
+ ENV GIT_SSH_COMMAND="ssh -i /run/secrets/LYNXSCRIBE_DEPLOY_KEY -o StrictHostKeyChecking=no"
9
+ RUN --mount=type=secret,id=LYNXSCRIBE_DEPLOY_KEY,mode=0444,required=true \
10
+ uv venv && uv pip install \
11
  -e lynxkite-core \
12
  -e lynxkite-app \
13
  -e lynxkite-graph-analytics \
14
  -e lynxkite-bio \
15
+ -e lynxkite-lynxscribe \
16
+ -e lynxkite-pillow-example \
17
+ chromadb openai
18
  WORKDIR $HOME/app/examples
19
  ENV PORT=7860
20
  CMD ["uv", "run", "lynxkite"]
biome.json CHANGED
@@ -1,6 +1,10 @@
1
  {
 
 
 
2
  "formatter": {
3
  "ignore": ["**/node_modules/**", "**/dist/**"],
 
4
  "indentStyle": "space"
5
  },
6
  "linter": {
@@ -20,6 +24,7 @@
20
  "useKeyWithClickEvents": "off",
21
  "useValidAnchor": "off",
22
  "useButtonType": "off",
 
23
  "noNoninteractiveTabindex": "off"
24
  }
25
  }
 
1
  {
2
+ "files": {
3
+ "ignore": ["**/*.lynxkite.json"]
4
+ },
5
  "formatter": {
6
  "ignore": ["**/node_modules/**", "**/dist/**"],
7
+ "lineWidth": 100,
8
  "indentStyle": "space"
9
  },
10
  "linter": {
 
24
  "useKeyWithClickEvents": "off",
25
  "useValidAnchor": "off",
26
  "useButtonType": "off",
27
+ "noAutofocus": "off",
28
  "noNoninteractiveTabindex": "off"
29
  }
30
  }
examples/{AIMO → AIMO.lynxkite.json} RENAMED
File without changes
examples/{Airlines demo → Airlines demo.lynxkite.json} RENAMED
File without changes
examples/{Bio demo → Bio demo.lynxkite.json} RENAMED
File without changes
examples/{BioNeMo demo → BioNeMo demo.lynxkite.json} RENAMED
File without changes
examples/{LynxScribe RAG Chatbot.lynxkite.json → Generative drug screening.lynxkite.json} RENAMED
@@ -1,77 +1,56 @@
1
  {
2
  "edges": [
3
  {
4
- "id": "xy-edge__Truncate history 1output-Chat processor 1processor",
5
- "source": "Truncate history 1",
6
  "sourceHandle": "output",
7
- "target": "Chat processor 1",
8
- "targetHandle": "processor"
9
  },
10
  {
11
- "id": "xy-edge__Mask 1output-Chat processor 1processor",
12
- "source": "Mask 1",
13
  "sourceHandle": "output",
14
- "target": "Chat processor 1",
15
- "targetHandle": "processor"
16
  },
17
  {
18
- "id": "xy-edge__Mask 2output-Chat processor 1processor",
19
- "source": "Mask 2",
20
  "sourceHandle": "output",
21
- "target": "Chat processor 1",
22
- "targetHandle": "processor"
23
  },
24
  {
25
- "id": "xy-edge__Input chat 1output-Test Chat API 2message",
26
- "source": "Input chat 1",
27
  "sourceHandle": "output",
28
- "target": "Test Chat API 2",
29
- "targetHandle": "message"
30
  },
31
  {
32
- "id": "xy-edge__Test Chat API 2output-View 1input",
33
- "source": "Test Chat API 2",
34
  "sourceHandle": "output",
35
- "target": "View 1",
36
- "targetHandle": "input"
37
  },
38
  {
39
- "id": "LynxScribe RAG Graph Chatbot Backend 1 Test Chat API 2",
40
- "source": "LynxScribe RAG Graph Chatbot Backend 1",
41
  "sourceHandle": "output",
42
- "target": "Test Chat API 2",
43
- "targetHandle": "chat_api"
44
  },
45
  {
46
- "id": "Chat processor 1 LynxScribe RAG Graph Chatbot Backend 1",
47
- "source": "Chat processor 1",
48
  "sourceHandle": "output",
49
- "target": "LynxScribe RAG Graph Chatbot Backend 1",
50
- "targetHandle": "chat_processor"
51
- },
52
- {
53
- "id": "Cloud-sourced File Listing 1 LynxScribe Text RAG Loader 1",
54
- "source": "Cloud-sourced File Listing 1",
55
- "sourceHandle": "output",
56
- "target": "LynxScribe Text RAG Loader 1",
57
- "targetHandle": "file_urls"
58
- },
59
- {
60
- "id": "LynxScribe Text RAG Loader 1 LynxScribe RAG Graph Chatbot Builder 1",
61
- "source": "LynxScribe Text RAG Loader 1",
62
- "sourceHandle": "output",
63
- "target": "LynxScribe RAG Graph Chatbot Builder 1",
64
- "targetHandle": "rag_graph"
65
- },
66
- {
67
- "id": "LynxScribe RAG Graph Chatbot Builder 1 LynxScribe RAG Graph Chatbot Backend 1",
68
- "source": "LynxScribe RAG Graph Chatbot Builder 1",
69
- "sourceHandle": "output",
70
- "target": "LynxScribe RAG Graph Chatbot Backend 1",
71
- "targetHandle": "knowledge_base"
72
  }
73
  ],
74
- "env": "LynxScribe",
75
  "nodes": [
76
  {
77
  "data": {
@@ -81,7 +60,7 @@
81
  "error": null,
82
  "meta": {
83
  "inputs": {},
84
- "name": "Input chat",
85
  "outputs": {
86
  "output": {
87
  "name": "output",
@@ -92,9 +71,62 @@
92
  }
93
  },
94
  "params": {
95
- "chat": {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  "default": null,
97
- "name": "chat",
98
  "type": {
99
  "type": "<class 'str'>"
100
  }
@@ -103,635 +135,636 @@
103
  "type": "basic"
104
  },
105
  "params": {
106
- "chat": "Wgo is Gabor?"
 
 
 
 
107
  },
108
  "status": "done",
109
- "title": "Input chat"
110
  },
111
  "dragHandle": ".bg-primary",
112
- "height": 186.0,
113
- "id": "Input chat 1",
114
- "parentId": null,
115
  "position": {
116
- "x": -2606.8829929570456,
117
- "y": -648.2654341415332
118
  },
119
  "type": "basic",
120
- "width": 259.0
121
- },
122
- {
123
- "data": {
124
- "display": {
125
- "dataframes": {
126
- "df": {
127
- "columns": ["answer"],
128
- "data": [
129
- [
130
- "Lynx Analytics has two notable professionals named G\u00e1bor. Could you please specify which G\u00e1bor you are inquiring about?\n\n- **G\u00e1bor Benedek**: Chief Innovation Officer & Co-founder at Lynx Analytics. He specializes in economic and business simulations, social network analysis, data mining, and predictive analytics. He has an academic background as a former Associate Professor at Corvinus University of Budapest and has founded several data-related companies.\n\n- **G\u00e1bor Kriv\u00e1chy**: Country Manager at Lynx Analytics in Hungary. He is an experienced technology executive with a background in system implementation, integration, and project management, particularly in SAP implementations.\n\nLet me know which G\u00e1bor's details you would like to learn more about!"
131
- ]
132
- ]
133
- }
134
- }
135
- },
136
- "error": null,
137
- "meta": {
138
- "inputs": {
139
- "input": {
140
- "name": "input",
141
- "position": "left",
142
- "type": {
143
- "type": "<class 'inspect._empty'>"
144
- }
145
- }
146
- },
147
- "name": "View",
148
- "outputs": {},
149
- "params": {},
150
- "type": "table_view"
151
- },
152
- "params": {},
153
- "status": "done",
154
- "title": "View"
155
- },
156
- "dragHandle": ".bg-primary",
157
- "height": 950.0,
158
- "id": "View 1",
159
- "parentId": null,
160
- "position": {
161
- "x": -754.9225960536905,
162
- "y": -643.161064357758
163
- },
164
- "type": "table_view",
165
- "width": 1256.0
166
  },
167
  {
168
  "data": {
 
 
169
  "display": null,
170
  "error": null,
171
  "meta": {
172
  "inputs": {},
173
- "name": "Truncate history",
174
  "outputs": {
175
  "output": {
176
  "name": "output",
177
- "position": "top",
178
  "type": {
179
  "type": "None"
180
  }
181
  }
182
  },
183
  "params": {
184
- "max_tokens": {
185
- "default": 10000.0,
186
- "name": "max_tokens",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  "type": {
188
- "type": "<class 'int'>"
 
 
 
 
 
 
 
189
  }
190
  }
191
  },
192
  "type": "basic"
193
  },
194
  "params": {
195
- "max_tokens": 10000.0
 
 
 
 
196
  },
197
  "status": "done",
198
- "title": "Truncate history"
199
  },
200
  "dragHandle": ".bg-primary",
201
- "height": 200.0,
202
- "id": "Truncate history 1",
203
- "parentId": null,
204
  "position": {
205
- "x": -1536.508533731351,
206
- "y": 728.1204075546109
207
  },
208
  "type": "basic",
209
- "width": 200.0
210
  },
211
  {
212
  "data": {
213
- "__execution_delay": null,
214
- "collapsed": false,
215
  "display": null,
216
  "error": null,
217
  "meta": {
218
  "inputs": {
219
- "processor": {
220
- "name": "processor",
221
- "position": "bottom",
222
- "type": {
223
- "type": "<class 'inspect._empty'>"
224
- }
225
- }
226
- },
227
- "name": "Chat processor",
228
- "outputs": {
229
- "output": {
230
- "name": "output",
231
- "position": "top",
232
  "type": {
233
- "type": "None"
234
  }
235
  }
236
  },
237
- "params": {},
238
- "type": "basic"
239
- },
240
- "params": {},
241
- "status": "done",
242
- "title": "Chat processor"
243
- },
244
- "dragHandle": ".bg-primary",
245
- "height": 89.0,
246
- "id": "Chat processor 1",
247
- "parentId": null,
248
- "position": {
249
- "x": -1527.1027075359414,
250
- "y": 605.2129408898476
251
- },
252
- "type": "basic",
253
- "width": 416.0
254
- },
255
- {
256
- "data": {
257
- "__execution_delay": 0.0,
258
- "collapsed": null,
259
- "display": null,
260
- "error": null,
261
- "meta": {
262
- "inputs": {},
263
- "name": "Mask",
264
  "outputs": {
265
  "output": {
266
  "name": "output",
267
- "position": "top",
268
  "type": {
269
  "type": "None"
270
  }
271
  }
272
  },
273
  "params": {
274
- "exceptions": {
275
- "default": "",
276
- "name": "exceptions",
277
  "type": {
278
  "type": "<class 'str'>"
279
  }
280
  },
281
- "mask_pattern": {
282
- "default": "",
283
- "name": "mask_pattern",
284
  "type": {
285
  "type": "<class 'str'>"
286
  }
287
  },
288
- "name": {
289
- "default": "",
290
- "name": "name",
291
  "type": {
292
- "type": "<class 'str'>"
293
  }
294
  },
295
- "regex": {
296
- "default": "",
297
- "name": "regex",
 
 
 
 
 
 
 
298
  "type": {
299
  "type": "<class 'str'>"
300
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  }
302
  },
 
 
 
 
303
  "type": "basic"
304
  },
305
  "params": {
306
307
- "mask_pattern": "masked_email_address_{}",
308
- "name": "email",
309
- "regex": "([a-z0-9!#$%&'*+\\/=?^_`{|.}~-]+@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)"
 
 
 
310
  },
311
  "status": "done",
312
- "title": "Mask"
313
  },
314
  "dragHandle": ".bg-primary",
315
- "height": 358.0,
316
- "id": "Mask 1",
317
- "parentId": null,
318
  "position": {
319
- "x": -1309.5065330408577,
320
- "y": 731.6791509394458
321
  },
322
  "type": "basic",
323
- "width": 313.0
324
  },
325
  {
326
  "data": {
327
- "__execution_delay": 0.0,
328
  "collapsed": null,
329
  "display": null,
330
  "error": null,
331
  "meta": {
332
- "inputs": {},
333
- "name": "Mask",
 
 
 
 
 
 
 
 
334
  "outputs": {
335
  "output": {
336
  "name": "output",
337
- "position": "top",
338
  "type": {
339
  "type": "None"
340
  }
341
  }
342
  },
343
  "params": {
344
- "exceptions": {
345
- "default": "",
346
- "name": "exceptions",
347
  "type": {
348
  "type": "<class 'str'>"
349
  }
350
  },
351
- "mask_pattern": {
352
- "default": "",
353
- "name": "mask_pattern",
354
  "type": {
355
- "type": "<class 'str'>"
356
  }
357
  },
358
- "name": {
359
- "default": "",
360
- "name": "name",
361
  "type": {
362
- "type": "<class 'str'>"
363
  }
364
  },
365
- "regex": {
366
- "default": "",
367
- "name": "regex",
 
 
 
368
  "type": {
369
- "type": "<class 'str'>"
370
  }
371
- }
372
- },
373
- "type": "basic"
374
- },
375
- "params": {
376
- "exceptions": "",
377
- "mask_pattern": "masked_credit_card_number_{}",
378
- "name": "credit_card",
379
- "regex": "((?:(?:\\\\d{4}[- ]?){3}\\\\d{4}|\\\\d{15,16}))(?![\\\\d])"
380
- },
381
- "status": "done",
382
- "title": "Mask"
383
- },
384
- "dragHandle": ".bg-primary",
385
- "height": 358.0,
386
- "id": "Mask 2",
387
- "parentId": null,
388
- "position": {
389
- "x": -983.2612912523697,
390
- "y": 731.5859900002104
391
- },
392
- "type": "basic",
393
- "width": 315.0
394
- },
395
- {
396
- "data": {
397
- "__execution_delay": 0.0,
398
- "collapsed": false,
399
- "display": null,
400
- "error": null,
401
- "meta": {
402
- "inputs": {
403
- "chat_api": {
404
- "name": "chat_api",
405
- "position": "bottom",
406
  "type": {
407
- "type": "<class 'inspect._empty'>"
408
  }
409
  },
410
- "message": {
411
- "name": "message",
412
- "position": "left",
413
  "type": {
414
- "type": "<class 'inspect._empty'>"
415
  }
416
- }
417
- },
418
- "name": "Test Chat API",
419
- "outputs": {
420
- "output": {
421
- "name": "output",
422
- "position": "right",
423
  "type": {
424
- "type": "None"
 
 
 
425
  }
426
  }
427
  },
428
- "params": {
429
- "show_details": {
430
- "default": false,
431
- "name": "show_details",
432
- "type": {
433
- "type": "<class 'bool'>"
434
- }
435
- }
436
  },
437
  "type": "basic"
438
  },
439
- "params": {},
 
 
 
 
 
 
 
 
 
 
 
440
  "status": "done",
441
- "title": "Test Chat API"
442
  },
443
  "dragHandle": ".bg-primary",
444
- "height": 201.0,
445
- "id": "Test Chat API 2",
446
- "parentId": null,
447
  "position": {
448
- "x": -2024.044443214723,
449
- "y": -654.8412606520155
450
  },
451
  "type": "basic",
452
- "width": 906.0
453
  },
454
  {
455
  "data": {
456
- "__execution_delay": 0.0,
457
- "collapsed": null,
458
  "display": null,
459
  "error": null,
460
  "meta": {
461
  "inputs": {
462
- "chat_processor": {
463
- "name": "chat_processor",
464
- "position": "bottom",
465
  "type": {
466
- "type": "<class 'inspect._empty'>"
467
  }
468
  },
469
- "knowledge_base": {
470
- "name": "knowledge_base",
471
- "position": "bottom",
472
  "type": {
473
- "type": "<class 'inspect._empty'>"
474
  }
475
  }
476
  },
477
- "name": "LynxScribe RAG Graph Chatbot Backend",
478
  "outputs": {
479
  "output": {
480
  "name": "output",
481
- "position": "top",
482
  "type": {
483
  "type": "None"
484
  }
485
  }
486
  },
487
  "params": {
488
- "llm_interface": {
489
- "default": "openai",
490
- "name": "llm_interface",
491
- "type": {
492
- "type": "<class 'str'>"
493
- }
494
- },
495
- "llm_model_name": {
496
- "default": "gpt-4o",
497
- "name": "llm_model_name",
498
  "type": {
499
  "type": "<class 'str'>"
500
  }
501
  },
502
- "negative_answer": {
503
- "default": "I'm sorry, but the data I've been trained on does not contain any information related to your question.",
504
- "name": "negative_answer",
505
  "type": {
506
  "type": "<class 'str'>"
507
  }
508
  },
509
- "retriever_limits_by_type": {
510
- "default": "{}",
511
- "name": "retriever_limits_by_type",
512
  "type": {
513
- "type": "<class 'str'>"
514
  }
515
  },
516
- "retriever_max_iterations": {
517
- "default": 3.0,
518
- "name": "retriever_max_iterations",
519
  "type": {
520
  "type": "<class 'int'>"
521
  }
522
  },
523
- "retriever_overall_chunk_limit": {
524
- "default": 20.0,
525
- "name": "retriever_overall_chunk_limit",
526
  "type": {
527
- "type": "<class 'int'>"
528
  }
529
  },
530
- "retriever_overall_token_limit": {
531
- "default": 3000.0,
532
- "name": "retriever_overall_token_limit",
533
  "type": {
534
- "type": "<class 'int'>"
535
  }
536
  },
537
- "retriever_strict_limits": {
538
- "default": true,
539
- "name": "retriever_strict_limits",
540
  "type": {
541
- "type": "<class 'bool'>"
542
  }
543
  }
544
  },
545
  "position": {
546
- "x": 543.0,
547
- "y": 256.0
548
  },
549
  "type": "basic"
550
  },
551
  "params": {
552
- "llm_interface": "openai",
553
- "llm_model_name": "gpt-4o",
554
- "negative_answer": "I'm sorry, but the data I've been trained on does not contain any information related to your question.",
555
- "retriever_limits_by_type": "{\"information\": [1, 5], \"summary\": [0, 2], \"template_qna\": [1, 3], \"QnA question\": [0, 0]}",
556
- "retriever_max_iterations": 3.0,
557
- "retriever_overall_chunk_limit": "20",
558
- "retriever_overall_token_limit": 3000.0,
559
- "retriever_strict_limits": true
560
  },
561
  "status": "done",
562
- "title": "LynxScribe RAG Graph Chatbot Backend"
563
  },
564
  "dragHandle": ".bg-primary",
565
- "height": 556.0,
566
- "id": "LynxScribe RAG Graph Chatbot Backend 1",
567
  "position": {
568
- "x": -2020.0,
569
- "y": -188.33333333333334
570
  },
571
  "type": "basic",
572
- "width": 903.0
573
  },
574
  {
575
  "data": {
576
- "__execution_delay": 0.0,
577
- "collapsed": null,
578
- "display": null,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
579
  "error": null,
580
  "meta": {
581
  "inputs": {
582
- "file_urls": {
583
- "name": "file_urls",
584
  "position": "left",
585
  "type": {
586
- "type": "<class 'inspect._empty'>"
587
- }
588
- }
589
- },
590
- "name": "LynxScribe Text RAG Loader",
591
- "outputs": {
592
- "output": {
593
- "name": "output",
594
- "position": "right",
595
- "type": {
596
- "type": "None"
597
  }
598
  }
599
  },
 
 
600
  "params": {
601
- "input_type": {
602
- "default": "v1",
603
- "name": "input_type",
604
- "type": {
605
- "enum": ["V1", "V2"]
606
- }
607
- },
608
- "text_embedder_interface": {
609
- "default": "openai",
610
- "name": "text_embedder_interface",
611
  "type": {
612
  "type": "<class 'str'>"
613
  }
614
  },
615
- "text_embedder_model_name_or_path": {
616
- "default": "text-embedding-3-large",
617
- "name": "text_embedder_model_name_or_path",
618
- "type": {
619
- "type": "<class 'str'>"
620
- }
621
- },
622
- "vdb_collection_name": {
623
- "default": "lynx",
624
- "name": "vdb_collection_name",
625
  "type": {
626
  "type": "<class 'str'>"
627
  }
628
  },
629
- "vdb_num_dimensions": {
630
- "default": 3072.0,
631
- "name": "vdb_num_dimensions",
632
- "type": {
633
- "type": "<class 'int'>"
634
- }
635
- },
636
- "vdb_provider_name": {
637
- "default": "faiss",
638
- "name": "vdb_provider_name",
639
  "type": {
640
  "type": "<class 'str'>"
641
  }
642
  }
643
  },
644
  "position": {
645
- "x": 870.0,
646
- "y": 926.0
647
  },
648
- "type": "basic"
649
  },
650
  "params": {
651
- "input_type": "V1",
652
- "text_embedder_interface": "openai",
653
- "text_embedder_model_name_or_path": "text-embedding-ada-002",
654
- "vdb_collection_name": "lynx",
655
- "vdb_num_dimensions": "1536",
656
- "vdb_provider_name": "faiss"
657
  },
658
  "status": "done",
659
- "title": "LynxScribe Text RAG Loader"
660
  },
661
  "dragHandle": ".bg-primary",
662
- "height": 520.0,
663
- "id": "LynxScribe Text RAG Loader 1",
664
  "position": {
665
- "x": -2980.4063452955706,
666
- "y": 787.1039827859594
667
  },
668
- "type": "basic",
669
- "width": 318.0
670
  },
671
  {
672
  "data": {
673
- "__execution_delay": 0.0,
674
- "collapsed": null,
675
- "display": null,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
676
  "error": null,
677
  "meta": {
678
- "inputs": {},
679
- "name": "Cloud-sourced File Listing",
680
- "outputs": {
681
- "output": {
682
- "name": "output",
683
- "position": "right",
684
  "type": {
685
- "type": "None"
686
  }
687
  }
688
  },
 
 
689
  "params": {
690
- "accepted_file_types": {
691
- "default": ".jpg, .jpeg, .png",
692
- "name": "accepted_file_types",
693
  "type": {
694
  "type": "<class 'str'>"
695
  }
696
  },
697
- "cloud_provider": {
698
- "default": "gcp",
699
- "name": "cloud_provider",
700
  "type": {
701
- "enum": ["GCP", "AWS", "AZURE"]
702
  }
703
  },
704
- "folder_URL": {
705
- "default": "https://storage.googleapis.com/lynxkite_public_data/lynxscribe-images/image-rag-test",
706
- "name": "folder_URL",
707
  "type": {
708
  "type": "<class 'str'>"
709
  }
710
  }
711
  },
712
  "position": {
713
- "x": 451.0,
714
- "y": 505.0
715
  },
716
- "type": "basic"
717
  },
718
  "params": {
719
- "accepted_file_types": ".pickle",
720
- "cloud_provider": "GCP",
721
- "folder_URL": "https://storage.googleapis.com/lynxkite_public_data/lynxscribe-knowledge-graphs/lynx-chatbot"
722
  },
723
  "status": "done",
724
- "title": "Cloud-sourced File Listing"
725
  },
726
  "dragHandle": ".bg-primary",
727
- "height": 324.0,
728
- "id": "Cloud-sourced File Listing 1",
729
  "position": {
730
- "x": -3827.1644268005352,
731
- "y": 883.7859821532916
732
  },
733
- "type": "basic",
734
- "width": 613.0
735
  },
736
  {
737
  "data": {
@@ -739,62 +772,102 @@
739
  "error": null,
740
  "meta": {
741
  "inputs": {
742
- "rag_graph": {
743
- "name": "rag_graph",
744
  "position": "left",
745
  "type": {
746
- "type": "<class 'inspect._empty'>"
747
  }
748
  }
749
  },
750
- "name": "LynxScribe RAG Graph Chatbot Builder",
751
  "outputs": {
752
  "output": {
753
  "name": "output",
754
- "position": "top",
755
  "type": {
756
  "type": "None"
757
  }
758
  }
759
  },
760
  "params": {
761
- "node_types": {
762
- "default": "intent_cluster",
763
- "name": "node_types",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
764
  "type": {
765
  "type": "<class 'str'>"
766
  }
767
  },
768
- "scenario_file": {
769
- "default": "uploads/lynx_chatbot_scenario_selector.yaml",
770
- "name": "scenario_file",
 
 
 
 
 
 
 
771
  "type": {
772
  "type": "<class 'str'>"
773
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
774
  }
775
  },
776
  "position": {
777
- "x": 1314.0,
778
- "y": 1003.0
779
  },
780
  "type": "basic"
781
  },
782
  "params": {
783
- "node_types": "intent_cluster",
784
- "scenario_file": "uploads/lynx_chatbot_scenario_selector.yaml"
 
 
 
 
 
785
  },
786
  "status": "done",
787
- "title": "LynxScribe RAG Graph Chatbot Builder"
788
  },
789
  "dragHandle": ".bg-primary",
790
- "height": 238.0,
791
- "id": "LynxScribe RAG Graph Chatbot Builder 1",
792
  "position": {
793
- "x": -2453.755433834285,
794
- "y": 927.5600547745715
795
  },
796
  "type": "basic",
797
- "width": 448.0
798
  }
799
  ]
800
  }
 
1
  {
2
  "edges": [
3
  {
4
+ "id": "Import file 2 Query GenMol 1",
5
+ "source": "Import file 2",
6
  "sourceHandle": "output",
7
+ "target": "Query GenMol 1",
8
+ "targetHandle": "bundle"
9
  },
10
  {
11
+ "id": "Import file 1 MSA-search 1",
12
+ "source": "Import file 1",
13
  "sourceHandle": "output",
14
+ "target": "MSA-search 1",
15
+ "targetHandle": "bundle"
16
  },
17
  {
18
+ "id": "Query GenMol 1 Query DiffDock 1",
19
+ "source": "Query GenMol 1",
20
  "sourceHandle": "output",
21
+ "target": "Query DiffDock 1",
22
+ "targetHandle": "ligands"
23
  },
24
  {
25
+ "id": "Query DiffDock 1 View molecules 1",
26
+ "source": "Query DiffDock 1",
27
  "sourceHandle": "output",
28
+ "target": "View molecules 1",
29
+ "targetHandle": "bundle"
30
  },
31
  {
32
+ "id": "MSA-search 1 Query OpenFold2 1",
33
+ "source": "MSA-search 1",
34
  "sourceHandle": "output",
35
+ "target": "Query OpenFold2 1",
36
+ "targetHandle": "bundle"
37
  },
38
  {
39
+ "id": "Query OpenFold2 1 View molecules 3",
40
+ "source": "Query OpenFold2 1",
41
  "sourceHandle": "output",
42
+ "target": "View molecules 3",
43
+ "targetHandle": "bundle"
44
  },
45
  {
46
+ "id": "Query OpenFold2 1 Query DiffDock 1",
47
+ "source": "Query OpenFold2 1",
48
  "sourceHandle": "output",
49
+ "target": "Query DiffDock 1",
50
+ "targetHandle": "proteins"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  }
52
  ],
53
+ "env": "LynxKite Graph Analytics",
54
  "nodes": [
55
  {
56
  "data": {
 
60
  "error": null,
61
  "meta": {
62
  "inputs": {},
63
+ "name": "Import file",
64
  "outputs": {
65
  "output": {
66
  "name": "output",
 
71
  }
72
  },
73
  "params": {
74
+ "file_format": {
75
+ "default": "csv",
76
+ "groups": {
77
+ "csv": [
78
+ {
79
+ "default": "<from file>",
80
+ "name": "columns",
81
+ "type": {
82
+ "type": "<class 'str'>"
83
+ }
84
+ },
85
+ {
86
+ "default": "<auto>",
87
+ "name": "separator",
88
+ "type": {
89
+ "type": "<class 'str'>"
90
+ }
91
+ }
92
+ ],
93
+ "excel": [
94
+ {
95
+ "default": "Sheet1",
96
+ "name": "sheet_name",
97
+ "type": {
98
+ "type": "<class 'str'>"
99
+ }
100
+ }
101
+ ],
102
+ "json": [],
103
+ "parquet": []
104
+ },
105
+ "name": "file_format",
106
+ "selector": {
107
+ "default": "csv",
108
+ "name": "file_format",
109
+ "type": {
110
+ "enum": [
111
+ "csv",
112
+ "parquet",
113
+ "json",
114
+ "excel"
115
+ ]
116
+ }
117
+ },
118
+ "type": "group"
119
+ },
120
+ "file_path": {
121
+ "default": null,
122
+ "name": "file_path",
123
+ "type": {
124
+ "type": "<class 'str'>"
125
+ }
126
+ },
127
+ "table_name": {
128
  "default": null,
129
+ "name": "table_name",
130
  "type": {
131
  "type": "<class 'str'>"
132
  }
 
135
  "type": "basic"
136
  },
137
  "params": {
138
+ "columns": "<from file>",
139
+ "file_format": "csv",
140
+ "file_path": "uploads/protein.csv",
141
+ "separator": "<auto>",
142
+ "table_name": ""
143
  },
144
  "status": "done",
145
+ "title": "Import file"
146
  },
147
  "dragHandle": ".bg-primary",
148
+ "height": 487.0,
149
+ "id": "Import file 1",
 
150
  "position": {
151
+ "x": -755.0582906538923,
152
+ "y": 543.770372030674
153
  },
154
  "type": "basic",
155
+ "width": 439.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  },
157
  {
158
  "data": {
159
+ "__execution_delay": 0.0,
160
+ "collapsed": null,
161
  "display": null,
162
  "error": null,
163
  "meta": {
164
  "inputs": {},
165
+ "name": "Import file",
166
  "outputs": {
167
  "output": {
168
  "name": "output",
169
+ "position": "right",
170
  "type": {
171
  "type": "None"
172
  }
173
  }
174
  },
175
  "params": {
176
+ "file_format": {
177
+ "default": "csv",
178
+ "groups": {
179
+ "csv": [
180
+ {
181
+ "default": "<from file>",
182
+ "name": "columns",
183
+ "type": {
184
+ "type": "<class 'str'>"
185
+ }
186
+ },
187
+ {
188
+ "default": "<auto>",
189
+ "name": "separator",
190
+ "type": {
191
+ "type": "<class 'str'>"
192
+ }
193
+ }
194
+ ],
195
+ "excel": [
196
+ {
197
+ "default": "Sheet1",
198
+ "name": "sheet_name",
199
+ "type": {
200
+ "type": "<class 'str'>"
201
+ }
202
+ }
203
+ ],
204
+ "json": [],
205
+ "parquet": []
206
+ },
207
+ "name": "file_format",
208
+ "selector": {
209
+ "default": "csv",
210
+ "name": "file_format",
211
+ "type": {
212
+ "enum": [
213
+ "csv",
214
+ "parquet",
215
+ "json",
216
+ "excel"
217
+ ]
218
+ }
219
+ },
220
+ "type": "group"
221
+ },
222
+ "file_path": {
223
+ "default": null,
224
+ "name": "file_path",
225
  "type": {
226
+ "type": "<class 'str'>"
227
+ }
228
+ },
229
+ "table_name": {
230
+ "default": null,
231
+ "name": "table_name",
232
+ "type": {
233
+ "type": "<class 'str'>"
234
  }
235
  }
236
  },
237
  "type": "basic"
238
  },
239
  "params": {
240
+ "columns": "<from file>",
241
+ "file_format": "csv",
242
+ "file_path": "uploads/molecules.csv",
243
+ "separator": "<auto>",
244
+ "table_name": null
245
  },
246
  "status": "done",
247
+ "title": "Import file"
248
  },
249
  "dragHandle": ".bg-primary",
250
+ "height": 436.0,
251
+ "id": "Import file 2",
 
252
  "position": {
253
+ "x": 62.887657256500006,
254
+ "y": 1380.6697994924546
255
  },
256
  "type": "basic",
257
+ "width": 311.0
258
  },
259
  {
260
  "data": {
 
 
261
  "display": null,
262
  "error": null,
263
  "meta": {
264
  "inputs": {
265
+ "bundle": {
266
+ "name": "bundle",
267
+ "position": "left",
 
 
 
 
 
 
 
 
 
 
268
  "type": {
269
+ "type": "<class 'lynxkite_graph_analytics.core.Bundle'>"
270
  }
271
  }
272
  },
273
+ "name": "Query GenMol",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  "outputs": {
275
  "output": {
276
  "name": "output",
277
+ "position": "right",
278
  "type": {
279
  "type": "None"
280
  }
281
  }
282
  },
283
  "params": {
284
+ "molecule_column": {
285
+ "default": null,
286
+ "name": "molecule_column",
287
  "type": {
288
  "type": "<class 'str'>"
289
  }
290
  },
291
+ "molecule_table": {
292
+ "default": null,
293
+ "name": "molecule_table",
294
  "type": {
295
  "type": "<class 'str'>"
296
  }
297
  },
298
+ "noise": {
299
+ "default": 0.2,
300
+ "name": "noise",
301
  "type": {
302
+ "type": "<class 'float'>"
303
  }
304
  },
305
+ "num_molecules": {
306
+ "default": 5.0,
307
+ "name": "num_molecules",
308
+ "type": {
309
+ "type": "<class 'int'>"
310
+ }
311
+ },
312
+ "scoring": {
313
+ "default": "QED",
314
+ "name": "scoring",
315
  "type": {
316
  "type": "<class 'str'>"
317
  }
318
+ },
319
+ "step_size": {
320
+ "default": 4.0,
321
+ "name": "step_size",
322
+ "type": {
323
+ "type": "<class 'int'>"
324
+ }
325
+ },
326
+ "temperature": {
327
+ "default": 1.0,
328
+ "name": "temperature",
329
+ "type": {
330
+ "type": "<class 'float'>"
331
+ }
332
  }
333
  },
334
+ "position": {
335
+ "x": 594.0,
336
+ "y": 633.0
337
+ },
338
  "type": "basic"
339
  },
340
  "params": {
341
+ "molecule_column": null,
342
+ "molecule_table": null,
343
+ "noise": 0.2,
344
+ "num_molecules": 5.0,
345
+ "scoring": "QED",
346
+ "step_size": 4.0,
347
+ "temperature": 1.0
348
  },
349
  "status": "done",
350
+ "title": "Query GenMol"
351
  },
352
  "dragHandle": ".bg-primary",
353
+ "height": 601.0,
354
+ "id": "Query GenMol 1",
 
355
  "position": {
356
+ "x": 663.3333333333335,
357
+ "y": 1283.3333333333335
358
  },
359
  "type": "basic",
360
+ "width": 358.0
361
  },
362
  {
363
  "data": {
 
364
  "collapsed": null,
365
  "display": null,
366
  "error": null,
367
  "meta": {
368
+ "inputs": {
369
+ "bundle": {
370
+ "name": "bundle",
371
+ "position": "left",
372
+ "type": {
373
+ "type": "<class 'lynxkite_graph_analytics.core.Bundle'>"
374
+ }
375
+ }
376
+ },
377
+ "name": "MSA-search",
378
  "outputs": {
379
  "output": {
380
  "name": "output",
381
+ "position": "right",
382
  "type": {
383
  "type": "None"
384
  }
385
  }
386
  },
387
  "params": {
388
+ "databases": {
389
+ "default": "[\"Uniref30_2302\", \"colabfold_envdb_202108\", \"PDB70_220313\"]",
390
+ "name": "databases",
391
  "type": {
392
  "type": "<class 'str'>"
393
  }
394
  },
395
+ "e_value": {
396
+ "default": 0.0001,
397
+ "name": "e_value",
398
  "type": {
399
+ "type": "<class 'float'>"
400
  }
401
  },
402
+ "iterations": {
403
+ "default": 1.0,
404
+ "name": "iterations",
405
  "type": {
406
+ "type": "<class 'int'>"
407
  }
408
  },
409
+ "output_alignment_formats": {
410
+ "default": [
411
+ "fasta",
412
+ "a3m"
413
+ ],
414
+ "name": "output_alignment_formats",
415
  "type": {
416
+ "type": "list[lynxkite_bio.nims.AlignmentFormats]"
417
  }
418
+ },
419
+ "protein_column": {
420
+ "default": null,
421
+ "name": "protein_column",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
  "type": {
423
+ "type": "<class 'str'>"
424
  }
425
  },
426
+ "protein_table": {
427
+ "default": null,
428
+ "name": "protein_table",
429
  "type": {
430
+ "type": "<class 'str'>"
431
  }
432
+ },
433
+ "search_type": {
434
+ "default": "ALPHAFOLD2",
435
+ "name": "search_type",
 
 
 
436
  "type": {
437
+ "enum": [
438
+ "ALPHAFOLD2",
439
+ "ESM2"
440
+ ]
441
  }
442
  }
443
  },
444
+ "position": {
445
+ "x": 576.0,
446
+ "y": 228.0
 
 
 
 
 
447
  },
448
  "type": "basic"
449
  },
450
+ "params": {
451
+ "databases": "[\"Uniref30_2302\", \"colabfold_envdb_202108\", \"PDB70_220313\"]",
452
+ "e_value": 0.0001,
453
+ "iterations": 1.0,
454
+ "output_alignment_formats": [
455
+ "fasta",
456
+ "a3m"
457
+ ],
458
+ "protein_column": null,
459
+ "protein_table": null,
460
+ "search_type": "ALPHAFOLD2"
461
+ },
462
  "status": "done",
463
+ "title": "MSA-search"
464
  },
465
  "dragHandle": ".bg-primary",
466
+ "height": 550.0,
467
+ "id": "MSA-search 1",
 
468
  "position": {
469
+ "x": -45.0,
470
+ "y": 570.0
471
  },
472
  "type": "basic",
473
+ "width": 531.0
474
  },
475
  {
476
  "data": {
 
 
477
  "display": null,
478
  "error": null,
479
  "meta": {
480
  "inputs": {
481
+ "ligands": {
482
+ "name": "ligands",
483
+ "position": "left",
484
  "type": {
485
+ "type": "<class 'lynxkite_graph_analytics.core.Bundle'>"
486
  }
487
  },
488
+ "proteins": {
489
+ "name": "proteins",
490
+ "position": "left",
491
  "type": {
492
+ "type": "<class 'lynxkite_graph_analytics.core.Bundle'>"
493
  }
494
  }
495
  },
496
+ "name": "Query DiffDock",
497
  "outputs": {
498
  "output": {
499
  "name": "output",
500
+ "position": "right",
501
  "type": {
502
  "type": "None"
503
  }
504
  }
505
  },
506
  "params": {
507
+ "ligand_column": {
508
+ "default": null,
509
+ "name": "ligand_column",
 
 
 
 
 
 
 
510
  "type": {
511
  "type": "<class 'str'>"
512
  }
513
  },
514
+ "ligand_table": {
515
+ "default": null,
516
+ "name": "ligand_table",
517
  "type": {
518
  "type": "<class 'str'>"
519
  }
520
  },
521
+ "num_poses": {
522
+ "default": 10.0,
523
+ "name": "num_poses",
524
  "type": {
525
+ "type": "<class 'int'>"
526
  }
527
  },
528
+ "num_steps": {
529
+ "default": 18.0,
530
+ "name": "num_steps",
531
  "type": {
532
  "type": "<class 'int'>"
533
  }
534
  },
535
+ "protein_column": {
536
+ "default": null,
537
+ "name": "protein_column",
538
  "type": {
539
+ "type": "<class 'str'>"
540
  }
541
  },
542
+ "protein_table": {
543
+ "default": null,
544
+ "name": "protein_table",
545
  "type": {
546
+ "type": "<class 'str'>"
547
  }
548
  },
549
+ "time_divisions": {
550
+ "default": 20.0,
551
+ "name": "time_divisions",
552
  "type": {
553
+ "type": "<class 'int'>"
554
  }
555
  }
556
  },
557
  "position": {
558
+ "x": 852.0,
559
+ "y": 432.0
560
  },
561
  "type": "basic"
562
  },
563
  "params": {
564
+ "ligand_column": null,
565
+ "ligand_table": null,
566
+ "num_poses": 10.0,
567
+ "num_steps": 18.0,
568
+ "protein_column": null,
569
+ "protein_table": null,
570
+ "time_divisions": 20.0
 
571
  },
572
  "status": "done",
573
+ "title": "Query DiffDock"
574
  },
575
  "dragHandle": ".bg-primary",
576
+ "height": 635.0,
577
+ "id": "Query DiffDock 1",
578
  "position": {
579
+ "x": 1543.010053920781,
580
+ "y": 1167.386382170133
581
  },
582
  "type": "basic",
583
+ "width": 408.0
584
  },
585
  {
586
  "data": {
587
+ "display": {
588
+ "series": [
589
+ {
590
+ "data": [
591
+ {
592
+ "name": "Hydrogen",
593
+ "value": 2
594
+ },
595
+ {
596
+ "name": "Sulfur",
597
+ "value": 1
598
+ },
599
+ {
600
+ "name": "Oxygen",
601
+ "value": 4
602
+ }
603
+ ],
604
+ "itemStyle": {
605
+ "borderColor": "#fff",
606
+ "borderRadius": 10,
607
+ "borderWidth": 2
608
+ },
609
+ "radius": [
610
+ "40%",
611
+ "70%"
612
+ ],
613
+ "type": "pie"
614
+ }
615
+ ]
616
+ },
617
  "error": null,
618
  "meta": {
619
  "inputs": {
620
+ "bundle": {
621
+ "name": "bundle",
622
  "position": "left",
623
  "type": {
624
+ "type": "<class 'lynxkite_graph_analytics.core.Bundle'>"
 
 
 
 
 
 
 
 
 
 
625
  }
626
  }
627
  },
628
+ "name": "View molecules",
629
+ "outputs": {},
630
  "params": {
631
+ "color": {
632
+ "default": "spectrum",
633
+ "name": "color",
 
 
 
 
 
 
 
634
  "type": {
635
  "type": "<class 'str'>"
636
  }
637
  },
638
+ "molecule_column": {
639
+ "default": null,
640
+ "name": "molecule_column",
 
 
 
 
 
 
 
641
  "type": {
642
  "type": "<class 'str'>"
643
  }
644
  },
645
+ "molecule_table": {
646
+ "default": null,
647
+ "name": "molecule_table",
 
 
 
 
 
 
 
648
  "type": {
649
  "type": "<class 'str'>"
650
  }
651
  }
652
  },
653
  "position": {
654
+ "x": 1009.0,
655
+ "y": 124.0
656
  },
657
+ "type": "visualization"
658
  },
659
  "params": {
660
+ "color": "spectrum",
661
+ "molecule_column": null,
662
+ "molecule_table": null
 
 
 
663
  },
664
  "status": "done",
665
+ "title": "View molecules"
666
  },
667
  "dragHandle": ".bg-primary",
668
+ "height": 200.0,
669
+ "id": "View molecules 3",
670
  "position": {
671
+ "x": 1545.0,
672
+ "y": 585.0
673
  },
674
+ "type": "visualization",
675
+ "width": 200.0
676
  },
677
  {
678
  "data": {
679
+ "display": {
680
+ "series": [
681
+ {
682
+ "data": [
683
+ {
684
+ "name": "Hydrogen",
685
+ "value": 2
686
+ },
687
+ {
688
+ "name": "Sulfur",
689
+ "value": 1
690
+ },
691
+ {
692
+ "name": "Oxygen",
693
+ "value": 4
694
+ }
695
+ ],
696
+ "itemStyle": {
697
+ "borderColor": "#fff",
698
+ "borderRadius": 10,
699
+ "borderWidth": 2
700
+ },
701
+ "radius": [
702
+ "40%",
703
+ "70%"
704
+ ],
705
+ "type": "pie"
706
+ }
707
+ ]
708
+ },
709
  "error": null,
710
  "meta": {
711
+ "inputs": {
712
+ "bundle": {
713
+ "name": "bundle",
714
+ "position": "left",
 
 
715
  "type": {
716
+ "type": "<class 'lynxkite_graph_analytics.core.Bundle'>"
717
  }
718
  }
719
  },
720
+ "name": "View molecules",
721
+ "outputs": {},
722
  "params": {
723
+ "color": {
724
+ "default": "spectrum",
725
+ "name": "color",
726
  "type": {
727
  "type": "<class 'str'>"
728
  }
729
  },
730
+ "molecule_column": {
731
+ "default": null,
732
+ "name": "molecule_column",
733
  "type": {
734
+ "type": "<class 'str'>"
735
  }
736
  },
737
+ "molecule_table": {
738
+ "default": null,
739
+ "name": "molecule_table",
740
  "type": {
741
  "type": "<class 'str'>"
742
  }
743
  }
744
  },
745
  "position": {
746
+ "x": 859.0,
747
+ "y": 225.0
748
  },
749
+ "type": "visualization"
750
  },
751
  "params": {
752
+ "color": "spectrum",
753
+ "molecule_column": null,
754
+ "molecule_table": null
755
  },
756
  "status": "done",
757
+ "title": "View molecules"
758
  },
759
  "dragHandle": ".bg-primary",
760
+ "height": 200.0,
761
+ "id": "View molecules 1",
762
  "position": {
763
+ "x": 2230.0,
764
+ "y": 1598.3333333333333
765
  },
766
+ "type": "visualization",
767
+ "width": 200.0
768
  },
769
  {
770
  "data": {
 
772
  "error": null,
773
  "meta": {
774
  "inputs": {
775
+ "bundle": {
776
+ "name": "bundle",
777
  "position": "left",
778
  "type": {
779
+ "type": "<class 'lynxkite_graph_analytics.core.Bundle'>"
780
  }
781
  }
782
  },
783
+ "name": "Query OpenFold2",
784
  "outputs": {
785
  "output": {
786
  "name": "output",
787
+ "position": "right",
788
  "type": {
789
  "type": "None"
790
  }
791
  }
792
  },
793
  "params": {
794
+ "alignment_column": {
795
+ "default": null,
796
+ "name": "alignment_column",
797
+ "type": {
798
+ "type": "<class 'str'>"
799
+ }
800
+ },
801
+ "alignment_table": {
802
+ "default": null,
803
+ "name": "alignment_table",
804
+ "type": {
805
+ "type": "<class 'str'>"
806
+ }
807
+ },
808
+ "databases": {
809
+ "default": "[\"Uniref30_2302\", \"colabfold_envdb_202108\", \"PDB70_220313\"]",
810
+ "name": "databases",
811
  "type": {
812
  "type": "<class 'str'>"
813
  }
814
  },
815
+ "protein_column": {
816
+ "default": null,
817
+ "name": "protein_column",
818
+ "type": {
819
+ "type": "<class 'str'>"
820
+ }
821
+ },
822
+ "protein_table": {
823
+ "default": null,
824
+ "name": "protein_table",
825
  "type": {
826
  "type": "<class 'str'>"
827
  }
828
+ },
829
+ "relaxed_prediction": {
830
+ "default": false,
831
+ "name": "relaxed_prediction",
832
+ "type": {
833
+ "type": "<class 'bool'>"
834
+ }
835
+ },
836
+ "use_templates": {
837
+ "default": false,
838
+ "name": "use_templates",
839
+ "type": {
840
+ "type": "<class 'bool'>"
841
+ }
842
  }
843
  },
844
  "position": {
845
+ "x": 628.0,
846
+ "y": 184.0
847
  },
848
  "type": "basic"
849
  },
850
  "params": {
851
+ "alignment_column": null,
852
+ "alignment_table": null,
853
+ "databases": "[\"Uniref30_2302\", \"colabfold_envdb_202108\", \"PDB70_220313\"]",
854
+ "protein_column": null,
855
+ "protein_table": null,
856
+ "relaxed_prediction": false,
857
+ "use_templates": false
858
  },
859
  "status": "done",
860
+ "title": "Query OpenFold2"
861
  },
862
  "dragHandle": ".bg-primary",
863
+ "height": 653.0,
864
+ "id": "Query OpenFold2 1",
865
  "position": {
866
+ "x": 750.0,
867
+ "y": 480.0
868
  },
869
  "type": "basic",
870
+ "width": 523.0
871
  }
872
  ]
873
  }
examples/{Graph RAG → Graph RAG.lynxkite.json} RENAMED
@@ -510,8 +510,7 @@
510
  "title": "Ask LLM",
511
  "params": {
512
  "max_tokens": 100.0,
513
- "accepted_regex": "",
514
- "model": "SultanR/SmolTulu-1.7b-Instruct"
515
  },
516
  "display": null,
517
  "error": null,
@@ -541,13 +540,6 @@
541
  "type": {
542
  "type": "<class 'int'>"
543
  }
544
- },
545
- "model": {
546
- "type": {
547
- "type": "<class 'str'>"
548
- },
549
- "default": null,
550
- "name": "model"
551
  }
552
  },
553
  "outputs": {
 
510
  "title": "Ask LLM",
511
  "params": {
512
  "max_tokens": 100.0,
513
+ "accepted_regex": ""
 
514
  },
515
  "display": null,
516
  "error": null,
 
540
  "type": {
541
  "type": "<class 'int'>"
542
  }
 
 
 
 
 
 
 
543
  }
544
  },
545
  "outputs": {
examples/{Image processing → Image processing.lynxkite.json} RENAMED
File without changes
examples/Model definition.lynxkite.json ADDED
@@ -0,0 +1,614 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "edges": [
3
+ {
4
+ "id": "MSE loss 2 Optimizer 2",
5
+ "source": "MSE loss 2",
6
+ "sourceHandle": "output",
7
+ "target": "Optimizer 2",
8
+ "targetHandle": "loss"
9
+ },
10
+ {
11
+ "id": "Activation 1 Repeat 1",
12
+ "source": "Activation 1",
13
+ "sourceHandle": "output",
14
+ "target": "Repeat 1",
15
+ "targetHandle": "input"
16
+ },
17
+ {
18
+ "id": "Linear 1 Activation 1",
19
+ "source": "Linear 1",
20
+ "sourceHandle": "output",
21
+ "target": "Activation 1",
22
+ "targetHandle": "x"
23
+ },
24
+ {
25
+ "id": "Repeat 1 Linear 1",
26
+ "source": "Repeat 1",
27
+ "sourceHandle": "output",
28
+ "target": "Linear 1",
29
+ "targetHandle": "x"
30
+ },
31
+ {
32
+ "id": "Input: tensor 1 Linear 1",
33
+ "source": "Input: tensor 1",
34
+ "sourceHandle": "output",
35
+ "target": "Linear 1",
36
+ "targetHandle": "x"
37
+ },
38
+ {
39
+ "id": "Constant vector 1 Add 1",
40
+ "source": "Constant vector 1",
41
+ "sourceHandle": "output",
42
+ "target": "Add 1",
43
+ "targetHandle": "b"
44
+ },
45
+ {
46
+ "id": "Input: tensor 3 Add 1",
47
+ "source": "Input: tensor 3",
48
+ "sourceHandle": "output",
49
+ "target": "Add 1",
50
+ "targetHandle": "a"
51
+ },
52
+ {
53
+ "id": "Add 1 MSE loss 2",
54
+ "source": "Add 1",
55
+ "sourceHandle": "output",
56
+ "target": "MSE loss 2",
57
+ "targetHandle": "y"
58
+ },
59
+ {
60
+ "id": "Activation 1 Output 1",
61
+ "source": "Activation 1",
62
+ "sourceHandle": "output",
63
+ "target": "Output 1",
64
+ "targetHandle": "x"
65
+ },
66
+ {
67
+ "id": "Output 1 MSE loss 2",
68
+ "source": "Output 1",
69
+ "sourceHandle": "x",
70
+ "target": "MSE loss 2",
71
+ "targetHandle": "x"
72
+ }
73
+ ],
74
+ "env": "PyTorch model",
75
+ "nodes": [
76
+ {
77
+ "data": {
78
+ "__execution_delay": 0.0,
79
+ "collapsed": null,
80
+ "display": null,
81
+ "error": null,
82
+ "input_metadata": null,
83
+ "meta": {
84
+ "inputs": {
85
+ "loss": {
86
+ "name": "loss",
87
+ "position": "bottom",
88
+ "type": {
89
+ "type": "tensor"
90
+ }
91
+ }
92
+ },
93
+ "name": "Optimizer",
94
+ "outputs": {},
95
+ "params": {
96
+ "lr": {
97
+ "default": 0.001,
98
+ "name": "lr",
99
+ "type": {
100
+ "type": "<class 'float'>"
101
+ }
102
+ },
103
+ "type": {
104
+ "default": "AdamW",
105
+ "name": "type",
106
+ "type": {
107
+ "enum": [
108
+ "AdamW",
109
+ "Adafactor",
110
+ "Adagrad",
111
+ "SGD",
112
+ "Lion",
113
+ "Paged AdamW",
114
+ "Galore AdamW"
115
+ ]
116
+ }
117
+ }
118
+ },
119
+ "type": "basic"
120
+ },
121
+ "params": {
122
+ "lr": "0.1",
123
+ "type": "SGD"
124
+ },
125
+ "status": "planned",
126
+ "title": "Optimizer"
127
+ },
128
+ "dragHandle": ".bg-primary",
129
+ "height": 250.0,
130
+ "id": "Optimizer 2",
131
+ "position": {
132
+ "x": 359.75221367487865,
133
+ "y": -1560.7604266065723
134
+ },
135
+ "type": "basic",
136
+ "width": 232.0
137
+ },
138
+ {
139
+ "data": {
140
+ "__execution_delay": 0.0,
141
+ "collapsed": null,
142
+ "display": null,
143
+ "error": null,
144
+ "input_metadata": null,
145
+ "meta": {
146
+ "inputs": {
147
+ "x": {
148
+ "name": "x",
149
+ "position": "bottom",
150
+ "type": {
151
+ "type": "<class 'inspect._empty'>"
152
+ }
153
+ }
154
+ },
155
+ "name": "Activation",
156
+ "outputs": {
157
+ "output": {
158
+ "name": "output",
159
+ "position": "top",
160
+ "type": {
161
+ "type": "None"
162
+ }
163
+ }
164
+ },
165
+ "params": {
166
+ "type": {
167
+ "default": "ReLU",
168
+ "name": "type",
169
+ "type": {
170
+ "enum": [
171
+ "ReLU",
172
+ "Leaky_ReLU",
173
+ "Tanh",
174
+ "Mish"
175
+ ]
176
+ }
177
+ }
178
+ },
179
+ "type": "basic"
180
+ },
181
+ "params": {
182
+ "type": "Leaky_ReLU"
183
+ },
184
+ "status": "planned",
185
+ "title": "Activation"
186
+ },
187
+ "dragHandle": ".bg-primary",
188
+ "height": 200.0,
189
+ "id": "Activation 1",
190
+ "position": {
191
+ "x": 99.77615018185415,
192
+ "y": -249.43925929074078
193
+ },
194
+ "type": "basic",
195
+ "width": 200.0
196
+ },
197
+ {
198
+ "data": {
199
+ "__execution_delay": 0.0,
200
+ "collapsed": null,
201
+ "display": null,
202
+ "error": null,
203
+ "input_metadata": null,
204
+ "meta": {
205
+ "inputs": {},
206
+ "name": "Input: tensor",
207
+ "outputs": {
208
+ "output": {
209
+ "name": "output",
210
+ "position": "top",
211
+ "type": {
212
+ "type": "tensor"
213
+ }
214
+ }
215
+ },
216
+ "params": {
217
+ "name": {
218
+ "default": null,
219
+ "name": "name",
220
+ "type": {
221
+ "type": "None"
222
+ }
223
+ }
224
+ },
225
+ "type": "basic"
226
+ },
227
+ "params": {
228
+ "name": "Y"
229
+ },
230
+ "status": "planned",
231
+ "title": "Input: tensor"
232
+ },
233
+ "dragHandle": ".bg-primary",
234
+ "height": 200.0,
235
+ "id": "Input: tensor 3",
236
+ "position": {
237
+ "x": 485.8840220312055,
238
+ "y": -268.0485936515193
239
+ },
240
+ "type": "basic",
241
+ "width": 200.0
242
+ },
243
+ {
244
+ "data": {
245
+ "__execution_delay": null,
246
+ "collapsed": true,
247
+ "display": null,
248
+ "error": null,
249
+ "input_metadata": null,
250
+ "meta": {
251
+ "inputs": {
252
+ "x": {
253
+ "name": "x",
254
+ "position": "bottom",
255
+ "type": {
256
+ "type": "<class 'inspect._empty'>"
257
+ }
258
+ },
259
+ "y": {
260
+ "name": "y",
261
+ "position": "bottom",
262
+ "type": {
263
+ "type": "<class 'inspect._empty'>"
264
+ }
265
+ }
266
+ },
267
+ "name": "MSE loss",
268
+ "outputs": {
269
+ "output": {
270
+ "name": "output",
271
+ "position": "top",
272
+ "type": {
273
+ "type": "None"
274
+ }
275
+ }
276
+ },
277
+ "params": {},
278
+ "type": "basic"
279
+ },
280
+ "params": {},
281
+ "status": "planned",
282
+ "title": "MSE loss"
283
+ },
284
+ "dragHandle": ".bg-primary",
285
+ "height": 200.0,
286
+ "id": "MSE loss 2",
287
+ "position": {
288
+ "x": 384.54674698852955,
289
+ "y": -1184.4701545316577
290
+ },
291
+ "type": "basic",
292
+ "width": 200.0
293
+ },
294
+ {
295
+ "data": {
296
+ "__execution_delay": 0.0,
297
+ "collapsed": null,
298
+ "display": null,
299
+ "error": null,
300
+ "input_metadata": null,
301
+ "meta": {
302
+ "inputs": {
303
+ "input": {
304
+ "name": "input",
305
+ "position": "top",
306
+ "type": {
307
+ "type": "tensor"
308
+ }
309
+ }
310
+ },
311
+ "name": "Repeat",
312
+ "outputs": {
313
+ "output": {
314
+ "name": "output",
315
+ "position": "bottom",
316
+ "type": {
317
+ "type": "tensor"
318
+ }
319
+ }
320
+ },
321
+ "params": {
322
+ "same_weights": {
323
+ "default": false,
324
+ "name": "same_weights",
325
+ "type": {
326
+ "type": "<class 'bool'>"
327
+ }
328
+ },
329
+ "times": {
330
+ "default": 1.0,
331
+ "name": "times",
332
+ "type": {
333
+ "type": "<class 'int'>"
334
+ }
335
+ }
336
+ },
337
+ "type": "basic"
338
+ },
339
+ "params": {
340
+ "same_weights": false,
341
+ "times": "2"
342
+ },
343
+ "status": "planned",
344
+ "title": "Repeat"
345
+ },
346
+ "dragHandle": ".bg-primary",
347
+ "height": 200.0,
348
+ "id": "Repeat 1",
349
+ "position": {
350
+ "x": -210.0,
351
+ "y": -135.0
352
+ },
353
+ "type": "basic",
354
+ "width": 200.0
355
+ },
356
+ {
357
+ "data": {
358
+ "__execution_delay": 0.0,
359
+ "collapsed": null,
360
+ "display": null,
361
+ "error": null,
362
+ "input_metadata": null,
363
+ "meta": {
364
+ "inputs": {
365
+ "x": {
366
+ "name": "x",
367
+ "position": "bottom",
368
+ "type": {
369
+ "type": "<class 'inspect._empty'>"
370
+ }
371
+ }
372
+ },
373
+ "name": "Linear",
374
+ "outputs": {
375
+ "output": {
376
+ "name": "output",
377
+ "position": "top",
378
+ "type": {
379
+ "type": "None"
380
+ }
381
+ }
382
+ },
383
+ "params": {
384
+ "output_dim": {
385
+ "default": 1024.0,
386
+ "name": "output_dim",
387
+ "type": {
388
+ "type": "<class 'int'>"
389
+ }
390
+ }
391
+ },
392
+ "type": "basic"
393
+ },
394
+ "params": {
395
+ "output_dim": "4"
396
+ },
397
+ "status": "planned",
398
+ "title": "Linear"
399
+ },
400
+ "dragHandle": ".bg-primary",
401
+ "height": 200.0,
402
+ "id": "Linear 1",
403
+ "position": {
404
+ "x": 98.54861342271252,
405
+ "y": 14.121603973834155
406
+ },
407
+ "type": "basic",
408
+ "width": 200.0
409
+ },
410
+ {
411
+ "data": {
412
+ "__execution_delay": 0.0,
413
+ "collapsed": null,
414
+ "display": null,
415
+ "error": null,
416
+ "input_metadata": null,
417
+ "meta": {
418
+ "inputs": {},
419
+ "name": "Input: tensor",
420
+ "outputs": {
421
+ "output": {
422
+ "name": "output",
423
+ "position": "top",
424
+ "type": {
425
+ "type": "tensor"
426
+ }
427
+ }
428
+ },
429
+ "params": {
430
+ "name": {
431
+ "default": null,
432
+ "name": "name",
433
+ "type": {
434
+ "type": "None"
435
+ }
436
+ }
437
+ },
438
+ "type": "basic"
439
+ },
440
+ "params": {
441
+ "name": "X"
442
+ },
443
+ "status": "planned",
444
+ "title": "Input: tensor"
445
+ },
446
+ "dragHandle": ".bg-primary",
447
+ "height": 200.0,
448
+ "id": "Input: tensor 1",
449
+ "position": {
450
+ "x": 108.75735538875443,
451
+ "y": 331.53404347930933
452
+ },
453
+ "type": "basic",
454
+ "width": 200.0
455
+ },
456
+ {
457
+ "data": {
458
+ "__execution_delay": 0.0,
459
+ "collapsed": null,
460
+ "display": null,
461
+ "error": null,
462
+ "input_metadata": null,
463
+ "meta": {
464
+ "inputs": {},
465
+ "name": "Constant vector",
466
+ "outputs": {
467
+ "output": {
468
+ "name": "output",
469
+ "position": "top",
470
+ "type": {
471
+ "type": "None"
472
+ }
473
+ }
474
+ },
475
+ "params": {
476
+ "size": {
477
+ "default": 1.0,
478
+ "name": "size",
479
+ "type": {
480
+ "type": "<class 'int'>"
481
+ }
482
+ },
483
+ "value": {
484
+ "default": 0.0,
485
+ "name": "value",
486
+ "type": {
487
+ "type": "<class 'int'>"
488
+ }
489
+ }
490
+ },
491
+ "type": "basic"
492
+ },
493
+ "params": {
494
+ "size": "1",
495
+ "value": "1"
496
+ },
497
+ "status": "planned",
498
+ "title": "Constant vector"
499
+ },
500
+ "dragHandle": ".bg-primary",
501
+ "height": 258.0,
502
+ "id": "Constant vector 1",
503
+ "position": {
504
+ "x": 886.708922897265,
505
+ "y": -298.4394167425953
506
+ },
507
+ "type": "basic",
508
+ "width": 238.0
509
+ },
510
+ {
511
+ "data": {
512
+ "__execution_delay": null,
513
+ "collapsed": true,
514
+ "display": null,
515
+ "error": null,
516
+ "input_metadata": null,
517
+ "meta": {
518
+ "inputs": {
519
+ "a": {
520
+ "name": "a",
521
+ "position": "bottom",
522
+ "type": {
523
+ "type": "<class 'inspect._empty'>"
524
+ }
525
+ },
526
+ "b": {
527
+ "name": "b",
528
+ "position": "bottom",
529
+ "type": {
530
+ "type": "<class 'inspect._empty'>"
531
+ }
532
+ }
533
+ },
534
+ "name": "Add",
535
+ "outputs": {
536
+ "output": {
537
+ "name": "output",
538
+ "position": "top",
539
+ "type": {
540
+ "type": "None"
541
+ }
542
+ }
543
+ },
544
+ "params": {},
545
+ "type": "basic"
546
+ },
547
+ "params": {},
548
+ "status": "planned",
549
+ "title": "Add"
550
+ },
551
+ "dragHandle": ".bg-primary",
552
+ "height": 200.0,
553
+ "id": "Add 1",
554
+ "position": {
555
+ "x": 722.1292469875319,
556
+ "y": -762.6853551968964
557
+ },
558
+ "type": "basic",
559
+ "width": 200.0
560
+ },
561
+ {
562
+ "data": {
563
+ "__execution_delay": null,
564
+ "collapsed": true,
565
+ "display": null,
566
+ "error": null,
567
+ "input_metadata": null,
568
+ "meta": {
569
+ "inputs": {
570
+ "x": {
571
+ "name": "x",
572
+ "position": "bottom",
573
+ "type": {
574
+ "type": "tensor"
575
+ }
576
+ }
577
+ },
578
+ "name": "Output",
579
+ "outputs": {
580
+ "x": {
581
+ "name": "x",
582
+ "position": "top",
583
+ "type": {
584
+ "type": "tensor"
585
+ }
586
+ }
587
+ },
588
+ "params": {
589
+ "name": {
590
+ "default": null,
591
+ "name": "name",
592
+ "type": {
593
+ "type": "None"
594
+ }
595
+ }
596
+ },
597
+ "type": "basic"
598
+ },
599
+ "params": {},
600
+ "status": "planned",
601
+ "title": "Output"
602
+ },
603
+ "dragHandle": ".bg-primary",
604
+ "height": 200.0,
605
+ "id": "Output 1",
606
+ "position": {
607
+ "x": 185.15239170944702,
608
+ "y": -733.1526319565451
609
+ },
610
+ "type": "basic",
611
+ "width": 200.0
612
+ }
613
+ ]
614
+ }
examples/Model use.lynxkite.json ADDED
The diff for this file is too large to render. See raw diff
 
examples/{NetworkX demo → NetworkX demo.lynxkite.json} RENAMED
File without changes
examples/ODE-GNN experiment.lynxkite.json ADDED
@@ -0,0 +1,466 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "edges": [
3
+ {
4
+ "id": "Import CSV 1 Train/test split 1",
5
+ "source": "Import CSV 1",
6
+ "sourceHandle": "output",
7
+ "target": "Train/test split 1",
8
+ "targetHandle": "bundle"
9
+ },
10
+ {
11
+ "id": "Train/test split 1 Create graph 1",
12
+ "source": "Train/test split 1",
13
+ "sourceHandle": "output",
14
+ "target": "Create graph 1",
15
+ "targetHandle": "bundle"
16
+ },
17
+ {
18
+ "id": "Biomedical foundation graph (PLACEHOLDER) 1 Create graph 1",
19
+ "source": "Biomedical foundation graph (PLACEHOLDER) 1",
20
+ "sourceHandle": "output",
21
+ "target": "Create graph 1",
22
+ "targetHandle": "bundle"
23
+ },
24
+ {
25
+ "id": "Define model 1 Create graph 1",
26
+ "source": "Define model 1",
27
+ "sourceHandle": "output",
28
+ "target": "Create graph 1",
29
+ "targetHandle": "bundle"
30
+ },
31
+ {
32
+ "id": "Create graph 1 Train model 1",
33
+ "source": "Create graph 1",
34
+ "sourceHandle": "output",
35
+ "target": "Train model 1",
36
+ "targetHandle": "bundle"
37
+ },
38
+ {
39
+ "id": "Train model 1 Model inference 1",
40
+ "source": "Train model 1",
41
+ "sourceHandle": "output",
42
+ "target": "Model inference 1",
43
+ "targetHandle": "bundle"
44
+ }
45
+ ],
46
+ "env": "LynxKite Graph Analytics",
47
+ "nodes": [
48
+ {
49
+ "data": {
50
+ "__execution_delay": 0.0,
51
+ "collapsed": null,
52
+ "display": null,
53
+ "error": null,
54
+ "meta": {
55
+ "inputs": {},
56
+ "name": "Biomedical foundation graph (PLACEHOLDER)",
57
+ "outputs": {
58
+ "output": {
59
+ "name": "output",
60
+ "position": "right",
61
+ "type": {
62
+ "type": "None"
63
+ }
64
+ }
65
+ },
66
+ "params": {
67
+ "filter_nodes": {
68
+ "default": null,
69
+ "name": "filter_nodes",
70
+ "type": {
71
+ "type": "<class 'str'>"
72
+ }
73
+ }
74
+ },
75
+ "type": "basic"
76
+ },
77
+ "params": {
78
+ "filter_nodes": "drug,gene,disease"
79
+ },
80
+ "status": "done",
81
+ "title": "Biomedical foundation graph (PLACEHOLDER)"
82
+ },
83
+ "dragHandle": ".bg-primary",
84
+ "height": 200.0,
85
+ "id": "Biomedical foundation graph (PLACEHOLDER) 1",
86
+ "position": {
87
+ "x": 230.1082040835347,
88
+ "y": 643.2454063689602
89
+ },
90
+ "type": "basic",
91
+ "width": 200.0
92
+ },
93
+ {
94
+ "data": {
95
+ "__execution_delay": null,
96
+ "collapsed": true,
97
+ "display": null,
98
+ "error": "Missing input: bundle",
99
+ "meta": {
100
+ "inputs": {
101
+ "bundle": {
102
+ "name": "bundle",
103
+ "position": "left",
104
+ "type": {
105
+ "type": "<class 'lynxkite_graph_analytics.core.Bundle'>"
106
+ }
107
+ }
108
+ },
109
+ "name": "Train/test split",
110
+ "outputs": {
111
+ "output": {
112
+ "name": "output",
113
+ "position": "right",
114
+ "type": {
115
+ "type": "None"
116
+ }
117
+ }
118
+ },
119
+ "params": {
120
+ "table_name": {
121
+ "default": null,
122
+ "name": "table_name",
123
+ "type": {
124
+ "type": "<class 'str'>"
125
+ }
126
+ },
127
+ "test_ratio": {
128
+ "default": 0.1,
129
+ "name": "test_ratio",
130
+ "type": {
131
+ "type": "<class 'float'>"
132
+ }
133
+ }
134
+ },
135
+ "type": "basic"
136
+ },
137
+ "params": {
138
+ "table_name": null,
139
+ "test_ratio": 0.1
140
+ },
141
+ "status": "planned",
142
+ "title": "Train/test split"
143
+ },
144
+ "dragHandle": ".bg-primary",
145
+ "height": 200.0,
146
+ "id": "Train/test split 1",
147
+ "position": {
148
+ "x": 313.3745540124723,
149
+ "y": 412.5466021460861
150
+ },
151
+ "type": "basic",
152
+ "width": 200.0
153
+ },
154
+ {
155
+ "data": {
156
+ "display": null,
157
+ "error": "[Errno 2] No such file or directory: ''",
158
+ "meta": {
159
+ "inputs": {},
160
+ "name": "Import CSV",
161
+ "outputs": {
162
+ "output": {
163
+ "name": "output",
164
+ "position": "right",
165
+ "type": {
166
+ "type": "None"
167
+ }
168
+ }
169
+ },
170
+ "params": {
171
+ "columns": {
172
+ "default": "<from file>",
173
+ "name": "columns",
174
+ "type": {
175
+ "type": "<class 'str'>"
176
+ }
177
+ },
178
+ "filename": {
179
+ "default": null,
180
+ "name": "filename",
181
+ "type": {
182
+ "type": "<class 'str'>"
183
+ }
184
+ },
185
+ "separator": {
186
+ "default": "<auto>",
187
+ "name": "separator",
188
+ "type": {
189
+ "type": "<class 'str'>"
190
+ }
191
+ }
192
+ },
193
+ "type": "basic"
194
+ },
195
+ "params": {
196
+ "columns": "<from file>",
197
+ "filename": null,
198
+ "separator": "<auto>"
199
+ },
200
+ "status": "done",
201
+ "title": "Import CSV"
202
+ },
203
+ "dragHandle": ".bg-primary",
204
+ "height": 200.0,
205
+ "id": "Import CSV 1",
206
+ "position": {
207
+ "x": -2.1743215714344757,
208
+ "y": 346.06014722935214
209
+ },
210
+ "type": "basic",
211
+ "width": 200.0
212
+ },
213
+ {
214
+ "data": {
215
+ "__execution_delay": 0.0,
216
+ "collapsed": null,
217
+ "display": null,
218
+ "error": "Missing input: bundle",
219
+ "meta": {
220
+ "inputs": {
221
+ "bundle": {
222
+ "name": "bundle",
223
+ "position": "left",
224
+ "type": {
225
+ "type": "<class 'lynxkite_graph_analytics.core.Bundle'>"
226
+ }
227
+ }
228
+ },
229
+ "name": "Model inference",
230
+ "outputs": {
231
+ "output": {
232
+ "name": "output",
233
+ "position": "right",
234
+ "type": {
235
+ "type": "None"
236
+ }
237
+ }
238
+ },
239
+ "params": {
240
+ "model_mapping": {
241
+ "default": null,
242
+ "name": "model_mapping",
243
+ "type": {
244
+ "type": "<class 'str'>"
245
+ }
246
+ },
247
+ "model_name": {
248
+ "default": null,
249
+ "name": "model_name",
250
+ "type": {
251
+ "type": "<class 'str'>"
252
+ }
253
+ },
254
+ "save_output_as": {
255
+ "default": "prediction",
256
+ "name": "save_output_as",
257
+ "type": {
258
+ "type": "<class 'str'>"
259
+ }
260
+ }
261
+ },
262
+ "type": "basic"
263
+ },
264
+ "params": {
265
+ "model_mapping": "input: data_test",
266
+ "model_name": "model",
267
+ "save_output_as": "prediction"
268
+ },
269
+ "status": "done",
270
+ "title": "Model inference"
271
+ },
272
+ "dragHandle": ".bg-primary",
273
+ "height": 339.0,
274
+ "id": "Model inference 1",
275
+ "position": {
276
+ "x": 1736.5697434242886,
277
+ "y": 357.0743204289906
278
+ },
279
+ "type": "basic",
280
+ "width": 281.0
281
+ },
282
+ {
283
+ "data": {
284
+ "__execution_delay": null,
285
+ "collapsed": true,
286
+ "display": null,
287
+ "error": "Missing input: bundle",
288
+ "meta": {
289
+ "inputs": {
290
+ "bundle": {
291
+ "name": "bundle",
292
+ "position": "left",
293
+ "type": {
294
+ "type": "list[lynxkite_graph_analytics.core.Bundle]"
295
+ }
296
+ }
297
+ },
298
+ "name": "Organize",
299
+ "outputs": {
300
+ "output": {
301
+ "name": "output",
302
+ "position": "right",
303
+ "type": {
304
+ "type": "None"
305
+ }
306
+ }
307
+ },
308
+ "params": {
309
+ "relations": {
310
+ "default": null,
311
+ "name": "relations",
312
+ "type": {
313
+ "type": "<class 'str'>"
314
+ }
315
+ }
316
+ },
317
+ "type": "graph_creation_view"
318
+ },
319
+ "params": {
320
+ "relations": null
321
+ },
322
+ "status": "planned",
323
+ "title": "Organize"
324
+ },
325
+ "dragHandle": ".bg-primary",
326
+ "height": 322.0,
327
+ "id": "Create graph 1",
328
+ "position": {
329
+ "x": 846.6882598271658,
330
+ "y": 480.6258932907771
331
+ },
332
+ "type": "graph_creation_view",
333
+ "width": 313.0
334
+ },
335
+ {
336
+ "data": {
337
+ "__execution_delay": 0.0,
338
+ "collapsed": null,
339
+ "display": null,
340
+ "error": null,
341
+ "meta": {
342
+ "inputs": {},
343
+ "name": "Define model",
344
+ "outputs": {
345
+ "output": {
346
+ "name": "output",
347
+ "position": "right",
348
+ "type": {
349
+ "type": "None"
350
+ }
351
+ }
352
+ },
353
+ "params": {
354
+ "model_workspace": {
355
+ "default": null,
356
+ "name": "model_workspace",
357
+ "type": {
358
+ "type": "<class 'str'>"
359
+ }
360
+ },
361
+ "save_as": {
362
+ "default": "model",
363
+ "name": "save_as",
364
+ "type": {
365
+ "type": "<class 'str'>"
366
+ }
367
+ }
368
+ },
369
+ "position": {
370
+ "x": 286.0,
371
+ "y": 208.0
372
+ },
373
+ "type": "basic"
374
+ },
375
+ "params": {
376
+ "model_workspace": "ODE-GNN",
377
+ "save_as": "model"
378
+ },
379
+ "status": "done",
380
+ "title": "Define model"
381
+ },
382
+ "dragHandle": ".bg-primary",
383
+ "height": 200.0,
384
+ "id": "Define model 1",
385
+ "position": {
386
+ "x": 311.976524267066,
387
+ "y": 146.99006795914332
388
+ },
389
+ "type": "basic",
390
+ "width": 200.0
391
+ },
392
+ {
393
+ "data": {
394
+ "__execution_delay": 0.0,
395
+ "collapsed": null,
396
+ "display": null,
397
+ "error": "Missing input: bundle",
398
+ "meta": {
399
+ "inputs": {
400
+ "bundle": {
401
+ "name": "bundle",
402
+ "position": "left",
403
+ "type": {
404
+ "type": "<class 'lynxkite_graph_analytics.core.Bundle'>"
405
+ }
406
+ }
407
+ },
408
+ "name": "Train model",
409
+ "outputs": {
410
+ "output": {
411
+ "name": "output",
412
+ "position": "right",
413
+ "type": {
414
+ "type": "None"
415
+ }
416
+ }
417
+ },
418
+ "params": {
419
+ "epochs": {
420
+ "default": 1.0,
421
+ "name": "epochs",
422
+ "type": {
423
+ "type": "<class 'int'>"
424
+ }
425
+ },
426
+ "model_mapping": {
427
+ "default": null,
428
+ "name": "model_mapping",
429
+ "type": {
430
+ "type": "<class 'str'>"
431
+ }
432
+ },
433
+ "model_name": {
434
+ "default": null,
435
+ "name": "model_name",
436
+ "type": {
437
+ "type": "<class 'str'>"
438
+ }
439
+ }
440
+ },
441
+ "position": {
442
+ "x": 995.0,
443
+ "y": 350.0
444
+ },
445
+ "type": "basic"
446
+ },
447
+ "params": {
448
+ "epochs": 1.0,
449
+ "model_mapping": "input: data_train",
450
+ "model_name": "model"
451
+ },
452
+ "status": "planned",
453
+ "title": "Train model"
454
+ },
455
+ "dragHandle": ".bg-primary",
456
+ "height": 342.0,
457
+ "id": "Train model 1",
458
+ "position": {
459
+ "x": 1358.7213662492159,
460
+ "y": 352.03096133771896
461
+ },
462
+ "type": "basic",
463
+ "width": 296.0
464
+ }
465
+ ]
466
+ }
examples/ODE-GNN.lynxkite.json ADDED
@@ -0,0 +1,796 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "edges": [
3
+ {
4
+ "id": "Input: embedding 1 Graph conv 1",
5
+ "source": "Input: embedding 1",
6
+ "sourceHandle": "x",
7
+ "target": "Graph conv 1",
8
+ "targetHandle": "x"
9
+ },
10
+ {
11
+ "id": "Input: graph edges 1 Graph conv 1",
12
+ "source": "Input: graph edges 1",
13
+ "sourceHandle": "edges",
14
+ "target": "Graph conv 1",
15
+ "targetHandle": "edges"
16
+ },
17
+ {
18
+ "id": "Graph conv 1 Activation 1",
19
+ "source": "Graph conv 1",
20
+ "sourceHandle": "x",
21
+ "target": "Activation 1",
22
+ "targetHandle": "x"
23
+ },
24
+ {
25
+ "id": "Activation 1 Repeat 1",
26
+ "source": "Activation 1",
27
+ "sourceHandle": "x",
28
+ "target": "Repeat 1",
29
+ "targetHandle": "input"
30
+ },
31
+ {
32
+ "id": "Repeat 1 Graph conv 1",
33
+ "source": "Repeat 1",
34
+ "sourceHandle": "output",
35
+ "target": "Graph conv 1",
36
+ "targetHandle": "x"
37
+ },
38
+ {
39
+ "id": "Input: sequential 1 LSTM 1",
40
+ "source": "Input: sequential 1",
41
+ "sourceHandle": "y",
42
+ "target": "LSTM 1",
43
+ "targetHandle": "x"
44
+ },
45
+ {
46
+ "id": "Input: zeros 1 LSTM 1",
47
+ "source": "Input: zeros 1",
48
+ "sourceHandle": "x",
49
+ "target": "LSTM 1",
50
+ "targetHandle": "h"
51
+ },
52
+ {
53
+ "id": "Recurrent chain 1 LSTM 1",
54
+ "source": "Recurrent chain 1",
55
+ "sourceHandle": "output",
56
+ "target": "LSTM 1",
57
+ "targetHandle": "h"
58
+ },
59
+ {
60
+ "id": "LSTM 1 Recurrent chain 1",
61
+ "source": "LSTM 1",
62
+ "sourceHandle": "h",
63
+ "target": "Recurrent chain 1",
64
+ "targetHandle": "input"
65
+ },
66
+ {
67
+ "id": "Activation 1 Concatenate 1",
68
+ "source": "Activation 1",
69
+ "sourceHandle": "x",
70
+ "target": "Concatenate 1",
71
+ "targetHandle": "a"
72
+ },
73
+ {
74
+ "id": "LSTM 1 Concatenate 1",
75
+ "source": "LSTM 1",
76
+ "sourceHandle": "x",
77
+ "target": "Concatenate 1",
78
+ "targetHandle": "b"
79
+ },
80
+ {
81
+ "id": "Input: label 1 MSE loss 1",
82
+ "source": "Input: label 1",
83
+ "sourceHandle": "y",
84
+ "target": "MSE loss 1",
85
+ "targetHandle": "y"
86
+ },
87
+ {
88
+ "id": "MSE loss 1 Optimizer 1",
89
+ "source": "MSE loss 1",
90
+ "sourceHandle": "loss",
91
+ "target": "Optimizer 1",
92
+ "targetHandle": "loss"
93
+ },
94
+ {
95
+ "id": "Concatenate 1 Neural ODE 2",
96
+ "source": "Concatenate 1",
97
+ "sourceHandle": "x",
98
+ "target": "Neural ODE 2",
99
+ "targetHandle": "x"
100
+ },
101
+ {
102
+ "id": "Neural ODE 2 MSE loss 1",
103
+ "source": "Neural ODE 2",
104
+ "sourceHandle": "x",
105
+ "target": "MSE loss 1",
106
+ "targetHandle": "x"
107
+ }
108
+ ],
109
+ "env": "PyTorch model",
110
+ "nodes": [
111
+ {
112
+ "data": {
113
+ "display": null,
114
+ "error": null,
115
+ "meta": {
116
+ "inputs": {
117
+ "edges": {
118
+ "name": "edges",
119
+ "position": "bottom",
120
+ "type": {
121
+ "type": "tensor"
122
+ }
123
+ },
124
+ "x": {
125
+ "name": "x",
126
+ "position": "bottom",
127
+ "type": {
128
+ "type": "tensor"
129
+ }
130
+ }
131
+ },
132
+ "name": "Graph conv",
133
+ "outputs": {
134
+ "x": {
135
+ "name": "x",
136
+ "position": "top",
137
+ "type": {
138
+ "type": "tensor"
139
+ }
140
+ }
141
+ },
142
+ "params": {
143
+ "type": {
144
+ "default": "1",
145
+ "name": "type",
146
+ "type": {
147
+ "enum": [
148
+ "GCNConv",
149
+ "GATConv",
150
+ "GATv2Conv",
151
+ "SAGEConv"
152
+ ]
153
+ }
154
+ }
155
+ },
156
+ "type": "basic"
157
+ },
158
+ "params": {
159
+ "type": 1.0
160
+ },
161
+ "status": "planned",
162
+ "title": "Graph conv"
163
+ },
164
+ "dragHandle": ".bg-primary",
165
+ "height": 200.0,
166
+ "id": "Graph conv 1",
167
+ "position": {
168
+ "x": 350.98078368755864,
169
+ "y": 195.0
170
+ },
171
+ "type": "basic",
172
+ "width": 200.0
173
+ },
174
+ {
175
+ "data": {
176
+ "__execution_delay": 0.0,
177
+ "collapsed": null,
178
+ "display": null,
179
+ "error": null,
180
+ "meta": {
181
+ "inputs": {
182
+ "input": {
183
+ "name": "input",
184
+ "position": "top",
185
+ "type": {
186
+ "type": "tensor"
187
+ }
188
+ }
189
+ },
190
+ "name": "Repeat",
191
+ "outputs": {
192
+ "output": {
193
+ "name": "output",
194
+ "position": "bottom",
195
+ "type": {
196
+ "type": "tensor"
197
+ }
198
+ }
199
+ },
200
+ "params": {
201
+ "times": {
202
+ "default": 1.0,
203
+ "name": "times",
204
+ "type": {
205
+ "type": "<class 'int'>"
206
+ }
207
+ }
208
+ },
209
+ "type": "basic"
210
+ },
211
+ "params": {
212
+ "times": "5"
213
+ },
214
+ "status": "planned",
215
+ "title": "Repeat"
216
+ },
217
+ "dragHandle": ".bg-primary",
218
+ "height": 200.0,
219
+ "id": "Repeat 1",
220
+ "position": {
221
+ "x": -94.15168677219138,
222
+ "y": 14.525356969883305
223
+ },
224
+ "type": "basic",
225
+ "width": 200.0
226
+ },
227
+ {
228
+ "data": {
229
+ "__execution_delay": null,
230
+ "collapsed": true,
231
+ "display": null,
232
+ "error": null,
233
+ "meta": {
234
+ "inputs": {
235
+ "a": {
236
+ "name": "a",
237
+ "position": "bottom",
238
+ "type": {
239
+ "type": "tensor"
240
+ }
241
+ },
242
+ "b": {
243
+ "name": "b",
244
+ "position": "bottom",
245
+ "type": {
246
+ "type": "tensor"
247
+ }
248
+ }
249
+ },
250
+ "name": "Concatenate",
251
+ "outputs": {
252
+ "x": {
253
+ "name": "x",
254
+ "position": "top",
255
+ "type": {
256
+ "type": "tensor"
257
+ }
258
+ }
259
+ },
260
+ "params": {},
261
+ "type": "basic"
262
+ },
263
+ "params": {},
264
+ "status": "planned",
265
+ "title": "Concatenate"
266
+ },
267
+ "dragHandle": ".bg-primary",
268
+ "height": 200.0,
269
+ "id": "Concatenate 1",
270
+ "position": {
271
+ "x": 477.88148637482334,
272
+ "y": -372.62774030487003
273
+ },
274
+ "type": "basic",
275
+ "width": 200.0
276
+ },
277
+ {
278
+ "data": {
279
+ "__execution_delay": null,
280
+ "collapsed": true,
281
+ "display": null,
282
+ "error": null,
283
+ "meta": {
284
+ "inputs": {},
285
+ "name": "Input: graph edges",
286
+ "outputs": {
287
+ "edges": {
288
+ "name": "edges",
289
+ "position": "top",
290
+ "type": {
291
+ "type": "tensor"
292
+ }
293
+ }
294
+ },
295
+ "params": {},
296
+ "type": "basic"
297
+ },
298
+ "params": {},
299
+ "status": "planned",
300
+ "title": "Input: graph edges"
301
+ },
302
+ "dragHandle": ".bg-primary",
303
+ "height": 200.0,
304
+ "id": "Input: graph edges 1",
305
+ "position": {
306
+ "x": 515.6535517374441,
307
+ "y": 545.4709559884296
308
+ },
309
+ "type": "basic",
310
+ "width": 200.0
311
+ },
312
+ {
313
+ "data": {
314
+ "__execution_delay": null,
315
+ "collapsed": true,
316
+ "display": null,
317
+ "error": null,
318
+ "meta": {
319
+ "inputs": {},
320
+ "name": "Input: embedding",
321
+ "outputs": {
322
+ "x": {
323
+ "name": "x",
324
+ "position": "top",
325
+ "type": {
326
+ "type": "tensor"
327
+ }
328
+ }
329
+ },
330
+ "params": {},
331
+ "type": "basic"
332
+ },
333
+ "params": {},
334
+ "status": "planned",
335
+ "title": "Input: embedding"
336
+ },
337
+ "dragHandle": ".bg-primary",
338
+ "height": 200.0,
339
+ "id": "Input: embedding 1",
340
+ "position": {
341
+ "x": 246.6527948448857,
342
+ "y": 551.6313504198322
343
+ },
344
+ "type": "basic",
345
+ "width": 200.0
346
+ },
347
+ {
348
+ "data": {
349
+ "display": null,
350
+ "error": null,
351
+ "meta": {
352
+ "inputs": {
353
+ "x": {
354
+ "name": "x",
355
+ "position": "bottom",
356
+ "type": {
357
+ "type": "tensor"
358
+ }
359
+ }
360
+ },
361
+ "name": "Activation",
362
+ "outputs": {
363
+ "x": {
364
+ "name": "x",
365
+ "position": "top",
366
+ "type": {
367
+ "type": "tensor"
368
+ }
369
+ }
370
+ },
371
+ "params": {
372
+ "type": {
373
+ "default": "1",
374
+ "name": "type",
375
+ "type": {
376
+ "enum": [
377
+ "ReLU",
378
+ "LeakyReLU",
379
+ "Tanh",
380
+ "Mish"
381
+ ]
382
+ }
383
+ }
384
+ },
385
+ "type": "basic"
386
+ },
387
+ "params": {
388
+ "type": 1.0
389
+ },
390
+ "status": "planned",
391
+ "title": "Activation"
392
+ },
393
+ "dragHandle": ".bg-primary",
394
+ "height": 200.0,
395
+ "id": "Activation 1",
396
+ "position": {
397
+ "x": 354.3731834561054,
398
+ "y": -73.74768512965228
399
+ },
400
+ "type": "basic",
401
+ "width": 200.0
402
+ },
403
+ {
404
+ "data": {
405
+ "__execution_delay": null,
406
+ "collapsed": true,
407
+ "display": null,
408
+ "error": null,
409
+ "meta": {
410
+ "inputs": {
411
+ "h": {
412
+ "name": "h",
413
+ "position": "bottom",
414
+ "type": {
415
+ "type": "tensor"
416
+ }
417
+ },
418
+ "x": {
419
+ "name": "x",
420
+ "position": "bottom",
421
+ "type": {
422
+ "type": "tensor"
423
+ }
424
+ }
425
+ },
426
+ "name": "LSTM",
427
+ "outputs": {
428
+ "h": {
429
+ "name": "h",
430
+ "position": "top",
431
+ "type": {
432
+ "type": "tensor"
433
+ }
434
+ },
435
+ "x": {
436
+ "name": "x",
437
+ "position": "top",
438
+ "type": {
439
+ "type": "tensor"
440
+ }
441
+ }
442
+ },
443
+ "params": {},
444
+ "type": "basic"
445
+ },
446
+ "params": {},
447
+ "status": "planned",
448
+ "title": "LSTM"
449
+ },
450
+ "dragHandle": ".bg-primary",
451
+ "height": 200.0,
452
+ "id": "LSTM 1",
453
+ "position": {
454
+ "x": 960.0,
455
+ "y": 135.0
456
+ },
457
+ "type": "basic",
458
+ "width": 200.0
459
+ },
460
+ {
461
+ "data": {
462
+ "__execution_delay": null,
463
+ "collapsed": true,
464
+ "display": null,
465
+ "error": null,
466
+ "meta": {
467
+ "inputs": {},
468
+ "name": "Input: sequential",
469
+ "outputs": {
470
+ "y": {
471
+ "name": "y",
472
+ "position": "top",
473
+ "type": {
474
+ "type": "tensor"
475
+ }
476
+ }
477
+ },
478
+ "params": {},
479
+ "type": "basic"
480
+ },
481
+ "params": {},
482
+ "status": "planned",
483
+ "title": "Input: sequential"
484
+ },
485
+ "dragHandle": ".bg-primary",
486
+ "height": 200.0,
487
+ "id": "Input: sequential 1",
488
+ "position": {
489
+ "x": 1005.0,
490
+ "y": 510.0
491
+ },
492
+ "type": "basic",
493
+ "width": 200.0
494
+ },
495
+ {
496
+ "data": {
497
+ "__execution_delay": null,
498
+ "collapsed": true,
499
+ "display": null,
500
+ "error": null,
501
+ "meta": {
502
+ "inputs": {},
503
+ "name": "Input: zeros",
504
+ "outputs": {
505
+ "x": {
506
+ "name": "x",
507
+ "position": "top",
508
+ "type": {
509
+ "type": "tensor"
510
+ }
511
+ }
512
+ },
513
+ "params": {},
514
+ "type": "basic"
515
+ },
516
+ "params": {},
517
+ "status": "planned",
518
+ "title": "Input: zeros"
519
+ },
520
+ "dragHandle": ".bg-primary",
521
+ "height": 200.0,
522
+ "id": "Input: zeros 1",
523
+ "position": {
524
+ "x": 1290.0,
525
+ "y": 405.0
526
+ },
527
+ "type": "basic",
528
+ "width": 200.0
529
+ },
530
+ {
531
+ "data": {
532
+ "__execution_delay": null,
533
+ "collapsed": true,
534
+ "display": null,
535
+ "error": null,
536
+ "meta": {
537
+ "inputs": {
538
+ "input": {
539
+ "name": "input",
540
+ "position": "top",
541
+ "type": {
542
+ "type": "tensor"
543
+ }
544
+ }
545
+ },
546
+ "name": "Recurrent chain",
547
+ "outputs": {
548
+ "output": {
549
+ "name": "output",
550
+ "position": "bottom",
551
+ "type": {
552
+ "type": "tensor"
553
+ }
554
+ }
555
+ },
556
+ "params": {},
557
+ "type": "basic"
558
+ },
559
+ "params": {},
560
+ "status": "planned",
561
+ "title": "Recurrent chain"
562
+ },
563
+ "dragHandle": ".bg-primary",
564
+ "height": 200.0,
565
+ "id": "Recurrent chain 1",
566
+ "position": {
567
+ "x": 1224.6603040746108,
568
+ "y": 135.44839862151363
569
+ },
570
+ "type": "basic",
571
+ "width": 200.0
572
+ },
573
+ {
574
+ "data": {
575
+ "__execution_delay": null,
576
+ "collapsed": true,
577
+ "display": null,
578
+ "error": null,
579
+ "meta": {
580
+ "inputs": {
581
+ "x": {
582
+ "name": "x",
583
+ "position": "bottom",
584
+ "type": {
585
+ "type": "tensor"
586
+ }
587
+ },
588
+ "y": {
589
+ "name": "y",
590
+ "position": "bottom",
591
+ "type": {
592
+ "type": "tensor"
593
+ }
594
+ }
595
+ },
596
+ "name": "MSE loss",
597
+ "outputs": {
598
+ "loss": {
599
+ "name": "loss",
600
+ "position": "top",
601
+ "type": {
602
+ "type": "tensor"
603
+ }
604
+ }
605
+ },
606
+ "params": {},
607
+ "type": "basic"
608
+ },
609
+ "params": {},
610
+ "status": "planned",
611
+ "title": "MSE loss"
612
+ },
613
+ "dragHandle": ".bg-primary",
614
+ "height": 200.0,
615
+ "id": "MSE loss 1",
616
+ "position": {
617
+ "x": 915.0,
618
+ "y": -900.0
619
+ },
620
+ "type": "basic",
621
+ "width": 200.0
622
+ },
623
+ {
624
+ "data": {
625
+ "__execution_delay": null,
626
+ "collapsed": true,
627
+ "display": null,
628
+ "error": null,
629
+ "meta": {
630
+ "inputs": {},
631
+ "name": "Input: label",
632
+ "outputs": {
633
+ "y": {
634
+ "name": "y",
635
+ "position": "top",
636
+ "type": {
637
+ "type": "tensor"
638
+ }
639
+ }
640
+ },
641
+ "params": {},
642
+ "type": "basic"
643
+ },
644
+ "params": {},
645
+ "status": "planned",
646
+ "title": "Input: label"
647
+ },
648
+ "dragHandle": ".bg-primary",
649
+ "height": 200.0,
650
+ "id": "Input: label 1",
651
+ "position": {
652
+ "x": 1095.0,
653
+ "y": -450.0
654
+ },
655
+ "type": "basic",
656
+ "width": 200.0
657
+ },
658
+ {
659
+ "data": {
660
+ "display": null,
661
+ "error": null,
662
+ "meta": {
663
+ "inputs": {
664
+ "loss": {
665
+ "name": "loss",
666
+ "position": "bottom",
667
+ "type": {
668
+ "type": "tensor"
669
+ }
670
+ }
671
+ },
672
+ "name": "Optimizer",
673
+ "outputs": {},
674
+ "params": {
675
+ "lr": {
676
+ "default": 0.001,
677
+ "name": "lr",
678
+ "type": {
679
+ "type": "<class 'float'>"
680
+ }
681
+ },
682
+ "type": {
683
+ "default": "1",
684
+ "name": "type",
685
+ "type": {
686
+ "enum": [
687
+ "AdamW",
688
+ "Adafactor",
689
+ "Adagrad",
690
+ "SGD",
691
+ "Lion",
692
+ "Paged AdamW",
693
+ "Galore AdamW"
694
+ ]
695
+ }
696
+ }
697
+ },
698
+ "type": "basic"
699
+ },
700
+ "params": {
701
+ "lr": 0.001,
702
+ "type": 1.0
703
+ },
704
+ "status": "planned",
705
+ "title": "Optimizer"
706
+ },
707
+ "dragHandle": ".bg-primary",
708
+ "height": 247.0,
709
+ "id": "Optimizer 1",
710
+ "position": {
711
+ "x": 915.3430278730226,
712
+ "y": -1268.0577550022126
713
+ },
714
+ "type": "basic",
715
+ "width": 190.0
716
+ },
717
+ {
718
+ "data": {
719
+ "display": null,
720
+ "error": null,
721
+ "meta": {
722
+ "inputs": {
723
+ "x": {
724
+ "name": "x",
725
+ "position": "bottom",
726
+ "type": {
727
+ "type": "tensor"
728
+ }
729
+ }
730
+ },
731
+ "name": "Neural ODE",
732
+ "outputs": {
733
+ "x": {
734
+ "name": "x",
735
+ "position": "top",
736
+ "type": {
737
+ "type": "tensor"
738
+ }
739
+ }
740
+ },
741
+ "params": {
742
+ "absolute_tolerance": {
743
+ "default": null,
744
+ "name": "absolute_tolerance",
745
+ "type": {
746
+ "type": "None"
747
+ }
748
+ },
749
+ "method": {
750
+ "default": "1",
751
+ "name": "method",
752
+ "type": {
753
+ "enum": [
754
+ "dopri8",
755
+ "dopri5",
756
+ "bosh3",
757
+ "fehlberg2",
758
+ "adaptive_heun",
759
+ "euler",
760
+ "midpoint",
761
+ "rk4",
762
+ "explicit_adams",
763
+ "implicit_adams"
764
+ ]
765
+ }
766
+ },
767
+ "relative_tolerance": {
768
+ "default": null,
769
+ "name": "relative_tolerance",
770
+ "type": {
771
+ "type": "None"
772
+ }
773
+ }
774
+ },
775
+ "type": "basic"
776
+ },
777
+ "params": {
778
+ "absolute_tolerance": null,
779
+ "method": 1.0,
780
+ "relative_tolerance": null
781
+ },
782
+ "status": "planned",
783
+ "title": "Neural ODE"
784
+ },
785
+ "dragHandle": ".bg-primary",
786
+ "height": 200.0,
787
+ "id": "Neural ODE 2",
788
+ "position": {
789
+ "x": 342.3226409443945,
790
+ "y": -687.1882072175634
791
+ },
792
+ "type": "basic",
793
+ "width": 200.0
794
+ }
795
+ ]
796
+ }
examples/{PyTorch demo → PyTorch demo.lynxkite.json} RENAMED
File without changes
examples/{RAG chatbot app → RAG chatbot app.lynxkite.json} RENAMED
File without changes
examples/Word2vec.lynxkite.json ADDED
The diff for this file is too large to render. See raw diff
 
examples/fake_data.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from lynxkite.core.ops import op
2
+ from faker import Faker
3
+ import pandas as pd
4
+
5
+ faker = Faker()
6
+
7
+
8
+ @op("LynxKite Graph Analytics", "Fake data")
9
+ def fake(*, n=10):
10
+ df = pd.DataFrame(
11
+ {
12
+ "name": [faker.name() for _ in range(n)],
13
+ "address": [faker.address() for _ in range(n)],
14
+ }
15
+ )
16
+ return df
examples/requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ Faker
examples/{sql → sql.lynxkite.json} RENAMED
File without changes
examples/uploads/plus-one-dataset.parquet ADDED
Binary file (7.54 kB). View file
 
examples/word2vec.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from lynxkite.core.ops import op
2
+ import staticvectors
3
+ import pandas as pd
4
+
5
+ ENV = "LynxKite Graph Analytics"
6
+
7
+
8
+ @op(ENV, "Word2vec for the top 1000 words", slow=True)
9
+ def word2vec_1000():
10
+ model = staticvectors.StaticVectors("neuml/word2vec-quantized")
11
+ df = pd.read_csv(
12
+ "https://gist.githubusercontent.com/deekayen/4148741/raw/98d35708fa344717d8eee15d11987de6c8e26d7d/1-1000.txt",
13
+ names=["word"],
14
+ )
15
+ df["embedding"] = model.embeddings(df.word.tolist()).tolist()
16
+ return df
17
+
18
+
19
+ @op(ENV, "Take first N")
20
+ def first_n(df: pd.DataFrame, *, n=10):
21
+ return df.head(n)
22
+
23
+
24
+ @op(ENV, "Sample N")
25
+ def sample_n(df: pd.DataFrame, *, n=10):
26
+ return df.sample(n)
lynxkite-app/src/lynxkite_app/crdt.py CHANGED
@@ -26,11 +26,11 @@ def ws_exception_handler(exception, log):
26
  return True
27
 
28
 
29
- class WebsocketServer(pycrdt_websocket.WebsocketServer):
30
  async def init_room(self, name: str) -> pycrdt_websocket.YRoom:
31
  """Initialize a room for the workspace with the given name.
32
 
33
- The workspace is loaded from "crdt_data" if it exists there, or from "data", or a new workspace is created.
34
  """
35
  crdt_path = pathlib.Path(".crdt")
36
  path = crdt_path / f"{name}.crdt"
@@ -40,9 +40,7 @@ class WebsocketServer(pycrdt_websocket.WebsocketServer):
40
  ydoc["workspace"] = ws = pycrdt.Map()
41
  # Replay updates from the store.
42
  try:
43
- for update, timestamp in [
44
- (item[0], item[-1]) async for item in ystore.read()
45
- ]:
46
  ydoc.apply_update(update)
47
  except pycrdt_websocket.ystore.YDocNotFound:
48
  pass
@@ -84,14 +82,49 @@ class WebsocketServer(pycrdt_websocket.WebsocketServer):
84
  return room
85
 
86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  last_ws_input = None
88
 
89
 
90
  def clean_input(ws_pyd):
91
  for node in ws_pyd.nodes:
92
  node.data.display = None
 
93
  node.data.error = None
94
  node.data.status = workspace.NodeStatus.done
 
 
 
95
  node.position.x = 0
96
  node.position.y = 0
97
  if node.model_extra:
@@ -168,9 +201,12 @@ def try_to_load_workspace(ws: pycrdt.Map, name: str):
168
  """
169
  if os.path.exists(name):
170
  ws_pyd = workspace.load(name)
171
- # We treat the display field as a black box, since it is a large
172
- # dictionary that is meant to change as a whole.
173
- crdt_update(ws, ws_pyd.model_dump(), non_collaborative_fields={"display"})
 
 
 
174
 
175
 
176
  last_known_versions = {}
@@ -208,9 +244,7 @@ async def workspace_changed(name: str, changes: pycrdt.MapEvent, ws_crdt: pycrdt
208
  await execute(name, ws_crdt, ws_pyd)
209
 
210
 
211
- async def execute(
212
- name: str, ws_crdt: pycrdt.Map, ws_pyd: workspace.Workspace, delay: int = 0
213
- ):
214
  """Execute the workspace and update the CRDT object with the results.
215
 
216
  Args:
@@ -230,6 +264,7 @@ async def execute(
230
  assert path.is_relative_to(cwd), "Provided workspace path is invalid"
231
  # Save user changes before executing, in case the execution fails.
232
  workspace.save(ws_pyd, path)
 
233
  ws_pyd._crdt = ws_crdt
234
  with ws_crdt.doc.transaction():
235
  for nc, np in zip(ws_crdt["nodes"], ws_pyd.nodes):
@@ -243,14 +278,21 @@ async def execute(
243
  print(f"Finished running {name} in {ws_pyd.env}.")
244
 
245
 
 
 
 
 
 
 
246
  @contextlib.asynccontextmanager
247
  async def lifespan(app):
248
- global websocket_server
249
- websocket_server = WebsocketServer(
250
- auto_clean_rooms=False,
251
- )
252
- async with websocket_server:
253
- yield
 
254
  print("closing websocket server")
255
 
256
 
@@ -261,5 +303,12 @@ def sanitize_path(path):
261
  @router.websocket("/ws/crdt/{room_name}")
262
  async def crdt_websocket(websocket: fastapi.WebSocket, room_name: str):
263
  room_name = sanitize_path(room_name)
264
- server = pycrdt_websocket.ASGIServer(websocket_server)
 
 
 
 
 
 
 
265
  await server({"path": room_name}, websocket._receive, websocket._send)
 
26
  return True
27
 
28
 
29
+ class WorkspaceWebsocketServer(pycrdt_websocket.WebsocketServer):
30
  async def init_room(self, name: str) -> pycrdt_websocket.YRoom:
31
  """Initialize a room for the workspace with the given name.
32
 
33
+ The workspace is loaded from ".crdt" if it exists there, or from a JSON file, or a new workspace is created.
34
  """
35
  crdt_path = pathlib.Path(".crdt")
36
  path = crdt_path / f"{name}.crdt"
 
40
  ydoc["workspace"] = ws = pycrdt.Map()
41
  # Replay updates from the store.
42
  try:
43
+ for update, timestamp in [(item[0], item[-1]) async for item in ystore.read()]:
 
 
44
  ydoc.apply_update(update)
45
  except pycrdt_websocket.ystore.YDocNotFound:
46
  pass
 
82
  return room
83
 
84
 
85
+ class CodeWebsocketServer(WorkspaceWebsocketServer):
86
+ async def init_room(self, name: str) -> pycrdt_websocket.YRoom:
87
+ """Initialize a room for a text document with the given name."""
88
+ crdt_path = pathlib.Path(".crdt")
89
+ path = crdt_path / f"{name}.crdt"
90
+ assert path.is_relative_to(crdt_path)
91
+ ystore = pycrdt_websocket.ystore.FileYStore(path)
92
+ ydoc = pycrdt.Doc()
93
+ ydoc["text"] = text = pycrdt.Text()
94
+ # Replay updates from the store.
95
+ try:
96
+ for update, timestamp in [(item[0], item[-1]) async for item in ystore.read()]:
97
+ ydoc.apply_update(update)
98
+ except pycrdt_websocket.ystore.YDocNotFound:
99
+ pass
100
+ if len(text) == 0:
101
+ if os.path.exists(name):
102
+ with open(name) as f:
103
+ text += f.read()
104
+ room = pycrdt_websocket.YRoom(
105
+ ystore=ystore, ydoc=ydoc, exception_handler=ws_exception_handler
106
+ )
107
+ room.text = text
108
+
109
+ def on_change(changes):
110
+ asyncio.create_task(code_changed(name, changes, text))
111
+
112
+ text.observe(on_change)
113
+ return room
114
+
115
+
116
  last_ws_input = None
117
 
118
 
119
  def clean_input(ws_pyd):
120
  for node in ws_pyd.nodes:
121
  node.data.display = None
122
+ node.data.input_metadata = None
123
  node.data.error = None
124
  node.data.status = workspace.NodeStatus.done
125
+ for p in list(node.data.params):
126
+ if p.startswith("_"):
127
+ del node.data.params[p]
128
  node.position.x = 0
129
  node.position.y = 0
130
  if node.model_extra:
 
201
  """
202
  if os.path.exists(name):
203
  ws_pyd = workspace.load(name)
204
+ crdt_update(
205
+ ws,
206
+ ws_pyd.model_dump(),
207
+ # We treat some fields as black boxes. They are not edited on the frontend.
208
+ non_collaborative_fields={"display", "input_metadata"},
209
+ )
210
 
211
 
212
  last_known_versions = {}
 
244
  await execute(name, ws_crdt, ws_pyd)
245
 
246
 
247
+ async def execute(name: str, ws_crdt: pycrdt.Map, ws_pyd: workspace.Workspace, delay: int = 0):
 
 
248
  """Execute the workspace and update the CRDT object with the results.
249
 
250
  Args:
 
264
  assert path.is_relative_to(cwd), "Provided workspace path is invalid"
265
  # Save user changes before executing, in case the execution fails.
266
  workspace.save(ws_pyd, path)
267
+ ops.load_user_scripts(name)
268
  ws_pyd._crdt = ws_crdt
269
  with ws_crdt.doc.transaction():
270
  for nc, np in zip(ws_crdt["nodes"], ws_pyd.nodes):
 
278
  print(f"Finished running {name} in {ws_pyd.env}.")
279
 
280
 
281
+ async def code_changed(name: str, changes: pycrdt.TextEvent, text: pycrdt.Text):
282
+ # TODO: Make this more fancy?
283
+ with open(name, "w") as f:
284
+ f.write(str(text).strip() + "\n")
285
+
286
+
287
  @contextlib.asynccontextmanager
288
  async def lifespan(app):
289
+ global ws_websocket_server
290
+ global code_websocket_server
291
+ ws_websocket_server = WorkspaceWebsocketServer(auto_clean_rooms=False)
292
+ code_websocket_server = CodeWebsocketServer(auto_clean_rooms=False)
293
+ async with ws_websocket_server:
294
+ async with code_websocket_server:
295
+ yield
296
  print("closing websocket server")
297
 
298
 
 
303
  @router.websocket("/ws/crdt/{room_name}")
304
  async def crdt_websocket(websocket: fastapi.WebSocket, room_name: str):
305
  room_name = sanitize_path(room_name)
306
+ server = pycrdt_websocket.ASGIServer(ws_websocket_server)
307
+ await server({"path": room_name}, websocket._receive, websocket._send)
308
+
309
+
310
+ @router.websocket("/ws/code/crdt/{room_name}")
311
+ async def code_crdt_websocket(websocket: fastapi.WebSocket, room_name: str):
312
+ room_name = sanitize_path(room_name)
313
+ server = pycrdt_websocket.ASGIServer(code_websocket_server)
314
  await server({"path": room_name}, websocket._receive, websocket._send)
lynxkite-app/src/lynxkite_app/main.py CHANGED
@@ -26,6 +26,7 @@ def detect_plugins():
26
 
27
 
28
  lynxkite_plugins = detect_plugins()
 
29
 
30
  app = fastapi.FastAPI(lifespan=crdt.lifespan)
31
  app.include_router(crdt.router)
@@ -33,11 +34,9 @@ app.add_middleware(GZipMiddleware)
33
 
34
 
35
  @app.get("/api/catalog")
36
- def get_catalog():
37
- return {
38
- k: {op.name: op.model_dump() for op in v.values()}
39
- for k, v in ops.CATALOGS.items()
40
- }
41
 
42
 
43
  class SaveRequest(workspace.BaseConfig):
@@ -85,6 +84,15 @@ class DirectoryEntry(pydantic.BaseModel):
85
  type: str
86
 
87
 
 
 
 
 
 
 
 
 
 
88
  @app.get("/api/dir/list")
89
  def list_dir(path: str):
90
  path = data_path / path
@@ -93,7 +101,7 @@ def list_dir(path: str):
93
  [
94
  DirectoryEntry(
95
  name=str(p.relative_to(data_path)),
96
- type="directory" if p.is_dir() else "workspace",
97
  )
98
  for p in path.iterdir()
99
  if not p.name.startswith(".")
 
26
 
27
 
28
  lynxkite_plugins = detect_plugins()
29
+ ops.save_catalogs("plugins loaded")
30
 
31
  app = fastapi.FastAPI(lifespan=crdt.lifespan)
32
  app.include_router(crdt.router)
 
34
 
35
 
36
  @app.get("/api/catalog")
37
+ def get_catalog(workspace: str):
38
+ ops.load_user_scripts(workspace)
39
+ return {k: {op.name: op.model_dump() for op in v.values()} for k, v in ops.CATALOGS.items()}
 
 
40
 
41
 
42
  class SaveRequest(workspace.BaseConfig):
 
84
  type: str
85
 
86
 
87
+ def _get_path_type(path: pathlib.Path) -> str:
88
+ if path.is_dir():
89
+ return "directory"
90
+ elif path.suffixes[-2:] == [".lynxkite", ".json"]:
91
+ return "workspace"
92
+ else:
93
+ return "file"
94
+
95
+
96
  @app.get("/api/dir/list")
97
  def list_dir(path: str):
98
  path = data_path / path
 
101
  [
102
  DirectoryEntry(
103
  name=str(p.relative_to(data_path)),
104
+ type=_get_path_type(p),
105
  )
106
  for p in path.iterdir()
107
  if not p.name.startswith(".")
lynxkite-app/tests/test_main.py CHANGED
@@ -22,7 +22,7 @@ def test_detect_plugins_with_plugins():
22
 
23
 
24
  def test_get_catalog():
25
- response = client.get("/api/catalog")
26
  assert response.status_code == 200
27
 
28
 
@@ -37,6 +37,7 @@ def test_save_and_load():
37
  "type": "basic",
38
  "data": {
39
  "display": None,
 
40
  "error": "Unknown operation.",
41
  "title": "Test node",
42
  "params": {"param1": "value"},
@@ -58,15 +59,22 @@ def test_save_and_load():
58
  def test_list_dir():
59
  test_dir = pathlib.Path() / str(uuid.uuid4())
60
  test_dir.mkdir(parents=True, exist_ok=True)
61
- test_file = test_dir / "test_file.txt"
62
- test_file.touch()
 
 
 
 
63
  response = client.get(f"/api/dir/list?path={str(test_dir)}")
64
  assert response.status_code == 200
65
- assert len(response.json()) == 1
66
- assert response.json()[0]["name"] == f"{test_dir}/test_file.txt"
67
- assert response.json()[0]["type"] == "workspace"
68
- test_file.unlink()
69
- test_dir.rmdir()
 
 
 
70
 
71
 
72
  def test_make_dir():
 
22
 
23
 
24
  def test_get_catalog():
25
+ response = client.get("/api/catalog?workspace=test")
26
  assert response.status_code == 200
27
 
28
 
 
37
  "type": "basic",
38
  "data": {
39
  "display": None,
40
+ "input_metadata": None,
41
  "error": "Unknown operation.",
42
  "title": "Test node",
43
  "params": {"param1": "value"},
 
59
  def test_list_dir():
60
  test_dir = pathlib.Path() / str(uuid.uuid4())
61
  test_dir.mkdir(parents=True, exist_ok=True)
62
+ dir = test_dir / "test_dir"
63
+ dir.mkdir(exist_ok=True)
64
+ file = test_dir / "test_file.txt"
65
+ file.touch()
66
+ ws = test_dir / "test_workspace.lynxkite.json"
67
+ ws.touch()
68
  response = client.get(f"/api/dir/list?path={str(test_dir)}")
69
  assert response.status_code == 200
70
+ assert response.json() == [
71
+ {"name": f"{test_dir}/test_dir", "type": "directory"},
72
+ {"name": f"{test_dir}/test_file.txt", "type": "file"},
73
+ {"name": f"{test_dir}/test_workspace.lynxkite.json", "type": "workspace"},
74
+ ]
75
+ file.unlink()
76
+ ws.unlink()
77
+ dir.rmdir()
78
 
79
 
80
  def test_make_dir():
lynxkite-app/web/eslint.config.js CHANGED
@@ -19,10 +19,7 @@ export default tseslint.config(
19
  },
20
  rules: {
21
  ...reactHooks.configs.recommended.rules,
22
- "react-refresh/only-export-components": [
23
- "warn",
24
- { allowConstantExport: true },
25
- ],
26
  },
27
  },
28
  );
 
19
  },
20
  rules: {
21
  ...reactHooks.configs.recommended.rules,
22
+ "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
 
 
 
23
  },
24
  },
25
  );
lynxkite-app/web/index.html CHANGED
@@ -4,7 +4,6 @@
4
  <meta charset="UTF-8" />
5
  <link rel="icon" type="image/svg+xml" href="/src/assets/favicon.ico" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>LynxKite 2025</title>
8
  </head>
9
  <body>
10
  <div id="root"></div>
 
4
  <meta charset="UTF-8" />
5
  <link rel="icon" type="image/svg+xml" href="/src/assets/favicon.ico" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 
7
  </head>
8
  <body>
9
  <div id="root"></div>
lynxkite-app/web/package-lock.json CHANGED
@@ -10,6 +10,7 @@
10
  "dependencies": {
11
  "@esbuild/linux-x64": "^0.25.0",
12
  "@iconify-json/tabler": "^1.2.10",
 
13
  "@svgr/core": "^8.1.0",
14
  "@svgr/plugin-jsx": "^8.1.0",
15
  "@swc/core": "^1.10.1",
@@ -28,6 +29,7 @@
28
  "react-router-dom": "^7.0.2",
29
  "swr": "^2.2.5",
30
  "unplugin-icons": "^0.21.0",
 
31
  "y-websocket": "^2.0.4",
32
  "yjs": "^13.6.20"
33
  },
@@ -1105,6 +1107,29 @@
1105
  "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
1106
  "license": "MIT"
1107
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1108
  "node_modules/@nodelib/fs.scandir": {
1109
  "version": "2.1.5",
1110
  "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -5436,6 +5461,13 @@
5436
  "ufo": "^1.5.4"
5437
  }
5438
  },
 
 
 
 
 
 
 
5439
  "node_modules/ms": {
5440
  "version": "2.1.3",
5441
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -6437,6 +6469,12 @@
6437
  "url": "https://github.com/sponsors/wooorm"
6438
  }
6439
  },
 
 
 
 
 
 
6440
  "node_modules/string_decoder": {
6441
  "version": "1.3.0",
6442
  "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -7360,6 +7398,23 @@
7360
  "yjs": "^13.0.0"
7361
  }
7362
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7363
  "node_modules/y-protocols": {
7364
  "version": "1.0.6",
7365
  "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz",
 
10
  "dependencies": {
11
  "@esbuild/linux-x64": "^0.25.0",
12
  "@iconify-json/tabler": "^1.2.10",
13
+ "@monaco-editor/react": "^4.7.0",
14
  "@svgr/core": "^8.1.0",
15
  "@svgr/plugin-jsx": "^8.1.0",
16
  "@swc/core": "^1.10.1",
 
29
  "react-router-dom": "^7.0.2",
30
  "swr": "^2.2.5",
31
  "unplugin-icons": "^0.21.0",
32
+ "y-monaco": "^0.1.6",
33
  "y-websocket": "^2.0.4",
34
  "yjs": "^13.6.20"
35
  },
 
1107
  "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
1108
  "license": "MIT"
1109
  },
1110
+ "node_modules/@monaco-editor/loader": {
1111
+ "version": "1.5.0",
1112
+ "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz",
1113
+ "integrity": "sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==",
1114
+ "license": "MIT",
1115
+ "dependencies": {
1116
+ "state-local": "^1.0.6"
1117
+ }
1118
+ },
1119
+ "node_modules/@monaco-editor/react": {
1120
+ "version": "4.7.0",
1121
+ "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz",
1122
+ "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==",
1123
+ "license": "MIT",
1124
+ "dependencies": {
1125
+ "@monaco-editor/loader": "^1.5.0"
1126
+ },
1127
+ "peerDependencies": {
1128
+ "monaco-editor": ">= 0.25.0 < 1",
1129
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
1130
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
1131
+ }
1132
+ },
1133
  "node_modules/@nodelib/fs.scandir": {
1134
  "version": "2.1.5",
1135
  "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
 
5461
  "ufo": "^1.5.4"
5462
  }
5463
  },
5464
+ "node_modules/monaco-editor": {
5465
+ "version": "0.52.2",
5466
+ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz",
5467
+ "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==",
5468
+ "license": "MIT",
5469
+ "peer": true
5470
+ },
5471
  "node_modules/ms": {
5472
  "version": "2.1.3",
5473
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
 
6469
  "url": "https://github.com/sponsors/wooorm"
6470
  }
6471
  },
6472
+ "node_modules/state-local": {
6473
+ "version": "1.0.7",
6474
+ "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
6475
+ "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
6476
+ "license": "MIT"
6477
+ },
6478
  "node_modules/string_decoder": {
6479
  "version": "1.3.0",
6480
  "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
 
7398
  "yjs": "^13.0.0"
7399
  }
7400
  },
7401
+ "node_modules/y-monaco": {
7402
+ "version": "0.1.6",
7403
+ "resolved": "https://registry.npmjs.org/y-monaco/-/y-monaco-0.1.6.tgz",
7404
+ "integrity": "sha512-sYRywMmcylt+Nupl+11AvizD2am06ST8lkVbUXuaEmrtV6Tf+TD4rsEm6u9YGGowYue+Vfg1IJ97SUP2J+PVXg==",
7405
+ "license": "MIT",
7406
+ "dependencies": {
7407
+ "lib0": "^0.2.43"
7408
+ },
7409
+ "engines": {
7410
+ "node": ">=12.0.0",
7411
+ "npm": ">=6.0.0"
7412
+ },
7413
+ "peerDependencies": {
7414
+ "monaco-editor": ">=0.20.0",
7415
+ "yjs": "^13.3.1"
7416
+ }
7417
+ },
7418
  "node_modules/y-protocols": {
7419
  "version": "1.0.6",
7420
  "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz",
lynxkite-app/web/package.json CHANGED
@@ -13,6 +13,7 @@
13
  "dependencies": {
14
  "@esbuild/linux-x64": "^0.25.0",
15
  "@iconify-json/tabler": "^1.2.10",
 
16
  "@svgr/core": "^8.1.0",
17
  "@svgr/plugin-jsx": "^8.1.0",
18
  "@swc/core": "^1.10.1",
@@ -31,6 +32,7 @@
31
  "react-router-dom": "^7.0.2",
32
  "swr": "^2.2.5",
33
  "unplugin-icons": "^0.21.0",
 
34
  "y-websocket": "^2.0.4",
35
  "yjs": "^13.6.20"
36
  },
 
13
  "dependencies": {
14
  "@esbuild/linux-x64": "^0.25.0",
15
  "@iconify-json/tabler": "^1.2.10",
16
+ "@monaco-editor/react": "^4.7.0",
17
  "@svgr/core": "^8.1.0",
18
  "@svgr/plugin-jsx": "^8.1.0",
19
  "@swc/core": "^1.10.1",
 
32
  "react-router-dom": "^7.0.2",
33
  "swr": "^2.2.5",
34
  "unplugin-icons": "^0.21.0",
35
+ "y-monaco": "^0.1.6",
36
  "y-websocket": "^2.0.4",
37
  "yjs": "^13.6.20"
38
  },
lynxkite-app/web/playwright.config.ts CHANGED
@@ -7,6 +7,7 @@ export default defineConfig({
7
  /* Fail the build on CI if you accidentally left test.only in the source code. */
8
  forbidOnly: !!process.env.CI,
9
  retries: process.env.CI ? 1 : 0,
 
10
  workers: 1,
11
  reporter: process.env.CI ? [["github"], ["html"]] : "html",
12
  use: {
@@ -24,7 +25,7 @@ export default defineConfig({
24
  ],
25
  webServer: {
26
  command: "cd ../../examples && lynxkite",
27
- url: "http://127.0.0.1:8000",
28
  reuseExistingServer: false,
29
  },
30
  });
 
7
  /* Fail the build on CI if you accidentally left test.only in the source code. */
8
  forbidOnly: !!process.env.CI,
9
  retries: process.env.CI ? 1 : 0,
10
+ maxFailures: 5,
11
  workers: 1,
12
  reporter: process.env.CI ? [["github"], ["html"]] : "html",
13
  use: {
 
25
  ],
26
  webServer: {
27
  command: "cd ../../examples && lynxkite",
28
+ port: 8000,
29
  reuseExistingServer: false,
30
  },
31
  });
lynxkite-app/web/src/Code.tsx ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Full-page editor for code files.
2
+
3
+ import Editor, { type Monaco } from "@monaco-editor/react";
4
+ import type { editor } from "monaco-editor";
5
+ import { useEffect, useRef } from "react";
6
+ import { useParams } from "react-router";
7
+ import { WebsocketProvider } from "y-websocket";
8
+ import * as Y from "yjs";
9
+ // @ts-ignore
10
+ import Atom from "~icons/tabler/atom.jsx";
11
+ // @ts-ignore
12
+ import Backspace from "~icons/tabler/backspace.jsx";
13
+ // @ts-ignore
14
+ import Close from "~icons/tabler/x.jsx";
15
+ import favicon from "./assets/favicon.ico";
16
+ import theme from "./code-theme.ts";
17
+
18
+ export default function Code() {
19
+ const { path } = useParams();
20
+ const parentDir = path!.split("/").slice(0, -1).join("/");
21
+ const yDocRef = useRef<any>();
22
+ const wsProviderRef = useRef<any>();
23
+ const monacoBindingRef = useRef<any>();
24
+ const yMonacoRef = useRef<any>();
25
+ const editorRef = useRef<any>();
26
+ useEffect(() => {
27
+ const loadMonaco = async () => {
28
+ // y-monaco is gigantic. The other Monaco packages are small.
29
+ yMonacoRef.current = await import("y-monaco");
30
+ initCRDT();
31
+ };
32
+ loadMonaco();
33
+ }, []);
34
+ function beforeMount(monaco: Monaco) {
35
+ monaco.editor.defineTheme("lynxkite", theme);
36
+ }
37
+ function onMount(_editor: editor.IStandaloneCodeEditor) {
38
+ editorRef.current = _editor;
39
+ initCRDT();
40
+ }
41
+ function initCRDT() {
42
+ if (!yMonacoRef.current || !editorRef.current) return;
43
+ if (yDocRef.current) return;
44
+ yDocRef.current = new Y.Doc();
45
+ const text = yDocRef.current.getText("text");
46
+ const proto = location.protocol === "https:" ? "wss:" : "ws:";
47
+ wsProviderRef.current = new WebsocketProvider(
48
+ `${proto}//${location.host}/ws/code/crdt`,
49
+ path!,
50
+ yDocRef.current,
51
+ );
52
+ monacoBindingRef.current = new yMonacoRef.current.MonacoBinding(
53
+ text,
54
+ editorRef.current.getModel()!,
55
+ new Set([editorRef.current]),
56
+ wsProviderRef.current.awareness,
57
+ );
58
+ }
59
+ useEffect(() => {
60
+ return () => {
61
+ yDocRef.current?.destroy();
62
+ wsProviderRef.current?.destroy();
63
+ monacoBindingRef.current?.destroy();
64
+ };
65
+ });
66
+ return (
67
+ <div className="workspace">
68
+ <div className="top-bar bg-neutral">
69
+ <a className="logo" href="">
70
+ <img alt="" src={favicon} />
71
+ </a>
72
+ <div className="ws-name">{path}</div>
73
+ <div className="tools text-secondary">
74
+ <a href="">
75
+ <Atom />
76
+ </a>
77
+ <a href="">
78
+ <Backspace />
79
+ </a>
80
+ <a href={`/dir/${parentDir}`}>
81
+ <Close />
82
+ </a>
83
+ </div>
84
+ </div>
85
+ <Editor
86
+ defaultLanguage="python"
87
+ theme="lynxkite"
88
+ path={path}
89
+ beforeMount={beforeMount}
90
+ onMount={onMount}
91
+ loading={null}
92
+ options={{
93
+ cursorStyle: "block",
94
+ cursorBlinking: "solid",
95
+ minimap: { enabled: false },
96
+ renderLineHighlight: "none",
97
+ }}
98
+ />
99
+ </div>
100
+ );
101
+ }
lynxkite-app/web/src/Directory.tsx CHANGED
@@ -15,9 +15,46 @@ import FolderPlus from "~icons/tabler/folder-plus";
15
  // @ts-ignore
16
  import Home from "~icons/tabler/home";
17
  // @ts-ignore
 
 
 
 
18
  import Trash from "~icons/tabler/trash";
19
  import logo from "./assets/logo.png";
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  const fetcher = (url: string) => fetch(url).then((res) => res.json());
22
 
23
  export default function () {
@@ -27,68 +64,51 @@ export default function () {
27
  dedupingInterval: 0,
28
  });
29
  const navigate = useNavigate();
30
- const [isCreatingDir, setIsCreatingDir] = useState(false);
31
- const [isCreatingWorkspace, setIsCreatingWorkspace] = useState(false);
32
 
33
  function link(item: DirectoryEntry) {
34
  if (item.type === "directory") {
35
  return `/dir/${item.name}`;
36
  }
37
- return `/edit/${item.name}`;
 
 
 
38
  }
39
 
40
  function shortName(item: DirectoryEntry) {
41
- return item.name.split("/").pop();
 
 
 
42
  }
43
 
44
- function newName(list: DirectoryEntry[], baseName = "Untitled") {
45
- let i = 0;
46
- while (true) {
47
- const name = `${baseName}${i ? ` ${i}` : ""}`;
48
- if (!list.find((item) => item.name === name)) {
49
- return name;
50
- }
51
- i++;
52
- }
53
  }
54
-
55
- function newWorkspaceIn(
56
- path: string,
57
- list: DirectoryEntry[],
58
- workspaceName?: string,
59
- ) {
60
  const pathSlash = path ? `${path}/` : "";
61
- const name = workspaceName || newName(list);
62
- navigate(`/edit/${pathSlash}${name}`, { replace: true });
63
  }
64
-
65
- async function newFolderIn(
66
- path: string,
67
- list: DirectoryEntry[],
68
- folderName?: string,
69
- ) {
70
- const name = folderName || newName(list, "New Folder");
71
  const pathSlash = path ? `${path}/` : "";
72
-
73
  const res = await fetch("/api/dir/mkdir", {
74
  method: "POST",
75
  headers: { "Content-Type": "application/json" },
76
- body: JSON.stringify({ path: pathSlash + name }),
77
  });
78
  if (res.ok) {
79
- navigate(`/dir/${pathSlash}${name}`);
80
  } else {
81
  alert("Failed to create folder.");
82
  }
83
  }
84
 
85
  async function deleteItem(item: DirectoryEntry) {
86
- if (!window.confirm(`Are you sure you want to delete "${item.name}"?`))
87
- return;
88
  const pathSlash = path ? `${path}/` : "";
89
 
90
- const apiPath =
91
- item.type === "directory" ? "/api/dir/delete" : "/api/delete";
92
  await fetch(apiPath, {
93
  method: "POST",
94
  headers: { "Content-Type": "application/json" },
@@ -115,89 +135,66 @@ export default function () {
115
  {list.data && (
116
  <>
117
  <div className="actions">
118
- <div className="new-workspace">
119
- {isCreatingWorkspace && (
120
- // @ts-ignore
121
- <form
122
- onSubmit={(e) => {
123
- e.preventDefault();
124
- newWorkspaceIn(
125
- path || "",
126
- list.data,
127
- (
128
- e.target as HTMLFormElement
129
- ).workspaceName.value.trim(),
130
- );
131
- }}
132
- >
133
- <input
134
- type="text"
135
- name="workspaceName"
136
- defaultValue={newName(list.data)}
137
- placeholder={newName(list.data)}
138
- />
139
- </form>
140
- )}
141
- <button
142
- type="button"
143
- onClick={() => setIsCreatingWorkspace(true)}
144
- >
145
- <FolderPlus /> New workspace
146
- </button>
147
- </div>
148
-
149
- <div className="new-folder">
150
- {isCreatingDir && (
151
- // @ts-ignore
152
- <form
153
- onSubmit={(e) => {
154
- e.preventDefault();
155
- newFolderIn(
156
- path || "",
157
- list.data,
158
- (e.target as HTMLFormElement).folderName.value.trim(),
159
- );
160
- }}
161
- >
162
- <input
163
- type="text"
164
- name="folderName"
165
- defaultValue={newName(list.data)}
166
- placeholder={newName(list.data)}
167
- />
168
- </form>
169
- )}
170
- <button type="button" onClick={() => setIsCreatingDir(true)}>
171
- <FolderPlus /> New folder
172
- </button>
173
- </div>
174
  </div>
175
 
176
- {path && (
177
  <div className="breadcrumbs">
178
  <Link to="/dir/">
179
  <Home />
180
  </Link>{" "}
181
  <span className="current-folder">{path}</span>
 
182
  </div>
 
 
183
  )}
184
 
185
- {list.data.map((item: DirectoryEntry) => (
186
- <div key={item.name} className="entry">
187
- <Link key={link(item)} to={link(item)}>
188
- {item.type === "directory" ? <Folder /> : <File />}
189
- {shortName(item)}
190
- </Link>
191
- <button
192
- type="button"
193
- onClick={() => {
194
- deleteItem(item);
195
- }}
196
- >
197
- <Trash />
198
- </button>
199
- </div>
200
- ))}
 
 
 
 
 
 
 
 
 
201
  </>
202
  )}
203
  </div>{" "}
 
15
  // @ts-ignore
16
  import Home from "~icons/tabler/home";
17
  // @ts-ignore
18
+ import LayoutGrid from "~icons/tabler/layout-grid";
19
+ // @ts-ignore
20
+ import LayoutGridAdd from "~icons/tabler/layout-grid-add";
21
+ // @ts-ignore
22
  import Trash from "~icons/tabler/trash";
23
  import logo from "./assets/logo.png";
24
 
25
+ function EntryCreator(props: {
26
+ label: string;
27
+ icon: JSX.Element;
28
+ onCreate: (name: string) => void;
29
+ }) {
30
+ const [isCreating, setIsCreating] = useState(false);
31
+ return (
32
+ <>
33
+ {isCreating ? (
34
+ <form
35
+ onSubmit={(e) => {
36
+ e.preventDefault();
37
+ props.onCreate((e.target as HTMLFormElement).entryName.value.trim());
38
+ }}
39
+ >
40
+ <input
41
+ className="input input-ghost w-full"
42
+ autoFocus
43
+ type="text"
44
+ name="entryName"
45
+ onBlur={() => setIsCreating(false)}
46
+ placeholder={`${props.label} name`}
47
+ />
48
+ </form>
49
+ ) : (
50
+ <button type="button" onClick={() => setIsCreating(true)}>
51
+ {props.icon} {props.label}
52
+ </button>
53
+ )}
54
+ </>
55
+ );
56
+ }
57
+
58
  const fetcher = (url: string) => fetch(url).then((res) => res.json());
59
 
60
  export default function () {
 
64
  dedupingInterval: 0,
65
  });
66
  const navigate = useNavigate();
 
 
67
 
68
  function link(item: DirectoryEntry) {
69
  if (item.type === "directory") {
70
  return `/dir/${item.name}`;
71
  }
72
+ if (item.type === "workspace") {
73
+ return `/edit/${item.name}`;
74
+ }
75
+ return `/code/${item.name}`;
76
  }
77
 
78
  function shortName(item: DirectoryEntry) {
79
+ return item.name
80
+ .split("/")
81
+ .pop()
82
+ ?.replace(/[.]lynxkite[.]json$/, "");
83
  }
84
 
85
+ function newWorkspaceIn(path: string, workspaceName: string) {
86
+ const pathSlash = path ? `${path}/` : "";
87
+ navigate(`/edit/${pathSlash}${workspaceName}.lynxkite.json`, { replace: true });
 
 
 
 
 
 
88
  }
89
+ function newCodeFile(path: string, name: string) {
 
 
 
 
 
90
  const pathSlash = path ? `${path}/` : "";
91
+ navigate(`/code/${pathSlash}${name}`, { replace: true });
 
92
  }
93
+ async function newFolderIn(path: string, folderName: string) {
 
 
 
 
 
 
94
  const pathSlash = path ? `${path}/` : "";
 
95
  const res = await fetch("/api/dir/mkdir", {
96
  method: "POST",
97
  headers: { "Content-Type": "application/json" },
98
+ body: JSON.stringify({ path: pathSlash + folderName }),
99
  });
100
  if (res.ok) {
101
+ navigate(`/dir/${pathSlash}${folderName}`);
102
  } else {
103
  alert("Failed to create folder.");
104
  }
105
  }
106
 
107
  async function deleteItem(item: DirectoryEntry) {
108
+ if (!window.confirm(`Are you sure you want to delete "${item.name}"?`)) return;
 
109
  const pathSlash = path ? `${path}/` : "";
110
 
111
+ const apiPath = item.type === "directory" ? "/api/dir/delete" : "/api/delete";
 
112
  await fetch(apiPath, {
113
  method: "POST",
114
  headers: { "Content-Type": "application/json" },
 
135
  {list.data && (
136
  <>
137
  <div className="actions">
138
+ <EntryCreator
139
+ onCreate={(name) => {
140
+ newWorkspaceIn(path || "", name);
141
+ }}
142
+ icon={<LayoutGridAdd />}
143
+ label="New workspace"
144
+ />
145
+ <EntryCreator
146
+ onCreate={(name) => {
147
+ newCodeFile(path || "", name);
148
+ }}
149
+ icon={<FilePlus />}
150
+ label="New code file"
151
+ />
152
+ <EntryCreator
153
+ onCreate={(name: string) => {
154
+ newFolderIn(path || "", name);
155
+ }}
156
+ icon={<FolderPlus />}
157
+ label="New folder"
158
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  </div>
160
 
161
+ {path ? (
162
  <div className="breadcrumbs">
163
  <Link to="/dir/">
164
  <Home />
165
  </Link>{" "}
166
  <span className="current-folder">{path}</span>
167
+ <title>{path}</title>
168
  </div>
169
+ ) : (
170
+ <title>LynxKite 2000:MM</title>
171
  )}
172
 
173
+ {list.data.map(
174
+ (item: DirectoryEntry) =>
175
+ !shortName(item)?.startsWith("__") && (
176
+ <div key={item.name} className="entry">
177
+ <Link key={link(item)} to={link(item)}>
178
+ {item.type === "directory" ? (
179
+ <Folder />
180
+ ) : item.type === "workspace" ? (
181
+ <LayoutGrid />
182
+ ) : (
183
+ <File />
184
+ )}
185
+ {shortName(item)}
186
+ </Link>
187
+ <button
188
+ type="button"
189
+ onClick={() => {
190
+ deleteItem(item);
191
+ }}
192
+ >
193
+ <Trash />
194
+ </button>
195
+ </div>
196
+ ),
197
+ )}
198
  </>
199
  )}
200
  </div>{" "}
lynxkite-app/web/src/apiTypes.ts CHANGED
@@ -5,6 +5,8 @@
5
  /* Do not modify it by hand - just update the pydantic models and then re-run the script
6
  */
7
 
 
 
8
  export interface DirectoryEntry {
9
  name: string;
10
  type: string;
@@ -40,8 +42,9 @@ export interface WorkspaceNodeData {
40
  [k: string]: unknown;
41
  };
42
  display?: unknown;
 
43
  error?: string | null;
44
- in_progress?: boolean;
45
  [k: string]: unknown;
46
  }
47
  export interface Position {
 
5
  /* Do not modify it by hand - just update the pydantic models and then re-run the script
6
  */
7
 
8
+ export type NodeStatus = "planned" | "active" | "done";
9
+
10
  export interface DirectoryEntry {
11
  name: string;
12
  type: string;
 
42
  [k: string]: unknown;
43
  };
44
  display?: unknown;
45
+ input_metadata?: unknown;
46
  error?: string | null;
47
+ status?: NodeStatus;
48
  [k: string]: unknown;
49
  }
50
  export interface Position {
lynxkite-app/web/src/code-theme.ts ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // A simple theme using the LynxKite colors.
2
+
3
+ import type { editor } from "monaco-editor/esm/vs/editor/editor.api";
4
+
5
+ const theme: editor.IStandaloneThemeData = {
6
+ base: "vs-dark",
7
+ inherit: true,
8
+ rules: [
9
+ {
10
+ foreground: "ff8800",
11
+ token: "keyword",
12
+ },
13
+ {
14
+ foreground: "0088ff",
15
+ fontStyle: "italic",
16
+ token: "comment",
17
+ },
18
+ {
19
+ foreground: "39bcf3",
20
+ token: "string",
21
+ },
22
+ {
23
+ foreground: "ffc600",
24
+ token: "",
25
+ },
26
+ ],
27
+ colors: {
28
+ "editor.foreground": "#FFFFFF",
29
+ "editor.background": "#002a4c",
30
+ "editor.selectionBackground": "#0050a4",
31
+ "editor.lineHighlightBackground": "#1f4662",
32
+ "editorCursor.foreground": "#ffc600",
33
+ "editorWhitespace.foreground": "#7f7f7fb2",
34
+ "editorIndentGuide.background": "#3b5364",
35
+ "editorIndentGuide.activeBackground": "#ffc600",
36
+ },
37
+ };
38
+ export default theme;
lynxkite-app/web/src/index.css CHANGED
@@ -102,15 +102,14 @@ body {
102
  --status-color-1: oklch(75% 0.2 55);
103
  --status-color-2: oklch(75% 0.2 55);
104
  --status-color-3: oklch(75% 0.2 55);
105
- transition: --status-color-1 0.3s, --status-color-2 0.3s, --status-color-3
106
- 0.3s;
107
  }
108
 
109
  .lynxkite-node .title.active {
110
  --status-color-1: oklch(75% 0.2 55);
111
  --status-color-2: oklch(90% 0.2 55);
112
  --status-color-3: oklch(75% 0.1 55);
113
- /* animation: active-node-gradient-animation 2s ease-in-out infinite; */
114
  }
115
 
116
  .lynxkite-node .title.planned {
@@ -256,6 +255,14 @@ body {
256
  cursor: pointer;
257
  }
258
  }
 
 
 
 
 
 
 
 
259
  }
260
 
261
  .params-expander {
@@ -319,7 +326,14 @@ body {
319
  .actions {
320
  display: flex;
321
  justify-content: space-evenly;
 
 
322
  padding: 5px;
 
 
 
 
 
323
  }
324
 
325
  .actions a {
@@ -527,3 +541,25 @@ body {
527
  .add-relationship-button:hover {
528
  background-color: #218838;
529
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  --status-color-1: oklch(75% 0.2 55);
103
  --status-color-2: oklch(75% 0.2 55);
104
  --status-color-3: oklch(75% 0.2 55);
105
+ transition: --status-color-1 0.3s, --status-color-2 0.3s, --status-color-3 0.3s;
 
106
  }
107
 
108
  .lynxkite-node .title.active {
109
  --status-color-1: oklch(75% 0.2 55);
110
  --status-color-2: oklch(90% 0.2 55);
111
  --status-color-3: oklch(75% 0.1 55);
112
+ animation: active-node-gradient-animation 2s ease-in-out infinite;
113
  }
114
 
115
  .lynxkite-node .title.planned {
 
255
  cursor: pointer;
256
  }
257
  }
258
+
259
+ .model-mapping-param {
260
+ border: 1px solid var(--fallback-bc, oklch(var(--bc) / 0.2));
261
+ border-collapse: separate;
262
+ border-radius: 5px;
263
+ padding: 5px 10px;
264
+ width: 100%;
265
+ }
266
  }
267
 
268
  .params-expander {
 
326
  .actions {
327
  display: flex;
328
  justify-content: space-evenly;
329
+ align-items: center;
330
+ height: 50px;
331
  padding: 5px;
332
+
333
+ form,
334
+ button {
335
+ flex: 1;
336
+ }
337
  }
338
 
339
  .actions a {
 
541
  .add-relationship-button:hover {
542
  background-color: #218838;
543
  }
544
+
545
+ .yRemoteSelection {
546
+ background-color: rgb(250, 129, 0, 0.5);
547
+ }
548
+
549
+ .yRemoteSelectionHead {
550
+ position: absolute;
551
+ border-left: #ff8800 solid 2px;
552
+ border-top: #ff8800 solid 2px;
553
+ border-bottom: #ff8800 solid 2px;
554
+ height: 100%;
555
+ box-sizing: border-box;
556
+ }
557
+
558
+ .yRemoteSelectionHead::after {
559
+ position: absolute;
560
+ content: " ";
561
+ border: 3px solid #ff8800;
562
+ border-radius: 4px;
563
+ left: -4px;
564
+ top: -5px;
565
+ }
lynxkite-app/web/src/main.tsx CHANGED
@@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client";
3
  import "@xyflow/react/dist/style.css";
4
  import "./index.css";
5
  import { BrowserRouter, Route, Routes } from "react-router";
 
6
  import Directory from "./Directory.tsx";
7
  import Workspace from "./workspace/Workspace.tsx";
8
 
@@ -14,6 +15,7 @@ createRoot(document.getElementById("root")!).render(
14
  <Route path="/dir" element={<Directory />} />
15
  <Route path="/dir/:path" element={<Directory />} />
16
  <Route path="/edit/:path" element={<Workspace />} />
 
17
  </Routes>
18
  </BrowserRouter>
19
  </StrictMode>,
 
3
  import "@xyflow/react/dist/style.css";
4
  import "./index.css";
5
  import { BrowserRouter, Route, Routes } from "react-router";
6
+ import Code from "./Code.tsx";
7
  import Directory from "./Directory.tsx";
8
  import Workspace from "./workspace/Workspace.tsx";
9
 
 
15
  <Route path="/dir" element={<Directory />} />
16
  <Route path="/dir/:path" element={<Directory />} />
17
  <Route path="/edit/:path" element={<Workspace />} />
18
+ <Route path="/code/:path" element={<Code />} />
19
  </Routes>
20
  </BrowserRouter>
21
  </StrictMode>,
lynxkite-app/web/src/workspace/NodeSearch.tsx CHANGED
@@ -30,9 +30,7 @@ export default function (props: {
30
  boxes.sort((a, b) => a.item.name.localeCompare(b.item.name));
31
  return boxes;
32
  }, [props.boxes]);
33
- const hits: { item: OpsOp }[] = searchText
34
- ? fuse.search<OpsOp>(searchText)
35
- : allOps;
36
  const [selectedIndex, setSelectedIndex] = useState(0);
37
  useEffect(() => searchBox.current.focus());
38
  function typed(text: string) {
@@ -64,10 +62,7 @@ export default function (props: {
64
  }
65
 
66
  return (
67
- <div
68
- className="node-search"
69
- style={{ top: props.pos.y, left: props.pos.x }}
70
- >
71
  <input
72
  ref={searchBox}
73
  value={searchText}
 
30
  boxes.sort((a, b) => a.item.name.localeCompare(b.item.name));
31
  return boxes;
32
  }, [props.boxes]);
33
+ const hits: { item: OpsOp }[] = searchText ? fuse.search<OpsOp>(searchText) : allOps;
 
 
34
  const [selectedIndex, setSelectedIndex] = useState(0);
35
  useEffect(() => searchBox.current.focus());
36
  function typed(text: string) {
 
62
  }
63
 
64
  return (
65
+ <div className="node-search" style={{ top: props.pos.y, left: props.pos.x }}>
 
 
 
66
  <input
67
  ref={searchBox}
68
  value={searchText}
lynxkite-app/web/src/workspace/Workspace.tsx CHANGED
@@ -1,3 +1,5 @@
 
 
1
  import { getYjsDoc, syncedStore } from "@syncedstore/core";
2
  import {
3
  type Connection,
@@ -15,14 +17,7 @@ import {
15
  useUpdateNodeInternals,
16
  } from "@xyflow/react";
17
  import axios from "axios";
18
- import {
19
- type MouseEvent,
20
- useCallback,
21
- useEffect,
22
- useMemo,
23
- useState,
24
- } from "react";
25
- // The LynxKite workspace editor.
26
  import { useParams } from "react-router";
27
  import useSWR, { type Fetcher } from "swr";
28
  import { WebsocketProvider } from "y-websocket";
@@ -37,11 +32,7 @@ import favicon from "../assets/favicon.ico";
37
  // import NodeWithTableView from './NodeWithTableView';
38
  import EnvironmentSelector from "./EnvironmentSelector";
39
  import { LynxKiteState } from "./LynxKiteState";
40
- import NodeSearch, {
41
- type OpsOp,
42
- type Catalog,
43
- type Catalogs,
44
- } from "./NodeSearch.tsx";
45
  import NodeWithGraphCreationView from "./nodes/GraphCreationNode.tsx";
46
  import NodeWithImage from "./nodes/NodeWithImage.tsx";
47
  import NodeWithParams from "./nodes/NodeWithParams";
@@ -62,6 +53,10 @@ function LynxKiteFlow() {
62
  const [nodes, setNodes] = useState([] as Node[]);
63
  const [edges, setEdges] = useState([] as Edge[]);
64
  const { path } = useParams();
 
 
 
 
65
  const [state, setState] = useState({ workspace: {} as Workspace });
66
  const [message, setMessage] = useState(null as string | null);
67
  useEffect(() => {
@@ -69,11 +64,7 @@ function LynxKiteFlow() {
69
  setState(state);
70
  const doc = getYjsDoc(state);
71
  const proto = location.protocol === "https:" ? "wss:" : "ws:";
72
- const wsProvider = new WebsocketProvider(
73
- `${proto}//${location.host}/ws/crdt`,
74
- path!,
75
- doc,
76
- );
77
  const onChange = (_update: any, origin: any, _doc: any, _tr: any) => {
78
  if (origin === wsProvider) {
79
  // An update from the CRDT. Apply it to the local state.
@@ -165,7 +156,7 @@ function LynxKiteFlow() {
165
 
166
  const fetcher: Fetcher<Catalogs> = (resource: string, init?: RequestInit) =>
167
  fetch(resource, init).then((res) => res.json());
168
- const catalog = useSWR("/api/catalog", fetcher);
169
  const [suppressSearchUntil, setSuppressSearchUntil] = useState(0);
170
  const [nodeSearchSettings, setNodeSearchSettings] = useState(
171
  undefined as
@@ -190,11 +181,7 @@ function LynxKiteFlow() {
190
  useEffect(() => {
191
  const handleKeyDown = (event: KeyboardEvent) => {
192
  // Show the node search dialog on "/".
193
- if (
194
- event.key === "/" &&
195
- !nodeSearchSettings &&
196
- !isTypingInFormElement()
197
- ) {
198
  event.preventDefault();
199
  setNodeSearchSettings({
200
  pos: { x: 100, y: 100 },
@@ -238,11 +225,7 @@ function LynxKiteFlow() {
238
  },
239
  [catalog, state, nodeSearchSettings, suppressSearchUntil, closeNodeSearch],
240
  );
241
- function addNode(
242
- node: Partial<WorkspaceNode>,
243
- state: { workspace: Workspace },
244
- nodes: Node[],
245
- ) {
246
  const title = node.data?.title;
247
  let i = 1;
248
  node.id = `${title} ${i}`;
@@ -260,9 +243,7 @@ function LynxKiteFlow() {
260
  data: {
261
  meta: meta,
262
  title: meta.name,
263
- params: Object.fromEntries(
264
- Object.values(meta.params).map((p) => [p.name, p.default]),
265
- ),
266
  },
267
  };
268
  return node;
@@ -310,9 +291,7 @@ function LynxKiteFlow() {
310
  try {
311
  await axios.post("/api/upload", formData, {
312
  onUploadProgress: (progressEvent) => {
313
- const percentCompleted = Math.round(
314
- (100 * progressEvent.loaded) / progressEvent.total!,
315
- );
316
  if (percentCompleted === 100) setMessage("Processing file...");
317
  else setMessage(`Uploading ${percentCompleted}%`);
318
  },
@@ -345,7 +324,8 @@ function LynxKiteFlow() {
345
  <a className="logo" href="">
346
  <img alt="" src={favicon} />
347
  </a>
348
- <div className="ws-name">{path}</div>
 
349
  <EnvironmentSelector
350
  options={Object.keys(catalog.data || {})}
351
  value={state.workspace.env!}
@@ -365,11 +345,7 @@ function LynxKiteFlow() {
365
  </a>
366
  </div>
367
  </div>
368
- <div
369
- style={{ height: "100%", width: "100vw" }}
370
- onDragOver={onDragOver}
371
- onDrop={onDrop}
372
- >
373
  <LynxKiteState.Provider value={state}>
374
  <ReactFlow
375
  nodes={nodes}
@@ -382,7 +358,9 @@ function LynxKiteFlow() {
382
  onConnect={onConnect}
383
  proOptions={{ hideAttribution: true }}
384
  maxZoom={1}
385
- minZoom={0.3}
 
 
386
  defaultEdgeOptions={{
387
  markerEnd: {
388
  type: MarkerType.ArrowClosed,
 
1
+ // The LynxKite workspace editor.
2
+
3
  import { getYjsDoc, syncedStore } from "@syncedstore/core";
4
  import {
5
  type Connection,
 
17
  useUpdateNodeInternals,
18
  } from "@xyflow/react";
19
  import axios from "axios";
20
+ import { type MouseEvent, useCallback, useEffect, useMemo, useState } from "react";
 
 
 
 
 
 
 
21
  import { useParams } from "react-router";
22
  import useSWR, { type Fetcher } from "swr";
23
  import { WebsocketProvider } from "y-websocket";
 
32
  // import NodeWithTableView from './NodeWithTableView';
33
  import EnvironmentSelector from "./EnvironmentSelector";
34
  import { LynxKiteState } from "./LynxKiteState";
35
+ import NodeSearch, { type OpsOp, type Catalog, type Catalogs } from "./NodeSearch.tsx";
 
 
 
 
36
  import NodeWithGraphCreationView from "./nodes/GraphCreationNode.tsx";
37
  import NodeWithImage from "./nodes/NodeWithImage.tsx";
38
  import NodeWithParams from "./nodes/NodeWithParams";
 
53
  const [nodes, setNodes] = useState([] as Node[]);
54
  const [edges, setEdges] = useState([] as Edge[]);
55
  const { path } = useParams();
56
+ const shortPath = path!
57
+ .split("/")
58
+ .pop()!
59
+ .replace(/[.]lynxkite[.]json$/, "");
60
  const [state, setState] = useState({ workspace: {} as Workspace });
61
  const [message, setMessage] = useState(null as string | null);
62
  useEffect(() => {
 
64
  setState(state);
65
  const doc = getYjsDoc(state);
66
  const proto = location.protocol === "https:" ? "wss:" : "ws:";
67
+ const wsProvider = new WebsocketProvider(`${proto}//${location.host}/ws/crdt`, path!, doc);
 
 
 
 
68
  const onChange = (_update: any, origin: any, _doc: any, _tr: any) => {
69
  if (origin === wsProvider) {
70
  // An update from the CRDT. Apply it to the local state.
 
156
 
157
  const fetcher: Fetcher<Catalogs> = (resource: string, init?: RequestInit) =>
158
  fetch(resource, init).then((res) => res.json());
159
+ const catalog = useSWR(`/api/catalog?workspace=${path}`, fetcher);
160
  const [suppressSearchUntil, setSuppressSearchUntil] = useState(0);
161
  const [nodeSearchSettings, setNodeSearchSettings] = useState(
162
  undefined as
 
181
  useEffect(() => {
182
  const handleKeyDown = (event: KeyboardEvent) => {
183
  // Show the node search dialog on "/".
184
+ if (event.key === "/" && !nodeSearchSettings && !isTypingInFormElement()) {
 
 
 
 
185
  event.preventDefault();
186
  setNodeSearchSettings({
187
  pos: { x: 100, y: 100 },
 
225
  },
226
  [catalog, state, nodeSearchSettings, suppressSearchUntil, closeNodeSearch],
227
  );
228
+ function addNode(node: Partial<WorkspaceNode>, state: { workspace: Workspace }, nodes: Node[]) {
 
 
 
 
229
  const title = node.data?.title;
230
  let i = 1;
231
  node.id = `${title} ${i}`;
 
243
  data: {
244
  meta: meta,
245
  title: meta.name,
246
+ params: Object.fromEntries(Object.values(meta.params).map((p) => [p.name, p.default])),
 
 
247
  },
248
  };
249
  return node;
 
291
  try {
292
  await axios.post("/api/upload", formData, {
293
  onUploadProgress: (progressEvent) => {
294
+ const percentCompleted = Math.round((100 * progressEvent.loaded) / progressEvent.total!);
 
 
295
  if (percentCompleted === 100) setMessage("Processing file...");
296
  else setMessage(`Uploading ${percentCompleted}%`);
297
  },
 
324
  <a className="logo" href="">
325
  <img alt="" src={favicon} />
326
  </a>
327
+ <div className="ws-name">{shortPath}</div>
328
+ <title>{shortPath}</title>
329
  <EnvironmentSelector
330
  options={Object.keys(catalog.data || {})}
331
  value={state.workspace.env!}
 
345
  </a>
346
  </div>
347
  </div>
348
+ <div style={{ height: "100%", width: "100vw" }} onDragOver={onDragOver} onDrop={onDrop}>
 
 
 
 
349
  <LynxKiteState.Provider value={state}>
350
  <ReactFlow
351
  nodes={nodes}
 
358
  onConnect={onConnect}
359
  proOptions={{ hideAttribution: true }}
360
  maxZoom={1}
361
+ minZoom={0.2}
362
+ zoomOnScroll={false}
363
+ preventScrolling={false}
364
  defaultEdgeOptions={{
365
  markerEnd: {
366
  type: MarkerType.ArrowClosed,
lynxkite-app/web/src/workspace/nodes/GraphCreationNode.tsx CHANGED
@@ -20,12 +20,7 @@ function toMD(v: any): string {
20
  function displayTable(name: string, df: any) {
21
  if (df.data.length > 1) {
22
  return (
23
- <Table
24
- key={`${name}-table`}
25
- name={`${name}-table`}
26
- columns={df.columns}
27
- data={df.data}
28
- />
29
  );
30
  }
31
  if (df.data.length) {
@@ -60,9 +55,7 @@ export default function NodeWithGraphCreationView(props: any) {
60
  const display = props.data.display?.value;
61
  const tables = display?.dataframes || {};
62
  const singleTable = tables && Object.keys(tables).length === 1;
63
- const [relations, setRelations] = useState(
64
- relationsToDict(display?.relations) || {},
65
- );
66
  const singleRelation = relations && Object.keys(relations).length === 1;
67
  function setParam(name: string, newValue: any, opts: UpdateOptions) {
68
  reactFlow.updateNodeData(props.id, {
@@ -211,9 +204,7 @@ export default function NodeWithGraphCreationView(props: any) {
211
 
212
  <datalist id="edges-column-options">
213
  {tables[relation.source_table] &&
214
- tables[relation.df].columns.map((name: string) => (
215
- <option key={name} value={name} />
216
- ))}
217
  </datalist>
218
 
219
  <datalist id="source-node-column-options">
@@ -274,10 +265,7 @@ export default function NodeWithGraphCreationView(props: any) {
274
  <div className="graph-relations">
275
  <div className="graph-table-header">
276
  Relationships
277
- <button
278
- className="add-relationship-button"
279
- onClick={(_) => addRelation()}
280
- >
281
  +
282
  </button>
283
  </div>
 
20
  function displayTable(name: string, df: any) {
21
  if (df.data.length > 1) {
22
  return (
23
+ <Table key={`${name}-table`} name={`${name}-table`} columns={df.columns} data={df.data} />
 
 
 
 
 
24
  );
25
  }
26
  if (df.data.length) {
 
55
  const display = props.data.display?.value;
56
  const tables = display?.dataframes || {};
57
  const singleTable = tables && Object.keys(tables).length === 1;
58
+ const [relations, setRelations] = useState(relationsToDict(display?.relations) || {});
 
 
59
  const singleRelation = relations && Object.keys(relations).length === 1;
60
  function setParam(name: string, newValue: any, opts: UpdateOptions) {
61
  reactFlow.updateNodeData(props.id, {
 
204
 
205
  <datalist id="edges-column-options">
206
  {tables[relation.source_table] &&
207
+ tables[relation.df].columns.map((name: string) => <option key={name} value={name} />)}
 
 
208
  </datalist>
209
 
210
  <datalist id="source-node-column-options">
 
265
  <div className="graph-relations">
266
  <div className="graph-table-header">
267
  Relationships
268
+ <button className="add-relationship-button" onClick={(_) => addRelation()}>
 
 
 
269
  +
270
  </button>
271
  </div>
lynxkite-app/web/src/workspace/nodes/LynxKiteNode.tsx CHANGED
@@ -1,9 +1,4 @@
1
- import {
2
- Handle,
3
- NodeResizeControl,
4
- type Position,
5
- useReactFlow,
6
- } from "@xyflow/react";
7
  // @ts-ignore
8
  import ChevronDownRight from "~icons/tabler/chevron-down-right.jsx";
9
 
@@ -38,10 +33,8 @@ function getHandles(inputs: object, outputs: object) {
38
  }
39
  for (const e of handles) {
40
  e.offsetPercentage = (100 * (e.index + 1)) / (counts[e.position] + 1);
41
- const simpleHorizontal =
42
- counts.top === 0 && counts.bottom === 0 && handles.length <= 2;
43
- const simpleVertical =
44
- counts.left === 0 && counts.right === 0 && handles.length <= 2;
45
  e.showLabel = !simpleHorizontal && !simpleVertical;
46
  }
47
  return handles;
@@ -71,10 +64,7 @@ export default function LynxKiteNode(props: LynxKiteNodeProps) {
71
  }}
72
  >
73
  <div className="lynxkite-node" style={props.nodeStyle}>
74
- <div
75
- className={`title bg-primary ${data.status}`}
76
- onClick={titleClicked}
77
- >
78
  {data.title}
79
  {data.error && <span className="title-icon">⚠️</span>}
80
  {expanded || <span className="title-icon">⋯</span>}
@@ -99,14 +89,11 @@ export default function LynxKiteNode(props: LynxKiteNodeProps) {
99
  type={handle.type}
100
  position={handle.position as Position}
101
  style={{
102
- [handleOffsetDirection[handle.position]]:
103
- `${handle.offsetPercentage}% `,
104
  }}
105
  >
106
  {handle.showLabel && (
107
- <span className="handle-name">
108
- {handle.name.replace(/_/g, " ")}
109
- </span>
110
  )}
111
  </Handle>
112
  ))}
 
1
+ import { Handle, NodeResizeControl, type Position, useReactFlow } from "@xyflow/react";
 
 
 
 
 
2
  // @ts-ignore
3
  import ChevronDownRight from "~icons/tabler/chevron-down-right.jsx";
4
 
 
33
  }
34
  for (const e of handles) {
35
  e.offsetPercentage = (100 * (e.index + 1)) / (counts[e.position] + 1);
36
+ const simpleHorizontal = counts.top === 0 && counts.bottom === 0 && handles.length <= 2;
37
+ const simpleVertical = counts.left === 0 && counts.right === 0 && handles.length <= 2;
 
 
38
  e.showLabel = !simpleHorizontal && !simpleVertical;
39
  }
40
  return handles;
 
64
  }}
65
  >
66
  <div className="lynxkite-node" style={props.nodeStyle}>
67
+ <div className={`title bg-primary ${data.status}`} onClick={titleClicked}>
 
 
 
68
  {data.title}
69
  {data.error && <span className="title-icon">⚠️</span>}
70
  {expanded || <span className="title-icon">⋯</span>}
 
89
  type={handle.type}
90
  position={handle.position as Position}
91
  style={{
92
+ [handleOffsetDirection[handle.position]]: `${handle.offsetPercentage}% `,
 
93
  }}
94
  >
95
  {handle.showLabel && (
96
+ <span className="handle-name">{handle.name.replace(/_/g, " ")}</span>
 
 
97
  )}
98
  </Handle>
99
  ))}
lynxkite-app/web/src/workspace/nodes/NodeGroupParameter.tsx CHANGED
@@ -24,6 +24,7 @@ interface GroupsType {
24
  interface NodeGroupParameterProps {
25
  meta: { selector: SelectorType; groups: GroupsType };
26
  value: any;
 
27
  setParam: (name: string, value: any, options?: { delay: number }) => void;
28
  deleteParam: (name: string, options?: { delay: number }) => void;
29
  }
@@ -31,14 +32,13 @@ interface NodeGroupParameterProps {
31
  export default function NodeGroupParameter({
32
  meta,
33
  value,
 
34
  setParam,
35
  deleteParam,
36
  }: NodeGroupParameterProps) {
37
  const selector = meta.selector;
38
  const groups = meta.groups;
39
- const [selectedValue, setSelectedValue] = useState<string>(
40
- value || selector.default,
41
- );
42
 
43
  const handleSelectorChange = (value: any, opts?: { delay: number }) => {
44
  setSelectedValue(value);
@@ -47,9 +47,7 @@ export default function NodeGroupParameter({
47
 
48
  useEffect(() => {
49
  // Clean possible previous parameters first
50
- Object.values(groups).flatMap((group) =>
51
- group.map((entry) => deleteParam(entry.name)),
52
- );
53
  for (const param of groups[selectedValue]) {
54
  setParam(param.name, param.default);
55
  }
@@ -60,6 +58,7 @@ export default function NodeGroupParameter({
60
  name={selector.name}
61
  key={selector.name}
62
  value={selectedValue}
 
63
  meta={selector}
64
  onChange={handleSelectorChange}
65
  />
 
24
  interface NodeGroupParameterProps {
25
  meta: { selector: SelectorType; groups: GroupsType };
26
  value: any;
27
+ data: any;
28
  setParam: (name: string, value: any, options?: { delay: number }) => void;
29
  deleteParam: (name: string, options?: { delay: number }) => void;
30
  }
 
32
  export default function NodeGroupParameter({
33
  meta,
34
  value,
35
+ data,
36
  setParam,
37
  deleteParam,
38
  }: NodeGroupParameterProps) {
39
  const selector = meta.selector;
40
  const groups = meta.groups;
41
+ const [selectedValue, setSelectedValue] = useState<string>(value || selector.default);
 
 
42
 
43
  const handleSelectorChange = (value: any, opts?: { delay: number }) => {
44
  setSelectedValue(value);
 
47
 
48
  useEffect(() => {
49
  // Clean possible previous parameters first
50
+ Object.values(groups).flatMap((group) => group.map((entry) => deleteParam(entry.name)));
 
 
51
  for (const param of groups[selectedValue]) {
52
  setParam(param.name, param.default);
53
  }
 
58
  name={selector.name}
59
  key={selector.name}
60
  value={selectedValue}
61
+ data={data}
62
  meta={selector}
63
  onChange={handleSelectorChange}
64
  />
lynxkite-app/web/src/workspace/nodes/NodeParameter.tsx CHANGED
@@ -1,8 +1,186 @@
1
- const BOOLEAN = "<class 'bool'>";
 
 
2
 
 
 
 
 
 
 
3
  function ParamName({ name }: { name: string }) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  return (
5
- <span className="param-name bg-base-200">{name.replace(/_/g, " ")}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  );
7
  }
8
 
@@ -10,15 +188,11 @@ interface NodeParameterProps {
10
  name: string;
11
  value: any;
12
  meta: any;
 
13
  onChange: (value: any, options?: { delay: number }) => void;
14
  }
15
 
16
- export default function NodeParameter({
17
- name,
18
- value,
19
- meta,
20
- onChange,
21
- }: NodeParameterProps) {
22
  return (
23
  // biome-ignore lint/a11y/noLabelWithoutControl: Most of the time there is a control.
24
  <label className="param">
@@ -56,28 +230,34 @@ export default function NodeParameter({
56
  ) : meta?.type?.type === BOOLEAN ? (
57
  <div className="form-control">
58
  <label className="label cursor-pointer">
 
59
  <input
60
  className="checkbox"
61
  type="checkbox"
62
  checked={value}
63
  onChange={(evt) => onChange(evt.currentTarget.checked)}
64
  />
65
- {name.replace(/_/g, " ")}
66
  </label>
67
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  ) : (
69
  <>
70
  <ParamName name={name} />
71
- <input
72
- className="input input-bordered w-full"
73
- value={value || ""}
74
- onChange={(evt) => onChange(evt.currentTarget.value, { delay: 2 })}
75
- onBlur={(evt) => onChange(evt.currentTarget.value, { delay: 0 })}
76
- onKeyDown={(evt) =>
77
- evt.code === "Enter" &&
78
- onChange(evt.currentTarget.value, { delay: 0 })
79
- }
80
- />
81
  </>
82
  )}
83
  </label>
 
1
+ import { useRef } from "react";
2
+ // @ts-ignore
3
+ import ArrowsHorizontal from "~icons/tabler/arrows-horizontal.jsx";
4
 
5
+ const BOOLEAN = "<class 'bool'>";
6
+ const MODEL_TRAINING_INPUT_MAPPING =
7
+ "<class 'lynxkite_graph_analytics.ml_ops.ModelTrainingInputMapping'>";
8
+ const MODEL_INFERENCE_INPUT_MAPPING =
9
+ "<class 'lynxkite_graph_analytics.ml_ops.ModelInferenceInputMapping'>";
10
+ const MODEL_OUTPUT_MAPPING = "<class 'lynxkite_graph_analytics.ml_ops.ModelOutputMapping'>";
11
  function ParamName({ name }: { name: string }) {
12
+ return <span className="param-name bg-base-200">{name.replace(/_/g, " ")}</span>;
13
+ }
14
+
15
+ function Input({
16
+ value,
17
+ onChange,
18
+ inputRef,
19
+ }: {
20
+ value: string;
21
+ onChange: (value: string, options?: { delay: number }) => void;
22
+ inputRef?: React.Ref<HTMLInputElement>;
23
+ }) {
24
+ return (
25
+ <input
26
+ className="input input-bordered w-full"
27
+ ref={inputRef}
28
+ value={value ?? ""}
29
+ onChange={(evt) => onChange(evt.currentTarget.value, { delay: 2 })}
30
+ onBlur={(evt) => onChange(evt.currentTarget.value, { delay: 0 })}
31
+ onKeyDown={(evt) => evt.code === "Enter" && onChange(evt.currentTarget.value, { delay: 0 })}
32
+ />
33
+ );
34
+ }
35
+
36
+ type Bindings = {
37
+ [key: string]: {
38
+ df: string;
39
+ column: string;
40
+ };
41
+ };
42
+
43
+ function getModelBindings(
44
+ data: any,
45
+ variant: "training input" | "inference input" | "output",
46
+ ): string[] {
47
+ function bindingsOfModel(m: any): string[] {
48
+ switch (variant) {
49
+ case "training input":
50
+ return [...m.inputs, ...m.loss_inputs.filter((i: string) => !m.outputs.includes(i))];
51
+ case "inference input":
52
+ return m.inputs;
53
+ case "output":
54
+ return m.outputs;
55
+ }
56
+ }
57
+ const bindings = new Set<string>();
58
+ const inputs = data?.input_metadata?.value ?? data?.input_metadata ?? [];
59
+ for (const input of inputs) {
60
+ const other = input.other ?? {};
61
+ for (const e of Object.values(other) as any[]) {
62
+ if (e.type === "model") {
63
+ for (const b of bindingsOfModel(e.model)) {
64
+ bindings.add(b);
65
+ }
66
+ }
67
+ }
68
+ }
69
+ const list = [...bindings];
70
+ list.sort();
71
+ return list;
72
+ }
73
+
74
+ function parseJsonOrEmpty(json: string): object {
75
+ try {
76
+ const j = JSON.parse(json);
77
+ if (j !== null && typeof j === "object") {
78
+ return j;
79
+ }
80
+ } catch (e) {}
81
+ return {};
82
+ }
83
+
84
+ function ModelMapping({ value, onChange, data, variant }: any) {
85
+ const dfsRef = useRef({} as { [binding: string]: HTMLSelectElement | null });
86
+ const columnsRef = useRef(
87
+ {} as { [binding: string]: HTMLSelectElement | HTMLInputElement | null },
88
+ );
89
+ const v: any = parseJsonOrEmpty(value);
90
+ v.map ??= {};
91
+ const dfs: { [df: string]: string[] } = {};
92
+ const inputs = data?.input_metadata?.value ?? data?.input_metadata ?? [];
93
+ for (const input of inputs) {
94
+ if (!input.dataframes) continue;
95
+ const dataframes = input.dataframes as {
96
+ [df: string]: { columns: string[] };
97
+ };
98
+ for (const [df, { columns }] of Object.entries(dataframes)) {
99
+ dfs[df] = columns;
100
+ }
101
+ }
102
+ const bindings = getModelBindings(data, variant);
103
+ function getMap() {
104
+ const map: Bindings = {};
105
+ for (const binding of bindings) {
106
+ const df = dfsRef.current[binding]?.value ?? "";
107
+ const column = columnsRef.current[binding]?.value ?? "";
108
+ if (df.length || column.length) {
109
+ map[binding] = { df, column };
110
+ }
111
+ }
112
+ return map;
113
+ }
114
  return (
115
+ <table className="model-mapping-param">
116
+ <tbody>
117
+ {bindings.length > 0 ? (
118
+ bindings.map((binding: string) => (
119
+ <tr key={binding}>
120
+ <td>{binding}</td>
121
+ <td>
122
+ <ArrowsHorizontal />
123
+ </td>
124
+ <td>
125
+ <select
126
+ className="select select-ghost"
127
+ value={v.map?.[binding]?.df}
128
+ ref={(el) => {
129
+ dfsRef.current[binding] = el;
130
+ }}
131
+ onChange={() => onChange(JSON.stringify({ map: getMap() }))}
132
+ >
133
+ <option key="" value="" />
134
+ {Object.keys(dfs).map((df: string) => (
135
+ <option key={df} value={df}>
136
+ {df}
137
+ </option>
138
+ ))}
139
+ </select>
140
+ </td>
141
+ <td>
142
+ {variant === "output" ? (
143
+ <Input
144
+ inputRef={(el) => {
145
+ columnsRef.current[binding] = el;
146
+ }}
147
+ value={v.map?.[binding]?.column}
148
+ onChange={(column, options) => {
149
+ const map = getMap();
150
+ // At this point the <input> has not been updated yet. We use the value from the event.
151
+ const df = dfsRef.current[binding]?.value ?? "";
152
+ map[binding] ??= { df, column };
153
+ map[binding].column = column;
154
+ onChange(JSON.stringify({ map }), options);
155
+ }}
156
+ />
157
+ ) : (
158
+ <select
159
+ className="select select-ghost"
160
+ value={v.map?.[binding]?.column}
161
+ ref={(el) => {
162
+ columnsRef.current[binding] = el;
163
+ }}
164
+ onChange={() => onChange(JSON.stringify({ map: getMap() }))}
165
+ >
166
+ <option key="" value="" />
167
+ {dfs[v.map?.[binding]?.df]?.map((col: string) => (
168
+ <option key={col} value={col}>
169
+ {col}
170
+ </option>
171
+ ))}
172
+ </select>
173
+ )}
174
+ </td>
175
+ </tr>
176
+ ))
177
+ ) : (
178
+ <tr>
179
+ <td>no bindings</td>
180
+ </tr>
181
+ )}
182
+ </tbody>
183
+ </table>
184
  );
185
  }
186
 
 
188
  name: string;
189
  value: any;
190
  meta: any;
191
+ data: any;
192
  onChange: (value: any, options?: { delay: number }) => void;
193
  }
194
 
195
+ export default function NodeParameter({ name, value, meta, data, onChange }: NodeParameterProps) {
 
 
 
 
 
196
  return (
197
  // biome-ignore lint/a11y/noLabelWithoutControl: Most of the time there is a control.
198
  <label className="param">
 
230
  ) : meta?.type?.type === BOOLEAN ? (
231
  <div className="form-control">
232
  <label className="label cursor-pointer">
233
+ {name.replace(/_/g, " ")}
234
  <input
235
  className="checkbox"
236
  type="checkbox"
237
  checked={value}
238
  onChange={(evt) => onChange(evt.currentTarget.checked)}
239
  />
 
240
  </label>
241
  </div>
242
+ ) : meta?.type?.type === MODEL_TRAINING_INPUT_MAPPING ? (
243
+ <>
244
+ <ParamName name={name} />
245
+ <ModelMapping value={value} data={data} variant="training input" onChange={onChange} />
246
+ </>
247
+ ) : meta?.type?.type === MODEL_INFERENCE_INPUT_MAPPING ? (
248
+ <>
249
+ <ParamName name={name} />
250
+ <ModelMapping value={value} data={data} variant="inference input" onChange={onChange} />
251
+ </>
252
+ ) : meta?.type?.type === MODEL_OUTPUT_MAPPING ? (
253
+ <>
254
+ <ParamName name={name} />
255
+ <ModelMapping value={value} data={data} variant="output" onChange={onChange} />
256
+ </>
257
  ) : (
258
  <>
259
  <ParamName name={name} />
260
+ <Input value={value} onChange={onChange} />
 
 
 
 
 
 
 
 
 
261
  </>
262
  )}
263
  </label>
lynxkite-app/web/src/workspace/nodes/NodeWithImage.tsx CHANGED
@@ -3,9 +3,7 @@ import NodeWithParams from "./NodeWithParams";
3
  const NodeWithImage = (props: any) => {
4
  return (
5
  <NodeWithParams {...props}>
6
- {props.data.display && (
7
- <img src={props.data.display} alt="Node Display" />
8
- )}
9
  </NodeWithParams>
10
  );
11
  };
 
3
  const NodeWithImage = (props: any) => {
4
  return (
5
  <NodeWithParams {...props}>
6
+ {props.data.display && <img src={props.data.display} alt="Node Display" />}
 
 
7
  </NodeWithParams>
8
  );
9
  };
lynxkite-app/web/src/workspace/nodes/NodeWithParams.tsx CHANGED
@@ -35,11 +35,8 @@ function NodeWithParams(props: any) {
35
 
36
  return (
37
  <LynxKiteNode {...props}>
38
- {props.collapsed && (
39
- <div
40
- className="params-expander"
41
- onClick={() => setCollapsed(!collapsed)}
42
- >
43
  <Triangle className={`flippy ${collapsed ? "flippy-90" : ""}`} />
44
  </div>
45
  )}
@@ -49,23 +46,21 @@ function NodeWithParams(props: any) {
49
  <NodeGroupParameter
50
  key={name}
51
  value={value}
 
52
  meta={metaParams?.[name]}
53
  setParam={(name: string, value: any, opts?: UpdateOptions) =>
54
  setParam(name, value, opts || {})
55
  }
56
- deleteParam={(name: string, opts?: UpdateOptions) =>
57
- deleteParam(name, opts || {})
58
- }
59
  />
60
  ) : (
61
  <NodeParameter
62
  name={name}
63
  key={name}
64
  value={value}
 
65
  meta={metaParams?.[name]}
66
- onChange={(value: any, opts?: UpdateOptions) =>
67
- setParam(name, value, opts || {})
68
- }
69
  />
70
  ),
71
  )}
 
35
 
36
  return (
37
  <LynxKiteNode {...props}>
38
+ {props.collapsed && params.length > 0 && (
39
+ <div className="params-expander" onClick={() => setCollapsed(!collapsed)}>
 
 
 
40
  <Triangle className={`flippy ${collapsed ? "flippy-90" : ""}`} />
41
  </div>
42
  )}
 
46
  <NodeGroupParameter
47
  key={name}
48
  value={value}
49
+ data={props.data}
50
  meta={metaParams?.[name]}
51
  setParam={(name: string, value: any, opts?: UpdateOptions) =>
52
  setParam(name, value, opts || {})
53
  }
54
+ deleteParam={(name: string, opts?: UpdateOptions) => deleteParam(name, opts || {})}
 
 
55
  />
56
  ) : (
57
  <NodeParameter
58
  name={name}
59
  key={name}
60
  value={value}
61
+ data={props.data}
62
  meta={metaParams?.[name]}
63
+ onChange={(value: any, opts?: UpdateOptions) => setParam(name, value, opts || {})}
 
 
64
  />
65
  ),
66
  )}
lynxkite-app/web/src/workspace/nodes/NodeWithTableView.tsx CHANGED
@@ -1,3 +1,4 @@
 
1
  import { useState } from "react";
2
  import React from "react";
3
  import Markdown from "react-markdown";
@@ -14,34 +15,41 @@ function toMD(v: any): string {
14
  return JSON.stringify(v);
15
  }
16
 
 
 
17
  export default function NodeWithTableView(props: any) {
18
- const [open, setOpen] = useState({} as { [name: string]: boolean });
 
19
  const display = props.data.display?.value;
20
- const single =
21
- display?.dataframes && Object.keys(display?.dataframes).length === 1;
22
  const dfs = Object.entries(display?.dataframes || {});
23
  dfs.sort();
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  return (
25
  <LynxKiteNode {...props}>
26
  {display && [
27
  dfs.map(([name, df]: [string, any]) => (
28
  <React.Fragment key={name}>
29
  {!single && (
30
- <div
31
- key={`${name}-header`}
32
- className="df-head"
33
- onClick={() => setOpen({ ...open, [name]: !open[name] })}
34
- >
35
  {name}
36
  </div>
37
  )}
38
  {(single || open[name]) &&
39
  (df.data.length > 1 ? (
40
- <Table
41
- key={`${name}-table`}
42
- columns={df.columns}
43
- data={df.data}
44
- />
45
  ) : df.data.length ? (
46
  <dl key={`${name}-dl`}>
47
  {df.columns.map((c: string, i: number) => (
@@ -60,11 +68,7 @@ export default function NodeWithTableView(props: any) {
60
  )),
61
  Object.entries(display.others || {}).map(([name, o]) => (
62
  <>
63
- <div
64
- key={`${name}-header`}
65
- className="df-head"
66
- onClick={() => setOpen({ ...open, [name]: !open[name] })}
67
- >
68
  {name}
69
  </div>
70
  {open[name] && <pre>{(o as any).toString()}</pre>}
 
1
+ import { useReactFlow } from "@xyflow/react";
2
  import { useState } from "react";
3
  import React from "react";
4
  import Markdown from "react-markdown";
 
15
  return JSON.stringify(v);
16
  }
17
 
18
+ type OpenState = { [name: string]: boolean };
19
+
20
  export default function NodeWithTableView(props: any) {
21
+ const reactFlow = useReactFlow();
22
+ const [open, setOpen] = useState((props.data?.params?._tables_open ?? {}) as OpenState);
23
  const display = props.data.display?.value;
24
+ const single = display?.dataframes && Object.keys(display?.dataframes).length === 1;
 
25
  const dfs = Object.entries(display?.dataframes || {});
26
  dfs.sort();
27
+ function setParam(name: string, newValue: any) {
28
+ reactFlow.updateNodeData(props.id, (prevData: any) => ({
29
+ ...prevData,
30
+ params: { ...prevData.data.params, [name]: newValue },
31
+ }));
32
+ }
33
+ function toggleTable(name: string) {
34
+ setOpen((prevOpen: OpenState) => {
35
+ const newOpen = { ...prevOpen, [name]: !prevOpen[name] };
36
+ setParam("_tables_open", newOpen);
37
+ return newOpen;
38
+ });
39
+ }
40
  return (
41
  <LynxKiteNode {...props}>
42
  {display && [
43
  dfs.map(([name, df]: [string, any]) => (
44
  <React.Fragment key={name}>
45
  {!single && (
46
+ <div key={`${name}-header`} className="df-head" onClick={() => toggleTable(name)}>
 
 
 
 
47
  {name}
48
  </div>
49
  )}
50
  {(single || open[name]) &&
51
  (df.data.length > 1 ? (
52
+ <Table key={`${name}-table`} columns={df.columns} data={df.data} />
 
 
 
 
53
  ) : df.data.length ? (
54
  <dl key={`${name}-dl`}>
55
  {df.columns.map((c: string, i: number) => (
 
68
  )),
69
  Object.entries(display.others || {}).map(([name, o]) => (
70
  <>
71
+ <div key={`${name}-header`} className="df-head" onClick={() => toggleTable(name)}>
 
 
 
 
72
  {name}
73
  </div>
74
  {open[name] && <pre>{(o as any).toString()}</pre>}
lynxkite-app/web/src/workspace/nodes/NodeWithVisualization.tsx CHANGED
@@ -8,6 +8,10 @@ const NodeWithVisualization = (props: any) => {
8
  useEffect(() => {
9
  const opts = props.data?.display?.value;
10
  if (!opts || !chartsRef.current) return;
 
 
 
 
11
  chartsInstanceRef.current = echarts.init(chartsRef.current, null, {
12
  renderer: "canvas",
13
  width: "auto",
 
8
  useEffect(() => {
9
  const opts = props.data?.display?.value;
10
  if (!opts || !chartsRef.current) return;
11
+ if (opts.tooltip?.formatter === "GET_THIRD_VALUE") {
12
+ // We can't pass a function from the backend, and can't get good tooltips otherwise.
13
+ opts.tooltip.formatter = (params: any) => params.value[2];
14
+ }
15
  chartsInstanceRef.current = echarts.init(chartsRef.current, null, {
16
  renderer: "canvas",
17
  width: "auto",
lynxkite-app/web/tests/directory.spec.ts CHANGED
@@ -14,13 +14,6 @@ test.describe("Directory operations", () => {
14
  splash = await Splash.open(page);
15
  });
16
 
17
- test("Create workspace with default name", async () => {
18
- const workspace = await Workspace.empty(splash.page);
19
- // Not checking for exact match, since there may be pre-existing "Untitled" workspaces
20
- expect(workspace.name).toContain("Untitled");
21
- await workspace.close();
22
- });
23
-
24
  test("Create & delete workspace", async () => {
25
  const workspaceName = `TestWorkspace-${Date.now()}`;
26
  const workspace = await Workspace.empty(splash.page, workspaceName);
@@ -40,11 +33,6 @@ test.describe("Directory operations", () => {
40
  await splash.deleteEntry(folderName);
41
  await expect(splash.getEntry(folderName)).not.toBeVisible();
42
  });
43
-
44
- test("Create folder with default name", async () => {
45
- await splash.createFolder();
46
- await expect(splash.currentFolder()).toContainText("Untitled");
47
- });
48
  });
49
 
50
  test.describe
 
14
  splash = await Splash.open(page);
15
  });
16
 
 
 
 
 
 
 
 
17
  test("Create & delete workspace", async () => {
18
  const workspaceName = `TestWorkspace-${Date.now()}`;
19
  const workspace = await Workspace.empty(splash.page, workspaceName);
 
33
  await splash.deleteEntry(folderName);
34
  await expect(splash.getEntry(folderName)).not.toBeVisible();
35
  });
 
 
 
 
 
36
  });
37
 
38
  test.describe
lynxkite-app/web/tests/errors.spec.ts CHANGED
@@ -35,9 +35,7 @@ test("unknown operation", async () => {
35
  await graphBox.getByLabel("n", { exact: true }).fill("10");
36
  await workspace.setEnv("LynxScribe");
37
  const csvBox = workspace.getBox("NX › Scale-Free Graph 1");
38
- await expect(csvBox.locator(".error")).toHaveText(
39
- 'Operation "NX › Scale-Free Graph" not found.',
40
- );
41
  await workspace.setEnv("LynxKite Graph Analytics");
42
  await expect(csvBox.locator(".error")).not.toBeVisible();
43
  });
 
35
  await graphBox.getByLabel("n", { exact: true }).fill("10");
36
  await workspace.setEnv("LynxScribe");
37
  const csvBox = workspace.getBox("NX › Scale-Free Graph 1");
38
+ await expect(csvBox.locator(".error")).toHaveText('Operation "NX › Scale-Free Graph" not found.');
 
 
39
  await workspace.setEnv("LynxKite Graph Analytics");
40
  await expect(csvBox.locator(".error")).not.toBeVisible();
41
  });
lynxkite-app/web/tests/graph_creation.spec.ts CHANGED
@@ -5,15 +5,9 @@ import { Splash, Workspace } from "./lynxkite";
5
  let workspace: Workspace;
6
 
7
  test.beforeEach(async ({ browser }) => {
8
- workspace = await Workspace.empty(
9
- await browser.newPage(),
10
- "graph_creation_spec_test",
11
- );
12
  await workspace.addBox("NX › Scale-Free Graph");
13
- await workspace
14
- .getBox("NX › Scale-Free Graph 1")
15
- .getByLabel("n", { exact: true })
16
- .fill("10");
17
  await workspace.addBox("Create graph");
18
  await workspace.connectBoxes("NX › Scale-Free Graph 1", "Create graph 1");
19
  });
@@ -45,9 +39,7 @@ test("Tables are displayed in the Graph creation box", async () => {
45
 
46
  test("Adding and removing relationships", async () => {
47
  const graphBox = await workspace.getBox("Create graph 1");
48
- const addRelationshipButton = await graphBox.locator(
49
- ".add-relationship-button",
50
- );
51
  await addRelationshipButton.click();
52
  const formData: Record<string, string> = {
53
  name: "relation_1",
@@ -69,10 +61,9 @@ test("Adding and removing relationships", async () => {
69
  // check that the relationship has been saved in the backend
70
  await workspace.page.reload();
71
  const graphBoxAfterReload = await workspace.getBox("Create graph 1");
72
- const relationHeader = await graphBoxAfterReload.locator(
73
- ".graph-relations .df-head",
74
- { hasText: "relation_1" },
75
- );
76
  await expect(relationHeader).toBeVisible();
77
  await relationHeader.locator("button").click(); // Delete the relationship
78
  await expect(relationHeader).not.toBeVisible();
 
5
  let workspace: Workspace;
6
 
7
  test.beforeEach(async ({ browser }) => {
8
+ workspace = await Workspace.empty(await browser.newPage(), "graph_creation_spec_test");
 
 
 
9
  await workspace.addBox("NX › Scale-Free Graph");
10
+ await workspace.getBox("NX › Scale-Free Graph 1").getByLabel("n", { exact: true }).fill("10");
 
 
 
11
  await workspace.addBox("Create graph");
12
  await workspace.connectBoxes("NX › Scale-Free Graph 1", "Create graph 1");
13
  });
 
39
 
40
  test("Adding and removing relationships", async () => {
41
  const graphBox = await workspace.getBox("Create graph 1");
42
+ const addRelationshipButton = await graphBox.locator(".add-relationship-button");
 
 
43
  await addRelationshipButton.click();
44
  const formData: Record<string, string> = {
45
  name: "relation_1",
 
61
  // check that the relationship has been saved in the backend
62
  await workspace.page.reload();
63
  const graphBoxAfterReload = await workspace.getBox("Create graph 1");
64
+ const relationHeader = await graphBoxAfterReload.locator(".graph-relations .df-head", {
65
+ hasText: "relation_1",
66
+ });
 
67
  await expect(relationHeader).toBeVisible();
68
  await relationHeader.locator("button").click(); // Delete the relationship
69
  await expect(relationHeader).not.toBeVisible();