flowk 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.
flowk-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,177 @@
1
+ Metadata-Version: 2.4
2
+ Name: flowk
3
+ Version: 0.1.0
4
+ Summary: A lightweight, modular, and developer-first workflow orchestration engine for AI/LLM pipelines.
5
+ Author-email: Folk Nallathambi <folkadonis7@gmail.com>
6
+ Project-URL: Homepage, https://github.com/folkadonis/flowk
7
+ Project-URL: Bug Tracker, https://github.com/folkadonis/flowk/issues
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: pydantic>=2.0.0
16
+
17
+ # Flowk 🌊
18
+
19
+ **Flowk** is a lightweight, high-performance workflow orchestration engine specifically designed for AI and LLM pipelines. It offers a simpler, developer-first alternative to complex frameworks while providing first-class support for complex routing, shared state memory, observability, and debugging.
20
+
21
+ ---
22
+
23
+ ## πŸš€ Key Features
24
+ - **Extremely Simple API:** Turn standard Python functions into executable graph nodes seamlessly.
25
+ - **Node Retries & Fallbacks:** Built-in resilience out-of-the-box.
26
+ - **Dynamic Routing:** Direct your execution paths dynamically on the fly based on outputs.
27
+ - **Stepping & Time Travel:** Pausable execution steps and total trace replay capabilities.
28
+ - **Telemetry & Visualization:** Live terminal tracking, cost metric emulation, and highly readable CLI flow rendering.
29
+ - **Pluggable Architecture:** Tap into lifecycle hooks using Plugins effortlessly.
30
+
31
+ ---
32
+
33
+ ## πŸ› οΈ Core Concepts
34
+
35
+ ### 1. The Graph
36
+ The `Graph` is the brain of Flowk. It wires up nodes sequentially or through condition-based router intersections:
37
+ ```python
38
+ from flowk import Graph
39
+ g = Graph()
40
+ ```
41
+
42
+ ### 2. Nodes & State
43
+ Nodes are just typical Python functions decorated with `@g.node()`. An internal `GraphState` mutable dictionary is implicitly available across your pipeline.
44
+
45
+ ```python
46
+ # Pass `state` as an argument to read/write shared data across the lifecycle map
47
+ @g.node(retries=3)
48
+ def prepare_prompt(input_text: str, state: dict):
49
+ state["original_query"] = input_text
50
+ return input_text.upper()
51
+ ```
52
+
53
+ ### 3. Connections
54
+ Bind nodes synchronously. The `Graph` auto-detects the first configured node as the entrypoint. All data returned from Node A automatically gets piped into Node B as the `input_text`.
55
+
56
+ ```python
57
+ g.connect(prepare_prompt, call_llm)
58
+ ```
59
+
60
+ ### 4. Routing (Conditional Branching)
61
+ When execution forks depend on context (e.g., standard request vs. priority request), use `g.route()`.
62
+ ```python
63
+ def check_priority(result_from_previous_node: str):
64
+ return "fast" if "URGENT" in result_from_previous_node else "standard"
65
+
66
+ # Map condition strings to actual handling Nodes
67
+ router_node = g.route(check_priority, {
68
+ "fast": priority_handler_node,
69
+ "standard": normal_handler_node
70
+ })
71
+
72
+ g.connect(prepare_prompt, router_node)
73
+ ```
74
+
75
+ ---
76
+
77
+ ## πŸ” Tooling & Observability
78
+
79
+ Flowk ships with beautiful tooling crafted identically for both fast prototyping and robust production monitoring.
80
+
81
+ ### Visualizing Graphs
82
+ Check exactly how your configuration looks using `g.show()`.
83
+ ```text
84
+ ==================================================
85
+ πŸ“Š FLOWK EXECUTION FLOW
86
+ ==================================================
87
+
88
+ [ prepare_prompt ]
89
+ β”‚
90
+ β–Ό
91
+ βŸͺ priority_check ⟫ (Router)
92
+ β”‚
93
+ β”œβ”€[fast]──────► [ priority_handler ]
94
+ β”‚ β”‚
95
+ β”‚ β–Ό
96
+ β”‚ [ cleanup ]
97
+ β”‚
98
+ └─[standard]──► [ standard_handler ]
99
+ β”‚
100
+ β–Ό
101
+ [ cleanup ] πŸ”„ (already visited)
102
+
103
+ ==================================================
104
+ ```
105
+
106
+ ### Metrics Tracking
107
+ Built-in timing tracking per node alongside mock LLM tracking usage:
108
+ ```python
109
+ g.run("Hello!")
110
+
111
+ from flowk import MetricsRegistry
112
+ print(MetricsRegistry.get_summary())
113
+ ```
114
+
115
+ ### 🧠 Session Memory Management
116
+ Flowk supports native execution memory persistence across multiple `.run()` calls via the `session_id` parameter. This is critically useful for multi-turn chat workflows where the LLM needs to continually append messages to the `GraphState` instead of wiping the slate clean!
117
+
118
+ ```python
119
+ # Turns persist data appended into state automatically
120
+ r1 = g.run("Hello", session_id="user_john")
121
+ r2 = g.run("Are you there?", session_id="user_john")
122
+
123
+ r3_anon = g.run("Who am I?") # Anonymous runs use empty states
124
+ ```
125
+
126
+ ### ⚑ Async, Streaming, and Parallel Execution (v2)
127
+ Flowk utilizes high-performance asynchronous primitives to match enterprise scale:
128
+ - Define any node as `async def` and Flowk natively awaits it without blocking the thread pool.
129
+ - Use `g.arun()` for standard async resolution.
130
+ - Broadcast real-time node outputs manually using `async for event in g.astream(...)`. This is extremely optimal for mapping LLM outputs into WebSocket frontends!
131
+ - **Fan-Out Parallelism:** If a node splits into multiple separate nodes, Flowk executes all concurrent branches exactly concurrently using `asyncio.gather`.
132
+
133
+ ### πŸ›‘ Human-in-The-Loop (Breakpoints)
134
+ Need a human to review an action before it commits to a database? Interrupt the graph!
135
+ ```python
136
+ # 1. Compile the graph with a breakpoint
137
+ g.compile(interrupt_before=["commit_to_database"])
138
+
139
+ # 2. Execution will stop and exit when reaching the node
140
+ for event in g.astream(input_data, session_id="user_1"):
141
+ if event["type"] == "interrupt":
142
+ print("Waiting for human...")
143
+
144
+ # 3. Later, resume using the exact same session_id!
145
+ g.arun(None, session_id="user_1")
146
+ ```
147
+
148
+ ### πŸ›‘οΈ Pydantic Safe-State Validation
149
+ Never let a silent property typo crash a 20-minute LLM pipeline again. Wrap your shared state in a Pydantic schema:
150
+ ```python
151
+ from pydantic import BaseModel
152
+ class AgentState(BaseModel):
153
+ messages: list
154
+ cost: float
155
+
156
+ g = Graph(state_schema=AgentState)
157
+ # Flowk will validate `AgentState(**state)` between EVERY node execution.
158
+ ```
159
+
160
+ ### Debug & Time Travel
161
+ Encountering bugs in a complex run? Flowk saves runs by default!
162
+ - To run with highly verbose sequential logging, replace `g.run()` with `g.debug()`.
163
+ - To sequentially replay historic traces visually in terminal, grab the `run_id` outputted from any run:
164
+ `g.replay("run-123-abc")`
165
+
166
+ ---
167
+
168
+ ## 🧩 Plugins (Extensions)
169
+
170
+ Under the hood, flow runs evaluate through hooks (`on_run_start`, `on_node_start`, `on_node_end`, `on_run_end`). Check `flowk.plugins.base.Plugin` to extend the system yourselfβ€”like intercepting runs to store trace files via `FileStoragePlugin`!
171
+
172
+ ```python
173
+ from flowk.plugins.base import PluginManager
174
+ from flowk.plugins.storage import FileStoragePlugin
175
+
176
+ PluginManager.register(FileStoragePlugin("server_logs.jsonl"))
177
+ ```
flowk-0.1.0/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # Flowk 🌊
2
+
3
+ **Flowk** is a lightweight, high-performance workflow orchestration engine specifically designed for AI and LLM pipelines. It offers a simpler, developer-first alternative to complex frameworks while providing first-class support for complex routing, shared state memory, observability, and debugging.
4
+
5
+ ---
6
+
7
+ ## πŸš€ Key Features
8
+ - **Extremely Simple API:** Turn standard Python functions into executable graph nodes seamlessly.
9
+ - **Node Retries & Fallbacks:** Built-in resilience out-of-the-box.
10
+ - **Dynamic Routing:** Direct your execution paths dynamically on the fly based on outputs.
11
+ - **Stepping & Time Travel:** Pausable execution steps and total trace replay capabilities.
12
+ - **Telemetry & Visualization:** Live terminal tracking, cost metric emulation, and highly readable CLI flow rendering.
13
+ - **Pluggable Architecture:** Tap into lifecycle hooks using Plugins effortlessly.
14
+
15
+ ---
16
+
17
+ ## πŸ› οΈ Core Concepts
18
+
19
+ ### 1. The Graph
20
+ The `Graph` is the brain of Flowk. It wires up nodes sequentially or through condition-based router intersections:
21
+ ```python
22
+ from flowk import Graph
23
+ g = Graph()
24
+ ```
25
+
26
+ ### 2. Nodes & State
27
+ Nodes are just typical Python functions decorated with `@g.node()`. An internal `GraphState` mutable dictionary is implicitly available across your pipeline.
28
+
29
+ ```python
30
+ # Pass `state` as an argument to read/write shared data across the lifecycle map
31
+ @g.node(retries=3)
32
+ def prepare_prompt(input_text: str, state: dict):
33
+ state["original_query"] = input_text
34
+ return input_text.upper()
35
+ ```
36
+
37
+ ### 3. Connections
38
+ Bind nodes synchronously. The `Graph` auto-detects the first configured node as the entrypoint. All data returned from Node A automatically gets piped into Node B as the `input_text`.
39
+
40
+ ```python
41
+ g.connect(prepare_prompt, call_llm)
42
+ ```
43
+
44
+ ### 4. Routing (Conditional Branching)
45
+ When execution forks depend on context (e.g., standard request vs. priority request), use `g.route()`.
46
+ ```python
47
+ def check_priority(result_from_previous_node: str):
48
+ return "fast" if "URGENT" in result_from_previous_node else "standard"
49
+
50
+ # Map condition strings to actual handling Nodes
51
+ router_node = g.route(check_priority, {
52
+ "fast": priority_handler_node,
53
+ "standard": normal_handler_node
54
+ })
55
+
56
+ g.connect(prepare_prompt, router_node)
57
+ ```
58
+
59
+ ---
60
+
61
+ ## πŸ” Tooling & Observability
62
+
63
+ Flowk ships with beautiful tooling crafted identically for both fast prototyping and robust production monitoring.
64
+
65
+ ### Visualizing Graphs
66
+ Check exactly how your configuration looks using `g.show()`.
67
+ ```text
68
+ ==================================================
69
+ πŸ“Š FLOWK EXECUTION FLOW
70
+ ==================================================
71
+
72
+ [ prepare_prompt ]
73
+ β”‚
74
+ β–Ό
75
+ βŸͺ priority_check ⟫ (Router)
76
+ β”‚
77
+ β”œβ”€[fast]──────► [ priority_handler ]
78
+ β”‚ β”‚
79
+ β”‚ β–Ό
80
+ β”‚ [ cleanup ]
81
+ β”‚
82
+ └─[standard]──► [ standard_handler ]
83
+ β”‚
84
+ β–Ό
85
+ [ cleanup ] πŸ”„ (already visited)
86
+
87
+ ==================================================
88
+ ```
89
+
90
+ ### Metrics Tracking
91
+ Built-in timing tracking per node alongside mock LLM tracking usage:
92
+ ```python
93
+ g.run("Hello!")
94
+
95
+ from flowk import MetricsRegistry
96
+ print(MetricsRegistry.get_summary())
97
+ ```
98
+
99
+ ### 🧠 Session Memory Management
100
+ Flowk supports native execution memory persistence across multiple `.run()` calls via the `session_id` parameter. This is critically useful for multi-turn chat workflows where the LLM needs to continually append messages to the `GraphState` instead of wiping the slate clean!
101
+
102
+ ```python
103
+ # Turns persist data appended into state automatically
104
+ r1 = g.run("Hello", session_id="user_john")
105
+ r2 = g.run("Are you there?", session_id="user_john")
106
+
107
+ r3_anon = g.run("Who am I?") # Anonymous runs use empty states
108
+ ```
109
+
110
+ ### ⚑ Async, Streaming, and Parallel Execution (v2)
111
+ Flowk utilizes high-performance asynchronous primitives to match enterprise scale:
112
+ - Define any node as `async def` and Flowk natively awaits it without blocking the thread pool.
113
+ - Use `g.arun()` for standard async resolution.
114
+ - Broadcast real-time node outputs manually using `async for event in g.astream(...)`. This is extremely optimal for mapping LLM outputs into WebSocket frontends!
115
+ - **Fan-Out Parallelism:** If a node splits into multiple separate nodes, Flowk executes all concurrent branches exactly concurrently using `asyncio.gather`.
116
+
117
+ ### πŸ›‘ Human-in-The-Loop (Breakpoints)
118
+ Need a human to review an action before it commits to a database? Interrupt the graph!
119
+ ```python
120
+ # 1. Compile the graph with a breakpoint
121
+ g.compile(interrupt_before=["commit_to_database"])
122
+
123
+ # 2. Execution will stop and exit when reaching the node
124
+ for event in g.astream(input_data, session_id="user_1"):
125
+ if event["type"] == "interrupt":
126
+ print("Waiting for human...")
127
+
128
+ # 3. Later, resume using the exact same session_id!
129
+ g.arun(None, session_id="user_1")
130
+ ```
131
+
132
+ ### πŸ›‘οΈ Pydantic Safe-State Validation
133
+ Never let a silent property typo crash a 20-minute LLM pipeline again. Wrap your shared state in a Pydantic schema:
134
+ ```python
135
+ from pydantic import BaseModel
136
+ class AgentState(BaseModel):
137
+ messages: list
138
+ cost: float
139
+
140
+ g = Graph(state_schema=AgentState)
141
+ # Flowk will validate `AgentState(**state)` between EVERY node execution.
142
+ ```
143
+
144
+ ### Debug & Time Travel
145
+ Encountering bugs in a complex run? Flowk saves runs by default!
146
+ - To run with highly verbose sequential logging, replace `g.run()` with `g.debug()`.
147
+ - To sequentially replay historic traces visually in terminal, grab the `run_id` outputted from any run:
148
+ `g.replay("run-123-abc")`
149
+
150
+ ---
151
+
152
+ ## 🧩 Plugins (Extensions)
153
+
154
+ Under the hood, flow runs evaluate through hooks (`on_run_start`, `on_node_start`, `on_node_end`, `on_run_end`). Check `flowk.plugins.base.Plugin` to extend the system yourselfβ€”like intercepting runs to store trace files via `FileStoragePlugin`!
155
+
156
+ ```python
157
+ from flowk.plugins.base import PluginManager
158
+ from flowk.plugins.storage import FileStoragePlugin
159
+
160
+ PluginManager.register(FileStoragePlugin("server_logs.jsonl"))
161
+ ```
@@ -0,0 +1,20 @@
1
+ """
2
+ Flowk: A lightweight, modular, and extensible workflow orchestration engine for AI/LLM pipelines.
3
+ """
4
+
5
+ from flowk.graph import Graph
6
+ from flowk.exceptions import GraphError, NodeExecutionError, InvalidGraphError, ReplayError
7
+ from flowk.state import GraphState
8
+ from flowk.metrics import MetricsRegistry
9
+
10
+ __all__ = [
11
+ "Graph",
12
+ "GraphState",
13
+ "MetricsRegistry",
14
+ "GraphError",
15
+ "NodeExecutionError",
16
+ "InvalidGraphError",
17
+ "ReplayError"
18
+ ]
19
+
20
+ __version__ = "0.1.0"
@@ -0,0 +1,68 @@
1
+ import time
2
+ import uuid
3
+ from typing import Any
4
+
5
+ from flowk.graph import Graph
6
+ from flowk.storage import StorageRegistry
7
+ from flowk.plugins.base import PluginManager, DebugPlugin
8
+ from flowk.executor import SequentialExecutor
9
+
10
+ class Debugger:
11
+ def __init__(self, graph: Graph):
12
+ self.graph = graph
13
+
14
+ def run(self, input_data: Any, session_id: str = None) -> Any:
15
+ run_id = f"debug_{uuid.uuid4().hex[:8]}"
16
+ debug_plugin = DebugPlugin()
17
+
18
+ # Attach strictly for this run
19
+ PluginManager.register(debug_plugin)
20
+ try:
21
+ executor = SequentialExecutor(self.graph)
22
+ return executor.execute(input_data, run_id=run_id, session_id=session_id)
23
+ finally:
24
+ # Cleanup plugin so subsequent graph.run() goes silent
25
+ PluginManager.plugins.remove(debug_plugin)
26
+
27
+ def replay(self, run_id: str):
28
+ print(f"\nβͺ REPLAYING RUN: {run_id}")
29
+ print("="*50)
30
+ trace = StorageRegistry.get_trace(run_id)
31
+
32
+ for step in trace:
33
+ print(f"\nStep {step['step']}: Node '{step['node']}'")
34
+ print(f" Input: {step['input']}")
35
+ print(f" State: {step['state_snapshot']}")
36
+ print(f" Status: {step['status']}")
37
+
38
+ if step['status'] == 'error':
39
+ print(f" Error: {step['error']}")
40
+ else:
41
+ print(f" Output: {step['output']}")
42
+ print(f" Duration: {step['duration']:.4f}s")
43
+ time.sleep(0.5)
44
+
45
+ print("\n" + "="*50)
46
+ print("⏹️ REPLAY COMPLETE\n")
47
+
48
+ def step(self, input_data: Any):
49
+ """Interactive stepping generator (v1 simple console input)."""
50
+ run_id = f"step_{uuid.uuid4().hex[:8]}"
51
+ executor = SequentialExecutor(self.graph)
52
+
53
+ print("\n" + "="*50)
54
+ print("πŸ‘ž STEP DEBUGGER STARTED")
55
+ print("="*50)
56
+
57
+ # We hook into nodes via a custom plugin
58
+ class SteppingPlugin(DebugPlugin):
59
+ def on_node_start(self, run_id, node, input_data, state):
60
+ super().on_node_start(run_id, node, input_data, state)
61
+ input("\n[Press Enter to execute this node...]")
62
+
63
+ step_plugin = SteppingPlugin()
64
+ PluginManager.register(step_plugin)
65
+ try:
66
+ return executor.execute(input_data, run_id=run_id)
67
+ finally:
68
+ PluginManager.plugins.remove(step_plugin)
@@ -0,0 +1,15 @@
1
+ class GraphError(Exception):
2
+ """Base exception for all graph-related errors."""
3
+ pass
4
+
5
+ class NodeExecutionError(GraphError):
6
+ """Raised when a node fails to execute after all retries."""
7
+ pass
8
+
9
+ class InvalidGraphError(GraphError):
10
+ """Raised when the graph structure is invalid."""
11
+ pass
12
+
13
+ class ReplayError(GraphError):
14
+ """Raised when an execution trace cannot be replayed."""
15
+ pass