darabos commited on
Commit
e4ff751
·
1 Parent(s): 253206b

Use cwd instead of LYNXKITE_DATA.

Browse files
Dockerfile CHANGED
@@ -11,6 +11,6 @@ RUN uv venv && uv pip install \
11
  -e lynxkite-graph-analytics \
12
  -e lynxkite-bio \
13
  -e lynxkite-pillow-example
14
- ENV LYNXKITE_DATA=examples
15
  ENV PORT=7860
16
  CMD ["uv", "run", "lynxkite"]
 
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
@@ -41,7 +41,7 @@ uv pip install -e lynxkite-core/[dev] -e lynxkite-app/[dev] -e lynxkite-graph-an
41
  This also builds the frontend, hopefully very quickly. To run it:
42
 
43
  ```bash
44
- LYNXKITE_DATA=examples LYNXKITE_RELOAD=1 lynxkite
45
  ```
46
 
47
  If you also want to make changes to the frontend with hot reloading:
 
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": "data/aimo-examples.csv",
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/Bio demo CHANGED
@@ -7,7 +7,7 @@
7
  "data": {
8
  "title": "Import CSV",
9
  "params": {
10
- "filename": "examples/drug_target_data_sample.csv",
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/Graph RAG CHANGED
@@ -7,7 +7,7 @@
7
  "data": {
8
  "title": "Input document",
9
  "params": {
10
- "filename": "examples/example-pizza.md"
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": "/home/darabos/nvme/lynxscribe/examples/chat_api/scenarios.yaml",
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": "/home/darabos/nvme/lynxscribe/examples/chat_api/data/lynx/tempclusters.pickle",
256
- "edges_path": "/home/darabos/nvme/lynxscribe/examples/chat_api/data/lynx/edges.pickle",
257
- "nodes_path": "/home/darabos/nvme/lynxscribe/examples/chat_api/data/lynx/nodes.pickle"
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/{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
- LYNXKITE_DATA=../examples LYNXKITE_RELOAD=1 lynxkite
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/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
- path = config.CRDT_PATH / f"{name}.crdt"
36
- assert path.is_relative_to(config.CRDT_PATH)
 
37
  ystore = pycrdt_websocket.ystore.FileYStore(path)
38
  ydoc = pycrdt.Doc()
39
  ydoc["workspace"] = ws = pycrdt.Map()
@@ -165,9 +166,8 @@ def try_to_load_workspace(ws: pycrdt.Map, name: str):
165
  ws: CRDT object to udpate with the workspace contents.
166
  name: Name of the workspace to load.
167
  """
168
- json_path = f"{config.DATA_PATH}/{name}"
169
- if os.path.exists(json_path):
170
- ws_pyd = workspace.load(json_path)
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"})
@@ -225,8 +225,9 @@ async def execute(
225
  except asyncio.CancelledError:
226
  return
227
  print(f"Running {name} in {ws_pyd.env}...")
228
- path = config.DATA_PATH / name
229
- assert path.is_relative_to(config.DATA_PATH), "Provided workspace path is invalid"
 
230
  # Save user changes before executing, in case the execution fails.
231
  workspace.save(ws_pyd, path)
232
  ws_pyd._crdt = ws_crdt
 
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()
 
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"})
 
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
lynxkite-app/src/lynxkite_app/main.py CHANGED
@@ -11,7 +11,7 @@ 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, config
15
 
16
 
17
  def detect_plugins():
@@ -45,9 +45,12 @@ class SaveRequest(workspace.BaseConfig):
45
  ws: workspace.Workspace
46
 
47
 
 
 
 
48
  def save(req: SaveRequest):
49
- path = config.DATA_PATH / req.path
50
- assert path.is_relative_to(config.DATA_PATH)
51
  workspace.save(req.ws, path)
52
 
53
 
@@ -61,18 +64,17 @@ async def save_and_execute(req: SaveRequest):
61
 
62
  @app.post("/api/delete")
63
  async def delete_workspace(req: dict):
64
- json_path: pathlib.Path = config.DATA_PATH / req["path"]
65
- crdt_path: pathlib.Path = config.CRDT_PATH / f"{req['path']}.crdt"
66
- assert json_path.is_relative_to(config.DATA_PATH)
67
- assert crdt_path.is_relative_to(config.CRDT_PATH)
68
  json_path.unlink()
69
  crdt_path.unlink()
70
 
71
 
72
  @app.get("/api/load")
73
  def load(path: str):
74
- path = config.DATA_PATH / path
75
- assert path.is_relative_to(config.DATA_PATH)
76
  if not path.exists():
77
  return workspace.Workspace()
78
  return workspace.load(path)
@@ -85,15 +87,16 @@ class DirectoryEntry(pydantic.BaseModel):
85
 
86
  @app.get("/api/dir/list")
87
  def list_dir(path: str):
88
- path = config.DATA_PATH / path
89
- assert path.is_relative_to(config.DATA_PATH)
90
  return sorted(
91
  [
92
  DirectoryEntry(
93
- name=str(p.relative_to(config.DATA_PATH)),
94
  type="directory" if p.is_dir() else "workspace",
95
  )
96
  for p in path.iterdir()
 
97
  ],
98
  key=lambda x: x.name,
99
  )
@@ -101,16 +104,16 @@ def list_dir(path: str):
101
 
102
  @app.post("/api/dir/mkdir")
103
  def make_dir(req: dict):
104
- path = config.DATA_PATH / req["path"]
105
- assert path.is_relative_to(config.DATA_PATH)
106
  assert not path.exists(), f"{path} already exists"
107
  path.mkdir()
108
 
109
 
110
  @app.post("/api/dir/delete")
111
  def delete_dir(req: dict):
112
- path: pathlib.Path = config.DATA_PATH / req["path"]
113
- assert all([path.is_relative_to(config.DATA_PATH), path.exists(), path.is_dir()])
114
  shutil.rmtree(path)
115
 
116
 
@@ -133,8 +136,8 @@ async def upload(req: fastapi.Request):
133
  """Receives file uploads and stores them in DATA_PATH."""
134
  form = await req.form()
135
  for file in form.values():
136
- file_path = config.DATA_PATH / file.filename
137
- assert file_path.is_relative_to(config.DATA_PATH), "Invalid file path"
138
  with file_path.open("wb") as buffer:
139
  shutil.copyfileobj(file.file, buffer)
140
  return {"status": "ok"}
 
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():
 
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
 
 
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"}
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
- 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)}")
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
- test_dir_full_path.rmdir()
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(DATA_PATH / dir_name)
78
- os.rmdir(DATA_PATH / dir_name)
 
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/playwright.config.ts CHANGED
@@ -23,7 +23,7 @@ export default defineConfig({
23
  },
24
  ],
25
  webServer: {
26
- command: "cd ../.. && LYNXKITE_DATA=examples lynxkite",
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/workspace/Workspace.tsx CHANGED
@@ -324,7 +324,7 @@ function LynxKiteFlow() {
324
  x: e.clientX,
325
  y: e.clientY,
326
  });
327
- node.data!.params.file_path = file.name;
328
  if (file.name.includes(".csv")) {
329
  node.data!.params.file_format = "csv";
330
  } else if (file.name.includes(".parquet")) {
 
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")) {
lynxkite-core/src/lynxkite/core/workspace.py CHANGED
@@ -105,7 +105,8 @@ def save(ws: Workspace, path: str):
105
  j = ws.model_dump()
106
  j = json.dumps(j, indent=2, sort_keys=True) + "\n"
107
  dirname, basename = os.path.split(path)
108
- os.makedirs(dirname, exist_ok=True)
 
109
  # Create temp file in the same directory to make sure it's on the same filesystem.
110
  with tempfile.NamedTemporaryFile(
111
  "w", prefix=f".{basename}.", dir=dirname, delete=False
 
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-lynxscribe/src/lynxkite_lynxscribe/lynxscribe_ops.py CHANGED
@@ -2,7 +2,6 @@
2
  LynxScribe configuration and testing in LynxKite.
3
  """
4
 
5
- import os
6
  import pathlib
7
  from lynxscribe.core.llm.base import get_llm_engine
8
  from lynxscribe.core.vector_store.base import get_vector_store
@@ -222,11 +221,12 @@ def view(input):
222
  return v
223
 
224
 
225
- async def get_chat_api(ws):
226
  from lynxkite.core import workspace
227
 
228
- path = DATA_PATH / ws
229
- assert path.is_relative_to(DATA_PATH)
 
230
  assert path.exists(), f"Workspace {path} does not exist"
231
  ws = workspace.load(path)
232
  contexts = await ops.EXECUTORS[ENV](ws)
@@ -285,19 +285,16 @@ async def api_service_get(request):
285
  return {"error": "Not found"}
286
 
287
 
288
- DATA_PATH = pathlib.Path(os.environ.get("LYNXKITE_DATA", "lynxkite_data"))
289
-
290
-
291
  def get_lynxscribe_workspaces():
292
  from lynxkite.core import workspace
293
 
294
  workspaces = []
295
- for p in DATA_PATH.glob("**/*"):
296
  if p.is_file():
297
  try:
298
  ws = workspace.load(p)
299
  if ws.env == ENV:
300
- workspaces.append(p.relative_to(DATA_PATH))
301
  except Exception:
302
  pass # Ignore files that are not valid workspaces.
303
  workspaces.sort()
 
2
  LynxScribe configuration and testing in LynxKite.
3
  """
4
 
 
5
  import pathlib
6
  from lynxscribe.core.llm.base import get_llm_engine
7
  from lynxscribe.core.vector_store.base import get_vector_store
 
221
  return v
222
 
223
 
224
+ async def get_chat_api(ws: str):
225
  from lynxkite.core import workspace
226
 
227
+ cwd = pathlib.Path()
228
+ path = cwd / ws
229
+ assert path.is_relative_to(cwd)
230
  assert path.exists(), f"Workspace {path} does not exist"
231
  ws = workspace.load(path)
232
  contexts = await ops.EXECUTORS[ENV](ws)
 
285
  return {"error": "Not found"}
286
 
287
 
 
 
 
288
  def get_lynxscribe_workspaces():
289
  from lynxkite.core import workspace
290
 
291
  workspaces = []
292
+ for p in pathlib.Path().glob("**/*"):
293
  if p.is_file():
294
  try:
295
  ws = workspace.load(p)
296
  if ws.env == ENV:
297
+ workspaces.append(p)
298
  except Exception:
299
  pass # Ignore files that are not valid workspaces.
300
  workspaces.sort()