graphcore-studio 0.2.0__py3-none-win32.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- graphcore/__init__.py +303 -0
- graphcore/graphcore.dll +0 -0
- graphcore_studio-0.2.0.dist-info/METADATA +171 -0
- graphcore_studio-0.2.0.dist-info/RECORD +28 -0
- graphcore_studio-0.2.0.dist-info/WHEEL +5 -0
- graphcore_studio-0.2.0.dist-info/entry_points.txt +3 -0
- graphcore_studio-0.2.0.dist-info/licenses/LICENSE +21 -0
- include/graphcore/c_api.h +51 -0
- include/graphcore/graphcore.hpp +83 -0
- lib/cmake/GraphCore/GraphCoreConfig.cmake +26 -0
- lib/cmake/GraphCore/GraphCoreConfigVersion.cmake +65 -0
- lib/cmake/GraphCore/GraphCoreTargets-release.cmake +19 -0
- lib/cmake/GraphCore/GraphCoreTargets.cmake +107 -0
- lib/graphcore.lib +0 -0
- lib/graphcore_c.lib +0 -0
- studio/__init__.py +1 -0
- studio/cli.py +9 -0
- studio/engine.py +338 -0
- studio/example_plugin.py +8 -0
- studio/server.py +366 -0
- studio/templates/01-research.json +114 -0
- studio/templates/02-structured.json +86 -0
- studio/templates/03-tools.json +70 -0
- studio/templates/04-team.json +117 -0
- studio/templates/05-model.json +19 -0
- studio/web/app.js +108 -0
- studio/web/index.html +43 -0
- studio/web/style.css +9 -0
graphcore/__init__.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""Dependency-free Python SDK for the GraphCore native runtime.
|
|
2
|
+
|
|
3
|
+
Build the shared library first and set GRAPHCORE_LIBRARY if it is outside build/.
|
|
4
|
+
State values are JSON encoded individually; Python objects stay out of checkpoints.
|
|
5
|
+
"""
|
|
6
|
+
import asyncio
|
|
7
|
+
import ctypes as C
|
|
8
|
+
import inspect
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import sys
|
|
13
|
+
import threading
|
|
14
|
+
import traceback
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
END = "__end__"
|
|
19
|
+
__version__ = "0.2.0"
|
|
20
|
+
|
|
21
|
+
class GraphCoreError(RuntimeError):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Context:
|
|
26
|
+
resumed: bool = False
|
|
27
|
+
response: object = None
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class Update:
|
|
31
|
+
values: dict = field(default_factory=dict)
|
|
32
|
+
next: Optional[str] = None
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Interrupt:
|
|
36
|
+
prompt: str
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class Result:
|
|
40
|
+
state: dict
|
|
41
|
+
suspended: bool
|
|
42
|
+
prompt: str
|
|
43
|
+
steps: int
|
|
44
|
+
|
|
45
|
+
_EVENT = C.CFUNCTYPE(None, C.c_char_p, C.c_char_p, C.c_void_p)
|
|
46
|
+
_CALLBACK = C.CFUNCTYPE(C.c_int, C.c_char_p, C.c_int, C.c_char_p, C.c_void_p, C.c_void_p)
|
|
47
|
+
|
|
48
|
+
def _text(value):
|
|
49
|
+
if not isinstance(value, str) or "\0" in value:
|
|
50
|
+
raise ValueError("Expected a string without NUL characters")
|
|
51
|
+
return value.encode("utf-8")
|
|
52
|
+
|
|
53
|
+
def _dump(value):
|
|
54
|
+
return json.dumps(value, ensure_ascii=True, allow_nan=False, sort_keys=True)
|
|
55
|
+
|
|
56
|
+
def _encode(state):
|
|
57
|
+
return "".join(_text(k).hex() + ":" + _dump(v).encode().hex() + "\n"
|
|
58
|
+
for k, v in state.items()).encode()
|
|
59
|
+
|
|
60
|
+
def _decode(wire):
|
|
61
|
+
result = {}
|
|
62
|
+
for line in wire.decode().splitlines():
|
|
63
|
+
key, value = line.split(":", 1)
|
|
64
|
+
result[bytes.fromhex(key).decode()] = json.loads(bytes.fromhex(value).decode())
|
|
65
|
+
return result
|
|
66
|
+
|
|
67
|
+
def _load():
|
|
68
|
+
suffix = ".dll" if sys.platform == "win32" else ".dylib" if sys.platform == "darwin" else ".so"
|
|
69
|
+
name = ("" if sys.platform == "win32" else "lib") + "graphcore" + suffix
|
|
70
|
+
root = Path(__file__).resolve().parents[2]
|
|
71
|
+
candidates = [Path(os.environ["GRAPHCORE_LIBRARY"])] if "GRAPHCORE_LIBRARY" in os.environ else [
|
|
72
|
+
Path(__file__).parent / name, root / "build" / name, root / "build" / "Release" / name]
|
|
73
|
+
path = next((p for p in candidates if p.is_file()), None)
|
|
74
|
+
if path is None:
|
|
75
|
+
raise GraphCoreError("Native library missing. Build it first; see README or set GRAPHCORE_LIBRARY.")
|
|
76
|
+
lib = C.CDLL(str(path))
|
|
77
|
+
definitions = {
|
|
78
|
+
"gc_cancel": ([C.c_void_p], None),
|
|
79
|
+
"gc_set_observer": ([C.c_void_p, _EVENT, C.c_void_p], C.c_int),
|
|
80
|
+
"gc_add_condition": ([C.c_void_p, C.c_char_p, C.c_char_p, C.c_char_p, C.c_char_p, C.c_char_p], C.c_int),
|
|
81
|
+
"gc_create": ([C.c_char_p], C.c_void_p),
|
|
82
|
+
"gc_destroy": ([C.c_void_p], None),
|
|
83
|
+
"gc_error": ([C.c_void_p], C.c_char_p),
|
|
84
|
+
"gc_add_node": ([C.c_void_p, C.c_char_p, _CALLBACK, C.c_void_p, C.c_uint], C.c_int),
|
|
85
|
+
"gc_add_edge": ([C.c_void_p, C.c_char_p, C.c_char_p], C.c_int),
|
|
86
|
+
"gc_set_entry": ([C.c_void_p, C.c_char_p], C.c_int),
|
|
87
|
+
"gc_run": ([C.c_void_p, C.c_char_p, C.c_char_p, C.c_int, C.c_char_p, C.c_size_t], C.c_int),
|
|
88
|
+
"gc_inspect_checkpoint": ([C.c_void_p, C.c_char_p], C.c_int),
|
|
89
|
+
"gc_result": ([C.c_void_p], C.c_char_p),
|
|
90
|
+
"gc_prompt": ([C.c_void_p], C.c_char_p),
|
|
91
|
+
"gc_suspended": ([C.c_void_p], C.c_int),
|
|
92
|
+
"gc_steps": ([C.c_void_p], C.c_size_t),
|
|
93
|
+
"gc_reply_update": ([C.c_void_p, C.c_char_p, C.c_char_p], C.c_int),
|
|
94
|
+
"gc_reply_next": ([C.c_void_p, C.c_char_p], C.c_int),
|
|
95
|
+
"gc_reply_suspend": ([C.c_void_p, C.c_char_p], C.c_int),
|
|
96
|
+
"gc_reply_error": ([C.c_void_p, C.c_char_p], C.c_int),
|
|
97
|
+
}
|
|
98
|
+
for name, (args, result) in definitions.items():
|
|
99
|
+
fn = getattr(lib, name)
|
|
100
|
+
fn.argtypes, fn.restype = args, result
|
|
101
|
+
return lib
|
|
102
|
+
|
|
103
|
+
class Graph:
|
|
104
|
+
"""Sequential durable graph. Use a separate Graph instance for concurrent runs.
|
|
105
|
+
|
|
106
|
+
Python callbacks return a dict, Update, or Interrupt. Async callbacks require
|
|
107
|
+
ainvoke/aresume. Explicitly close the graph or use it as a context manager.
|
|
108
|
+
"""
|
|
109
|
+
def __init__(self, version="1"):
|
|
110
|
+
self._lib = _load()
|
|
111
|
+
self._handle = self._lib.gc_create(_text(version))
|
|
112
|
+
if not self._handle:
|
|
113
|
+
raise GraphCoreError("Cannot create graph")
|
|
114
|
+
self._callbacks = []
|
|
115
|
+
self._lock = threading.Lock()
|
|
116
|
+
self._loop = None
|
|
117
|
+
self._has_async = False
|
|
118
|
+
self._observer = None
|
|
119
|
+
self._lifecycle = threading.Lock()
|
|
120
|
+
|
|
121
|
+
def _check(self, code):
|
|
122
|
+
if code:
|
|
123
|
+
raise GraphCoreError(self._lib.gc_error(self._handle).decode())
|
|
124
|
+
|
|
125
|
+
def _enter(self):
|
|
126
|
+
if not self._lock.acquire(blocking=False):
|
|
127
|
+
raise GraphCoreError("Graph is in use; concurrent calls are unsupported")
|
|
128
|
+
if not self._handle:
|
|
129
|
+
self._lock.release()
|
|
130
|
+
raise GraphCoreError("Graph is closed")
|
|
131
|
+
|
|
132
|
+
def add_condition(self, name, field, expected, yes, no):
|
|
133
|
+
"""Compare canonical JSON values and select a route entirely in C++."""
|
|
134
|
+
self._enter()
|
|
135
|
+
try:
|
|
136
|
+
self._check(self._lib.gc_add_condition(self._handle, _text(name), _text(field),
|
|
137
|
+
_text(_dump(expected)), _text(yes), _text(no)))
|
|
138
|
+
finally:
|
|
139
|
+
self._lock.release()
|
|
140
|
+
return self
|
|
141
|
+
|
|
142
|
+
def observe(self, callback):
|
|
143
|
+
def emit(kind, node, user):
|
|
144
|
+
try:
|
|
145
|
+
callback(kind.decode(), node.decode())
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
self._enter()
|
|
149
|
+
try:
|
|
150
|
+
observer = _EVENT(emit)
|
|
151
|
+
self._check(self._lib.gc_set_observer(self._handle, observer, None))
|
|
152
|
+
self._observer = observer
|
|
153
|
+
finally:
|
|
154
|
+
self._lock.release()
|
|
155
|
+
return self
|
|
156
|
+
|
|
157
|
+
def cancel(self):
|
|
158
|
+
"""Cooperatively cancel; use a new Graph after cancellation."""
|
|
159
|
+
with self._lifecycle:
|
|
160
|
+
if self._handle:
|
|
161
|
+
self._lib.gc_cancel(self._handle)
|
|
162
|
+
|
|
163
|
+
def add_node(self, name, function, retries=0):
|
|
164
|
+
if not callable(function):
|
|
165
|
+
raise TypeError("Node must be callable")
|
|
166
|
+
if not isinstance(retries, int) or not 0 <= retries <= 1000:
|
|
167
|
+
raise ValueError("retries must be between 0 and 1000")
|
|
168
|
+
is_async = inspect.iscoroutinefunction(function)
|
|
169
|
+
def call(wire, resumed, response, reply, user):
|
|
170
|
+
try:
|
|
171
|
+
context = Context(bool(resumed), json.loads(response) if resumed else None)
|
|
172
|
+
result = function(_decode(wire), context)
|
|
173
|
+
if inspect.isawaitable(result):
|
|
174
|
+
if self._loop is None:
|
|
175
|
+
if inspect.iscoroutine(result):
|
|
176
|
+
result.close()
|
|
177
|
+
raise GraphCoreError("Async callbacks require ainvoke/aresume")
|
|
178
|
+
async def await_result():
|
|
179
|
+
return await result
|
|
180
|
+
result = asyncio.run_coroutine_threadsafe(await_result(), self._loop).result()
|
|
181
|
+
if isinstance(result, Interrupt):
|
|
182
|
+
if self._lib.gc_reply_suspend(reply, _text(result.prompt)):
|
|
183
|
+
raise GraphCoreError("Cannot construct interrupt")
|
|
184
|
+
else:
|
|
185
|
+
update = result if isinstance(result, Update) else Update(result)
|
|
186
|
+
if not isinstance(update.values, dict):
|
|
187
|
+
raise TypeError("Node must return dict, Update, or Interrupt")
|
|
188
|
+
for key, value in update.values.items():
|
|
189
|
+
if self._lib.gc_reply_update(reply, _text(key), _text(_dump(value))):
|
|
190
|
+
raise GraphCoreError("Cannot construct update")
|
|
191
|
+
if update.next is not None and self._lib.gc_reply_next(reply, _text(update.next)):
|
|
192
|
+
raise GraphCoreError("Cannot construct route")
|
|
193
|
+
return 0
|
|
194
|
+
except BaseException:
|
|
195
|
+
self._lib.gc_reply_error(reply, traceback.format_exc().replace("\0", "\\0").encode())
|
|
196
|
+
return -1
|
|
197
|
+
callback = _CALLBACK(call)
|
|
198
|
+
self._enter()
|
|
199
|
+
try:
|
|
200
|
+
self._check(self._lib.gc_add_node(self._handle, _text(name), callback, None, retries))
|
|
201
|
+
self._callbacks.append(callback)
|
|
202
|
+
self._has_async |= is_async
|
|
203
|
+
finally:
|
|
204
|
+
self._lock.release()
|
|
205
|
+
return self
|
|
206
|
+
|
|
207
|
+
def add_edge(self, source, target):
|
|
208
|
+
self._enter()
|
|
209
|
+
try:
|
|
210
|
+
self._check(self._lib.gc_add_edge(self._handle, _text(source), _text(target)))
|
|
211
|
+
finally:
|
|
212
|
+
self._lock.release()
|
|
213
|
+
return self
|
|
214
|
+
|
|
215
|
+
def set_entry(self, name):
|
|
216
|
+
self._enter()
|
|
217
|
+
try:
|
|
218
|
+
self._check(self._lib.gc_set_entry(self._handle, _text(name)))
|
|
219
|
+
finally:
|
|
220
|
+
self._lock.release()
|
|
221
|
+
return self
|
|
222
|
+
|
|
223
|
+
def _run(self, state, checkpoint, resume, response, max_steps):
|
|
224
|
+
if not isinstance(max_steps, int) or max_steps < 1:
|
|
225
|
+
raise ValueError("max_steps must be positive")
|
|
226
|
+
path = _text(os.fspath(checkpoint)) if checkpoint is not None else None
|
|
227
|
+
reply = _text(_dump(response)) if response is not _MISSING else None
|
|
228
|
+
self._check(self._lib.gc_run(self._handle, _encode(state), path, resume, reply, max_steps))
|
|
229
|
+
return Result(_decode(self._lib.gc_result(self._handle)),
|
|
230
|
+
bool(self._lib.gc_suspended(self._handle)),
|
|
231
|
+
self._lib.gc_prompt(self._handle).decode(), self._lib.gc_steps(self._handle))
|
|
232
|
+
|
|
233
|
+
def inspect_checkpoint(self, checkpoint):
|
|
234
|
+
"""Read saved state without executing callbacks or advancing the graph."""
|
|
235
|
+
self._enter()
|
|
236
|
+
try:
|
|
237
|
+
self._check(self._lib.gc_inspect_checkpoint(self._handle, _text(os.fspath(checkpoint))))
|
|
238
|
+
return Result(_decode(self._lib.gc_result(self._handle)),
|
|
239
|
+
bool(self._lib.gc_suspended(self._handle)),
|
|
240
|
+
self._lib.gc_prompt(self._handle).decode(), self._lib.gc_steps(self._handle))
|
|
241
|
+
finally:
|
|
242
|
+
self._lock.release()
|
|
243
|
+
|
|
244
|
+
def invoke(self, state=None, *, checkpoint=None, max_steps=100):
|
|
245
|
+
return self._sync(state or {}, checkpoint, False, _MISSING, max_steps)
|
|
246
|
+
|
|
247
|
+
def resume(self, checkpoint, *, response=None, max_steps=100):
|
|
248
|
+
return self._sync({}, checkpoint, True, response, max_steps)
|
|
249
|
+
|
|
250
|
+
def recover(self, checkpoint, *, max_steps=100):
|
|
251
|
+
return self._sync({}, checkpoint, True, _MISSING, max_steps)
|
|
252
|
+
|
|
253
|
+
def _sync(self, *args):
|
|
254
|
+
if self._has_async:
|
|
255
|
+
raise GraphCoreError("Async callbacks require ainvoke/aresume")
|
|
256
|
+
self._enter()
|
|
257
|
+
try:
|
|
258
|
+
return self._run(*args)
|
|
259
|
+
finally:
|
|
260
|
+
self._lock.release()
|
|
261
|
+
|
|
262
|
+
async def ainvoke(self, state=None, *, checkpoint=None, max_steps=100):
|
|
263
|
+
return await self._async(state or {}, checkpoint, False, _MISSING, max_steps)
|
|
264
|
+
|
|
265
|
+
async def aresume(self, checkpoint, *, response=None, max_steps=100):
|
|
266
|
+
return await self._async({}, checkpoint, True, response, max_steps)
|
|
267
|
+
|
|
268
|
+
async def arecover(self, checkpoint, *, max_steps=100):
|
|
269
|
+
return await self._async({}, checkpoint, True, _MISSING, max_steps)
|
|
270
|
+
|
|
271
|
+
async def _async(self, *args):
|
|
272
|
+
self._enter()
|
|
273
|
+
self._loop = asyncio.get_running_loop()
|
|
274
|
+
future = self._loop.run_in_executor(None, self._run, *args)
|
|
275
|
+
# Native execution cannot be safely abandoned. Keep references and loop
|
|
276
|
+
# association until it finishes, even if the awaiting caller cancels.
|
|
277
|
+
def finish(_):
|
|
278
|
+
self._loop = None
|
|
279
|
+
self._lock.release()
|
|
280
|
+
if not future.cancelled():
|
|
281
|
+
future.exception() # Retrieve errors if the caller was cancelled.
|
|
282
|
+
future.add_done_callback(finish)
|
|
283
|
+
return await asyncio.shield(future)
|
|
284
|
+
|
|
285
|
+
def close(self):
|
|
286
|
+
if not self._handle:
|
|
287
|
+
return
|
|
288
|
+
self._enter()
|
|
289
|
+
try:
|
|
290
|
+
with self._lifecycle:
|
|
291
|
+
self._lib.gc_destroy(self._handle)
|
|
292
|
+
self._handle = None
|
|
293
|
+
self._callbacks.clear()
|
|
294
|
+
finally:
|
|
295
|
+
self._lock.release()
|
|
296
|
+
|
|
297
|
+
def __enter__(self):
|
|
298
|
+
return self
|
|
299
|
+
|
|
300
|
+
def __exit__(self, *args):
|
|
301
|
+
self.close()
|
|
302
|
+
|
|
303
|
+
_MISSING = object()
|
graphcore/graphcore.dll
ADDED
|
Binary file
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: graphcore-studio
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A local-first visual agent workflow studio powered by a native C++ graph runtime
|
|
5
|
+
Keywords: agents,agentic-workflows,graph,C++,visual-workflow-builder
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: C++
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Provides-Extra: models
|
|
22
|
+
Requires-Dist: openrouter<1,>=0.10; extra == "models"
|
|
23
|
+
Provides-Extra: pydantic
|
|
24
|
+
Requires-Dist: pydantic<3,>=2; extra == "pydantic"
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# GraphCore Studio
|
|
28
|
+
|
|
29
|
+
**Design local agent workflows on a canvas. Run them with a C++ engine. Extend them with Python.**
|
|
30
|
+
|
|
31
|
+
GraphCore Studio is an interactive, local-first visual agent framework. Drag nodes onto a canvas, connect agent handoffs, add conditional branches, validate structured output, call Python tools, and pause for human approval. Save and run workflows on your own machine.
|
|
32
|
+
|
|
33
|
+
## Start Studio
|
|
34
|
+
|
|
35
|
+
Requirements: Python 3.9+ and a C++17 compiler. You do not need CMake, Node.js, Docker, API keys, an internet connection, or a model to try the included workflows.
|
|
36
|
+
|
|
37
|
+
From the project folder, run one command:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
python3 run_studio.py
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Open [http://127.0.0.1:8787](http://127.0.0.1:8787). The launcher compiles the C++ shared library on first run, then opens a server accessible only from your machine. On Windows, use PowerShell in a Visual Studio Developer Terminal with the C++ workload installed:
|
|
44
|
+
|
|
45
|
+
```powershell
|
|
46
|
+
py run_studio.py
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
If Python was installed through the Windows Store and `py` is unavailable, run `python run_studio.py` from the Visual Studio Developer Terminal.
|
|
50
|
+
|
|
51
|
+
Stop the server with Ctrl+C. Workflows and run history live in `studio/data/`; drafts are saved in this browser. Run `python3 run_studio.py --help` to configure the port, data directory, or Python tool plugins.
|
|
52
|
+
|
|
53
|
+
### Install the PyPI package
|
|
54
|
+
|
|
55
|
+
After the first release is published, install the prebuilt package with:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
python -m pip install "graphcore-studio[models,pydantic]"
|
|
59
|
+
graphcore-studio
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The wheel bundles the C++ runtime and Studio UI. Source installs build the native runtime and therefore require a C++17 compiler and CMake. The distribution is named `graphcore-studio`; its Python API imports as `graphcore`.
|
|
63
|
+
|
|
64
|
+
### Use Pydantic
|
|
65
|
+
|
|
66
|
+
The built-in structured-output validator works offline with no extra packages. To enable optional Pydantic v2 validation:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
python3 -m pip install -r requirements-pydantic.txt
|
|
70
|
+
python3 run_studio.py
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
On Windows, replace `python3` with `py -3`. Restart Studio after installing packages. The Structured output node shows whether Pydantic is available.
|
|
74
|
+
|
|
75
|
+
### Use OpenRouter models
|
|
76
|
+
|
|
77
|
+
The **Chat Model** and **Agent** nodes use GraphCore's model adapter with the official OpenRouter Python SDK. C++ owns graph scheduling, state commits, conditions, and checkpoints; the Python wrapper calls the SDK. The OpenRouter request runs through the project-owned adapter rather than a chain framework.
|
|
78
|
+
|
|
79
|
+
Install the SDK into the Python environment used to run Studio:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
python3 -m pip install -r requirements-models.txt
|
|
83
|
+
python3 run_studio.py
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
For a PyPI installation, use the `models` extra instead: `python -m pip install "graphcore-studio[models]"`.
|
|
87
|
+
|
|
88
|
+
In Studio, open **Model integrations**, add `OPENROUTER_API_KEY`, and save. Select **OpenRouter** on a Chat Model node and enter a model ID from the [OpenRouter model catalog](https://openrouter.ai/models). Compose system/user/assistant messages and use **Insert variable** to reference workflow inputs and prior node outputs. Studio loads the local `.env` file automatically; secret values are not included in workflow files. The offline fixture needs no SDK, key, or network call.
|
|
89
|
+
|
|
90
|
+
## Build a workflow
|
|
91
|
+
|
|
92
|
+
1. Pick a template or drag a node from the left panel.
|
|
93
|
+
2. Connect the output dots to the next node's input. Conditional nodes have separate true and false outputs.
|
|
94
|
+
3. Select nodes to edit prompts, tools, schemas, and routing rules.
|
|
95
|
+
4. Give the workflow JSON input in the panel under the canvas.
|
|
96
|
+
5. Run it and inspect the output, state, and per-node trace.
|
|
97
|
+
6. Save locally, export the workflow JSON, or approve a saved human checkpoint.
|
|
98
|
+
|
|
99
|
+
The starter templates cover research and review, structured output, Python tools, a supervisor delegating work to specialist agents, and a chat-model playground. The canvas also supports node search, keyboard undo/redo, zoom, pan, layout changes, validation, and run history.
|
|
100
|
+
|
|
101
|
+
Prompt templates reference state with `{{input}}`, `{{research}}`, or nested fields such as `{{record.summary}}`. Structured Output defines required fields and types. Python plugins extend the node's registered-tool list without placing executable source code in the workflow document.
|
|
102
|
+
|
|
103
|
+
## Register a Python library or tool
|
|
104
|
+
|
|
105
|
+
A plugin is a local, trusted Python module. Studio loads it when the server starts. Import a library in your tool and register an ordinary function:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
def register(add_tool):
|
|
109
|
+
from my_library import Client
|
|
110
|
+
client = Client()
|
|
111
|
+
|
|
112
|
+
def search(value, config):
|
|
113
|
+
return {"matches": client.search(str(value))}
|
|
114
|
+
|
|
115
|
+
add_tool("search", search, "Search our knowledge base")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Start Studio with `python3 run_studio.py --plugin path/to/plugin.py`, then select the registered function in a Python Tool node. Plugins execute as the current user; load only modules you trust. The included `studio/example_plugin.py` shows a small plugin.
|
|
119
|
+
|
|
120
|
+
## What runs where?
|
|
121
|
+
|
|
122
|
+
```text
|
|
123
|
+
Visual editor & local HTTP API
|
|
124
|
+
│ workflow JSON and node registrations
|
|
125
|
+
Python integrations ───── OpenRouter SDK / Pydantic / your packages
|
|
126
|
+
│ Python C ABI
|
|
127
|
+
C++17 graph scheduler ─── conditions / routes / checkpoints
|
|
128
|
+
│
|
|
129
|
+
Local workflow & run files
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The C++ runtime owns graph validation at compile time, step scheduling, native equality conditions, and checkpoint transitions. Python supplies model and tool integrations and the local editor server. Model and Python tool code executes in Python; agent handoffs are sequential. Performance benefits depend on the workload. No blanket speedup claim is made.
|
|
133
|
+
|
|
134
|
+
## Build or embed the C++ core
|
|
135
|
+
|
|
136
|
+
The editor launcher builds the native shared library automatically. To build the core tests and examples as well:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
./scripts/build.sh
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
For CMake users with CMake 3.16 or newer:
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
|
146
|
+
cmake --build build --config Release
|
|
147
|
+
ctest --test-dir build -C Release --output-on-failure
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Compile a C++ example directly with a compiler:
|
|
151
|
+
|
|
152
|
+
```sh
|
|
153
|
+
c++ -std=c++17 -O2 -Iinclude src/graphcore.cpp examples/approval.cpp -o graphcore_demo
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The C++ interface is in `include/graphcore/graphcore.hpp`; the experimental C ABI used by Python is in `include/graphcore/c_api.h`.
|
|
157
|
+
|
|
158
|
+
## Scope and guarantees
|
|
159
|
+
|
|
160
|
+
This is an early, working local framework. The UI has runnable templates and a native backend. The runtime currently executes one node at a time. Retries can repeat side effects; use idempotency keys in external systems. File checkpoints support local process-restart recovery but do not promise power-loss durability, multiple writers, encryption, or database transactions. Python async cancellation is cooperative only between callbacks. See the [execution contract](docs/SEMANTICS.md).
|
|
161
|
+
|
|
162
|
+
Parallel fan-out and joins, database-backed persistence, distributed worker coordination, and an authenticated server remain future work. A release workflow now builds CPython wheels for Linux, macOS, and Windows; those hosted builds still need to run before the wheels are verified for release. The local server uses a session token and origin checks, and binds only to loopback; it is not designed for public network exposure.
|
|
163
|
+
|
|
164
|
+
## Project notes
|
|
165
|
+
|
|
166
|
+
- [Architecture and extension points](docs/ARCHITECTURE.md)
|
|
167
|
+
- [Local UI and runtime semantics](docs/SEMANTICS.md)
|
|
168
|
+
- [Roadmap](docs/ROADMAP.md)
|
|
169
|
+
- [Local verification record](docs/VALIDATION.md)
|
|
170
|
+
- [PyPI release checklist](docs/PYPI_RELEASE.md)
|
|
171
|
+
- [Example plugin](studio/example_plugin.py)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
graphcore/__init__.py,sha256=7A3dUzs_gxgRho0k98uGT8OgdAAX8EYT5FU2UqdBneg,12408
|
|
2
|
+
graphcore/graphcore.dll,sha256=MMr6IeuFxBWfVlUBD0f86-BqDMaIY95jx0V3F-ikeCw,100864
|
|
3
|
+
include/graphcore/c_api.h,sha256=H7IA88yP-Q9tazxr6Tx0QobFd-RqvuljzJQa9XNkIC0,2459
|
|
4
|
+
include/graphcore/graphcore.hpp,sha256=-pH7yfUTiGw6JmxJ-vkUgFLEnvE3jdKuDLWkLTCu3iA,3103
|
|
5
|
+
lib/cmake/GraphCore/GraphCoreConfig.cmake,sha256=jkFXP-6HfSoHZIpSGlRAZlNj-YOZodRj1KQ5HZ6lhRk,968
|
|
6
|
+
lib/cmake/GraphCore/GraphCoreConfigVersion.cmake,sha256=Gmf_CCVo0WEvg1LKKv1DfGjDE8LJM00sH-WCna2bI-k,2827
|
|
7
|
+
lib/cmake/GraphCore/GraphCoreTargets-release.cmake,sha256=31TYO8LSi6XmD7zN-dLOR9kK7Z934q5VGhhEOuGBmkA,896
|
|
8
|
+
lib/cmake/GraphCore/GraphCoreTargets.cmake,sha256=czj1gBdRN4ZU_mEuJbK2GL4jPgF7W9TkBgae5fAfnyE,4291
|
|
9
|
+
lib/graphcore.lib,sha256=9Bgjkl_2D5Z1IlQ9FDkQmydO_8B1KzIH-5fBoMY4JQU,919760
|
|
10
|
+
lib/graphcore_c.lib,sha256=bpp9CYryHKAstQXq4jyHlWRBMJAo0kmWWzKRiPzDIIM,5188
|
|
11
|
+
studio/__init__.py,sha256=Cn0hUv9J3GXbu86e3qjzm5DFb2M8cOMRk_NX3oq2Ero,79
|
|
12
|
+
studio/cli.py,sha256=Xjdg0DNRMK_falfV63WZZzp5bPVZOsvJ0hPYZy8y5ro,268
|
|
13
|
+
studio/engine.py,sha256=_-XZaRlxjzfJRBXkzR2De3cttHLnB4Mvt1zyaqEVMfk,20118
|
|
14
|
+
studio/example_plugin.py,sha256=WtIwtZOkFapr99IKGet_5igN8VxdMVuxsO_d1NEsl6I,495
|
|
15
|
+
studio/server.py,sha256=lV7PbJJV3Lk-CIrcRlaVkFHHpVhLNpiUFlLzN9B5rbU,19834
|
|
16
|
+
studio/templates/01-research.json,sha256=IG3U8oCKj-rb96LEcCIPS77YRL1GFNja8IFofpRpoj0,2860
|
|
17
|
+
studio/templates/02-structured.json,sha256=jrX9r_rJoq_Exl9wBH_kyIfgfdJraHgPzJH8CIo2in4,2020
|
|
18
|
+
studio/templates/03-tools.json,sha256=Yh80vAmODCSZ9-JQX_8r_nmW8rSws1MG5U9ABcd5lbs,1494
|
|
19
|
+
studio/templates/04-team.json,sha256=mRoyXCA6ow3ZXFjhhkfQ3-b4K11lTgFkEsJsbbqGr9Y,2857
|
|
20
|
+
studio/templates/05-model.json,sha256=-BQq4fr2V62B1uVhZqrFX8CNbwAgJZNqlmcquaY95mk,1140
|
|
21
|
+
studio/web/app.js,sha256=2RlRUrSqW2o-xQ1TWbQ7QsiaEzO1Mtdq_u9dMpLMWkg,46513
|
|
22
|
+
studio/web/index.html,sha256=Ls0ePIekgN91D7fXbgEffQH1Dpn3t9qtyuLCeg1476w,5376
|
|
23
|
+
studio/web/style.css,sha256=RfxMCpe0LzNkR7AjSjO67P66mpo4Ln5gQlJvrBBfTmY,22282
|
|
24
|
+
graphcore_studio-0.2.0.dist-info/METADATA,sha256=66QnViLSScawTjd7JF3SBNGU-zQOD-mBqojN0lf13yk,8617
|
|
25
|
+
graphcore_studio-0.2.0.dist-info/WHEEL,sha256=IXwO9xsBdL-YdgY95jrgzilU_rVfj9lNZZ6Y3UzNUkc,98
|
|
26
|
+
graphcore_studio-0.2.0.dist-info/entry_points.txt,sha256=ICHwg3dwPHf5dLOh88EOwvkWol7nGIHK6NWo6pmrnUU,54
|
|
27
|
+
graphcore_studio-0.2.0.dist-info/licenses/LICENSE,sha256=skdWxhD4isyrtl6JSxMWrkxVXlFV0Y5bIHulD9fHQUo,1100
|
|
28
|
+
graphcore_studio-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GraphCore contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#ifndef GRAPHCORE_C_API_H
|
|
2
|
+
#define GRAPHCORE_C_API_H
|
|
3
|
+
#include <stddef.h>
|
|
4
|
+
#if defined(_WIN32)
|
|
5
|
+
# if defined(GRAPHCORE_BUILD_SHARED)
|
|
6
|
+
# define GC_API __declspec(dllexport)
|
|
7
|
+
# else
|
|
8
|
+
# define GC_API __declspec(dllimport)
|
|
9
|
+
# endif
|
|
10
|
+
#else
|
|
11
|
+
# define GC_API __attribute__((visibility("default")))
|
|
12
|
+
#endif
|
|
13
|
+
#ifdef __cplusplus
|
|
14
|
+
extern "C" {
|
|
15
|
+
#endif
|
|
16
|
+
/* One handle per graph/run; handles must not be used concurrently.
|
|
17
|
+
Strings are UTF-8, NUL terminated. Returned strings live until the next call.
|
|
18
|
+
Callback state is hex wire format; callback and user data must outlive handle. */
|
|
19
|
+
typedef struct gc_handle gc_handle;
|
|
20
|
+
typedef int (*gc_callback)(const char* state, int resumed, const char* response,
|
|
21
|
+
void* reply, void* user_data);
|
|
22
|
+
typedef void (*gc_event_callback)(const char* type, const char* node, void* user_data);
|
|
23
|
+
/* Set observer before running; cancel is the only method safe during gc_run. */
|
|
24
|
+
GC_API int gc_set_observer(gc_handle*, gc_event_callback, void* user_data);
|
|
25
|
+
GC_API void gc_cancel(gc_handle*);
|
|
26
|
+
/* Native equality branch; values use the caller's state encoding. */
|
|
27
|
+
GC_API int gc_add_condition(gc_handle*, const char* name, const char* key,
|
|
28
|
+
const char* expected, const char* yes, const char* no);
|
|
29
|
+
GC_API gc_handle* gc_create(const char* version);
|
|
30
|
+
GC_API void gc_destroy(gc_handle*);
|
|
31
|
+
GC_API const char* gc_error(gc_handle*);
|
|
32
|
+
GC_API int gc_add_node(gc_handle*, const char* name, gc_callback, void* user_data, unsigned retries);
|
|
33
|
+
GC_API int gc_add_edge(gc_handle*, const char* source, const char* target);
|
|
34
|
+
GC_API int gc_set_entry(gc_handle*, const char* name);
|
|
35
|
+
/* checkpoint=NULL disables persistence; response=NULL means no resume response. */
|
|
36
|
+
GC_API int gc_run(gc_handle*, const char* initial_wire, const char* checkpoint,
|
|
37
|
+
int resume, const char* response, size_t max_steps);
|
|
38
|
+
GC_API int gc_inspect_checkpoint(gc_handle*, const char* path);
|
|
39
|
+
GC_API const char* gc_result(gc_handle*);
|
|
40
|
+
GC_API const char* gc_prompt(gc_handle*);
|
|
41
|
+
GC_API int gc_suspended(gc_handle*);
|
|
42
|
+
GC_API size_t gc_steps(gc_handle*);
|
|
43
|
+
/* Reply helpers return -1 on failure; callback must propagate failure. */
|
|
44
|
+
GC_API int gc_reply_update(void*, const char* key, const char* value);
|
|
45
|
+
GC_API int gc_reply_next(void*, const char* target);
|
|
46
|
+
GC_API int gc_reply_suspend(void*, const char* prompt);
|
|
47
|
+
GC_API int gc_reply_error(void*, const char* message);
|
|
48
|
+
#ifdef __cplusplus
|
|
49
|
+
}
|
|
50
|
+
#endif
|
|
51
|
+
#endif
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
#include <atomic>
|
|
3
|
+
#include <filesystem>
|
|
4
|
+
#include <functional>
|
|
5
|
+
#include <map>
|
|
6
|
+
#include <optional>
|
|
7
|
+
#include <stdexcept>
|
|
8
|
+
#include <string>
|
|
9
|
+
#include <vector>
|
|
10
|
+
#include <utility>
|
|
11
|
+
|
|
12
|
+
namespace graphcore {
|
|
13
|
+
using State = std::map<std::string, std::string>;
|
|
14
|
+
inline constexpr const char* END = "__end__";
|
|
15
|
+
struct Error : std::runtime_error { using std::runtime_error::runtime_error; };
|
|
16
|
+
struct NodeResult {
|
|
17
|
+
State updates;
|
|
18
|
+
std::optional<std::string> next;
|
|
19
|
+
bool suspend = false;
|
|
20
|
+
std::string prompt;
|
|
21
|
+
NodeResult(State changes = {}, std::optional<std::string> target = {},
|
|
22
|
+
bool paused = false, std::string question = {})
|
|
23
|
+
: updates(std::move(changes)), next(std::move(target)), suspend(paused),
|
|
24
|
+
prompt(std::move(question)) {}
|
|
25
|
+
};
|
|
26
|
+
struct Context { bool resumed = false; std::string response; };
|
|
27
|
+
using Node = std::function<NodeResult(const State&, const Context&)>;
|
|
28
|
+
struct NodeOptions { unsigned retries = 0; }; // Only opt in for safe/idempotent nodes.
|
|
29
|
+
struct Snapshot {
|
|
30
|
+
std::string graph_version, next;
|
|
31
|
+
State state;
|
|
32
|
+
std::size_t steps = 0;
|
|
33
|
+
bool suspended = false;
|
|
34
|
+
std::string prompt;
|
|
35
|
+
};
|
|
36
|
+
class CheckpointStore {
|
|
37
|
+
public:
|
|
38
|
+
virtual ~CheckpointStore() = default;
|
|
39
|
+
virtual void save(const Snapshot&) = 0;
|
|
40
|
+
virtual Snapshot load() = 0;
|
|
41
|
+
};
|
|
42
|
+
class FileCheckpoint final : public CheckpointStore {
|
|
43
|
+
std::filesystem::path path_;
|
|
44
|
+
public:
|
|
45
|
+
explicit FileCheckpoint(std::filesystem::path path) : path_(std::move(path)) {}
|
|
46
|
+
void save(const Snapshot&) override;
|
|
47
|
+
Snapshot load() override;
|
|
48
|
+
};
|
|
49
|
+
class Graph {
|
|
50
|
+
public:
|
|
51
|
+
struct Entry { Node fn; NodeOptions options; std::vector<std::string> targets; };
|
|
52
|
+
std::map<std::string, Entry> nodes;
|
|
53
|
+
std::string entry, version;
|
|
54
|
+
};
|
|
55
|
+
class GraphBuilder {
|
|
56
|
+
Graph graph_;
|
|
57
|
+
public:
|
|
58
|
+
explicit GraphBuilder(std::string version);
|
|
59
|
+
GraphBuilder& add_node(std::string name, Node node, NodeOptions options = {});
|
|
60
|
+
// All dynamic routes must be declared; the first is the default route.
|
|
61
|
+
GraphBuilder& edges(const std::string& source, std::vector<std::string> targets);
|
|
62
|
+
GraphBuilder& entry(std::string name);
|
|
63
|
+
Graph compile() const;
|
|
64
|
+
};
|
|
65
|
+
struct RunOptions {
|
|
66
|
+
std::size_t max_steps = 100;
|
|
67
|
+
const std::atomic<bool>* cancelled = nullptr;
|
|
68
|
+
// Observer exceptions are isolated from execution.
|
|
69
|
+
std::function<void(const std::string&, const std::string&)> on_event;
|
|
70
|
+
};
|
|
71
|
+
class Runtime {
|
|
72
|
+
Snapshot execute(const Graph&, Snapshot, CheckpointStore*, const RunOptions&, Context);
|
|
73
|
+
public:
|
|
74
|
+
Snapshot invoke(const Graph&, State initial = {}, CheckpointStore* store = nullptr,
|
|
75
|
+
const RunOptions& options = {});
|
|
76
|
+
// A response is required only when the saved snapshot is suspended.
|
|
77
|
+
Snapshot resume(const Graph&, CheckpointStore&, std::optional<std::string> response = {},
|
|
78
|
+
const RunOptions& options = {});
|
|
79
|
+
};
|
|
80
|
+
// Portable text wire format shared by the C ABI and Python wrapper.
|
|
81
|
+
std::string encode_state(const State&);
|
|
82
|
+
State decode_state(const std::string&);
|
|
83
|
+
}
|