Merge pull request #109 from Dawinia/fix107

FIX: RecursionError when loop flow
This commit is contained in:
Zachary Huang 2025-08-13 10:27:39 -07:00 committed by GitHub
commit 23e36bfbdf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 160 additions and 6 deletions

View File

@ -0,0 +1,73 @@
from async_flow import *
from pocketflow import Flow, AsyncParallelBatchNode, Node
# Create node instances
validate_payment = ValidatePayment()
process_payment = ProcessPayment()
payment_confirmation = PaymentConfirmation()
check_stock = CheckStock()
reserve_items = ReserveItems()
update_inventory = UpdateInventory()
create_label = CreateLabel()
assign_carrier = AssignCarrier()
schedule_pickup = SchedulePickup()
# Payment processing sub-flow
validate_payment >> process_payment
validate_payment - "out_of_stock" >> validate_payment # 循环重试
process_payment - 'something fail' >> validate_payment
process_payment - 'pass' >> payment_confirmation
payment_flow = AsyncFlow(start=validate_payment)
# Inventory sub-flow
check_stock >> reserve_items >> update_inventory
inventory_flow = AsyncFlow(start=check_stock)
# Shipping sub-flow
create_label >> assign_carrier >> schedule_pickup
shipping_flow = AsyncFlow(start=create_label)
# Connect the flows into a main order pipeline
payment_flow >> inventory_flow >> shipping_flow
# payment_flow >> inventory_flow >> create_label
# payment_flow >> inventory_flow >> assign_carrier
# Create the master flow
class OrderFlow(AsyncFlow):
pass
order_pipeline = OrderFlow(start=payment_flow)
# Create shared data structure
shared_data = {
"order_id": "ORD-12345",
"customer": "John Doe",
"items": [
{"id": "ITEM-001", "name": "Smartphone", "price": 999.99, "quantity": 1},
{"id": "ITEM-002", "name": "Phone case", "price": 29.99, "quantity": 1},
],
"shipping_address": {
"street": "123 Main St",
"city": "Anytown",
"state": "CA",
"zip": "12345",
},
}
# Run the entire pipeline asynchronously
async def main():
await order_pipeline.run_async(shared_data)
# Print final status
print("\nOrder processing completed!")
print(f"Payment: {shared_data.get('payment_confirmation')}")
print(f"Inventory: {shared_data.get('inventory_update')}")
print(f"Shipping: {shared_data.get('pickup_status')}")
if __name__ == "__main__":
asyncio.run(main())

View File

@ -77,6 +77,7 @@ def flow_to_json(start):
node_types = {}
flow_nodes = {} # Keep track of flow nodes
ctr = 1
visited = set()
def get_id(n):
nonlocal ctr
@ -99,6 +100,9 @@ def flow_to_json(start):
action: Action label on the edge from parent to this node
"""
node_id = get_id(node)
if (node_id, action) in visited:
return
visited.add((node_id, action))
# Add node if not already in nodes list and not a Flow
if not any(n["id"] == node_id for n in nodes) and not isinstance(node, Flow):
@ -552,8 +556,38 @@ def create_d3_visualization(
// Update positions on each tick
simulation.on("tick", () => {
// Update links with straight lines
// Update links with curved paths for bidirectional connections
link.attr("d", d => {
// Handle self-referencing links with a water-drop shape
if (d.source === d.target) {
const nodeX = d.source.x;
const nodeY = d.source.y;
const offsetX = 40;
const offsetY = 10;
const controlOffset = 50;
// Create a water-drop shaped path
return `M ${nodeX},${nodeY - 5}
C ${nodeX + controlOffset},${nodeY - 30}
${nodeX + offsetX},${nodeY + offsetY}
${nodeX},${nodeY}`;
}
// Check if there's a reverse connection
const isReverse = data.links.some(l =>
l.source === d.target && l.target === d.source
);
// If it's part of a bidirectional connection, curve the path
if (isReverse) {
const dx = d.target.x - d.source.x;
const dy = d.target.y - d.source.y;
const dr = Math.sqrt(dx * dx + dy * dy) * 0.9;
return `M${d.source.x},${d.source.y}A${dr},${dr} 0 0,1 ${d.target.x},${d.target.y}`;
}
// For unidirectional connections, use straight lines
return `M${d.source.x},${d.source.y} L${d.target.x},${d.target.y}`;
});
@ -567,10 +601,57 @@ def create_d3_visualization(
.attr("x", d => d.x)
.attr("y", d => d.y);
// Position link labels at midpoint
linkLabel
.attr("x", d => (d.source.x + d.target.x) / 2)
.attr("y", d => (d.source.y + d.target.y) / 2);
// Position link labels with offset for bidirectional connections
linkLabel.attr("x", d => {
// Handle self-referencing links
if (d.source === d.target) {
return d.source.x + 30;
}
// Check if there's a reverse connection
const reverseLink = data.links.find(l =>
l.source === d.target && l.target === d.source
);
// If it's part of a bidirectional connection, offset the label
if (reverseLink) {
const dx = d.target.x - d.source.x;
const dy = d.target.y - d.source.y;
// Calculate perpendicular offset
const length = Math.sqrt(dx * dx + dy * dy);
const offsetX = -dy / length * 10; // Perpendicular offset
return (d.source.x + d.target.x) / 2 + offsetX;
}
// For unidirectional connections, use midpoint
return (d.source.x + d.target.x) / 2;
})
.attr("y", d => {
// Handle self-referencing links
if (d.source === d.target) {
return d.source.y;
}
// Check if there's a reverse connection
const reverseLink = data.links.find(l =>
l.source === d.target && l.target === d.source
);
// If it's part of a bidirectional connection, offset the label
if (reverseLink) {
const dx = d.target.x - d.source.x;
const dy = d.target.y - d.source.y;
// Calculate perpendicular offset
const length = Math.sqrt(dx * dx + dy * dy);
const offsetY = dx / length * 10; // Perpendicular offset
return (d.source.y + d.target.y) / 2 + offsetY;
}
// For unidirectional connections, use midpoint
return (d.source.y + d.target.y) / 2;
});
// Update group containers
groupContainers.each(function(d) {
@ -893,7 +974,7 @@ if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Visualize a PocketFlow flow")
parser.add_argument(
"--module", default="async_flow", help="Module containing the flow"
"--module", default="async_loop_flow", help="Module containing the flow"
)
parser.add_argument(
"--flow", default="order_pipeline", help="Flow variable name in the module"