mcp-blackboard-graph 0.1.0__tar.gz
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.
- mcp_blackboard_graph-0.1.0/PKG-INFO +6 -0
- mcp_blackboard_graph-0.1.0/README.md +20 -0
- mcp_blackboard_graph-0.1.0/mcp_blackboard_graph/__init__.py +0 -0
- mcp_blackboard_graph-0.1.0/mcp_blackboard_graph/main.py +354 -0
- mcp_blackboard_graph-0.1.0/mcp_blackboard_graph.egg-info/PKG-INFO +6 -0
- mcp_blackboard_graph-0.1.0/mcp_blackboard_graph.egg-info/SOURCES.txt +11 -0
- mcp_blackboard_graph-0.1.0/mcp_blackboard_graph.egg-info/dependency_links.txt +1 -0
- mcp_blackboard_graph-0.1.0/mcp_blackboard_graph.egg-info/entry_points.txt +2 -0
- mcp_blackboard_graph-0.1.0/mcp_blackboard_graph.egg-info/requires.txt +1 -0
- mcp_blackboard_graph-0.1.0/mcp_blackboard_graph.egg-info/top_level.txt +1 -0
- mcp_blackboard_graph-0.1.0/pyproject.toml +16 -0
- mcp_blackboard_graph-0.1.0/setup.cfg +4 -0
- mcp_blackboard_graph-0.1.0/tests/test_graph.py +101 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# mcp-blackboard-graph
|
|
2
|
+
A common blackboard for a number of LLM agents to write on for coordination.
|
|
3
|
+
This takes the form of a directed graph. You can send messages to nodes.
|
|
4
|
+
|
|
5
|
+
This is a relative general structure but could be used for a tree of tasks.
|
|
6
|
+
|
|
7
|
+
This is pure AI-generated code - review before use.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
pipx install mcp-blackboard-graph
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
mcp-blackboard-graph will be on your path. Wire it up to your mcp server.
|
|
15
|
+
|
|
16
|
+
## Alternatives
|
|
17
|
+
`task-tree-mcp` seems to concern itself with plans. This was too high leel for my interestes.
|
|
18
|
+
|
|
19
|
+
`mcp-blackboard` did not have messages which I really wanted.
|
|
20
|
+
|
|
File without changes
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
MCP server for multi-agent coordination via shared DAG.
|
|
4
|
+
|
|
5
|
+
Nodes have: id, parents, children, status, notes, messages, data.
|
|
6
|
+
Multiple agents can read/write to the graph.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from mcp.server import Server
|
|
16
|
+
from mcp.server.stdio import stdio_server
|
|
17
|
+
from mcp.types import Tool, TextContent
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_graph_path():
|
|
21
|
+
"""Get path to graph.json file."""
|
|
22
|
+
path = os.environ.get("BLACKBOARD_GRAPH_PATH")
|
|
23
|
+
if path:
|
|
24
|
+
return Path(path)
|
|
25
|
+
return Path.cwd() / "graph.json"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_graph():
|
|
29
|
+
"""Load graph from JSON file."""
|
|
30
|
+
path = get_graph_path()
|
|
31
|
+
if path.exists():
|
|
32
|
+
return json.loads(path.read_text())
|
|
33
|
+
return {
|
|
34
|
+
"nodes": {
|
|
35
|
+
"root": {
|
|
36
|
+
"id": "root",
|
|
37
|
+
"parents": [],
|
|
38
|
+
"children": [],
|
|
39
|
+
"status": "active",
|
|
40
|
+
"notes": [],
|
|
41
|
+
"messages": [],
|
|
42
|
+
"data": {}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def save_graph(graph):
|
|
49
|
+
"""Save graph to JSON file."""
|
|
50
|
+
path = get_graph_path()
|
|
51
|
+
path.write_text(json.dumps(graph, indent=2) + "\n")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_node(graph, node_id):
|
|
55
|
+
"""Get a node by id."""
|
|
56
|
+
return graph["nodes"].get(node_id)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def ensure_node_fields(node):
|
|
60
|
+
"""Ensure node has all required fields."""
|
|
61
|
+
node.setdefault("parents", [])
|
|
62
|
+
node.setdefault("children", [])
|
|
63
|
+
node.setdefault("status", "active")
|
|
64
|
+
node.setdefault("notes", [])
|
|
65
|
+
node.setdefault("messages", [])
|
|
66
|
+
node.setdefault("data", {})
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def format_node(node, show_notes=True, show_messages=True, show_data=True):
|
|
70
|
+
"""Format a single node for display."""
|
|
71
|
+
parts = [f"{node['id']} [{node.get('status', 'active')}]"]
|
|
72
|
+
if node.get("parents"):
|
|
73
|
+
parts.append(f" parents: {', '.join(node['parents'])}")
|
|
74
|
+
if node.get("children"):
|
|
75
|
+
parts.append(f" children: {', '.join(node['children'])}")
|
|
76
|
+
if show_notes and node.get("notes"):
|
|
77
|
+
parts.append(f" notes: {len(node['notes'])}")
|
|
78
|
+
for n in node["notes"]:
|
|
79
|
+
parts.append(f" - {n[:50]}{'...' if len(n) > 50 else ''}")
|
|
80
|
+
if show_messages and node.get("messages"):
|
|
81
|
+
parts.append(f" messages: {len(node['messages'])}")
|
|
82
|
+
for m in node["messages"]:
|
|
83
|
+
parts.append(f" - [{m.get('from', '?')}] {m['text'][:40]}{'...' if len(m['text']) > 40 else ''}")
|
|
84
|
+
if show_data and node.get("data"):
|
|
85
|
+
parts.append(f" data: {json.dumps(node['data'])[:60]}{'...' if len(json.dumps(node['data'])) > 60 else ''}")
|
|
86
|
+
return "\n".join(parts)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def format_graph(graph, node_id=None, show_notes=True, show_messages=True, show_data=True):
|
|
90
|
+
"""Format graph or single node for display."""
|
|
91
|
+
if node_id:
|
|
92
|
+
node = get_node(graph, node_id)
|
|
93
|
+
if not node:
|
|
94
|
+
return f"Error: node '{node_id}' not found"
|
|
95
|
+
return format_node(node, show_notes, show_messages, show_data)
|
|
96
|
+
|
|
97
|
+
lines = []
|
|
98
|
+
for nid, node in graph["nodes"].items():
|
|
99
|
+
lines.append(format_node(node, show_notes, show_messages, show_data))
|
|
100
|
+
lines.append("")
|
|
101
|
+
return "\n".join(lines).strip()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def run_server():
|
|
105
|
+
"""Run the MCP server."""
|
|
106
|
+
server = Server("mcp-blackboard-graph")
|
|
107
|
+
|
|
108
|
+
tools = [
|
|
109
|
+
Tool(
|
|
110
|
+
name="show",
|
|
111
|
+
description="Show the graph or a single node",
|
|
112
|
+
inputSchema={
|
|
113
|
+
"type": "object",
|
|
114
|
+
"properties": {
|
|
115
|
+
"node": {"type": "string", "description": "Node id to show (omit for all)"},
|
|
116
|
+
"hide_notes": {"type": "boolean", "description": "Hide notes", "default": False},
|
|
117
|
+
"hide_messages": {"type": "boolean", "description": "Hide messages", "default": False},
|
|
118
|
+
"hide_data": {"type": "boolean", "description": "Hide data", "default": False}
|
|
119
|
+
},
|
|
120
|
+
"required": []
|
|
121
|
+
}
|
|
122
|
+
),
|
|
123
|
+
Tool(
|
|
124
|
+
name="add",
|
|
125
|
+
description="Add a new node to the graph",
|
|
126
|
+
inputSchema={
|
|
127
|
+
"type": "object",
|
|
128
|
+
"properties": {
|
|
129
|
+
"id": {"type": "string", "description": "Node id (unique)"},
|
|
130
|
+
"parents": {
|
|
131
|
+
"type": "array",
|
|
132
|
+
"items": {"type": "string"},
|
|
133
|
+
"description": "Parent node ids (for DAG structure)"
|
|
134
|
+
},
|
|
135
|
+
"status": {"type": "string", "description": "Initial status (default: active)"},
|
|
136
|
+
"data": {"type": "object", "description": "Initial data dict"}
|
|
137
|
+
},
|
|
138
|
+
"required": ["id"]
|
|
139
|
+
}
|
|
140
|
+
),
|
|
141
|
+
Tool(
|
|
142
|
+
name="remove",
|
|
143
|
+
description="Remove a node from the graph",
|
|
144
|
+
inputSchema={
|
|
145
|
+
"type": "object",
|
|
146
|
+
"properties": {
|
|
147
|
+
"id": {"type": "string", "description": "Node id to remove"}
|
|
148
|
+
},
|
|
149
|
+
"required": ["id"]
|
|
150
|
+
}
|
|
151
|
+
),
|
|
152
|
+
Tool(
|
|
153
|
+
name="get",
|
|
154
|
+
description="Get full details of a node as JSON",
|
|
155
|
+
inputSchema={
|
|
156
|
+
"type": "object",
|
|
157
|
+
"properties": {
|
|
158
|
+
"id": {"type": "string", "description": "Node id"}
|
|
159
|
+
},
|
|
160
|
+
"required": ["id"]
|
|
161
|
+
}
|
|
162
|
+
),
|
|
163
|
+
Tool(
|
|
164
|
+
name="set_status",
|
|
165
|
+
description="Set status of a node",
|
|
166
|
+
inputSchema={
|
|
167
|
+
"type": "object",
|
|
168
|
+
"properties": {
|
|
169
|
+
"id": {"type": "string", "description": "Node id"},
|
|
170
|
+
"status": {"type": "string", "description": "New status"}
|
|
171
|
+
},
|
|
172
|
+
"required": ["id", "status"]
|
|
173
|
+
}
|
|
174
|
+
),
|
|
175
|
+
Tool(
|
|
176
|
+
name="add_data",
|
|
177
|
+
description="Merge data dict into a node's data",
|
|
178
|
+
inputSchema={
|
|
179
|
+
"type": "object",
|
|
180
|
+
"properties": {
|
|
181
|
+
"id": {"type": "string", "description": "Node id"},
|
|
182
|
+
"data": {"type": "object", "description": "Data to merge"}
|
|
183
|
+
},
|
|
184
|
+
"required": ["id", "data"]
|
|
185
|
+
}
|
|
186
|
+
),
|
|
187
|
+
Tool(
|
|
188
|
+
name="add_note",
|
|
189
|
+
description="Add a note to a node",
|
|
190
|
+
inputSchema={
|
|
191
|
+
"type": "object",
|
|
192
|
+
"properties": {
|
|
193
|
+
"id": {"type": "string", "description": "Node id"},
|
|
194
|
+
"note": {"type": "string", "description": "Note text"}
|
|
195
|
+
},
|
|
196
|
+
"required": ["id", "note"]
|
|
197
|
+
}
|
|
198
|
+
),
|
|
199
|
+
Tool(
|
|
200
|
+
name="send_message",
|
|
201
|
+
description="Send a message to a node",
|
|
202
|
+
inputSchema={
|
|
203
|
+
"type": "object",
|
|
204
|
+
"properties": {
|
|
205
|
+
"id": {"type": "string", "description": "Node id"},
|
|
206
|
+
"message": {"type": "string", "description": "Message text"},
|
|
207
|
+
"from_agent": {"type": "string", "description": "Sender name (optional)"}
|
|
208
|
+
},
|
|
209
|
+
"required": ["id", "message"]
|
|
210
|
+
}
|
|
211
|
+
),
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
@server.list_tools()
|
|
215
|
+
async def list_tools():
|
|
216
|
+
return tools
|
|
217
|
+
|
|
218
|
+
@server.call_tool()
|
|
219
|
+
async def call_tool(name: str, arguments: dict):
|
|
220
|
+
graph = load_graph()
|
|
221
|
+
|
|
222
|
+
if name == "show":
|
|
223
|
+
node_id = arguments.get("node")
|
|
224
|
+
show_notes = not arguments.get("hide_notes", False)
|
|
225
|
+
show_messages = not arguments.get("hide_messages", False)
|
|
226
|
+
show_data = not arguments.get("hide_data", False)
|
|
227
|
+
result = format_graph(graph, node_id, show_notes, show_messages, show_data)
|
|
228
|
+
return [TextContent(type="text", text=result)]
|
|
229
|
+
|
|
230
|
+
elif name == "add":
|
|
231
|
+
node_id = arguments.get("id", "").strip()
|
|
232
|
+
parents = arguments.get("parents", [])
|
|
233
|
+
status = arguments.get("status", "active")
|
|
234
|
+
data = arguments.get("data", {})
|
|
235
|
+
|
|
236
|
+
if not node_id:
|
|
237
|
+
return [TextContent(type="text", text="Error: id is required")]
|
|
238
|
+
if node_id in graph["nodes"]:
|
|
239
|
+
return [TextContent(type="text", text=f"Error: node '{node_id}' already exists")]
|
|
240
|
+
|
|
241
|
+
# Validate parents exist
|
|
242
|
+
for p in parents:
|
|
243
|
+
if p not in graph["nodes"]:
|
|
244
|
+
return [TextContent(type="text", text=f"Error: parent '{p}' not found")]
|
|
245
|
+
|
|
246
|
+
# Create node
|
|
247
|
+
graph["nodes"][node_id] = {
|
|
248
|
+
"id": node_id,
|
|
249
|
+
"parents": parents,
|
|
250
|
+
"children": [],
|
|
251
|
+
"status": status,
|
|
252
|
+
"notes": [],
|
|
253
|
+
"messages": [],
|
|
254
|
+
"data": data
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
# Update parent nodes' children lists
|
|
258
|
+
for p in parents:
|
|
259
|
+
if node_id not in graph["nodes"][p]["children"]:
|
|
260
|
+
graph["nodes"][p]["children"].append(node_id)
|
|
261
|
+
|
|
262
|
+
save_graph(graph)
|
|
263
|
+
return [TextContent(type="text", text=f"Added node '{node_id}'")]
|
|
264
|
+
|
|
265
|
+
elif name == "remove":
|
|
266
|
+
node_id = arguments.get("id", "")
|
|
267
|
+
if node_id == "root":
|
|
268
|
+
return [TextContent(type="text", text="Error: cannot remove root")]
|
|
269
|
+
if node_id not in graph["nodes"]:
|
|
270
|
+
return [TextContent(type="text", text=f"Error: node '{node_id}' not found")]
|
|
271
|
+
|
|
272
|
+
node = graph["nodes"][node_id]
|
|
273
|
+
|
|
274
|
+
# Remove from parents' children lists
|
|
275
|
+
for p in node.get("parents", []):
|
|
276
|
+
if p in graph["nodes"]:
|
|
277
|
+
graph["nodes"][p]["children"] = [c for c in graph["nodes"][p]["children"] if c != node_id]
|
|
278
|
+
|
|
279
|
+
# Remove from children's parents lists
|
|
280
|
+
for c in node.get("children", []):
|
|
281
|
+
if c in graph["nodes"]:
|
|
282
|
+
graph["nodes"][c]["parents"] = [p for p in graph["nodes"][c]["parents"] if p != node_id]
|
|
283
|
+
|
|
284
|
+
del graph["nodes"][node_id]
|
|
285
|
+
save_graph(graph)
|
|
286
|
+
return [TextContent(type="text", text=f"Removed node '{node_id}'")]
|
|
287
|
+
|
|
288
|
+
elif name == "get":
|
|
289
|
+
node_id = arguments.get("id", "")
|
|
290
|
+
node = get_node(graph, node_id)
|
|
291
|
+
if not node:
|
|
292
|
+
return [TextContent(type="text", text=f"Error: node '{node_id}' not found")]
|
|
293
|
+
return [TextContent(type="text", text=json.dumps(node, indent=2))]
|
|
294
|
+
|
|
295
|
+
elif name == "set_status":
|
|
296
|
+
node_id = arguments.get("id", "")
|
|
297
|
+
status = arguments.get("status", "")
|
|
298
|
+
node = get_node(graph, node_id)
|
|
299
|
+
if not node:
|
|
300
|
+
return [TextContent(type="text", text=f"Error: node '{node_id}' not found")]
|
|
301
|
+
node["status"] = status
|
|
302
|
+
save_graph(graph)
|
|
303
|
+
return [TextContent(type="text", text=f"Set status of '{node_id}' to '{status}'")]
|
|
304
|
+
|
|
305
|
+
elif name == "add_data":
|
|
306
|
+
node_id = arguments.get("id", "")
|
|
307
|
+
data = arguments.get("data", {})
|
|
308
|
+
node = get_node(graph, node_id)
|
|
309
|
+
if not node:
|
|
310
|
+
return [TextContent(type="text", text=f"Error: node '{node_id}' not found")]
|
|
311
|
+
ensure_node_fields(node)
|
|
312
|
+
node["data"].update(data)
|
|
313
|
+
save_graph(graph)
|
|
314
|
+
return [TextContent(type="text", text=f"Added data to '{node_id}'")]
|
|
315
|
+
|
|
316
|
+
elif name == "add_note":
|
|
317
|
+
node_id = arguments.get("id", "")
|
|
318
|
+
note = arguments.get("note", "")
|
|
319
|
+
node = get_node(graph, node_id)
|
|
320
|
+
if not node:
|
|
321
|
+
return [TextContent(type="text", text=f"Error: node '{node_id}' not found")]
|
|
322
|
+
ensure_node_fields(node)
|
|
323
|
+
node["notes"].append(note)
|
|
324
|
+
save_graph(graph)
|
|
325
|
+
return [TextContent(type="text", text=f"Added note to '{node_id}'")]
|
|
326
|
+
|
|
327
|
+
elif name == "send_message":
|
|
328
|
+
node_id = arguments.get("id", "")
|
|
329
|
+
message = arguments.get("message", "")
|
|
330
|
+
from_agent = arguments.get("from_agent", "anonymous")
|
|
331
|
+
node = get_node(graph, node_id)
|
|
332
|
+
if not node:
|
|
333
|
+
return [TextContent(type="text", text=f"Error: node '{node_id}' not found")]
|
|
334
|
+
ensure_node_fields(node)
|
|
335
|
+
node["messages"].append({
|
|
336
|
+
"from": from_agent,
|
|
337
|
+
"text": message,
|
|
338
|
+
"time": datetime.now().isoformat()
|
|
339
|
+
})
|
|
340
|
+
save_graph(graph)
|
|
341
|
+
return [TextContent(type="text", text=f"Message sent to '{node_id}'")]
|
|
342
|
+
|
|
343
|
+
return [TextContent(type="text", text=f"Unknown tool: {name}")]
|
|
344
|
+
|
|
345
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
346
|
+
await server.run(read_stream, write_stream, server.create_initialization_options())
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def main():
|
|
350
|
+
asyncio.run(run_server())
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
if __name__ == "__main__":
|
|
354
|
+
main()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
mcp_blackboard_graph/__init__.py
|
|
4
|
+
mcp_blackboard_graph/main.py
|
|
5
|
+
mcp_blackboard_graph.egg-info/PKG-INFO
|
|
6
|
+
mcp_blackboard_graph.egg-info/SOURCES.txt
|
|
7
|
+
mcp_blackboard_graph.egg-info/dependency_links.txt
|
|
8
|
+
mcp_blackboard_graph.egg-info/entry_points.txt
|
|
9
|
+
mcp_blackboard_graph.egg-info/requires.txt
|
|
10
|
+
mcp_blackboard_graph.egg-info/top_level.txt
|
|
11
|
+
tests/test_graph.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mcp>=1.0.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mcp_blackboard_graph
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mcp-blackboard-graph"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "MCP server for multi-agent coordination via shared graph - nodes with messages, notes, and status"
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
dependencies = ["mcp>=1.0.0"]
|
|
7
|
+
|
|
8
|
+
[project.scripts]
|
|
9
|
+
mcp-blackboard-graph = "mcp_blackboard_graph.main:main"
|
|
10
|
+
|
|
11
|
+
[build-system]
|
|
12
|
+
requires = ["setuptools"]
|
|
13
|
+
build-backend = "setuptools.build_meta"
|
|
14
|
+
|
|
15
|
+
[tool.setuptools]
|
|
16
|
+
packages = ["mcp_blackboard_graph"]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Test the blackboard graph."""
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
|
|
6
|
+
# Use temp dir for graph
|
|
7
|
+
tmpdir = tempfile.mkdtemp(prefix="bbg-test-")
|
|
8
|
+
os.environ["BLACKBOARD_GRAPH_PATH"] = f"{tmpdir}/graph.json"
|
|
9
|
+
|
|
10
|
+
from mcp_blackboard_graph.main import load_graph, save_graph, get_node, ensure_node_fields
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_load_empty():
|
|
14
|
+
graph = load_graph()
|
|
15
|
+
assert "nodes" in graph
|
|
16
|
+
assert "root" in graph["nodes"]
|
|
17
|
+
print(" load empty: ok")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_add_node():
|
|
21
|
+
graph = load_graph()
|
|
22
|
+
graph["nodes"]["child1"] = {
|
|
23
|
+
"id": "child1",
|
|
24
|
+
"parents": ["root"],
|
|
25
|
+
"children": [],
|
|
26
|
+
"status": "active",
|
|
27
|
+
"notes": [],
|
|
28
|
+
"messages": [],
|
|
29
|
+
"data": {}
|
|
30
|
+
}
|
|
31
|
+
graph["nodes"]["root"]["children"].append("child1")
|
|
32
|
+
save_graph(graph)
|
|
33
|
+
|
|
34
|
+
graph = load_graph()
|
|
35
|
+
node = get_node(graph, "child1")
|
|
36
|
+
assert node is not None
|
|
37
|
+
assert node["id"] == "child1"
|
|
38
|
+
assert "root" in node["parents"]
|
|
39
|
+
print(" add node: ok")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_dag_multiple_parents():
|
|
43
|
+
graph = load_graph()
|
|
44
|
+
# Add second parent
|
|
45
|
+
graph["nodes"]["parent2"] = {
|
|
46
|
+
"id": "parent2",
|
|
47
|
+
"parents": ["root"],
|
|
48
|
+
"children": ["child1"],
|
|
49
|
+
"status": "active",
|
|
50
|
+
"notes": [],
|
|
51
|
+
"messages": [],
|
|
52
|
+
"data": {}
|
|
53
|
+
}
|
|
54
|
+
graph["nodes"]["root"]["children"].append("parent2")
|
|
55
|
+
graph["nodes"]["child1"]["parents"].append("parent2")
|
|
56
|
+
save_graph(graph)
|
|
57
|
+
|
|
58
|
+
graph = load_graph()
|
|
59
|
+
node = get_node(graph, "child1")
|
|
60
|
+
assert len(node["parents"]) == 2
|
|
61
|
+
assert "root" in node["parents"]
|
|
62
|
+
assert "parent2" in node["parents"]
|
|
63
|
+
print(" dag multiple parents: ok")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_notes_and_messages():
|
|
67
|
+
graph = load_graph()
|
|
68
|
+
node = get_node(graph, "child1")
|
|
69
|
+
ensure_node_fields(node)
|
|
70
|
+
node["notes"].append("test note")
|
|
71
|
+
node["messages"].append({"from": "agent1", "text": "hello", "time": "now"})
|
|
72
|
+
save_graph(graph)
|
|
73
|
+
|
|
74
|
+
graph = load_graph()
|
|
75
|
+
node = get_node(graph, "child1")
|
|
76
|
+
assert len(node["notes"]) == 1
|
|
77
|
+
assert len(node["messages"]) == 1
|
|
78
|
+
print(" notes and messages: ok")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_data():
|
|
82
|
+
graph = load_graph()
|
|
83
|
+
node = get_node(graph, "child1")
|
|
84
|
+
ensure_node_fields(node)
|
|
85
|
+
node["data"]["key"] = "value"
|
|
86
|
+
node["data"]["count"] = 42
|
|
87
|
+
save_graph(graph)
|
|
88
|
+
|
|
89
|
+
graph = load_graph()
|
|
90
|
+
node = get_node(graph, "child1")
|
|
91
|
+
assert node["data"]["key"] == "value"
|
|
92
|
+
assert node["data"]["count"] == 42
|
|
93
|
+
print(" data: ok")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
tests = [test_load_empty, test_add_node, test_dag_multiple_parents, test_notes_and_messages, test_data]
|
|
98
|
+
for t in tests:
|
|
99
|
+
print(f"{t.__name__}:")
|
|
100
|
+
t()
|
|
101
|
+
print(f"\nAll {len(tests)} tests passed!")
|