darabos commited on
Commit
75a337a
·
2 Parent(s): e8e05ff 5bf75e0

Merge pull request #168 from biggraph/darabos-ws-docs

Browse files
examples/fake_data.py CHANGED
@@ -7,6 +7,11 @@ faker = Faker()
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)],
 
7
 
8
  @op("LynxKite Graph Analytics", "Fake data")
9
  def fake(*, n=10):
10
+ """Creates a DataFrame with random-generated names and postal addresses.
11
+
12
+ Parameters:
13
+ n: Number of rows to create.
14
+ """
15
  df = pd.DataFrame(
16
  {
17
  "name": [faker.name() for _ in range(n)],
lynxkite-app/pyproject.toml CHANGED
@@ -10,6 +10,7 @@ dependencies = [
10
  "orjson>=3.10.13",
11
  "pycrdt-websocket>=0.15.3",
12
  "sse-starlette>=2.2.1",
 
13
  ]
14
 
15
  [project.optional-dependencies]
 
10
  "orjson>=3.10.13",
11
  "pycrdt-websocket>=0.15.3",
12
  "sse-starlette>=2.2.1",
13
+ "griffe>=1.7.3",
14
  ]
15
 
16
  [project.optional-dependencies]
lynxkite-app/web/package-lock.json CHANGED
@@ -30,6 +30,7 @@
30
  "react-error-boundary": "^5.0.0",
31
  "react-markdown": "^9.0.1",
32
  "react-router-dom": "^7.5.2",
 
33
  "swr": "^2.2.5",
34
  "unplugin-icons": "^0.21.0",
35
  "y-monaco": "^0.1.6",
@@ -940,6 +941,31 @@
940
  "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
941
  }
942
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
943
  "node_modules/@humanfs/core": {
944
  "version": "0.19.1",
945
  "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -2879,6 +2905,12 @@
2879
  "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
2880
  "license": "MIT"
2881
  },
 
 
 
 
 
 
2882
  "node_modules/client-only": {
2883
  "version": "0.0.1",
2884
  "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
@@ -6239,6 +6271,20 @@
6239
  "react-dom": ">=18"
6240
  }
6241
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6242
  "node_modules/read-cache": {
6243
  "version": "1.0.0",
6244
  "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
 
30
  "react-error-boundary": "^5.0.0",
31
  "react-markdown": "^9.0.1",
32
  "react-router-dom": "^7.5.2",
33
+ "react-tooltip": "^5.28.1",
34
  "swr": "^2.2.5",
35
  "unplugin-icons": "^0.21.0",
36
  "y-monaco": "^0.1.6",
 
941
  "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
942
  }
943
  },
944
+ "node_modules/@floating-ui/core": {
945
+ "version": "1.6.9",
946
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
947
+ "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
948
+ "license": "MIT",
949
+ "dependencies": {
950
+ "@floating-ui/utils": "^0.2.9"
951
+ }
952
+ },
953
+ "node_modules/@floating-ui/dom": {
954
+ "version": "1.6.13",
955
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
956
+ "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
957
+ "license": "MIT",
958
+ "dependencies": {
959
+ "@floating-ui/core": "^1.6.0",
960
+ "@floating-ui/utils": "^0.2.9"
961
+ }
962
+ },
963
+ "node_modules/@floating-ui/utils": {
964
+ "version": "0.2.9",
965
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
966
+ "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
967
+ "license": "MIT"
968
+ },
969
  "node_modules/@humanfs/core": {
970
  "version": "0.19.1",
971
  "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
 
2905
  "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
2906
  "license": "MIT"
2907
  },
2908
+ "node_modules/classnames": {
2909
+ "version": "2.5.1",
2910
+ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
2911
+ "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
2912
+ "license": "MIT"
2913
+ },
2914
  "node_modules/client-only": {
2915
  "version": "0.0.1",
2916
  "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
 
6271
  "react-dom": ">=18"
6272
  }
6273
  },
6274
+ "node_modules/react-tooltip": {
6275
+ "version": "5.28.1",
6276
+ "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.28.1.tgz",
6277
+ "integrity": "sha512-ZA4oHwoIIK09TS7PvSLFcRlje1wGZaxw6xHvfrzn6T82UcMEfEmHVCad16Gnr4NDNDh93HyN037VK4HDi5odfQ==",
6278
+ "license": "MIT",
6279
+ "dependencies": {
6280
+ "@floating-ui/dom": "^1.6.1",
6281
+ "classnames": "^2.3.0"
6282
+ },
6283
+ "peerDependencies": {
6284
+ "react": ">=16.14.0",
6285
+ "react-dom": ">=16.14.0"
6286
+ }
6287
+ },
6288
  "node_modules/read-cache": {
6289
  "version": "1.0.0",
6290
  "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
lynxkite-app/web/package.json CHANGED
@@ -33,6 +33,7 @@
33
  "react-error-boundary": "^5.0.0",
34
  "react-markdown": "^9.0.1",
35
  "react-router-dom": "^7.5.2",
 
36
  "swr": "^2.2.5",
37
  "unplugin-icons": "^0.21.0",
38
  "y-monaco": "^0.1.6",
 
33
  "react-error-boundary": "^5.0.0",
34
  "react-markdown": "^9.0.1",
35
  "react-router-dom": "^7.5.2",
36
+ "react-tooltip": "^5.28.1",
37
  "swr": "^2.2.5",
38
  "unplugin-icons": "^0.21.0",
39
  "y-monaco": "^0.1.6",
lynxkite-app/web/src/Tooltip.tsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useId } from "react";
2
+ import Markdown from "react-markdown";
3
+ import { Tooltip as ReactTooltip } from "react-tooltip";
4
+
5
+ export default function Tooltip(props: any) {
6
+ const id = useId();
7
+ if (!props.doc) return null;
8
+ return (
9
+ <>
10
+ <a data-tooltip-id={id} tabIndex={0}>
11
+ {props.children}
12
+ </a>
13
+ <ReactTooltip id={id} className="tooltip" place="top-end">
14
+ {props.doc.map?.(
15
+ (section: any, i: number) =>
16
+ section.kind === "text" && <Markdown key={i}>{section.value}</Markdown>,
17
+ ) ?? <Markdown>{props.doc}</Markdown>}
18
+ </ReactTooltip>
19
+ </>
20
+ );
21
+ }
lynxkite-app/web/src/index.css CHANGED
@@ -68,11 +68,6 @@ body {
68
  font-size: 12px;
69
  }
70
 
71
- .title-icon {
72
- margin-left: 5px;
73
- float: right;
74
- }
75
-
76
  .node-container {
77
  padding: 8px;
78
  position: relative;
@@ -93,8 +88,21 @@ body {
93
  }
94
  }
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  .expanded .lynxkite-node {
97
- overflow-y: auto;
98
  height: 100%;
99
  }
100
 
@@ -114,6 +122,13 @@ body {
114
  --status-color-2: #0000;
115
  --status-color-3: #0000;
116
  transition: --status-color-1 0.3s, --status-color-2 0.3s, --status-color-3 0.3s;
 
 
 
 
 
 
 
117
  }
118
 
119
  .lynxkite-node .title.active {
@@ -143,24 +158,40 @@ body {
143
  visibility: hidden;
144
  }
145
 
146
- .react-flow__handle-left .handle-name {
147
- right: 20px;
 
 
 
 
148
  }
149
 
150
- .react-flow__handle-right .handle-name {
151
- left: 20px;
 
 
 
 
152
  }
153
 
154
- .react-flow__handle-top .handle-name {
155
- top: -20px;
156
- left: 5px;
157
- backdrop-filter: none;
 
 
 
 
158
  }
159
 
160
- .react-flow__handle-bottom .handle-name {
161
- top: 0px;
162
- left: 5px;
163
- backdrop-filter: none;
 
 
 
 
164
  }
165
 
166
  .node-container:hover .handle-name {
@@ -180,6 +211,13 @@ body {
180
  display: block;
181
  }
182
 
 
 
 
 
 
 
 
183
  .param-name {
184
  display: block;
185
  font-size: 10px;
@@ -453,8 +491,9 @@ body {
453
  .react-flow__handle {
454
  border-color: black;
455
  background: white;
456
- width: 10px;
457
- height: 10px;
 
458
  }
459
 
460
  .react-flow__arrowhead * {
 
68
  font-size: 12px;
69
  }
70
 
 
 
 
 
 
71
  .node-container {
72
  padding: 8px;
73
  position: relative;
 
88
  }
89
  }
90
 
91
+ .tooltip {
92
+ padding: 8px;
93
+ border-radius: 4px;
94
+ opacity: 1;
95
+ text-align: left;
96
+ background: #fffa;
97
+ color: black;
98
+ box-shadow: 0px 5px 50px 0px rgba(0, 0, 0, 0.1);
99
+ backdrop-filter: blur(10px);
100
+ font-size: 16px;
101
+ font-weight: initial;
102
+ max-width: 300px;
103
+ }
104
+
105
  .expanded .lynxkite-node {
 
106
  height: 100%;
107
  }
108
 
 
122
  --status-color-2: #0000;
123
  --status-color-3: #0000;
124
  transition: --status-color-1 0.3s, --status-color-2 0.3s, --status-color-3 0.3s;
125
+ display: flex;
126
+ flex-direction: row;
127
+ gap: 10px;
128
+
129
+ .title-title {
130
+ flex: 1;
131
+ }
132
  }
133
 
134
  .lynxkite-node .title.active {
 
158
  visibility: hidden;
159
  }
160
 
161
+ .react-flow__handle-left {
162
+ left: -5px;
163
+
164
+ .handle-name {
165
+ right: 30px;
166
+ }
167
  }
168
 
169
+ .react-flow__handle-right {
170
+ right: -5px;
171
+
172
+ .handle-name {
173
+ left: 30px;
174
+ }
175
  }
176
 
177
+ .react-flow__handle-top {
178
+ top: -5px;
179
+
180
+ .handle-name {
181
+ top: -3px;
182
+ left: 13px;
183
+ backdrop-filter: none;
184
+ }
185
  }
186
 
187
+ .react-flow__handle-bottom {
188
+ bottom: -5px;
189
+
190
+ .handle-name {
191
+ top: 0px;
192
+ left: 13px;
193
+ backdrop-filter: none;
194
+ }
195
  }
196
 
197
  .node-container:hover .handle-name {
 
211
  display: block;
212
  }
213
 
214
+ .param-name-row {
215
+ display: flex;
216
+ flex-direction: row;
217
+ justify-content: space-between;
218
+ align-items: end;
219
+ }
220
+
221
  .param-name {
222
  display: block;
223
  font-size: 10px;
 
491
  .react-flow__handle {
492
  border-color: black;
493
  background: white;
494
+ width: 20px;
495
+ height: 20px;
496
+ border-width: 2px;
497
  }
498
 
499
  .react-flow__arrowhead * {
lynxkite-app/web/src/workspace/LynxKiteEdge.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BaseEdge, Position } from "@xyflow/react";
2
+
3
+ function addOffset(x: number, y: number, p: Position, offset: number) {
4
+ if (p === Position.Top) return `${x},${y - offset}`;
5
+ if (p === Position.Bottom) return `${x},${y + offset}`;
6
+ if (p === Position.Left) return `${x - offset},${y}`;
7
+ return `${x + offset},${y}`;
8
+ }
9
+
10
+ export default function LynxKiteEdge(props: any) {
11
+ const offset = 0.3 * Math.hypot(props.targetX - props.sourceX, props.targetY - props.sourceY);
12
+ const s = addOffset(props.sourceX, props.sourceY, props.sourcePosition, 0);
13
+ const sc = addOffset(props.sourceX, props.sourceY, props.sourcePosition, offset);
14
+ const tc = addOffset(props.targetX, props.targetY, props.targetPosition, offset);
15
+ const t = addOffset(props.targetX, props.targetY, props.targetPosition, 0);
16
+ const path = `M${s} C${sc} ${tc} ${t}`;
17
+ return (
18
+ <>
19
+ <BaseEdge
20
+ id={props.id}
21
+ path={path}
22
+ {...props}
23
+ style={{
24
+ strokeWidth: 2,
25
+ stroke: "black",
26
+ }}
27
+ />
28
+ </>
29
+ );
30
+ }
lynxkite-app/web/src/workspace/Workspace.tsx CHANGED
@@ -34,6 +34,7 @@ import favicon from "../assets/favicon.ico";
34
  import { usePath } from "../common.ts";
35
  // import NodeWithTableView from './NodeWithTableView';
36
  import EnvironmentSelector from "./EnvironmentSelector";
 
37
  import { LynxKiteState } from "./LynxKiteState";
38
  import NodeSearch, { type OpsOp, type Catalog, type Catalogs } from "./NodeSearch.tsx";
39
  import NodeWithGraphCreationView from "./nodes/GraphCreationNode.tsx";
@@ -186,6 +187,12 @@ function LynxKiteFlow() {
186
  }),
187
  [],
188
  );
 
 
 
 
 
 
189
 
190
  // Global keyboard shortcuts.
191
  useEffect(() => {
@@ -280,7 +287,7 @@ function LynxKiteFlow() {
280
  (connection: Connection) => {
281
  setSuppressSearchUntil(Date.now() + 200);
282
  const edge = {
283
- id: `${connection.source} ${connection.target}`,
284
  source: connection.source,
285
  sourceHandle: connection.sourceHandle!,
286
  target: connection.target,
@@ -374,6 +381,7 @@ function LynxKiteFlow() {
374
  nodes={nodes}
375
  edges={edges}
376
  nodeTypes={nodeTypes}
 
377
  fitView
378
  onNodesChange={onNodesChange}
379
  onEdgesChange={onEdgesChange}
 
34
  import { usePath } from "../common.ts";
35
  // import NodeWithTableView from './NodeWithTableView';
36
  import EnvironmentSelector from "./EnvironmentSelector";
37
+ import LynxKiteEdge from "./LynxKiteEdge.tsx";
38
  import { LynxKiteState } from "./LynxKiteState";
39
  import NodeSearch, { type OpsOp, type Catalog, type Catalogs } from "./NodeSearch.tsx";
40
  import NodeWithGraphCreationView from "./nodes/GraphCreationNode.tsx";
 
187
  }),
188
  [],
189
  );
190
+ const edgeTypes = useMemo(
191
+ () => ({
192
+ default: LynxKiteEdge,
193
+ }),
194
+ [],
195
+ );
196
 
197
  // Global keyboard shortcuts.
198
  useEffect(() => {
 
287
  (connection: Connection) => {
288
  setSuppressSearchUntil(Date.now() + 200);
289
  const edge = {
290
+ id: `${connection.source} ${connection.sourceHandle} ${connection.target} ${connection.targetHandle}`,
291
  source: connection.source,
292
  sourceHandle: connection.sourceHandle!,
293
  target: connection.target,
 
381
  nodes={nodes}
382
  edges={edges}
383
  nodeTypes={nodeTypes}
384
+ edgeTypes={edgeTypes}
385
  fitView
386
  onNodesChange={onNodesChange}
387
  onEdgesChange={onEdgesChange}
lynxkite-app/web/src/workspace/nodes/LynxKiteNode.tsx CHANGED
@@ -1,9 +1,17 @@
1
  import { Handle, NodeResizeControl, type Position, useReactFlow } from "@xyflow/react";
 
2
  import { ErrorBoundary } from "react-error-boundary";
3
  // @ts-ignore
 
 
4
  import ChevronDownRight from "~icons/tabler/chevron-down-right.jsx";
5
  // @ts-ignore
 
 
 
 
6
  import Skull from "~icons/tabler/skull.jsx";
 
7
 
8
  interface LynxKiteNodeProps {
9
  id: string;
@@ -44,9 +52,11 @@ function getHandles(inputs: any[], outputs: any[]) {
44
  }
45
 
46
  const OP_COLORS: { [key: string]: string } = {
 
47
  orange: "oklch(75% 0.2 55)",
 
48
  blue: "oklch(75% 0.2 230)",
49
- green: "oklch(75% 0.2 130)",
50
  };
51
 
52
  function LynxKiteNodeComponent(props: LynxKiteNodeProps) {
@@ -81,9 +91,20 @@ function LynxKiteNodeComponent(props: LynxKiteNodeProps) {
81
  style={titleStyle}
82
  onClick={titleClicked}
83
  >
84
- {data.title}
85
- {data.error && <span className="title-icon">⚠️</span>}
86
- {expanded || <span className="title-icon">⋯</span>}
 
 
 
 
 
 
 
 
 
 
 
87
  </div>
88
  {expanded && (
89
  <>
 
1
  import { Handle, NodeResizeControl, type Position, useReactFlow } from "@xyflow/react";
2
+ import type React from "react";
3
  import { ErrorBoundary } from "react-error-boundary";
4
  // @ts-ignore
5
+ import AlertTriangle from "~icons/tabler/alert-triangle-filled.jsx";
6
+ // @ts-ignore
7
  import ChevronDownRight from "~icons/tabler/chevron-down-right.jsx";
8
  // @ts-ignore
9
+ import Dots from "~icons/tabler/dots.jsx";
10
+ // @ts-ignore
11
+ import Help from "~icons/tabler/question-mark.jsx";
12
+ // @ts-ignore
13
  import Skull from "~icons/tabler/skull.jsx";
14
+ import Tooltip from "../../Tooltip";
15
 
16
  interface LynxKiteNodeProps {
17
  id: string;
 
52
  }
53
 
54
  const OP_COLORS: { [key: string]: string } = {
55
+ pink: "oklch(75% 0.2 0)",
56
  orange: "oklch(75% 0.2 55)",
57
+ green: "oklch(75% 0.2 150)",
58
  blue: "oklch(75% 0.2 230)",
59
+ purple: "oklch(75% 0.2 290)",
60
  };
61
 
62
  function LynxKiteNodeComponent(props: LynxKiteNodeProps) {
 
91
  style={titleStyle}
92
  onClick={titleClicked}
93
  >
94
+ <span className="title-title">{data.title}</span>
95
+ {data.error && (
96
+ <Tooltip doc={`Error: ${data.error}`}>
97
+ <AlertTriangle />
98
+ </Tooltip>
99
+ )}
100
+ {expanded || (
101
+ <Tooltip doc="Click to expand node">
102
+ <Dots />
103
+ </Tooltip>
104
+ )}
105
+ <Tooltip doc={data.meta?.value?.doc}>
106
+ <Help />
107
+ </Tooltip>
108
  </div>
109
  {expanded && (
110
  <>
lynxkite-app/web/src/workspace/nodes/NodeParameter.tsx CHANGED
@@ -1,6 +1,9 @@
1
  import { useRef } from "react";
2
  // @ts-ignore
3
  import ArrowsHorizontal from "~icons/tabler/arrows-horizontal.jsx";
 
 
 
4
  import NodeGroupParameter from "./NodeGroupParameter";
5
 
6
  const BOOLEAN = "<class 'bool'>";
@@ -9,8 +12,19 @@ const MODEL_TRAINING_INPUT_MAPPING =
9
  const MODEL_INFERENCE_INPUT_MAPPING =
10
  "<class 'lynxkite_graph_analytics.ml_ops.ModelInferenceInputMapping'>";
11
  const MODEL_OUTPUT_MAPPING = "<class 'lynxkite_graph_analytics.ml_ops.ModelOutputMapping'>";
12
- function ParamName({ name }: { name: string }) {
13
- return <span className="param-name bg-base-200">{name.replace(/_/g, " ")}</span>;
 
 
 
 
 
 
 
 
 
 
 
14
  }
15
 
16
  function Input({
@@ -195,18 +209,31 @@ interface NodeParameterProps {
195
 
196
  export type UpdateOptions = { delay?: number };
197
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  export default function NodeParameter({ name, value, meta, data, setParam }: NodeParameterProps) {
 
199
  function onChange(value: any, opts?: UpdateOptions) {
200
  setParam(meta.name, value, opts || {});
201
  }
202
  return meta?.type?.format === "collapsed" ? (
203
  <label className="param">
204
- <ParamName name={name} />
205
  <button className="collapsed-param">⋯</button>
206
  </label>
207
  ) : meta?.type?.format === "textarea" ? (
208
  <label className="param">
209
- <ParamName name={name} />
210
  <textarea
211
  className="textarea textarea-bordered w-full"
212
  rows={6}
@@ -219,7 +246,7 @@ export default function NodeParameter({ name, value, meta, data, setParam }: Nod
219
  <NodeGroupParameter meta={meta} data={data} setParam={setParam} />
220
  ) : meta?.type?.enum ? (
221
  <label className="param">
222
- <ParamName name={name} />
223
  <select
224
  className="select select-bordered w-full"
225
  value={value || meta.type.enum[0]}
@@ -246,22 +273,22 @@ export default function NodeParameter({ name, value, meta, data, setParam }: Nod
246
  </div>
247
  ) : meta?.type?.type === MODEL_TRAINING_INPUT_MAPPING ? (
248
  <label className="param">
249
- <ParamName name={name} />
250
  <ModelMapping value={value} data={data} variant="training input" onChange={onChange} />
251
  </label>
252
  ) : meta?.type?.type === MODEL_INFERENCE_INPUT_MAPPING ? (
253
  <label className="param">
254
- <ParamName name={name} />
255
  <ModelMapping value={value} data={data} variant="inference input" onChange={onChange} />
256
  </label>
257
  ) : meta?.type?.type === MODEL_OUTPUT_MAPPING ? (
258
  <label className="param">
259
- <ParamName name={name} />
260
  <ModelMapping value={value} data={data} variant="output" onChange={onChange} />
261
  </label>
262
  ) : (
263
  <label className="param">
264
- <ParamName name={name} />
265
  <Input value={value} onChange={onChange} />
266
  </label>
267
  );
 
1
  import { useRef } from "react";
2
  // @ts-ignore
3
  import ArrowsHorizontal from "~icons/tabler/arrows-horizontal.jsx";
4
+ // @ts-ignore
5
+ import Help from "~icons/tabler/question-mark.jsx";
6
+ import Tooltip from "../../Tooltip";
7
  import NodeGroupParameter from "./NodeGroupParameter";
8
 
9
  const BOOLEAN = "<class 'bool'>";
 
12
  const MODEL_INFERENCE_INPUT_MAPPING =
13
  "<class 'lynxkite_graph_analytics.ml_ops.ModelInferenceInputMapping'>";
14
  const MODEL_OUTPUT_MAPPING = "<class 'lynxkite_graph_analytics.ml_ops.ModelOutputMapping'>";
15
+
16
+ function ParamName({ name, doc }: { name: string; doc: string }) {
17
+ const help = doc && (
18
+ <Tooltip doc={doc} width={200}>
19
+ <Help />
20
+ </Tooltip>
21
+ );
22
+ return (
23
+ <div className="param-name-row">
24
+ <span className="param-name bg-base-200">{name.replace(/_/g, " ")}</span>
25
+ {help}
26
+ </div>
27
+ );
28
  }
29
 
30
  function Input({
 
209
 
210
  export type UpdateOptions = { delay?: number };
211
 
212
+ function findDocs(docs: any, parameter: string) {
213
+ for (const sec of docs) {
214
+ if (sec.kind === "parameters") {
215
+ for (const p of sec.value) {
216
+ if (p.name === parameter) {
217
+ return p.description;
218
+ }
219
+ }
220
+ }
221
+ }
222
+ }
223
+
224
  export default function NodeParameter({ name, value, meta, data, setParam }: NodeParameterProps) {
225
+ const doc = findDocs(data.meta?.value?.doc ?? [], name);
226
  function onChange(value: any, opts?: UpdateOptions) {
227
  setParam(meta.name, value, opts || {});
228
  }
229
  return meta?.type?.format === "collapsed" ? (
230
  <label className="param">
231
+ <ParamName name={name} doc={doc} />
232
  <button className="collapsed-param">⋯</button>
233
  </label>
234
  ) : meta?.type?.format === "textarea" ? (
235
  <label className="param">
236
+ <ParamName name={name} doc={doc} />
237
  <textarea
238
  className="textarea textarea-bordered w-full"
239
  rows={6}
 
246
  <NodeGroupParameter meta={meta} data={data} setParam={setParam} />
247
  ) : meta?.type?.enum ? (
248
  <label className="param">
249
+ <ParamName name={name} doc={doc} />
250
  <select
251
  className="select select-bordered w-full"
252
  value={value || meta.type.enum[0]}
 
273
  </div>
274
  ) : meta?.type?.type === MODEL_TRAINING_INPUT_MAPPING ? (
275
  <label className="param">
276
+ <ParamName name={name} doc={doc} />
277
  <ModelMapping value={value} data={data} variant="training input" onChange={onChange} />
278
  </label>
279
  ) : meta?.type?.type === MODEL_INFERENCE_INPUT_MAPPING ? (
280
  <label className="param">
281
+ <ParamName name={name} doc={doc} />
282
  <ModelMapping value={value} data={data} variant="inference input" onChange={onChange} />
283
  </label>
284
  ) : meta?.type?.type === MODEL_OUTPUT_MAPPING ? (
285
  <label className="param">
286
+ <ParamName name={name} doc={doc} />
287
  <ModelMapping value={value} data={data} variant="output" onChange={onChange} />
288
  </label>
289
  ) : (
290
  <label className="param">
291
+ <ParamName name={name} doc={doc} />
292
  <Input value={value} onChange={onChange} />
293
  </label>
294
  );
lynxkite-core/pyproject.toml CHANGED
@@ -5,6 +5,7 @@ description = "A lightweight dependency for authoring LynxKite operations and ex
5
  readme = "README.md"
6
  requires-python = ">=3.11"
7
  dependencies = [
 
8
  ]
9
 
10
  [project.optional-dependencies]
 
5
  readme = "README.md"
6
  requires-python = ">=3.11"
7
  dependencies = [
8
+ "griffe>=1.7.3",
9
  ]
10
 
11
  [project.optional-dependencies]
lynxkite-core/src/lynxkite/core/ops.py CHANGED
@@ -1,19 +1,22 @@
1
  """API for implementing LynxKite operations."""
2
 
3
  from __future__ import annotations
 
4
  import asyncio
5
  import enum
6
  import functools
 
7
  import importlib
8
  import inspect
9
  import pathlib
10
  import subprocess
11
  import traceback
12
- import joblib
13
  import types
14
- import pydantic
15
  import typing
16
  from dataclasses import dataclass
 
 
 
17
  from typing_extensions import Annotated
18
 
19
  if typing.TYPE_CHECKING:
@@ -180,6 +183,7 @@ class Op(BaseConfig):
180
  # TODO: Make type an enum with the possible values.
181
  type: str = "basic" # The UI to use for this operation.
182
  color: str = "orange" # The color of the operation in the UI.
 
183
 
184
  def __call__(self, *inputs, **params):
185
  # Convert parameters.
@@ -236,6 +240,7 @@ def op(
236
  """Decorator for defining an operation."""
237
 
238
  def decorator(func):
 
239
  sig = inspect.signature(func)
240
  _view = view
241
  if view == "matplotlib":
@@ -262,6 +267,7 @@ def op(
262
  _outputs = [Output(name="output", type=None)] if view == "basic" else []
263
  op = Op(
264
  func=func,
 
265
  name=name,
266
  params=_params,
267
  inputs=inputs,
@@ -279,10 +285,11 @@ def op(
279
 
280
  def matplotlib_to_image(func):
281
  """Decorator for converting a matplotlib figure to an image."""
282
- import matplotlib.pyplot as plt
283
  import base64
284
  import io
285
 
 
 
286
  @functools.wraps(func)
287
  def wrapper(*args, **kwargs):
288
  func(*args, **kwargs)
@@ -423,3 +430,15 @@ def run_user_script(script_path: pathlib.Path):
423
  spec = importlib.util.spec_from_file_location(script_path.stem, str(script_path))
424
  module = importlib.util.module_from_spec(spec)
425
  spec.loader.exec_module(module)
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """API for implementing LynxKite operations."""
2
 
3
  from __future__ import annotations
4
+
5
  import asyncio
6
  import enum
7
  import functools
8
+ import json
9
  import importlib
10
  import inspect
11
  import pathlib
12
  import subprocess
13
  import traceback
 
14
  import types
 
15
  import typing
16
  from dataclasses import dataclass
17
+
18
+ import joblib
19
+ import pydantic
20
  from typing_extensions import Annotated
21
 
22
  if typing.TYPE_CHECKING:
 
183
  # TODO: Make type an enum with the possible values.
184
  type: str = "basic" # The UI to use for this operation.
185
  color: str = "orange" # The color of the operation in the UI.
186
+ doc: object = None
187
 
188
  def __call__(self, *inputs, **params):
189
  # Convert parameters.
 
240
  """Decorator for defining an operation."""
241
 
242
  def decorator(func):
243
+ doc = get_doc(func)
244
  sig = inspect.signature(func)
245
  _view = view
246
  if view == "matplotlib":
 
267
  _outputs = [Output(name="output", type=None)] if view == "basic" else []
268
  op = Op(
269
  func=func,
270
+ doc=doc,
271
  name=name,
272
  params=_params,
273
  inputs=inputs,
 
285
 
286
  def matplotlib_to_image(func):
287
  """Decorator for converting a matplotlib figure to an image."""
 
288
  import base64
289
  import io
290
 
291
+ import matplotlib.pyplot as plt
292
+
293
  @functools.wraps(func)
294
  def wrapper(*args, **kwargs):
295
  func(*args, **kwargs)
 
430
  spec = importlib.util.spec_from_file_location(script_path.stem, str(script_path))
431
  module = importlib.util.module_from_spec(spec)
432
  spec.loader.exec_module(module)
433
+
434
+
435
+ def get_doc(func):
436
+ """Griffe is an optional dependency. When available, we returned the parsed docstring."""
437
+ try:
438
+ import griffe
439
+ except ImportError:
440
+ return func.__doc__
441
+ if func.__doc__ is None:
442
+ return None
443
+ doc = griffe.Docstring(func.__doc__).parse("google")
444
+ return json.loads(json.dumps(doc, cls=griffe.JSONEncoder))
lynxkite-graph-analytics/src/lynxkite_graph_analytics/lynxkite_ops.py CHANGED
@@ -8,7 +8,6 @@ from collections import deque
8
 
9
  from . import core
10
  import grandcypher
11
- import joblib
12
  import matplotlib
13
  import networkx as nx
14
  import pandas as pd
@@ -16,7 +15,6 @@ import polars as pl
16
  import json
17
 
18
 
19
- mem = joblib.Memory(".joblib-cache")
20
  op = ops.op_registration(core.ENV)
21
 
22
 
@@ -82,8 +80,7 @@ def import_parquet(*, filename: str):
82
  return pd.read_parquet(filename)
83
 
84
 
85
- @op("Import CSV")
86
- @mem.cache
87
  def import_csv(*, filename: str, columns: str = "<from file>", separator: str = "<auto>"):
88
  """Imports a CSV file."""
89
  return pd.read_csv(
@@ -93,8 +90,7 @@ def import_csv(*, filename: str, columns: str = "<from file>", separator: str =
93
  )
94
 
95
 
96
- @op("Import GraphML")
97
- @mem.cache
98
  def import_graphml(*, filename: str):
99
  """Imports a GraphML file."""
100
  files = fsspec.open_files(filename, compression="infer")
@@ -105,8 +101,7 @@ def import_graphml(*, filename: str):
105
  raise ValueError(f"No .graphml file found at {filename}")
106
 
107
 
108
- @op("Graph from OSM")
109
- @mem.cache
110
  def import_osm(*, location: str):
111
  import osmnx as ox
112
 
 
8
 
9
  from . import core
10
  import grandcypher
 
11
  import matplotlib
12
  import networkx as nx
13
  import pandas as pd
 
15
  import json
16
 
17
 
 
18
  op = ops.op_registration(core.ENV)
19
 
20
 
 
80
  return pd.read_parquet(filename)
81
 
82
 
83
+ @op("Import CSV", slow=True)
 
84
  def import_csv(*, filename: str, columns: str = "<from file>", separator: str = "<auto>"):
85
  """Imports a CSV file."""
86
  return pd.read_csv(
 
90
  )
91
 
92
 
93
+ @op("Import GraphML", slow=True)
 
94
  def import_graphml(*, filename: str):
95
  """Imports a GraphML file."""
96
  files = fsspec.open_files(filename, compression="infer")
 
101
  raise ValueError(f"No .graphml file found at {filename}")
102
 
103
 
104
+ @op("Graph from OSM", slow=True)
 
105
  def import_osm(*, location: str):
106
  import osmnx as ox
107
 
lynxkite-graph-analytics/src/lynxkite_graph_analytics/pytorch/pytorch_ops.py CHANGED
@@ -8,7 +8,7 @@ from .pytorch_core import op, reg, ENV
8
 
9
  reg("Input: tensor", outputs=["output"], params=[P.basic("name")])
10
  reg("Input: graph edges", outputs=["edges"])
11
- reg("Input: sequential", outputs=["y"])
12
  reg("Output", inputs=["x"], outputs=["x"], params=[P.basic("name")])
13
 
14
 
@@ -19,6 +19,7 @@ def lstm(x, *, input_size=1024, hidden_size=1024, dropout=0.0):
19
 
20
  reg(
21
  "Neural ODE",
 
22
  inputs=["x"],
23
  params=[
24
  P.basic("relative_tolerance"),
@@ -99,6 +100,7 @@ def concatenate(a, b):
99
 
100
  reg(
101
  "Graph conv",
 
102
  inputs=["x", "edges"],
103
  outputs=["x"],
104
  params=[P.options("type", ["GCNConv", "GATConv", "GATv2Conv", "SAGEConv"])],
 
8
 
9
  reg("Input: tensor", outputs=["output"], params=[P.basic("name")])
10
  reg("Input: graph edges", outputs=["edges"])
11
+ reg("Input: sequential", outputs=["y"], params=[P.basic("name")])
12
  reg("Output", inputs=["x"], outputs=["x"], params=[P.basic("name")])
13
 
14
 
 
19
 
20
  reg(
21
  "Neural ODE",
22
+ color="blue",
23
  inputs=["x"],
24
  params=[
25
  P.basic("relative_tolerance"),
 
100
 
101
  reg(
102
  "Graph conv",
103
+ color="blue",
104
  inputs=["x", "edges"],
105
  outputs=["x"],
106
  params=[P.options("type", ["GCNConv", "GATConv", "GATv2Conv", "SAGEConv"])],