asyncnode

This commit is contained in:
zachary62 2024-12-25 20:19:21 +00:00
parent dcf7225131
commit 83dbc13054
2 changed files with 71 additions and 110 deletions

View File

@ -1,3 +1,5 @@
import asyncio
class BaseNode: class BaseNode:
""" """
A base node that provides: A base node that provides:
@ -33,9 +35,6 @@ class BaseNode:
return "default" return "default"
def run(self, shared_storage=None): def run(self, shared_storage=None):
if not shared_storage:
shared_storage = {}
prep = self.preprocess(shared_storage) prep = self.preprocess(shared_storage)
proc = self._process(shared_storage, prep) proc = self._process(shared_storage, prep)
return self.postprocess(shared_storage, prep, proc) return self.postprocess(shared_storage, prep, proc)
@ -93,117 +92,79 @@ class Node(BaseNode):
if attempt == self.max_retries - 1: if attempt == self.max_retries - 1:
return self.process_after_fail(shared_storage, data, e) return self.process_after_fail(shared_storage, data, e)
class InteractiveNode(BaseNode): class Flow(BaseNode):
"""
Interactive node. Instead of returning a condition,
we 'signal' the condition via a callback provided by the Flow.
"""
def postprocess(self, shared_storage, prep_result, proc_result, next_node_callback):
"""
We do NOT return anything. We call 'next_node_callback("some_condition")'
to tell the Flow which successor to pick.
"""
# e.g. here we pick "default", but in real usage you'd do logic or rely on user input
next_node_callback("default")
def run(self, shared_storage=None):
"""
Run just THIS node (no chain).
"""
if not shared_storage:
shared_storage = {}
# 1) Preprocess
prep = self.preprocess(shared_storage)
# 2) Process
proc = self._process(shared_storage, prep)
# 3) Postprocess with a dummy callback
def dummy_callback(condition="default"):
print("[Dummy callback] To run the flow, pass this node into a Flow instance.")
self.postprocess(shared_storage, prep, proc, dummy_callback)
def is_interactive(self):
return True
class Flow:
"""
A Flow that runs through a chain of nodes, from a start node onward.
Each iteration:
- preprocess
- process
- postprocess
The postprocess is given a callback to choose the next node.
We'll 'yield' the current node each time, so the caller can see progress.
"""
def __init__(self, start_node=None): def __init__(self, start_node=None):
self.start_node = start_node self.start_node = start_node
def run(self, shared_storage=None): def _process(self, shared_storage, _):
if shared_storage is None:
shared_storage = {}
current_node = self.start_node current_node = self.start_node
print("hihihi")
while current_node: while current_node:
# 1) Preprocess condition = current_node.run(shared_storage)
prep_result = current_node.preprocess(shared_storage) current_node = current_node.successors.get(condition, None)
print("prep")
# 2) Process
proc_result = current_node._process(shared_storage, prep_result)
# Prepare next_node variable def postprocess(self, shared_storage, prep_result, proc_result):
next_node = [None] return None
# We'll define a callback only if this is an interactive node.
# The callback sets next_node[0] based on condition.
def next_node_callback(condition="default"):
nxt = current_node.successors.get(condition)
next_node[0] = nxt
# 3) Check if it's an interactive node
is_interactive = (
hasattr(current_node, 'is_interactive')
and current_node.is_interactive()
)
if is_interactive: class AsyncNode(Node):
print("ineractive") """
# A Node whose postprocess step is async.
# ---- INTERACTIVE CASE ---- You can also override process() to be async if needed.
# """
# a) yield so that external code can do UI, etc.
# yield current_node, prep_result, proc_result, next_node_callback
# # b) Now we do postprocess WITH the callback: async def postprocess_async(self, shared_storage, prep_result, proc_result):
# current_node.postprocess( """
# shared_storage, Async version of postprocess. By default, returns "default".
# prep_result, Override as needed.
# proc_result, """
# next_node_callback await asyncio.sleep(0) # trivial async pause (no-op)
# ) return "default"
# # once postprocess is done, next_node[0] should be set
async def run_async(self, shared_storage=None):
"""
Async version of run.
If your process method is also async, you'll need to adapt accordingly.
"""
# We can keep preprocess synchronous or make it async as well,
# depending on your usage. Here it's left as sync for simplicity.
prep = self.preprocess(shared_storage)
# process can remain sync if you prefer, or you can define an async process.
proc = self._process(shared_storage, prep)
# postprocess is async
return await self.postprocess_async(shared_storage, prep, proc)
class AsyncFlow(Flow):
"""
A Flow that can handle a mixture of sync and async nodes.
If the node is an AsyncNode, calls `run_async`.
Otherwise, calls `run`.
"""
async def _process(self, shared_storage, _):
current_node = self.start_node
while current_node:
if hasattr(current_node, "run_async") and callable(current_node.run_async):
# If it's an async node, await its run_async
condition = await current_node.run_async(shared_storage)
else: else:
# # Otherwise, assume it's a sync node
# ---- NON-INTERACTIVE CASE ---- condition = current_node.run(shared_storage)
#
# We just call postprocess WITHOUT callback,
# and let it return the condition string:
condition = current_node.postprocess(
shared_storage,
prep_result,
proc_result
)
# Then we figure out the next node:
next_node[0] = current_node.successors.get(condition, None)
# 5) Move on to the next node current_node = current_node.successors.get(condition, None)
current_node = next_node[0]
async def run_async(self, shared_storage=None):
"""
Kicks off the async flow. Similar to Flow.run,
but uses our async _process method.
"""
prep = self.preprocess(shared_storage)
# Note: flows typically don't need a meaningful process step
# because the "process" is the iteration through the nodes.
await self._process(shared_storage, prep)
return self.postprocess(shared_storage, prep, None)
class BatchNode(BaseNode): class BatchNode(BaseNode):
def __init__(self, max_retries=5, delay_s=0.1): def __init__(self, max_retries=5, delay_s=0.1):