Iterative Problem Solver
The IterativeProblemSolver is an agent that iteratively attempts to solve problems using available tools. It employs a chat-based approach and continues working until it succeeds, fails explicitly, or reaches the maximum number of iterations.
Overview
The Iterative Problem Solver follows a simple but powerful pattern:
- Plan - Analyze the problem and identify what needs to be done
- Act - Execute actions using available tools
- Observe - Review the results
- Refine - Adjust the approach based on observations
- Terminate - Stop when the goal is met or the iteration budget is exhausted
Key Features
- Tool Integration: Seamlessly integrates with any
LlmToolimplementations - Automatic Termination: Stops when the LLM responds with “DONE” or “FAIL”
- Iteration Control: Configurable maximum iteration count prevents infinite loops
- Chat-Based Context: Maintains conversation history for context-aware problem solving
- Summary Generation: Provides a clean summary of the final result
Usage
Basic Example
use mojentic::agents::IterativeProblemSolver;
use mojentic::llm::{LlmBroker, LlmTool};
use mojentic::llm::gateways::OllamaGateway;
use mojentic::llm::tools::simple_date_tool::SimpleDateTool;
use std::sync::Arc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize the LLM broker
let gateway = Arc::new(OllamaGateway::default());
let broker = LlmBroker::new("qwen3:32b", gateway, None);
// Define available tools
let tools: Vec<Box<dyn LlmTool>> = vec![
Box::new(SimpleDateTool),
];
// Create the solver
let mut solver = IterativeProblemSolver::builder(broker)
.tools(tools)
.max_iterations(5)
.build();
// Solve a problem
let result = solver.solve("What's the date next Friday?").await?;
println!("Result: {}", result);
Ok(())
}
Custom System Prompt
You can customize the system prompt to guide the solver’s behavior:
#![allow(unused)]
fn main() {
let mut solver = IterativeProblemSolver::builder(broker)
.tools(tools)
.max_iterations(10)
.system_prompt(
"You are a specialized data analysis assistant. \
Break down complex queries into clear steps and use tools methodically."
)
.build();
}
With Multiple Tools
The solver works best when given appropriate tools for the problem domain:
#![allow(unused)]
fn main() {
use mojentic::llm::tools::ask_user_tool::AskUserTool;
use mojentic::llm::tools::simple_date_tool::SimpleDateTool;
let tools: Vec<Box<dyn LlmTool>> = vec![
Box::new(AskUserTool::new()),
Box::new(SimpleDateTool),
];
let mut solver = IterativeProblemSolver::builder(broker)
.tools(tools)
.max_iterations(5)
.build();
}
How It Works
Step-by-Step Process
- Initialization: The solver creates a
ChatSessionwith the provided system prompt and tools - Iteration Loop: For each iteration:
- Sends the problem description with instructions to use tools
- Checks the response for “DONE” (success) or “FAIL” (failure)
- Continues if neither keyword is present and iterations remain
- Summary: After termination, requests a concise summary of the result
- Return: Returns the summary as the final result
Termination Conditions
The solver terminates when one of these conditions is met:
- Success: The LLM’s response contains “DONE” (case-insensitive)
- Failure: The LLM’s response contains “FAIL” (case-insensitive)
- Exhaustion: The maximum number of iterations is reached
Logging
The solver uses the tracing crate to log important events:
info: Logged when a task completes successfully or failswarn: Logged when maximum iterations are reached
Configuration Options
Builder Pattern
The IterativeProblemSolver uses the builder pattern for configuration:
#![allow(unused)]
fn main() {
IterativeProblemSolver::builder(broker)
.tools(tools) // Set available tools
.max_iterations(10) // Set max iterations (default: 3)
.system_prompt("...") // Set custom system prompt
.build()
}
Default Values
- max_iterations: 3
- system_prompt:
“You are a problem-solving assistant that can solve complex problems step by step. You analyze problems, break them down into smaller parts, and solve them systematically. If you cannot solve a problem completely in one step, you make progress and identify what to do next.”
Best Practices
1. Choose Appropriate Tools
Select tools that are relevant to your problem domain:
#![allow(unused)]
fn main() {
// For date/time problems
let tools = vec![Box::new(SimpleDateTool)];
// For user interaction
let tools = vec![Box::new(AskUserTool::new())];
// For data analysis
let tools = vec![
Box::new(CalculatorTool),
Box::new(DataRetrievalTool),
];
}
2. Set Reasonable Iteration Limits
Balance between giving the solver enough attempts and preventing excessive computation:
- Simple queries: 3-5 iterations
- Complex analyses: 10-15 iterations
- Open-ended exploration: 20+ iterations
3. Provide Context in Problem Description
The more context you provide, the better the solver can work:
#![allow(unused)]
fn main() {
// Less effective
solver.solve("Analyze the data").await?;
// More effective
solver.solve(
"Analyze the sales data from Q1 2024. \
Focus on trends in the technology sector. \
Provide insights on growth patterns."
).await?;
}
4. Monitor Logs
Enable tracing to understand solver behavior:
#![allow(unused)]
fn main() {
tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.init();
}
Common Patterns
Retry Logic
For operations that might fail transiently:
#![allow(unused)]
fn main() {
let mut attempts = 0;
let max_attempts = 3;
let result = loop {
attempts += 1;
match solver.solve(problem).await {
Ok(result) if !result.contains("FAIL") => break result,
Ok(_) if attempts < max_attempts => continue,
Ok(result) => break result,
Err(e) => return Err(e),
}
};
}
Multi-Stage Problems
For problems that require multiple phases:
#![allow(unused)]
fn main() {
// Phase 1: Data gathering
let mut solver = IterativeProblemSolver::builder(broker.clone())
.tools(data_tools)
.max_iterations(5)
.build();
let data = solver.solve("Gather all relevant data").await?;
// Phase 2: Analysis
let mut solver = IterativeProblemSolver::builder(broker)
.tools(analysis_tools)
.max_iterations(10)
.build();
let analysis = solver.solve(&format!("Analyze: {}", data)).await?;
}
Examples
See the complete examples at:
examples/iterative_solver.rs- Basic usage with date and user interaction toolsexamples/solver_chat_session.rs- Interactive chat session with solver delegation pattern
Error Handling
The solver returns Result<String, MojenticError>:
#![allow(unused)]
fn main() {
match solver.solve(problem).await {
Ok(result) => println!("Solution: {}", result),
Err(MojenticError::GatewayError(msg)) => {
eprintln!("Gateway error: {}", msg);
}
Err(MojenticError::ToolError(msg)) => {
eprintln!("Tool error: {}", msg);
}
Err(e) => {
eprintln!("Unexpected error: {}", e);
}
}
}
Advanced: Solver as a Tool
The IterativeProblemSolver can be wrapped as a tool and used within a ChatSession, enabling powerful delegation patterns where a chat assistant can offload complex problems to a specialized solver agent.
Creating a Solver Tool
#![allow(unused)]
fn main() {
use mojentic::llm::tools::{FunctionDescriptor, LlmTool, ToolDescriptor};
use mojentic::agents::IterativeProblemSolver;
use serde_json::{json, Value};
use std::collections::HashMap;
use std::sync::Arc;
struct IterativeProblemSolverTool {
broker: Arc<LlmBroker>,
tools: Vec<Box<dyn LlmTool>>,
}
impl IterativeProblemSolverTool {
fn new(broker: Arc<LlmBroker>, tools: Vec<Box<dyn LlmTool>>) -> Self {
Self { broker, tools }
}
}
impl Clone for IterativeProblemSolverTool {
fn clone(&self) -> Self {
Self {
broker: self.broker.clone(),
tools: self.tools.iter().map(|t| t.clone_box()).collect(),
}
}
}
impl LlmTool for IterativeProblemSolverTool {
fn run(&self, args: &HashMap<String, Value>) -> mojentic::error::Result<Value> {
let problem_to_solve = args
.get("problem_to_solve")
.and_then(|v| v.as_str())
.ok_or_else(|| {
mojentic::error::MojenticError::ToolError(
"Missing required argument: problem_to_solve".to_string(),
)
})?;
let solver_tools: Vec<Box<dyn LlmTool>> =
self.tools.iter().map(|t| t.clone_box()).collect();
let runtime = tokio::runtime::Handle::current();
let broker_clone = (*self.broker).clone();
let result = runtime.block_on(async move {
let mut solver = IterativeProblemSolver::builder(broker_clone)
.tools(solver_tools)
.max_iterations(5)
.build();
solver.solve(problem_to_solve).await
})?;
Ok(json!({"solution": result}))
}
fn descriptor(&self) -> ToolDescriptor {
ToolDescriptor {
r#type: "function".to_string(),
function: FunctionDescriptor {
name: "iterative_problem_solver".to_string(),
description: "Iteratively solve a complex multi-step problem using available tools.".to_string(),
parameters: json!({
"type": "object",
"properties": {
"problem_to_solve": {
"type": "string",
"description": "The problem or request to be solved."
}
},
"required": ["problem_to_solve"],
"additionalProperties": false
}),
},
}
}
fn clone_box(&self) -> Box<dyn LlmTool> {
Box::new(self.clone())
}
}
}
Using the Solver Tool in a Chat Session
use mojentic::llm::{ChatSession, LlmBroker};
use mojentic::llm::tools::simple_date_tool::SimpleDateTool;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let gateway = Arc::new(OllamaGateway::default());
let broker = Arc::new(LlmBroker::new("qwq", gateway, None));
// Create the solver tool with SimpleDateTool as the inner tool
let solver_tools: Vec<Box<dyn LlmTool>> = vec![Box::new(SimpleDateTool)];
let solver_tool = IterativeProblemSolverTool::new(broker.clone(), solver_tools);
// Create chat session with the solver tool
let mut session = ChatSession::builder((*broker).clone())
.system_prompt(
"You are a helpful assistant with access to an iterative problem solver. \
When faced with complex multi-step problems or questions that require \
reasoning and tool usage, use the iterative_problem_solver tool."
)
.tools(vec![Box::new(solver_tool)])
.build();
// Interactive loop
loop {
let mut query = String::new();
print!("Query: ");
io::stdout().flush()?;
io::stdin().read_line(&mut query)?;
if query.trim().is_empty() {
break;
}
let response = session.send(query.trim()).await?;
println!("{}\n", response);
}
Ok(())
}
Benefits of This Pattern
- Delegation: The chat assistant can offload complex problems to a specialized solver
- Composability: Mix solver capabilities with other tools in the same session
- Context Preservation: The chat session maintains conversation history
- Flexible Interaction: Users can ask simple questions directly or complex problems that trigger the solver
See the complete example at:
examples/solver_chat_session.rs- Interactive chat session with solver delegation
Limitations
- LLM Dependency: Quality of results depends on the underlying LLM’s capabilities
- Tool Design: Effectiveness relies on well-designed tools with clear descriptions
- Token Limits: Long iterations may hit context window limits
- Cost: Multiple LLM calls per problem can increase API costs
See Also
- Chat Sessions - Understanding the underlying chat mechanism
- Building Tools - Creating custom tools for the solver
- Simple Recursive Agent - Alternative problem-solving pattern