darabos commited on
Commit
b0968aa
·
1 Parent(s): e9b29c4

Change formatters from 80 columns to 100 columns.

Browse files
Files changed (31) hide show
  1. biome.json +1 -0
  2. lynxkite-app/src/lynxkite_app/crdt.py +2 -6
  3. lynxkite-app/src/lynxkite_app/main.py +1 -4
  4. lynxkite-app/web/eslint.config.js +1 -4
  5. lynxkite-app/web/src/Directory.tsx +6 -21
  6. lynxkite-app/web/src/index.css +1 -2
  7. lynxkite-app/web/src/workspace/NodeSearch.tsx +2 -7
  8. lynxkite-app/web/src/workspace/Workspace.tsx +8 -38
  9. lynxkite-app/web/src/workspace/nodes/GraphCreationNode.tsx +4 -16
  10. lynxkite-app/web/src/workspace/nodes/LynxKiteNode.tsx +6 -19
  11. lynxkite-app/web/src/workspace/nodes/NodeGroupParameter.tsx +2 -6
  12. lynxkite-app/web/src/workspace/nodes/NodeParameter.tsx +8 -37
  13. lynxkite-app/web/src/workspace/nodes/NodeWithImage.tsx +1 -3
  14. lynxkite-app/web/src/workspace/nodes/NodeWithParams.tsx +3 -10
  15. lynxkite-app/web/src/workspace/nodes/NodeWithTableView.tsx +2 -7
  16. lynxkite-app/web/tests/errors.spec.ts +1 -3
  17. lynxkite-app/web/tests/graph_creation.spec.ts +6 -15
  18. lynxkite-app/web/tests/import.spec.ts +2 -9
  19. lynxkite-app/web/tests/lynxkite.ts +6 -15
  20. lynxkite-app/web/tsconfig.json +1 -4
  21. lynxkite-bio/src/lynxkite_bio/rdkit.py +1 -3
  22. lynxkite-core/src/lynxkite/core/executors/one_by_one.py +3 -3
  23. lynxkite-core/src/lynxkite/core/ops.py +3 -7
  24. lynxkite-core/src/lynxkite/core/workspace.py +1 -3
  25. lynxkite-core/tests/test_ops.py +3 -9
  26. lynxkite-graph-analytics/src/lynxkite_graph_analytics/bionemo_ops.py +9 -28
  27. lynxkite-graph-analytics/src/lynxkite_graph_analytics/core.py +5 -17
  28. lynxkite-graph-analytics/src/lynxkite_graph_analytics/lynxkite_ops.py +12 -39
  29. lynxkite-graph-analytics/src/lynxkite_graph_analytics/networkx_ops.py +1 -3
  30. lynxkite-graph-analytics/src/lynxkite_graph_analytics/pytorch_model_ops.py +5 -15
  31. lynxkite-lynxscribe/src/lynxkite_lynxscribe/llm_ops.py +2 -6
biome.json CHANGED
@@ -1,6 +1,7 @@
1
  {
2
  "formatter": {
3
  "ignore": ["**/node_modules/**", "**/dist/**"],
 
4
  "indentStyle": "space"
5
  },
6
  "linter": {
 
1
  {
2
  "formatter": {
3
  "ignore": ["**/node_modules/**", "**/dist/**"],
4
+ "lineWidth": 100,
5
  "indentStyle": "space"
6
  },
7
  "linter": {
lynxkite-app/src/lynxkite_app/crdt.py CHANGED
@@ -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
@@ -212,9 +210,7 @@ async def workspace_changed(name: str, changes: pycrdt.MapEvent, ws_crdt: pycrdt
212
  await execute(name, ws_crdt, ws_pyd)
213
 
214
 
215
- async def execute(
216
- name: str, ws_crdt: pycrdt.Map, ws_pyd: workspace.Workspace, delay: int = 0
217
- ):
218
  """Execute the workspace and update the CRDT object with the results.
219
 
220
  Args:
 
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
 
210
  await execute(name, ws_crdt, ws_pyd)
211
 
212
 
213
+ async def execute(name: str, ws_crdt: pycrdt.Map, ws_pyd: workspace.Workspace, delay: int = 0):
 
 
214
  """Execute the workspace and update the CRDT object with the results.
215
 
216
  Args:
lynxkite-app/src/lynxkite_app/main.py CHANGED
@@ -34,10 +34,7 @@ app.add_middleware(GZipMiddleware)
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):
 
34
 
35
  @app.get("/api/catalog")
36
  def get_catalog():
37
+ return {k: {op.name: op.model_dump() for op in v.values()} for k, v in ops.CATALOGS.items()}
 
 
 
38
 
39
 
40
  class SaveRequest(workspace.BaseConfig):
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/src/Directory.tsx CHANGED
@@ -52,21 +52,13 @@ export default function () {
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
 
@@ -83,12 +75,10 @@ export default function () {
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" },
@@ -124,9 +114,7 @@ export default function () {
124
  newWorkspaceIn(
125
  path || "",
126
  list.data,
127
- (
128
- e.target as HTMLFormElement
129
- ).workspaceName.value.trim(),
130
  );
131
  }}
132
  >
@@ -138,10 +126,7 @@ export default function () {
138
  />
139
  </form>
140
  )}
141
- <button
142
- type="button"
143
- onClick={() => setIsCreatingWorkspace(true)}
144
- >
145
  <FolderPlus /> New workspace
146
  </button>
147
  </div>
 
52
  }
53
  }
54
 
55
+ function newWorkspaceIn(path: string, list: DirectoryEntry[], workspaceName?: string) {
 
 
 
 
56
  const pathSlash = path ? `${path}/` : "";
57
  const name = workspaceName || newName(list);
58
  navigate(`/edit/${pathSlash}${name}`, { replace: true });
59
  }
60
 
61
+ async function newFolderIn(path: string, list: DirectoryEntry[], folderName?: string) {
 
 
 
 
62
  const name = folderName || newName(list, "New Folder");
63
  const pathSlash = path ? `${path}/` : "";
64
 
 
75
  }
76
 
77
  async function deleteItem(item: DirectoryEntry) {
78
+ if (!window.confirm(`Are you sure you want to delete "${item.name}"?`)) return;
 
79
  const pathSlash = path ? `${path}/` : "";
80
 
81
+ const apiPath = item.type === "directory" ? "/api/dir/delete" : "/api/delete";
 
82
  await fetch(apiPath, {
83
  method: "POST",
84
  headers: { "Content-Type": "application/json" },
 
114
  newWorkspaceIn(
115
  path || "",
116
  list.data,
117
+ (e.target as HTMLFormElement).workspaceName.value.trim(),
 
 
118
  );
119
  }}
120
  >
 
126
  />
127
  </form>
128
  )}
129
+ <button type="button" onClick={() => setIsCreatingWorkspace(true)}>
 
 
 
130
  <FolderPlus /> New workspace
131
  </button>
132
  </div>
lynxkite-app/web/src/index.css CHANGED
@@ -102,8 +102,7 @@ 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 {
 
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 {
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
@@ -15,13 +15,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";
@@ -37,11 +31,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";
@@ -69,11 +59,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.
@@ -190,11 +176,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 +220,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 +238,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 +286,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
  },
@@ -365,11 +339,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}
 
15
  useUpdateNodeInternals,
16
  } from "@xyflow/react";
17
  import axios from "axios";
18
+ import { type MouseEvent, useCallback, useEffect, useMemo, useState } from "react";
 
 
 
 
 
 
19
  // The LynxKite workspace editor.
20
  import { useParams } from "react-router";
21
  import useSWR, { type Fetcher } from "swr";
 
31
  // import NodeWithTableView from './NodeWithTableView';
32
  import EnvironmentSelector from "./EnvironmentSelector";
33
  import { LynxKiteState } from "./LynxKiteState";
34
+ import NodeSearch, { type OpsOp, type Catalog, type Catalogs } from "./NodeSearch.tsx";
 
 
 
 
35
  import NodeWithGraphCreationView from "./nodes/GraphCreationNode.tsx";
36
  import NodeWithImage from "./nodes/NodeWithImage.tsx";
37
  import NodeWithParams from "./nodes/NodeWithParams";
 
59
  setState(state);
60
  const doc = getYjsDoc(state);
61
  const proto = location.protocol === "https:" ? "wss:" : "ws:";
62
+ const wsProvider = new WebsocketProvider(`${proto}//${location.host}/ws/crdt`, path!, doc);
 
 
 
 
63
  const onChange = (_update: any, origin: any, _doc: any, _tr: any) => {
64
  if (origin === wsProvider) {
65
  // An update from the CRDT. Apply it to the local state.
 
176
  useEffect(() => {
177
  const handleKeyDown = (event: KeyboardEvent) => {
178
  // Show the node search dialog on "/".
179
+ if (event.key === "/" && !nodeSearchSettings && !isTypingInFormElement()) {
 
 
 
 
180
  event.preventDefault();
181
  setNodeSearchSettings({
182
  pos: { x: 100, y: 100 },
 
220
  },
221
  [catalog, state, nodeSearchSettings, suppressSearchUntil, closeNodeSearch],
222
  );
223
+ function addNode(node: Partial<WorkspaceNode>, state: { workspace: Workspace }, nodes: Node[]) {
 
 
 
 
224
  const title = node.data?.title;
225
  let i = 1;
226
  node.id = `${title} ${i}`;
 
238
  data: {
239
  meta: meta,
240
  title: meta.name,
241
+ params: Object.fromEntries(Object.values(meta.params).map((p) => [p.name, p.default])),
 
 
242
  },
243
  };
244
  return node;
 
286
  try {
287
  await axios.post("/api/upload", formData, {
288
  onUploadProgress: (progressEvent) => {
289
+ const percentCompleted = Math.round((100 * progressEvent.loaded) / progressEvent.total!);
 
 
290
  if (percentCompleted === 100) setMessage("Processing file...");
291
  else setMessage(`Uploading ${percentCompleted}%`);
292
  },
 
339
  </a>
340
  </div>
341
  </div>
342
+ <div style={{ height: "100%", width: "100vw" }} onDragOver={onDragOver} onDrop={onDrop}>
 
 
 
 
343
  <LynxKiteState.Provider value={state}>
344
  <ReactFlow
345
  nodes={nodes}
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
@@ -38,9 +38,7 @@ export default function NodeGroupParameter({
38
  }: NodeGroupParameterProps) {
39
  const selector = meta.selector;
40
  const groups = meta.groups;
41
- const [selectedValue, setSelectedValue] = useState<string>(
42
- value || selector.default,
43
- );
44
 
45
  const handleSelectorChange = (value: any, opts?: { delay: number }) => {
46
  setSelectedValue(value);
@@ -49,9 +47,7 @@ export default function NodeGroupParameter({
49
 
50
  useEffect(() => {
51
  // Clean possible previous parameters first
52
- Object.values(groups).flatMap((group) =>
53
- group.map((entry) => deleteParam(entry.name)),
54
- );
55
  for (const param of groups[selectedValue]) {
56
  setParam(param.name, param.default);
57
  }
 
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
  }
lynxkite-app/web/src/workspace/nodes/NodeParameter.tsx CHANGED
@@ -6,12 +6,9 @@ const MODEL_TRAINING_INPUT_MAPPING =
6
  "<class 'lynxkite_graph_analytics.lynxkite_ops.ModelTrainingInputMapping'>";
7
  const MODEL_INFERENCE_INPUT_MAPPING =
8
  "<class 'lynxkite_graph_analytics.lynxkite_ops.ModelInferenceInputMapping'>";
9
- const MODEL_OUTPUT_MAPPING =
10
- "<class 'lynxkite_graph_analytics.lynxkite_ops.ModelOutputMapping'>";
11
  function ParamName({ name }: { name: string }) {
12
- return (
13
- <span className="param-name bg-base-200">{name.replace(/_/g, " ")}</span>
14
- );
15
  }
16
 
17
  function Input({
@@ -27,9 +24,7 @@ function Input({
27
  value={value || ""}
28
  onChange={(evt) => onChange(evt.currentTarget.value, { delay: 2 })}
29
  onBlur={(evt) => onChange(evt.currentTarget.value, { delay: 0 })}
30
- onKeyDown={(evt) =>
31
- evt.code === "Enter" && onChange(evt.currentTarget.value, { delay: 0 })
32
- }
33
  />
34
  );
35
  }
@@ -41,10 +36,7 @@ function getModelBindings(
41
  function bindingsOfModel(m: any): string[] {
42
  switch (variant) {
43
  case "training input":
44
- return [
45
- ...m.inputs,
46
- ...m.loss_inputs.filter((i: string) => !m.outputs.includes(i)),
47
- ];
48
  case "inference input":
49
  return m.inputs;
50
  case "output":
@@ -185,13 +177,7 @@ interface NodeParameterProps {
185
  onChange: (value: any, options?: { delay: number }) => void;
186
  }
187
 
188
- export default function NodeParameter({
189
- name,
190
- value,
191
- meta,
192
- data,
193
- onChange,
194
- }: NodeParameterProps) {
195
  return (
196
  // biome-ignore lint/a11y/noLabelWithoutControl: Most of the time there is a control.
197
  <label className="param">
@@ -241,32 +227,17 @@ export default function NodeParameter({
241
  ) : meta?.type?.type === MODEL_TRAINING_INPUT_MAPPING ? (
242
  <>
243
  <ParamName name={name} />
244
- <ModelMapping
245
- value={value}
246
- data={data}
247
- variant="training input"
248
- onChange={onChange}
249
- />
250
  </>
251
  ) : meta?.type?.type === MODEL_INFERENCE_INPUT_MAPPING ? (
252
  <>
253
  <ParamName name={name} />
254
- <ModelMapping
255
- value={value}
256
- data={data}
257
- variant="inference input"
258
- onChange={onChange}
259
- />
260
  </>
261
  ) : meta?.type?.type === MODEL_OUTPUT_MAPPING ? (
262
  <>
263
  <ParamName name={name} />
264
- <ModelMapping
265
- value={value}
266
- data={data}
267
- variant="output"
268
- onChange={onChange}
269
- />
270
  </>
271
  ) : (
272
  <>
 
6
  "<class 'lynxkite_graph_analytics.lynxkite_ops.ModelTrainingInputMapping'>";
7
  const MODEL_INFERENCE_INPUT_MAPPING =
8
  "<class 'lynxkite_graph_analytics.lynxkite_ops.ModelInferenceInputMapping'>";
9
+ const MODEL_OUTPUT_MAPPING = "<class 'lynxkite_graph_analytics.lynxkite_ops.ModelOutputMapping'>";
 
10
  function ParamName({ name }: { name: string }) {
11
+ return <span className="param-name bg-base-200">{name.replace(/_/g, " ")}</span>;
 
 
12
  }
13
 
14
  function Input({
 
24
  value={value || ""}
25
  onChange={(evt) => onChange(evt.currentTarget.value, { delay: 2 })}
26
  onBlur={(evt) => onChange(evt.currentTarget.value, { delay: 0 })}
27
+ onKeyDown={(evt) => evt.code === "Enter" && onChange(evt.currentTarget.value, { delay: 0 })}
 
 
28
  />
29
  );
30
  }
 
36
  function bindingsOfModel(m: any): string[] {
37
  switch (variant) {
38
  case "training input":
39
+ return [...m.inputs, ...m.loss_inputs.filter((i: string) => !m.outputs.includes(i))];
 
 
 
40
  case "inference input":
41
  return m.inputs;
42
  case "output":
 
177
  onChange: (value: any, options?: { delay: number }) => void;
178
  }
179
 
180
+ export default function NodeParameter({ name, value, meta, data, onChange }: NodeParameterProps) {
 
 
 
 
 
 
181
  return (
182
  // biome-ignore lint/a11y/noLabelWithoutControl: Most of the time there is a control.
183
  <label className="param">
 
227
  ) : meta?.type?.type === MODEL_TRAINING_INPUT_MAPPING ? (
228
  <>
229
  <ParamName name={name} />
230
+ <ModelMapping value={value} data={data} variant="training input" onChange={onChange} />
 
 
 
 
 
231
  </>
232
  ) : meta?.type?.type === MODEL_INFERENCE_INPUT_MAPPING ? (
233
  <>
234
  <ParamName name={name} />
235
+ <ModelMapping value={value} data={data} variant="inference input" onChange={onChange} />
 
 
 
 
 
236
  </>
237
  ) : meta?.type?.type === MODEL_OUTPUT_MAPPING ? (
238
  <>
239
  <ParamName name={name} />
240
+ <ModelMapping value={value} data={data} variant="output" onChange={onChange} />
 
 
 
 
 
241
  </>
242
  ) : (
243
  <>
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
@@ -36,10 +36,7 @@ function NodeWithParams(props: any) {
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
  )}
@@ -54,9 +51,7 @@ function NodeWithParams(props: any) {
54
  setParam={(name: string, value: any, opts?: UpdateOptions) =>
55
  setParam(name, value, opts || {})
56
  }
57
- deleteParam={(name: string, opts?: UpdateOptions) =>
58
- deleteParam(name, opts || {})
59
- }
60
  />
61
  ) : (
62
  <NodeParameter
@@ -65,9 +60,7 @@ function NodeWithParams(props: any) {
65
  value={value}
66
  data={props.data}
67
  meta={metaParams?.[name]}
68
- onChange={(value: any, opts?: UpdateOptions) =>
69
- setParam(name, value, opts || {})
70
- }
71
  />
72
  ),
73
  )}
 
36
  return (
37
  <LynxKiteNode {...props}>
38
  {props.collapsed && (
39
+ <div className="params-expander" onClick={() => setCollapsed(!collapsed)}>
 
 
 
40
  <Triangle className={`flippy ${collapsed ? "flippy-90" : ""}`} />
41
  </div>
42
  )}
 
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
 
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
@@ -17,8 +17,7 @@ function toMD(v: any): string {
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 (
@@ -37,11 +36,7 @@ export default function NodeWithTableView(props: any) {
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) => (
 
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 = display?.dataframes && Object.keys(display?.dataframes).length === 1;
 
21
  const dfs = Object.entries(display?.dataframes || {});
22
  dfs.sort();
23
  return (
 
36
  )}
37
  {(single || open[name]) &&
38
  (df.data.length > 1 ? (
39
+ <Table key={`${name}-table`} columns={df.columns} data={df.data} />
 
 
 
 
40
  ) : df.data.length ? (
41
  <dl key={`${name}-dl`}>
42
  {df.columns.map((c: string, i: number) => (
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();
lynxkite-app/web/tests/import.spec.ts CHANGED
@@ -7,10 +7,7 @@ import { Splash, Workspace } from "./lynxkite";
7
  let workspace: Workspace;
8
 
9
  test.beforeEach(async ({ browser }) => {
10
- workspace = await Workspace.empty(
11
- await browser.newPage(),
12
- "import_spec_test",
13
- );
14
  });
15
 
16
  test.afterEach(async () => {
@@ -22,11 +19,7 @@ test.afterEach(async () => {
22
  await splash.deleteEntry("import_spec_test");
23
  });
24
 
25
- async function validateImport(
26
- workspace: Workspace,
27
- fileName: string,
28
- fileFormat: string,
29
- ) {
30
  const __filename = fileURLToPath(import.meta.url);
31
  const __dirname = dirname(__filename);
32
  const filePath = join(__dirname, "data", fileName);
 
7
  let workspace: Workspace;
8
 
9
  test.beforeEach(async ({ browser }) => {
10
+ workspace = await Workspace.empty(await browser.newPage(), "import_spec_test");
 
 
 
11
  });
12
 
13
  test.afterEach(async () => {
 
19
  await splash.deleteEntry("import_spec_test");
20
  });
21
 
22
+ async function validateImport(workspace: Workspace, fileName: string, fileFormat: string) {
 
 
 
 
23
  const __filename = fileURLToPath(import.meta.url);
24
  const __dirname = dirname(__filename);
25
  const filePath = join(__dirname, "data", fileName);
lynxkite-app/web/tests/lynxkite.ts CHANGED
@@ -63,10 +63,7 @@ export class Workspace {
63
 
64
  await this.page.locator(".ws-name").click();
65
  await this.page.keyboard.press("/");
66
- await this.page
67
- .locator(".node-search")
68
- .getByText(boxName, { exact: true })
69
- .click();
70
  await expect(this.getBoxes()).toHaveCount(allBoxes.length + 1);
71
  }
72
 
@@ -107,9 +104,7 @@ export class Workspace {
107
 
108
  getBoxHandle(boxId: string, pos?: string) {
109
  if (pos) {
110
- return this.page.locator(
111
- `[data-id="${boxId}"] [data-handlepos="${pos}"]`,
112
- );
113
  }
114
  return this.page.getByTestId(boxId);
115
  }
@@ -133,11 +128,9 @@ export class Workspace {
133
  } else if (offset) {
134
  // Without steps the movement is too fast and the box is not dragged. The more steps,
135
  // the better the movement is captured
136
- await this.page.mouse.move(
137
- boxCenterX + offset.offsetX,
138
- boxCenterY + offset.offsetY,
139
- { steps: 5 },
140
- );
141
  }
142
  await this.page.mouse.up();
143
  }
@@ -197,9 +190,7 @@ export class Splash {
197
  workspaceName = name;
198
  await this.page.locator('input[name="workspaceName"]').fill(name);
199
  } else {
200
- workspaceName = await this.page
201
- .locator('input[name="workspaceName"]')
202
- .inputValue();
203
  }
204
  await this.page.locator('input[name="workspaceName"]').press("Enter");
205
  const ws = new Workspace(this.page, workspaceName);
 
63
 
64
  await this.page.locator(".ws-name").click();
65
  await this.page.keyboard.press("/");
66
+ await this.page.locator(".node-search").getByText(boxName, { exact: true }).click();
 
 
 
67
  await expect(this.getBoxes()).toHaveCount(allBoxes.length + 1);
68
  }
69
 
 
104
 
105
  getBoxHandle(boxId: string, pos?: string) {
106
  if (pos) {
107
+ return this.page.locator(`[data-id="${boxId}"] [data-handlepos="${pos}"]`);
 
 
108
  }
109
  return this.page.getByTestId(boxId);
110
  }
 
128
  } else if (offset) {
129
  // Without steps the movement is too fast and the box is not dragged. The more steps,
130
  // the better the movement is captured
131
+ await this.page.mouse.move(boxCenterX + offset.offsetX, boxCenterY + offset.offsetY, {
132
+ steps: 5,
133
+ });
 
 
134
  }
135
  await this.page.mouse.up();
136
  }
 
190
  workspaceName = name;
191
  await this.page.locator('input[name="workspaceName"]').fill(name);
192
  } else {
193
+ workspaceName = await this.page.locator('input[name="workspaceName"]').inputValue();
 
 
194
  }
195
  await this.page.locator('input[name="workspaceName"]').press("Enter");
196
  const ws = new Workspace(this.page, workspaceName);
lynxkite-app/web/tsconfig.json CHANGED
@@ -1,7 +1,4 @@
1
  {
2
  "files": [],
3
- "references": [
4
- { "path": "./tsconfig.app.json" },
5
- { "path": "./tsconfig.node.json" }
6
- ]
7
  }
 
1
  {
2
  "files": [],
3
+ "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
 
 
 
4
  }
lynxkite-bio/src/lynxkite_bio/rdkit.py CHANGED
@@ -36,9 +36,7 @@ def _get_similarity_matrix(mols):
36
 
37
 
38
  @op("Graph from molecule similarity")
39
- def graph_from_similarity(
40
- bundle: Bundle, *, table="df", mols_column="mols", average_degree=10
41
- ):
42
  """Creates edges for pairs of molecules that are the most similar."""
43
  df = bundle.dfs[table]
44
  mols = df[mols_column]
 
36
 
37
 
38
  @op("Graph from molecule similarity")
39
+ def graph_from_similarity(bundle: Bundle, *, table="df", mols_column="mols", average_degree=10):
 
 
40
  """Creates edges for pairs of molecules that are the most similar."""
41
  df = bundle.dfs[table]
42
  mols = df[mols_column]
lynxkite-core/src/lynxkite/core/executors/one_by_one.py CHANGED
@@ -167,9 +167,9 @@ async def execute(ws: workspace.Workspace, catalog, cache=None):
167
  op = catalog[t.data.title]
168
  i = op.inputs[edge.targetHandle]
169
  if i.position in "top or bottom":
170
- batch_inputs.setdefault(
171
- (edge.target, edge.targetHandle), []
172
- ).extend(results)
173
  else:
174
  tasks.setdefault(edge.target, []).extend(results)
175
  node.publish_result(result)
 
167
  op = catalog[t.data.title]
168
  i = op.inputs[edge.targetHandle]
169
  if i.position in "top or bottom":
170
+ batch_inputs.setdefault((edge.target, edge.targetHandle), []).extend(
171
+ results
172
+ )
173
  else:
174
  tasks.setdefault(edge.target, []).extend(results)
175
  node.publish_result(result)
lynxkite-core/src/lynxkite/core/ops.py CHANGED
@@ -208,9 +208,7 @@ def op(env: str, name: str, *, view="basic", outputs=None, params=None):
208
  if outputs:
209
  _outputs = {name: Output(name=name, type=None) for name in outputs}
210
  else:
211
- _outputs = (
212
- {"output": Output(name="output", type=None)} if view == "basic" else {}
213
- )
214
  op = Op(
215
  func=func,
216
  name=name,
@@ -264,12 +262,10 @@ def register_passive_op(env: str, name: str, inputs=[], outputs=["output"], para
264
  name=name,
265
  params={p.name: p for p in params},
266
  inputs=dict(
267
- (i, Input(name=i, type=None)) if isinstance(i, str) else (i.name, i)
268
- for i in inputs
269
  ),
270
  outputs=dict(
271
- (o, Output(name=o, type=None)) if isinstance(o, str) else (o.name, o)
272
- for o in outputs
273
  ),
274
  )
275
  CATALOGS.setdefault(env, {})
 
208
  if outputs:
209
  _outputs = {name: Output(name=name, type=None) for name in outputs}
210
  else:
211
+ _outputs = {"output": Output(name="output", type=None)} if view == "basic" else {}
 
 
212
  op = Op(
213
  func=func,
214
  name=name,
 
262
  name=name,
263
  params={p.name: p for p in params},
264
  inputs=dict(
265
+ (i, Input(name=i, type=None)) if isinstance(i, str) else (i.name, i) for i in inputs
 
266
  ),
267
  outputs=dict(
268
+ (o, Output(name=o, type=None)) if isinstance(o, str) else (o.name, o) for o in outputs
 
269
  ),
270
  )
271
  CATALOGS.setdefault(env, {})
lynxkite-core/src/lynxkite/core/workspace.py CHANGED
@@ -111,9 +111,7 @@ def save(ws: Workspace, path: str):
111
  if dirname:
112
  os.makedirs(dirname, exist_ok=True)
113
  # Create temp file in the same directory to make sure it's on the same filesystem.
114
- with tempfile.NamedTemporaryFile(
115
- "w", prefix=f".{basename}.", dir=dirname, delete=False
116
- ) as f:
117
  temp_name = f.name
118
  f.write(j)
119
  os.replace(temp_name, path)
 
111
  if dirname:
112
  os.makedirs(dirname, exist_ok=True)
113
  # Create temp file in the same directory to make sure it's on the same filesystem.
114
+ with tempfile.NamedTemporaryFile("w", prefix=f".{basename}.", dir=dirname, delete=False) as f:
 
 
115
  temp_name = f.name
116
  f.write(j)
117
  os.replace(temp_name, path)
lynxkite-core/tests/test_ops.py CHANGED
@@ -14,9 +14,7 @@ def test_op_decorator_no_params_no_types_default_positions():
14
  "a": ops.Input(name="a", type=inspect._empty, position="left"),
15
  "b": ops.Input(name="b", type=inspect._empty, position="left"),
16
  }
17
- assert add.__op__.outputs == {
18
- "result": ops.Output(name="result", type=None, position="right")
19
- }
20
  assert add.__op__.type == "basic"
21
  assert ops.CATALOGS["test"]["add"] == add.__op__
22
 
@@ -34,9 +32,7 @@ def test_op_decorator_custom_positions():
34
  "a": ops.Input(name="a", type=inspect._empty, position="right"),
35
  "b": ops.Input(name="b", type=inspect._empty, position="top"),
36
  }
37
- assert add.__op__.outputs == {
38
- "result": ops.Output(name="result", type=None, position="bottom")
39
- }
40
  assert add.__op__.type == "basic"
41
  assert ops.CATALOGS["test"]["add"] == add.__op__
42
 
@@ -76,9 +72,7 @@ def test_op_decorator_with_complex_types():
76
  assert complex_op.__op__.inputs == {
77
  "color": ops.Input(name="color", type=Color, position="left"),
78
  "color_list": ops.Input(name="color_list", type=list[Color], position="left"),
79
- "color_dict": ops.Input(
80
- name="color_dict", type=dict[str, Color], position="left"
81
- ),
82
  }
83
  assert complex_op.__op__.type == "basic"
84
  assert complex_op.__op__.outputs == {
 
14
  "a": ops.Input(name="a", type=inspect._empty, position="left"),
15
  "b": ops.Input(name="b", type=inspect._empty, position="left"),
16
  }
17
+ assert add.__op__.outputs == {"result": ops.Output(name="result", type=None, position="right")}
 
 
18
  assert add.__op__.type == "basic"
19
  assert ops.CATALOGS["test"]["add"] == add.__op__
20
 
 
32
  "a": ops.Input(name="a", type=inspect._empty, position="right"),
33
  "b": ops.Input(name="b", type=inspect._empty, position="top"),
34
  }
35
+ assert add.__op__.outputs == {"result": ops.Output(name="result", type=None, position="bottom")}
 
 
36
  assert add.__op__.type == "basic"
37
  assert ops.CATALOGS["test"]["add"] == add.__op__
38
 
 
72
  assert complex_op.__op__.inputs == {
73
  "color": ops.Input(name="color", type=Color, position="left"),
74
  "color_list": ops.Input(name="color_list", type=list[Color], position="left"),
75
+ "color_dict": ops.Input(name="color_dict", type=dict[str, Color], position="left"),
 
 
76
  }
77
  assert complex_op.__op__.type == "basic"
78
  assert complex_op.__op__.outputs == {
lynxkite-graph-analytics/src/lynxkite_graph_analytics/bionemo_ops.py CHANGED
@@ -89,9 +89,7 @@ def download_cellxgene_dataset(
89
  adata.write_h5ad(h5ad_outfile)
90
  with tempfile.TemporaryDirectory() as temp_dir:
91
  coll = SingleCellCollection(temp_dir)
92
- coll.load_h5ad_multi(
93
- h5ad_outfile.parent, max_workers=max_workers, use_processes=use_mp
94
- )
95
  coll.flatten(DATA_PATH / save_path, destroy_on_copy=True)
96
  return DATA_PATH / save_path
97
 
@@ -148,9 +146,7 @@ def download_model(*, model_name: str) -> str:
148
 
149
  @op("BioNeMo > Infer")
150
  @mem.cache(verbose=1)
151
- def infer(
152
- dataset_path: str, model_path: str | None = None, *, results_path: str
153
- ) -> str:
154
  """Infer on a dataset."""
155
  # This import is slow, so we only import it when we need it.
156
  from bionemo.geneformer.scripts.infer_geneformer import infer_model
@@ -175,10 +171,7 @@ def infer(
175
  @op("BioNeMo > Load results")
176
  def load_results(results_path: str):
177
  embeddings = (
178
- torch.load(f"{results_path}/predictions__rank_0.pt")["embeddings"]
179
- .float()
180
- .cpu()
181
- .numpy()
182
  )
183
  return embeddings
184
 
@@ -248,9 +241,7 @@ def run_benchmark(data, labels, *, use_pca: bool = False):
248
  ]
249
  )
250
  else:
251
- pipeline = Pipeline(
252
- [("classifier", RandomForestClassifier(class_weight="balanced"))]
253
- )
254
 
255
  # Set up StratifiedKFold to ensure each fold reflects the overall distribution of labels
256
  cv = StratifiedKFold(n_splits=5)
@@ -258,9 +249,7 @@ def run_benchmark(data, labels, *, use_pca: bool = False):
258
  # Define the scoring functions
259
  scoring = {
260
  "accuracy": make_scorer(accuracy_score),
261
- "precision": make_scorer(
262
- precision_score, average="macro"
263
- ), # 'macro' averages over classes
264
  "recall": make_scorer(recall_score, average="macro"),
265
  "f1_score": make_scorer(f1_score, average="macro"),
266
  # 'roc_auc' requires probability or decision function; hence use multi_class if applicable
@@ -298,9 +287,7 @@ def plot_confusion_matrix(benchmark_output, labels):
298
  # heatmap has the 0,0 at the bottom left corner
299
  num_rows = len(str_labels)
300
  heatmap_data = [
301
- [j, num_rows - i - 1, norm_cm[i][j]]
302
- for i in range(len(labels))
303
- for j in range(len(labels))
304
  ]
305
 
306
  options = {
@@ -332,9 +319,7 @@ def plot_confusion_matrix(benchmark_output, labels):
332
  "orient": "vertical",
333
  "right": 10,
334
  "top": "center",
335
- "inRange": {
336
- "color": ["#E0F7FA", "#81D4FA", "#29B6F6", "#0288D1", "#01579B"]
337
- },
338
  },
339
  "series": [
340
  {
@@ -424,9 +409,7 @@ def accuracy_comparison(benchmark_output10m, benchmark_output100m):
424
  {
425
  "name": "Error Bars",
426
  "type": "errorbar",
427
- "data": [
428
- [val - err, val + err] for val, err in zip(values, error_bars)
429
- ],
430
  "itemStyle": {"color": "#1f77b4"},
431
  },
432
  ],
@@ -509,9 +492,7 @@ def f1_comparison(benchmark_output10m, benchmark_output100m):
509
  {
510
  "name": "Error Bars",
511
  "type": "errorbar",
512
- "data": [
513
- [val - err, val + err] for val, err in zip(values, error_bars)
514
- ],
515
  "itemStyle": {"color": "#1f77b4"},
516
  },
517
  ],
 
89
  adata.write_h5ad(h5ad_outfile)
90
  with tempfile.TemporaryDirectory() as temp_dir:
91
  coll = SingleCellCollection(temp_dir)
92
+ coll.load_h5ad_multi(h5ad_outfile.parent, max_workers=max_workers, use_processes=use_mp)
 
 
93
  coll.flatten(DATA_PATH / save_path, destroy_on_copy=True)
94
  return DATA_PATH / save_path
95
 
 
146
 
147
  @op("BioNeMo > Infer")
148
  @mem.cache(verbose=1)
149
+ def infer(dataset_path: str, model_path: str | None = None, *, results_path: str) -> str:
 
 
150
  """Infer on a dataset."""
151
  # This import is slow, so we only import it when we need it.
152
  from bionemo.geneformer.scripts.infer_geneformer import infer_model
 
171
  @op("BioNeMo > Load results")
172
  def load_results(results_path: str):
173
  embeddings = (
174
+ torch.load(f"{results_path}/predictions__rank_0.pt")["embeddings"].float().cpu().numpy()
 
 
 
175
  )
176
  return embeddings
177
 
 
241
  ]
242
  )
243
  else:
244
+ pipeline = Pipeline([("classifier", RandomForestClassifier(class_weight="balanced"))])
 
 
245
 
246
  # Set up StratifiedKFold to ensure each fold reflects the overall distribution of labels
247
  cv = StratifiedKFold(n_splits=5)
 
249
  # Define the scoring functions
250
  scoring = {
251
  "accuracy": make_scorer(accuracy_score),
252
+ "precision": make_scorer(precision_score, average="macro"), # 'macro' averages over classes
 
 
253
  "recall": make_scorer(recall_score, average="macro"),
254
  "f1_score": make_scorer(f1_score, average="macro"),
255
  # 'roc_auc' requires probability or decision function; hence use multi_class if applicable
 
287
  # heatmap has the 0,0 at the bottom left corner
288
  num_rows = len(str_labels)
289
  heatmap_data = [
290
+ [j, num_rows - i - 1, norm_cm[i][j]] for i in range(len(labels)) for j in range(len(labels))
 
 
291
  ]
292
 
293
  options = {
 
319
  "orient": "vertical",
320
  "right": 10,
321
  "top": "center",
322
+ "inRange": {"color": ["#E0F7FA", "#81D4FA", "#29B6F6", "#0288D1", "#01579B"]},
 
 
323
  },
324
  "series": [
325
  {
 
409
  {
410
  "name": "Error Bars",
411
  "type": "errorbar",
412
+ "data": [[val - err, val + err] for val, err in zip(values, error_bars)],
 
 
413
  "itemStyle": {"color": "#1f77b4"},
414
  },
415
  ],
 
492
  {
493
  "name": "Error Bars",
494
  "type": "errorbar",
495
+ "data": [[val - err, val + err] for val, err in zip(values, error_bars)],
 
 
496
  "itemStyle": {"color": "#1f77b4"},
497
  },
498
  ],
lynxkite-graph-analytics/src/lynxkite_graph_analytics/core.py CHANGED
@@ -19,12 +19,8 @@ class RelationDefinition:
19
  """Defines a set of edges."""
20
 
21
  df: str # The DataFrame that contains the edges.
22
- source_column: (
23
- str # The column in the edge DataFrame that contains the source node ID.
24
- )
25
- target_column: (
26
- str # The column in the edge DataFrame that contains the target node ID.
27
- )
28
  source_table: str # The DataFrame that contains the source nodes.
29
  target_table: str # The DataFrame that contains the target nodes.
30
  source_key: str # The column in the source table that contains the node ID.
@@ -86,11 +82,7 @@ class Bundle:
86
  (
87
  e["source"],
88
  e["target"],
89
- {
90
- k: e[k]
91
- for k in edges.columns
92
- if k not in ["source", "target"]
93
- },
94
  )
95
  for e in edges.to_records()
96
  ]
@@ -129,9 +121,7 @@ class Bundle:
129
  for name, df in self.dfs.items()
130
  },
131
  "relations": [dataclasses.asdict(relation) for relation in self.relations],
132
- "other": {
133
- k: getattr(v, "metadata", lambda: {})() for k, v in self.other.items()
134
- },
135
  }
136
 
137
 
@@ -199,9 +189,7 @@ def _execute_node(node, ws, catalog, outputs):
199
  node.publish_started()
200
  # TODO: Handle multi-inputs.
201
  input_map = {
202
- edge.targetHandle: outputs[edge.source]
203
- for edge in ws.edges
204
- if edge.target == node.id
205
  }
206
  # Convert inputs types to match operation signature.
207
  try:
 
19
  """Defines a set of edges."""
20
 
21
  df: str # The DataFrame that contains the edges.
22
+ source_column: str # The column in the edge DataFrame that contains the source node ID.
23
+ target_column: str # The column in the edge DataFrame that contains the target node ID.
 
 
 
 
24
  source_table: str # The DataFrame that contains the source nodes.
25
  target_table: str # The DataFrame that contains the target nodes.
26
  source_key: str # The column in the source table that contains the node ID.
 
82
  (
83
  e["source"],
84
  e["target"],
85
+ {k: e[k] for k in edges.columns if k not in ["source", "target"]},
 
 
 
 
86
  )
87
  for e in edges.to_records()
88
  ]
 
121
  for name, df in self.dfs.items()
122
  },
123
  "relations": [dataclasses.asdict(relation) for relation in self.relations],
124
+ "other": {k: getattr(v, "metadata", lambda: {})() for k, v in self.other.items()},
 
 
125
  }
126
 
127
 
 
189
  node.publish_started()
190
  # TODO: Handle multi-inputs.
191
  input_map = {
192
+ edge.targetHandle: outputs[edge.source] for edge in ws.edges if edge.target == node.id
 
 
193
  }
194
  # Convert inputs types to match operation signature.
195
  try:
lynxkite-graph-analytics/src/lynxkite_graph_analytics/lynxkite_ops.py CHANGED
@@ -35,9 +35,7 @@ class FileFormat(enum.StrEnum):
35
  params={
36
  "file_format": ops.ParameterGroup(
37
  name="file_format",
38
- selector=ops.Parameter(
39
- name="file_format", type=FileFormat, default=FileFormat.csv
40
- ),
41
  groups={
42
  "csv": [
43
  ops.Parameter.basic("columns", type=str, default="<from file>"),
@@ -45,9 +43,7 @@ class FileFormat(enum.StrEnum):
45
  ],
46
  "parquet": [],
47
  "json": [],
48
- "excel": [
49
- ops.Parameter.basic("sheet_name", type=str, default="Sheet1")
50
- ],
51
  },
52
  default=FileFormat.csv,
53
  ),
@@ -68,9 +64,7 @@ def import_file(
68
  """
69
  if file_format == "csv":
70
  names = kwargs.pop("columns", "<from file>")
71
- names = (
72
- pd.api.extensions.no_default if names == "<from file>" else names.split(",")
73
- )
74
  sep = kwargs.pop("separator", "<auto>")
75
  sep = pd.api.extensions.no_default if sep == "<auto>" else sep
76
  df = pd.read_csv(file_path, names=names, sep=sep, **kwargs)
@@ -93,15 +87,11 @@ def import_parquet(*, filename: str):
93
 
94
  @op("Import CSV")
95
  @mem.cache
96
- def import_csv(
97
- *, filename: str, columns: str = "<from file>", separator: str = "<auto>"
98
- ):
99
  """Imports a CSV file."""
100
  return pd.read_csv(
101
  filename,
102
- names=pd.api.extensions.no_default
103
- if columns == "<from file>"
104
- else columns.split(","),
105
  sep=pd.api.extensions.no_default if separator == "<auto>" else separator,
106
  )
107
 
@@ -145,12 +135,7 @@ def sql(bundle: core.Bundle, *, query: ops.LongStr, save_as: str = "result"):
145
  if os.environ.get("NX_CUGRAPH_AUTOCONFIG", "").strip().lower() == "true":
146
  with pl.Config() as cfg:
147
  cfg.set_verbose(True)
148
- res = (
149
- pl.SQLContext(bundle.dfs)
150
- .execute(query)
151
- .collect(engine="gpu")
152
- .to_pandas()
153
- )
154
  # TODO: Currently `collect()` moves the data from cuDF to Polars. Then we convert it to Pandas,
155
  # which (hopefully) puts it back into cuDF. Hopefully we will be able to keep it in cuDF.
156
  else:
@@ -211,9 +196,7 @@ def _map_color(value):
211
  colors = cmap.colors[: len(categories)]
212
  return [
213
  "#{:02x}{:02x}{:02x}".format(int(r * 255), int(g * 255), int(b * 255))
214
- for r, g, b in [
215
- colors[min(len(colors) - 1, categories.get_loc(v))] for v in value
216
- ]
217
  ]
218
 
219
 
@@ -250,14 +233,10 @@ def visualize_graph(
250
  curveness = 0 # Street maps are better with straight streets.
251
  break
252
  else:
253
- pos = nx.spring_layout(
254
- graph.to_nx(), iterations=max(1, int(10000 / len(nodes)))
255
- )
256
  curveness = 0.3
257
  nodes = nodes.to_records()
258
- edges = core.df_for_frontend(
259
- graph.dfs["edges"].drop_duplicates(["source", "target"]), 10_000
260
- )
261
  if color_edges_by:
262
  edges["color"] = _map_color(edges[color_edges_by])
263
  edges = edges.to_records()
@@ -291,9 +270,7 @@ def visualize_graph(
291
  "itemStyle": {"color": n.color} if color_nodes_by else {},
292
  "label": {"show": label_by is not None},
293
  "name": str(getattr(n, label_by, "")) if label_by else None,
294
- "value": str(getattr(n, color_nodes_by, ""))
295
- if color_nodes_by
296
- else None,
297
  }
298
  for n in nodes
299
  ],
@@ -302,9 +279,7 @@ def visualize_graph(
302
  "source": str(r.source),
303
  "target": str(r.target),
304
  "lineStyle": {"color": r.color} if color_edges_by else {},
305
- "value": str(getattr(r, color_edges_by, ""))
306
- if color_edges_by
307
- else None,
308
  }
309
  for r in edges
310
  ],
@@ -342,9 +317,7 @@ def create_graph(bundle: core.Bundle, *, relations: str = None) -> core.Bundle:
342
  """
343
  bundle = bundle.copy()
344
  if not (relations is None or relations.strip() == ""):
345
- bundle.relations = [
346
- core.RelationDefinition(**r) for r in json.loads(relations).values()
347
- ]
348
  return ops.Result(output=bundle, display=bundle.to_dict(limit=100))
349
 
350
 
 
35
  params={
36
  "file_format": ops.ParameterGroup(
37
  name="file_format",
38
+ selector=ops.Parameter(name="file_format", type=FileFormat, default=FileFormat.csv),
 
 
39
  groups={
40
  "csv": [
41
  ops.Parameter.basic("columns", type=str, default="<from file>"),
 
43
  ],
44
  "parquet": [],
45
  "json": [],
46
+ "excel": [ops.Parameter.basic("sheet_name", type=str, default="Sheet1")],
 
 
47
  },
48
  default=FileFormat.csv,
49
  ),
 
64
  """
65
  if file_format == "csv":
66
  names = kwargs.pop("columns", "<from file>")
67
+ names = pd.api.extensions.no_default if names == "<from file>" else names.split(",")
 
 
68
  sep = kwargs.pop("separator", "<auto>")
69
  sep = pd.api.extensions.no_default if sep == "<auto>" else sep
70
  df = pd.read_csv(file_path, names=names, sep=sep, **kwargs)
 
87
 
88
  @op("Import CSV")
89
  @mem.cache
90
+ def import_csv(*, filename: str, columns: str = "<from file>", separator: str = "<auto>"):
 
 
91
  """Imports a CSV file."""
92
  return pd.read_csv(
93
  filename,
94
+ names=pd.api.extensions.no_default if columns == "<from file>" else columns.split(","),
 
 
95
  sep=pd.api.extensions.no_default if separator == "<auto>" else separator,
96
  )
97
 
 
135
  if os.environ.get("NX_CUGRAPH_AUTOCONFIG", "").strip().lower() == "true":
136
  with pl.Config() as cfg:
137
  cfg.set_verbose(True)
138
+ res = pl.SQLContext(bundle.dfs).execute(query).collect(engine="gpu").to_pandas()
 
 
 
 
 
139
  # TODO: Currently `collect()` moves the data from cuDF to Polars. Then we convert it to Pandas,
140
  # which (hopefully) puts it back into cuDF. Hopefully we will be able to keep it in cuDF.
141
  else:
 
196
  colors = cmap.colors[: len(categories)]
197
  return [
198
  "#{:02x}{:02x}{:02x}".format(int(r * 255), int(g * 255), int(b * 255))
199
+ for r, g, b in [colors[min(len(colors) - 1, categories.get_loc(v))] for v in value]
 
 
200
  ]
201
 
202
 
 
233
  curveness = 0 # Street maps are better with straight streets.
234
  break
235
  else:
236
+ pos = nx.spring_layout(graph.to_nx(), iterations=max(1, int(10000 / len(nodes))))
 
 
237
  curveness = 0.3
238
  nodes = nodes.to_records()
239
+ edges = core.df_for_frontend(graph.dfs["edges"].drop_duplicates(["source", "target"]), 10_000)
 
 
240
  if color_edges_by:
241
  edges["color"] = _map_color(edges[color_edges_by])
242
  edges = edges.to_records()
 
270
  "itemStyle": {"color": n.color} if color_nodes_by else {},
271
  "label": {"show": label_by is not None},
272
  "name": str(getattr(n, label_by, "")) if label_by else None,
273
+ "value": str(getattr(n, color_nodes_by, "")) if color_nodes_by else None,
 
 
274
  }
275
  for n in nodes
276
  ],
 
279
  "source": str(r.source),
280
  "target": str(r.target),
281
  "lineStyle": {"color": r.color} if color_edges_by else {},
282
+ "value": str(getattr(r, color_edges_by, "")) if color_edges_by else None,
 
 
283
  }
284
  for r in edges
285
  ],
 
317
  """
318
  bundle = bundle.copy()
319
  if not (relations is None or relations.strip() == ""):
320
+ bundle.relations = [core.RelationDefinition(**r) for r in json.loads(relations).values()]
 
 
321
  return ops.Result(output=bundle, display=bundle.to_dict(limit=100))
322
 
323
 
lynxkite-graph-analytics/src/lynxkite_graph_analytics/networkx_ops.py CHANGED
@@ -210,9 +210,7 @@ def _get_params(func) -> dict | None:
210
  continue
211
  params[name] = ops.Parameter.basic(
212
  name=name,
213
- default=str(param.default)
214
- if type(param.default) in [str, int, float]
215
- else None,
216
  type=_type,
217
  )
218
  return params
 
210
  continue
211
  params[name] = ops.Parameter.basic(
212
  name=name,
213
+ default=str(param.default) if type(param.default) in [str, int, float] else None,
 
 
214
  type=_type,
215
  )
216
  return params
lynxkite-graph-analytics/src/lynxkite_graph_analytics/pytorch_model_ops.py CHANGED
@@ -21,12 +21,8 @@ def reg(name, inputs=[], outputs=None, params=[]):
21
  return ops.register_passive_op(
22
  ENV,
23
  name,
24
- inputs=[
25
- ops.Input(name=name, position="bottom", type="tensor") for name in inputs
26
- ],
27
- outputs=[
28
- ops.Output(name=name, position="top", type="tensor") for name in outputs
29
- ],
30
  params=params,
31
  )
32
 
@@ -196,9 +192,7 @@ class ModelConfig:
196
  }
197
 
198
 
199
- def build_model(
200
- ws: workspace.Workspace, inputs: dict[str, torch.Tensor]
201
- ) -> ModelConfig:
202
  """Builds the model described in the workspace."""
203
  catalog = ops.CATALOGS[ENV]
204
  optimizers = []
@@ -272,9 +266,7 @@ def build_model(
272
  ls.append((torch.nn.Linear(isize, osize), f"{inputs.x} -> {outputs.x}"))
273
  sizes[outputs.x] = osize
274
  case "Activation":
275
- f = getattr(
276
- torch.nn.functional, p["type"].name.lower().replace(" ", "_")
277
- )
278
  ls.append((f, f"{inputs.x} -> {outputs.x}"))
279
  sizes[outputs.x] = sizes.get(inputs.x, 1)
280
  case "MSE loss":
@@ -316,7 +308,5 @@ def to_tensors(b: core.Bundle, m: ModelMapping | None) -> dict[str, torch.Tensor
316
  tensors = {}
317
  for k, v in m.map.items():
318
  if v.df in b.dfs and v.column in b.dfs[v.df]:
319
- tensors[k] = torch.tensor(
320
- b.dfs[v.df][v.column].to_list(), dtype=torch.float32
321
- )
322
  return tensors
 
21
  return ops.register_passive_op(
22
  ENV,
23
  name,
24
+ inputs=[ops.Input(name=name, position="bottom", type="tensor") for name in inputs],
25
+ outputs=[ops.Output(name=name, position="top", type="tensor") for name in outputs],
 
 
 
 
26
  params=params,
27
  )
28
 
 
192
  }
193
 
194
 
195
+ def build_model(ws: workspace.Workspace, inputs: dict[str, torch.Tensor]) -> ModelConfig:
 
 
196
  """Builds the model described in the workspace."""
197
  catalog = ops.CATALOGS[ENV]
198
  optimizers = []
 
266
  ls.append((torch.nn.Linear(isize, osize), f"{inputs.x} -> {outputs.x}"))
267
  sizes[outputs.x] = osize
268
  case "Activation":
269
+ f = getattr(torch.nn.functional, p["type"].name.lower().replace(" ", "_"))
 
 
270
  ls.append((f, f"{inputs.x} -> {outputs.x}"))
271
  sizes[outputs.x] = sizes.get(inputs.x, 1)
272
  case "MSE loss":
 
308
  tensors = {}
309
  for k, v in m.map.items():
310
  if v.df in b.dfs and v.column in b.dfs[v.df]:
311
+ tensors[k] = torch.tensor(b.dfs[v.df][v.column].to_list(), dtype=torch.float32)
 
 
312
  return tensors
lynxkite-lynxscribe/src/lynxkite_lynxscribe/llm_ops.py CHANGED
@@ -32,9 +32,7 @@ def chat(*args, **kwargs):
32
 
33
  chat_client = openai.OpenAI(base_url=LLM_BASE_URL)
34
  kwargs.setdefault("model", LLM_MODEL)
35
- key = json.dumps(
36
- {"method": "chat", "base_url": LLM_BASE_URL, "args": args, "kwargs": kwargs}
37
- )
38
  if key not in LLM_CACHE:
39
  completion = chat_client.chat.completions.create(*args, **kwargs)
40
  LLM_CACHE[key] = [c.message.content for c in completion.choices]
@@ -121,9 +119,7 @@ def add_neighbors(nodes, edges, item):
121
 
122
  @op("Create prompt")
123
  def create_prompt(input, *, save_as="prompt", template: ops.LongStr):
124
- assert template, (
125
- "Please specify the template. Refer to columns using the Jinja2 syntax."
126
- )
127
  t = jinja.from_string(template)
128
  prompt = t.render(**input)
129
  return {**input, save_as: prompt}
 
32
 
33
  chat_client = openai.OpenAI(base_url=LLM_BASE_URL)
34
  kwargs.setdefault("model", LLM_MODEL)
35
+ key = json.dumps({"method": "chat", "base_url": LLM_BASE_URL, "args": args, "kwargs": kwargs})
 
 
36
  if key not in LLM_CACHE:
37
  completion = chat_client.chat.completions.create(*args, **kwargs)
38
  LLM_CACHE[key] = [c.message.content for c in completion.choices]
 
119
 
120
  @op("Create prompt")
121
  def create_prompt(input, *, save_as="prompt", template: ops.LongStr):
122
+ assert template, "Please specify the template. Refer to columns using the Jinja2 syntax."
 
 
123
  t = jinja.from_string(template)
124
  prompt = t.render(**input)
125
  return {**input, save_as: prompt}