@swirls/sdk 0.0.11 → 0.0.12
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.
- package/README.md +31 -0
- package/dist/graph/index.d.ts +263 -0
- package/dist/graph/index.js +603 -0
- package/package.json +7 -1
package/README.md
CHANGED
|
@@ -34,6 +34,36 @@ The [`@swirls/sdk/form`](src/form/README.md) subpackage provides a form library
|
|
|
34
34
|
import { useSwirlsFormAdapter } from '@swirls/sdk/form'
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
### Graph Builder
|
|
38
|
+
|
|
39
|
+
The [`@swirls/sdk/graph`](src/graph/README.md) subpackage lets you build and manipulate workflow graphs in code. Use a fluent API to add nodes and edges by name, then validate (DAG, single root), persist via the API with `save()`, or serialize with `toJSON()` for storage or version control. Dynamic values (e.g. LLM prompts, code transforms) use TypeScript function bodies with a `context` object; when you set `inputSchema` and `outputSchema` on nodes, `context.inputs` is typed for intellisense.
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { Swirls } from '@swirls/sdk/client'
|
|
43
|
+
import { GraphBuilder } from '@swirls/sdk/graph'
|
|
44
|
+
|
|
45
|
+
const swirls = new Swirls({ apiKey: process.env.SWIRLS_API_KEY! })
|
|
46
|
+
const graph = new GraphBuilder({ client: swirls.client })
|
|
47
|
+
.setName('my_workflow')
|
|
48
|
+
.setLabel('My Workflow')
|
|
49
|
+
.addNode('fetch', {
|
|
50
|
+
type: 'http',
|
|
51
|
+
label: 'Fetch',
|
|
52
|
+
config: { type: 'http', url: 'https://api.example.com/data', method: 'GET' },
|
|
53
|
+
})
|
|
54
|
+
.addNode('summarize', {
|
|
55
|
+
type: 'llm',
|
|
56
|
+
label: 'Summarize',
|
|
57
|
+
config: {
|
|
58
|
+
type: 'llm',
|
|
59
|
+
model: 'gpt-4o',
|
|
60
|
+
prompt: 'return `Summarize the following: ${context.inputs.fetch.output}`',
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
.addEdge({ source: 'fetch', target: 'summarize' })
|
|
64
|
+
await graph.save({ projectId: '...' })
|
|
65
|
+
```
|
|
66
|
+
|
|
37
67
|
## Installation
|
|
38
68
|
|
|
39
69
|
```sh
|
|
@@ -52,3 +82,4 @@ bun add @tanstack/react-query
|
|
|
52
82
|
- [Client Documentation](src/client/README.md): API client and TanStack Query utils
|
|
53
83
|
- [Config Documentation](src/config/README.md): Configuration reference
|
|
54
84
|
- [Form Documentation](src/form/README.md): Complete guide to forms
|
|
85
|
+
- [Graph Documentation](src/graph/README.md): Build and manipulate workflow graphs
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { NodeType, GraphNodeConfig, ReviewConfig, JsonSchema, Position } from '@swirls/core/schemas';
|
|
2
|
+
|
|
3
|
+
/** Options for adding a node to a graph. Config is typed by node type. */
|
|
4
|
+
type AddNodeOptions = {
|
|
5
|
+
type: NodeType;
|
|
6
|
+
label: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
config: GraphNodeConfig;
|
|
9
|
+
reviewConfig?: ReviewConfig | null;
|
|
10
|
+
inputSchema?: JsonSchema | null;
|
|
11
|
+
outputSchema?: JsonSchema | null;
|
|
12
|
+
position?: Position;
|
|
13
|
+
};
|
|
14
|
+
/** Serialized node shape for toJSON/fromJSON. */
|
|
15
|
+
type SerializedNode = {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
label: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
type: NodeType;
|
|
21
|
+
config: GraphNodeConfig;
|
|
22
|
+
reviewConfig?: ReviewConfig | null;
|
|
23
|
+
inputSchema?: JsonSchema | null;
|
|
24
|
+
outputSchema?: JsonSchema | null;
|
|
25
|
+
position?: Position;
|
|
26
|
+
};
|
|
27
|
+
/** Serialized edge shape for toJSON/fromJSON. */
|
|
28
|
+
type SerializedEdge = {
|
|
29
|
+
id: string;
|
|
30
|
+
sourceNodeId: string;
|
|
31
|
+
targetNodeId: string;
|
|
32
|
+
label?: string | null;
|
|
33
|
+
};
|
|
34
|
+
/** Serialized graph shape for toJSON/fromJSON (no API-only fields). */
|
|
35
|
+
type SerializedGraph = {
|
|
36
|
+
id: string;
|
|
37
|
+
name: string;
|
|
38
|
+
label: string;
|
|
39
|
+
description?: string | null;
|
|
40
|
+
projectId?: string;
|
|
41
|
+
folderId?: string | null;
|
|
42
|
+
nodes: SerializedNode[];
|
|
43
|
+
edges: SerializedEdge[];
|
|
44
|
+
};
|
|
45
|
+
/** Options for saving a graph to the Swirls API. */
|
|
46
|
+
type SaveOptions = {
|
|
47
|
+
projectId: string;
|
|
48
|
+
folderId?: string | null;
|
|
49
|
+
};
|
|
50
|
+
/** Options for adding an edge by node names. */
|
|
51
|
+
type AddEdgeOptions = {
|
|
52
|
+
source: string;
|
|
53
|
+
target: string;
|
|
54
|
+
label?: string | null;
|
|
55
|
+
};
|
|
56
|
+
/** Options for removing an edge by node names. */
|
|
57
|
+
type RemoveEdgeOptions = {
|
|
58
|
+
source: string;
|
|
59
|
+
target: string;
|
|
60
|
+
};
|
|
61
|
+
/** Swirls client type: must have graphs.getGraph and graphs.syncGraph (and createGraph for new graphs). */
|
|
62
|
+
type SwirlsGraphClient = {
|
|
63
|
+
graphs: {
|
|
64
|
+
getGraph: (input: {
|
|
65
|
+
input: {
|
|
66
|
+
id: string;
|
|
67
|
+
};
|
|
68
|
+
}) => Promise<{
|
|
69
|
+
id: string;
|
|
70
|
+
projectId: string;
|
|
71
|
+
folderId: string | null;
|
|
72
|
+
name: string;
|
|
73
|
+
label: string;
|
|
74
|
+
description: string | null;
|
|
75
|
+
nodes: Array<{
|
|
76
|
+
id: string;
|
|
77
|
+
graphId: string;
|
|
78
|
+
name: string;
|
|
79
|
+
label: string;
|
|
80
|
+
description: string | null;
|
|
81
|
+
type: string;
|
|
82
|
+
config: Record<string, unknown>;
|
|
83
|
+
reviewConfig: unknown;
|
|
84
|
+
position: {
|
|
85
|
+
x: number;
|
|
86
|
+
y: number;
|
|
87
|
+
} | null;
|
|
88
|
+
outputSchema: unknown;
|
|
89
|
+
inputSchema: unknown;
|
|
90
|
+
}>;
|
|
91
|
+
edges: Array<{
|
|
92
|
+
id: string;
|
|
93
|
+
graphId: string;
|
|
94
|
+
sourceNodeId: string;
|
|
95
|
+
targetNodeId: string;
|
|
96
|
+
label: string | null;
|
|
97
|
+
}>;
|
|
98
|
+
}>;
|
|
99
|
+
createGraph: (input: {
|
|
100
|
+
input: {
|
|
101
|
+
projectId: string;
|
|
102
|
+
folderId?: string | null;
|
|
103
|
+
name: string;
|
|
104
|
+
label: string;
|
|
105
|
+
description?: string;
|
|
106
|
+
};
|
|
107
|
+
}) => Promise<{
|
|
108
|
+
id: string;
|
|
109
|
+
}>;
|
|
110
|
+
syncGraph: (input: {
|
|
111
|
+
input: {
|
|
112
|
+
graphId: string;
|
|
113
|
+
nodes: Array<{
|
|
114
|
+
id: string;
|
|
115
|
+
name: string;
|
|
116
|
+
label: string;
|
|
117
|
+
description?: string;
|
|
118
|
+
type: string;
|
|
119
|
+
config: Record<string, unknown>;
|
|
120
|
+
reviewConfig?: unknown;
|
|
121
|
+
position?: {
|
|
122
|
+
x: number;
|
|
123
|
+
y: number;
|
|
124
|
+
};
|
|
125
|
+
outputSchema?: unknown;
|
|
126
|
+
inputSchema?: unknown;
|
|
127
|
+
}>;
|
|
128
|
+
edges: Array<{
|
|
129
|
+
id: string;
|
|
130
|
+
sourceNodeId: string;
|
|
131
|
+
targetNodeId: string;
|
|
132
|
+
label?: string;
|
|
133
|
+
}>;
|
|
134
|
+
};
|
|
135
|
+
}) => Promise<{
|
|
136
|
+
id: string;
|
|
137
|
+
nodes: unknown[];
|
|
138
|
+
edges: unknown[];
|
|
139
|
+
}>;
|
|
140
|
+
executeGraph: (input: {
|
|
141
|
+
input: {
|
|
142
|
+
graphId: string;
|
|
143
|
+
input?: Record<string, unknown>;
|
|
144
|
+
};
|
|
145
|
+
}) => Promise<{
|
|
146
|
+
executionId: string;
|
|
147
|
+
}>;
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
type NodeRefUpdate = {
|
|
152
|
+
label?: string;
|
|
153
|
+
description?: string;
|
|
154
|
+
type?: NodeType;
|
|
155
|
+
config?: GraphNodeConfig;
|
|
156
|
+
reviewConfig?: ReviewConfig | null;
|
|
157
|
+
inputSchema?: JsonSchema | null;
|
|
158
|
+
outputSchema?: JsonSchema | null;
|
|
159
|
+
position?: Position;
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* Reference to a node within a graph. Created by GraphBuilder.addNode().
|
|
163
|
+
* incomingEdges/outgoingEdges are computed from the parent graph.
|
|
164
|
+
*/
|
|
165
|
+
declare class NodeRef {
|
|
166
|
+
readonly id: string;
|
|
167
|
+
readonly name: string;
|
|
168
|
+
label: string;
|
|
169
|
+
description: string | null;
|
|
170
|
+
type: NodeType;
|
|
171
|
+
config: GraphNodeConfig;
|
|
172
|
+
reviewConfig: ReviewConfig | null;
|
|
173
|
+
inputSchema: JsonSchema | null;
|
|
174
|
+
outputSchema: JsonSchema | null;
|
|
175
|
+
position: Position | null;
|
|
176
|
+
private getIncoming;
|
|
177
|
+
private getOutgoing;
|
|
178
|
+
constructor(data: SerializedNode, getIncoming: () => EdgeRef[], getOutgoing: () => EdgeRef[]);
|
|
179
|
+
get incomingEdges(): EdgeRef[];
|
|
180
|
+
get outgoingEdges(): EdgeRef[];
|
|
181
|
+
get isRootNode(): boolean;
|
|
182
|
+
update(options: NodeRefUpdate): this;
|
|
183
|
+
/** Serialize to plain object for toJSON/syncGraph. */
|
|
184
|
+
toJSON(): SerializedNode;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Reference to an edge within a graph. Created by GraphBuilder.addEdge().
|
|
189
|
+
* source and target resolve to NodeRefs from the parent graph.
|
|
190
|
+
*/
|
|
191
|
+
declare class EdgeRef {
|
|
192
|
+
readonly id: string;
|
|
193
|
+
readonly sourceNodeId: string;
|
|
194
|
+
readonly targetNodeId: string;
|
|
195
|
+
label: string | null;
|
|
196
|
+
private getSource;
|
|
197
|
+
private getTarget;
|
|
198
|
+
constructor(data: SerializedEdge, getSource: () => NodeRef | undefined, getTarget: () => NodeRef | undefined);
|
|
199
|
+
get source(): NodeRef | undefined;
|
|
200
|
+
get target(): NodeRef | undefined;
|
|
201
|
+
/** Serialize to plain object for toJSON/syncGraph. */
|
|
202
|
+
toJSON(): SerializedEdge;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Thrown when graph validation fails (cycles, multiple roots, invalid references, etc.).
|
|
207
|
+
*/
|
|
208
|
+
declare class GraphValidationError extends Error {
|
|
209
|
+
/** Machine-readable code for programmatic handling. */
|
|
210
|
+
readonly code: 'CYCLE_DETECTED' | 'NO_ROOT_NODE' | 'MULTIPLE_ROOT_NODES' | 'INVALID_SOURCE_NODE' | 'INVALID_TARGET_NODE' | 'SELF_LOOP' | 'DUPLICATE_NODE_NAME' | 'INVALID_NODE_NAME' | 'UNKNOWN';
|
|
211
|
+
/** Optional details (e.g. node names or IDs involved). */
|
|
212
|
+
readonly details?: Record<string, unknown>;
|
|
213
|
+
constructor(message: string, options?: {
|
|
214
|
+
code?: GraphValidationError['code'];
|
|
215
|
+
details?: Record<string, unknown>;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Builder for Swirls graphs. Build locally, then save() to the API or toJSON() for storage.
|
|
221
|
+
*/
|
|
222
|
+
declare class GraphBuilder {
|
|
223
|
+
id: string;
|
|
224
|
+
name: string;
|
|
225
|
+
label: string;
|
|
226
|
+
description: string | null;
|
|
227
|
+
private client;
|
|
228
|
+
private _nodesById;
|
|
229
|
+
private _nodesByName;
|
|
230
|
+
private _edgesById;
|
|
231
|
+
private _edges;
|
|
232
|
+
private _loadedFromApi;
|
|
233
|
+
constructor(options?: {
|
|
234
|
+
client?: SwirlsGraphClient;
|
|
235
|
+
});
|
|
236
|
+
setName(name: string): this;
|
|
237
|
+
setLabel(label: string): this;
|
|
238
|
+
setDescription(description: string | null): this;
|
|
239
|
+
getNode(name: string): NodeRef | undefined;
|
|
240
|
+
getNodeById(id: string): NodeRef | undefined;
|
|
241
|
+
private getEdgesByTargetId;
|
|
242
|
+
private getEdgesBySourceId;
|
|
243
|
+
addNode(name: string, options: AddNodeOptions): this;
|
|
244
|
+
private _addNodeRaw;
|
|
245
|
+
addEdge(options: AddEdgeOptions): this;
|
|
246
|
+
private _addEdgeRaw;
|
|
247
|
+
removeNode(name: string): this;
|
|
248
|
+
removeEdge(options: RemoveEdgeOptions): this;
|
|
249
|
+
get rootNode(): NodeRef | undefined;
|
|
250
|
+
get nodeList(): NodeRef[];
|
|
251
|
+
get edgeList(): EdgeRef[];
|
|
252
|
+
validate(): void;
|
|
253
|
+
save(options: SaveOptions): Promise<this>;
|
|
254
|
+
private _syncToApi;
|
|
255
|
+
saveExisting(_options: SaveOptions): Promise<this>;
|
|
256
|
+
execute(input?: Record<string, unknown>): Promise<string>;
|
|
257
|
+
toJSON(): SerializedGraph;
|
|
258
|
+
toMermaid(): string;
|
|
259
|
+
static fromJSON(json: string | SerializedGraph): GraphBuilder;
|
|
260
|
+
static fromAPI(client: SwirlsGraphClient, graphId: string): Promise<GraphBuilder>;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export { type AddEdgeOptions, type AddNodeOptions, EdgeRef, GraphBuilder, GraphValidationError, NodeRef, type NodeRefUpdate, type RemoveEdgeOptions, type SaveOptions, type SerializedEdge, type SerializedGraph, type SerializedNode, type SwirlsGraphClient };
|
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
// src/graph/edge-ref.ts
|
|
2
|
+
var EdgeRef = class {
|
|
3
|
+
id;
|
|
4
|
+
sourceNodeId;
|
|
5
|
+
targetNodeId;
|
|
6
|
+
label;
|
|
7
|
+
getSource;
|
|
8
|
+
getTarget;
|
|
9
|
+
constructor(data, getSource, getTarget) {
|
|
10
|
+
this.id = data.id;
|
|
11
|
+
this.sourceNodeId = data.sourceNodeId;
|
|
12
|
+
this.targetNodeId = data.targetNodeId;
|
|
13
|
+
this.label = data.label ?? null;
|
|
14
|
+
this.getSource = getSource;
|
|
15
|
+
this.getTarget = getTarget;
|
|
16
|
+
}
|
|
17
|
+
get source() {
|
|
18
|
+
return this.getSource();
|
|
19
|
+
}
|
|
20
|
+
get target() {
|
|
21
|
+
return this.getTarget();
|
|
22
|
+
}
|
|
23
|
+
/** Serialize to plain object for toJSON/syncGraph. */
|
|
24
|
+
toJSON() {
|
|
25
|
+
return {
|
|
26
|
+
id: this.id,
|
|
27
|
+
sourceNodeId: this.sourceNodeId,
|
|
28
|
+
targetNodeId: this.targetNodeId,
|
|
29
|
+
label: this.label ?? void 0
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// src/graph/errors.ts
|
|
35
|
+
var GraphValidationError = class extends Error {
|
|
36
|
+
/** Machine-readable code for programmatic handling. */
|
|
37
|
+
code;
|
|
38
|
+
/** Optional details (e.g. node names or IDs involved). */
|
|
39
|
+
details;
|
|
40
|
+
constructor(message, options) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = "GraphValidationError";
|
|
43
|
+
this.code = options?.code ?? "UNKNOWN";
|
|
44
|
+
this.details = options?.details;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// src/graph/node-ref.ts
|
|
49
|
+
var NodeRef = class {
|
|
50
|
+
id;
|
|
51
|
+
name;
|
|
52
|
+
label;
|
|
53
|
+
description;
|
|
54
|
+
type;
|
|
55
|
+
config;
|
|
56
|
+
reviewConfig;
|
|
57
|
+
inputSchema;
|
|
58
|
+
outputSchema;
|
|
59
|
+
position;
|
|
60
|
+
getIncoming;
|
|
61
|
+
getOutgoing;
|
|
62
|
+
constructor(data, getIncoming, getOutgoing) {
|
|
63
|
+
this.id = data.id;
|
|
64
|
+
this.name = data.name;
|
|
65
|
+
this.label = data.label;
|
|
66
|
+
this.description = data.description ?? null;
|
|
67
|
+
this.type = data.type;
|
|
68
|
+
this.config = { ...data.config };
|
|
69
|
+
this.reviewConfig = data.reviewConfig ?? null;
|
|
70
|
+
this.inputSchema = data.inputSchema ?? null;
|
|
71
|
+
this.outputSchema = data.outputSchema ?? null;
|
|
72
|
+
this.position = data.position ?? null;
|
|
73
|
+
this.getIncoming = getIncoming;
|
|
74
|
+
this.getOutgoing = getOutgoing;
|
|
75
|
+
}
|
|
76
|
+
get incomingEdges() {
|
|
77
|
+
return this.getIncoming();
|
|
78
|
+
}
|
|
79
|
+
get outgoingEdges() {
|
|
80
|
+
return this.getOutgoing();
|
|
81
|
+
}
|
|
82
|
+
get isRootNode() {
|
|
83
|
+
return this.incomingEdges.length === 0;
|
|
84
|
+
}
|
|
85
|
+
update(options) {
|
|
86
|
+
if (options.label !== void 0) {
|
|
87
|
+
this.label = options.label;
|
|
88
|
+
}
|
|
89
|
+
if (options.description !== void 0) {
|
|
90
|
+
this.description = options.description;
|
|
91
|
+
}
|
|
92
|
+
if (options.type !== void 0) {
|
|
93
|
+
this.type = options.type;
|
|
94
|
+
}
|
|
95
|
+
if (options.config !== void 0) {
|
|
96
|
+
this.config = { ...options.config };
|
|
97
|
+
}
|
|
98
|
+
if (options.reviewConfig !== void 0) {
|
|
99
|
+
this.reviewConfig = options.reviewConfig;
|
|
100
|
+
}
|
|
101
|
+
if (options.inputSchema !== void 0) {
|
|
102
|
+
this.inputSchema = options.inputSchema;
|
|
103
|
+
}
|
|
104
|
+
if (options.outputSchema !== void 0) {
|
|
105
|
+
this.outputSchema = options.outputSchema;
|
|
106
|
+
}
|
|
107
|
+
if (options.position !== void 0) {
|
|
108
|
+
this.position = options.position;
|
|
109
|
+
}
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
/** Serialize to plain object for toJSON/syncGraph. */
|
|
113
|
+
toJSON() {
|
|
114
|
+
return {
|
|
115
|
+
id: this.id,
|
|
116
|
+
name: this.name,
|
|
117
|
+
label: this.label,
|
|
118
|
+
description: this.description ?? void 0,
|
|
119
|
+
type: this.type,
|
|
120
|
+
config: this.config,
|
|
121
|
+
reviewConfig: this.reviewConfig ?? void 0,
|
|
122
|
+
inputSchema: this.inputSchema ?? void 0,
|
|
123
|
+
outputSchema: this.outputSchema ?? void 0,
|
|
124
|
+
position: this.position ?? void 0
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// src/graph/utils.ts
|
|
130
|
+
function mermaidSafeId(nameOrId) {
|
|
131
|
+
return nameOrId.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/graph/validation.ts
|
|
135
|
+
function topologicalSort(nodes, edges) {
|
|
136
|
+
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
137
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
138
|
+
const adjacentList = /* @__PURE__ */ new Map();
|
|
139
|
+
for (const node of nodes) {
|
|
140
|
+
inDegree.set(node.id, 0);
|
|
141
|
+
adjacentList.set(node.id, []);
|
|
142
|
+
}
|
|
143
|
+
for (const edge of edges) {
|
|
144
|
+
if (!nodeIds.has(edge.sourceNodeId)) {
|
|
145
|
+
throw new GraphValidationError(
|
|
146
|
+
`Edge references non-existent source node: ${edge.sourceNodeId}`,
|
|
147
|
+
{
|
|
148
|
+
code: "INVALID_SOURCE_NODE",
|
|
149
|
+
details: { sourceNodeId: edge.sourceNodeId }
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (!nodeIds.has(edge.targetNodeId)) {
|
|
154
|
+
throw new GraphValidationError(
|
|
155
|
+
`Edge references non-existent target node: ${edge.targetNodeId}`,
|
|
156
|
+
{
|
|
157
|
+
code: "INVALID_TARGET_NODE",
|
|
158
|
+
details: { targetNodeId: edge.targetNodeId }
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (edge.sourceNodeId === edge.targetNodeId) {
|
|
163
|
+
throw new GraphValidationError("Edge cannot connect a node to itself", {
|
|
164
|
+
code: "SELF_LOOP",
|
|
165
|
+
details: { nodeId: edge.sourceNodeId }
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
adjacentList.get(edge.sourceNodeId)?.push(edge.targetNodeId);
|
|
169
|
+
inDegree.set(edge.targetNodeId, (inDegree.get(edge.targetNodeId) ?? 0) + 1);
|
|
170
|
+
}
|
|
171
|
+
const queue = [];
|
|
172
|
+
for (const [nodeId, degree] of inDegree.entries()) {
|
|
173
|
+
if (degree === 0) {
|
|
174
|
+
queue.push(nodeId);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (queue.length === 0) {
|
|
178
|
+
throw new GraphValidationError(
|
|
179
|
+
"Graph must have exactly one root node, but none were found.",
|
|
180
|
+
{ code: "NO_ROOT_NODE" }
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
if (queue.length > 1) {
|
|
184
|
+
throw new GraphValidationError(
|
|
185
|
+
`Graph must have exactly one root node, but found ${queue.length}.`,
|
|
186
|
+
{ code: "MULTIPLE_ROOT_NODES", details: { count: queue.length } }
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
const sorted = [];
|
|
190
|
+
while (queue.length > 0) {
|
|
191
|
+
const current = queue.shift();
|
|
192
|
+
if (current === void 0) {
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
sorted.push(current);
|
|
196
|
+
const neighbors = adjacentList.get(current) ?? [];
|
|
197
|
+
for (const neighbor of neighbors) {
|
|
198
|
+
const newDegree = (inDegree.get(neighbor) ?? 0) - 1;
|
|
199
|
+
inDegree.set(neighbor, newDegree);
|
|
200
|
+
if (newDegree === 0) {
|
|
201
|
+
queue.push(neighbor);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (sorted.length !== nodes.length) {
|
|
206
|
+
throw new GraphValidationError(
|
|
207
|
+
"Graph contains a cycle - DAG workflows cannot have cycles",
|
|
208
|
+
{ code: "CYCLE_DETECTED" }
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
return sorted;
|
|
212
|
+
}
|
|
213
|
+
function validateNewEdge(nodes, edges, newEdge) {
|
|
214
|
+
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
215
|
+
if (!nodeIds.has(newEdge.sourceNodeId)) {
|
|
216
|
+
throw new GraphValidationError(
|
|
217
|
+
`Source node does not exist: ${newEdge.sourceNodeId}`,
|
|
218
|
+
{
|
|
219
|
+
code: "INVALID_SOURCE_NODE",
|
|
220
|
+
details: { sourceNodeId: newEdge.sourceNodeId }
|
|
221
|
+
}
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
if (!nodeIds.has(newEdge.targetNodeId)) {
|
|
225
|
+
throw new GraphValidationError(
|
|
226
|
+
`Target node does not exist: ${newEdge.targetNodeId}`,
|
|
227
|
+
{
|
|
228
|
+
code: "INVALID_TARGET_NODE",
|
|
229
|
+
details: { targetNodeId: newEdge.targetNodeId }
|
|
230
|
+
}
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
if (newEdge.sourceNodeId === newEdge.targetNodeId) {
|
|
234
|
+
throw new GraphValidationError("Cannot create self-loop edge", {
|
|
235
|
+
code: "SELF_LOOP",
|
|
236
|
+
details: { nodeId: newEdge.sourceNodeId }
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
topologicalSort(nodes, [...edges, newEdge]);
|
|
240
|
+
}
|
|
241
|
+
var RESOURCE_NAME_REGEX = /^[a-zA-Z0-9_]+$/;
|
|
242
|
+
function validateResourceName(name) {
|
|
243
|
+
if (!name || name.length === 0) {
|
|
244
|
+
throw new GraphValidationError("Name is required", {
|
|
245
|
+
code: "INVALID_NODE_NAME",
|
|
246
|
+
details: { name }
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
if (!RESOURCE_NAME_REGEX.test(name)) {
|
|
250
|
+
throw new GraphValidationError(
|
|
251
|
+
`Name must contain only letters, numbers, and underscores: ${name}`,
|
|
252
|
+
{ code: "INVALID_NODE_NAME", details: { name } }
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// src/graph/graph-builder.ts
|
|
258
|
+
function randomId() {
|
|
259
|
+
return typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `gen-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
260
|
+
}
|
|
261
|
+
var GraphBuilder = class _GraphBuilder {
|
|
262
|
+
id;
|
|
263
|
+
name;
|
|
264
|
+
label;
|
|
265
|
+
description;
|
|
266
|
+
client;
|
|
267
|
+
_nodesById = /* @__PURE__ */ new Map();
|
|
268
|
+
_nodesByName = /* @__PURE__ */ new Map();
|
|
269
|
+
_edgesById = /* @__PURE__ */ new Map();
|
|
270
|
+
_edges = [];
|
|
271
|
+
_loadedFromApi = false;
|
|
272
|
+
constructor(options) {
|
|
273
|
+
this.id = randomId();
|
|
274
|
+
this.name = "graph";
|
|
275
|
+
this.label = "Graph";
|
|
276
|
+
this.description = null;
|
|
277
|
+
this.client = options?.client ?? null;
|
|
278
|
+
}
|
|
279
|
+
setName(name) {
|
|
280
|
+
validateResourceName(name);
|
|
281
|
+
this.name = name;
|
|
282
|
+
return this;
|
|
283
|
+
}
|
|
284
|
+
setLabel(label) {
|
|
285
|
+
this.label = label;
|
|
286
|
+
return this;
|
|
287
|
+
}
|
|
288
|
+
setDescription(description) {
|
|
289
|
+
this.description = description;
|
|
290
|
+
return this;
|
|
291
|
+
}
|
|
292
|
+
getNode(name) {
|
|
293
|
+
return this._nodesByName.get(name);
|
|
294
|
+
}
|
|
295
|
+
getNodeById(id) {
|
|
296
|
+
return this._nodesById.get(id);
|
|
297
|
+
}
|
|
298
|
+
getEdgesByTargetId(id) {
|
|
299
|
+
return this._edges.filter((e) => e.targetNodeId === id);
|
|
300
|
+
}
|
|
301
|
+
getEdgesBySourceId(id) {
|
|
302
|
+
return this._edges.filter((e) => e.sourceNodeId === id);
|
|
303
|
+
}
|
|
304
|
+
addNode(name, options) {
|
|
305
|
+
validateResourceName(name);
|
|
306
|
+
if (this._nodesByName.has(name)) {
|
|
307
|
+
throw new GraphValidationError(`Duplicate node name: ${name}`, {
|
|
308
|
+
code: "DUPLICATE_NODE_NAME",
|
|
309
|
+
details: { name }
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
const config = options.config;
|
|
313
|
+
if (config?.type !== options.type) {
|
|
314
|
+
throw new GraphValidationError(
|
|
315
|
+
`Node config type "${String(config?.type)}" does not match options type "${options.type}"`,
|
|
316
|
+
{ code: "INVALID_NODE_NAME", details: { name } }
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
const id = randomId();
|
|
320
|
+
const serialized = {
|
|
321
|
+
id,
|
|
322
|
+
name,
|
|
323
|
+
label: options.label,
|
|
324
|
+
description: options.description,
|
|
325
|
+
type: options.type,
|
|
326
|
+
config: options.config,
|
|
327
|
+
reviewConfig: options.reviewConfig ?? null,
|
|
328
|
+
inputSchema: options.inputSchema ?? null,
|
|
329
|
+
outputSchema: options.outputSchema ?? null,
|
|
330
|
+
position: options.position
|
|
331
|
+
};
|
|
332
|
+
this._addNodeRaw(serialized);
|
|
333
|
+
return this;
|
|
334
|
+
}
|
|
335
|
+
_addNodeRaw(data) {
|
|
336
|
+
const node = new NodeRef(
|
|
337
|
+
data,
|
|
338
|
+
() => this.getEdgesByTargetId(data.id),
|
|
339
|
+
() => this.getEdgesBySourceId(data.id)
|
|
340
|
+
);
|
|
341
|
+
this._nodesById.set(data.id, node);
|
|
342
|
+
this._nodesByName.set(data.name, node);
|
|
343
|
+
}
|
|
344
|
+
addEdge(options) {
|
|
345
|
+
const sourceNode = this._nodesByName.get(options.source);
|
|
346
|
+
const targetNode = this._nodesByName.get(options.target);
|
|
347
|
+
if (!sourceNode) {
|
|
348
|
+
throw new GraphValidationError(
|
|
349
|
+
`Source node not found: ${options.source}`,
|
|
350
|
+
{
|
|
351
|
+
code: "INVALID_SOURCE_NODE",
|
|
352
|
+
details: { source: options.source }
|
|
353
|
+
}
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
if (!targetNode) {
|
|
357
|
+
throw new GraphValidationError(
|
|
358
|
+
`Target node not found: ${options.target}`,
|
|
359
|
+
{
|
|
360
|
+
code: "INVALID_TARGET_NODE",
|
|
361
|
+
details: { target: options.target }
|
|
362
|
+
}
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
const validationNodes = Array.from(this._nodesById.values()).map((n) => ({
|
|
366
|
+
id: n.id
|
|
367
|
+
}));
|
|
368
|
+
const validationEdges = this._edges.map((e) => ({
|
|
369
|
+
sourceNodeId: e.sourceNodeId,
|
|
370
|
+
targetNodeId: e.targetNodeId
|
|
371
|
+
}));
|
|
372
|
+
const newEdge = {
|
|
373
|
+
sourceNodeId: sourceNode.id,
|
|
374
|
+
targetNodeId: targetNode.id
|
|
375
|
+
};
|
|
376
|
+
validateNewEdge(validationNodes, validationEdges, newEdge);
|
|
377
|
+
const id = randomId();
|
|
378
|
+
const serialized = {
|
|
379
|
+
id,
|
|
380
|
+
sourceNodeId: sourceNode.id,
|
|
381
|
+
targetNodeId: targetNode.id,
|
|
382
|
+
label: options.label ?? null
|
|
383
|
+
};
|
|
384
|
+
this._addEdgeRaw(serialized);
|
|
385
|
+
return this;
|
|
386
|
+
}
|
|
387
|
+
_addEdgeRaw(data) {
|
|
388
|
+
const edge = new EdgeRef(
|
|
389
|
+
data,
|
|
390
|
+
() => this.getNodeById(data.sourceNodeId),
|
|
391
|
+
() => this.getNodeById(data.targetNodeId)
|
|
392
|
+
);
|
|
393
|
+
this._edgesById.set(data.id, edge);
|
|
394
|
+
this._edges.push(edge);
|
|
395
|
+
}
|
|
396
|
+
removeNode(name) {
|
|
397
|
+
const node = this._nodesByName.get(name);
|
|
398
|
+
if (!node) {
|
|
399
|
+
return this;
|
|
400
|
+
}
|
|
401
|
+
const toRemove = this._edges.filter(
|
|
402
|
+
(e) => e.sourceNodeId === node.id || e.targetNodeId === node.id
|
|
403
|
+
);
|
|
404
|
+
for (const e of toRemove) {
|
|
405
|
+
this._edges = this._edges.filter((edge) => edge.id !== e.id);
|
|
406
|
+
this._edgesById.delete(e.id);
|
|
407
|
+
}
|
|
408
|
+
this._nodesById.delete(node.id);
|
|
409
|
+
this._nodesByName.delete(name);
|
|
410
|
+
return this;
|
|
411
|
+
}
|
|
412
|
+
removeEdge(options) {
|
|
413
|
+
const sourceNode = this._nodesByName.get(options.source);
|
|
414
|
+
const targetNode = this._nodesByName.get(options.target);
|
|
415
|
+
if (!sourceNode || !targetNode) {
|
|
416
|
+
return this;
|
|
417
|
+
}
|
|
418
|
+
const found = this._edges.find(
|
|
419
|
+
(e) => e.sourceNodeId === sourceNode.id && e.targetNodeId === targetNode.id
|
|
420
|
+
);
|
|
421
|
+
if (found) {
|
|
422
|
+
this._edges = this._edges.filter((edge) => edge.id !== found.id);
|
|
423
|
+
this._edgesById.delete(found.id);
|
|
424
|
+
}
|
|
425
|
+
return this;
|
|
426
|
+
}
|
|
427
|
+
get rootNode() {
|
|
428
|
+
const roots = this.nodeList.filter((n) => n.incomingEdges.length === 0);
|
|
429
|
+
return roots.length === 1 ? roots[0] : void 0;
|
|
430
|
+
}
|
|
431
|
+
get nodeList() {
|
|
432
|
+
return Array.from(this._nodesById.values());
|
|
433
|
+
}
|
|
434
|
+
get edgeList() {
|
|
435
|
+
return [...this._edges];
|
|
436
|
+
}
|
|
437
|
+
validate() {
|
|
438
|
+
const nodes = this.nodeList.map((n) => ({ id: n.id }));
|
|
439
|
+
const edges = this._edges.map((e) => ({
|
|
440
|
+
sourceNodeId: e.sourceNodeId,
|
|
441
|
+
targetNodeId: e.targetNodeId
|
|
442
|
+
}));
|
|
443
|
+
topologicalSort(nodes, edges);
|
|
444
|
+
}
|
|
445
|
+
async save(options) {
|
|
446
|
+
if (!this.client) {
|
|
447
|
+
throw new Error("Cannot save: no Swirls client provided");
|
|
448
|
+
}
|
|
449
|
+
this.validate();
|
|
450
|
+
if (!this._loadedFromApi) {
|
|
451
|
+
const created = await this.client.graphs.createGraph({
|
|
452
|
+
input: {
|
|
453
|
+
projectId: options.projectId,
|
|
454
|
+
folderId: options.folderId ?? null,
|
|
455
|
+
name: this.name,
|
|
456
|
+
label: this.label,
|
|
457
|
+
description: this.description ?? void 0
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
this.id = created.id;
|
|
461
|
+
this._loadedFromApi = true;
|
|
462
|
+
}
|
|
463
|
+
await this._syncToApi(this.id);
|
|
464
|
+
return this;
|
|
465
|
+
}
|
|
466
|
+
async _syncToApi(graphId) {
|
|
467
|
+
if (!this.client) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
const nodes = this.nodeList.map((node) => {
|
|
471
|
+
const nodeJson = node.toJSON();
|
|
472
|
+
return {
|
|
473
|
+
id: nodeJson.id,
|
|
474
|
+
name: nodeJson.name,
|
|
475
|
+
label: nodeJson.label,
|
|
476
|
+
description: nodeJson.description,
|
|
477
|
+
type: nodeJson.type,
|
|
478
|
+
config: nodeJson.config,
|
|
479
|
+
reviewConfig: nodeJson.reviewConfig ?? void 0,
|
|
480
|
+
position: nodeJson.position,
|
|
481
|
+
outputSchema: nodeJson.outputSchema ?? void 0,
|
|
482
|
+
inputSchema: nodeJson.inputSchema ?? void 0
|
|
483
|
+
};
|
|
484
|
+
});
|
|
485
|
+
const edges = this._edges.map((e) => e.toJSON());
|
|
486
|
+
await this.client.graphs.syncGraph({
|
|
487
|
+
input: {
|
|
488
|
+
graphId,
|
|
489
|
+
nodes,
|
|
490
|
+
edges: edges.map((ed) => ({
|
|
491
|
+
id: ed.id,
|
|
492
|
+
sourceNodeId: ed.sourceNodeId,
|
|
493
|
+
targetNodeId: ed.targetNodeId,
|
|
494
|
+
label: ed.label ?? void 0
|
|
495
|
+
}))
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
async saveExisting(_options) {
|
|
500
|
+
if (!this.client) {
|
|
501
|
+
throw new Error("Cannot save: no Swirls client provided");
|
|
502
|
+
}
|
|
503
|
+
if (!this._loadedFromApi) {
|
|
504
|
+
throw new Error(
|
|
505
|
+
"saveExisting() can only be used for graphs loaded via fromAPI(). Use save() for new graphs."
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
this.validate();
|
|
509
|
+
await this._syncToApi(this.id);
|
|
510
|
+
return this;
|
|
511
|
+
}
|
|
512
|
+
async execute(input) {
|
|
513
|
+
if (!this.client) {
|
|
514
|
+
throw new Error("Cannot execute: no Swirls client provided");
|
|
515
|
+
}
|
|
516
|
+
this.validate();
|
|
517
|
+
const result = await this.client.graphs.executeGraph({
|
|
518
|
+
input: { graphId: this.id, input }
|
|
519
|
+
});
|
|
520
|
+
return result.executionId;
|
|
521
|
+
}
|
|
522
|
+
toJSON() {
|
|
523
|
+
return {
|
|
524
|
+
id: this.id,
|
|
525
|
+
name: this.name,
|
|
526
|
+
label: this.label,
|
|
527
|
+
description: this.description,
|
|
528
|
+
nodes: this.nodeList.map((n) => n.toJSON()),
|
|
529
|
+
edges: this._edges.map((e) => e.toJSON())
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
toMermaid() {
|
|
533
|
+
const lines = ["flowchart LR"];
|
|
534
|
+
const nodeIds = new Set(this.nodeList.map((n) => n.id));
|
|
535
|
+
const safeId = (id) => mermaidSafeId(this.getNodeById(id)?.name ?? id);
|
|
536
|
+
for (const node of this.nodeList) {
|
|
537
|
+
const sid = safeId(node.id);
|
|
538
|
+
lines.push(` ${sid}["${node.label || node.name}"]`);
|
|
539
|
+
}
|
|
540
|
+
for (const edge of this._edges) {
|
|
541
|
+
if (nodeIds.has(edge.sourceNodeId) && nodeIds.has(edge.targetNodeId)) {
|
|
542
|
+
const from = safeId(edge.sourceNodeId);
|
|
543
|
+
const to = safeId(edge.targetNodeId);
|
|
544
|
+
const label = edge.label ? `|${edge.label}|` : "";
|
|
545
|
+
lines.push(` ${from} -->${label} ${to}`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return lines.join("\n");
|
|
549
|
+
}
|
|
550
|
+
static fromJSON(json) {
|
|
551
|
+
const data = typeof json === "string" ? JSON.parse(json) : json;
|
|
552
|
+
const g = new _GraphBuilder();
|
|
553
|
+
g.id = data.id;
|
|
554
|
+
g.name = data.name;
|
|
555
|
+
g.label = data.label;
|
|
556
|
+
g.description = data.description ?? null;
|
|
557
|
+
for (const node of data.nodes) {
|
|
558
|
+
g._addNodeRaw(node);
|
|
559
|
+
}
|
|
560
|
+
for (const edge of data.edges) {
|
|
561
|
+
g._addEdgeRaw(edge);
|
|
562
|
+
}
|
|
563
|
+
return g;
|
|
564
|
+
}
|
|
565
|
+
static async fromAPI(client, graphId) {
|
|
566
|
+
const res = await client.graphs.getGraph({ input: { id: graphId } });
|
|
567
|
+
const g = new _GraphBuilder({ client });
|
|
568
|
+
g.id = res.id;
|
|
569
|
+
g.name = res.name;
|
|
570
|
+
g.label = res.label;
|
|
571
|
+
g.description = res.description ?? null;
|
|
572
|
+
g._loadedFromApi = true;
|
|
573
|
+
for (const node of res.nodes) {
|
|
574
|
+
g._addNodeRaw({
|
|
575
|
+
id: node.id,
|
|
576
|
+
name: node.name,
|
|
577
|
+
label: node.label,
|
|
578
|
+
description: node.description ?? void 0,
|
|
579
|
+
type: node.type,
|
|
580
|
+
config: node.config,
|
|
581
|
+
reviewConfig: node.reviewConfig ?? void 0,
|
|
582
|
+
inputSchema: node.inputSchema ?? void 0,
|
|
583
|
+
outputSchema: node.outputSchema ?? void 0,
|
|
584
|
+
position: node.position ?? void 0
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
for (const edge of res.edges) {
|
|
588
|
+
g._addEdgeRaw({
|
|
589
|
+
id: edge.id,
|
|
590
|
+
sourceNodeId: edge.sourceNodeId,
|
|
591
|
+
targetNodeId: edge.targetNodeId,
|
|
592
|
+
label: edge.label ?? void 0
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
return g;
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
export {
|
|
599
|
+
EdgeRef,
|
|
600
|
+
GraphBuilder,
|
|
601
|
+
GraphValidationError,
|
|
602
|
+
NodeRef
|
|
603
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swirls/sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "Swirls SDK",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Swirls",
|
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
"types": "./dist/config/config.d.ts",
|
|
28
28
|
"import": "./dist/config/config.js",
|
|
29
29
|
"default": "./dist/config/config.js"
|
|
30
|
+
},
|
|
31
|
+
"./graph": {
|
|
32
|
+
"types": "./dist/graph/index.d.ts",
|
|
33
|
+
"import": "./dist/graph/index.js",
|
|
34
|
+
"default": "./dist/graph/index.js"
|
|
30
35
|
}
|
|
31
36
|
},
|
|
32
37
|
"files": [
|
|
@@ -48,6 +53,7 @@
|
|
|
48
53
|
},
|
|
49
54
|
"devDependencies": {
|
|
50
55
|
"@swirls/core": "0.0.1",
|
|
56
|
+
"@types/bun": "1.3.9",
|
|
51
57
|
"@types/node": "24.10.0",
|
|
52
58
|
"@types/react": "19.2.6",
|
|
53
59
|
"json-schema-to-zod": "2.7.0",
|