Spaces:
Running
Running
Merge branch 'main' into feature/image-search-v0
Browse files- .dockerignore +25 -0
- .gitignore +3 -2
- .pre-commit-config.yaml +2 -2
- Dockerfile +16 -0
- README.md +10 -1
- examples/AIMO +1 -1
- examples/Airlines demo +0 -0
- examples/Bio demo +1 -1
- examples/{BioNemo demo → BioNeMo demo} +0 -0
- examples/Graph RAG +1 -1
- examples/LynxScribe demo +4 -4
- examples/NetworkX demo +0 -0
- examples/{aimo-examples.csv → uploads/aimo-examples.csv} +0 -0
- examples/{drug_target_data_sample.csv → uploads/drug_target_data_sample.csv} +0 -0
- examples/{example-pizza.md → uploads/example-pizza.md} +0 -0
- lynxkite-app/README.md +1 -1
- lynxkite-app/src/lynxkite_app/__main__.py +1 -0
- lynxkite-app/src/lynxkite_app/config.py +0 -8
- lynxkite-app/src/lynxkite_app/crdt.py +16 -9
- lynxkite-app/src/lynxkite_app/main.py +33 -16
- lynxkite-app/tests/test_main.py +7 -8
- lynxkite-app/web/package-lock.json +276 -20
- lynxkite-app/web/package.json +4 -3
- lynxkite-app/web/playwright.config.ts +1 -1
- lynxkite-app/web/src/Directory.tsx +8 -6
- lynxkite-app/web/src/index.css +35 -0
- lynxkite-app/web/src/workspace/EnvironmentSelector.tsx +1 -1
- lynxkite-app/web/src/workspace/NodeSearch.tsx +6 -1
- lynxkite-app/web/src/workspace/Workspace.tsx +99 -29
- lynxkite-app/web/src/workspace/nodes/NodeParameter.tsx +4 -0
- lynxkite-app/web/src/workspace/nodes/NodeWithParams.tsx +37 -24
- lynxkite-app/web/src/workspace/nodes/NodeWithVisualization.tsx +15 -7
- lynxkite-app/web/tests/basic.spec.ts +3 -6
- lynxkite-app/web/tests/errors.spec.ts +11 -11
- lynxkite-app/web/tests/examples.spec.ts +3 -10
- lynxkite-app/web/tests/graph_creation.spec.ts +6 -2
- lynxkite-app/web/tests/lynxkite.ts +10 -13
- lynxkite-bio/src/lynxkite_bio/__init__.py +1 -1
- lynxkite-core/src/lynxkite/core/executors/one_by_one.py +4 -4
- lynxkite-core/src/lynxkite/core/ops.py +21 -6
- lynxkite-core/src/lynxkite/core/workspace.py +3 -1
- lynxkite-graph-analytics/Dockerfile.bionemo +4 -0
- lynxkite-graph-analytics/README.md +1 -2
- lynxkite-graph-analytics/src/lynxkite_graph_analytics/bionemo_ops.py +1 -1
- lynxkite-graph-analytics/src/lynxkite_graph_analytics/core.py +57 -37
- lynxkite-graph-analytics/src/lynxkite_graph_analytics/lynxkite_ops.py +1 -20
- lynxkite-graph-analytics/src/lynxkite_graph_analytics/networkx_ops.py +236 -24
- lynxkite-graph-analytics/tests/test_lynxkite_ops.py +71 -3
- lynxkite-lynxscribe/README.md +1 -1
- lynxkite-lynxscribe/src/lynxkite_lynxscribe/lynxscribe_ops.py +7 -9
.dockerignore
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.venv
|
2 |
+
.vscode/*
|
3 |
+
!.vscode/extensions.json
|
4 |
+
!.vscode/settings.json
|
5 |
+
.idea
|
6 |
+
.DS_Store
|
7 |
+
*.suo
|
8 |
+
*.ntvs*
|
9 |
+
*.njsproj
|
10 |
+
*.sln
|
11 |
+
*.sw?
|
12 |
+
__pycache__
|
13 |
+
node_modules
|
14 |
+
dist
|
15 |
+
build
|
16 |
+
joblib-cache
|
17 |
+
*.egg-info
|
18 |
+
|
19 |
+
lynxkite_crdt_data
|
20 |
+
|
21 |
+
# Playwright
|
22 |
+
test-results
|
23 |
+
playwright-report
|
24 |
+
/blob-report/
|
25 |
+
/playwright/.cache/
|
.gitignore
CHANGED
@@ -1,3 +1,4 @@
|
|
|
|
1 |
.vscode/*
|
2 |
!.vscode/extensions.json
|
3 |
!.vscode/settings.json
|
@@ -18,7 +19,7 @@ joblib-cache
|
|
18 |
lynxkite_crdt_data
|
19 |
|
20 |
# Playwright
|
21 |
-
|
22 |
-
|
23 |
/blob-report/
|
24 |
/playwright/.cache/
|
|
|
1 |
+
.venv
|
2 |
.vscode/*
|
3 |
!.vscode/extensions.json
|
4 |
!.vscode/settings.json
|
|
|
19 |
lynxkite_crdt_data
|
20 |
|
21 |
# Playwright
|
22 |
+
test-results
|
23 |
+
playwright-report
|
24 |
/blob-report/
|
25 |
/playwright/.cache/
|
.pre-commit-config.yaml
CHANGED
@@ -1,12 +1,12 @@
|
|
1 |
repos:
|
2 |
- repo: https://github.com/pre-commit/pre-commit-hooks
|
3 |
-
rev:
|
4 |
hooks:
|
5 |
- id: trailing-whitespace
|
6 |
- id: end-of-file-fixer
|
7 |
- id: check-yaml
|
8 |
- repo: https://github.com/astral-sh/ruff-pre-commit
|
9 |
-
rev: v0.9.
|
10 |
hooks:
|
11 |
- id: ruff
|
12 |
args: [ --fix ]
|
|
|
1 |
repos:
|
2 |
- repo: https://github.com/pre-commit/pre-commit-hooks
|
3 |
+
rev: v5.0.0
|
4 |
hooks:
|
5 |
- id: trailing-whitespace
|
6 |
- id: end-of-file-fixer
|
7 |
- id: check-yaml
|
8 |
- repo: https://github.com/astral-sh/ruff-pre-commit
|
9 |
+
rev: v0.9.10
|
10 |
hooks:
|
11 |
- id: ruff
|
12 |
args: [ --fix ]
|
Dockerfile
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM node:22
|
2 |
+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
3 |
+
RUN apt-get update && apt-get install -y git
|
4 |
+
USER node
|
5 |
+
ENV HOME=/home/node PATH=/home/node/.local/bin:$PATH
|
6 |
+
WORKDIR $HOME/app
|
7 |
+
COPY --chown=node . $HOME/app
|
8 |
+
RUN uv venv && uv pip install \
|
9 |
+
-e lynxkite-core \
|
10 |
+
-e lynxkite-app \
|
11 |
+
-e lynxkite-graph-analytics \
|
12 |
+
-e lynxkite-bio \
|
13 |
+
-e lynxkite-pillow-example
|
14 |
+
WORKDIR $HOME/app/examples
|
15 |
+
ENV PORT=7860
|
16 |
+
CMD ["uv", "run", "lynxkite"]
|
README.md
CHANGED
@@ -1,3 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
# LynxKite 2024
|
2 |
|
3 |
This is an experimental rewrite of [LynxKite](https://github.com/lynxkite/lynxkite). It is not compatible with the
|
@@ -32,7 +41,7 @@ uv pip install -e lynxkite-core/[dev] -e lynxkite-app/[dev] -e lynxkite-graph-an
|
|
32 |
This also builds the frontend, hopefully very quickly. To run it:
|
33 |
|
34 |
```bash
|
35 |
-
|
36 |
```
|
37 |
|
38 |
If you also want to make changes to the frontend with hot reloading:
|
|
|
1 |
+
---
|
2 |
+
title: LynxKite 2000:MM
|
3 |
+
emoji: 🪁
|
4 |
+
colorFrom: purple
|
5 |
+
colorTo: gray
|
6 |
+
sdk: docker
|
7 |
+
app_port: 7860
|
8 |
+
---
|
9 |
+
|
10 |
# LynxKite 2024
|
11 |
|
12 |
This is an experimental rewrite of [LynxKite](https://github.com/lynxkite/lynxkite). It is not compatible with the
|
|
|
41 |
This also builds the frontend, hopefully very quickly. To run it:
|
42 |
|
43 |
```bash
|
44 |
+
cd examples && LYNXKITE_RELOAD=1 lynxkite
|
45 |
```
|
46 |
|
47 |
If you also want to make changes to the frontend with hot reloading:
|
examples/AIMO
CHANGED
@@ -786,7 +786,7 @@
|
|
786 |
"data": {
|
787 |
"title": "Input CSV",
|
788 |
"params": {
|
789 |
-
"filename": "
|
790 |
"key": "problem"
|
791 |
},
|
792 |
"display": null,
|
|
|
786 |
"data": {
|
787 |
"title": "Input CSV",
|
788 |
"params": {
|
789 |
+
"filename": "uploads/aimo-examples.csv",
|
790 |
"key": "problem"
|
791 |
},
|
792 |
"display": null,
|
examples/Airlines demo
CHANGED
The diff for this file is too large to render.
See raw diff
|
|
examples/Bio demo
CHANGED
@@ -7,7 +7,7 @@
|
|
7 |
"data": {
|
8 |
"title": "Import CSV",
|
9 |
"params": {
|
10 |
-
"filename": "
|
11 |
"separator": "<auto>",
|
12 |
"columns": "<from file>"
|
13 |
},
|
|
|
7 |
"data": {
|
8 |
"title": "Import CSV",
|
9 |
"params": {
|
10 |
+
"filename": "uploads/drug_target_data_sample.csv",
|
11 |
"separator": "<auto>",
|
12 |
"columns": "<from file>"
|
13 |
},
|
examples/{BioNemo demo → BioNeMo demo}
RENAMED
File without changes
|
examples/Graph RAG
CHANGED
@@ -7,7 +7,7 @@
|
|
7 |
"data": {
|
8 |
"title": "Input document",
|
9 |
"params": {
|
10 |
-
"filename": "
|
11 |
},
|
12 |
"display": null,
|
13 |
"error": null,
|
|
|
7 |
"data": {
|
8 |
"title": "Input document",
|
9 |
"params": {
|
10 |
+
"filename": "uploads/example-pizza.md"
|
11 |
},
|
12 |
"display": null,
|
13 |
"error": null,
|
examples/LynxScribe demo
CHANGED
@@ -138,7 +138,7 @@
|
|
138 |
"data": {
|
139 |
"title": "Scenario selector",
|
140 |
"params": {
|
141 |
-
"scenario_file": "/
|
142 |
"node_types": "intent_cluster"
|
143 |
},
|
144 |
"display": null,
|
@@ -252,9 +252,9 @@
|
|
252 |
"data": {
|
253 |
"title": "Knowledge base",
|
254 |
"params": {
|
255 |
-
"template_cluster_path": "/
|
256 |
-
"edges_path": "/
|
257 |
-
"nodes_path": "/
|
258 |
},
|
259 |
"display": null,
|
260 |
"error": null,
|
|
|
138 |
"data": {
|
139 |
"title": "Scenario selector",
|
140 |
"params": {
|
141 |
+
"scenario_file": "uploads/chat_api/scenarios.yaml",
|
142 |
"node_types": "intent_cluster"
|
143 |
},
|
144 |
"display": null,
|
|
|
252 |
"data": {
|
253 |
"title": "Knowledge base",
|
254 |
"params": {
|
255 |
+
"template_cluster_path": "uploads/chat_api/data/lynx/tempclusters.pickle",
|
256 |
+
"edges_path": "uploads/chat_api/data/lynx/edges.pickle",
|
257 |
+
"nodes_path": "uploads/chat_api/data/lynx/nodes.pickle"
|
258 |
},
|
259 |
"display": null,
|
260 |
"error": null,
|
examples/NetworkX demo
CHANGED
The diff for this file is too large to render.
See raw diff
|
|
examples/{aimo-examples.csv → uploads/aimo-examples.csv}
RENAMED
File without changes
|
examples/{drug_target_data_sample.csv → uploads/drug_target_data_sample.csv}
RENAMED
File without changes
|
examples/{example-pizza.md → uploads/example-pizza.md}
RENAMED
File without changes
|
lynxkite-app/README.md
CHANGED
@@ -13,7 +13,7 @@ To run the backend:
|
|
13 |
|
14 |
```bash
|
15 |
uv pip install -e .
|
16 |
-
|
17 |
```
|
18 |
|
19 |
To run the frontend:
|
|
|
13 |
|
14 |
```bash
|
15 |
uv pip install -e .
|
16 |
+
cd ../examples && LYNXKITE_RELOAD=1 lynxkite
|
17 |
```
|
18 |
|
19 |
To run the frontend:
|
lynxkite-app/src/lynxkite_app/__main__.py
CHANGED
@@ -12,6 +12,7 @@ def main():
|
|
12 |
port=port,
|
13 |
reload=reload,
|
14 |
loop="asyncio",
|
|
|
15 |
)
|
16 |
|
17 |
|
|
|
12 |
port=port,
|
13 |
reload=reload,
|
14 |
loop="asyncio",
|
15 |
+
proxy_headers=True,
|
16 |
)
|
17 |
|
18 |
|
lynxkite-app/src/lynxkite_app/config.py
DELETED
@@ -1,8 +0,0 @@
|
|
1 |
-
"""Some common configuration."""
|
2 |
-
|
3 |
-
import os
|
4 |
-
import pathlib
|
5 |
-
|
6 |
-
|
7 |
-
DATA_PATH = pathlib.Path(os.environ.get("LYNXKITE_DATA", "lynxkite_data"))
|
8 |
-
CRDT_PATH = pathlib.Path(os.environ.get("LYNXKITE_CRDT_DATA", "lynxkite_crdt_data"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-app/src/lynxkite_app/crdt.py
CHANGED
@@ -3,6 +3,7 @@
|
|
3 |
import asyncio
|
4 |
import contextlib
|
5 |
import enum
|
|
|
6 |
import fastapi
|
7 |
import os.path
|
8 |
import pycrdt
|
@@ -11,7 +12,6 @@ import pycrdt_websocket.ystore
|
|
11 |
import uvicorn
|
12 |
import builtins
|
13 |
from lynxkite.core import workspace, ops
|
14 |
-
from . import config
|
15 |
|
16 |
router = fastapi.APIRouter()
|
17 |
|
@@ -32,8 +32,9 @@ class WebsocketServer(pycrdt_websocket.WebsocketServer):
|
|
32 |
|
33 |
The workspace is loaded from "crdt_data" if it exists there, or from "data", or a new workspace is created.
|
34 |
"""
|
35 |
-
|
36 |
-
|
|
|
37 |
ystore = pycrdt_websocket.ystore.FileYStore(path)
|
38 |
ydoc = pycrdt.Doc()
|
39 |
ydoc["workspace"] = ws = pycrdt.Map()
|
@@ -54,6 +55,10 @@ class WebsocketServer(pycrdt_websocket.WebsocketServer):
|
|
54 |
# We have two possible sources of truth for the workspaces, the YStore and the JSON files.
|
55 |
# In case we didn't find the workspace in the YStore, we try to load it from the JSON files.
|
56 |
try_to_load_workspace(ws, name)
|
|
|
|
|
|
|
|
|
57 |
room = pycrdt_websocket.YRoom(
|
58 |
ystore=ystore, ydoc=ydoc, exception_handler=ws_exception_handler
|
59 |
)
|
@@ -161,9 +166,8 @@ def try_to_load_workspace(ws: pycrdt.Map, name: str):
|
|
161 |
ws: CRDT object to udpate with the workspace contents.
|
162 |
name: Name of the workspace to load.
|
163 |
"""
|
164 |
-
|
165 |
-
|
166 |
-
ws_pyd = workspace.load(json_path)
|
167 |
# We treat the display field as a black box, since it is a large
|
168 |
# dictionary that is meant to change as a whole.
|
169 |
crdt_update(ws, ws_pyd.model_dump(), non_collaborative_fields={"display"})
|
@@ -197,7 +201,6 @@ async def workspace_changed(name: str, changes: pycrdt.MapEvent, ws_crdt: pycrdt
|
|
197 |
getattr(change, "keys", {}).get("__execution_delay", {}).get("newValue", 0)
|
198 |
for change in changes
|
199 |
)
|
200 |
-
print(f"Running {name} in {ws_pyd.env}...")
|
201 |
if delay:
|
202 |
task = asyncio.create_task(execute(name, ws_crdt, ws_pyd, delay))
|
203 |
delayed_executions[name] = task
|
@@ -221,10 +224,13 @@ async def execute(
|
|
221 |
await asyncio.sleep(delay)
|
222 |
except asyncio.CancelledError:
|
223 |
return
|
224 |
-
|
225 |
-
|
|
|
|
|
226 |
# Save user changes before executing, in case the execution fails.
|
227 |
workspace.save(ws_pyd, path)
|
|
|
228 |
with ws_crdt.doc.transaction():
|
229 |
for nc, np in zip(ws_crdt["nodes"], ws_pyd.nodes):
|
230 |
if "data" not in nc:
|
@@ -234,6 +240,7 @@ async def execute(
|
|
234 |
np._crdt = nc
|
235 |
await workspace.execute(ws_pyd)
|
236 |
workspace.save(ws_pyd, path)
|
|
|
237 |
|
238 |
|
239 |
@contextlib.asynccontextmanager
|
|
|
3 |
import asyncio
|
4 |
import contextlib
|
5 |
import enum
|
6 |
+
import pathlib
|
7 |
import fastapi
|
8 |
import os.path
|
9 |
import pycrdt
|
|
|
12 |
import uvicorn
|
13 |
import builtins
|
14 |
from lynxkite.core import workspace, ops
|
|
|
15 |
|
16 |
router = fastapi.APIRouter()
|
17 |
|
|
|
32 |
|
33 |
The workspace is loaded from "crdt_data" if it exists there, or from "data", or a new workspace is created.
|
34 |
"""
|
35 |
+
crdt_path = pathlib.Path(".crdt")
|
36 |
+
path = crdt_path / f"{name}.crdt"
|
37 |
+
assert path.is_relative_to(crdt_path)
|
38 |
ystore = pycrdt_websocket.ystore.FileYStore(path)
|
39 |
ydoc = pycrdt.Doc()
|
40 |
ydoc["workspace"] = ws = pycrdt.Map()
|
|
|
55 |
# We have two possible sources of truth for the workspaces, the YStore and the JSON files.
|
56 |
# In case we didn't find the workspace in the YStore, we try to load it from the JSON files.
|
57 |
try_to_load_workspace(ws, name)
|
58 |
+
ws_simple = workspace.Workspace.model_validate(ws.to_py())
|
59 |
+
clean_input(ws_simple)
|
60 |
+
# Set the last known version to the current state, so we don't trigger a change event.
|
61 |
+
last_known_versions[name] = ws_simple
|
62 |
room = pycrdt_websocket.YRoom(
|
63 |
ystore=ystore, ydoc=ydoc, exception_handler=ws_exception_handler
|
64 |
)
|
|
|
166 |
ws: CRDT object to udpate with the workspace contents.
|
167 |
name: Name of the workspace to load.
|
168 |
"""
|
169 |
+
if os.path.exists(name):
|
170 |
+
ws_pyd = workspace.load(name)
|
|
|
171 |
# We treat the display field as a black box, since it is a large
|
172 |
# dictionary that is meant to change as a whole.
|
173 |
crdt_update(ws, ws_pyd.model_dump(), non_collaborative_fields={"display"})
|
|
|
201 |
getattr(change, "keys", {}).get("__execution_delay", {}).get("newValue", 0)
|
202 |
for change in changes
|
203 |
)
|
|
|
204 |
if delay:
|
205 |
task = asyncio.create_task(execute(name, ws_crdt, ws_pyd, delay))
|
206 |
delayed_executions[name] = task
|
|
|
224 |
await asyncio.sleep(delay)
|
225 |
except asyncio.CancelledError:
|
226 |
return
|
227 |
+
print(f"Running {name} in {ws_pyd.env}...")
|
228 |
+
cwd = pathlib.Path()
|
229 |
+
path = cwd / name
|
230 |
+
assert path.is_relative_to(cwd), "Provided workspace path is invalid"
|
231 |
# Save user changes before executing, in case the execution fails.
|
232 |
workspace.save(ws_pyd, path)
|
233 |
+
ws_pyd._crdt = ws_crdt
|
234 |
with ws_crdt.doc.transaction():
|
235 |
for nc, np in zip(ws_crdt["nodes"], ws_pyd.nodes):
|
236 |
if "data" not in nc:
|
|
|
240 |
np._crdt = nc
|
241 |
await workspace.execute(ws_pyd)
|
242 |
workspace.save(ws_pyd, path)
|
243 |
+
print(f"Finished running {name} in {ws_pyd.env}.")
|
244 |
|
245 |
|
246 |
@contextlib.asynccontextmanager
|
lynxkite-app/src/lynxkite_app/main.py
CHANGED
@@ -7,10 +7,11 @@ import importlib
|
|
7 |
import pathlib
|
8 |
import pkgutil
|
9 |
from fastapi.staticfiles import StaticFiles
|
|
|
10 |
import starlette
|
11 |
from lynxkite.core import ops
|
12 |
from lynxkite.core import workspace
|
13 |
-
from . import crdt
|
14 |
|
15 |
|
16 |
def detect_plugins():
|
@@ -28,6 +29,7 @@ lynxkite_plugins = detect_plugins()
|
|
28 |
|
29 |
app = fastapi.FastAPI(lifespan=crdt.lifespan)
|
30 |
app.include_router(crdt.router)
|
|
|
31 |
|
32 |
|
33 |
@app.get("/api/catalog")
|
@@ -43,9 +45,12 @@ class SaveRequest(workspace.BaseConfig):
|
|
43 |
ws: workspace.Workspace
|
44 |
|
45 |
|
|
|
|
|
|
|
46 |
def save(req: SaveRequest):
|
47 |
-
path =
|
48 |
-
assert path.is_relative_to(
|
49 |
workspace.save(req.ws, path)
|
50 |
|
51 |
|
@@ -59,18 +64,17 @@ async def save_and_execute(req: SaveRequest):
|
|
59 |
|
60 |
@app.post("/api/delete")
|
61 |
async def delete_workspace(req: dict):
|
62 |
-
json_path: pathlib.Path =
|
63 |
-
crdt_path: pathlib.Path =
|
64 |
-
assert json_path.is_relative_to(
|
65 |
-
assert crdt_path.is_relative_to(config.CRDT_PATH)
|
66 |
json_path.unlink()
|
67 |
crdt_path.unlink()
|
68 |
|
69 |
|
70 |
@app.get("/api/load")
|
71 |
def load(path: str):
|
72 |
-
path =
|
73 |
-
assert path.is_relative_to(
|
74 |
if not path.exists():
|
75 |
return workspace.Workspace()
|
76 |
return workspace.load(path)
|
@@ -83,15 +87,16 @@ class DirectoryEntry(pydantic.BaseModel):
|
|
83 |
|
84 |
@app.get("/api/dir/list")
|
85 |
def list_dir(path: str):
|
86 |
-
path =
|
87 |
-
assert path.is_relative_to(
|
88 |
return sorted(
|
89 |
[
|
90 |
DirectoryEntry(
|
91 |
-
name=str(p.relative_to(
|
92 |
type="directory" if p.is_dir() else "workspace",
|
93 |
)
|
94 |
for p in path.iterdir()
|
|
|
95 |
],
|
96 |
key=lambda x: x.name,
|
97 |
)
|
@@ -99,16 +104,16 @@ def list_dir(path: str):
|
|
99 |
|
100 |
@app.post("/api/dir/mkdir")
|
101 |
def make_dir(req: dict):
|
102 |
-
path =
|
103 |
-
assert path.is_relative_to(
|
104 |
assert not path.exists(), f"{path} already exists"
|
105 |
path.mkdir()
|
106 |
|
107 |
|
108 |
@app.post("/api/dir/delete")
|
109 |
def delete_dir(req: dict):
|
110 |
-
path: pathlib.Path =
|
111 |
-
assert all([path.is_relative_to(
|
112 |
shutil.rmtree(path)
|
113 |
|
114 |
|
@@ -126,6 +131,18 @@ async def service_post(req: fastapi.Request, module_path: str):
|
|
126 |
return await module.api_service_post(req)
|
127 |
|
128 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
129 |
class SPAStaticFiles(StaticFiles):
|
130 |
"""Route everything to index.html. https://stackoverflow.com/a/73552966/3318517"""
|
131 |
|
|
|
7 |
import pathlib
|
8 |
import pkgutil
|
9 |
from fastapi.staticfiles import StaticFiles
|
10 |
+
from fastapi.middleware.gzip import GZipMiddleware
|
11 |
import starlette
|
12 |
from lynxkite.core import ops
|
13 |
from lynxkite.core import workspace
|
14 |
+
from . import crdt
|
15 |
|
16 |
|
17 |
def detect_plugins():
|
|
|
29 |
|
30 |
app = fastapi.FastAPI(lifespan=crdt.lifespan)
|
31 |
app.include_router(crdt.router)
|
32 |
+
app.add_middleware(GZipMiddleware)
|
33 |
|
34 |
|
35 |
@app.get("/api/catalog")
|
|
|
45 |
ws: workspace.Workspace
|
46 |
|
47 |
|
48 |
+
data_path = pathlib.Path()
|
49 |
+
|
50 |
+
|
51 |
def save(req: SaveRequest):
|
52 |
+
path = data_path / req.path
|
53 |
+
assert path.is_relative_to(data_path)
|
54 |
workspace.save(req.ws, path)
|
55 |
|
56 |
|
|
|
64 |
|
65 |
@app.post("/api/delete")
|
66 |
async def delete_workspace(req: dict):
|
67 |
+
json_path: pathlib.Path = data_path / req["path"]
|
68 |
+
crdt_path: pathlib.Path = data_path / ".crdt" / f"{req['path']}.crdt"
|
69 |
+
assert json_path.is_relative_to(data_path)
|
|
|
70 |
json_path.unlink()
|
71 |
crdt_path.unlink()
|
72 |
|
73 |
|
74 |
@app.get("/api/load")
|
75 |
def load(path: str):
|
76 |
+
path = data_path / path
|
77 |
+
assert path.is_relative_to(data_path)
|
78 |
if not path.exists():
|
79 |
return workspace.Workspace()
|
80 |
return workspace.load(path)
|
|
|
87 |
|
88 |
@app.get("/api/dir/list")
|
89 |
def list_dir(path: str):
|
90 |
+
path = data_path / path
|
91 |
+
assert path.is_relative_to(data_path)
|
92 |
return sorted(
|
93 |
[
|
94 |
DirectoryEntry(
|
95 |
+
name=str(p.relative_to(data_path)),
|
96 |
type="directory" if p.is_dir() else "workspace",
|
97 |
)
|
98 |
for p in path.iterdir()
|
99 |
+
if not p.name.startswith(".")
|
100 |
],
|
101 |
key=lambda x: x.name,
|
102 |
)
|
|
|
104 |
|
105 |
@app.post("/api/dir/mkdir")
|
106 |
def make_dir(req: dict):
|
107 |
+
path = data_path / req["path"]
|
108 |
+
assert path.is_relative_to(data_path)
|
109 |
assert not path.exists(), f"{path} already exists"
|
110 |
path.mkdir()
|
111 |
|
112 |
|
113 |
@app.post("/api/dir/delete")
|
114 |
def delete_dir(req: dict):
|
115 |
+
path: pathlib.Path = data_path / req["path"]
|
116 |
+
assert all([path.is_relative_to(data_path), path.exists(), path.is_dir()])
|
117 |
shutil.rmtree(path)
|
118 |
|
119 |
|
|
|
131 |
return await module.api_service_post(req)
|
132 |
|
133 |
|
134 |
+
@app.post("/api/upload")
|
135 |
+
async def upload(req: fastapi.Request):
|
136 |
+
"""Receives file uploads and stores them in DATA_PATH."""
|
137 |
+
form = await req.form()
|
138 |
+
for file in form.values():
|
139 |
+
file_path = data_path / "uploads" / file.filename
|
140 |
+
assert file_path.is_relative_to(data_path), "Invalid file path"
|
141 |
+
with file_path.open("wb") as buffer:
|
142 |
+
shutil.copyfileobj(file.file, buffer)
|
143 |
+
return {"status": "ok"}
|
144 |
+
|
145 |
+
|
146 |
class SPAStaticFiles(StaticFiles):
|
147 |
"""Route everything to index.html. https://stackoverflow.com/a/73552966/3318517"""
|
148 |
|
lynxkite-app/tests/test_main.py
CHANGED
@@ -1,7 +1,7 @@
|
|
|
|
1 |
import uuid
|
2 |
from fastapi.testclient import TestClient
|
3 |
from lynxkite_app.main import app, detect_plugins
|
4 |
-
from lynxkite_app.config import DATA_PATH
|
5 |
import os
|
6 |
|
7 |
|
@@ -56,10 +56,9 @@ def test_save_and_load():
|
|
56 |
|
57 |
|
58 |
def test_list_dir():
|
59 |
-
test_dir = str(uuid.uuid4())
|
60 |
-
|
61 |
-
|
62 |
-
test_file = test_dir_full_path / "test_file.txt"
|
63 |
test_file.touch()
|
64 |
response = client.get(f"/api/dir/list?path={str(test_dir)}")
|
65 |
assert response.status_code == 200
|
@@ -67,12 +66,12 @@ def test_list_dir():
|
|
67 |
assert response.json()[0]["name"] == f"{test_dir}/test_file.txt"
|
68 |
assert response.json()[0]["type"] == "workspace"
|
69 |
test_file.unlink()
|
70 |
-
|
71 |
|
72 |
|
73 |
def test_make_dir():
|
74 |
dir_name = str(uuid.uuid4())
|
75 |
response = client.post("/api/dir/mkdir", json={"path": dir_name})
|
76 |
assert response.status_code == 200
|
77 |
-
assert os.path.exists(
|
78 |
-
os.rmdir(
|
|
|
1 |
+
import pathlib
|
2 |
import uuid
|
3 |
from fastapi.testclient import TestClient
|
4 |
from lynxkite_app.main import app, detect_plugins
|
|
|
5 |
import os
|
6 |
|
7 |
|
|
|
56 |
|
57 |
|
58 |
def test_list_dir():
|
59 |
+
test_dir = pathlib.Path() / str(uuid.uuid4())
|
60 |
+
test_dir.mkdir(parents=True, exist_ok=True)
|
61 |
+
test_file = test_dir / "test_file.txt"
|
|
|
62 |
test_file.touch()
|
63 |
response = client.get(f"/api/dir/list?path={str(test_dir)}")
|
64 |
assert response.status_code == 200
|
|
|
66 |
assert response.json()[0]["name"] == f"{test_dir}/test_file.txt"
|
67 |
assert response.json()[0]["type"] == "workspace"
|
68 |
test_file.unlink()
|
69 |
+
test_dir.rmdir()
|
70 |
|
71 |
|
72 |
def test_make_dir():
|
73 |
dir_name = str(uuid.uuid4())
|
74 |
response = client.post("/api/dir/mkdir", json={"path": dir_name})
|
75 |
assert response.status_code == 200
|
76 |
+
assert os.path.exists(dir_name)
|
77 |
+
os.rmdir(dir_name)
|
lynxkite-app/web/package-lock.json
CHANGED
@@ -17,6 +17,7 @@
|
|
17 |
"@syncedstore/react": "^0.6.0",
|
18 |
"@types/node": "^22.10.1",
|
19 |
"@xyflow/react": "^12.3.5",
|
|
|
20 |
"daisyui": "^4.12.20",
|
21 |
"echarts": "^5.5.1",
|
22 |
"fuse.js": "^7.0.0",
|
@@ -278,25 +279,25 @@
|
|
278 |
}
|
279 |
},
|
280 |
"node_modules/@babel/helpers": {
|
281 |
-
"version": "7.26.
|
282 |
-
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.
|
283 |
-
"integrity": "sha512-
|
284 |
"license": "MIT",
|
285 |
"dependencies": {
|
286 |
-
"@babel/template": "^7.
|
287 |
-
"@babel/types": "^7.26.
|
288 |
},
|
289 |
"engines": {
|
290 |
"node": ">=6.9.0"
|
291 |
}
|
292 |
},
|
293 |
"node_modules/@babel/parser": {
|
294 |
-
"version": "7.26.
|
295 |
-
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.
|
296 |
-
"integrity": "sha512-
|
297 |
"license": "MIT",
|
298 |
"dependencies": {
|
299 |
-
"@babel/types": "^7.26.
|
300 |
},
|
301 |
"bin": {
|
302 |
"parser": "bin/babel-parser.js"
|
@@ -306,14 +307,14 @@
|
|
306 |
}
|
307 |
},
|
308 |
"node_modules/@babel/template": {
|
309 |
-
"version": "7.
|
310 |
-
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.
|
311 |
-
"integrity": "sha512-
|
312 |
"license": "MIT",
|
313 |
"dependencies": {
|
314 |
-
"@babel/code-frame": "^7.
|
315 |
-
"@babel/parser": "^7.
|
316 |
-
"@babel/types": "^7.
|
317 |
},
|
318 |
"engines": {
|
319 |
"node": ">=6.9.0"
|
@@ -347,9 +348,9 @@
|
|
347 |
}
|
348 |
},
|
349 |
"node_modules/@babel/types": {
|
350 |
-
"version": "7.26.
|
351 |
-
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.
|
352 |
-
"integrity": "sha512-
|
353 |
"license": "MIT",
|
354 |
"dependencies": {
|
355 |
"@babel/helper-string-parser": "^7.25.9",
|
@@ -2467,6 +2468,12 @@
|
|
2467 |
"license": "MIT",
|
2468 |
"optional": true
|
2469 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
2470 |
"node_modules/autoprefixer": {
|
2471 |
"version": "10.4.20",
|
2472 |
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
|
@@ -2505,6 +2512,17 @@
|
|
2505 |
"postcss": "^8.1.0"
|
2506 |
}
|
2507 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2508 |
"node_modules/bail": {
|
2509 |
"version": "2.0.2",
|
2510 |
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
|
@@ -2637,6 +2655,19 @@
|
|
2637 |
"ieee754": "^1.1.13"
|
2638 |
}
|
2639 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2640 |
"node_modules/callsites": {
|
2641 |
"version": "3.1.0",
|
2642 |
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
@@ -2824,6 +2855,18 @@
|
|
2824 |
"dev": true,
|
2825 |
"license": "MIT"
|
2826 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2827 |
"node_modules/comma-separated-tokens": {
|
2828 |
"version": "2.0.3",
|
2829 |
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
@@ -3126,6 +3169,15 @@
|
|
3126 |
"node": ">=6"
|
3127 |
}
|
3128 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3129 |
"node_modules/dequal": {
|
3130 |
"version": "2.0.3",
|
3131 |
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
@@ -3172,6 +3224,20 @@
|
|
3172 |
"tslib": "^2.0.3"
|
3173 |
}
|
3174 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3175 |
"node_modules/eastasianwidth": {
|
3176 |
"version": "0.2.0",
|
3177 |
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
@@ -3252,6 +3318,51 @@
|
|
3252 |
"is-arrayish": "^0.2.1"
|
3253 |
}
|
3254 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3255 |
"node_modules/esbuild": {
|
3256 |
"version": "0.25.0",
|
3257 |
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
|
@@ -3639,6 +3750,26 @@
|
|
3639 |
"dev": true,
|
3640 |
"license": "ISC"
|
3641 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3642 |
"node_modules/foreground-child": {
|
3643 |
"version": "3.3.0",
|
3644 |
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
|
@@ -3656,6 +3787,21 @@
|
|
3656 |
"url": "https://github.com/sponsors/isaacs"
|
3657 |
}
|
3658 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3659 |
"node_modules/fraction.js": {
|
3660 |
"version": "4.3.7",
|
3661 |
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
@@ -3689,7 +3835,6 @@
|
|
3689 |
"version": "1.1.2",
|
3690 |
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
3691 |
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
3692 |
-
"dev": true,
|
3693 |
"license": "MIT",
|
3694 |
"funding": {
|
3695 |
"url": "https://github.com/sponsors/ljharb"
|
@@ -3713,6 +3858,43 @@
|
|
3713 |
"node": ">=6.9.0"
|
3714 |
}
|
3715 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3716 |
"node_modules/glob": {
|
3717 |
"version": "10.4.5",
|
3718 |
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
@@ -3785,6 +3967,18 @@
|
|
3785 |
"url": "https://github.com/sponsors/sindresorhus"
|
3786 |
}
|
3787 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3788 |
"node_modules/graphemer": {
|
3789 |
"version": "1.4.0",
|
3790 |
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
@@ -3802,11 +3996,37 @@
|
|
3802 |
"node": ">=8"
|
3803 |
}
|
3804 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3805 |
"node_modules/hasown": {
|
3806 |
"version": "2.0.2",
|
3807 |
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
3808 |
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
3809 |
-
"dev": true,
|
3810 |
"license": "MIT",
|
3811 |
"dependencies": {
|
3812 |
"function-bind": "^1.1.2"
|
@@ -4523,6 +4743,15 @@
|
|
4523 |
"license": "MIT",
|
4524 |
"optional": true
|
4525 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4526 |
"node_modules/mdast-util-from-markdown": {
|
4527 |
"version": "2.0.2",
|
4528 |
"resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz",
|
@@ -5142,6 +5371,27 @@
|
|
5142 |
"node": ">=8.6"
|
5143 |
}
|
5144 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5145 |
"node_modules/minimatch": {
|
5146 |
"version": "3.1.2",
|
5147 |
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
@@ -5766,6 +6016,12 @@
|
|
5766 |
"url": "https://github.com/sponsors/wooorm"
|
5767 |
}
|
5768 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
5769 |
"node_modules/prr": {
|
5770 |
"version": "1.0.1",
|
5771 |
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
|
|
|
17 |
"@syncedstore/react": "^0.6.0",
|
18 |
"@types/node": "^22.10.1",
|
19 |
"@xyflow/react": "^12.3.5",
|
20 |
+
"axios": "^1.8.2",
|
21 |
"daisyui": "^4.12.20",
|
22 |
"echarts": "^5.5.1",
|
23 |
"fuse.js": "^7.0.0",
|
|
|
279 |
}
|
280 |
},
|
281 |
"node_modules/@babel/helpers": {
|
282 |
+
"version": "7.26.10",
|
283 |
+
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz",
|
284 |
+
"integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==",
|
285 |
"license": "MIT",
|
286 |
"dependencies": {
|
287 |
+
"@babel/template": "^7.26.9",
|
288 |
+
"@babel/types": "^7.26.10"
|
289 |
},
|
290 |
"engines": {
|
291 |
"node": ">=6.9.0"
|
292 |
}
|
293 |
},
|
294 |
"node_modules/@babel/parser": {
|
295 |
+
"version": "7.26.10",
|
296 |
+
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz",
|
297 |
+
"integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==",
|
298 |
"license": "MIT",
|
299 |
"dependencies": {
|
300 |
+
"@babel/types": "^7.26.10"
|
301 |
},
|
302 |
"bin": {
|
303 |
"parser": "bin/babel-parser.js"
|
|
|
307 |
}
|
308 |
},
|
309 |
"node_modules/@babel/template": {
|
310 |
+
"version": "7.26.9",
|
311 |
+
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz",
|
312 |
+
"integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==",
|
313 |
"license": "MIT",
|
314 |
"dependencies": {
|
315 |
+
"@babel/code-frame": "^7.26.2",
|
316 |
+
"@babel/parser": "^7.26.9",
|
317 |
+
"@babel/types": "^7.26.9"
|
318 |
},
|
319 |
"engines": {
|
320 |
"node": ">=6.9.0"
|
|
|
348 |
}
|
349 |
},
|
350 |
"node_modules/@babel/types": {
|
351 |
+
"version": "7.26.10",
|
352 |
+
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz",
|
353 |
+
"integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==",
|
354 |
"license": "MIT",
|
355 |
"dependencies": {
|
356 |
"@babel/helper-string-parser": "^7.25.9",
|
|
|
2468 |
"license": "MIT",
|
2469 |
"optional": true
|
2470 |
},
|
2471 |
+
"node_modules/asynckit": {
|
2472 |
+
"version": "0.4.0",
|
2473 |
+
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
2474 |
+
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
2475 |
+
"license": "MIT"
|
2476 |
+
},
|
2477 |
"node_modules/autoprefixer": {
|
2478 |
"version": "10.4.20",
|
2479 |
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
|
|
|
2512 |
"postcss": "^8.1.0"
|
2513 |
}
|
2514 |
},
|
2515 |
+
"node_modules/axios": {
|
2516 |
+
"version": "1.8.2",
|
2517 |
+
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz",
|
2518 |
+
"integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
|
2519 |
+
"license": "MIT",
|
2520 |
+
"dependencies": {
|
2521 |
+
"follow-redirects": "^1.15.6",
|
2522 |
+
"form-data": "^4.0.0",
|
2523 |
+
"proxy-from-env": "^1.1.0"
|
2524 |
+
}
|
2525 |
+
},
|
2526 |
"node_modules/bail": {
|
2527 |
"version": "2.0.2",
|
2528 |
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
|
|
|
2655 |
"ieee754": "^1.1.13"
|
2656 |
}
|
2657 |
},
|
2658 |
+
"node_modules/call-bind-apply-helpers": {
|
2659 |
+
"version": "1.0.2",
|
2660 |
+
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
2661 |
+
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
2662 |
+
"license": "MIT",
|
2663 |
+
"dependencies": {
|
2664 |
+
"es-errors": "^1.3.0",
|
2665 |
+
"function-bind": "^1.1.2"
|
2666 |
+
},
|
2667 |
+
"engines": {
|
2668 |
+
"node": ">= 0.4"
|
2669 |
+
}
|
2670 |
+
},
|
2671 |
"node_modules/callsites": {
|
2672 |
"version": "3.1.0",
|
2673 |
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
|
|
2855 |
"dev": true,
|
2856 |
"license": "MIT"
|
2857 |
},
|
2858 |
+
"node_modules/combined-stream": {
|
2859 |
+
"version": "1.0.8",
|
2860 |
+
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
2861 |
+
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
2862 |
+
"license": "MIT",
|
2863 |
+
"dependencies": {
|
2864 |
+
"delayed-stream": "~1.0.0"
|
2865 |
+
},
|
2866 |
+
"engines": {
|
2867 |
+
"node": ">= 0.8"
|
2868 |
+
}
|
2869 |
+
},
|
2870 |
"node_modules/comma-separated-tokens": {
|
2871 |
"version": "2.0.3",
|
2872 |
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
|
|
3169 |
"node": ">=6"
|
3170 |
}
|
3171 |
},
|
3172 |
+
"node_modules/delayed-stream": {
|
3173 |
+
"version": "1.0.0",
|
3174 |
+
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
3175 |
+
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
3176 |
+
"license": "MIT",
|
3177 |
+
"engines": {
|
3178 |
+
"node": ">=0.4.0"
|
3179 |
+
}
|
3180 |
+
},
|
3181 |
"node_modules/dequal": {
|
3182 |
"version": "2.0.3",
|
3183 |
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
|
|
3224 |
"tslib": "^2.0.3"
|
3225 |
}
|
3226 |
},
|
3227 |
+
"node_modules/dunder-proto": {
|
3228 |
+
"version": "1.0.1",
|
3229 |
+
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
3230 |
+
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
3231 |
+
"license": "MIT",
|
3232 |
+
"dependencies": {
|
3233 |
+
"call-bind-apply-helpers": "^1.0.1",
|
3234 |
+
"es-errors": "^1.3.0",
|
3235 |
+
"gopd": "^1.2.0"
|
3236 |
+
},
|
3237 |
+
"engines": {
|
3238 |
+
"node": ">= 0.4"
|
3239 |
+
}
|
3240 |
+
},
|
3241 |
"node_modules/eastasianwidth": {
|
3242 |
"version": "0.2.0",
|
3243 |
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
|
|
3318 |
"is-arrayish": "^0.2.1"
|
3319 |
}
|
3320 |
},
|
3321 |
+
"node_modules/es-define-property": {
|
3322 |
+
"version": "1.0.1",
|
3323 |
+
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
3324 |
+
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
3325 |
+
"license": "MIT",
|
3326 |
+
"engines": {
|
3327 |
+
"node": ">= 0.4"
|
3328 |
+
}
|
3329 |
+
},
|
3330 |
+
"node_modules/es-errors": {
|
3331 |
+
"version": "1.3.0",
|
3332 |
+
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
3333 |
+
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
3334 |
+
"license": "MIT",
|
3335 |
+
"engines": {
|
3336 |
+
"node": ">= 0.4"
|
3337 |
+
}
|
3338 |
+
},
|
3339 |
+
"node_modules/es-object-atoms": {
|
3340 |
+
"version": "1.1.1",
|
3341 |
+
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
3342 |
+
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
3343 |
+
"license": "MIT",
|
3344 |
+
"dependencies": {
|
3345 |
+
"es-errors": "^1.3.0"
|
3346 |
+
},
|
3347 |
+
"engines": {
|
3348 |
+
"node": ">= 0.4"
|
3349 |
+
}
|
3350 |
+
},
|
3351 |
+
"node_modules/es-set-tostringtag": {
|
3352 |
+
"version": "2.1.0",
|
3353 |
+
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
3354 |
+
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
3355 |
+
"license": "MIT",
|
3356 |
+
"dependencies": {
|
3357 |
+
"es-errors": "^1.3.0",
|
3358 |
+
"get-intrinsic": "^1.2.6",
|
3359 |
+
"has-tostringtag": "^1.0.2",
|
3360 |
+
"hasown": "^2.0.2"
|
3361 |
+
},
|
3362 |
+
"engines": {
|
3363 |
+
"node": ">= 0.4"
|
3364 |
+
}
|
3365 |
+
},
|
3366 |
"node_modules/esbuild": {
|
3367 |
"version": "0.25.0",
|
3368 |
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
|
|
|
3750 |
"dev": true,
|
3751 |
"license": "ISC"
|
3752 |
},
|
3753 |
+
"node_modules/follow-redirects": {
|
3754 |
+
"version": "1.15.9",
|
3755 |
+
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
3756 |
+
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
3757 |
+
"funding": [
|
3758 |
+
{
|
3759 |
+
"type": "individual",
|
3760 |
+
"url": "https://github.com/sponsors/RubenVerborgh"
|
3761 |
+
}
|
3762 |
+
],
|
3763 |
+
"license": "MIT",
|
3764 |
+
"engines": {
|
3765 |
+
"node": ">=4.0"
|
3766 |
+
},
|
3767 |
+
"peerDependenciesMeta": {
|
3768 |
+
"debug": {
|
3769 |
+
"optional": true
|
3770 |
+
}
|
3771 |
+
}
|
3772 |
+
},
|
3773 |
"node_modules/foreground-child": {
|
3774 |
"version": "3.3.0",
|
3775 |
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
|
|
|
3787 |
"url": "https://github.com/sponsors/isaacs"
|
3788 |
}
|
3789 |
},
|
3790 |
+
"node_modules/form-data": {
|
3791 |
+
"version": "4.0.2",
|
3792 |
+
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
3793 |
+
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
3794 |
+
"license": "MIT",
|
3795 |
+
"dependencies": {
|
3796 |
+
"asynckit": "^0.4.0",
|
3797 |
+
"combined-stream": "^1.0.8",
|
3798 |
+
"es-set-tostringtag": "^2.1.0",
|
3799 |
+
"mime-types": "^2.1.12"
|
3800 |
+
},
|
3801 |
+
"engines": {
|
3802 |
+
"node": ">= 6"
|
3803 |
+
}
|
3804 |
+
},
|
3805 |
"node_modules/fraction.js": {
|
3806 |
"version": "4.3.7",
|
3807 |
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
|
|
3835 |
"version": "1.1.2",
|
3836 |
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
3837 |
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
|
|
3838 |
"license": "MIT",
|
3839 |
"funding": {
|
3840 |
"url": "https://github.com/sponsors/ljharb"
|
|
|
3858 |
"node": ">=6.9.0"
|
3859 |
}
|
3860 |
},
|
3861 |
+
"node_modules/get-intrinsic": {
|
3862 |
+
"version": "1.3.0",
|
3863 |
+
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
3864 |
+
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
3865 |
+
"license": "MIT",
|
3866 |
+
"dependencies": {
|
3867 |
+
"call-bind-apply-helpers": "^1.0.2",
|
3868 |
+
"es-define-property": "^1.0.1",
|
3869 |
+
"es-errors": "^1.3.0",
|
3870 |
+
"es-object-atoms": "^1.1.1",
|
3871 |
+
"function-bind": "^1.1.2",
|
3872 |
+
"get-proto": "^1.0.1",
|
3873 |
+
"gopd": "^1.2.0",
|
3874 |
+
"has-symbols": "^1.1.0",
|
3875 |
+
"hasown": "^2.0.2",
|
3876 |
+
"math-intrinsics": "^1.1.0"
|
3877 |
+
},
|
3878 |
+
"engines": {
|
3879 |
+
"node": ">= 0.4"
|
3880 |
+
},
|
3881 |
+
"funding": {
|
3882 |
+
"url": "https://github.com/sponsors/ljharb"
|
3883 |
+
}
|
3884 |
+
},
|
3885 |
+
"node_modules/get-proto": {
|
3886 |
+
"version": "1.0.1",
|
3887 |
+
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
3888 |
+
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
3889 |
+
"license": "MIT",
|
3890 |
+
"dependencies": {
|
3891 |
+
"dunder-proto": "^1.0.1",
|
3892 |
+
"es-object-atoms": "^1.0.0"
|
3893 |
+
},
|
3894 |
+
"engines": {
|
3895 |
+
"node": ">= 0.4"
|
3896 |
+
}
|
3897 |
+
},
|
3898 |
"node_modules/glob": {
|
3899 |
"version": "10.4.5",
|
3900 |
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
|
|
3967 |
"url": "https://github.com/sponsors/sindresorhus"
|
3968 |
}
|
3969 |
},
|
3970 |
+
"node_modules/gopd": {
|
3971 |
+
"version": "1.2.0",
|
3972 |
+
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
3973 |
+
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
3974 |
+
"license": "MIT",
|
3975 |
+
"engines": {
|
3976 |
+
"node": ">= 0.4"
|
3977 |
+
},
|
3978 |
+
"funding": {
|
3979 |
+
"url": "https://github.com/sponsors/ljharb"
|
3980 |
+
}
|
3981 |
+
},
|
3982 |
"node_modules/graphemer": {
|
3983 |
"version": "1.4.0",
|
3984 |
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
|
|
3996 |
"node": ">=8"
|
3997 |
}
|
3998 |
},
|
3999 |
+
"node_modules/has-symbols": {
|
4000 |
+
"version": "1.1.0",
|
4001 |
+
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
4002 |
+
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
4003 |
+
"license": "MIT",
|
4004 |
+
"engines": {
|
4005 |
+
"node": ">= 0.4"
|
4006 |
+
},
|
4007 |
+
"funding": {
|
4008 |
+
"url": "https://github.com/sponsors/ljharb"
|
4009 |
+
}
|
4010 |
+
},
|
4011 |
+
"node_modules/has-tostringtag": {
|
4012 |
+
"version": "1.0.2",
|
4013 |
+
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
4014 |
+
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
4015 |
+
"license": "MIT",
|
4016 |
+
"dependencies": {
|
4017 |
+
"has-symbols": "^1.0.3"
|
4018 |
+
},
|
4019 |
+
"engines": {
|
4020 |
+
"node": ">= 0.4"
|
4021 |
+
},
|
4022 |
+
"funding": {
|
4023 |
+
"url": "https://github.com/sponsors/ljharb"
|
4024 |
+
}
|
4025 |
+
},
|
4026 |
"node_modules/hasown": {
|
4027 |
"version": "2.0.2",
|
4028 |
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
4029 |
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
|
|
4030 |
"license": "MIT",
|
4031 |
"dependencies": {
|
4032 |
"function-bind": "^1.1.2"
|
|
|
4743 |
"license": "MIT",
|
4744 |
"optional": true
|
4745 |
},
|
4746 |
+
"node_modules/math-intrinsics": {
|
4747 |
+
"version": "1.1.0",
|
4748 |
+
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
4749 |
+
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
4750 |
+
"license": "MIT",
|
4751 |
+
"engines": {
|
4752 |
+
"node": ">= 0.4"
|
4753 |
+
}
|
4754 |
+
},
|
4755 |
"node_modules/mdast-util-from-markdown": {
|
4756 |
"version": "2.0.2",
|
4757 |
"resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz",
|
|
|
5371 |
"node": ">=8.6"
|
5372 |
}
|
5373 |
},
|
5374 |
+
"node_modules/mime-db": {
|
5375 |
+
"version": "1.52.0",
|
5376 |
+
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
5377 |
+
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
5378 |
+
"license": "MIT",
|
5379 |
+
"engines": {
|
5380 |
+
"node": ">= 0.6"
|
5381 |
+
}
|
5382 |
+
},
|
5383 |
+
"node_modules/mime-types": {
|
5384 |
+
"version": "2.1.35",
|
5385 |
+
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
5386 |
+
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
5387 |
+
"license": "MIT",
|
5388 |
+
"dependencies": {
|
5389 |
+
"mime-db": "1.52.0"
|
5390 |
+
},
|
5391 |
+
"engines": {
|
5392 |
+
"node": ">= 0.6"
|
5393 |
+
}
|
5394 |
+
},
|
5395 |
"node_modules/minimatch": {
|
5396 |
"version": "3.1.2",
|
5397 |
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
|
|
6016 |
"url": "https://github.com/sponsors/wooorm"
|
6017 |
}
|
6018 |
},
|
6019 |
+
"node_modules/proxy-from-env": {
|
6020 |
+
"version": "1.1.0",
|
6021 |
+
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
6022 |
+
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
6023 |
+
"license": "MIT"
|
6024 |
+
},
|
6025 |
"node_modules/prr": {
|
6026 |
"version": "1.0.1",
|
6027 |
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
|
lynxkite-app/web/package.json
CHANGED
@@ -20,6 +20,7 @@
|
|
20 |
"@syncedstore/react": "^0.6.0",
|
21 |
"@types/node": "^22.10.1",
|
22 |
"@xyflow/react": "^12.3.5",
|
|
|
23 |
"daisyui": "^4.12.20",
|
24 |
"echarts": "^5.5.1",
|
25 |
"fuse.js": "^7.0.0",
|
@@ -35,6 +36,8 @@
|
|
35 |
},
|
36 |
"devDependencies": {
|
37 |
"@eslint/js": "^9.15.0",
|
|
|
|
|
38 |
"@types/react": "^18.3.14",
|
39 |
"@types/react-dom": "^18.3.2",
|
40 |
"@vitejs/plugin-react-swc": "^3.5.0",
|
@@ -47,9 +50,7 @@
|
|
47 |
"tailwindcss": "^3.4.16",
|
48 |
"typescript": "~5.6.2",
|
49 |
"typescript-eslint": "^8.15.0",
|
50 |
-
"vite": "^6.2.0"
|
51 |
-
"@playwright/test": "^1.50.1",
|
52 |
-
"@types/node": "^22.13.1"
|
53 |
},
|
54 |
"optionalDependencies": {
|
55 |
"@rollup/rollup-linux-x64-gnu": "^4.28.1"
|
|
|
20 |
"@syncedstore/react": "^0.6.0",
|
21 |
"@types/node": "^22.10.1",
|
22 |
"@xyflow/react": "^12.3.5",
|
23 |
+
"axios": "^1.8.2",
|
24 |
"daisyui": "^4.12.20",
|
25 |
"echarts": "^5.5.1",
|
26 |
"fuse.js": "^7.0.0",
|
|
|
36 |
},
|
37 |
"devDependencies": {
|
38 |
"@eslint/js": "^9.15.0",
|
39 |
+
"@playwright/test": "^1.50.1",
|
40 |
+
"@types/node": "^22.13.1",
|
41 |
"@types/react": "^18.3.14",
|
42 |
"@types/react-dom": "^18.3.2",
|
43 |
"@vitejs/plugin-react-swc": "^3.5.0",
|
|
|
50 |
"tailwindcss": "^3.4.16",
|
51 |
"typescript": "~5.6.2",
|
52 |
"typescript-eslint": "^8.15.0",
|
53 |
+
"vite": "^6.2.0"
|
|
|
|
|
54 |
},
|
55 |
"optionalDependencies": {
|
56 |
"@rollup/rollup-linux-x64-gnu": "^4.28.1"
|
lynxkite-app/web/playwright.config.ts
CHANGED
@@ -23,7 +23,7 @@ export default defineConfig({
|
|
23 |
},
|
24 |
],
|
25 |
webServer: {
|
26 |
-
command: "cd
|
27 |
url: "http://127.0.0.1:8000",
|
28 |
reuseExistingServer: false,
|
29 |
},
|
|
|
23 |
},
|
24 |
],
|
25 |
webServer: {
|
26 |
+
command: "cd ../../examples && lynxkite",
|
27 |
url: "http://127.0.0.1:8000",
|
28 |
reuseExistingServer: false,
|
29 |
},
|
lynxkite-app/web/src/Directory.tsx
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
import { useState } from "react";
|
2 |
// The directory browser.
|
3 |
-
import { useNavigate, useParams } from "react-router";
|
4 |
import useSWR from "swr";
|
5 |
import type { DirectoryEntry } from "./apiTypes.ts";
|
6 |
|
@@ -23,7 +23,9 @@ const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
|
23 |
export default function () {
|
24 |
const { path } = useParams();
|
25 |
const encodedPath = encodeURIComponent(path || "");
|
26 |
-
const list = useSWR(`/api/dir/list?path=${encodedPath}`, fetcher
|
|
|
|
|
27 |
const navigate = useNavigate();
|
28 |
const [isCreatingDir, setIsCreatingDir] = useState(false);
|
29 |
const [isCreatingWorkspace, setIsCreatingWorkspace] = useState(false);
|
@@ -173,19 +175,19 @@ export default function () {
|
|
173 |
|
174 |
{path && (
|
175 |
<div className="breadcrumbs">
|
176 |
-
<
|
177 |
<Home />
|
178 |
-
</
|
179 |
<span className="current-folder">{path}</span>
|
180 |
</div>
|
181 |
)}
|
182 |
|
183 |
{list.data.map((item: DirectoryEntry) => (
|
184 |
<div key={item.name} className="entry">
|
185 |
-
<
|
186 |
{item.type === "directory" ? <Folder /> : <File />}
|
187 |
{shortName(item)}
|
188 |
-
</
|
189 |
<button
|
190 |
type="button"
|
191 |
onClick={() => {
|
|
|
1 |
import { useState } from "react";
|
2 |
// The directory browser.
|
3 |
+
import { Link, useNavigate, useParams } from "react-router";
|
4 |
import useSWR from "swr";
|
5 |
import type { DirectoryEntry } from "./apiTypes.ts";
|
6 |
|
|
|
23 |
export default function () {
|
24 |
const { path } = useParams();
|
25 |
const encodedPath = encodeURIComponent(path || "");
|
26 |
+
const list = useSWR(`/api/dir/list?path=${encodedPath}`, fetcher, {
|
27 |
+
dedupingInterval: 0,
|
28 |
+
});
|
29 |
const navigate = useNavigate();
|
30 |
const [isCreatingDir, setIsCreatingDir] = useState(false);
|
31 |
const [isCreatingWorkspace, setIsCreatingWorkspace] = useState(false);
|
|
|
175 |
|
176 |
{path && (
|
177 |
<div className="breadcrumbs">
|
178 |
+
<Link to="/dir/">
|
179 |
<Home />
|
180 |
+
</Link>{" "}
|
181 |
<span className="current-folder">{path}</span>
|
182 |
</div>
|
183 |
)}
|
184 |
|
185 |
{list.data.map((item: DirectoryEntry) => (
|
186 |
<div key={item.name} className="entry">
|
187 |
+
<Link key={link(item)} to={link(item)}>
|
188 |
{item.type === "directory" ? <Folder /> : <File />}
|
189 |
{shortName(item)}
|
190 |
+
</Link>
|
191 |
<button
|
192 |
type="button"
|
193 |
onClick={() => {
|
lynxkite-app/web/src/index.css
CHANGED
@@ -235,6 +235,41 @@ body {
|
|
235 |
margin: 10px;
|
236 |
}
|
237 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
238 |
}
|
239 |
|
240 |
.directory {
|
|
|
235 |
margin: 10px;
|
236 |
}
|
237 |
}
|
238 |
+
|
239 |
+
.env-select {
|
240 |
+
background: transparent;
|
241 |
+
color: #39bcf3;
|
242 |
+
}
|
243 |
+
|
244 |
+
.workspace-message {
|
245 |
+
position: absolute;
|
246 |
+
left: 50%;
|
247 |
+
bottom: 20px;
|
248 |
+
transform: translateX(-50%);
|
249 |
+
box-shadow: 0 5px 50px 0px #8008;
|
250 |
+
padding: 10px 40px 10px 20px;
|
251 |
+
border-radius: 5px;
|
252 |
+
|
253 |
+
.close {
|
254 |
+
position: absolute;
|
255 |
+
right: 10px;
|
256 |
+
cursor: pointer;
|
257 |
+
}
|
258 |
+
}
|
259 |
+
}
|
260 |
+
|
261 |
+
.params-expander {
|
262 |
+
font-size: 15px;
|
263 |
+
padding: 4px;
|
264 |
+
color: #000a;
|
265 |
+
}
|
266 |
+
|
267 |
+
.flippy {
|
268 |
+
transition: transform 0.5s;
|
269 |
+
}
|
270 |
+
|
271 |
+
.flippy.flippy-90 {
|
272 |
+
transform: rotate(-90deg);
|
273 |
}
|
274 |
|
275 |
.directory {
|
lynxkite-app/web/src/workspace/EnvironmentSelector.tsx
CHANGED
@@ -6,7 +6,7 @@ export default function EnvironmentSelector(props: {
|
|
6 |
return (
|
7 |
<>
|
8 |
<select
|
9 |
-
className="select w-full max-w-xs"
|
10 |
name="workspace-env"
|
11 |
value={props.value}
|
12 |
onChange={(evt) => props.onChange(evt.currentTarget.value)}
|
|
|
6 |
return (
|
7 |
<>
|
8 |
<select
|
9 |
+
className="env-select select w-full max-w-xs"
|
10 |
name="workspace-env"
|
11 |
value={props.value}
|
12 |
onChange={(evt) => props.onChange(evt.currentTarget.value)}
|
lynxkite-app/web/src/workspace/NodeSearch.tsx
CHANGED
@@ -25,9 +25,14 @@ export default function (props: {
|
|
25 |
}),
|
26 |
[props.boxes],
|
27 |
);
|
|
|
|
|
|
|
|
|
|
|
28 |
const hits: { item: OpsOp }[] = searchText
|
29 |
? fuse.search<OpsOp>(searchText)
|
30 |
-
:
|
31 |
const [selectedIndex, setSelectedIndex] = useState(0);
|
32 |
useEffect(() => searchBox.current.focus());
|
33 |
function typed(text: string) {
|
|
|
25 |
}),
|
26 |
[props.boxes],
|
27 |
);
|
28 |
+
const allOps = useMemo(() => {
|
29 |
+
const boxes = Object.values(props.boxes).map((box) => ({ item: box }));
|
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) {
|
lynxkite-app/web/src/workspace/Workspace.tsx
CHANGED
@@ -14,6 +14,7 @@ import {
|
|
14 |
useReactFlow,
|
15 |
useUpdateNodeInternals,
|
16 |
} from "@xyflow/react";
|
|
|
17 |
import {
|
18 |
type MouseEvent,
|
19 |
useCallback,
|
@@ -26,11 +27,11 @@ import { useParams } from "react-router";
|
|
26 |
import useSWR, { type Fetcher } from "swr";
|
27 |
import { WebsocketProvider } from "y-websocket";
|
28 |
// @ts-ignore
|
29 |
-
import ArrowBack from "~icons/tabler/arrow-back.jsx";
|
30 |
-
// @ts-ignore
|
31 |
import Atom from "~icons/tabler/atom.jsx";
|
32 |
// @ts-ignore
|
33 |
import Backspace from "~icons/tabler/backspace.jsx";
|
|
|
|
|
34 |
import type { Workspace, WorkspaceNode } from "../apiTypes.ts";
|
35 |
import favicon from "../assets/favicon.ico";
|
36 |
// import NodeWithTableView from './NodeWithTableView';
|
@@ -62,12 +63,14 @@ function LynxKiteFlow() {
|
|
62 |
const [edges, setEdges] = useState([] as Edge[]);
|
63 |
const { path } = useParams();
|
64 |
const [state, setState] = useState({ workspace: {} as Workspace });
|
|
|
65 |
useEffect(() => {
|
66 |
const state = syncedStore({ workspace: {} as Workspace });
|
67 |
setState(state);
|
68 |
const doc = getYjsDoc(state);
|
|
|
69 |
const wsProvider = new WebsocketProvider(
|
70 |
-
|
71 |
path!,
|
72 |
doc,
|
73 |
);
|
@@ -79,7 +82,9 @@ function LynxKiteFlow() {
|
|
79 |
if (!state.workspace.nodes) return;
|
80 |
if (!state.workspace.edges) return;
|
81 |
for (const n of state.workspace.nodes) {
|
82 |
-
n.dragHandle
|
|
|
|
|
83 |
}
|
84 |
setNodes([...state.workspace.nodes] as Node[]);
|
85 |
setEdges([...state.workspace.edges] as Edge[]);
|
@@ -233,33 +238,44 @@ function LynxKiteFlow() {
|
|
233 |
},
|
234 |
[catalog, state, nodeSearchSettings, suppressSearchUntil, closeNodeSearch],
|
235 |
);
|
236 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
237 |
(meta: OpsOp) => {
|
238 |
-
const node
|
239 |
-
type: meta.type,
|
240 |
-
data: {
|
241 |
-
meta: meta,
|
242 |
-
title: meta.name,
|
243 |
-
params: Object.fromEntries(
|
244 |
-
Object.values(meta.params).map((p) => [p.name, p.default]),
|
245 |
-
),
|
246 |
-
},
|
247 |
-
};
|
248 |
const nss = nodeSearchSettings!;
|
249 |
node.position = reactFlow.screenToFlowPosition({
|
250 |
x: nss.pos.x,
|
251 |
y: nss.pos.y,
|
252 |
});
|
253 |
-
|
254 |
-
let i = 1;
|
255 |
-
node.id = `${title} ${i}`;
|
256 |
-
const wnodes = state.workspace.nodes!;
|
257 |
-
while (wnodes.find((x) => x.id === node.id)) {
|
258 |
-
i += 1;
|
259 |
-
node.id = `${title} ${i}`;
|
260 |
-
}
|
261 |
-
wnodes.push(node as WorkspaceNode);
|
262 |
-
setNodes([...nodes, node as WorkspaceNode]);
|
263 |
closeNodeSearch();
|
264 |
},
|
265 |
[nodeSearchSettings, state, reactFlow, nodes, closeNodeSearch],
|
@@ -281,6 +297,48 @@ function LynxKiteFlow() {
|
|
281 |
[state],
|
282 |
);
|
283 |
const parentDir = path!.split("/").slice(0, -1).join("/");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
284 |
return (
|
285 |
<div className="workspace">
|
286 |
<div className="top-bar bg-neutral">
|
@@ -303,11 +361,15 @@ function LynxKiteFlow() {
|
|
303 |
<Backspace />
|
304 |
</a>
|
305 |
<a href={`/dir/${parentDir}`}>
|
306 |
-
<
|
307 |
</a>
|
308 |
</div>
|
309 |
</div>
|
310 |
-
<div
|
|
|
|
|
|
|
|
|
311 |
<LynxKiteState.Provider value={state}>
|
312 |
<ReactFlow
|
313 |
nodes={nodes}
|
@@ -319,7 +381,7 @@ function LynxKiteFlow() {
|
|
319 |
onPaneClick={toggleNodeSearch}
|
320 |
onConnect={onConnect}
|
321 |
proOptions={{ hideAttribution: true }}
|
322 |
-
maxZoom={
|
323 |
minZoom={0.3}
|
324 |
defaultEdgeOptions={{
|
325 |
markerEnd: {
|
@@ -341,11 +403,19 @@ function LynxKiteFlow() {
|
|
341 |
pos={nodeSearchSettings.pos}
|
342 |
boxes={nodeSearchSettings.boxes}
|
343 |
onCancel={closeNodeSearch}
|
344 |
-
onAdd={
|
345 |
/>
|
346 |
)}
|
347 |
</ReactFlow>
|
348 |
</LynxKiteState.Provider>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
349 |
</div>
|
350 |
</div>
|
351 |
);
|
|
|
14 |
useReactFlow,
|
15 |
useUpdateNodeInternals,
|
16 |
} from "@xyflow/react";
|
17 |
+
import axios from "axios";
|
18 |
import {
|
19 |
type MouseEvent,
|
20 |
useCallback,
|
|
|
27 |
import useSWR, { type Fetcher } from "swr";
|
28 |
import { WebsocketProvider } from "y-websocket";
|
29 |
// @ts-ignore
|
|
|
|
|
30 |
import Atom from "~icons/tabler/atom.jsx";
|
31 |
// @ts-ignore
|
32 |
import Backspace from "~icons/tabler/backspace.jsx";
|
33 |
+
// @ts-ignore
|
34 |
+
import Close from "~icons/tabler/x.jsx";
|
35 |
import type { Workspace, WorkspaceNode } from "../apiTypes.ts";
|
36 |
import favicon from "../assets/favicon.ico";
|
37 |
// import NodeWithTableView from './NodeWithTableView';
|
|
|
63 |
const [edges, setEdges] = useState([] as Edge[]);
|
64 |
const { path } = useParams();
|
65 |
const [state, setState] = useState({ workspace: {} as Workspace });
|
66 |
+
const [message, setMessage] = useState(null as string | null);
|
67 |
useEffect(() => {
|
68 |
const state = syncedStore({ workspace: {} as Workspace });
|
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 |
);
|
|
|
82 |
if (!state.workspace.nodes) return;
|
83 |
if (!state.workspace.edges) return;
|
84 |
for (const n of state.workspace.nodes) {
|
85 |
+
if (n.dragHandle !== ".bg-primary") {
|
86 |
+
n.dragHandle = ".bg-primary";
|
87 |
+
}
|
88 |
}
|
89 |
setNodes([...state.workspace.nodes] as Node[]);
|
90 |
setEdges([...state.workspace.edges] as Edge[]);
|
|
|
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}`;
|
249 |
+
const wnodes = state.workspace.nodes!;
|
250 |
+
while (wnodes.find((x) => x.id === node.id)) {
|
251 |
+
i += 1;
|
252 |
+
node.id = `${title} ${i}`;
|
253 |
+
}
|
254 |
+
wnodes.push(node as WorkspaceNode);
|
255 |
+
setNodes([...nodes, node as WorkspaceNode]);
|
256 |
+
}
|
257 |
+
function nodeFromMeta(meta: OpsOp): Partial<WorkspaceNode> {
|
258 |
+
const node: Partial<WorkspaceNode> = {
|
259 |
+
type: meta.type,
|
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;
|
269 |
+
}
|
270 |
+
const addNodeFromSearch = useCallback(
|
271 |
(meta: OpsOp) => {
|
272 |
+
const node = nodeFromMeta(meta);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
273 |
const nss = nodeSearchSettings!;
|
274 |
node.position = reactFlow.screenToFlowPosition({
|
275 |
x: nss.pos.x,
|
276 |
y: nss.pos.y,
|
277 |
});
|
278 |
+
addNode(node, state, nodes);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
279 |
closeNodeSearch();
|
280 |
},
|
281 |
[nodeSearchSettings, state, reactFlow, nodes, closeNodeSearch],
|
|
|
297 |
[state],
|
298 |
);
|
299 |
const parentDir = path!.split("/").slice(0, -1).join("/");
|
300 |
+
function onDragOver(e: React.DragEvent<HTMLDivElement>) {
|
301 |
+
e.stopPropagation();
|
302 |
+
e.preventDefault();
|
303 |
+
}
|
304 |
+
async function onDrop(e: React.DragEvent<HTMLDivElement>) {
|
305 |
+
e.stopPropagation();
|
306 |
+
e.preventDefault();
|
307 |
+
const file = e.dataTransfer.files[0];
|
308 |
+
const formData = new FormData();
|
309 |
+
formData.append("file", file);
|
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 |
+
},
|
319 |
+
});
|
320 |
+
setMessage(null);
|
321 |
+
const cat = catalog.data![state.workspace.env!];
|
322 |
+
const node = nodeFromMeta(cat["Import file"]);
|
323 |
+
node.position = reactFlow.screenToFlowPosition({
|
324 |
+
x: e.clientX,
|
325 |
+
y: e.clientY,
|
326 |
+
});
|
327 |
+
node.data!.params.file_path = `uploads/${file.name}`;
|
328 |
+
if (file.name.includes(".csv")) {
|
329 |
+
node.data!.params.file_format = "csv";
|
330 |
+
} else if (file.name.includes(".parquet")) {
|
331 |
+
node.data!.params.file_format = "parquet";
|
332 |
+
} else if (file.name.includes(".json")) {
|
333 |
+
node.data!.params.file_format = "json";
|
334 |
+
} else if (file.name.includes(".xls")) {
|
335 |
+
node.data!.params.file_format = "excel";
|
336 |
+
}
|
337 |
+
addNode(node, state, nodes);
|
338 |
+
} catch (error) {
|
339 |
+
setMessage("File upload failed.");
|
340 |
+
}
|
341 |
+
}
|
342 |
return (
|
343 |
<div className="workspace">
|
344 |
<div className="top-bar bg-neutral">
|
|
|
361 |
<Backspace />
|
362 |
</a>
|
363 |
<a href={`/dir/${parentDir}`}>
|
364 |
+
<Close />
|
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}
|
|
|
381 |
onPaneClick={toggleNodeSearch}
|
382 |
onConnect={onConnect}
|
383 |
proOptions={{ hideAttribution: true }}
|
384 |
+
maxZoom={1}
|
385 |
minZoom={0.3}
|
386 |
defaultEdgeOptions={{
|
387 |
markerEnd: {
|
|
|
403 |
pos={nodeSearchSettings.pos}
|
404 |
boxes={nodeSearchSettings.boxes}
|
405 |
onCancel={closeNodeSearch}
|
406 |
+
onAdd={addNodeFromSearch}
|
407 |
/>
|
408 |
)}
|
409 |
</ReactFlow>
|
410 |
</LynxKiteState.Provider>
|
411 |
+
{message && (
|
412 |
+
<div className="workspace-message">
|
413 |
+
<span className="close" onClick={() => setMessage(null)}>
|
414 |
+
<Close />
|
415 |
+
</span>
|
416 |
+
{message}
|
417 |
+
</div>
|
418 |
+
)}
|
419 |
</div>
|
420 |
</div>
|
421 |
);
|
lynxkite-app/web/src/workspace/nodes/NodeParameter.tsx
CHANGED
@@ -73,6 +73,10 @@ export default function NodeParameter({
|
|
73 |
value={value || ""}
|
74 |
onChange={(evt) => onChange(evt.currentTarget.value, { delay: 2 })}
|
75 |
onBlur={(evt) => onChange(evt.currentTarget.value, { delay: 0 })}
|
|
|
|
|
|
|
|
|
76 |
/>
|
77 |
</>
|
78 |
)}
|
|
|
73 |
value={value || ""}
|
74 |
onChange={(evt) => onChange(evt.currentTarget.value, { delay: 2 })}
|
75 |
onBlur={(evt) => onChange(evt.currentTarget.value, { delay: 0 })}
|
76 |
+
onKeyDown={(evt) =>
|
77 |
+
evt.code === "Enter" &&
|
78 |
+
onChange(evt.currentTarget.value, { delay: 0 })
|
79 |
+
}
|
80 |
/>
|
81 |
</>
|
82 |
)}
|
lynxkite-app/web/src/workspace/nodes/NodeWithParams.tsx
CHANGED
@@ -1,4 +1,7 @@
|
|
1 |
import { useReactFlow } from "@xyflow/react";
|
|
|
|
|
|
|
2 |
import LynxKiteNode from "./LynxKiteNode";
|
3 |
import NodeGroupParameter from "./NodeGroupParameter";
|
4 |
import NodeParameter from "./NodeParameter";
|
@@ -8,6 +11,7 @@ export type UpdateOptions = { delay?: number };
|
|
8 |
function NodeWithParams(props: any) {
|
9 |
const reactFlow = useReactFlow();
|
10 |
const metaParams = props.data.meta?.params;
|
|
|
11 |
|
12 |
function setParam(name: string, newValue: any, opts: UpdateOptions) {
|
13 |
reactFlow.updateNodeData(props.id, (prevData: any) => ({
|
@@ -31,31 +35,40 @@ function NodeWithParams(props: any) {
|
|
31 |
|
32 |
return (
|
33 |
<LynxKiteNode {...props}>
|
34 |
-
{
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
setParam(name, value, opts || {})
|
42 |
-
}
|
43 |
-
deleteParam={(name: string, opts?: UpdateOptions) =>
|
44 |
-
deleteParam(name, opts || {})
|
45 |
-
}
|
46 |
-
/>
|
47 |
-
) : (
|
48 |
-
<NodeParameter
|
49 |
-
name={name}
|
50 |
-
key={name}
|
51 |
-
value={value}
|
52 |
-
meta={metaParams?.[name]}
|
53 |
-
onChange={(value: any, opts?: UpdateOptions) =>
|
54 |
-
setParam(name, value, opts || {})
|
55 |
-
}
|
56 |
-
/>
|
57 |
-
),
|
58 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
59 |
{props.children}
|
60 |
</LynxKiteNode>
|
61 |
);
|
|
|
1 |
import { useReactFlow } from "@xyflow/react";
|
2 |
+
import React from "react";
|
3 |
+
// @ts-ignore
|
4 |
+
import Triangle from "~icons/tabler/triangle-inverted-filled.jsx";
|
5 |
import LynxKiteNode from "./LynxKiteNode";
|
6 |
import NodeGroupParameter from "./NodeGroupParameter";
|
7 |
import NodeParameter from "./NodeParameter";
|
|
|
11 |
function NodeWithParams(props: any) {
|
12 |
const reactFlow = useReactFlow();
|
13 |
const metaParams = props.data.meta?.params;
|
14 |
+
const [collapsed, setCollapsed] = React.useState(props.collapsed);
|
15 |
|
16 |
function setParam(name: string, newValue: any, opts: UpdateOptions) {
|
17 |
reactFlow.updateNodeData(props.id, (prevData: any) => ({
|
|
|
35 |
|
36 |
return (
|
37 |
<LynxKiteNode {...props}>
|
38 |
+
{props.collapsed && (
|
39 |
+
<div
|
40 |
+
className="params-expander"
|
41 |
+
onClick={() => setCollapsed(!collapsed)}
|
42 |
+
>
|
43 |
+
<Triangle className={`flippy ${collapsed ? "flippy-90" : ""}`} />
|
44 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
45 |
)}
|
46 |
+
{!collapsed &&
|
47 |
+
params.map(([name, value]) =>
|
48 |
+
metaParams?.[name]?.type === "group" ? (
|
49 |
+
<NodeGroupParameter
|
50 |
+
key={name}
|
51 |
+
value={value}
|
52 |
+
meta={metaParams?.[name]}
|
53 |
+
setParam={(name: string, value: any, opts?: UpdateOptions) =>
|
54 |
+
setParam(name, value, opts || {})
|
55 |
+
}
|
56 |
+
deleteParam={(name: string, opts?: UpdateOptions) =>
|
57 |
+
deleteParam(name, opts || {})
|
58 |
+
}
|
59 |
+
/>
|
60 |
+
) : (
|
61 |
+
<NodeParameter
|
62 |
+
name={name}
|
63 |
+
key={name}
|
64 |
+
value={value}
|
65 |
+
meta={metaParams?.[name]}
|
66 |
+
onChange={(value: any, opts?: UpdateOptions) =>
|
67 |
+
setParam(name, value, opts || {})
|
68 |
+
}
|
69 |
+
/>
|
70 |
+
),
|
71 |
+
)}
|
72 |
{props.children}
|
73 |
</LynxKiteNode>
|
74 |
);
|
lynxkite-app/web/src/workspace/nodes/NodeWithVisualization.tsx
CHANGED
@@ -10,20 +10,28 @@ const NodeWithVisualization = (props: any) => {
|
|
10 |
if (!opts || !chartsRef.current) return;
|
11 |
chartsInstanceRef.current = echarts.init(chartsRef.current, null, {
|
12 |
renderer: "canvas",
|
13 |
-
width:
|
14 |
-
height:
|
15 |
});
|
16 |
chartsInstanceRef.current.setOption(opts);
|
17 |
-
const
|
18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
return () => {
|
20 |
-
|
21 |
chartsInstanceRef.current?.dispose();
|
22 |
};
|
23 |
}, [props.data?.display?.value]);
|
|
|
|
|
24 |
return (
|
25 |
-
<NodeWithParams {...props}>
|
26 |
-
<div
|
27 |
</NodeWithParams>
|
28 |
);
|
29 |
};
|
|
|
10 |
if (!opts || !chartsRef.current) return;
|
11 |
chartsInstanceRef.current = echarts.init(chartsRef.current, null, {
|
12 |
renderer: "canvas",
|
13 |
+
width: "auto",
|
14 |
+
height: "auto",
|
15 |
});
|
16 |
chartsInstanceRef.current.setOption(opts);
|
17 |
+
const resizeObserver = new ResizeObserver(() => {
|
18 |
+
const e = chartsRef.current!;
|
19 |
+
e.style.padding = "1px";
|
20 |
+
chartsInstanceRef.current?.resize();
|
21 |
+
e.style.padding = "0";
|
22 |
+
});
|
23 |
+
const observed = chartsRef.current;
|
24 |
+
resizeObserver.observe(observed);
|
25 |
return () => {
|
26 |
+
resizeObserver.unobserve(observed);
|
27 |
chartsInstanceRef.current?.dispose();
|
28 |
};
|
29 |
}, [props.data?.display?.value]);
|
30 |
+
const nodeStyle = { display: "flex", flexDirection: "column" };
|
31 |
+
const vizStyle = { flex: 1 };
|
32 |
return (
|
33 |
+
<NodeWithParams nodeStyle={nodeStyle} collapsed {...props}>
|
34 |
+
<div style={vizStyle} ref={chartsRef} />
|
35 |
</NodeWithParams>
|
36 |
);
|
37 |
};
|
lynxkite-app/web/tests/basic.spec.ts
CHANGED
@@ -21,9 +21,6 @@ test("Box creation & deletion per env", async () => {
|
|
21 |
const envs = await workspace.getEnvs();
|
22 |
for (const env of envs) {
|
23 |
await workspace.setEnv(env);
|
24 |
-
// TODO: Opening the catalog immediately after setting the env can fail.
|
25 |
-
// Let's fix this!
|
26 |
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
27 |
const catalog = await workspace.getCatalog();
|
28 |
expect(catalog).not.toHaveLength(0);
|
29 |
const op = catalog[0];
|
@@ -35,9 +32,9 @@ test("Box creation & deletion per env", async () => {
|
|
35 |
});
|
36 |
|
37 |
test("Delete multi-handle boxes", async () => {
|
38 |
-
await workspace.addBox("
|
39 |
-
await workspace.deleteBoxes(["
|
40 |
-
await expect(workspace.getBox("
|
41 |
});
|
42 |
|
43 |
test("Drag box", async () => {
|
|
|
21 |
const envs = await workspace.getEnvs();
|
22 |
for (const env of envs) {
|
23 |
await workspace.setEnv(env);
|
|
|
|
|
|
|
24 |
const catalog = await workspace.getCatalog();
|
25 |
expect(catalog).not.toHaveLength(0);
|
26 |
const op = catalog[0];
|
|
|
32 |
});
|
33 |
|
34 |
test("Delete multi-handle boxes", async () => {
|
35 |
+
await workspace.addBox("NX › PageRank");
|
36 |
+
await workspace.deleteBoxes(["NX › PageRank 1"]);
|
37 |
+
await expect(workspace.getBox("NX › PageRank 1")).not.toBeVisible();
|
38 |
});
|
39 |
|
40 |
test("Drag box", async () => {
|
lynxkite-app/web/tests/errors.spec.ts
CHANGED
@@ -20,24 +20,24 @@ test.afterEach(async () => {
|
|
20 |
test("missing parameter", async () => {
|
21 |
// Test the correct error message is displayed when a required parameter is missing,
|
22 |
// and that the error message is removed when the parameter is filled.
|
23 |
-
await workspace.addBox("
|
24 |
-
const graphBox = workspace.getBox("
|
25 |
-
await graphBox.locator("
|
26 |
-
|
27 |
-
"invalid literal for int() with base 10: ''",
|
28 |
-
);
|
29 |
-
await graphBox.locator("input").fill("10");
|
30 |
await expect(graphBox.locator(".error")).not.toBeVisible();
|
31 |
});
|
32 |
|
33 |
test("unknown operation", async () => {
|
34 |
// Test that the correct error is displayed when the operation does not belong to
|
35 |
// the current environment.
|
36 |
-
await workspace.addBox("
|
|
|
|
|
37 |
await workspace.setEnv("LynxScribe");
|
38 |
-
const csvBox = workspace.getBox("
|
39 |
-
|
40 |
-
|
|
|
41 |
await workspace.setEnv("LynxKite Graph Analytics");
|
42 |
await expect(csvBox.locator(".error")).not.toBeVisible();
|
43 |
});
|
|
|
20 |
test("missing parameter", async () => {
|
21 |
// Test the correct error message is displayed when a required parameter is missing,
|
22 |
// and that the error message is removed when the parameter is filled.
|
23 |
+
await workspace.addBox("NX › Scale-Free Graph");
|
24 |
+
const graphBox = workspace.getBox("NX › Scale-Free Graph 1");
|
25 |
+
await expect(graphBox.locator(".error")).toHaveText("n is unset.");
|
26 |
+
await graphBox.getByLabel("n", { exact: true }).fill("10");
|
|
|
|
|
|
|
27 |
await expect(graphBox.locator(".error")).not.toBeVisible();
|
28 |
});
|
29 |
|
30 |
test("unknown operation", async () => {
|
31 |
// Test that the correct error is displayed when the operation does not belong to
|
32 |
// the current environment.
|
33 |
+
await workspace.addBox("NX › Scale-Free Graph");
|
34 |
+
const graphBox = workspace.getBox("NX › Scale-Free Graph 1");
|
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 |
});
|
lynxkite-app/web/tests/examples.spec.ts
CHANGED
@@ -17,30 +17,23 @@ test("Pytorch example", async ({ page }) => {
|
|
17 |
await ws.expectErrorFree();
|
18 |
});
|
19 |
|
20 |
-
test
|
21 |
-
// Fails because of missing OPENAI_API_KEY
|
22 |
const ws = await Workspace.open(page, "AIMO");
|
23 |
await ws.expectErrorFree();
|
24 |
});
|
25 |
|
26 |
-
test
|
27 |
// Fails because of missing OPENAI_API_KEY
|
28 |
const ws = await Workspace.open(page, "LynxScribe demo");
|
29 |
await ws.expectErrorFree();
|
30 |
});
|
31 |
|
32 |
-
test
|
33 |
// Fails due to some issue with ChromaDB
|
34 |
const ws = await Workspace.open(page, "Graph RAG");
|
35 |
await ws.expectErrorFree(process.env.CI ? 2000 : 500);
|
36 |
});
|
37 |
|
38 |
-
test.fail("RAG chatbot app", async ({ page }) => {
|
39 |
-
// Fail due to all operation being unknown
|
40 |
-
const ws = await Workspace.open(page, "RAG chatbot app");
|
41 |
-
await ws.expectErrorFree();
|
42 |
-
});
|
43 |
-
|
44 |
test("Airlines demo", async ({ page }) => {
|
45 |
const ws = await Workspace.open(page, "Airlines demo");
|
46 |
await ws.expectErrorFree(process.env.CI ? 10000 : 500);
|
|
|
17 |
await ws.expectErrorFree();
|
18 |
});
|
19 |
|
20 |
+
test("AIMO example", async ({ page }) => {
|
|
|
21 |
const ws = await Workspace.open(page, "AIMO");
|
22 |
await ws.expectErrorFree();
|
23 |
});
|
24 |
|
25 |
+
test("LynxScribe example", async ({ page }) => {
|
26 |
// Fails because of missing OPENAI_API_KEY
|
27 |
const ws = await Workspace.open(page, "LynxScribe demo");
|
28 |
await ws.expectErrorFree();
|
29 |
});
|
30 |
|
31 |
+
test("Graph RAG", async ({ page }) => {
|
32 |
// Fails due to some issue with ChromaDB
|
33 |
const ws = await Workspace.open(page, "Graph RAG");
|
34 |
await ws.expectErrorFree(process.env.CI ? 2000 : 500);
|
35 |
});
|
36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
test("Airlines demo", async ({ page }) => {
|
38 |
const ws = await Workspace.open(page, "Airlines demo");
|
39 |
await ws.expectErrorFree(process.env.CI ? 10000 : 500);
|
lynxkite-app/web/tests/graph_creation.spec.ts
CHANGED
@@ -9,9 +9,13 @@ test.beforeEach(async ({ browser }) => {
|
|
9 |
await browser.newPage(),
|
10 |
"graph_creation_spec_test",
|
11 |
);
|
12 |
-
await workspace.addBox("
|
|
|
|
|
|
|
|
|
13 |
await workspace.addBox("Create graph");
|
14 |
-
await workspace.connectBoxes("
|
15 |
});
|
16 |
|
17 |
test.afterEach(async () => {
|
|
|
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 |
});
|
20 |
|
21 |
test.afterEach(async () => {
|
lynxkite-app/web/tests/lynxkite.ts
CHANGED
@@ -59,21 +59,23 @@ export class Workspace {
|
|
59 |
// Avoid overlapping with existing nodes
|
60 |
const numNodes = allBoxes.length || 1;
|
61 |
await this.page.mouse.wheel(0, numNodes * 400);
|
62 |
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
63 |
}
|
64 |
|
65 |
-
// Some x,y offset, otherwise the box handle may fall outside the viewport.
|
66 |
await this.page.locator(".ws-name").click();
|
67 |
await this.page.keyboard.press("/");
|
68 |
-
await this.page
|
|
|
|
|
|
|
69 |
await expect(this.getBoxes()).toHaveCount(allBoxes.length + 1);
|
70 |
}
|
71 |
|
72 |
async getCatalog() {
|
73 |
-
await this.page.locator(".
|
74 |
-
|
75 |
-
|
76 |
-
|
|
|
77 |
// Dismiss the catalog menu
|
78 |
await this.page.keyboard.press("Escape");
|
79 |
await expect(this.page.locator(".node-search")).not.toBeVisible();
|
@@ -150,11 +152,6 @@ export class Workspace {
|
|
150 |
}
|
151 |
|
152 |
async expectErrorFree(executionWaitTime?) {
|
153 |
-
// TODO: Workaround, to account for workspace execution. Once
|
154 |
-
// we have a load indicator we can use that instead.
|
155 |
-
await new Promise((resolve) =>
|
156 |
-
setTimeout(resolve, executionWaitTime ? executionWaitTime : 500),
|
157 |
-
);
|
158 |
await expect(this.getBoxes().locator(".error").first()).not.toBeVisible();
|
159 |
}
|
160 |
|
@@ -185,7 +182,7 @@ export class Splash {
|
|
185 |
}
|
186 |
|
187 |
workspace(name: string) {
|
188 |
-
return this.page.getByRole("link", { name: name });
|
189 |
}
|
190 |
|
191 |
getEntry(name: string) {
|
|
|
59 |
// Avoid overlapping with existing nodes
|
60 |
const numNodes = allBoxes.length || 1;
|
61 |
await this.page.mouse.wheel(0, numNodes * 400);
|
|
|
62 |
}
|
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 |
|
73 |
async getCatalog() {
|
74 |
+
await this.page.locator(".ws-name").click();
|
75 |
+
await this.page.keyboard.press("/");
|
76 |
+
const results = this.page.locator(".node-search .matches .search-result");
|
77 |
+
await expect(results.first()).toBeVisible();
|
78 |
+
const catalog = await results.allInnerTexts();
|
79 |
// Dismiss the catalog menu
|
80 |
await this.page.keyboard.press("Escape");
|
81 |
await expect(this.page.locator(".node-search")).not.toBeVisible();
|
|
|
152 |
}
|
153 |
|
154 |
async expectErrorFree(executionWaitTime?) {
|
|
|
|
|
|
|
|
|
|
|
155 |
await expect(this.getBoxes().locator(".error").first()).not.toBeVisible();
|
156 |
}
|
157 |
|
|
|
182 |
}
|
183 |
|
184 |
workspace(name: string) {
|
185 |
+
return this.page.getByRole("link", { name: name, exact: true });
|
186 |
}
|
187 |
|
188 |
getEntry(name: string) {
|
lynxkite-bio/src/lynxkite_bio/__init__.py
CHANGED
@@ -10,7 +10,7 @@ import rdkit.Chem.rdFingerprintGenerator
|
|
10 |
import rdkit.Chem.Fingerprints.ClusterMols
|
11 |
import scipy
|
12 |
|
13 |
-
mem = joblib.Memory("
|
14 |
ENV = "LynxKite Graph Analytics"
|
15 |
op = ops.op_registration(ENV)
|
16 |
|
|
|
10 |
import rdkit.Chem.Fingerprints.ClusterMols
|
11 |
import scipy
|
12 |
|
13 |
+
mem = joblib.Memory("joblib-cache")
|
14 |
ENV = "LynxKite Graph Analytics"
|
15 |
op = ops.op_registration(ENV)
|
16 |
|
lynxkite-core/src/lynxkite/core/executors/one_by_one.py
CHANGED
@@ -142,12 +142,12 @@ async def execute(ws: workspace.Workspace, catalog, cache=None):
|
|
142 |
key = make_cache_key((inputs, params))
|
143 |
if key not in cache:
|
144 |
result: ops.Result = op(*inputs, **params)
|
145 |
-
output = await await_if_needed(result.output)
|
146 |
-
cache[key] =
|
147 |
-
|
148 |
else:
|
149 |
result = op(*inputs, **params)
|
150 |
-
|
151 |
except Exception as e:
|
152 |
traceback.print_exc()
|
153 |
node.publish_error(e)
|
|
|
142 |
key = make_cache_key((inputs, params))
|
143 |
if key not in cache:
|
144 |
result: ops.Result = op(*inputs, **params)
|
145 |
+
result.output = await await_if_needed(result.output)
|
146 |
+
cache[key] = result
|
147 |
+
result = cache[key]
|
148 |
else:
|
149 |
result = op(*inputs, **params)
|
150 |
+
output = await await_if_needed(result.output)
|
151 |
except Exception as e:
|
152 |
traceback.print_exc()
|
153 |
node.publish_error(e)
|
lynxkite-core/src/lynxkite/core/ops.py
CHANGED
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
4 |
import enum
|
5 |
import functools
|
6 |
import inspect
|
|
|
7 |
import pydantic
|
8 |
import typing
|
9 |
from dataclasses import dataclass
|
@@ -123,6 +124,25 @@ def basic_outputs(*names):
|
|
123 |
return {name: Output(name=name, type=None) for name in names}
|
124 |
|
125 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
126 |
class Op(BaseConfig):
|
127 |
func: typing.Callable = pydantic.Field(exclude=True)
|
128 |
name: str
|
@@ -136,12 +156,7 @@ class Op(BaseConfig):
|
|
136 |
# Convert parameters.
|
137 |
for p in params:
|
138 |
if p in self.params:
|
139 |
-
|
140 |
-
params[p] = int(params[p])
|
141 |
-
elif self.params[p].type is float:
|
142 |
-
params[p] = float(params[p])
|
143 |
-
elif isinstance(self.params[p].type, enum.EnumMeta):
|
144 |
-
params[p] = self.params[p].type[params[p]]
|
145 |
res = self.func(*inputs, **params)
|
146 |
if not isinstance(res, Result):
|
147 |
# Automatically wrap the result in a Result object, if it isn't already.
|
|
|
4 |
import enum
|
5 |
import functools
|
6 |
import inspect
|
7 |
+
import types
|
8 |
import pydantic
|
9 |
import typing
|
10 |
from dataclasses import dataclass
|
|
|
124 |
return {name: Output(name=name, type=None) for name in names}
|
125 |
|
126 |
|
127 |
+
def _param_to_type(name, value, type):
|
128 |
+
value = value or ""
|
129 |
+
if type is int:
|
130 |
+
assert value != "", f"{name} is unset."
|
131 |
+
return int(value)
|
132 |
+
if type is float:
|
133 |
+
assert value != "", f"{name} is unset."
|
134 |
+
return float(value)
|
135 |
+
if isinstance(type, enum.EnumMeta):
|
136 |
+
return type[value]
|
137 |
+
if isinstance(type, types.UnionType):
|
138 |
+
match type.__args__:
|
139 |
+
case (types.NoneType, type):
|
140 |
+
return None if value == "" else _param_to_type(name, value, type)
|
141 |
+
case (type, types.NoneType):
|
142 |
+
return None if value == "" else _param_to_type(name, value, type)
|
143 |
+
return value
|
144 |
+
|
145 |
+
|
146 |
class Op(BaseConfig):
|
147 |
func: typing.Callable = pydantic.Field(exclude=True)
|
148 |
name: str
|
|
|
156 |
# Convert parameters.
|
157 |
for p in params:
|
158 |
if p in self.params:
|
159 |
+
params[p] = _param_to_type(p, params[p], self.params[p].type)
|
|
|
|
|
|
|
|
|
|
|
160 |
res = self.func(*inputs, **params)
|
161 |
if not isinstance(res, Result):
|
162 |
# Automatically wrap the result in a Result object, if it isn't already.
|
lynxkite-core/src/lynxkite/core/workspace.py
CHANGED
@@ -92,6 +92,7 @@ class Workspace(BaseConfig):
|
|
92 |
env: str = ""
|
93 |
nodes: list[WorkspaceNode] = dataclasses.field(default_factory=list)
|
94 |
edges: list[WorkspaceEdge] = dataclasses.field(default_factory=list)
|
|
|
95 |
|
96 |
|
97 |
async def execute(ws: Workspace):
|
@@ -104,7 +105,8 @@ def save(ws: Workspace, path: str):
|
|
104 |
j = ws.model_dump()
|
105 |
j = json.dumps(j, indent=2, sort_keys=True) + "\n"
|
106 |
dirname, basename = os.path.split(path)
|
107 |
-
|
|
|
108 |
# Create temp file in the same directory to make sure it's on the same filesystem.
|
109 |
with tempfile.NamedTemporaryFile(
|
110 |
"w", prefix=f".{basename}.", dir=dirname, delete=False
|
|
|
92 |
env: str = ""
|
93 |
nodes: list[WorkspaceNode] = dataclasses.field(default_factory=list)
|
94 |
edges: list[WorkspaceEdge] = dataclasses.field(default_factory=list)
|
95 |
+
_crdt: pycrdt.Map
|
96 |
|
97 |
|
98 |
async def execute(ws: Workspace):
|
|
|
105 |
j = ws.model_dump()
|
106 |
j = json.dumps(j, indent=2, sort_keys=True) + "\n"
|
107 |
dirname, basename = os.path.split(path)
|
108 |
+
if dirname:
|
109 |
+
os.makedirs(dirname, exist_ok=True)
|
110 |
# Create temp file in the same directory to make sure it's on the same filesystem.
|
111 |
with tempfile.NamedTemporaryFile(
|
112 |
"w", prefix=f".{basename}.", dir=dirname, delete=False
|
lynxkite-graph-analytics/Dockerfile.bionemo
CHANGED
@@ -15,3 +15,7 @@ RUN uv pip install -e lynxkite-core/[dev] -e lynxkite-app/[dev] -e lynxkite-grap
|
|
15 |
|
16 |
# bionemo cellxgene_census needs this version of numpy
|
17 |
RUN uv pip install numpy==1.26.4
|
|
|
|
|
|
|
|
|
|
15 |
|
16 |
# bionemo cellxgene_census needs this version of numpy
|
17 |
RUN uv pip install numpy==1.26.4
|
18 |
+
|
19 |
+
ENV LYNXKITE_DATA=examples
|
20 |
+
|
21 |
+
CMD ["lynxkite"]
|
lynxkite-graph-analytics/README.md
CHANGED
@@ -28,13 +28,12 @@ To build the image:
|
|
28 |
```bash
|
29 |
# in lynxkite-graph-analytics folder
|
30 |
$ docker build -f Dockerfile.bionemo -t lynxkite-bionemo ..
|
|
|
31 |
```
|
32 |
|
33 |
Take into account that this Dockerfile does not include the lynxkite-lynxscribe package. If you want to include it you will
|
34 |
need to set up git credentials inside the container.
|
35 |
|
36 |
-
Then, inside the image you can start LynxKite as usual.
|
37 |
-
|
38 |
If you want to do some development, then it is recommend to use the [devcontainers](https://code.visualstudio.com/docs/devcontainers/containers)
|
39 |
vscode extension. The following is a basic configuration to get started:
|
40 |
|
|
|
28 |
```bash
|
29 |
# in lynxkite-graph-analytics folder
|
30 |
$ docker build -f Dockerfile.bionemo -t lynxkite-bionemo ..
|
31 |
+
$ docker run --name bionemo -d -p8000:8000 --gpus=all lynxkite-bionemo
|
32 |
```
|
33 |
|
34 |
Take into account that this Dockerfile does not include the lynxkite-lynxscribe package. If you want to include it you will
|
35 |
need to set up git credentials inside the container.
|
36 |
|
|
|
|
|
37 |
If you want to do some development, then it is recommend to use the [devcontainers](https://code.visualstudio.com/docs/devcontainers/containers)
|
38 |
vscode extension. The following is a basic configuration to get started:
|
39 |
|
lynxkite-graph-analytics/src/lynxkite_graph_analytics/bionemo_ops.py
CHANGED
@@ -40,7 +40,7 @@ from bionemo.scdl.io.single_cell_collection import SingleCellCollection
|
|
40 |
import scanpy
|
41 |
|
42 |
|
43 |
-
mem = joblib.Memory("
|
44 |
op = ops.op_registration(core.ENV)
|
45 |
DATA_PATH = Path("/workspace")
|
46 |
|
|
|
40 |
import scanpy
|
41 |
|
42 |
|
43 |
+
mem = joblib.Memory("joblib-cache")
|
44 |
op = ops.op_registration(core.ENV)
|
45 |
DATA_PATH = Path("/workspace")
|
46 |
|
lynxkite-graph-analytics/src/lynxkite_graph_analytics/core.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1 |
"""Graph analytics executor and data types."""
|
2 |
|
3 |
-
|
|
|
4 |
import dataclasses
|
5 |
import functools
|
6 |
import networkx as nx
|
@@ -134,54 +135,73 @@ def nx_node_attribute_func(name):
|
|
134 |
return decorator
|
135 |
|
136 |
|
137 |
-
def disambiguate_edges(ws):
|
138 |
"""If an input plug is connected to multiple edges, keep only the last edge."""
|
139 |
seen = set()
|
140 |
for edge in reversed(ws.edges):
|
141 |
if (edge.target, edge.targetHandle) in seen:
|
142 |
-
ws.edges.
|
|
|
|
|
|
|
143 |
seen.add((edge.target, edge.targetHandle))
|
144 |
|
145 |
|
146 |
@ops.register_executor(ENV)
|
147 |
-
async def execute(ws):
|
148 |
catalog: dict[str, ops.Op] = ops.CATALOGS[ws.env]
|
149 |
disambiguate_edges(ws)
|
150 |
outputs = {}
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
|
|
159 |
# All inputs for this node are ready, we can compute the output.
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
185 |
|
186 |
|
187 |
def df_for_frontend(df: pd.DataFrame, limit: int) -> pd.DataFrame:
|
|
|
1 |
"""Graph analytics executor and data types."""
|
2 |
|
3 |
+
import os
|
4 |
+
from lynxkite.core import ops, workspace
|
5 |
import dataclasses
|
6 |
import functools
|
7 |
import networkx as nx
|
|
|
135 |
return decorator
|
136 |
|
137 |
|
138 |
+
def disambiguate_edges(ws: workspace.Workspace):
|
139 |
"""If an input plug is connected to multiple edges, keep only the last edge."""
|
140 |
seen = set()
|
141 |
for edge in reversed(ws.edges):
|
142 |
if (edge.target, edge.targetHandle) in seen:
|
143 |
+
i = ws.edges.index(edge)
|
144 |
+
del ws.edges[i]
|
145 |
+
if hasattr(ws, "_crdt"):
|
146 |
+
del ws._crdt["edges"][i]
|
147 |
seen.add((edge.target, edge.targetHandle))
|
148 |
|
149 |
|
150 |
@ops.register_executor(ENV)
|
151 |
+
async def execute(ws: workspace.Workspace):
|
152 |
catalog: dict[str, ops.Op] = ops.CATALOGS[ws.env]
|
153 |
disambiguate_edges(ws)
|
154 |
outputs = {}
|
155 |
+
nodes = {node.id: node for node in ws.nodes}
|
156 |
+
todo = set(nodes.keys())
|
157 |
+
progress = True
|
158 |
+
while progress:
|
159 |
+
progress = False
|
160 |
+
for id in list(todo):
|
161 |
+
node = nodes[id]
|
162 |
+
input_nodes = [edge.source for edge in ws.edges if edge.target == id]
|
163 |
+
if all(input in outputs for input in input_nodes):
|
164 |
# All inputs for this node are ready, we can compute the output.
|
165 |
+
todo.remove(id)
|
166 |
+
progress = True
|
167 |
+
_execute_node(node, ws, catalog, outputs)
|
168 |
+
|
169 |
+
|
170 |
+
def _execute_node(node, ws, catalog, outputs):
|
171 |
+
params = {**node.data.params}
|
172 |
+
op = catalog.get(node.data.title)
|
173 |
+
if not op:
|
174 |
+
node.publish_error("Operation not found in catalog")
|
175 |
+
return
|
176 |
+
node.publish_started()
|
177 |
+
input_map = {
|
178 |
+
edge.targetHandle: outputs[edge.source]
|
179 |
+
for edge in ws.edges
|
180 |
+
if edge.target == node.id
|
181 |
+
}
|
182 |
+
try:
|
183 |
+
# Convert inputs types to match operation signature.
|
184 |
+
inputs = []
|
185 |
+
for p in op.inputs.values():
|
186 |
+
if p.name not in input_map:
|
187 |
+
node.publish_error(f"Missing input: {p.name}")
|
188 |
+
return
|
189 |
+
x = input_map[p.name]
|
190 |
+
if p.type == nx.Graph and isinstance(x, Bundle):
|
191 |
+
x = x.to_nx()
|
192 |
+
elif p.type == Bundle and isinstance(x, nx.Graph):
|
193 |
+
x = Bundle.from_nx(x)
|
194 |
+
elif p.type == Bundle and isinstance(x, pd.DataFrame):
|
195 |
+
x = Bundle.from_df(x)
|
196 |
+
inputs.append(x)
|
197 |
+
result = op(*inputs, **params)
|
198 |
+
except Exception as e:
|
199 |
+
if os.environ.get("LYNXKITE_LOG_OP_ERRORS"):
|
200 |
+
traceback.print_exc()
|
201 |
+
node.publish_error(e)
|
202 |
+
return
|
203 |
+
outputs[node.id] = result.output
|
204 |
+
node.publish_result(result)
|
205 |
|
206 |
|
207 |
def df_for_frontend(df: pd.DataFrame, limit: int) -> pd.DataFrame:
|
lynxkite-graph-analytics/src/lynxkite_graph_analytics/lynxkite_ops.py
CHANGED
@@ -15,7 +15,7 @@ import polars as pl
|
|
15 |
import json
|
16 |
|
17 |
|
18 |
-
mem = joblib.Memory("
|
19 |
op = ops.op_registration(core.ENV)
|
20 |
|
21 |
|
@@ -122,25 +122,6 @@ def import_osm(*, location: str):
|
|
122 |
return ox.graph.graph_from_place(location, network_type="drive")
|
123 |
|
124 |
|
125 |
-
@op("Create scale-free graph")
|
126 |
-
def create_scale_free_graph(*, nodes: int = 10):
|
127 |
-
"""Creates a scale-free graph with the given number of nodes."""
|
128 |
-
return nx.scale_free_graph(nodes)
|
129 |
-
|
130 |
-
|
131 |
-
@op("Compute PageRank")
|
132 |
-
@core.nx_node_attribute_func("pagerank")
|
133 |
-
def compute_pagerank(graph: nx.Graph, *, damping=0.85, iterations=100):
|
134 |
-
# TODO: This requires scipy to be installed.
|
135 |
-
return nx.pagerank(graph, alpha=damping, max_iter=iterations)
|
136 |
-
|
137 |
-
|
138 |
-
@op("Compute betweenness centrality")
|
139 |
-
@core.nx_node_attribute_func("betweenness_centrality")
|
140 |
-
def compute_betweenness_centrality(graph: nx.Graph, *, k=10):
|
141 |
-
return nx.betweenness_centrality(graph, k=k)
|
142 |
-
|
143 |
-
|
144 |
@op("Discard loop edges")
|
145 |
def discard_loop_edges(graph: nx.Graph):
|
146 |
graph = graph.copy()
|
|
|
15 |
import json
|
16 |
|
17 |
|
18 |
+
mem = joblib.Memory("joblib-cache")
|
19 |
op = ops.op_registration(core.ENV)
|
20 |
|
21 |
|
|
|
122 |
return ox.graph.graph_from_place(location, network_type="drive")
|
123 |
|
124 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
125 |
@op("Discard loop edges")
|
126 |
def discard_loop_edges(graph: nx.Graph):
|
127 |
graph = graph.copy()
|
lynxkite-graph-analytics/src/lynxkite_graph_analytics/networkx_ops.py
CHANGED
@@ -1,13 +1,155 @@
|
|
1 |
"""Automatically wraps all NetworkX functions as LynxKite operations."""
|
2 |
|
|
|
|
|
3 |
from lynxkite.core import ops
|
4 |
import functools
|
5 |
import inspect
|
6 |
import networkx as nx
|
|
|
|
|
|
|
7 |
|
8 |
ENV = "LynxKite Graph Analytics"
|
9 |
|
10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
def wrapped(name: str, func):
|
12 |
@functools.wraps(func)
|
13 |
def wrapper(*args, **kwargs):
|
@@ -15,48 +157,118 @@ def wrapped(name: str, func):
|
|
15 |
if v == "None":
|
16 |
kwargs[k] = None
|
17 |
res = func(*args, **kwargs)
|
|
|
18 |
if isinstance(res, nx.Graph):
|
19 |
return res
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
|
25 |
return wrapper
|
26 |
|
27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
def register_networkx(env: str):
|
29 |
cat = ops.CATALOGS.setdefault(env, {})
|
|
|
30 |
for name, func in nx.__dict__.items():
|
31 |
if hasattr(func, "graphs"):
|
32 |
-
|
|
|
|
|
|
|
33 |
inputs = {k: ops.Input(name=k, type=nx.Graph) for k in func.graphs}
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
str(param.default)
|
38 |
-
if type(param.default) in [str, int, float]
|
39 |
-
else None,
|
40 |
-
param.annotation,
|
41 |
-
)
|
42 |
-
for name, param in sig.parameters.items()
|
43 |
-
if name not in ["G", "backend", "backend_kwargs", "create_using"]
|
44 |
-
}
|
45 |
-
for p in params.values():
|
46 |
-
if not p.type:
|
47 |
-
# Guess the type based on the name.
|
48 |
-
if len(p.name) == 1:
|
49 |
-
p.type = int
|
50 |
-
name = "NX › " + name.replace("_", " ").title()
|
51 |
op = ops.Op(
|
52 |
func=wrapped(name, func),
|
53 |
-
name=
|
54 |
params=params,
|
55 |
inputs=inputs,
|
56 |
outputs={"output": ops.Output(name="output", type=nx.Graph)},
|
57 |
type="basic",
|
58 |
)
|
59 |
-
cat[
|
|
|
|
|
60 |
|
61 |
|
62 |
register_networkx(ENV)
|
|
|
1 |
"""Automatically wraps all NetworkX functions as LynxKite operations."""
|
2 |
|
3 |
+
import collections
|
4 |
+
import types
|
5 |
from lynxkite.core import ops
|
6 |
import functools
|
7 |
import inspect
|
8 |
import networkx as nx
|
9 |
+
import re
|
10 |
+
|
11 |
+
import pandas as pd
|
12 |
|
13 |
ENV = "LynxKite Graph Analytics"
|
14 |
|
15 |
|
16 |
+
class UnsupportedParameterType(Exception):
|
17 |
+
pass
|
18 |
+
|
19 |
+
|
20 |
+
_UNSUPPORTED = object()
|
21 |
+
_SKIP = object()
|
22 |
+
|
23 |
+
|
24 |
+
def doc_to_type(name: str, type_hint: str) -> type:
|
25 |
+
type_hint = type_hint.lower()
|
26 |
+
type_hint = re.sub("[(][^)]+[)]", "", type_hint).strip().strip(".")
|
27 |
+
if " " in name or "http" in name:
|
28 |
+
return _UNSUPPORTED # Not a parameter type.
|
29 |
+
if type_hint.endswith(", optional"):
|
30 |
+
w = doc_to_type(name, type_hint.removesuffix(", optional").strip())
|
31 |
+
if w is _UNSUPPORTED:
|
32 |
+
return _SKIP
|
33 |
+
return w if w is _SKIP else w | None
|
34 |
+
if type_hint in [
|
35 |
+
"a digraph or multidigraph",
|
36 |
+
"a graph g",
|
37 |
+
"graph",
|
38 |
+
"graphs",
|
39 |
+
"networkx graph instance",
|
40 |
+
"networkx graph",
|
41 |
+
"networkx undirected graph",
|
42 |
+
"nx.graph",
|
43 |
+
"undirected graph",
|
44 |
+
"undirected networkx graph",
|
45 |
+
] or type_hint.startswith("networkx graph"):
|
46 |
+
return nx.Graph
|
47 |
+
elif type_hint in [
|
48 |
+
"digraph-like",
|
49 |
+
"digraph",
|
50 |
+
"directed graph",
|
51 |
+
"networkx digraph",
|
52 |
+
"networkx directed graph",
|
53 |
+
"nx.digraph",
|
54 |
+
]:
|
55 |
+
return nx.DiGraph
|
56 |
+
elif type_hint == "node":
|
57 |
+
return _UNSUPPORTED
|
58 |
+
elif type_hint == '"node (optional)"':
|
59 |
+
return _SKIP
|
60 |
+
elif type_hint == '"edge"':
|
61 |
+
return _UNSUPPORTED
|
62 |
+
elif type_hint == '"edge (optional)"':
|
63 |
+
return _SKIP
|
64 |
+
elif type_hint in ["class", "data type"]:
|
65 |
+
return _UNSUPPORTED
|
66 |
+
elif type_hint in ["string", "str", "node label"]:
|
67 |
+
return str
|
68 |
+
elif type_hint in ["string or none", "none or string", "string, or none"]:
|
69 |
+
return str | None
|
70 |
+
elif type_hint in ["int", "integer"]:
|
71 |
+
return int
|
72 |
+
elif type_hint in ["bool", "boolean"]:
|
73 |
+
return bool
|
74 |
+
elif type_hint == "tuple":
|
75 |
+
return _UNSUPPORTED
|
76 |
+
elif type_hint == "set":
|
77 |
+
return _UNSUPPORTED
|
78 |
+
elif type_hint == "list of floats":
|
79 |
+
return _UNSUPPORTED
|
80 |
+
elif type_hint == "list of floats or float":
|
81 |
+
return float
|
82 |
+
elif type_hint in ["dict", "dictionary"]:
|
83 |
+
return _UNSUPPORTED
|
84 |
+
elif type_hint == "scalar or dictionary":
|
85 |
+
return float
|
86 |
+
elif type_hint == "none or dict":
|
87 |
+
return _SKIP
|
88 |
+
elif type_hint in ["function", "callable"]:
|
89 |
+
return _UNSUPPORTED
|
90 |
+
elif type_hint in [
|
91 |
+
"collection",
|
92 |
+
"container of nodes",
|
93 |
+
"list of nodes",
|
94 |
+
]:
|
95 |
+
return _UNSUPPORTED
|
96 |
+
elif type_hint in [
|
97 |
+
"container",
|
98 |
+
"generator",
|
99 |
+
"iterable",
|
100 |
+
"iterator",
|
101 |
+
"list or iterable container",
|
102 |
+
"list or iterable",
|
103 |
+
"list or set",
|
104 |
+
"list or tuple",
|
105 |
+
"list",
|
106 |
+
]:
|
107 |
+
return _UNSUPPORTED
|
108 |
+
elif type_hint == "generator of sets":
|
109 |
+
return _UNSUPPORTED
|
110 |
+
elif type_hint == "dict or a set of 2 or 3 tuples":
|
111 |
+
return _UNSUPPORTED
|
112 |
+
elif type_hint == "set of 2 or 3 tuples":
|
113 |
+
return _UNSUPPORTED
|
114 |
+
elif type_hint == "none, string or function":
|
115 |
+
return str | None
|
116 |
+
elif type_hint == "string or function" and name == "weight":
|
117 |
+
return str
|
118 |
+
elif type_hint == "integer, float, or none":
|
119 |
+
return float | None
|
120 |
+
elif type_hint in [
|
121 |
+
"float",
|
122 |
+
"int or float",
|
123 |
+
"integer or float",
|
124 |
+
"integer, float",
|
125 |
+
"number",
|
126 |
+
"numeric",
|
127 |
+
"real",
|
128 |
+
"scalar",
|
129 |
+
]:
|
130 |
+
return float
|
131 |
+
elif type_hint in ["integer or none", "int or none"]:
|
132 |
+
return int | None
|
133 |
+
elif name == "seed":
|
134 |
+
return int | None
|
135 |
+
elif name == "weight":
|
136 |
+
return str
|
137 |
+
elif type_hint == "object":
|
138 |
+
return _UNSUPPORTED
|
139 |
+
return _SKIP
|
140 |
+
|
141 |
+
|
142 |
+
def types_from_doc(doc: str) -> dict[str, type]:
|
143 |
+
types = {}
|
144 |
+
for line in doc.splitlines():
|
145 |
+
if ":" in line:
|
146 |
+
a, b = line.split(":", 1)
|
147 |
+
for a in a.split(","):
|
148 |
+
a = a.strip()
|
149 |
+
types[a] = doc_to_type(a, b)
|
150 |
+
return types
|
151 |
+
|
152 |
+
|
153 |
def wrapped(name: str, func):
|
154 |
@functools.wraps(func)
|
155 |
def wrapper(*args, **kwargs):
|
|
|
157 |
if v == "None":
|
158 |
kwargs[k] = None
|
159 |
res = func(*args, **kwargs)
|
160 |
+
# Figure out what the returned value is.
|
161 |
if isinstance(res, nx.Graph):
|
162 |
return res
|
163 |
+
if isinstance(res, types.GeneratorType):
|
164 |
+
res = list(res)
|
165 |
+
if name in ["articulation_points"]:
|
166 |
+
graph = args[0].copy()
|
167 |
+
nx.set_node_attributes(graph, 0, name=name)
|
168 |
+
nx.set_node_attributes(graph, {r: 1 for r in res}, name=name)
|
169 |
+
return graph
|
170 |
+
if isinstance(res, collections.abc.Sized):
|
171 |
+
if len(res) == 0:
|
172 |
+
return pd.DataFrame()
|
173 |
+
for a in args:
|
174 |
+
if isinstance(a, nx.Graph):
|
175 |
+
if a.number_of_nodes() == len(res):
|
176 |
+
graph = a.copy()
|
177 |
+
nx.set_node_attributes(graph, values=res, name=name)
|
178 |
+
return graph
|
179 |
+
if a.number_of_edges() == len(res):
|
180 |
+
graph = a.copy()
|
181 |
+
nx.set_edge_attributes(graph, values=res, name=name)
|
182 |
+
return graph
|
183 |
+
return pd.DataFrame({name: res})
|
184 |
+
return pd.DataFrame({name: [res]})
|
185 |
|
186 |
return wrapper
|
187 |
|
188 |
|
189 |
+
def _get_params(func) -> dict | None:
|
190 |
+
sig = inspect.signature(func)
|
191 |
+
# Get types from docstring.
|
192 |
+
types = types_from_doc(func.__doc__)
|
193 |
+
# Always hide these.
|
194 |
+
for k in ["backend", "backend_kwargs", "create_using"]:
|
195 |
+
types[k] = _SKIP
|
196 |
+
# Add in types based on signature.
|
197 |
+
for k, param in sig.parameters.items():
|
198 |
+
if k in types:
|
199 |
+
continue
|
200 |
+
if param.annotation is not param.empty:
|
201 |
+
types[k] = param.annotation
|
202 |
+
if k in ["i", "j", "n"]:
|
203 |
+
types[k] = int
|
204 |
+
params = {}
|
205 |
+
for name, param in sig.parameters.items():
|
206 |
+
_type = types.get(name, _UNSUPPORTED)
|
207 |
+
if _type is _UNSUPPORTED:
|
208 |
+
raise UnsupportedParameterType(name)
|
209 |
+
if _type is _SKIP or _type in [nx.Graph, nx.DiGraph]:
|
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
|
219 |
+
|
220 |
+
|
221 |
+
_REPLACEMENTS = [
|
222 |
+
("Barabasi Albert", "Barabasi–Albert"),
|
223 |
+
("Bellman Ford", "Bellman–Ford"),
|
224 |
+
("Bethe Hessian", "Bethe–Hessian"),
|
225 |
+
("Bfs", "BFS"),
|
226 |
+
("Dag ", "DAG "),
|
227 |
+
("Dfs", "DFS"),
|
228 |
+
("Dorogovtsev Goltsev Mendes", "Dorogovtsev–Goltsev–Mendes"),
|
229 |
+
("Erdos Renyi", "Erdos–Renyi"),
|
230 |
+
("Floyd Warshall", "Floyd–Warshall"),
|
231 |
+
("Gnc", "G(n,c)"),
|
232 |
+
("Gnm", "G(n,m)"),
|
233 |
+
("Gnp", "G(n,p)"),
|
234 |
+
("Gnr", "G(n,r)"),
|
235 |
+
("Havel Hakimi", "Havel–Hakimi"),
|
236 |
+
("Hkn", "H(k,n)"),
|
237 |
+
("Hnm", "H(n,m)"),
|
238 |
+
("Kl ", "KL "),
|
239 |
+
("Moebius Kantor", "Moebius–Kantor"),
|
240 |
+
("Pagerank", "PageRank"),
|
241 |
+
("Scale Free", "Scale-Free"),
|
242 |
+
("Vf2Pp", "VF2++"),
|
243 |
+
("Watts Strogatz", "Watts–Strogatz"),
|
244 |
+
("Weisfeiler Lehman", "Weisfeiler–Lehman"),
|
245 |
+
]
|
246 |
+
|
247 |
+
|
248 |
def register_networkx(env: str):
|
249 |
cat = ops.CATALOGS.setdefault(env, {})
|
250 |
+
counter = 0
|
251 |
for name, func in nx.__dict__.items():
|
252 |
if hasattr(func, "graphs"):
|
253 |
+
try:
|
254 |
+
params = _get_params(func)
|
255 |
+
except UnsupportedParameterType:
|
256 |
+
continue
|
257 |
inputs = {k: ops.Input(name=k, type=nx.Graph) for k in func.graphs}
|
258 |
+
nicename = "NX › " + name.replace("_", " ").title()
|
259 |
+
for a, b in _REPLACEMENTS:
|
260 |
+
nicename = nicename.replace(a, b)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
261 |
op = ops.Op(
|
262 |
func=wrapped(name, func),
|
263 |
+
name=nicename,
|
264 |
params=params,
|
265 |
inputs=inputs,
|
266 |
outputs={"output": ops.Output(name="output", type=nx.Graph)},
|
267 |
type="basic",
|
268 |
)
|
269 |
+
cat[nicename] = op
|
270 |
+
counter += 1
|
271 |
+
print(f"Registered {counter} NetworkX operations.")
|
272 |
|
273 |
|
274 |
register_networkx(ENV)
|
lynxkite-graph-analytics/tests/test_lynxkite_ops.py
CHANGED
@@ -77,13 +77,13 @@ async def test_execute_operation_inputs_correct_cast():
|
|
77 |
)
|
78 |
ws.edges = [
|
79 |
workspace.WorkspaceEdge(
|
80 |
-
id="1", source="1", target="2", sourceHandle="
|
81 |
),
|
82 |
workspace.WorkspaceEdge(
|
83 |
-
id="2", source="2", target="3", sourceHandle="
|
84 |
),
|
85 |
workspace.WorkspaceEdge(
|
86 |
-
id="3", source="3", target="4", sourceHandle="
|
87 |
),
|
88 |
]
|
89 |
|
@@ -92,5 +92,73 @@ async def test_execute_operation_inputs_correct_cast():
|
|
92 |
assert all([node.data.error is None for node in ws.nodes])
|
93 |
|
94 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
95 |
if __name__ == "__main__":
|
96 |
pytest.main()
|
|
|
77 |
)
|
78 |
ws.edges = [
|
79 |
workspace.WorkspaceEdge(
|
80 |
+
id="1", source="1", target="2", sourceHandle="output", targetHandle="graph"
|
81 |
),
|
82 |
workspace.WorkspaceEdge(
|
83 |
+
id="2", source="2", target="3", sourceHandle="output", targetHandle="bundle"
|
84 |
),
|
85 |
workspace.WorkspaceEdge(
|
86 |
+
id="3", source="3", target="4", sourceHandle="output", targetHandle="bundle"
|
87 |
),
|
88 |
]
|
89 |
|
|
|
92 |
assert all([node.data.error is None for node in ws.nodes])
|
93 |
|
94 |
|
95 |
+
async def test_multiple_inputs():
|
96 |
+
"""Make sure each input goes to the right argument."""
|
97 |
+
op = ops.op_registration("test")
|
98 |
+
|
99 |
+
@op("One")
|
100 |
+
def one():
|
101 |
+
return 1
|
102 |
+
|
103 |
+
@op("Two")
|
104 |
+
def two():
|
105 |
+
return 2
|
106 |
+
|
107 |
+
@op("Smaller?", view="visualization")
|
108 |
+
def is_smaller(a, b):
|
109 |
+
return a < b
|
110 |
+
|
111 |
+
ws = workspace.Workspace(env="test")
|
112 |
+
ws.nodes.append(
|
113 |
+
workspace.WorkspaceNode(
|
114 |
+
id="one",
|
115 |
+
type="cool",
|
116 |
+
data=workspace.WorkspaceNodeData(title="One", params={}),
|
117 |
+
position=workspace.Position(x=0, y=0),
|
118 |
+
)
|
119 |
+
)
|
120 |
+
ws.nodes.append(
|
121 |
+
workspace.WorkspaceNode(
|
122 |
+
id="two",
|
123 |
+
type="cool",
|
124 |
+
data=workspace.WorkspaceNodeData(title="Two", params={}),
|
125 |
+
position=workspace.Position(x=100, y=0),
|
126 |
+
)
|
127 |
+
)
|
128 |
+
ws.nodes.append(
|
129 |
+
workspace.WorkspaceNode(
|
130 |
+
id="smaller",
|
131 |
+
type="cool",
|
132 |
+
data=workspace.WorkspaceNodeData(title="Smaller?", params={}),
|
133 |
+
position=workspace.Position(x=200, y=0),
|
134 |
+
)
|
135 |
+
)
|
136 |
+
ws.edges = [
|
137 |
+
workspace.WorkspaceEdge(
|
138 |
+
id="one",
|
139 |
+
source="one",
|
140 |
+
target="smaller",
|
141 |
+
sourceHandle="output",
|
142 |
+
targetHandle="a",
|
143 |
+
),
|
144 |
+
workspace.WorkspaceEdge(
|
145 |
+
id="two",
|
146 |
+
source="two",
|
147 |
+
target="smaller",
|
148 |
+
sourceHandle="output",
|
149 |
+
targetHandle="b",
|
150 |
+
),
|
151 |
+
]
|
152 |
+
|
153 |
+
await execute(ws)
|
154 |
+
|
155 |
+
assert ws.nodes[-1].data.display is True
|
156 |
+
# Flip the inputs.
|
157 |
+
ws.edges[0].targetHandle = "b"
|
158 |
+
ws.edges[1].targetHandle = "a"
|
159 |
+
await execute(ws)
|
160 |
+
assert ws.nodes[-1].data.display is False
|
161 |
+
|
162 |
+
|
163 |
if __name__ == "__main__":
|
164 |
pytest.main()
|
lynxkite-lynxscribe/README.md
CHANGED
@@ -5,7 +5,7 @@ LynxKite UI for building LynxScribe chat applications. Also runs the chat applic
|
|
5 |
To run a chat UI for LynxScribe workspaces:
|
6 |
|
7 |
```bash
|
8 |
-
WEBUI_AUTH=false OPENAI_API_BASE_URL=http://localhost:8000/api/service/
|
9 |
```
|
10 |
|
11 |
Or use [Lynx WebUI](https://github.com/biggraph/lynx-webui/) instead of Open WebUI.
|
|
|
5 |
To run a chat UI for LynxScribe workspaces:
|
6 |
|
7 |
```bash
|
8 |
+
WEBUI_AUTH=false OPENAI_API_BASE_URL=http://localhost:8000/api/service/lynxkite_lynxscribe uvx open-webui serve
|
9 |
```
|
10 |
|
11 |
Or use [Lynx WebUI](https://github.com/biggraph/lynx-webui/) instead of Open WebUI.
|
lynxkite-lynxscribe/src/lynxkite_lynxscribe/lynxscribe_ops.py
CHANGED
@@ -9,6 +9,7 @@ import pandas as pd
|
|
9 |
import os
|
10 |
import joblib
|
11 |
|
|
|
12 |
from lynxscribe.core.llm.base import get_llm_engine
|
13 |
from lynxscribe.core.vector_store.base import get_vector_store
|
14 |
from lynxscribe.common.config import load_config
|
@@ -483,13 +484,12 @@ def view(input):
|
|
483 |
return v
|
484 |
|
485 |
|
486 |
-
async def get_chat_api(ws):
|
487 |
-
import pathlib
|
488 |
from lynxkite.core import workspace
|
489 |
|
490 |
-
|
491 |
-
path =
|
492 |
-
assert path.is_relative_to(
|
493 |
assert path.exists(), f"Workspace {path} does not exist"
|
494 |
ws = workspace.load(path)
|
495 |
contexts = await ops.EXECUTORS[ENV](ws)
|
@@ -549,17 +549,15 @@ async def api_service_get(request):
|
|
549 |
|
550 |
|
551 |
def get_lynxscribe_workspaces():
|
552 |
-
import pathlib
|
553 |
from lynxkite.core import workspace
|
554 |
|
555 |
-
DATA_DIR = pathlib.Path.cwd() / "data"
|
556 |
workspaces = []
|
557 |
-
for p in
|
558 |
if p.is_file():
|
559 |
try:
|
560 |
ws = workspace.load(p)
|
561 |
if ws.env == ENV:
|
562 |
-
workspaces.append(p
|
563 |
except Exception:
|
564 |
pass # Ignore files that are not valid workspaces.
|
565 |
workspaces.sort()
|
|
|
9 |
import os
|
10 |
import joblib
|
11 |
|
12 |
+
import pathlib
|
13 |
from lynxscribe.core.llm.base import get_llm_engine
|
14 |
from lynxscribe.core.vector_store.base import get_vector_store
|
15 |
from lynxscribe.common.config import load_config
|
|
|
484 |
return v
|
485 |
|
486 |
|
487 |
+
async def get_chat_api(ws: str):
|
|
|
488 |
from lynxkite.core import workspace
|
489 |
|
490 |
+
cwd = pathlib.Path()
|
491 |
+
path = cwd / ws
|
492 |
+
assert path.is_relative_to(cwd)
|
493 |
assert path.exists(), f"Workspace {path} does not exist"
|
494 |
ws = workspace.load(path)
|
495 |
contexts = await ops.EXECUTORS[ENV](ws)
|
|
|
549 |
|
550 |
|
551 |
def get_lynxscribe_workspaces():
|
|
|
552 |
from lynxkite.core import workspace
|
553 |
|
|
|
554 |
workspaces = []
|
555 |
+
for p in pathlib.Path().glob("**/*"):
|
556 |
if p.is_file():
|
557 |
try:
|
558 |
ws = workspace.load(p)
|
559 |
if ws.env == ENV:
|
560 |
+
workspaces.append(p)
|
561 |
except Exception:
|
562 |
pass # Ignore files that are not valid workspaces.
|
563 |
workspaces.sort()
|