Loops and Conditions
Overview
Dhenara Agent DSL (DAD) provides powerful flow control mechanisms that allow you to create dynamic, responsive agent workflows. The two primary flow control structures are conditionals and loops, enabling branching logic and iterative processing within your flows.
This document explains how to use these control structures effectively with practical examples.
Conditional Execution
Conditional execution allows your flow to take different paths based on the evaluation of expressions. This enables creating flexible agents that can make decisions based on previous results, user input, or other data.
Basic Conditional Syntax
from dhenara.agent.dsl import FlowDefinition
from dhenara.ai.types import ObjectTemplate
# Create a flow
my_flow = FlowDefinition()
# Add some nodes
my_flow.node("analyzer", analyzer_node)
# Add a conditional
my_flow.conditional(
id="decision_point", # Unique identifier for this conditional
statement=ObjectTemplate(
expression="$expr{$hier{analyzer}.outcome.structured.success == True}"
),
true_branch=success_flow, # Flow to execute if the condition is true
false_branch=failure_flow # Flow to execute if the condition is false
)
The statement
parameter takes an ObjectTemplate
with an expression that evaluates to a boolean value. If the
expression evaluates to True
, the true_branch
flow is executed; otherwise, the false_branch
flow is executed.
Expression Syntax
Conditional expressions can use different syntaxes:
Direct Expression
statement=ObjectTemplate(
expression="$expr{child.word_count > 20}"
)
Python Expression
statement=ObjectTemplate(
expression="$expr{py: len($hier{initial_repo_analysis}.outcome.results) >= 1}"
)
Complex Boolean Logic
statement=ObjectTemplate(
expression="$expr{(child.word_count > 20) && (child.word_count < 40)}"
)
Advanced Python Logic
statement=ObjectTemplate(
expression="$expr{py: len($hier{initial_repo_analysis}.outcome.results) >= 1 and \
all(child.word_count > 10 for result in $hier{initial_repo_analysis}.outcome.results \
for child in result.analysis.children)}"
)
Practical Example
Here's a complete example of a conditional that checks if a file analysis has a certain word count and takes different actions based on the result:
my_flow.conditional(
id="complexity_check",
statement=ObjectTemplate(
expression="$expr{$hier{file_analyzer}.outcome.structured.word_count > 1000}"
),
true_branch=FlowDefinition().node(
"complex_processor",
AIModelNode(
settings=AIModelNodeSettings(
models=["claude-3-7-sonnet"],
system_instructions=["You are processing a complex document."],
prompt=Prompt.with_dad_text("Process this complex document: $hier{file_analyzer}.outcome.text"),
)
)
),
false_branch=FlowDefinition().node(
"simple_processor",
AIModelNode(
settings=AIModelNodeSettings(
models=["claude-3-5-haiku"],
system_instructions=["You are processing a simple document."],
prompt=Prompt.with_dad_text("Process this simple document: $hier{file_analyzer}.outcome.text"),
)
)
)
)
Loop Execution (ForEach)
The for_each
method allows you to iterate over a collection of items, executing a "body" flow for each item. This is
particularly useful for processing lists of files, API results, or other collections.
Basic Loop Syntax
from dhenara.agent.dsl import FlowDefinition
from dhenara.ai.types import ObjectTemplate
my_flow = FlowDefinition()
my_flow.node("data_collector", collector_node)
# Define a loop
my_flow.for_each(
id="item_processor", # Unique identifier for this loop
statement=ObjectTemplate(
expression="$expr{$hier{data_collector}.outcome.structured.items}"
),
item_var="current_item", # Variable name for the current item
index_var="item_index", # Variable name for the current index
max_iterations=10, # Maximum number of iterations
body=item_processing_flow # Flow to execute for each item
)
For each iteration of the loop:
- The current item is assigned to the variable named by
item_var
- The current index is assigned to the variable named by
index_var
- The
body
flow is executed with these variables available
Accessing Loop Variables
Within the body flow, you can access the loop variables using the $expr{}
syntax:
# In the body flow
item_processing_flow = FlowDefinition()
item_processing_flow.node(
"processor",
AIModelNode(
settings=AIModelNodeSettings(
prompt=Prompt.with_dad_text(
"Processing item $expr{item_index + 1}: $expr{current_item.name}"
)
)
)
)
Nested Loops
You can nest loops for more complex processing patterns:
outer_loop_flow = FlowDefinition()
# Outer loop
outer_loop_flow.for_each(
id="category_processor",
statement=ObjectTemplate(expression="$expr{$hier{categories}.outcome.structured.categories}"),
item_var="category",
index_var="category_index",
max_iterations=10,
body=FlowDefinition().for_each(
# Inner loop
id="item_processor",
statement=ObjectTemplate(expression="$expr{category.items}"),
item_var="item",
index_var="item_index",
max_iterations=20,
body=item_processing_flow
)
)
Practical Example
Here's a complete example of using a loop to process files from a folder analysis:
my_flow = FlowDefinition()
# Folder analysis node
my_flow.node(
"folder_analyzer",
FolderAnalyzerNode(
settings=FolderAnalyzerSettings(
base_directory="$var{run_root}/global_data",
operations=[
FolderAnalysisOperation(
operation_type="analyze_folder",
path="project/src",
content_read_mode="structure",
)
]
)
)
)
# Process each file using a loop
my_flow.for_each(
id="file_processor",
statement=ObjectTemplate(
expression="$expr{$hier{folder_analyzer}.outcome.results[0].analysis.children}"
),
item_var="file",
index_var="file_index",
max_iterations=50,
body=FlowDefinition().node(
"file_processor",
AIModelNode(
settings=AIModelNodeSettings(
models=["claude-3-5-haiku"],
system_instructions=["You analyze code files."],
prompt=Prompt.with_dad_text(
"Analyzing file $expr{file_index + 1}: $expr{file.path}\n"
"File type: $expr{file.extension}\n"
"Content: $expr{file.content}\n"
"Please provide a brief analysis of this file."
),
)
)
)
)
Real-World Example
Here's a more complex example showing nested loops and conditionals together, similar to what's used in actual DAD agents:
# Analyze repository structure
flow.node(
"repo_analysis",
FolderAnalyzerNode(
settings=FolderAnalyzerSettings(
base_directory=global_data_directory,
operations=[...]
)
)
)
# Process each analysis result
flow.for_each(
id="analysis_processor",
statement=ObjectTemplate(expression="$expr{$hier{repo_analysis}.outcome.results}"),
item_var="analysis_result",
index_var="result_index",
max_iterations=10,
body=FlowDefinition()
# Process each file in this analysis result
.for_each(
id="file_processor",
statement=ObjectTemplate(expression="$expr{analysis_result.analysis.children}"),
item_var="file",
index_var="file_index",
max_iterations=100,
body=FlowDefinition()
# Apply different processing based on file size
.conditional(
id="file_size_check",
statement=ObjectTemplate(
expression="$expr{file.word_count > 500}"
),
true_branch=FlowDefinition().node(
"large_file_processor",
AIModelNode(settings=large_file_settings)
),
false_branch=FlowDefinition().node(
"small_file_processor",
AIModelNode(settings=small_file_settings)
)
)
)
)
Implementation in the Flow Coordinator
The control flow structures are also useful in the main flow coordinator for high-level orchestration:
coordinator_flow = FlowDefinition()
# Run planning phase
coordinator_flow.subflow("planner", planner_flow)
# Conditionally execute implementation based on planning success
coordinator_flow.conditional(
id="plan_executor",
statement=ObjectTemplate(
expression="$expr{py: $hier{planner.plan_generator}.outcome.structured is not None}"
),
# If planning was successful, iterate through each task
true_branch=FlowDefinition().for_each(
id="implementation_loop",
statement=ObjectTemplate(
expression="$expr{py: $hier{planner.plan_generator}.outcome.structured.implementation_tasks}"
),
item_var="task_spec",
index_var="task_index",
max_iterations=20,
body=implementation_flow
),
# If planning failed, show an error message
false_branch=FlowDefinition().node(
"no_plan_generated",
CommandNode(
settings=CommandNodeSettings(
commands=["echo 'Planning was unsuccessful.'"]
)
)
)
)
Important Considerations
Loop Limitations
- Always set a reasonable
max_iterations
value to prevent infinite loops - Be mindful of the performance impact when processing large collections
- Consider the memory usage when dealing with large datasets in loops
Conditional Branching Complexity
- Avoid overly complex conditional expressions that are hard to understand
- Consider breaking complex logic into separate variables for clarity
- Remember that the entire branch flow is defined before execution, so both branches are defined regardless of which one executes
Variable Scope
- Loop variables (
item_var
,index_var
) are only available within the body flow - Component variables defined with
flow.vars({...})
are available to all nodes in the flow, including conditional branches and loop bodies - Use hierarchical references (
$hier{}
) to access results from nodes in parent flows
Best Practices
- Use Clear Identifiers: Give meaningful names to loops and conditionals for better readability
- Set Reasonable Limits: Always specify
max_iterations
for loops - Break Down Complex Logic: Use multiple simple conditionals rather than a single complex one
- Component Variables: Use component variables for shared configuration
- Consistent Patterns: Follow consistent patterns for loop and conditional structures
- Error Handling: Include conditional branches for handling errors
- Documentation: Document the purpose of complex control structures
By effectively using flow control mechanisms, you can build sophisticated, adaptive agents that respond intelligently to various conditions and process complex data structures efficiently.