Best Practices
Developing robust and maintainable applications with BrainyFlow involves adhering to certain best practices. These guidelines help ensure your flows are clear, efficient, and easy to debug and extend.
General Principles
Modularity: Break down complex problems into smaller, manageable nodes and sub-flows.
Explicitness: Make data dependencies and flow transitions clear and easy to follow.
Separation of Concerns: Keep computation logic (in
exec
) separate from data handling (prep
,post
) and orchestration (Flow
).
Design & Architecture
Memory Planning: Clearly define the structure of your global and local memory stores upfront (e.g., using
TypedDict
in Python or interfaces/types in TypeScript). Decide what state needs to be globally accessible versus what should be passed down specific branches viaforkingData
into the local store.Action Naming: Use descriptive, meaningful action names (e.g.,
'user_clarification_needed'
,'data_validated'
) rather than generic names like'next'
or'step2'
. This improves the readability of your flow logic and the resultingExecutionTree
.Explicit Transitions: Clearly define transitions for all expected actions a node might trigger using
.on()
or>>
. Consider adding a default.next()
transition for unexpected or general completion actions.Cycle Management: Be mindful of loops. Use the
maxVisits
option in theFlow
constructor (default is now 15, can be customized) to prevent accidental infinite loops. TheExecutionTree
can also help visualize loops.Error Handling Strategy:
Use the built-in retry mechanism (
maxRetries
,wait
in Node constructor) for transient errors inexec()
.Implement
execFallback(prepRes, error: NodeError)
to provide a default result or perform cleanup if retries fail.Define specific error-handling nodes and transitions (e.g.,
node.on('error', errorHandlerNode)
) for critical errors.
Parallelism Choice: Use
ParallelFlow
when a node fans out to multiple independent branches that can benefit from concurrent execution. Stick with the standardFlow
(sequential branch execution) if branches have interdependencies or if concurrent modification of shared global memory state is a concern.Memory Isolation with
forkingData
: When triggering successors, use theforkingData
argument to pass data specifically to thelocal
store of the next node(s) in a branch. This keeps theglobal
store cleaner and is essential for correct state management in parallel branches.Test Incrementally:
Test individual nodes in isolation using
node.run(memory)
. Remember this only runs the single node and does not follow graph transitions.Test sub-flows before integrating them into larger pipelines.
Write tests that verify the final state of the
Memory
object and, if important, the structure of theExecutionTree
returned byflow.run()
.
Avoid Deep Nesting of Flows: While nesting flows is a powerful feature for modularity, keep the hierarchy reasonably flat (e.g., 2-3 levels deep) to maintain understandability and ease of debugging.
Code Quality
Type Hinting/Interfaces: Use Python's type hints (
TypedDict
,List
,Dict
,Optional
,Union
) and TypeScript interfaces/types to clearly define the expected shapes ofMemory
stores,prep_res
,exec_res
, andactions
. This improves readability, enables static analysis, and reduces runtime errors.Docstrings/Comments: Document your nodes, their purpose, expected inputs/outputs, and any complex logic.
Consistent Naming: Follow consistent naming conventions for nodes, actions, and memory keys.
Idempotent
exec
: Strive to make yourexec
methods idempotent where possible, meaning running them multiple times with the same input produces the same result and no additional side effects. This simplifies retries and debugging.
Project Structure
A well-organized project structure enhances maintainability and collaboration:
utils/
: Contains all utility functions.It's recommended to dedicate one file to each API call, for example
call_llm.py
orsearch_web.ts
.Each file should also include a
main()
function to try that API call
nodes.py
ornodes.ts
: Contains all the node definitions.flow.py
orflow.ts
: Implements functions that create flows by importing node definitions and connecting them.main.py
ormain.ts
: Serves as the project's entry point.
Last updated