Spaces:
Running
Running
Merge pull request #75 from biggraph/darabos-ga
Browse files
.github/workflows/test.yaml
CHANGED
|
@@ -24,7 +24,20 @@ jobs:
|
|
| 24 |
run: |
|
| 25 |
eval `ssh-agent -s`
|
| 26 |
ssh-add - <<< '${{ secrets.LYNXSCRIBE_DEPLOY_KEY }}'
|
| 27 |
-
uv pip install
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
env:
|
| 29 |
UV_SYSTEM_PYTHON: 1
|
| 30 |
|
|
@@ -43,6 +56,13 @@ jobs:
|
|
| 43 |
cd lynxkite-graph-analytics
|
| 44 |
pytest
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
- uses: actions/setup-node@v4
|
| 47 |
with:
|
| 48 |
node-version: lts/*
|
|
|
|
| 24 |
run: |
|
| 25 |
eval `ssh-agent -s`
|
| 26 |
ssh-add - <<< '${{ secrets.LYNXSCRIBE_DEPLOY_KEY }}'
|
| 27 |
+
uv pip install \
|
| 28 |
+
-e lynxkite-core/[dev] \
|
| 29 |
+
-e lynxkite-app/[dev] \
|
| 30 |
+
-e lynxkite-graph-analytics/[dev] \
|
| 31 |
+
-e lynxkite-bio \
|
| 32 |
+
-e lynxkite-lynxscribe/ \
|
| 33 |
+
-e lynxkite-pillow-example/
|
| 34 |
+
env:
|
| 35 |
+
UV_SYSTEM_PYTHON: 1
|
| 36 |
+
|
| 37 |
+
- name: Run pre-commits
|
| 38 |
+
run: |
|
| 39 |
+
uv pip install pre-commit
|
| 40 |
+
pre-commit run --all-files
|
| 41 |
env:
|
| 42 |
UV_SYSTEM_PYTHON: 1
|
| 43 |
|
|
|
|
| 56 |
cd lynxkite-graph-analytics
|
| 57 |
pytest
|
| 58 |
|
| 59 |
+
- name: Try building the documentation
|
| 60 |
+
run: |
|
| 61 |
+
uv pip install mkdocs-material mkdocstrings[python]
|
| 62 |
+
mkdocs build
|
| 63 |
+
env:
|
| 64 |
+
UV_SYSTEM_PYTHON: 1
|
| 65 |
+
|
| 66 |
- uses: actions/setup-node@v4
|
| 67 |
with:
|
| 68 |
node-version: lts/*
|
.gitignore
CHANGED
|
@@ -16,3 +16,9 @@ joblib-cache
|
|
| 16 |
*.egg-info
|
| 17 |
|
| 18 |
lynxkite_crdt_data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
*.egg-info
|
| 17 |
|
| 18 |
lynxkite_crdt_data
|
| 19 |
+
|
| 20 |
+
# Playwright
|
| 21 |
+
/test-results/
|
| 22 |
+
/playwright-report/
|
| 23 |
+
/blob-report/
|
| 24 |
+
/playwright/.cache/
|
docs/lynxkite-graph-analytics.md
CHANGED
|
@@ -3,4 +3,4 @@
|
|
| 3 |
This is the classical LynxKite experience!
|
| 4 |
The graph analytics plugin is a collection of graph algorithms that can be run on a LynxKite graph.
|
| 5 |
|
| 6 |
-
:::
|
|
|
|
| 3 |
This is the classical LynxKite experience!
|
| 4 |
The graph analytics plugin is a collection of graph algorithms that can be run on a LynxKite graph.
|
| 5 |
|
| 6 |
+
::: lynxkite_graph_analytics.lynxkite_ops
|
lynxkite-app/web/src/workspace/Workspace.tsx
CHANGED
|
@@ -180,6 +180,40 @@ function LynxKiteFlow() {
|
|
| 180 |
}),
|
| 181 |
[],
|
| 182 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
const closeNodeSearch = useCallback(() => {
|
| 184 |
setNodeSearchSettings(undefined);
|
| 185 |
setSuppressSearchUntil(Date.now() + 200);
|
|
|
|
| 180 |
}),
|
| 181 |
[],
|
| 182 |
);
|
| 183 |
+
|
| 184 |
+
// Global keyboard shortcuts.
|
| 185 |
+
useEffect(() => {
|
| 186 |
+
const handleKeyDown = (event: KeyboardEvent) => {
|
| 187 |
+
// Show the node search dialog on "/".
|
| 188 |
+
if (
|
| 189 |
+
event.key === "/" &&
|
| 190 |
+
!nodeSearchSettings &&
|
| 191 |
+
!isTypingInFormElement()
|
| 192 |
+
) {
|
| 193 |
+
event.preventDefault();
|
| 194 |
+
setNodeSearchSettings({
|
| 195 |
+
pos: { x: 100, y: 100 },
|
| 196 |
+
boxes: catalog.data![state.workspace.env!],
|
| 197 |
+
});
|
| 198 |
+
}
|
| 199 |
+
};
|
| 200 |
+
// TODO: Switch to keydown once https://github.com/xyflow/xyflow/pull/5055 is merged.
|
| 201 |
+
document.addEventListener("keyup", handleKeyDown);
|
| 202 |
+
return () => {
|
| 203 |
+
document.removeEventListener("keyup", handleKeyDown);
|
| 204 |
+
};
|
| 205 |
+
}, [catalog.data, nodeSearchSettings, state.workspace.env]);
|
| 206 |
+
|
| 207 |
+
function isTypingInFormElement() {
|
| 208 |
+
const activeElement = document.activeElement;
|
| 209 |
+
return (
|
| 210 |
+
activeElement &&
|
| 211 |
+
(activeElement.tagName === "INPUT" ||
|
| 212 |
+
activeElement.tagName === "TEXTAREA" ||
|
| 213 |
+
(activeElement as HTMLElement).isContentEditable)
|
| 214 |
+
);
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
const closeNodeSearch = useCallback(() => {
|
| 218 |
setNodeSearchSettings(undefined);
|
| 219 |
setSuppressSearchUntil(Date.now() + 200);
|
lynxkite-app/web/tests/basic.spec.ts
CHANGED
|
@@ -21,6 +21,9 @@ 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 |
const catalog = await workspace.getCatalog();
|
| 25 |
expect(catalog).not.toHaveLength(0);
|
| 26 |
const op = catalog[0];
|
|
|
|
| 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];
|
lynxkite-app/web/tests/examples.spec.ts
CHANGED
|
@@ -4,49 +4,49 @@ import { Workspace } from "./lynxkite";
|
|
| 4 |
|
| 5 |
test("LynxKite Graph Analytics example", async ({ page }) => {
|
| 6 |
const ws = await Workspace.open(page, "NetworkX demo");
|
| 7 |
-
|
| 8 |
});
|
| 9 |
|
| 10 |
test("Bio example", async ({ page }) => {
|
| 11 |
const ws = await Workspace.open(page, "Bio demo");
|
| 12 |
-
|
| 13 |
});
|
| 14 |
|
| 15 |
test("Pytorch example", async ({ page }) => {
|
| 16 |
const ws = await Workspace.open(page, "PyTorch demo");
|
| 17 |
-
|
| 18 |
});
|
| 19 |
|
| 20 |
test.fail("AIMO example", async ({ page }) => {
|
| 21 |
// Fails because of missing OPENAI_API_KEY
|
| 22 |
const ws = await Workspace.open(page, "AIMO");
|
| 23 |
-
|
| 24 |
});
|
| 25 |
|
| 26 |
test.fail("LynxScribe example", async ({ page }) => {
|
| 27 |
// Fails because of missing OPENAI_API_KEY
|
| 28 |
const ws = await Workspace.open(page, "LynxScribe demo");
|
| 29 |
-
|
| 30 |
});
|
| 31 |
|
| 32 |
test.fail("Graph RAG", async ({ page }) => {
|
| 33 |
// Fails due to some issue with ChromaDB
|
| 34 |
const ws = await Workspace.open(page, "Graph RAG");
|
| 35 |
-
|
| 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 |
-
|
| 42 |
});
|
| 43 |
|
| 44 |
test("Airlines demo", async ({ page }) => {
|
| 45 |
const ws = await Workspace.open(page, "Airlines demo");
|
| 46 |
-
|
| 47 |
});
|
| 48 |
|
| 49 |
test("Pillow example", async ({ page }) => {
|
| 50 |
const ws = await Workspace.open(page, "Image processing");
|
| 51 |
-
|
| 52 |
});
|
|
|
|
| 4 |
|
| 5 |
test("LynxKite Graph Analytics example", async ({ page }) => {
|
| 6 |
const ws = await Workspace.open(page, "NetworkX demo");
|
| 7 |
+
await ws.expectErrorFree(process.env.CI ? 2000 : 1000);
|
| 8 |
});
|
| 9 |
|
| 10 |
test("Bio example", async ({ page }) => {
|
| 11 |
const ws = await Workspace.open(page, "Bio demo");
|
| 12 |
+
await ws.expectErrorFree();
|
| 13 |
});
|
| 14 |
|
| 15 |
test("Pytorch example", async ({ page }) => {
|
| 16 |
const ws = await Workspace.open(page, "PyTorch demo");
|
| 17 |
+
await ws.expectErrorFree();
|
| 18 |
});
|
| 19 |
|
| 20 |
test.fail("AIMO example", async ({ page }) => {
|
| 21 |
// Fails because of missing OPENAI_API_KEY
|
| 22 |
const ws = await Workspace.open(page, "AIMO");
|
| 23 |
+
await ws.expectErrorFree();
|
| 24 |
});
|
| 25 |
|
| 26 |
test.fail("LynxScribe example", async ({ page }) => {
|
| 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.fail("Graph RAG", async ({ page }) => {
|
| 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);
|
| 47 |
});
|
| 48 |
|
| 49 |
test("Pillow example", async ({ page }) => {
|
| 50 |
const ws = await Workspace.open(page, "Image processing");
|
| 51 |
+
await ws.expectErrorFree();
|
| 52 |
});
|
lynxkite-app/web/tests/lynxkite.ts
CHANGED
|
@@ -54,7 +54,7 @@ export class Workspace {
|
|
| 54 |
|
| 55 |
async addBox(boxName) {
|
| 56 |
//TODO: Support passing box parameters (id, position, etc.)
|
| 57 |
-
const allBoxes = await this.getBoxes();
|
| 58 |
if (allBoxes) {
|
| 59 |
// Avoid overlapping with existing nodes
|
| 60 |
const numNodes = allBoxes.length || 1;
|
|
@@ -63,13 +63,10 @@ export class Workspace {
|
|
| 63 |
}
|
| 64 |
|
| 65 |
// Some x,y offset, otherwise the box handle may fall outside the viewport.
|
| 66 |
-
await this.page
|
| 67 |
-
|
| 68 |
-
.click({ position: { x: 20, y: 20 } });
|
| 69 |
await this.page.locator(".node-search").getByText(boxName).click();
|
| 70 |
-
await this.
|
| 71 |
-
// Workaround to wait for the deselection animation after choosing a box. Otherwise, the next box will not be added.
|
| 72 |
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
| 73 |
}
|
| 74 |
|
| 75 |
async getCatalog() {
|
|
@@ -79,14 +76,22 @@ export class Workspace {
|
|
| 79 |
.allInnerTexts();
|
| 80 |
// Dismiss the catalog menu
|
| 81 |
await this.page.keyboard.press("Escape");
|
| 82 |
-
await
|
| 83 |
return catalog;
|
| 84 |
}
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
async deleteBoxes(boxIds: string[]) {
|
| 87 |
for (const boxId of boxIds) {
|
| 88 |
-
await this.
|
| 89 |
await this.page.keyboard.press("Backspace");
|
|
|
|
| 90 |
}
|
| 91 |
}
|
| 92 |
|
|
@@ -95,7 +100,7 @@ export class Workspace {
|
|
| 95 |
}
|
| 96 |
|
| 97 |
getBoxes() {
|
| 98 |
-
return this.page.locator(".react-flow__node")
|
| 99 |
}
|
| 100 |
|
| 101 |
getBoxHandle(boxId: string, pos?: string) {
|
|
@@ -144,19 +149,13 @@ export class Workspace {
|
|
| 144 |
await this.page.mouse.up();
|
| 145 |
}
|
| 146 |
|
| 147 |
-
async
|
| 148 |
// TODO: Workaround, to account for workspace execution. Once
|
| 149 |
// we have a load indicator we can use that instead.
|
| 150 |
await new Promise((resolve) =>
|
| 151 |
setTimeout(resolve, executionWaitTime ? executionWaitTime : 500),
|
| 152 |
);
|
| 153 |
-
|
| 154 |
-
for (const box of boxes) {
|
| 155 |
-
if (await box.locator(".error").isVisible()) {
|
| 156 |
-
return false;
|
| 157 |
-
}
|
| 158 |
-
}
|
| 159 |
-
return true;
|
| 160 |
}
|
| 161 |
|
| 162 |
async close() {
|
|
|
|
| 54 |
|
| 55 |
async addBox(boxName) {
|
| 56 |
//TODO: Support passing box parameters (id, position, etc.)
|
| 57 |
+
const allBoxes = await this.getBoxes().all();
|
| 58 |
if (allBoxes) {
|
| 59 |
// Avoid overlapping with existing nodes
|
| 60 |
const numNodes = allBoxes.length || 1;
|
|
|
|
| 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.locator(".node-search").getByText(boxName).click();
|
| 69 |
+
await expect(this.getBoxes()).toHaveCount(allBoxes.length + 1);
|
|
|
|
|
|
|
| 70 |
}
|
| 71 |
|
| 72 |
async getCatalog() {
|
|
|
|
| 76 |
.allInnerTexts();
|
| 77 |
// Dismiss the catalog menu
|
| 78 |
await this.page.keyboard.press("Escape");
|
| 79 |
+
await expect(this.page.locator(".node-search")).not.toBeVisible();
|
| 80 |
return catalog;
|
| 81 |
}
|
| 82 |
|
| 83 |
+
async selectBox(boxId: string) {
|
| 84 |
+
const box = this.getBox(boxId);
|
| 85 |
+
// Click on the resizer, so we don't click on any parameters by accident.
|
| 86 |
+
await box.locator(".react-flow__resize-control").click();
|
| 87 |
+
await expect(box).toHaveClass(/selected/);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
async deleteBoxes(boxIds: string[]) {
|
| 91 |
for (const boxId of boxIds) {
|
| 92 |
+
await this.selectBox(boxId);
|
| 93 |
await this.page.keyboard.press("Backspace");
|
| 94 |
+
await expect(this.getBox(boxId)).not.toBeVisible();
|
| 95 |
}
|
| 96 |
}
|
| 97 |
|
|
|
|
| 100 |
}
|
| 101 |
|
| 102 |
getBoxes() {
|
| 103 |
+
return this.page.locator(".react-flow__node");
|
| 104 |
}
|
| 105 |
|
| 106 |
getBoxHandle(boxId: string, pos?: string) {
|
|
|
|
| 149 |
await this.page.mouse.up();
|
| 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")).not.toBeVisible();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
}
|
| 160 |
|
| 161 |
async close() {
|