Spaces:
Running
Running
Merge pull request #69 from biggraph/darabos-plugin-by-naming
Browse files- .github/workflows/test.yaml +6 -6
- README.md +2 -3
- {lynxkite-app/data β examples}/AIMO +0 -0
- {lynxkite-app/data β examples}/Graph RAG +1 -1
- {lynxkite-app/data β examples}/Image processing +0 -0
- {lynxkite-app/data β examples}/LynxScribe demo +0 -0
- {lynxkite-app/data β examples}/NetworkX demo +0 -0
- {lynxkite-app/data β examples}/PyTorch demo +0 -0
- {lynxkite-app/data β examples}/RAG chatbot app +0 -0
- {lynxkite-app/data β examples}/aimo-examples.csv +0 -0
- {lynxkite-app/data β examples}/example-pizza.md +0 -0
- {lynxkite-app/data β examples}/night demo +0 -0
- {lynxkite-app/data β examples}/sql +0 -0
- lynxkite-app/README.md +1 -1
- lynxkite-app/pyproject.toml +4 -4
- lynxkite-app/src/build_frontend.py +1 -1
- lynxkite-app/src/{lynxkite/app β lynxkite_app}/__init__.py +0 -0
- lynxkite-app/src/{lynxkite/app β lynxkite_app}/__main__.py +1 -1
- lynxkite-app/src/lynxkite_app/config.py +8 -0
- lynxkite-app/src/{lynxkite/app β lynxkite_app}/crdt.py +7 -8
- lynxkite-app/src/{lynxkite/app β lynxkite_app}/main.py +27 -38
- lynxkite-app/src/{lynxkite/app β lynxkite_app}/web_assets/__init__.py +0 -0
- lynxkite-app/src/{lynxkite/app β lynxkite_app}/web_assets/assets/__init__.py +0 -0
- lynxkite-app/tests/test_crdt.py +1 -1
- lynxkite-app/tests/test_main.py +6 -5
- lynxkite-app/web/playwright.config.ts +1 -1
- lynxkite-app/web/src/Directory.tsx +43 -43
- lynxkite-app/web/src/index.css +0 -1
- lynxkite-core/src/lynxkite/core/ops.py +2 -0
- lynxkite-core/src/lynxkite/core/workspace.py +1 -0
- lynxkite-graph-analytics/pyproject.toml +3 -3
- lynxkite-graph-analytics/src/lynxkite_graph_analytics/__init__.py +3 -0
- lynxkite-graph-analytics/src/{lynxkite_plugins/graph_analytics β lynxkite_graph_analytics}/lynxkite_ops.py +0 -0
- lynxkite-graph-analytics/src/{lynxkite_plugins/graph_analytics β lynxkite_graph_analytics}/networkx_ops.py +0 -0
- lynxkite-graph-analytics/src/{lynxkite_plugins/graph_analytics β lynxkite_graph_analytics}/pytorch_model_ops.py +0 -0
- lynxkite-graph-analytics/src/lynxkite_plugins/graph_analytics/__init__.py +0 -3
- lynxkite-graph-analytics/tests/test_lynxkite_ops.py +1 -1
- lynxkite-lynxscribe/README.md +1 -1
- lynxkite-lynxscribe/src/{lynxkite_plugins/lynxscribe β lynxkite_lynxscribe}/__init__.py +2 -2
- lynxkite-lynxscribe/src/{lynxkite_plugins/lynxscribe β lynxkite_lynxscribe}/llm_ops.py +0 -0
- lynxkite-lynxscribe/src/{lynxkite_plugins/lynxscribe β lynxkite_lynxscribe}/lynxscribe_ops.py +0 -0
- lynxkite-pillow-example/pyproject.toml +2 -2
- lynxkite-pillow-example/src/{lynxkite_plugins/pillow_example β lynxkite_pillow_example}/__init__.py +0 -0
.github/workflows/test.yaml
CHANGED
@@ -10,7 +10,7 @@ jobs:
|
|
10 |
runs-on: ubuntu-latest
|
11 |
steps:
|
12 |
- uses: actions/checkout@v4
|
13 |
-
|
14 |
- name: Install uv
|
15 |
uses: astral-sh/setup-uv@v5
|
16 |
with:
|
@@ -37,8 +37,8 @@ jobs:
|
|
37 |
run: |
|
38 |
cd lynxkite-app
|
39 |
pytest
|
40 |
-
|
41 |
-
- name: Run graph analytics tests
|
42 |
run: |
|
43 |
cd lynxkite-graph-analytics
|
44 |
pytest
|
@@ -51,17 +51,17 @@ jobs:
|
|
51 |
run: |
|
52 |
cd lynxkite-app/web
|
53 |
npm i
|
54 |
-
npx playwright install --with-deps
|
55 |
|
56 |
- name: Run Playwright tests
|
57 |
run: |
|
58 |
cd lynxkite-app/web
|
59 |
npm run test
|
60 |
-
|
61 |
- uses: actions/upload-artifact@v4
|
62 |
name: Upload playwright report
|
63 |
if: ${{ !cancelled() }}
|
64 |
with:
|
65 |
name: playwright-report
|
66 |
path: lynxkite-app/web/playwright-report/
|
67 |
-
retention-days: 30
|
|
|
10 |
runs-on: ubuntu-latest
|
11 |
steps:
|
12 |
- uses: actions/checkout@v4
|
13 |
+
|
14 |
- name: Install uv
|
15 |
uses: astral-sh/setup-uv@v5
|
16 |
with:
|
|
|
37 |
run: |
|
38 |
cd lynxkite-app
|
39 |
pytest
|
40 |
+
|
41 |
+
- name: Run graph analytics tests
|
42 |
run: |
|
43 |
cd lynxkite-graph-analytics
|
44 |
pytest
|
|
|
51 |
run: |
|
52 |
cd lynxkite-app/web
|
53 |
npm i
|
54 |
+
npx playwright install --with-deps
|
55 |
|
56 |
- name: Run Playwright tests
|
57 |
run: |
|
58 |
cd lynxkite-app/web
|
59 |
npm run test
|
60 |
+
|
61 |
- uses: actions/upload-artifact@v4
|
62 |
name: Upload playwright report
|
63 |
if: ${{ !cancelled() }}
|
64 |
with:
|
65 |
name: playwright-report
|
66 |
path: lynxkite-app/web/playwright-report/
|
67 |
+
retention-days: 30
|
README.md
CHANGED
@@ -24,14 +24,13 @@ Install everything like this:
|
|
24 |
uv venv
|
25 |
source .venv/bin/activate
|
26 |
# The [dev] tag is only needed if you intend on running tests
|
27 |
-
uv pip install -e lynxkite-core/[dev] lynxkite-app/[dev] lynxkite-graph-analytics/[dev] lynxkite-lynxscribe/ lynxkite-pillow-example/
|
28 |
```
|
29 |
|
30 |
This also builds the frontend, hopefully very quickly. To run it:
|
31 |
|
32 |
```bash
|
33 |
-
|
34 |
-
LYNXKITE_RELOAD=1 lynxkite
|
35 |
```
|
36 |
|
37 |
If you also want to make changes to the frontend with hot reloading:
|
|
|
24 |
uv venv
|
25 |
source .venv/bin/activate
|
26 |
# The [dev] tag is only needed if you intend on running tests
|
27 |
+
uv pip install -e lynxkite-core/[dev] -e lynxkite-app/[dev] -e lynxkite-graph-analytics/[dev] -e lynxkite-lynxscribe/ -e lynxkite-pillow-example/
|
28 |
```
|
29 |
|
30 |
This also builds the frontend, hopefully very quickly. To run it:
|
31 |
|
32 |
```bash
|
33 |
+
LYNXKITE_DATA=examples LYNXKITE_RELOAD=1 lynxkite
|
|
|
34 |
```
|
35 |
|
36 |
If you also want to make changes to the frontend with hot reloading:
|
{lynxkite-app/data β examples}/AIMO
RENAMED
File without changes
|
{lynxkite-app/data β examples}/Graph RAG
RENAMED
@@ -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": "examples/example-pizza.md"
|
11 |
},
|
12 |
"display": null,
|
13 |
"error": null,
|
{lynxkite-app/data β examples}/Image processing
RENAMED
File without changes
|
{lynxkite-app/data β examples}/LynxScribe demo
RENAMED
File without changes
|
{lynxkite-app/data β examples}/NetworkX demo
RENAMED
File without changes
|
{lynxkite-app/data β examples}/PyTorch demo
RENAMED
File without changes
|
{lynxkite-app/data β examples}/RAG chatbot app
RENAMED
File without changes
|
{lynxkite-app/data β examples}/aimo-examples.csv
RENAMED
File without changes
|
{lynxkite-app/data β examples}/example-pizza.md
RENAMED
File without changes
|
{lynxkite-app/data β examples}/night demo
RENAMED
File without changes
|
{lynxkite-app/data β examples}/sql
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 |
-
LYNXKITE_RELOAD=1 lynxkite
|
17 |
```
|
18 |
|
19 |
To run the frontend:
|
|
|
13 |
|
14 |
```bash
|
15 |
uv pip install -e .
|
16 |
+
LYNXKITE_DATA=../examples LYNXKITE_RELOAD=1 lynxkite
|
17 |
```
|
18 |
|
19 |
To run the frontend:
|
lynxkite-app/pyproject.toml
CHANGED
@@ -15,7 +15,7 @@ dependencies = [
|
|
15 |
|
16 |
[project.optional-dependencies]
|
17 |
dev = [
|
18 |
-
"pytest",
|
19 |
]
|
20 |
|
21 |
[tool.uv.sources]
|
@@ -30,8 +30,8 @@ namespaces = true
|
|
30 |
where = ["src"]
|
31 |
|
32 |
[tool.setuptools.package-data]
|
33 |
-
"
|
34 |
-
"
|
35 |
|
36 |
[tool.setuptools]
|
37 |
py-modules = ["build_frontend"]
|
@@ -41,4 +41,4 @@ include-package-data = true
|
|
41 |
build_py = "build_frontend.build_py"
|
42 |
|
43 |
[project.scripts]
|
44 |
-
lynxkite = "
|
|
|
15 |
|
16 |
[project.optional-dependencies]
|
17 |
dev = [
|
18 |
+
"pytest>=8.3.4",
|
19 |
]
|
20 |
|
21 |
[tool.uv.sources]
|
|
|
30 |
where = ["src"]
|
31 |
|
32 |
[tool.setuptools.package-data]
|
33 |
+
"lynxkite_app.web_assets" = ["*"]
|
34 |
+
"lynxkite_app.web_assets.assets" = ["*"]
|
35 |
|
36 |
[tool.setuptools]
|
37 |
py-modules = ["build_frontend"]
|
|
|
41 |
build_py = "build_frontend.build_py"
|
42 |
|
43 |
[project.scripts]
|
44 |
+
lynxkite = "lynxkite_app.__main__:main"
|
lynxkite-app/src/build_frontend.py
CHANGED
@@ -11,7 +11,7 @@ class build_py(_build_py):
|
|
11 |
print("\n\nBuilding frontend...", __file__)
|
12 |
here = Path(__file__).parent.parent
|
13 |
frontend_dir = here / "web"
|
14 |
-
package_dir = here / "src" / "
|
15 |
subprocess.check_call(["npm", "install"], cwd=frontend_dir)
|
16 |
subprocess.check_call(["npm", "run", "build"], cwd=frontend_dir)
|
17 |
print("files in", frontend_dir / "dist")
|
|
|
11 |
print("\n\nBuilding frontend...", __file__)
|
12 |
here = Path(__file__).parent.parent
|
13 |
frontend_dir = here / "web"
|
14 |
+
package_dir = here / "src" / "lynxkite_app" / "web_assets"
|
15 |
subprocess.check_call(["npm", "install"], cwd=frontend_dir)
|
16 |
subprocess.check_call(["npm", "run", "build"], cwd=frontend_dir)
|
17 |
print("files in", frontend_dir / "dist")
|
lynxkite-app/src/{lynxkite/app β lynxkite_app}/__init__.py
RENAMED
File without changes
|
lynxkite-app/src/{lynxkite/app β lynxkite_app}/__main__.py
RENAMED
@@ -6,7 +6,7 @@ import os
|
|
6 |
def main():
|
7 |
port = int(os.environ.get("PORT", "8000"))
|
8 |
reload = bool(os.environ.get("LYNXKITE_RELOAD", ""))
|
9 |
-
uvicorn.run("
|
10 |
|
11 |
|
12 |
if __name__ == "__main__":
|
|
|
6 |
def main():
|
7 |
port = int(os.environ.get("PORT", "8000"))
|
8 |
reload = bool(os.environ.get("LYNXKITE_RELOAD", ""))
|
9 |
+
uvicorn.run("lynxkite_app.main:app", host="0.0.0.0", port=port, reload=reload)
|
10 |
|
11 |
|
12 |
if __name__ == "__main__":
|
lynxkite-app/src/lynxkite_app/config.py
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 β lynxkite_app}/crdt.py
RENAMED
@@ -12,10 +12,9 @@ import pycrdt_websocket.ystore
|
|
12 |
import uvicorn
|
13 |
import builtins
|
14 |
from lynxkite.core import workspace, ops
|
|
|
15 |
|
16 |
router = fastapi.APIRouter()
|
17 |
-
DATA_PATH = pathlib.Path.cwd() / "data"
|
18 |
-
CRDT_PATH = pathlib.Path.cwd() / "crdt_data"
|
19 |
|
20 |
|
21 |
def ws_exception_handler(exception, log):
|
@@ -34,8 +33,8 @@ class WebsocketServer(pycrdt_websocket.WebsocketServer):
|
|
34 |
|
35 |
The workspace is loaded from "crdt_data" if it exists there, or from "data", or a new workspace is created.
|
36 |
"""
|
37 |
-
path = CRDT_PATH / f"{name}.crdt"
|
38 |
-
assert path.is_relative_to(CRDT_PATH)
|
39 |
ystore = pycrdt_websocket.ystore.FileYStore(path)
|
40 |
ydoc = pycrdt.Doc()
|
41 |
ydoc["workspace"] = ws = pycrdt.Map()
|
@@ -52,7 +51,7 @@ class WebsocketServer(pycrdt_websocket.WebsocketServer):
|
|
52 |
if "edges" not in ws:
|
53 |
ws["edges"] = pycrdt.Array()
|
54 |
if "env" not in ws:
|
55 |
-
ws["env"] = next(iter(ops.CATALOGS),
|
56 |
# We have two possible sources of truth for the workspaces, the YStore and the JSON files.
|
57 |
# In case we didn't find the workspace in the YStore, we try to load it from the JSON files.
|
58 |
try_to_load_workspace(ws, name)
|
@@ -162,7 +161,7 @@ def try_to_load_workspace(ws: pycrdt.Map, name: str):
|
|
162 |
ws: CRDT object to udpate with the workspace contents.
|
163 |
name: Name of the workspace to load.
|
164 |
"""
|
165 |
-
json_path = f"
|
166 |
if os.path.exists(json_path):
|
167 |
ws_pyd = workspace.load(json_path)
|
168 |
# We treat the display field as a black box, since it is a large
|
@@ -222,8 +221,8 @@ async def execute(
|
|
222 |
await asyncio.sleep(delay)
|
223 |
except asyncio.CancelledError:
|
224 |
return
|
225 |
-
path = DATA_PATH / name
|
226 |
-
assert path.is_relative_to(DATA_PATH),"Provided workspace path is invalid"
|
227 |
# Save user changes before executing, in case the execution fails.
|
228 |
workspace.save(ws_pyd, path)
|
229 |
await workspace.execute(ws_pyd)
|
|
|
12 |
import uvicorn
|
13 |
import builtins
|
14 |
from lynxkite.core import workspace, ops
|
15 |
+
from . import config
|
16 |
|
17 |
router = fastapi.APIRouter()
|
|
|
|
|
18 |
|
19 |
|
20 |
def ws_exception_handler(exception, log):
|
|
|
33 |
|
34 |
The workspace is loaded from "crdt_data" if it exists there, or from "data", or a new workspace is created.
|
35 |
"""
|
36 |
+
path = config.CRDT_PATH / f"{name}.crdt"
|
37 |
+
assert path.is_relative_to(config.CRDT_PATH)
|
38 |
ystore = pycrdt_websocket.ystore.FileYStore(path)
|
39 |
ydoc = pycrdt.Doc()
|
40 |
ydoc["workspace"] = ws = pycrdt.Map()
|
|
|
51 |
if "edges" not in ws:
|
52 |
ws["edges"] = pycrdt.Array()
|
53 |
if "env" not in ws:
|
54 |
+
ws["env"] = next(iter(ops.CATALOGS), "unset")
|
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)
|
|
|
161 |
ws: CRDT object to udpate with the workspace contents.
|
162 |
name: Name of the workspace to load.
|
163 |
"""
|
164 |
+
json_path = f"{config.DATA_PATH}/{name}"
|
165 |
if os.path.exists(json_path):
|
166 |
ws_pyd = workspace.load(json_path)
|
167 |
# We treat the display field as a black box, since it is a large
|
|
|
221 |
await asyncio.sleep(delay)
|
222 |
except asyncio.CancelledError:
|
223 |
return
|
224 |
+
path = config.DATA_PATH / name
|
225 |
+
assert path.is_relative_to(config.DATA_PATH), "Provided workspace path is invalid"
|
226 |
# Save user changes before executing, in case the execution fails.
|
227 |
workspace.save(ws_pyd, path)
|
228 |
await workspace.execute(ws_pyd)
|
lynxkite-app/src/{lynxkite/app β lynxkite_app}/main.py
RENAMED
@@ -1,5 +1,8 @@
|
|
|
|
|
|
1 |
import os
|
2 |
import shutil
|
|
|
3 |
if os.environ.get("NX_CUGRAPH_AUTOCONFIG", "").strip().lower() == "true":
|
4 |
import cudf.pandas
|
5 |
|
@@ -13,23 +16,17 @@ from fastapi.staticfiles import StaticFiles
|
|
13 |
import starlette
|
14 |
from lynxkite.core import ops
|
15 |
from lynxkite.core import workspace
|
16 |
-
from . import crdt
|
17 |
|
18 |
|
19 |
def detect_plugins():
|
20 |
-
try:
|
21 |
-
import lynxkite_plugins
|
22 |
-
except ImportError:
|
23 |
-
print("No modules found in lynxkite_plugins. Be sure to install some plugins.")
|
24 |
-
return {}
|
25 |
-
|
26 |
plugins = {}
|
27 |
-
for _, name, _ in pkgutil.iter_modules(
|
28 |
-
name
|
29 |
-
|
30 |
-
|
31 |
if not plugins:
|
32 |
-
print("No
|
33 |
return plugins
|
34 |
|
35 |
|
@@ -53,8 +50,8 @@ class SaveRequest(workspace.BaseConfig):
|
|
53 |
|
54 |
|
55 |
def save(req: SaveRequest):
|
56 |
-
path = DATA_PATH / req.path
|
57 |
-
assert path.is_relative_to(DATA_PATH)
|
58 |
workspace.save(req.ws, path)
|
59 |
|
60 |
|
@@ -68,26 +65,23 @@ async def save_and_execute(req: SaveRequest):
|
|
68 |
|
69 |
@app.post("/api/delete")
|
70 |
async def delete_workspace(req: dict):
|
71 |
-
json_path: pathlib.Path = DATA_PATH / req["path"]
|
72 |
-
crdt_path: pathlib.Path = CRDT_PATH / f"{req[
|
73 |
-
assert json_path.is_relative_to(DATA_PATH)
|
74 |
-
assert crdt_path.is_relative_to(CRDT_PATH)
|
75 |
json_path.unlink()
|
76 |
crdt_path.unlink()
|
77 |
|
78 |
|
79 |
@app.get("/api/load")
|
80 |
def load(path: str):
|
81 |
-
path = DATA_PATH / path
|
82 |
-
assert path.is_relative_to(DATA_PATH)
|
83 |
if not path.exists():
|
84 |
return workspace.Workspace()
|
85 |
return workspace.load(path)
|
86 |
|
87 |
|
88 |
-
DATA_PATH = pathlib.Path.cwd() / "data"
|
89 |
-
CRDT_PATH = pathlib.Path.cwd() / "crdt_data"
|
90 |
-
|
91 |
@dataclasses.dataclass(order=True)
|
92 |
class DirectoryEntry:
|
93 |
name: str
|
@@ -96,12 +90,13 @@ class DirectoryEntry:
|
|
96 |
|
97 |
@app.get("/api/dir/list")
|
98 |
def list_dir(path: str):
|
99 |
-
path = DATA_PATH / path
|
100 |
-
assert path.is_relative_to(DATA_PATH)
|
101 |
return sorted(
|
102 |
[
|
103 |
DirectoryEntry(
|
104 |
-
p.relative_to(DATA_PATH),
|
|
|
105 |
)
|
106 |
for p in path.iterdir()
|
107 |
]
|
@@ -110,23 +105,17 @@ def list_dir(path: str):
|
|
110 |
|
111 |
@app.post("/api/dir/mkdir")
|
112 |
def make_dir(req: dict):
|
113 |
-
path = DATA_PATH / req["path"]
|
114 |
-
assert path.is_relative_to(DATA_PATH)
|
115 |
-
assert not path.exists()
|
116 |
path.mkdir()
|
117 |
-
return list_dir(path.parent)
|
118 |
|
119 |
|
120 |
@app.post("/api/dir/delete")
|
121 |
def delete_dir(req: dict):
|
122 |
-
path: pathlib.Path = DATA_PATH / req["path"]
|
123 |
-
assert all([
|
124 |
-
path.is_relative_to(DATA_PATH),
|
125 |
-
path.exists(),
|
126 |
-
path.is_dir()
|
127 |
-
])
|
128 |
shutil.rmtree(path)
|
129 |
-
return list_dir(path.parent)
|
130 |
|
131 |
|
132 |
@app.get("/api/service/{module_path:path}")
|
@@ -159,5 +148,5 @@ class SPAStaticFiles(StaticFiles):
|
|
159 |
raise ex
|
160 |
|
161 |
|
162 |
-
static_dir = SPAStaticFiles(packages=[("
|
163 |
app.mount("/", static_dir, name="web_assets")
|
|
|
1 |
+
"""The FastAPI server for serving the LynxKite application."""
|
2 |
+
|
3 |
import os
|
4 |
import shutil
|
5 |
+
|
6 |
if os.environ.get("NX_CUGRAPH_AUTOCONFIG", "").strip().lower() == "true":
|
7 |
import cudf.pandas
|
8 |
|
|
|
16 |
import starlette
|
17 |
from lynxkite.core import ops
|
18 |
from lynxkite.core import workspace
|
19 |
+
from . import crdt, config
|
20 |
|
21 |
|
22 |
def detect_plugins():
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
plugins = {}
|
24 |
+
for _, name, _ in pkgutil.iter_modules():
|
25 |
+
if name.startswith("lynxkite_"):
|
26 |
+
print(f"Importing {name}")
|
27 |
+
plugins[name] = importlib.import_module(name)
|
28 |
if not plugins:
|
29 |
+
print("No LynxKite plugins found. Be sure to install some!")
|
30 |
return plugins
|
31 |
|
32 |
|
|
|
50 |
|
51 |
|
52 |
def save(req: SaveRequest):
|
53 |
+
path = config.DATA_PATH / req.path
|
54 |
+
assert path.is_relative_to(config.DATA_PATH)
|
55 |
workspace.save(req.ws, path)
|
56 |
|
57 |
|
|
|
65 |
|
66 |
@app.post("/api/delete")
|
67 |
async def delete_workspace(req: dict):
|
68 |
+
json_path: pathlib.Path = config.DATA_PATH / req["path"]
|
69 |
+
crdt_path: pathlib.Path = config.CRDT_PATH / f"{req['path']}.crdt"
|
70 |
+
assert json_path.is_relative_to(config.DATA_PATH)
|
71 |
+
assert crdt_path.is_relative_to(config.CRDT_PATH)
|
72 |
json_path.unlink()
|
73 |
crdt_path.unlink()
|
74 |
|
75 |
|
76 |
@app.get("/api/load")
|
77 |
def load(path: str):
|
78 |
+
path = config.DATA_PATH / path
|
79 |
+
assert path.is_relative_to(config.DATA_PATH)
|
80 |
if not path.exists():
|
81 |
return workspace.Workspace()
|
82 |
return workspace.load(path)
|
83 |
|
84 |
|
|
|
|
|
|
|
85 |
@dataclasses.dataclass(order=True)
|
86 |
class DirectoryEntry:
|
87 |
name: str
|
|
|
90 |
|
91 |
@app.get("/api/dir/list")
|
92 |
def list_dir(path: str):
|
93 |
+
path = config.DATA_PATH / path
|
94 |
+
assert path.is_relative_to(config.DATA_PATH)
|
95 |
return sorted(
|
96 |
[
|
97 |
DirectoryEntry(
|
98 |
+
p.relative_to(config.DATA_PATH),
|
99 |
+
"directory" if p.is_dir() else "workspace",
|
100 |
)
|
101 |
for p in path.iterdir()
|
102 |
]
|
|
|
105 |
|
106 |
@app.post("/api/dir/mkdir")
|
107 |
def make_dir(req: dict):
|
108 |
+
path = config.DATA_PATH / req["path"]
|
109 |
+
assert path.is_relative_to(config.DATA_PATH)
|
110 |
+
assert not path.exists(), f"{path} already exists"
|
111 |
path.mkdir()
|
|
|
112 |
|
113 |
|
114 |
@app.post("/api/dir/delete")
|
115 |
def delete_dir(req: dict):
|
116 |
+
path: pathlib.Path = config.DATA_PATH / req["path"]
|
117 |
+
assert all([path.is_relative_to(config.DATA_PATH), path.exists(), path.is_dir()])
|
|
|
|
|
|
|
|
|
118 |
shutil.rmtree(path)
|
|
|
119 |
|
120 |
|
121 |
@app.get("/api/service/{module_path:path}")
|
|
|
148 |
raise ex
|
149 |
|
150 |
|
151 |
+
static_dir = SPAStaticFiles(packages=[("lynxkite_app", "web_assets")], html=True)
|
152 |
app.mount("/", static_dir, name="web_assets")
|
lynxkite-app/src/{lynxkite/app β lynxkite_app}/web_assets/__init__.py
RENAMED
File without changes
|
lynxkite-app/src/{lynxkite/app β lynxkite_app}/web_assets/assets/__init__.py
RENAMED
File without changes
|
lynxkite-app/tests/test_crdt.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1 |
from enum import Enum
|
2 |
import pycrdt
|
3 |
import pytest
|
4 |
-
from
|
5 |
|
6 |
|
7 |
@pytest.fixture
|
|
|
1 |
from enum import Enum
|
2 |
import pycrdt
|
3 |
import pytest
|
4 |
+
from lynxkite_app.crdt import crdt_update
|
5 |
|
6 |
|
7 |
@pytest.fixture
|
lynxkite-app/tests/test_main.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1 |
import uuid
|
2 |
from fastapi.testclient import TestClient
|
3 |
-
from
|
|
|
4 |
import os
|
5 |
|
6 |
|
@@ -13,9 +14,9 @@ def test_detect_plugins_with_plugins():
|
|
13 |
assert all(
|
14 |
plugin in plugins.keys()
|
15 |
for plugin in [
|
16 |
-
"
|
17 |
-
"
|
18 |
-
"
|
19 |
]
|
20 |
)
|
21 |
|
@@ -57,7 +58,7 @@ def test_save_and_load():
|
|
57 |
def test_list_dir():
|
58 |
test_dir = str(uuid.uuid4())
|
59 |
test_dir_full_path = DATA_PATH / test_dir
|
60 |
-
test_dir_full_path.mkdir(exist_ok=True)
|
61 |
test_file = test_dir_full_path / "test_file.txt"
|
62 |
test_file.touch()
|
63 |
response = client.get(f"/api/dir/list?path={str(test_dir)}")
|
|
|
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 |
|
|
|
14 |
assert all(
|
15 |
plugin in plugins.keys()
|
16 |
for plugin in [
|
17 |
+
"lynxkite_graph_analytics",
|
18 |
+
"lynxkite_lynxscribe",
|
19 |
+
"lynxkite_pillow_example",
|
20 |
]
|
21 |
)
|
22 |
|
|
|
58 |
def test_list_dir():
|
59 |
test_dir = str(uuid.uuid4())
|
60 |
test_dir_full_path = DATA_PATH / test_dir
|
61 |
+
test_dir_full_path.mkdir(parents=True, exist_ok=True)
|
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)}")
|
lynxkite-app/web/playwright.config.ts
CHANGED
@@ -24,7 +24,7 @@ export default defineConfig({
|
|
24 |
},
|
25 |
],
|
26 |
webServer: {
|
27 |
-
command: 'cd
|
28 |
url: 'http://127.0.0.1:8000',
|
29 |
reuseExistingServer: false,
|
30 |
},
|
|
|
24 |
},
|
25 |
],
|
26 |
webServer: {
|
27 |
+
command: 'cd ../.. && LYNXKITE_DATA=examples lynxkite',
|
28 |
url: 'http://127.0.0.1:8000',
|
29 |
reuseExistingServer: false,
|
30 |
},
|
lynxkite-app/web/src/Directory.tsx
CHANGED
@@ -29,8 +29,8 @@ export default function () {
|
|
29 |
const navigate = useNavigate();
|
30 |
const [isCreatingDir, setIsCreatingDir] = useState(false);
|
31 |
const [isCreatingWorkspace, setIsCreatingWorkspace] = useState(false);
|
32 |
-
|
33 |
-
|
34 |
function link(item: any) {
|
35 |
if (item.type === 'directory') {
|
36 |
return `/dir/${item.name}`;
|
@@ -38,7 +38,7 @@ export default function () {
|
|
38 |
return `/edit/${item.name}`;
|
39 |
}
|
40 |
}
|
41 |
-
|
42 |
function shortName(item: any) {
|
43 |
return item.name.split('/').pop();
|
44 |
}
|
@@ -53,18 +53,18 @@ export default function () {
|
|
53 |
i++;
|
54 |
}
|
55 |
}
|
56 |
-
|
57 |
function newWorkspaceIn(path: string, list: any[], workspaceName?: string) {
|
58 |
const pathSlash = path ? `${path}/` : "";
|
59 |
const name = workspaceName || newName(list);
|
60 |
-
navigate(`/edit/${pathSlash}${name}`, {replace: true});
|
61 |
}
|
62 |
-
|
63 |
|
64 |
async function newFolderIn(path: string, list: any[], folderName?: string) {
|
65 |
const name = folderName || newName(list, "New Folder");
|
66 |
const pathSlash = path ? `${path}/` : "";
|
67 |
-
|
68 |
const res = await fetch(`/api/dir/mkdir`, {
|
69 |
method: 'POST',
|
70 |
headers: { 'Content-Type': 'application/json' },
|
@@ -76,12 +76,12 @@ export default function () {
|
|
76 |
alert("Failed to create folder.");
|
77 |
}
|
78 |
}
|
79 |
-
|
80 |
async function deleteItem(item: any) {
|
81 |
if (!window.confirm(`Are you sure you want to delete "${item.name}"?`)) return;
|
82 |
const pathSlash = path ? `${path}/` : "";
|
83 |
|
84 |
-
const apiPath = item.type === "directory" ? `/api/dir/delete
|
85 |
await fetch(apiPath, {
|
86 |
method: "POST",
|
87 |
headers: { "Content-Type": "application/json" },
|
@@ -109,39 +109,39 @@ export default function () {
|
|
109 |
{list.data && (
|
110 |
<>
|
111 |
<div className="actions">
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
</div>
|
146 |
|
147 |
{path && (
|
@@ -155,7 +155,7 @@ export default function () {
|
|
155 |
|
156 |
{list.data.map((item: any) => (
|
157 |
<div key={item.name} className="entry">
|
158 |
-
<a key={link(item)}
|
159 |
{item.type === 'directory' ? <Folder /> : <File />}
|
160 |
{shortName(item)}
|
161 |
</a>
|
|
|
29 |
const navigate = useNavigate();
|
30 |
const [isCreatingDir, setIsCreatingDir] = useState(false);
|
31 |
const [isCreatingWorkspace, setIsCreatingWorkspace] = useState(false);
|
32 |
+
|
33 |
+
|
34 |
function link(item: any) {
|
35 |
if (item.type === 'directory') {
|
36 |
return `/dir/${item.name}`;
|
|
|
38 |
return `/edit/${item.name}`;
|
39 |
}
|
40 |
}
|
41 |
+
|
42 |
function shortName(item: any) {
|
43 |
return item.name.split('/').pop();
|
44 |
}
|
|
|
53 |
i++;
|
54 |
}
|
55 |
}
|
56 |
+
|
57 |
function newWorkspaceIn(path: string, list: any[], workspaceName?: string) {
|
58 |
const pathSlash = path ? `${path}/` : "";
|
59 |
const name = workspaceName || newName(list);
|
60 |
+
navigate(`/edit/${pathSlash}${name}`, { replace: true });
|
61 |
}
|
62 |
+
|
63 |
|
64 |
async function newFolderIn(path: string, list: any[], folderName?: string) {
|
65 |
const name = folderName || newName(list, "New Folder");
|
66 |
const pathSlash = path ? `${path}/` : "";
|
67 |
+
|
68 |
const res = await fetch(`/api/dir/mkdir`, {
|
69 |
method: 'POST',
|
70 |
headers: { 'Content-Type': 'application/json' },
|
|
|
76 |
alert("Failed to create folder.");
|
77 |
}
|
78 |
}
|
79 |
+
|
80 |
async function deleteItem(item: any) {
|
81 |
if (!window.confirm(`Are you sure you want to delete "${item.name}"?`)) return;
|
82 |
const pathSlash = path ? `${path}/` : "";
|
83 |
|
84 |
+
const apiPath = item.type === "directory" ? `/api/dir/delete` : `/api/delete`;
|
85 |
await fetch(apiPath, {
|
86 |
method: "POST",
|
87 |
headers: { "Content-Type": "application/json" },
|
|
|
109 |
{list.data && (
|
110 |
<>
|
111 |
<div className="actions">
|
112 |
+
<div className="new-workspace">
|
113 |
+
{isCreatingWorkspace &&
|
114 |
+
// @ts-ignore
|
115 |
+
<form onSubmit={(e) => { e.preventDefault(); newWorkspaceIn(path || "", list.data, e.target.workspaceName.value.trim()) }}>
|
116 |
+
<input
|
117 |
+
type="text"
|
118 |
+
name="workspaceName"
|
119 |
+
defaultValue={newName(list.data)}
|
120 |
+
placeholder={newName(list.data)}
|
121 |
+
/>
|
122 |
+
</form>
|
123 |
+
}
|
124 |
+
<button type="button" onClick={() => setIsCreatingWorkspace(true)}>
|
125 |
+
<FolderPlus /> New workspace
|
126 |
+
</button>
|
127 |
+
</div>
|
128 |
+
|
129 |
+
<div className="new-folder">
|
130 |
+
{isCreatingDir &&
|
131 |
+
// @ts-ignore
|
132 |
+
<form onSubmit={(e) => { e.preventDefault(); newFolderIn(path || "", list.data, e.target.folderName.value.trim()) }}>
|
133 |
+
<input
|
134 |
+
type="text"
|
135 |
+
name="folderName"
|
136 |
+
defaultValue={newName(list.data)}
|
137 |
+
placeholder={newName(list.data)}
|
138 |
+
/>
|
139 |
+
</form>
|
140 |
+
}
|
141 |
+
<button type="button" onClick={() => setIsCreatingDir(true)}>
|
142 |
+
<FolderPlus /> New folder
|
143 |
+
</button>
|
144 |
+
</div>
|
145 |
</div>
|
146 |
|
147 |
{path && (
|
|
|
155 |
|
156 |
{list.data.map((item: any) => (
|
157 |
<div key={item.name} className="entry">
|
158 |
+
<a key={link(item)} href={link(item)}>
|
159 |
{item.type === 'directory' ? <Folder /> : <File />}
|
160 |
{shortName(item)}
|
161 |
</a>
|
lynxkite-app/web/src/index.css
CHANGED
@@ -308,7 +308,6 @@ body {
|
|
308 |
}
|
309 |
|
310 |
a {
|
311 |
-
color: black;
|
312 |
text-decoration: none;
|
313 |
}
|
314 |
|
|
|
308 |
}
|
309 |
|
310 |
a {
|
|
|
311 |
text-decoration: none;
|
312 |
}
|
313 |
|
lynxkite-core/src/lynxkite/core/ops.py
CHANGED
@@ -206,8 +206,10 @@ def register_executor(env: str):
|
|
206 |
|
207 |
|
208 |
def op_registration(env: str):
|
|
|
209 |
return functools.partial(op, env)
|
210 |
|
211 |
|
212 |
def passive_op_registration(env: str):
|
|
|
213 |
return functools.partial(register_passive_op, env)
|
|
|
206 |
|
207 |
|
208 |
def op_registration(env: str):
|
209 |
+
"""Returns a decorator that can be used for registering functions as operations."""
|
210 |
return functools.partial(op, env)
|
211 |
|
212 |
|
213 |
def passive_op_registration(env: str):
|
214 |
+
"""Returns a function that can be used to register operations without associated code."""
|
215 |
return functools.partial(register_passive_op, env)
|
lynxkite-core/src/lynxkite/core/workspace.py
CHANGED
@@ -67,6 +67,7 @@ def save(ws: Workspace, path: str):
|
|
67 |
"""Persist a workspace to a local file in JSON format."""
|
68 |
j = ws.model_dump_json(indent=2)
|
69 |
dirname, basename = os.path.split(path)
|
|
|
70 |
# Create temp file in the same directory to make sure it's on the same filesystem.
|
71 |
with tempfile.NamedTemporaryFile(
|
72 |
"w", prefix=f".{basename}.", dir=dirname, delete=False
|
|
|
67 |
"""Persist a workspace to a local file in JSON format."""
|
68 |
j = ws.model_dump_json(indent=2)
|
69 |
dirname, basename = os.path.split(path)
|
70 |
+
os.makedirs(dirname, exist_ok=True)
|
71 |
# Create temp file in the same directory to make sure it's on the same filesystem.
|
72 |
with tempfile.NamedTemporaryFile(
|
73 |
"w", prefix=f".{basename}.", dir=dirname, delete=False
|
lynxkite-graph-analytics/pyproject.toml
CHANGED
@@ -17,8 +17,8 @@ dependencies = [
|
|
17 |
|
18 |
[project.optional-dependencies]
|
19 |
dev = [
|
20 |
-
"pytest",
|
21 |
-
"pytest-asyncio",
|
22 |
]
|
23 |
gpu = [
|
24 |
"nx-cugraph-cu12>=24.12.0",
|
@@ -28,4 +28,4 @@ gpu = [
|
|
28 |
lynxkite-core = { path = "../lynxkite-core" }
|
29 |
|
30 |
[tool.pytest.ini_options]
|
31 |
-
asyncio_mode = "auto"
|
|
|
17 |
|
18 |
[project.optional-dependencies]
|
19 |
dev = [
|
20 |
+
"pytest>=8.3.4",
|
21 |
+
"pytest-asyncio>=0.25.3",
|
22 |
]
|
23 |
gpu = [
|
24 |
"nx-cugraph-cu12>=24.12.0",
|
|
|
28 |
lynxkite-core = { path = "../lynxkite-core" }
|
29 |
|
30 |
[tool.pytest.ini_options]
|
31 |
+
asyncio_mode = "auto"
|
lynxkite-graph-analytics/src/lynxkite_graph_analytics/__init__.py
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
from . import lynxkite_ops # noqa (imported to trigger registration)
|
2 |
+
from . import networkx_ops # noqa (imported to trigger registration)
|
3 |
+
from . import pytorch_model_ops # noqa (imported to trigger registration)
|
lynxkite-graph-analytics/src/{lynxkite_plugins/graph_analytics β lynxkite_graph_analytics}/lynxkite_ops.py
RENAMED
File without changes
|
lynxkite-graph-analytics/src/{lynxkite_plugins/graph_analytics β lynxkite_graph_analytics}/networkx_ops.py
RENAMED
File without changes
|
lynxkite-graph-analytics/src/{lynxkite_plugins/graph_analytics β lynxkite_graph_analytics}/pytorch_model_ops.py
RENAMED
File without changes
|
lynxkite-graph-analytics/src/lynxkite_plugins/graph_analytics/__init__.py
DELETED
@@ -1,3 +0,0 @@
|
|
1 |
-
from . import lynxkite_ops
|
2 |
-
from . import networkx_ops
|
3 |
-
from . import pytorch_model_ops
|
|
|
|
|
|
|
|
lynxkite-graph-analytics/tests/test_lynxkite_ops.py
CHANGED
@@ -3,7 +3,7 @@ import pytest
|
|
3 |
import networkx as nx
|
4 |
|
5 |
from lynxkite.core import workspace
|
6 |
-
from
|
7 |
|
8 |
|
9 |
async def test_execute_operation_not_in_catalog():
|
|
|
3 |
import networkx as nx
|
4 |
|
5 |
from lynxkite.core import workspace
|
6 |
+
from lynxkite_graph_analytics.lynxkite_ops import Bundle, execute, op
|
7 |
|
8 |
|
9 |
async def test_execute_operation_not_in_catalog():
|
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/lynxscribe/lynxscribe_ops 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_plugins/lynxscribe β lynxkite_lynxscribe}/__init__.py
RENAMED
@@ -1,5 +1,5 @@
|
|
1 |
-
from . import lynxscribe_ops
|
2 |
-
from . import llm_ops
|
3 |
from .lynxscribe_ops import api_service_post, api_service_get
|
4 |
|
5 |
__all__ = ["api_service_post", "api_service_get"]
|
|
|
1 |
+
from . import lynxscribe_ops # noqa (imported to trigger registration)
|
2 |
+
from . import llm_ops # noqa (imported to trigger registration)
|
3 |
from .lynxscribe_ops import api_service_post, api_service_get
|
4 |
|
5 |
__all__ = ["api_service_post", "api_service_get"]
|
lynxkite-lynxscribe/src/{lynxkite_plugins/lynxscribe β lynxkite_lynxscribe}/llm_ops.py
RENAMED
File without changes
|
lynxkite-lynxscribe/src/{lynxkite_plugins/lynxscribe β lynxkite_lynxscribe}/lynxscribe_ops.py
RENAMED
File without changes
|
lynxkite-pillow-example/pyproject.toml
CHANGED
@@ -8,8 +8,8 @@ dependencies = [
|
|
8 |
"fsspec>=2025.2.0",
|
9 |
"lynxkite-core",
|
10 |
"pillow>=11.1.0",
|
11 |
-
"requests",
|
12 |
-
"aiohttp",
|
13 |
]
|
14 |
|
15 |
[tool.uv.sources]
|
|
|
8 |
"fsspec>=2025.2.0",
|
9 |
"lynxkite-core",
|
10 |
"pillow>=11.1.0",
|
11 |
+
"requests>=2.32.3",
|
12 |
+
"aiohttp>=3.11.11",
|
13 |
]
|
14 |
|
15 |
[tool.uv.sources]
|
lynxkite-pillow-example/src/{lynxkite_plugins/pillow_example β lynxkite_pillow_example}/__init__.py
RENAMED
File without changes
|