π₯ Helios Engine - LLM Agent Framework
Helios Engine is a powerful and flexible Rust framework for building LLM-powered agents with tool support, streaming chat capabilities, and easy configuration management. Create intelligent agents that can interact with users, call tools, and maintain conversation context - with both online and offline local model support.
π Key Features
- π Forest of Agents: Multi-agent collaboration system where agents can communicate, delegate tasks, and share context
- Agent System: Create multiple agents with different personalities and capabilities
- π Tool Builder: Simplified tool creation with builder pattern - wrap any function as a tool without manual trait implementation
- Tool Registry: Extensible tool system for adding custom functionality
- Extensive Tool Suite: 16+ built-in tools including web scraping, JSON parsing, timestamp operations, file I/O, shell commands, HTTP requests, system info, and text processing
- π RAG System: Retrieval-Augmented Generation with vector stores (InMemory and Qdrant)
- Streaming Support: True real-time response streaming for both remote and local models with immediate token delivery
- Local Model Support: Run local models offline using llama.cpp with HuggingFace integration (optional
localfeature) - HTTP Server & API: Expose OpenAI-compatible API endpoints with full parameter support
- Dual Mode Support: Auto, online (remote API), and offline (local) modes
- CLI & Library: Use as both a command-line tool and a Rust library crate
- π Feature Flags: Optional
localfeature for offline model support - build only what you need!
Installation
You can install Helios Engine as a command-line tool or use it as a library in your own Rust projects.
As a CLI Tool
Standard Installation
To install the CLI tool without local model support (which is lighter and faster to install), run the following command:
cargo install helios-engine
With Local Model Support
If you want to use Helios Engine with local models, you'll need to install it with the local feature enabled. This will also install llama-cpp-2 and its dependencies.
cargo install helios-engine --features local
As a Library
To use Helios Engine as a library in your own project, add the following to your Cargo.toml file:
[dependencies]
helios-engine = "0.4.3"
tokio = { version = "1.35", features = ["full"] }
Configuration
Before you can start using Helios Engine, you'll need to create a config.toml file to store your API keys and other settings.
Initializing Configuration
The easiest way to get started is to use the init command:
helios-engine init
This will create a config.toml file in your current directory with the following content:
[llm]
model_name = "gpt-3.5-turbo"
base_url = "https://api.openai.com/v1"
api_key = "your-api-key-here"
temperature = 0.7
max_tokens = 2048
You'll need to replace "your-api-key-here" with your actual API key.
Common Providers
Here are some examples of how to configure Helios Engine for different LLM providers:
OpenAI
[llm]
base_url = "https://api.openai.com/v1"
model_name = "gpt-4"
api_key = "sk-..."
Local (LM Studio)
[llm]
base_url = "http://localhost:1234/v1"
model_name = "local-model"
api_key = "not-needed"
Ollama
[llm]
base_url = "http://localhost:11434/v1"
model_name = "llama2"
api_key = "not-needed"
Anthropic
[llm]
base_url = "https://api.anthropic.com/v1"
model_name = "claude-3-opus-20240229"
api_key = "sk-ant-..."
Building Your First Agent
Agents are the core of the Helios Engine. They are autonomous entities that can use tools to accomplish tasks. Here's how to build your first agent:
use helios_engine::{Agent, Config, CalculatorTool}; #[tokio::main] async fn main() -> helios_engine::Result<()> { // Load the configuration from a config.toml file let config = Config::from_file("config.toml")?; // Create a new agent using the AgentBuilder let mut agent = Agent::builder("MathAgent") .config(config) .system_prompt("You are a helpful math assistant.") .tool(Box::new(CalculatorTool)) .max_iterations(5) .build() .await?; // Chat with the agent let response = agent.chat("What is 15 * 8 + 42?").await?; println!("Agent: {}", response); Ok(()) }
In this example, we create an agent named "MathAgent" with a system prompt that tells it how to behave. We also give it a CalculatorTool, which allows it to perform mathematical calculations. When we ask the agent to solve a math problem, it will automatically use the calculator to find the answer.
Agents
At the heart of the Helios Engine is the Agent, an autonomous entity that can interact with users, use tools, and manage its own chat history. This chapter will cover the creation, configuration, and core functionalities of agents.
The Agent Struct
The Agent struct is the main component of the Helios Engine. It encapsulates all the necessary components for an agent to function, including:
name: The name of the agent.llm_client: The client for interacting with the Large Language Model.tool_registry: The registry of tools available to the agent.chat_session: The chat session, which stores the conversation history.max_iterations: The maximum number of iterations for tool execution in a single turn.
The AgentBuilder
The AgentBuilder provides a convenient and flexible way to construct and configure agents. It uses the builder pattern to allow you to chain methods together to set the agent's properties.
Creating an Agent
Here's a basic example of how to create an agent using the AgentBuilder:
use helios_engine::{Agent, Config}; #[tokio::main] async fn main() -> helios_engine::Result<()> { let config = Config::from_file("config.toml")?; let mut agent = Agent::builder("MyAgent") .config(config) .build() .await?; Ok(()) }
Configuring an Agent
The AgentBuilder provides several methods for configuring an agent:
config(config: Config): Sets the configuration for the agent. This is a required method.system_prompt(prompt: impl Into<String>): Sets the system prompt for the agent. This tells the agent how to behave.tool(tool: Box<dyn crate::tools::Tool>): Adds a single tool to the agent.tools(tools: Vec<Box<dyn crate::tools::Tool>>): Adds multiple tools to the agent at once.max_iterations(max: usize): Sets the maximum number of iterations for tool execution in a single turn.react(): Enables ReAct mode for reasoning before acting. See ReAct for details.react_with_prompt(prompt): Enables ReAct mode with a custom reasoning prompt.
Here's a more advanced example of how to create and configure an agent:
use helios_engine::{Agent, Config, CalculatorTool, EchoTool}; #[tokio::main] async fn main() -> helios_engine::Result<()> { let config = Config::from_file("config.toml")?; let mut agent = Agent::builder("MyAgent") .config(config) .system_prompt("You are a helpful assistant.") .tools(vec![ Box::new(CalculatorTool), Box::new(EchoTool), ]) .max_iterations(5) .build() .await?; Ok(()) }
Core Functionalities
Once you've created an agent, you can interact with it using the following methods:
chat(message: impl Into<String>): Sends a message to the agent and gets a response.send_message(message: impl Into<String>): A more explicit way to send a message to the agent.clear_history(): Clears the agent's chat history.get_session_summary(): Returns a summary of the current chat session.
The Agent also provides methods for managing its memory, which allows it to store and retrieve information between conversations. You can learn more about this in the Chat chapter.
ReAct Mode
Agents can be configured to use ReAct (Reasoning and Acting) mode, where they reason about tasks before taking actions:
use helios_engine::{Agent, Config, CalculatorTool}; #[tokio::main] async fn main() -> helios_engine::Result<()> { let config = Config::from_file("config.toml")?; let mut agent = Agent::builder("ReActAgent") .config(config) .tool(Box::new(CalculatorTool)) .react() // Enable ReAct mode .build() .await?; let response = agent.chat("Calculate (25 * 4) + (100 / 5)").await?; println!("{}", response); Ok(()) }
This enables the agent to think through problems systematically before executing. Learn more in the ReAct chapter.
ReAct (Reasoning and Acting)
ReAct is a powerful feature in Helios Engine that enables agents to reason about tasks before taking actions. This pattern leads to more thoughtful, systematic problem-solving and makes the agent's decision-making process transparent.
What is ReAct?
ReAct (Reasoning and Acting) is a pattern where the agent follows a two-phase approach:
- π Reasoning Phase: The agent analyzes the task, identifies what's needed, and creates a plan
- β‘ Action Phase: The agent executes the plan using available tools
This separation helps agents handle complex, multi-step tasks more effectively and provides visibility into their thinking process.
Enabling ReAct Mode
Enabling ReAct is incredibly simple - just add .react() to your agent builder:
use helios_engine::{Agent, Config, CalculatorTool}; #[tokio::main] async fn main() -> helios_engine::Result<()> { let config = Config::from_file("config.toml")?; let mut agent = Agent::builder("ReActAgent") .config(config) .tool(Box::new(CalculatorTool)) .react() // β¨ Enable ReAct mode .build() .await?; let response = agent.chat("Calculate (25 * 4) + (100 / 5)").await?; println!("{}", response); Ok(()) }
How It Works
When you send a message to a ReAct-enabled agent, here's what happens:
User Query: "Calculate (25 * 4) + (100 / 5)"
π Reasoning Phase:
Agent thinks: "I need to:
1. Calculate 25 * 4 = 100
2. Calculate 100 / 5 = 20
3. Add the results: 100 + 20 = 120"
β‘ Action Phase:
- Uses calculator tool: 25 * 4 β 100
- Uses calculator tool: 100 / 5 β 20
- Uses calculator tool: 100 + 20 β 120
Response: "The result is 120"
The reasoning is displayed with a π ReAct Reasoning: prefix, making it easy to follow the agent's thought process.
Custom Reasoning Prompts
For domain-specific tasks, you can customize the reasoning prompt:
use helios_engine::{Agent, Config, CalculatorTool}; #[tokio::main] async fn main() -> helios_engine::Result<()> { let config = Config::from_file("config.toml")?; let math_prompt = r#"As a mathematical problem solver: 1. Identify the mathematical operations needed 2. Break down complex calculations into steps 3. Determine the order of operations (PEMDAS) 4. Plan which calculator functions to use 5. Verify the logic of your approach Provide clear mathematical reasoning."#; let mut agent = Agent::builder("MathExpert") .config(config) .system_prompt("You are a mathematics expert.") .tool(Box::new(CalculatorTool)) .react_with_prompt(math_prompt) // π― Custom reasoning .build() .await?; let response = agent.chat("Calculate ((15 * 8) + (20 * 3)) / 2").await?; println!("{}", response); Ok(()) }
When to Use ReAct
Use ReAct When:
- Complex Multi-Step Tasks: Tasks that require planning and coordination
- Debugging: When you want to see how the agent approaches problems
- Critical Operations: When accuracy is more important than speed
- Learning: Understanding agent behavior and decision-making
- Domain-Specific Tasks: With custom prompts for specialized reasoning
β Don't Use ReAct When:
- Simple Queries: Straightforward tasks where reasoning adds unnecessary overhead
- Speed Critical: Applications where latency is paramount
- No Tools Available: ReAct is designed for tool-using agents
- High-Volume Operations: When the extra LLM call impacts throughput
Examples
Example 1: Basic ReAct Agent
use helios_engine::{Agent, Config, CalculatorTool, EchoTool}; #[tokio::main] async fn main() -> helios_engine::Result<()> { let config = Config::from_file("config.toml")?; let mut agent = Agent::builder("Assistant") .config(config) .tools(vec![ Box::new(CalculatorTool), Box::new(EchoTool), ]) .react() .build() .await?; // Multi-step task let response = agent .chat("Calculate 15 * 7, then echo the result") .await?; println!("{}", response); Ok(()) }
Example 2: Domain-Specific Reasoning
use helios_engine::{Agent, Config, FileReadTool, CalculatorTool}; #[tokio::main] async fn main() -> helios_engine::Result<()> { let config = Config::from_file("config.toml")?; let data_analysis_prompt = r#"As a data analyst: 1. UNDERSTAND: What data am I working with? 2. EXTRACT: What information do I need? 3. PROCESS: What calculations are required? 4. TOOLS: Which tools should I use? 5. OUTPUT: How should I present the result? Think through the data pipeline systematically."#; let mut analyst = Agent::builder("DataAnalyst") .config(config) .system_prompt("You are a data analysis expert.") .tools(vec![ Box::new(FileReadTool), Box::new(CalculatorTool), ]) .react_with_prompt(data_analysis_prompt) .build() .await?; let response = analyst .chat("Analyze the numbers: 10, 20, 30, 40, 50. Calculate their average.") .await?; println!("{}", response); Ok(()) }
Example 3: Comparing With and Without ReAct
use helios_engine::{Agent, Config, CalculatorTool}; #[tokio::main] async fn main() -> helios_engine::Result<()> { let config1 = Config::from_file("config.toml")?; let config2 = Config::from_file("config.toml")?; // Standard agent let mut standard = Agent::builder("Standard") .config(config1) .tool(Box::new(CalculatorTool)) .build() .await?; // ReAct agent let mut react = Agent::builder("ReAct") .config(config2) .tool(Box::new(CalculatorTool)) .react() .build() .await?; let query = "Calculate (15 * 3) + (20 * 2)"; println!("Standard agent:"); let r1 = standard.chat(query).await?; println!("{}\n", r1); println!("ReAct agent:"); let r2 = react.chat(query).await?; println!("{}\n", r2); Ok(()) }
Builder Methods
.react()
Enables ReAct mode with the default reasoning prompt.
#![allow(unused)] fn main() { let agent = Agent::builder("MyAgent") .config(config) .react() .build() .await?; }
.react_with_prompt(prompt)
Enables ReAct mode with a custom reasoning prompt.
#![allow(unused)] fn main() { let custom_prompt = "Think step by step about this problem..."; let agent = Agent::builder("MyAgent") .config(config) .react_with_prompt(custom_prompt) .build() .await?; }
Both methods can be placed anywhere in the builder chain:
#![allow(unused)] fn main() { // Before tools let agent = Agent::builder("Agent") .config(config) .react() .tool(Box::new(CalculatorTool)) .build() .await?; // After tools let agent = Agent::builder("Agent") .config(config) .tool(Box::new(CalculatorTool)) .react() .build() .await?; }
Performance Considerations
Latency
ReAct adds one extra LLM call for reasoning:
- Without ReAct: 1 LLM call + tool executions
- With ReAct: 2 LLM calls + tool executions
Impact: Approximately 1-2 seconds additional latency (varies by model)
Token Usage
Additional tokens are consumed for:
- Reasoning prompt: ~50 tokens
- Reasoning response: ~100-300 tokens
- Context storage: ~100-300 tokens
Impact: ~250-650 additional tokens per query
Optimization Tips
For applications where performance matters:
#![allow(unused)] fn main() { // Use ReAct selectively if query_is_complex { react_agent.chat(query).await? } else { standard_agent.chat(query).await? } // Or create specialized agents let quick_agent = Agent::builder("Quick") .config(config) .build() .await?; let thinking_agent = Agent::builder("Thinker") .config(config) .react() .build() .await?; }
Benefits
1. Better Accuracy
Thinking before acting reduces errors and improves decision quality.
2. Transparency
See exactly how the agent approaches problems, making debugging easier.
3. Complex Task Handling
Multi-step problems are handled more systematically with clear planning.
4. Explainability
Understand agent reasoning for compliance, auditing, or learning purposes.
5. Domain Adaptation
Custom prompts tailor reasoning to specific domains or tasks.
Best Practices
1. Use Descriptive System Prompts
#![allow(unused)] fn main() { .system_prompt("You are a methodical assistant who thinks through problems carefully.") }
2. Combine with Appropriate Tools
#![allow(unused)] fn main() { .tools(vec![ Box::new(CalculatorTool), Box::new(FileReadTool), Box::new(JsonParserTool), ]) .react() }
3. Set Reasonable Iteration Limits
#![allow(unused)] fn main() { .max_iterations(15) // Allow enough steps for complex reasoning .react() }
4. Monitor Reasoning Output
Watch the π ReAct Reasoning: output to understand agent behavior and optimize prompts.
5. Use Custom Prompts for Specific Domains
Tailor the reasoning prompt to match your use case (mathematics, data analysis, planning, etc.).
Troubleshooting
Reasoning Not Showing
Problem: No reasoning output visible
Solution:
- Ensure
.react()or.react_with_prompt()is called - Verify the agent has tools registered
- Check stdout for
π ReAct Reasoning:prefix
Too Much Overhead
Problem: ReAct adds too much latency
Solution:
- Use ReAct selectively for complex tasks only
- Consider disabling for simple queries
- Use faster models for reasoning phase
Poor Reasoning Quality
Problem: Agent reasoning is unclear or unhelpful
Solution:
- Improve the system prompt to encourage better thinking
- Use more capable models (e.g., GPT-4 vs GPT-3.5)
- Create custom reasoning prompts with examples
- Adjust the prompt structure for your specific domain
Next Steps
- Check out the examples directory for complete working examples
- See react_agent.rs for a basic demo
- See react_custom_prompt.rs for domain-specific examples
- Read the Tools documentation to learn about available tools
Summary
ReAct mode enables agents to think before acting, leading to:
- π― More accurate results
- ποΈ Transparent decision-making
- π§© Better handling of complex tasks
- π§ Easier debugging and optimization
Simply add .react() to your agent builder to enable this powerful feature!
LLMs
The LLMClient is the primary interface for interacting with Large Language Models (LLMs) in the Helios Engine. It provides a unified API for both remote LLMs (like OpenAI) and local LLMs (via llama.cpp).
The LLMClient
The LLMClient is responsible for sending requests to the LLM and receiving responses. It can be created with either a Remote or Local provider type.
Creating an LLMClient
Here's how to create an LLMClient with a remote provider:
use helios_engine::{llm::{LLMClient, LLMProviderType}, config::LLMConfig}; #[tokio::main] async fn main() -> helios_engine::Result<()> { let llm_config = LLMConfig { model_name: "gpt-3.5-turbo".to_string(), base_url: "https://api.openai.com/v1".to_string(), api_key: std::env::var("OPENAI_API_KEY").unwrap(), temperature: 0.7, max_tokens: 2048, }; let client = LLMClient::new(LLMProviderType::Remote(llm_config)).await?; Ok(()) }
And here's how to create an LLMClient with a local provider:
#[cfg(feature = "local")] use helios_engine::{llm::{LLMClient, LLMProviderType}, config::LocalConfig}; #[tokio::main] async fn main() -> helios_engine::Result<()> { let local_config = LocalConfig { huggingface_repo: "unsloth/Qwen3-0.6B-GGUF".to_string(), model_file: "Qwen3-0.6B-Q4_K_M.gguf".to_string(), temperature: 0.7, max_tokens: 2048, }; let client = LLMClient::new(LLMProviderType::Local(local_config)).await?; Ok(()) }
Note: To use the local provider, you must install Helios Engine with the local feature enabled.
Sending Requests
Once you have an LLMClient, you can send requests to the LLM using the chat method.
Simple Chat
Here's a simple example of how to send a chat request:
use helios_engine::{llm::{LLMClient, LLMProviderType}, config::LLMConfig, ChatMessage}; #[tokio::main] async fn main() -> helios_engine::Result<()> { let llm_config = LLMConfig { model_name: "gpt-3.5-turbo".to_string(), base_url: "https://api.openai.com/v1".to_string(), api_key: std::env::var("OPENAI_API_KEY").unwrap(), temperature: 0.7, max_tokens: 2048, }; let client = LLMClient::new(LLMProviderType::Remote(llm_config)).await?; let messages = vec![ChatMessage::user("Hello, world!")]; let response = client.chat(messages, None, None, None, None).await?; println!("Assistant: {}", response.content); Ok(()) }
Streaming Responses
The LLMClient also supports streaming responses. Here's an example of how to use the chat_stream method:
use helios_engine::{llm::{LLMClient, LLMProviderType}, config::LLMConfig, ChatMessage}; #[tokio::main] async fn main() -> helios_engine::Result<()> { let llm_config = LLMConfig { model_name: "gpt-3.5-turbo".to_string(), base_url: "https://api.openai.com/v1".to_string(), api_key: std::env::var("OPENAI_API_KEY").unwrap(), temperature: 0.7, max_tokens: 2048, }; let client = LLMClient::new(LLMProviderType::Remote(llm_config)).await?; let messages = vec![ChatMessage::user("Hello, world!")]; let response = client.chat_stream(messages, None, None, None, None, |chunk| { print!("{}", chunk); }).await?; Ok(()) }
Chat
The Helios Engine provides a robust set of tools for managing chat conversations. At the core of this system are the ChatMessage and ChatSession structs, which allow you to create, manage, and persist conversations with agents.
ChatMessage
A ChatMessage represents a single message in a chat conversation. It has the following properties:
role: The role of the message sender. This can beSystem,User,Assistant, orTool.content: The content of the message.name: The name of the message sender.tool_calls: Any tool calls requested by the assistant.tool_call_id: The ID of the tool call this message is a response to.
Creating ChatMessages
You can create ChatMessages using the following constructor methods:
ChatMessage::system(content: impl Into<String>): Creates a new system message.ChatMessage::user(content: impl Into<String>): Creates a new user message.ChatMessage::assistant(content: impl Into<String>): Creates a new assistant message.ChatMessage::tool(content: impl Into<String>, tool_call_id: impl Into<String>): Creates a new tool message.
ChatSession
A ChatSession represents a complete chat conversation. It stores the conversation history and any associated metadata.
Creating a ChatSession
You can create a new ChatSession using the ChatSession::new() method. You can also set a system prompt when you create the session:
#![allow(unused)] fn main() { use helios_engine::ChatSession; let mut session = ChatSession::new() .with_system_prompt("You are a helpful coding assistant."); }
Managing Messages
The ChatSession provides several methods for managing messages:
add_message(message: ChatMessage): Adds a message to the chat session.add_user_message(content: impl Into<String>): Adds a user message to the chat session.add_assistant_message(content: impl Into<String>): Adds an assistant message to the chat session.get_messages(): Returns all messages in the chat session, including the system prompt.clear(): Clears all messages from the chat session.
Here's an example of how to manage a conversation with a ChatSession:
use helios_engine::{llm::{LLMClient, LLMProviderType}, config::LLMConfig, ChatMessage, ChatSession}; #[tokio::main] async fn main() -> helios_engine::Result<()> { let llm_config = LLMConfig { model_name: "gpt-3.5-turbo".to_string(), base_url: "https://api.openai.com/v1".to_string(), api_key: std::env::var("OPENAI_API_KEY").unwrap(), temperature: 0.7, max_tokens: 2048, }; let client = LLMClient::new(LLMProviderType::Remote(llm_config)).await?; let mut session = ChatSession::new() .with_system_prompt("You are a helpful coding assistant."); session.add_user_message("Explain async/await in Rust"); let response = client.chat(session.get_messages(), None, None, None, None).await?; session.add_assistant_message(&response.content); // Continue the conversation session.add_user_message("Can you give an example?"); let response2 = client.chat(session.get_messages(), None, None, None, None).await?; session.add_assistant_message(&response2.content); Ok(()) }
Metadata
The ChatSession also allows you to store and retrieve metadata associated with the conversation. This can be useful for storing information like the user's name, the current topic, or any other relevant data.
set_metadata(key: impl Into<String>, value: impl Into<String>): Sets a metadata key-value pair for the session.get_metadata(key: &str): Gets a metadata value by key.remove_metadata(key: &str): Removes a metadata key-value pair.
Error Handling
The Helios Engine uses a custom error type called HeliosError to represent all possible errors that can occur within the framework. This chapter will cover the different types of errors and how to handle them.
HeliosError
The HeliosError enum is the single error type used throughout the Helios Engine. It has the following variants:
ConfigError(String): An error related to configuration.LLMError(String): An error related to the Language Model (LLM).ToolError(String): An error related to a tool.AgentError(String): An error related to an agent.NetworkError(#[from] reqwest::Error): An error related to a network request.SerializationError(#[from] serde_json::Error): An error related to serialization or deserialization.IoError(#[from] std::io::Error): An I/O error.TomlError(#[from] toml::de::Error): An error related to parsing TOML.LlamaCppError(String): An error from the Llama C++ backend (only available with thelocalfeature).
Result<T>
The Helios Engine also provides a convenient Result type alias that uses the HeliosError as the error type:
#![allow(unused)] fn main() { pub type Result<T> = std::result::Result<T, HeliosError>; }
This means that you can use the ? operator to propagate errors in your code, just like you would with a standard std::result::Result.
Handling Errors
Here's an example of how to handle errors in the Helios Engine:
use helios_engine::{Agent, Config, CalculatorTool, Result}; #[tokio::main] async fn main() { if let Err(e) = run().await { eprintln!("Error: {}", e); } } async fn run() -> Result<()> { let config = Config::from_file("config.toml")?; let mut agent = Agent::builder("MathAgent") .config(config) .system_prompt("You are a helpful math assistant.") .tool(Box::new(CalculatorTool)) .max_iterations(5) .build() .await?; let response = agent.chat("What is 15 * 8 + 42?").await?; println!("Agent: {}", response); Ok(()) }
In this example, the run function returns a Result<()>. If any of the operations within the function fail, the error will be propagated up to the main function, where it will be printed to the console.
Using Tools
Tools allow agents to perform actions beyond just text generation, enabling them to interact with files, execute commands, access web resources, and manipulate data. This chapter will cover the built-in tools and how to use them.
Built-in Tools
Helios Engine includes 16+ built-in tools for common tasks. Here's an overview of the most common ones:
Core Tools
CalculatorTool
Performs mathematical calculations and evaluations.
#![allow(unused)] fn main() { use helios_engine::CalculatorTool; let mut agent = Agent::builder("MathAgent") .config(config) .tool(Box::new(CalculatorTool)) .build() .await?; }
Parameters:
expression(string, required): Mathematical expression to evaluate
Example Usage:
#![allow(unused)] fn main() { let result = agent.chat("Calculate 15 * 7 + 3").await?; }
EchoTool
Simply echoes back the input message (useful for testing).
#![allow(unused)] fn main() { use helios_engine::EchoTool; agent.tool(Box::new(EchoTool)); }
Parameters:
message(string, required): Message to echo back
File Management Tools
FileSearchTool
Search for files by name pattern or content within files.
#![allow(unused)] fn main() { use helios_engine::FileSearchTool; agent.tool(Box::new(FileSearchTool)); }
Parameters:
path(string, optional): Directory path to search (default: current directory)pattern(string, optional): File name pattern with wildcards (e.g.,*.rs)content(string, optional): Text content to search for within filesmax_results(number, optional): Maximum number of results (default: 50)
FileReadTool
Read the contents of a file with optional line range selection.
#![allow(unused)] fn main() { use helios_engine::FileReadTool; agent.tool(Box::new(FileReadTool)); }
Parameters:
path(string, required): File path to readstart_line(number, optional): Starting line number (1-indexed)end_line(number, optional): Ending line number (1-indexed)
FileWriteTool
Write content to a file (creates new or overwrites existing).
#![allow(unused)] fn main() { use helios_engine::FileWriteTool; agent.tool(Box::new(FileWriteTool)); }
Parameters:
path(string, required): File path to write tocontent(string, required): Content to write
FileEditTool
Edit a file by replacing specific text (find and replace).
#![allow(unused)] fn main() { use helios_engine::FileEditTool; agent.tool(Box::new(FileEditTool)); }
Parameters:
path(string, required): File path to editfind(string, required): Text to findreplace(string, required): Replacement text
Web & API Tools
WebScraperTool
Fetch and extract content from web URLs.
#![allow(unused)] fn main() { use helios_engine::WebScraperTool; agent.tool(Box::new(WebScraperTool)); }
Parameters:
url(string, required): URL to scrapeextract_text(boolean, optional): Extract readable text from HTMLtimeout_seconds(number, optional): Request timeout
HttpRequestTool
Make HTTP requests with various methods.
#![allow(unused)] fn main() { use helios_engine::HttpRequestTool; agent.tool(Box::new(HttpRequestTool)); }
Parameters:
method(string, required): HTTP method (GET, POST, PUT, DELETE, etc.)url(string, required): Request URLheaders(object, optional): Request headersbody(string, optional): Request bodytimeout_seconds(number, optional): Request timeout
System & Utility Tools
ShellCommandTool
Execute shell commands safely with security restrictions.
#![allow(unused)] fn main() { use helios_engine::ShellCommandTool; agent.tool(Box::new(ShellCommandTool)); }
Parameters:
command(string, required): Shell command to executetimeout_seconds(number, optional): Command timeout
SystemInfoTool
Retrieve system information (OS, CPU, memory, disk, network).
#![allow(unused)] fn main() { use helios_engine::SystemInfoTool; agent.tool(Box::new(SystemInfoTool)); }
Parameters:
category(string, optional): Info category (all, os, cpu, memory, disk, network)
Creating Custom Tools
Helios Engine provides a flexible system for creating custom tools. This chapter will cover the two main ways to create custom tools: using the ToolBuilder (the easy way) and implementing the Tool trait directly (the advanced way).
Using ToolBuilder (Recommended)
The ToolBuilder provides a simplified way to create custom tools without implementing the Tool trait manually. This is the recommended approach for most use cases.
quick_tool! Macro
The easiest way to create a tool is with the quick_tool! macro. It handles all the boilerplate for you, including parameter extraction and type conversion.
#![allow(unused)] fn main() { use helios_engine::quick_tool; // Create a tool in ONE expression! let volume_tool = quick_tool! { name: calculate_volume, description: "Calculate the volume of a box", params: (width: f64, height: f64, depth: f64), execute: |width, height, depth| { format!("Volume: {:.2} cubic meters", width * height * depth) } }; }
ToolBuilder API
If you need more control, you can use the ToolBuilder API directly.
#![allow(unused)] fn main() { use helios_engine::{ToolBuilder, ToolResult}; use serde_json::Value; let tool = ToolBuilder::new("my_tool") .description("Does something useful") .required_parameter("input", "string", "The input value") .sync_function(|args: Value| { let input = args.get("input").and_then(|v| v.as_str()) .ok_or_else(|| helios_engine::HeliosError::ToolError( "Missing input parameter".to_string() ))?; Ok(ToolResult::success(format!("Processed: {}", input))) }) .build(); }
Implementing the Tool Trait (Advanced)
For advanced use cases or when you need more control, you can implement the Tool trait directly.
#![allow(unused)] fn main() { use async_trait::async_trait; use helios_engine::{Tool, ToolParameter, ToolResult}; use serde_json::Value; use std::collections::HashMap; struct WeatherTool; #[async_trait] impl Tool for WeatherTool { fn name(&self) -> &str { "get_weather" } fn description(&self) -> &str { "Get the current weather for a location" } fn parameters(&self) -> HashMap<String, ToolParameter> { let mut params = HashMap::new(); params.insert( "location".to_string(), ToolParameter { param_type: "string".to_string(), description: "City name or location".to_string(), required: Some(true), }, ); params } async fn execute(&self, args: Value) -> helios_engine::Result<ToolResult> { let location = args["location"] .as_str() .ok_or_else(|| helios_engine::HeliosError::ToolError("location is required".to_string()))?; // Your weather API logic here let weather_data = format!("The weather in {} is sunny, 72Β°F", location); Ok(ToolResult::success(weather_data)) } } }
Tool Builder
The ToolBuilder provides a simplified way to create custom tools without implementing the Tool trait manually. This is the recommended approach for most use cases.
quick_tool! Macro
The easiest way to create a tool is with the quick_tool! macro. It handles all the boilerplate for you, including parameter extraction and type conversion.
#![allow(unused)] fn main() { use helios_engine::quick_tool; // Create a tool in ONE expression! let volume_tool = quick_tool! { name: calculate_volume, description: "Calculate the volume of a box", params: (width: f64, height: f64, depth: f64), execute: |width, height, depth| { format!("Volume: {:.2} cubic meters", width * height * depth) } }; }
ToolBuilder API
If you need more control, you can use the ToolBuilder API directly.
Creating a ToolBuilder
You can create a new ToolBuilder using the ToolBuilder::new() method.
#![allow(unused)] fn main() { use helios_engine::ToolBuilder; let tool_builder = ToolBuilder::new("my_tool"); }
Configuring a ToolBuilder
The ToolBuilder provides several methods for configuring a tool:
description(description: impl Into<String>): Sets the description of the tool.parameter(name: impl Into<String>, param_type: impl Into<String>, description: impl Into<String>, required: bool): Adds a parameter to the tool.optional_parameter(name: impl Into<String>, param_type: impl Into<String>, description: impl Into<String>): Adds an optional parameter to the tool.required_parameter(name: impl Into<String>, param_type: impl Into<String>, description: impl Into<String>): Adds a required parameter to the tool.parameters(params: impl Into<String>): Adds multiple parameters at once using a compact format.function<F, Fut>(f: F): Sets the function to execute when the tool is called.sync_function<F>(f: F): Sets the function using a synchronous closure.ftool<F, T1, T2, R>(f: F): Ultra-simple API: Pass a function directly with automatic type inference.ftool3<F, T1, T2, T3, R>(f: F): Ultra-simple API: Pass a 3-parameter function directly with automatic type inference.ftool4<F, T1, T2, T3, T4, R>(f: F): Ultra-simple API: Pass a 4-parameter function directly with automatic type inference.
Building a Tool
Once you've configured your tool, you can build it using the build() method.
#![allow(unused)] fn main() { use helios_engine::{ToolBuilder, ToolResult}; use serde_json::Value; let tool = ToolBuilder::new("my_tool") .description("Does something useful") .required_parameter("input", "string", "The input value") .sync_function(|args: Value| { let input = args.get("input").and_then(|v| v.as_str()) .ok_or_else(|| helios_engine::HeliosError::ToolError( "Missing input parameter".to_string() ))?; Ok(ToolResult::success(format!("Processed: {}", input))) }) .build(); }
You can also use the try_build() method, which returns a Result instead of panicking if the tool is not configured correctly.
Introduction to the Forest of Agents
The Forest of Agents is a multi-agent system where multiple AI agents collaborate to solve complex tasks. Each agent can have specialized roles, tools, and prompts, enabling sophisticated workflows.
Key Concepts
- Forest: The container that manages multiple agents
- Coordinator: An optional special agent that plans and delegates tasks
- Worker Agents: Specialized agents that execute specific tasks
- Task Planning: Automatic decomposition of complex tasks into subtasks
- Agent Communication: Agents can pass messages and results to each other
graph TD
A[Task] --> B(Forest);
B -- Manages --> C{Coordinator};
B -- Manages --> D[Worker Agent 1];
B -- Manages --> E[Worker Agent 2];
B -- Manages --> F[Worker Agent N];
C -- Delegates To --> D;
C -- Delegates To --> E;
C -- Delegates To --> F;
D -- Communicates With --> E;
E -- Communicates With --> F;
Basic Usage
Creating a Simple Forest
use helios_engine::{Agent, Config, ForestBuilder}; #[tokio::main] async fn main() -> helios_engine::Result<()> { let config = Config::from_file("config.toml")?; let mut forest = ForestBuilder::new() .config(config) .agent("worker1".to_string(), Agent::builder("worker1") .system_prompt("You are a data analyst.")) .agent("worker2".to_string(), Agent::builder("worker2") .system_prompt("You are a report writer.")) .max_iterations(15) .build() .await?; // Execute a task let result = forest.execute("Analyze sales data and write a report").await?; println!("Result: {}", result); Ok(()) }
Adding Multiple Agents at Once
Instead of chaining multiple .agent() calls, you can use the .agents() method:
#![allow(unused)] fn main() { let mut forest = ForestBuilder::new() .config(config) .agents(vec![ ("coordinator".to_string(), Agent::builder("coordinator") .system_prompt("You coordinate and plan tasks.")), ("researcher".to_string(), Agent::builder("researcher") .system_prompt("You research information.")), ("analyst".to_string(), Agent::builder("analyst") .system_prompt("You analyze data.")), ("writer".to_string(), Agent::builder("writer") .system_prompt("You write clear documentation.")), ]) .max_iterations(25) .build() .await?; }
Agent Communication
Agents in a Forest of Agents can communicate with each other to share information, delegate tasks, and collaborate on complex problems.
SendMessageTool
The primary mechanism for agent communication is the SendMessageTool. This tool allows an agent to send a message to another agent in the same forest.
Usage
To use the SendMessageTool, you must first register it with an agent.
#![allow(unused)] fn main() { use helios_engine::{Agent, SendMessageTool, ForestBuilder}; let forest = ForestBuilder::new() .config(config) .agent("agent1".to_string(), Agent::builder("agent1") .tool(Box::new(SendMessageTool::new(forest_handle.clone())))) .build() .await?; }
Once the tool is registered, the agent can use it to send messages to other agents. The agent will typically do this automatically when it determines that it needs to communicate with another agent to complete its task.
The SendMessageTool takes the following parameters:
to_agent: The ID of the agent to send the message to.message: The message to send.
Here's an example of the JSON that an agent might generate to use the SendMessageTool:
{
"to_agent": "agent2",
"message": "Please analyze this data: [1, 2, 3, 4, 5]"
}
Accessing Agent Results
You can also access the results of an agent's work by getting the agent from the forest and inspecting its chat history.
#![allow(unused)] fn main() { // Get a specific agent's last response if let Some(agent) = forest.get_agent("researcher") { let history = agent.chat_session().get_messages(); // Process history... } // List all agents let agent_ids = forest.list_agents(); for id in agent_ids { println!("Agent: {}", id); } }
Coordinator-Based Planning
The coordinator-based planning system enables automatic task decomposition and delegation. This is a powerful feature that allows you to create sophisticated multi-agent workflows with minimal effort.
How It Works
- Task Analysis: The coordinator analyzes the incoming task.
- Plan Creation: The coordinator creates a structured plan with subtasks.
- Agent Selection: The coordinator assigns subtasks to the most appropriate worker agents.
- Execution: The worker agents execute their assigned subtasks.
- Result Aggregation: The coordinator combines the results from the worker agents into a final output.
Enabling Coordinator Planning
To enable coordinator-based planning, you must do two things:
- Call the
enable_coordinator_planning()method on theForestBuilder. - Designate one of your agents as the coordinator using the
coordinator_agent()method.
Here's an example:
#![allow(unused)] fn main() { let forest = ForestBuilder::new() .config(config) .enable_coordinator_planning() .coordinator_agent("coordinator".to_string(), Agent::builder("coordinator") .system_prompt("You are a master coordinator who creates plans.")) .agents(vec![ ("researcher".to_string(), Agent::builder("researcher") .system_prompt("You research topics thoroughly.")), ("coder".to_string(), Agent::builder("coder") .system_prompt("You write clean, efficient code.")), ("tester".to_string(), Agent::builder("tester") .system_prompt("You test code for bugs and issues.")), ]) .max_iterations(30) .build() .await?; let result = forest.execute("Research, implement, and test a binary search algorithm").await?; }
Plan Structure
The coordinator creates plans in a JSON format that looks like this:
{
"task": "Research, implement, and test a binary search algorithm",
"plan": [
{
"step": 1,
"agent": "researcher",
"action": "Research binary search algorithm and best practices",
"expected_output": "Detailed explanation and pseudocode"
},
{
"step": 2,
"agent": "coder",
"action": "Implement binary search in Rust based on research",
"expected_output": "Working Rust implementation"
},
{
"step": 3,
"agent": "tester",
"action": "Test the implementation with various test cases",
"expected_output": "Test results and bug report"
}
]
}
Custom Coordinator Prompts
You can customize how the coordinator creates plans by providing a custom system prompt. This allows you to tailor the planning process to your specific needs.
#![allow(unused)] fn main() { let coordinator_prompt = r#"You are an expert project coordinator. Your role is to: 1. Analyze complex tasks and break them into clear subtasks 2. Assign subtasks to the most appropriate agent 3. Ensure dependencies between tasks are respected 4. Create comprehensive plans in JSON format Available agents: - researcher: Gathers information and does analysis - developer: Writes code and implements features - reviewer: Reviews code quality and suggests improvements Always create plans that are specific, measurable, and achievable."#; let forest = ForestBuilder::new() .config(config) .enable_coordinator_planning() .coordinator_agent("coordinator".to_string(), Agent::builder("coordinator") .system_prompt(coordinator_prompt)) .agents(vec![ ("researcher".to_string(), Agent::builder("researcher")), ("developer".to_string(), Agent::builder("developer")), ("reviewer".to_string(), Agent::builder("reviewer")), ]) .build() .await?; }
Introduction to Retrieval-Augmented Generation (RAG)
Helios Engine provides a powerful and flexible RAG (Retrieval-Augmented Generation) system that allows agents to store and retrieve documents using semantic search. The system supports multiple backends and embedding providers, making it suitable for both development and production use.
Architecture
The RAG system consists of three main components:
- Embedding Provider: Generates vector embeddings from text
- Vector Store: Stores and retrieves document embeddings
- RAG System: Coordinates embedding and storage operations
βββββββββββββββββββ
β RAG System β
βββββββββββββββββββ€
β β’ add_document β
β β’ search β
β β’ delete β
β β’ clear β
β β’ count β
ββββββββββ¬βββββββββ
β
ββββββ΄βββββ
β β
βββββΌβββββββ ββββΌββββββββββββ
βEmbedding β βVector Store β
βProvider β β β
ββββββββββββ€ ββββββββββββββββ€
β OpenAI β β In-Memory β
β (custom) β β Qdrant β
ββββββββββββ ββββββββββββββββ
Usage with Agents
The simplest way to use RAG is through the RAGTool with an agent.
In-Memory RAG
#![allow(unused)] fn main() { use helios_engine::{Agent, Config, RAGTool}; let config = Config::from_file("config.toml").unwrap_or_default(); let rag_tool = RAGTool::new_in_memory( "https://api.openai.com/v1/embeddings", std::env::var("OPENAI_API_KEY").unwrap() ); let mut agent = Agent::builder("KnowledgeAgent") .config(config) .tool(Box::new(rag_tool)) .build() .await?; // Add documents agent.chat("Store this: Rust is a systems programming language.").await?; // Search let response = agent.chat("What do you know about Rust?").await?; }
Qdrant RAG
#![allow(unused)] fn main() { let config = Config::from_file("config.toml").unwrap_or_default(); let rag_tool = RAGTool::new_qdrant( "http://localhost:6333", "my_collection", "https://api.openai.com/v1/embeddings", std::env::var("OPENAI_API_KEY").unwrap() ); let mut agent = Agent::builder("KnowledgeAgent") .config(config) .tool(Box::new(rag_tool)) .build() .await?; }
Vector Stores
Vector stores are responsible for storing and retrieving document embeddings. Helios Engine supports two vector stores out of the box: an in-memory store and a Qdrant store.
In-Memory Vector Store
A fast, lightweight vector store that keeps all data in memory.
#![allow(unused)] fn main() { use helios_engine::InMemoryVectorStore; let vector_store = InMemoryVectorStore::new(); }
Advantages:
- β No external dependencies
- β Fast performance
- β Simple setup
- β Perfect for development and testing
Disadvantages:
- β No persistence (data lost on restart)
- β Limited by available memory
- β Not suitable for large datasets
Use Cases:
- Development and testing
- Demos and examples
- Short-lived sessions
- Prototyping
Qdrant Vector Store
A production-ready vector store using the Qdrant vector database.
#![allow(unused)] fn main() { use helios_engine::QdrantVectorStore; let vector_store = QdrantVectorStore::new( "http://localhost:6333", "my_collection" ); }
Advantages:
- β Persistent storage
- β Highly scalable
- β Production-ready
- β Advanced features (filtering, etc.)
Disadvantages:
- β Requires Qdrant service
- β More complex setup
Use Cases:
- Production applications
- Large datasets
- Multi-user systems
- When persistence is required
Setting up Qdrant
You can run Qdrant using Docker:
docker run -p 6333:6333 qdrant/qdrant
Embedding Providers
Embedding providers are responsible for generating vector embeddings from text. Helios Engine supports OpenAI's embedding API out of the box.
OpenAI Embeddings
Uses OpenAI's embedding API (e.g., text-embedding-ada-002 or text-embedding-3-small).
#![allow(unused)] fn main() { use helios_engine::OpenAIEmbeddings; let embeddings = OpenAIEmbeddings::new( "https://api.openai.com/v1/embeddings", std::env::var("OPENAI_API_KEY").unwrap() ); // Or with a specific model let embeddings = OpenAIEmbeddings::with_model( "https://api.openai.com/v1/embeddings", std::env::var("OPENAI_API_KEY").unwrap(), "text-embedding-3-small" ); }
Features:
- High-quality embeddings
- 1536 dimensions (for
text-embedding-ada-002andtext-embedding-3-small) - Excellent for semantic search
- Requires an API key and an internet connection
Serve Module
The serve module provides functionality to serve fully OpenAI-compatible API endpoints with real-time streaming and parameter control, allowing you to expose your agents or LLM clients via HTTP.
Starting the Server
The serve module provides several functions for starting the server:
start_server(config: Config, address: &str): Starts the HTTP server with the given configuration.start_server_with_agent(agent: Agent, model_name: String, address: &str): Starts the HTTP server with an agent.start_server_with_custom_endpoints(config: Config, address: &str, custom_endpoints: Option<CustomEndpointsConfig>): Starts the HTTP server with custom endpoints.start_server_with_agent_and_custom_endpoints(agent: Agent, model_name: String, address: &str, custom_endpoints: Option<CustomEndpointsConfig>): Starts the HTTP server with an agent and custom endpoints.
Example
Here's an example of how to start the server with an agent:
use helios_engine::{Agent, Config, CalculatorTool, serve}; #[tokio::main] async fn main() -> helios_engine::Result<()> { let config = Config::from_file("config.toml")?; let agent = Agent::builder("API Agent") .config(config) .system_prompt("You are a helpful AI assistant with access to a calculator tool.") .tool(Box::new(CalculatorTool)) .max_iterations(5) .build() .await?; println!("Starting server on http://127.0.0.1:8000"); println!("Try: curl http://127.0.0.1:8000/v1/chat/completions \\"); println!(" -H 'Content-Type: application/json' \\"); println!(" -d '{{\"model\": \"local-model\", \"messages\": [{{\"role\": \"user\", \"content\": \"What is 15 * 7?\"}}]}}'"); serve::start_server_with_agent(agent, "local-model".to_string(), "127.0.0.1:8000").await?; Ok(()) }
API Endpoints
The serve module exposes the following OpenAI-compatible API endpoints:
POST /v1/chat/completions: Handles chat completion requests.GET /v1/models: Lists the available models.GET /health: A health check endpoint.
Custom Endpoints
You can also define your own custom endpoints by creating a custom_endpoints.toml file and loading it when you start the server.
custom_endpoints.toml
Here's an example of a custom_endpoints.toml file:
[[endpoints]]
method = "GET"
path = "/custom"
response = { message = "This is a custom endpoint" }
status_code = 200
Loading Custom Endpoints
You can load the custom endpoints configuration using the load_custom_endpoints_config() function:
#![allow(unused)] fn main() { let custom_endpoints = serve::load_custom_endpoints_config("custom_endpoints.toml")?; }
You can then pass the custom_endpoints to the start_server_with_custom_endpoints() or start_server_with_agent_and_custom_endpoints() function.
Examples Overview
This directory contains comprehensive examples demonstrating various features of the Helios Engine framework.
Table of Contents
Running Examples
All examples can be run using Cargo:
# Run a specific example
cargo run --example basic_chat
# List all available examples
cargo run --example --list
Individual Example Commands
# Basic chat example
cargo run --example basic_chat
# Agent with built-in tools (Calculator, Echo)
cargo run --example agent_with_tools
# Agent with file management tools
cargo run --example agent_with_file_tools
# Agent with in-memory database tool
cargo run --example agent_with_memory_db
# Custom tool implementation
cargo run --example custom_tool
# Multiple agents with different personalities
cargo run --example multiple_agents
# Forest of Agents - collaborative multi-agent system
cargo run --example forest_of_agents
# Forest with Coordinator - enhanced planning system
cargo run --example forest_with_coordinator
# Forest Simple Demo - simple reliable demo of planning system
cargo run --example forest_simple_demo
# Direct LLM usage without agents
cargo run --example direct_llm_usage
# Streaming chat with remote models
cargo run --example streaming_chat
# Local model streaming example
cargo run --example local_streaming
# Serve an agent via HTTP API
cargo run --example serve_agent
# Serve with custom endpoints
cargo run --example serve_with_custom_endpoints
# SendMessageTool demo - test messaging functionality
cargo run --example send_message_tool_demo
# Agent with RAG capabilities
cargo run --example agent_with_rag
# RAG with in-memory vector store
cargo run --example rag_in_memory
# Compare RAG implementations (Qdrant vs InMemory)
cargo run --example rag_qdrant_comparison
# Complete demo with all features
cargo run --example complete_demo
How to Contribute
Thank you for your interest in contributing to Helios Engine! This document provides guidelines and information for contributors.
π Quick Start for Contributors
Development Setup
-
Clone the repository:
git clone https://github.com/Ammar-Alnagar/Helios-Engine.git cd Helios-Engine -
Build the project:
cargo build -
Run tests:
cargo test -
Format code:
cargo fmt -
Check for issues:
cargo clippy
First Contribution
- Fork the repository on GitHub
- Create a feature branch:
git checkout -b feature/your-feature-name - Make your changes
- Run tests:
cargo test - Format code:
cargo fmt - Check for issues:
cargo clippy - Commit your changes:
git commit -m "Add your feature" - Push to your fork:
git push origin feature/your-feature-name - Create a Pull Request
ποΈ Development Workflow
Branching Strategy
main: Production-ready codedevelop: Integration branch for featuresfeature/*: New featuresbugfix/*: Bug fixeshotfix/*: Critical fixes for production
Commit Messages
Follow conventional commit format:
type(scope): description
[optional body]
[optional footer]
π§ͺ Testing
Running Tests
# Run all tests
cargo test
# Run specific test
cargo test test_name
π Documentation
Documentation Standards
- Use Markdown for all documentation
- Include code examples where relevant
- Provide both conceptual and practical information
- Keep documentation up-to-date with code changes
- Use clear, concise language accessible to different experience levels