Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
# GraphLink

![License](https://img.shields.io/badge/License-MIT-green)
![Python](https://img.shields.io/badge/Python-3.10%2B-blue)
![Python](https://img.shields.io/badge/Python-3.12%2B-blue)
![Stack](https://img.shields.io/badge/Stack-FastAPI%20%2B%20React-blue)
![Local First](https://img.shields.io/badge/Local--First-AI%20Workspace-orange)
![GitHub stars](https://img.shields.io/github/stars/dovvnloading/Graphlink?style=social)

<img alt="Graphlink canvas - one question branched into two parallel lines of inquiry, each with its own web research node" src="assets/screenshots/canvas-branching.png" />
<img alt="Graphlink canvas - one question branched into two answers inside a frame, one continuing into a Python sandbox, beside a finished build and the chart it produced" src="assets/screenshots/canvas-branching.png" />

**A local-first, graph-based AI workspace for branching reasoning, tool use, and multi-provider workflows.**

Expand Down Expand Up @@ -49,6 +49,7 @@ It is built with a Python (FastAPI) backend and a Vite/React/TypeScript single-p
- **Multiple model backends** — run locally with Ollama or direct GGUF loading via `llama-cpp-python`, or switch to API Endpoint mode for OpenAI-compatible, Anthropic Claude, or Google Gemini. The active mode is switchable in Settings.
- **Per-branch model routing** — pin a specific model to a node or a whole branch, on top of the per-task model defaults.
- **Plugin nodes** — attach specialist nodes for web research, code execution, drafting, and repository-aware changes (see [Plugins](#plugins)), plus a plugin SDK with per-plugin capability grants and optional MCP server integration.
- **The workspace agent** — point it at a folder and give it a task; it reads and writes files there, runs shell commands and Python, and asks before anything that changes your machine. Bounded by a per-task turn budget, and it reads an `AGENTS.md` if the workspace has one.
- **Repository-aware editing** — Gitlink loads a GitHub repo into structured context, previews file-level changes, and only writes after explicit approval.
- **Knowledge base and search** — ingest documents into a local knowledge store, search it from a node, and search across every workspace at once.
- **Workspaces and library** — organize graphs into workspaces with favorites, tags, and archiving; reopen any of them from the Library.
Expand All @@ -60,21 +61,21 @@ It is built with a Python (FastAPI) backend and a Vite/React/TypeScript single-p
- **Diagnostics** — a token and cost counter, a command palette, and an exportable diagnostic bundle for troubleshooting.
- **Export** — save the whole canvas as a PNG, or export individual nodes: Chat as Markdown, Code as a source file (extension inferred from language), Image as PNG.

Built-in node kinds on the graph surface: **Chat**, **Code**, **Document**, **Thinking**, **HTML**, **Image**, **Conversation**, **Web Research**, **Plan** (the Builder's checklist), **Artifact**, **Gitlink**, **Code Sandbox**, **Note**, and **Chart** — plus Frames, Containers, and Navigation Pins for organizing them.
Built-in node kinds on the graph surface: **Chat**, **Code**, **Document**, **Thinking**, **HTML**, **Image**, **Conversation**, **Web Research**, **Plan** (the Builder's checklist), **Agent** (the workspace agent's card), **Artifact**, **Gitlink**, **Code Sandbox**, **Note**, and **Chart** — plus Frames, Containers, and Navigation Pins for organizing them.

## Screenshots

**A build, on the canvas.** The plan node holds the checklist, the live budget counters, and an activity log of every tool call the run made — next to the nodes it actually created.

<img alt="A finished Builder run: plan node with checklist, budgets and activity log, beside the Py-Coder node and chart it produced" src="assets/screenshots/builder-run.png" />
<img alt="A finished Builder run: plan node with its checklist, spent budgets and activity log, beside the chart it produced" src="assets/screenshots/builder-run.png" />

**Launching a build.** Pick a recipe (its steps are previewed before you commit), choose how much oversight you want, and set the budgets.

<img alt="The Builder launch dialog showing a recipe with its steps previewed, co-pilot/autopilot oversight modes, and budget presets" src="assets/screenshots/builder-launcher.png" />

**Code and charts inline.** Code Sandbox runs Python in a per-node virtualenv with declared dependencies; any node's content can become a chart.
**Code and charts inline.** The Virtual Environment Runner keeps the requirements, the code and the terminal output on one node; any node's content can become a chart.

<img alt="A chat question feeding a Py-Coder node with code and output, feeding a bar chart" src="assets/screenshots/code-and-charts.png" />
<img alt="A Virtual Environment Runner node with its requirements, code and terminal output, beside the chart built from its results" src="assets/screenshots/code-and-charts.png" />

## The Builder

Expand Down Expand Up @@ -107,7 +108,7 @@ Attach these specialist nodes to a branch from the plugin picker:

### Requirements

- Python 3.10 or newer. Windows is the primary development target today.
- Python 3.12 or newer. Windows is the primary development target today.
- Node.js 22 or newer (`web_ui/.nvmrc` pins 24, the exact version this project is developed against), needed only to build the frontend once.
- Internet access is optional, and only needed for API Endpoint mode, GitHub-backed plugins, and web research.

Expand Down Expand Up @@ -222,7 +223,7 @@ Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, dev

| Symptom | Things to check |
| --- | --- |
| App does not start | Dependencies installed from `requirements.txt`; the frontend is built (`web_ui/dist/app/index.html` exists — run `cd web_ui && npm run build` if not); launched with `python graphlink_desktop.py` from the repo root; Python 3.10+. |
| App does not start | Dependencies installed from `requirements.txt`; the frontend is built (`web_ui/dist/app/index.html` exists — run `cd web_ui && npm run build` if not); launched with `python graphlink_desktop.py` from the repo root; Python 3.12+. |
| Ollama features fail | Ollama installed and running; the selected model has been pulled and exists locally. |
| Llama.cpp features fail | `llama-cpp-python` installed; the configured path points to a real `.gguf`; try a `chat_format` override or lower runtime settings. Use Ollama or API mode for image/audio. |
| API mode fails | API key present; base URL correct for OpenAI-compatible mode; the selected models exist on the endpoint. |
Expand Down
Binary file modified assets/screenshots/builder-launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/screenshots/builder-run.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/screenshots/canvas-branching.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/screenshots/code-and-charts.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions backend/domain/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -2320,8 +2320,8 @@ def _node_wire(self, n: SceneNode) -> dict[str, Any]:
"chartType": n.state.chart_type if isinstance(n.state, ChartState) else "",
"chartData": dict(n.state.chart_data) if isinstance(n.state, ChartState) else {},
"chartError": n.state.chart_error if isinstance(n.state, ChartState) else "",
"chartWidth": n.state.chart_width if isinstance(n.state, ChartState) else 680.0,
"chartHeight": n.state.chart_height if isinstance(n.state, ChartState) else 500.0,
"chartWidth": n.state.chart_width if isinstance(n.state, ChartState) else 480.0,
"chartHeight": n.state.chart_height if isinstance(n.state, ChartState) else 340.0,
"chartAspectLocked": n.state.chart_aspect_locked if isinstance(n.state, ChartState) else True,
"chartSourceNodeId": n.state.chart_source_node_id if isinstance(n.state, ChartState) else "",
# R6.3: HTML splitter + chat scroll gaps.
Expand Down
4 changes: 2 additions & 2 deletions backend/domain/node_states.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,8 @@ class ChartState(NodeState):
chart_type: str = ""
chart_data: dict = field(default_factory=dict)
chart_error: str = ""
chart_width: float = 680.0
chart_height: float = 500.0
chart_width: float = 480.0
chart_height: float = 340.0
chart_aspect_locked: bool = True
chart_source_node_id: str = ""

Expand Down
6 changes: 3 additions & 3 deletions backend/tests/test_canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -5745,7 +5745,7 @@ def test_add_chart_node_creates_a_chart_connected_to_its_parent():
assert chart.state.chart_type == "bar"
assert chart.state.chart_data == _CHART_DATA
assert chart.state.chart_source_node_id == parent.id
assert chart.state.chart_width == 680.0 and chart.state.chart_height == 500.0
assert chart.state.chart_width == 480.0 and chart.state.chart_height == 340.0
assert chart.state.chart_aspect_locked is True
assert any(e.source == parent.id and e.target == chart.id for e in doc.edges.values())

Expand Down Expand Up @@ -5833,8 +5833,8 @@ def test_scene_payload_exposes_all_chart_fields():
assert row["chartType"] == "bar"
assert row["chartData"] == _CHART_DATA
assert row["chartError"] == "degraded"
assert row["chartWidth"] == 680.0
assert row["chartHeight"] == 500.0
assert row["chartWidth"] == 480.0
assert row["chartHeight"] == 340.0
assert row["chartAspectLocked"] is True
assert row["chartSourceNodeId"] == parent.id

Expand Down
4 changes: 2 additions & 2 deletions contracts/graphlink_scene_payload.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,8 +552,8 @@ class SceneNodeRow:
chartType: str = ""
chartData: ChartDataRow = field(default_factory=ChartDataRow)
chartError: str = ""
chartWidth: float = 680.0
chartHeight: float = 500.0
chartWidth: float = 480.0
chartHeight: float = 340.0
chartAspectLocked: bool = True
chartSourceNodeId: str = ""
# R6.3: the Source/Preview splitter position (html) and chat scroll
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -323,8 +323,10 @@ ignore_missing_imports = true
# - backend/tests/perf/measure_baselines.py ... `python -m ...` perf measurement CLI (ADR-019 stage 19.1)
# - backend/tests/perf/check_baseline.py ..... nightly regression checker, its stdout IS the CI job log (ADR-019 stage 19.3)
# - tools/build_app_icon.py ........... one-off dev icon builder
# - tools/seed_demo_graph.py .......... builds the README screenshots' fixture; its stdout confirms what it wrote
"contracts/codegen.py" = ["T201"]
"backend/evals/__main__.py" = ["T201"]
"backend/tests/perf/measure_baselines.py" = ["T201"]
"backend/tests/perf/check_baseline.py" = ["T201"]
"tools/build_app_icon.py" = ["T201"]
"tools/seed_demo_graph.py" = ["T201"]
4 changes: 2 additions & 2 deletions tests/test_node_state_migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,8 +432,8 @@ def test_scene_payload_key_set_is_unchanged_by_the_migration():
"chartType": "",
"chartData": {},
"chartError": "",
"chartWidth": 680.0,
"chartHeight": 500.0,
"chartWidth": 480.0,
"chartHeight": 340.0,
"chartAspectLocked": True,
"chartSourceNodeId": "",
"isLocked": True,
Expand Down
51 changes: 51 additions & 0 deletions tools/capture_backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Boots the real backend for tools/capture_screenshots.mjs.

A sibling of tests_e2e/run_backend.py with two differences, both so the
screenshots are reproducible rather than dependent on whoever is running
them:

* the port and data directory are arguments, so the capture script owns
the temp dir it seeded a chats.db into (tools/seed_demo_graph.py) and
can hand the same one here;
* nothing else. This is create_app() exactly as shipped, serving the real
built SPA out of web_ui/dist/app - a screenshot of a mocked app would be
worth nothing.

auth_token=None disables the /api and /ws capability token, the same
"expected only in tests" posture run_backend.py documents: there is no
desktop shell here to mint one, and this binds to loopback only.

python tools/capture_backend.py <port> <data-dir>
"""

from __future__ import annotations

import sys
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT))


def main() -> None:
if len(sys.argv) != 3:
raise SystemExit("usage: python tools/capture_backend.py <port> <data-dir>")
port = int(sys.argv[1])
data_dir = Path(sys.argv[2])
data_dir.mkdir(parents=True, exist_ok=True)

import uvicorn

from backend.app import create_app

app = create_app(
spa_dir=REPO_ROOT / "web_ui" / "dist" / "app",
settings_state_file=data_dir / "session.dat",
chat_db_path=data_dir / "chats.db",
auth_token=None,
)
uvicorn.run(app, host="127.0.0.1", port=port, log_level="warning")


if __name__ == "__main__":
main()
Loading
Loading