Skip to content

Commit 830590c

Browse files
committed
fixed branch coordination
1 parent 846b388 commit 830590c

8 files changed

Lines changed: 337 additions & 80 deletions

File tree

examples/gambling.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ async def __call__(self, state: GamblingState, shared: Shared) -> None:
6666
### GRAPH
6767

6868
asyncio.run(Graph[GamblingState, Shared](
69-
edges=[
70-
((
69+
edges=[(
70+
(
7171
START,
7272
guess
7373
),

examples/hello_world.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,15 @@ async def __call__(self, state: MyState, shared: Shared) -> None:
3232
### GRAPH
3333

3434
graph = Graph[MyState, Shared](
35-
edges=[
35+
edges=[(
3636
(
3737
START,
3838
node
3939
),
4040
(
4141
node,
4242
END
43-
)
43+
), END)
4444
]
4545
)
4646

src/edgygraph/graph/branches.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,18 @@ class Branch[T: StateProtocol, S: SharedProtocol]:
2020
join: The node to join the branch after execution. If None the branch will not be joined.
2121
2222
"""
23-
24-
edges: Sequence[Edge[T, S] | ErrorEdge[T, S] | NodeTupel[T, S]]
2523

26-
join: SingleNext[T, S]
2724

28-
result: asyncio.Future[dict[tuple[Hashable, ...], Change]] | None = None
29-
30-
edge_index: dict[SingleSource[T, S], list[Entry[T, S]]]
31-
error_edge_index: dict[SingleErrorSource[T, S], list[ErrorEntry[T, S]]]
32-
33-
34-
def __init__(self, edges: Sequence[Edge[T, S] | ErrorEdge[T, S] | NodeTupel[T, S]], join: SingleNext[T, S] = None) -> None:
25+
def __init__(self, edges: Sequence[Edge[T, S] | ErrorEdge[T, S] | NodeTupel[T, S]], start: SingleSource[T, S], join: SingleNext[T, S] = None) -> None:
3526

3627
self.edges = edges
28+
self.start = start
3729
self.join = join
3830

39-
self.edge_index = defaultdict(list)
40-
self.error_edge_index = defaultdict(list)
31+
self.result: asyncio.Future[dict[tuple[Hashable, ...], Change]] | None = None
32+
33+
self.edge_index: dict[SingleSource[T, S], list[Entry[T, S]]] = defaultdict(list)
34+
self.error_edge_index: dict[SingleErrorSource[T, S], list[ErrorEntry[T, S]]] = defaultdict(list)
4135

4236
self.index_edges()
4337

@@ -66,11 +60,12 @@ def index_edges(self) -> None:
6660

6761
match edge:
6862
case (source, next, config): pass
69-
case (source, next): config = Config() if source is START or isinstance(source, (Node, Sequence)) else ErrorConfig()
63+
case (source, next): config = None
7064
case _: raise ValueError(f"Invalid edge format: {edge}")
7165

7266
if Types[T, S].is_error_source(source):
73-
assert isinstance(config, ErrorConfig), f"Unexpected properties type for error edge: {config}"
67+
config = config or ErrorConfig()
68+
assert isinstance(config, ErrorConfig), f"Unexpected properties type for error edge {edge}: {config}"
7469

7570
if Types[T, S].is_single_error_source(source):
7671
self.error_edge_index[source].append(ErrorEntry[T, S](next=next, config=config, index=i))
@@ -81,7 +76,8 @@ def index_edges(self) -> None:
8176
raise ValueError(f"Invalid error source: {source}")
8277

8378
elif Types[T, S].is_source(source):
84-
assert isinstance(config, Config), f"Unexpected properties type for node edge: {config}"
79+
config = config or Config()
80+
assert isinstance(config, Config), f"Unexpected properties type for node edge {edge}: {config}"
8581

8682
if Types[T, S].is_single_source(source):
8783
self.edge_index[source].append(Entry[T, S](next=next, config=config, index=i))

src/edgygraph/graph/graphs.py

Lines changed: 95 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
from __future__ import annotations
33

4-
from typing import cast, Any, Hashable
4+
from typing import cast, Any, Hashable, Callable
55
from collections import defaultdict
66
from collections.abc import Hashable, Sequence
77
import asyncio
@@ -10,7 +10,7 @@
1010
from ..states import StateProtocol, SharedProtocol
1111
from ..diff import Change, ChangeConflictException, Diff
1212
from ..nodes import Node, END, START
13-
from .types import SingleNext, NextNode, ErrorEntry, SingleErrorSource, Entries, BranchContainer, SingleSource
13+
from .types import SingleNext, NextNode, ErrorEntry, SingleErrorSource, Entries, BranchContainer, SingleSource, Source, Types, ResolvedNext
1414
from .hooks import GraphHook
1515
from .branches import Branch
1616

@@ -92,14 +92,26 @@ def __init__(self,
9292
self.index_branches()
9393

9494
def index_branches(self) -> None:
95-
for branch in self.edges:
95+
for branch_container in self.edges:
9696

97-
source = branch[0][0][0]
97+
source = branch_container[0][0]
9898

99-
start_nodes = [source] if isinstance(source, (Node, type)) else source
99+
if Types[T, S].is_single_source(source):
100100

101-
for start_node in start_nodes:
102-
self.branch_registry[start_node].append(Branch[T, S](branch[0], branch[1]))
101+
branch = Branch[T, S](branch_container[:-1], source, branch_container[-1])
102+
self.branch_registry[source].append(branch)
103+
104+
elif Types[T, S].is_single_source_sequence(source):
105+
106+
for start_node in source:
107+
108+
branch = Branch[T, S](branch_container[:-1], start_node, branch_container[-1])
109+
self.branch_registry[start_node].append(branch)
110+
111+
else:
112+
raise ValueError(f"Invalid source type: {source}")
113+
114+
print(self.branch_registry)
103115

104116

105117
async def __call__(self, state: T, shared: S) -> tuple[T, S]:
@@ -157,7 +169,9 @@ async def run_branch(self, state: T, shared: S, branch: Branch[T, S]) -> None:
157169

158170
try:
159171

160-
next_nodes: list[NextNode[T, S]] = await self.get_next(state, shared, START, branch)
172+
next_nodes: list[NextNode[T, S]] = await self.get_next(state, shared, branch.start, branch)
173+
174+
print("INITIAL NEXT:", next_nodes)
161175

162176

163177
while next_nodes:
@@ -197,7 +211,7 @@ async def run_branch(self, state: T, shared: S, branch: Branch[T, S]) -> None:
197211

198212
else:
199213

200-
next_nodes = await self.get_next(state, shared, next_nodes, branch)
214+
next_nodes = await self.get_next(state, shared, [n.node for n in next_nodes], branch)
201215

202216
finally:
203217

@@ -215,6 +229,8 @@ async def run_branch(self, state: T, shared: S, branch: Branch[T, S]) -> None:
215229

216230
if e:
217231
raise e
232+
233+
print(" --- BRANCH RESULT --- ")
218234

219235
branch.result.set_result(Diff.recursive_diff(initial_state.model_dump(), state.model_dump()))
220236

@@ -333,7 +349,12 @@ async def spawn_branches(self, state: T, shared: S, next_nodes: list[NextNode[T,
333349

334350
for node in next_nodes:
335351
for branch in self.branch_registry[node.node]:
352+
353+
for h in self.hooks: await h.on_spawn_branch_start(state, shared, branch, node, self.branch_registry, self.join_registry)
354+
336355
self.spawn_branch(state, shared, branch)
356+
357+
for h in self.hooks: await h.on_spawn_branch_end(state, shared, branch, node, self.branch_registry, self.join_registry)
337358

338359

339360
async def join_branches(self, state: T, next_nodes: list[NextNode[T, S]]) -> T:
@@ -366,7 +387,7 @@ async def join_branches(self, state: T, next_nodes: list[NextNode[T, S]]) -> T:
366387

367388

368389

369-
async def get_next(self, state: T, shared: S, current_nodes: Sequence[NextNode[T, S]] | type[START], branch: Branch[T, S]) -> list[NextNode[T, S]]:
390+
async def get_next(self, state: T, shared: S, current_nodes: Source[T, S], branch: Branch[T, S]) -> list[NextNode[T, S]]:
370391
"""
371392
Get the next nodes to run based on the current nodes and the graph's edges.
372393
@@ -381,22 +402,26 @@ async def get_next(self, state: T, shared: S, current_nodes: Sequence[NextNode[T
381402
The list of the next nodes including their edges that they were reached by.
382403
"""
383404

405+
print(f"GET NEXT FOR CURRENT NODES: {current_nodes}")
384406

385407
next_list: list[NextNode[T, S]] = []
386408

387-
if isinstance(current_nodes, type): # START
388-
next_list.extend(
389-
await self.resolve_entries(state, shared, branch.edge_index[START])
390-
)
391-
392-
else: # Regular nodes
409+
if Types[T, S].is_single_source_sequence(current_nodes):
410+
print("IS SINGLE SOURCE SEQUENCE")
393411
for current_node in current_nodes:
394-
395-
# Find the edge corresponding to the current node
396412
next_list.extend(
397-
await self.resolve_entries(state, shared, branch.edge_index[current_node.node])
413+
await self.resolve_entries(state, shared, branch.edge_index[current_node])
398414
)
399415

416+
elif Types[T, S].is_single_source(current_nodes):
417+
print("IS SINGLE SOURCE")
418+
next_list.extend(
419+
await self.resolve_entries(state, shared, branch.edge_index[current_nodes])
420+
)
421+
422+
else:
423+
raise ValueError(f"Invalid current_nodes type: {type(current_nodes)}")
424+
400425

401426
# Instant nodes
402427
current_instant_next_list: list[NextNode[T, S]] = []
@@ -489,23 +514,16 @@ def match_error(self, e: Exception, source: SingleErrorSource[T, S], source_node
489514

490515
async def resolve_entries(self, state: T, shared: S, entries: Sequence[Entries[T, S]]) -> list[NextNode[T, S]]:
491516

492-
next_nodes: list[NextNode[T, S]] = []
517+
print(f"RESOLVE: {entries}")
493518

494-
for entry in entries:
495-
next_list = await self.resolve_entry(state, shared, entry)
519+
return [
520+
next_node
521+
for entry in entries
522+
for next_node in await self.resolve_entry(state, shared, entry)
523+
]
496524

497-
for x in next_list:
498525

499-
match x:
500-
case NextNode():
501-
next_nodes.append(x)
502-
case Branch():
503-
self.spawn_branch(state, shared, x)
504-
505-
return next_nodes
506-
507-
508-
async def resolve_entry(self, state: T, shared: S, entry: Entries[T, S]) -> list[NextNode[T, S] | Branch[T, S]]:
526+
async def resolve_entry(self, state: T, shared: S, entry: Entries[T, S]) -> list[NextNode[T, S]]:
509527
"""
510528
Resolve the next to nodes.
511529
@@ -519,36 +537,60 @@ async def resolve_entry(self, state: T, shared: S, entry: Entries[T, S]) -> list
519537
The resolved nodes.
520538
"""
521539

522-
next_nodes: list[NextNode[T, S] | Branch[T, S]] = []
540+
print(f"RESOLVING: {entry}")
541+
523542
next = entry.next
524543

525-
match next:
526544

527-
case None:
528-
pass # END
529545

530-
case type():
531-
assert next is END, "Only END is allowed as a type here"
532-
533-
case Node():
534-
next_nodes.append(NextNode[T, S](node=next, reached_by=entry))
546+
if not Types[T, S].is_resolved_next(next):
547+
print("CALLABLE")
548+
next = cast(Callable[[T, S], ResolvedNext[T, S]], next)
549+
next = next(state, shared)
550+
551+
if inspect.isawaitable(next):
552+
next = await next
553+
554+
555+
return [
556+
NextNode[T, S](node=node, reached_by=entry)
557+
for node in self.get_next_nodes(next)
558+
]
559+
560+
561+
562+
def get_next_nodes(self, next: ResolvedNext[T, S]) -> list[Node[T, S]]:
563+
564+
next_nodes: list[Node[T, S]] = []
565+
566+
def match(x: SingleNext[T, S]) -> None:
535567

536-
case Sequence():
537-
for n in next:
538-
if isinstance(n, Node):
539-
next_nodes.append(NextNode[T, S](node=n, reached_by=entry))
568+
match x:
540569

570+
case None:
571+
print("None")
572+
pass # END
541573

542-
case _: # callable
543-
next = next
544-
res = next(state, shared)
545-
if inspect.isawaitable(res):
546-
res = await res # for awaitables
574+
case type():
575+
print("END")
576+
assert next is END, "Only END is allowed as a type here"
547577

548-
if isinstance(res, Node):
549-
next_nodes.append(NextNode[T, S](node=res, reached_by=entry))
578+
case Node():
579+
print("Node")
580+
next_nodes.append(x)
550581

551582

583+
if Types[T, S].is_single_next_sequence(next):
584+
print("Sequence")
585+
for x in next:
586+
match(x)
587+
588+
elif Types[T, S].is_single_next(next):
589+
print("Single")
590+
match(next)
591+
552592
return next_nodes
593+
594+
553595

554596

src/edgygraph/graph/hooks.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33

44
from ..states import StateProtocol, SharedProtocol
55
from ..diff import Change
6-
from .types import NextNode
6+
from .branches import Branch
7+
from .types import NextNode, SingleSource, SingleNext
78

89

910

@@ -55,6 +56,37 @@ async def on_step_end(self, state: T, shared: S, nodes: list[NextNode[T, S]]) ->
5556
pass
5657

5758

59+
async def on_spawn_branch_start(self, state: T, shared: S, branch: Branch[T, S], trigger: NextNode[T, S], branch_registry: dict[SingleSource[T, S], list[Branch[T, S]]], join_registry: dict[SingleNext[T, S], list[Branch[T, S]]]):
60+
"""
61+
Called before a branch is spawned.
62+
63+
Args:
64+
state: The state of the graph.
65+
shared: The shared state of the graph.
66+
branch: The branch to be spawned.
67+
branch_registry: The branch registry of the graph.
68+
source_node: The node that spawned the branch.
69+
"""
70+
71+
pass
72+
73+
74+
async def on_spawn_branch_end(self, state: T, shared: S, branch: Branch[T, S], trigger: NextNode[T, S], branch_registry: dict[SingleSource[T, S], list[Branch[T, S]]], join_registry: dict[SingleNext[T, S], list[Branch[T, S]]]):
75+
"""
76+
Called after a branch is spawned.
77+
78+
Args:
79+
state: The state of the graph.
80+
shared: The shared state of the graph.
81+
branch: The branch that was spawned.
82+
branch_registry: The branch registry of the graph.
83+
trigger: The node that spawned the branch.
84+
"""
85+
86+
pass
87+
88+
89+
5890
async def on_merge_start(self, state: T, result_states: list[T], changes: list[dict[tuple[Hashable, ...], Change]]) -> None:
5991
"""
6092
Called when the merge process starts.

0 commit comments

Comments
 (0)