Skip to content

Commit 2aa0dda

Browse files
authored
docs: improve clarity and correctness of documentation (#28)
1 parent 96d292b commit 2aa0dda

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+969
-906
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Please think deeper and proceed per continue-session-prompt.md
1+
Please read and think deeply about continue-session-prompt.md and then proceed per the instructions there.

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
![License](https://img.shields.io/badge/License-MIT-yellow)
88
[![llms.txt](https://img.shields.io/badge/llms.txt-green)](https://raw.githubusercontent.com/artificial-sapience/clearflow/main/llms.txt)
99

10-
Correctness-first orchestration for emergent AI. Type-safe, deeply immutable, 100% code coverage.
10+
Correctness-first orchestration for probabilistic AI. Type-safe, deeply immutable, 100% code coverage.
1111

1212
## Why ClearFlow?
1313

1414
- **Message-driven architecture** – Commands trigger actions, Events record facts
15-
- **100% test coverage** – Every path proven to work
15+
- **100% test coverage** – Every path verified to work
1616
- **Type-safe flows** – Full static typing with pyright strict mode
1717
- **Deep immutability** – All state transformations create new immutable data
1818
- **Minimal dependencies** – Only Pydantic for validation and immutability
@@ -45,9 +45,9 @@ pip install clearflow
4545

4646
| Example | What It Shows |
4747
|---------|---------------|
48-
| [Chat](examples/chat/) | Message routing between user and LLM |
49-
| [Portfolio Analysis](examples/portfolio_analysis/) | DSPy-driven portfolio analysis |
50-
| [RAG](examples/rag/) | Document processing pipeline |
48+
| [Chat](examples/chat/) | OpenAI integration with conversation history |
49+
| [Portfolio Analysis](examples/portfolio_analysis/) | Multi-agent coordination using DSPy |
50+
| [RAG](examples/rag/) | Document chunking and FAISS vector search |
5151

5252
## AI Assistant Integration
5353

clearflow/_internal/callback_handler.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
"""Internal callback handler implementation.
2-
3-
This module contains the private implementation for managing observers
4-
with automatic error isolation.
5-
"""
1+
"""Internal callback handler implementation."""
62

73
import sys
84
from collections.abc import Sequence
@@ -12,11 +8,7 @@
128

139

1410
class CallbackHandler:
15-
"""Internal handler that manages observers with automatic error isolation.
16-
17-
Executes all registered observers for each event, ensuring that
18-
errors in one observer don't affect others or the flow execution.
19-
"""
11+
"""Internal handler that manages observers with automatic error isolation."""
2012

2113
def __init__(self, observers: Sequence[Observer]) -> None:
2214
"""Initialize with observers.

clearflow/_internal/flow_impl.py

Lines changed: 10 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
1-
"""Message flow implementation for type-safe routing.
2-
3-
Provides an explicit routing API for building message-driven workflows where
4-
messages are routed based on their types. Uses strategic type erasure at
5-
routing boundaries to handle union types while maintaining type safety at
6-
flow input/output boundaries.
7-
"""
1+
"""Message flow implementation for type-safe routing."""
82

93
import types
104
from dataclasses import dataclass
@@ -159,51 +153,21 @@ def _validate_terminal_type_not_routed(
159153

160154
@final
161155
class _Flow[TStartIn: Message, TEnd: Message](Node[TStartIn, TEnd]):
162-
"""Executable AI workflow that routes messages through nodes based on their types.
163-
164-
Orchestrates message flow through a graph of AI operations, routing based on
165-
message types. Each node performs a specific AI task (LLM calls, vector search,
166-
validation) and produces typed outputs that determine the next step.
167-
168-
Why _Flow is internal:
169-
- Complex type erasure patterns needed for union type routing
170-
- Internal optimization strategies for message dispatch
171-
- Separation of builder API from execution mechanics
172-
- Maintains simpler public API surface
173-
174-
The flow provides:
175-
- Type-safe message routing at runtime
176-
- Automatic causality chain preservation
177-
- Observer hooks for monitoring AI decisions
178-
- Single termination enforcement for clear workflow completion
179-
180-
"""
156+
"""Executable workflow that routes messages through nodes based on their types."""
181157

182-
starting_node: NodeInterface[Message, Message] = Field(
183-
description="First node to process incoming messages, typically parsing or validation"
184-
)
158+
starting_node: NodeInterface[Message, Message] = Field(description="First node to process incoming messages")
185159
routes: tuple[RouteEntry, ...] = Field(
186160
description="Routing table mapping (source_node, message_type) pairs to destination nodes"
187161
)
188-
terminal_type: type[Message] = Field(
189-
description="Message type that immediately completes the flow when produced by any node"
190-
)
191-
callbacks: CallbackHandler | None = Field(
192-
default=None, description="Optional handler for observer callbacks to monitor flow execution events"
193-
)
162+
terminal_type: type[Message] = Field(description="Message type that completes the flow when produced")
163+
callbacks: CallbackHandler | None = Field(default=None, description="Optional handler for observer callbacks")
194164

195165
async def _safe_callback(self, method: str, *args: str | Message | Exception | None) -> None:
196166
"""Execute callback safely without affecting flow.
197167
198-
REQ-016: Zero overhead when no callbacks
199-
REQ-017: Async execution (non-blocking)
200-
201-
CallbackHandler internally handles all errors (REQ-005, REQ-006) so we don't
202-
need additional error handling here.
203-
204168
Args:
205169
method: Name of callback method to invoke
206-
*args: Arguments to pass to the callback method (flow_name, node_name, message, error)
170+
*args: Arguments to pass to the callback method
207171
208172
"""
209173
if not self.callbacks: # REQ-016: Zero overhead when no callbacks
@@ -311,20 +275,11 @@ async def process(self, message: TStartIn) -> TEnd:
311275
@final
312276
@dataclass(frozen=True)
313277
class _FlowBuilder[TStartIn: Message, TStartOut: Message](FlowBuilder[TStartIn, TStartOut]):
314-
"""Module private builder for composing message routes with explicit source nodes.
315-
316-
Uses the pattern from the original flow API where each route explicitly
317-
specifies: from_node -> outcome -> to_node. This enables sequential thinking
318-
about workflow construction.
278+
"""Internal builder for composing message routes with explicit source nodes.
319279
320280
Type parameters:
321281
TStartIn: The input message type the flow accepts
322-
TStartOut: The output type of the start node (remains constant throughout builder chain)
323-
324-
The builder maintains stable type parameters throughout the chain, unlike tracking
325-
current message types, because type erasure makes intermediate types meaningless.
326-
327-
Call end_flow() to specify where the flow terminates and get the completed flow.
282+
TStartOut: The output type of the start node
328283
"""
329284

330285
name: str
@@ -399,9 +354,6 @@ def _validate_and_create_route[TFromIn: Message, TFromOut: Message, TToIn: Messa
399354
def observe(self, *observers: Observer) -> "_FlowBuilder[TStartIn, TStartOut]":
400355
"""Attach observers to the flow.
401356
402-
REQ-009: MessageFlow accepts optional observers
403-
REQ-016: Zero overhead when no observers
404-
405357
Args:
406358
*observers: Observer instances to monitor flow execution
407359
@@ -428,17 +380,6 @@ def route[TFromIn: Message, TFromOut: Message, TToIn: Message, TToOut: Message](
428380
) -> "_FlowBuilder[TStartIn, TStartOut]":
429381
"""Route specific message type from source node to destination.
430382
431-
Explicitly specifies that when `from_node` produces a message of type `outcome`,
432-
route it to `to_node`. This matches the original flow API pattern for clarity.
433-
434-
Type Erasure Rationale:
435-
Python's type system cannot express "route only this specific type from
436-
a union to the next node." For example, if a node outputs
437-
UserMessage | SystemMessage, we cannot type-check at compile time that
438-
only UserMessage goes to a specific handler. We use type erasure
439-
(outcome: type[Message]) to allow this flexibility while maintaining
440-
runtime validation.
441-
442383
Args:
443384
from_node: Source node that may emit the outcome message type
444385
outcome: Specific message type that triggers this route
@@ -472,15 +413,11 @@ def end_flow[TEnd: Message](
472413
) -> Node[TStartIn, TEnd]:
473414
"""Declare the message type that completes this flow.
474415
475-
When any node in the flow produces an instance of the terminal type,
476-
the flow immediately terminates and returns that message. The terminal
477-
type cannot be routed between nodes - it always ends the flow.
478-
479416
Args:
480417
terminal_type: The message type that completes the flow
481418
482419
Returns:
483-
A Node that represents the complete flow with single terminal type
420+
A Node that represents the complete flow
484421
485422
"""
486423
# Validate that terminal type is not already routed
@@ -501,11 +438,8 @@ def create_flow[TStartIn: Message, TStartOut: Message](
501438
) -> FlowBuilder[TStartIn, TStartOut]:
502439
"""Create a flow with type-safe routing.
503440
504-
This is the entry point for building message-driven workflows. The flow
505-
starts at the given node and routes messages based on their types.
506-
507441
Args:
508-
name: The name of the flow for identification and debugging
442+
name: The name of the flow
509443
starting_node: The starting node that processes TStartIn
510444
511445
Returns:

clearflow/flow.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,6 @@
1212
class FlowBuilder[TStartIn: Message, TStartOut: Message](ABC):
1313
"""Builder for composing message-driven flows.
1414
15-
Provides a fluent API for composing message routes through nodes.
16-
Users interact with this interface to define how messages flow
17-
through their system based on message types.
18-
1915
Type parameters:
2016
TStartIn: The input message type the flow accepts
2117
TStartOut: The output type of the start node
@@ -61,18 +57,14 @@ def end_flow[TEnd: Message](
6157
) -> Node[TStartIn, TEnd]:
6258
"""Declare the message type that completes this flow.
6359
64-
When any node in the flow produces an instance of the terminal type,
65-
the flow immediately terminates and returns that message. The terminal
66-
type cannot be routed between nodes - it always ends the flow.
67-
68-
This enforces single responsibility: each flow has exactly one
69-
completion condition defined by its terminal type.
60+
When any node produces an instance of the terminal type, the flow
61+
immediately terminates and returns that message.
7062
7163
Args:
7264
terminal_type: The message type that completes the flow
7365
7466
Returns:
75-
A Node that represents the complete flow with single terminal type
67+
A Node that represents the complete flow
7668
7769
"""
7870
...

clearflow/message.py

Lines changed: 31 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,17 @@ def _utc_now() -> AwareDatetime:
2626

2727

2828
class Message(StrictBaseModel, ABC):
29-
"""Base message class for message-driven AI orchestration.
30-
31-
Messages enable type-safe, immutable data flow between nodes with full causality tracking.
32-
Each message carries metadata for tracing, debugging, and understanding AI decision chains.
33-
34-
Why use Message:
35-
- Type-safe routing: Route on message types, not strings
36-
- Causality tracking: Trace AI decisions through triggered_by chains
37-
- Immutable state: Prevent unintended mutations in complex flows
38-
- Session isolation: run_id ensures messages stay within their flow
39-
40-
Perfect for:
41-
- LLM orchestration with traceable decision chains
42-
- Multi-agent workflows requiring audit trails
43-
- RAG pipelines with clear data lineage
44-
- Any AI system requiring reproducible execution paths
29+
"""Abstract base class for Commands and Events in message-driven flows.
30+
31+
A Message is an immutable data structure that flows between nodes, carrying
32+
both domain data and metadata for causality tracking and flow isolation.
33+
34+
Attributes:
35+
id: Unique identifier for this message instance
36+
triggered_by_id: ID of the message that caused this one (None for root commands)
37+
timestamp: UTC time when this message was created
38+
run_id: Session identifier linking all messages in a single flow execution
39+
4540
"""
4641

4742
id: uuid.UUID = Field(
@@ -62,24 +57,19 @@ class Message(StrictBaseModel, ABC):
6257

6358

6459
class Event(Message):
65-
"""Immutable fact representing something that has occurred in the AI workflow.
66-
67-
Events capture completed actions, state transitions, and outcomes from AI operations.
68-
Every event MUST be triggered by another message, ensuring complete causality chains.
60+
"""An immutable record of something that has occurred.
6961
70-
Why use Event:
71-
- Past-tense facts: Events describe what HAS happened, not what should happen
72-
- Required causality: triggered_by_id is mandatory, ensuring traceable AI decisions
73-
- Immutable history: Once created, events form an unchangeable audit trail
74-
- Type-based routing: Different event types trigger different downstream actions
62+
Events represent facts about state changes or completed actions in the system.
63+
They are named in past tense (e.g., OrderPlaced, DocumentProcessed) and
64+
must always have a triggered_by_id linking to the message that caused them.
7565
76-
Example event types for AI systems:
77-
- LLMResponseGenerated: Captures model output with metadata
78-
- DocumentIndexed: Records successful vector storage operation
79-
- ValidationFailed: Documents why an AI output was rejected
80-
- ThresholdExceeded: Signals when metrics cross boundaries
66+
Events cannot be rejected or modified - they represent what has already happened.
67+
If an error occurs, emit a new event describing the failure rather than
68+
trying to undo the original event.
8169
82-
This is an abstract base - create domain-specific events for your AI workflow.
70+
Constraints:
71+
- Must have triggered_by_id (cannot be None)
72+
- Cannot be instantiated directly (create concrete subclasses)
8373
"""
8474

8575
@model_validator(mode="after")
@@ -111,24 +101,20 @@ def _validate_event(self) -> "Event":
111101

112102

113103
class Command(Message):
114-
"""Imperative request for an action to be performed in the AI workflow.
104+
"""An imperative request to perform an action that may change state.
115105
116-
Commands express intent and trigger operations like LLM calls, data retrieval, or analysis.
117-
Initial commands (triggered_by_id=None) start new flow executions.
106+
Commands express intent to transform or process data. They are named
107+
using imperative verbs (e.g., ProcessOrder, ValidateDocument) and are
108+
processed by a single node that decides how to fulfill the request.
118109
119-
Why use Command:
120-
- Clear intent: Commands use imperative language (ProcessDocument, GenerateResponse)
121-
- Flow initiation: Commands with triggered_by_id=None start new workflows
122-
- Decoupled execution: Nodes decide HOW to fulfill commands independently
123-
- Request/response pattern: Commands trigger events upon completion
110+
Commands may result in:
111+
- One or more events describing what happened
112+
- Error events if the operation fails
124113
125-
Example command types for AI systems:
126-
- AnalyzeDocument: Request document understanding via LLM
127-
- RetrieveContext: Fetch relevant vectors from embedding store
128-
- GenerateSummary: Produce condensed output from source material
129-
- ValidateOutput: Check AI response against quality criteria
114+
Root commands (triggered_by_id=None) initiate new flow executions.
130115
131-
This is an abstract base - create domain-specific commands for your AI operations.
116+
Constraints:
117+
- Cannot be instantiated directly (create concrete subclasses)
132118
"""
133119

134120
@model_validator(mode="after")

0 commit comments

Comments
 (0)