Chapter 7: Configuration (Config)
Welcome to Chapter 7! In Chapter 6: Schema, we learned how OpenManus uses schemas to define the structure of data passed between different components, like official forms ensuring everyone fills them out correctly.
Now, think about setting up a new application. You often need to tell it how to behave.
- Which AI model should it use?
- What’s the secret key to access that AI?
- Should it run code in a restricted “sandbox” environment?
- Which search engine should it prefer?
These are all settings or configurations. This chapter explores how OpenManus manages these settings using the Config
system.
What Problem Does Config Solve?
Imagine you’re building a simple app that uses an AI service. You need an API key to access it. Where do you put this key?
- Option 1: Hardcode it directly in the code.
# Bad idea! Don't do this! api_key = "MY_SUPER_SECRET_API_KEY_12345" # ... rest of the code uses api_key ...
This is a terrible idea! Your secret key is exposed in the code. Sharing the code means sharing your secret. Changing the key means editing the code. What if multiple parts of the code need the key? You’d have it scattered everywhere!
- Option 2: Use a Configuration System. Keep all settings in a separate, easy-to-read file. The application reads this file when it starts and makes the settings available wherever they’re needed.
OpenManus uses Option 2. It keeps settings in a file named config.toml
and uses a special Config
object to manage them.
Use Case: Let’s say we want our LLM component to use the “gpt-4o” model and a specific API key. Instead of writing “gpt-4o” and the key directly into the LLM
class code, the LLM
class will ask the Config
system: “What model should I use?” and “What’s the API key?”. The Config
system provides the answers it read from config.toml
.
Key Concepts: The Settings File and Manager
1. The Settings File (config.toml
)
This is a simple text file located in the config/
directory of your OpenManus project. It uses the TOML format (Tom’s Obvious, Minimal Language), which is designed to be easy for humans to read.
It contains sections for different parts of the application. Here’s a highly simplified snippet:
# config/config.toml (Simplified Example)
[llm] # Settings for the Large Language Model
model = "gpt-4o"
api_key = "YOUR_OPENAI_API_KEY_HERE" # Replace with your actual key
base_url = "https://api.openai.com/v1"
api_type = "openai"
[sandbox] # Settings for the code execution sandbox
use_sandbox = true
image = "python:3.12-slim"
memory_limit = "256m"
[search_config] # Settings for web search
engine = "DuckDuckGo"
[browser_config] # Settings for the browser tool
headless = false
Explanation:
[llm]
,[sandbox]
, etc., define sections.model = "gpt-4o"
assigns the value"gpt-4o"
to themodel
setting within thellm
section.api_key = "YOUR_..."
stores your secret key (you should put your real key here and never share this file publicly if it contains secrets!).use_sandbox = true
sets a boolean (true/false) value.
This file acts as the central “control panel” list for the application’s behavior.
2. The Settings Manager (Config
class in app/config.py
)
Okay, we have the settings file. How does the application use it?
OpenManus has a special Python class called Config
(defined in app/config.py
). Think of this class as the Settings Manager. Its job is:
- Read the File: When the application starts, the
Config
manager reads theconfig.toml
file. - Parse and Store: It understands the TOML format and stores the settings internally, often using the Pydantic Schemas we learned about (like
LLMSettings
,SandboxSettings
) to validate the data. - Provide Access: It offers a way for any other part of the application to easily ask for a specific setting (e.g., “Give me the LLM model name”).
3. The Singleton Pattern: One Manager to Rule Them All
The Config
class uses a special design pattern called a Singleton. This sounds fancy, but the idea is simple: There is only ever one instance (object) of the Config
manager in the entire application.
Analogy: Think of the principal’s office in a school. There’s only one principal’s office. If any teacher or student needs official school-wide information (like the date of the next holiday), they go to that single, central office. They don’t each have their own separate, potentially conflicting, information source.
The Config
object is like that principal’s office. When any part of OpenManus (like the LLM class or the DockerSandbox class) needs a setting, it asks the same, single Config
instance. This ensures everyone is using the same configuration values that were loaded at the start.
How Do We Use It? (Accessing Settings)
Because Config
is a singleton, accessing settings is straightforward. You import the pre-created instance and ask for the setting you need.
The single instance is created automatically when app/config.py
is first loaded and is made available as config
.
# Example of how another part of the code might use the config
from app.config import config # Import the singleton instance
# Access LLM settings
default_llm_settings = config.llm.get("default") # Get the 'default' LLM config
if default_llm_settings:
model_name = default_llm_settings.model
api_key = default_llm_settings.api_key
print(f"LLM Model: {model_name}")
# Don't print the API key in real code! This is just for illustration.
# print(f"LLM API Key: {api_key[:4]}...{api_key[-4:]}")
# Access Sandbox settings
use_sandbox_flag = config.sandbox.use_sandbox
sandbox_image = config.sandbox.image
print(f"Use Sandbox: {use_sandbox_flag}")
print(f"Sandbox Image: {sandbox_image}")
# Access Search settings (check if it exists)
if config.search_config:
search_engine = config.search_config.engine
print(f"Preferred Search Engine: {search_engine}")
# Access Browser settings (check if it exists)
if config.browser_config:
run_headless = config.browser_config.headless
print(f"Run Browser Headless: {run_headless}")
Explanation:
from app.config import config
: We import the single, sharedconfig
object.config.llm
: Accesses the dictionary of all LLM configurations read from the[llm]
sections inconfig.toml
. We use.get("default")
to get the settings specifically for the LLM named “default”.default_llm_settings.model
: Accesses themodel
attribute of theLLMSettings
object. Pydantic ensures this attribute exists and is the correct type.config.sandbox.use_sandbox
: Directly accesses theuse_sandbox
attribute within thesandbox
settings object (SandboxSettings
).- We check if
config.search_config
andconfig.browser_config
exist before accessing them, as they might be optional sections in theconfig.toml
file.
Use Case Example: How LLM
Gets Its Settings
Let’s revisit our use case. When an LLM
object is created (often inside a BaseAgent), its initialization code (__init__
) looks something like this (simplified):
# Simplified snippet from app/llm.py __init__ method
from app.config import config, LLMSettings # Import config and the schema
from typing import Optional
class LLM:
# ... other methods ...
def __init__(self, config_name: str = "default", llm_config: Optional[LLMSettings] = None):
# If specific llm_config isn't provided, get it from the global config
if llm_config is None:
# Ask the global 'config' object for the settings
# corresponding to 'config_name' (e.g., "default")
llm_settings = config.llm.get(config_name)
if not llm_settings: # Handle case where the name doesn't exist
llm_settings = config.llm.get("default") # Fallback to default
else: # Use the provided config if given
llm_settings = llm_config
# Store the settings read from the config object
self.model = llm_settings.model
self.api_key = llm_settings.api_key
self.base_url = llm_settings.base_url
# ... store other settings like max_tokens, temperature ...
print(f"LLM initialized with model: {self.model}")
# Initialize the actual API client using these settings
# self.client = AsyncOpenAI(api_key=self.api_key, base_url=self.base_url)
# ... rest of initialization ...
Explanation:
- The
LLM
class imports the globalconfig
object. - In its
__init__
, it usesconfig.llm.get(config_name)
to retrieve the specific settings (likemodel
,api_key
) it needs. - It then uses these retrieved values to configure itself and the underlying API client.
This way, the LLM
class doesn’t need the actual values hardcoded inside it. It just asks the central Config
manager. If you want to change the model or API key, you only need to update config.toml
and restart the application!
Under the Hood: Loading and Providing Settings
What happens when the application starts and the config
object is first used?
- First Access: The first time code tries to
import config
fromapp.config
, Python runs the code inapp.config.py
. - Singleton Check: The
Config
class’s special__new__
method checks if an instance (_instance
) already exists. If not, it creates a new one. If it does exist, it just returns the existing one. This ensures only one instance is ever made. - Initialization (
__init__
): The__init__
method (run only once for the single instance) calls_load_initial_config
. - Find File (
_get_config_path
): It looks forconfig/config.toml
. If that doesn’t exist, it looks forconfig/config.example.toml
as a fallback. - Read File (
_load_config
): It opens the found.toml
file and uses the standardtomllib
library to read its contents into a Python dictionary. - Parse & Validate:
_load_initial_config
takes this raw dictionary and carefully organizes it, using Pydantic models (LLMSettings
,SandboxSettings
,BrowserSettings
,SearchSettings
,MCPSettings
, all defined inapp/config.py
) to structure and validate the settings. For example, it createsLLMSettings
objects for each entry under[llm]
. If a required setting is missing or has the wrong type (e.g.,max_tokens
is text instead of a number), Pydantic will raise an error here, stopping the app from starting with bad configuration. - Store Internally: The validated settings (now nicely structured Pydantic objects) are stored within the
Config
instance (inself._config
). - Ready for Use: The
config
instance is now ready. Subsequent accesses simply return the stored, validated settings via properties likeconfig.llm
,config.sandbox
, etc.
Sequence Diagram:
sequenceDiagram
participant App as Application Start
participant CfgMod as app/config.py
participant Config as Config Singleton Object
participant TOML as config.toml File
participant Parser as TOML Parser & Pydantic
participant OtherMod as e.g., app/llm.py
App->>+CfgMod: import config
Note over CfgMod: First time loading module
CfgMod->>+Config: Config() called (implicitly via `config = Config()`)
Config->>Config: __new__ checks if _instance exists (it doesn't)
Config->>Config: Creates new Config instance (_instance)
Config->>Config: Calls __init__ (only runs once)
Config->>Config: _load_initial_config()
Config->>Config: _get_config_path() -> finds path
Config->>+TOML: Opens file
TOML-->>-Config: Returns file content
Config->>+Parser: Parses TOML content into dict
Parser-->>-Config: Returns raw_config dict
Config->>+Parser: Validates dict using Pydantic models (LLMSettings etc.)
Parser-->>-Config: Returns validated AppConfig object
Config->>Config: Stores validated config internally
Config-->>-CfgMod: Returns the single instance
CfgMod-->>-App: Provides `config` instance
App->>+OtherMod: Code runs (e.g., `LLM()`)
OtherMod->>+Config: Accesses property (e.g., `config.llm`)
Config-->>-OtherMod: Returns stored settings (e.g., Dict[str, LLMSettings])
Code Glimpse (app/config.py
):
Let’s look at the key parts:
# Simplified snippet from app/config.py
import threading
import tomllib
from pathlib import Path
from pydantic import BaseModel, Field
# ... other imports like typing ...
# --- Pydantic Models for Settings ---
class LLMSettings(BaseModel): # Defines structure for [llm] section
model: str
api_key: str
# ... other fields like base_url, max_tokens, api_type ...
class SandboxSettings(BaseModel): # Defines structure for [sandbox] section
use_sandbox: bool
image: str
# ... other fields like memory_limit, timeout ...
# ... Similar models for BrowserSettings, SearchSettings, MCPSettings ...
class AppConfig(BaseModel): # Holds all validated settings together
llm: Dict[str, LLMSettings]
sandbox: Optional[SandboxSettings]
browser_config: Optional[BrowserSettings]
search_config: Optional[SearchSettings]
mcp_config: Optional[MCPSettings]
# --- The Singleton Config Class ---
class Config:
_instance = None
_lock = threading.Lock() # Ensures thread-safety during creation
_initialized = False
def __new__(cls): # Controls instance creation (Singleton part 1)
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self): # Initializes the instance (runs only once)
if not self._initialized:
with self._lock:
if not self._initialized:
self._config: Optional[AppConfig] = None # Where settings are stored
self._load_initial_config() # Load from file
self._initialized = True
def _load_config(self) -> dict: # Reads the TOML file
config_path = self._get_config_path() # Finds config.toml or example
with config_path.open("rb") as f:
return tomllib.load(f) # Parses TOML into a dictionary
def _load_initial_config(self): # Parses dict and validates with Pydantic
raw_config = self._load_config()
# ... (logic to handle defaults and structure the raw_config dict) ...
# ... (creates LLMSettings, SandboxSettings etc. from raw_config) ...
# Validate the final structured dict using AppConfig
self._config = AppConfig(**structured_config_dict)
# --- Properties to Access Settings ---
@property
def llm(self) -> Dict[str, LLMSettings]:
# Provides easy access like 'config.llm'
return self._config.llm
@property
def sandbox(self) -> SandboxSettings:
# Provides easy access like 'config.sandbox'
return self._config.sandbox
# ... Properties for browser_config, search_config, mcp_config ...
# --- Create the Singleton Instance ---
# This line runs when the module is imported, creating the single instance.
config = Config()
Explanation:
- The Pydantic models (
LLMSettings
,SandboxSettings
,AppConfig
) define the expected structure and types for the settings read fromconfig.toml
. - The
Config
class uses__new__
and_lock
to implement the singleton pattern, ensuring only one instance. __init__
calls_load_initial_config
only once._load_initial_config
reads the TOML file and uses the Pydantic models (withinAppConfig
) to parse and validate the settings, storing the result inself._config
.@property
decorators provide clean access (e.g.,config.llm
) to the stored settings.config = Config()
at the end creates the actual singleton instance that gets imported elsewhere.
Wrapping Up Chapter 7
We’ve learned that the Config
system is OpenManus’s way of managing application settings. It reads configurations from the config.toml
file at startup, validates them using Pydantic Schemas, and makes them available throughout the application via a single, shared config
object (using the singleton pattern). This keeps settings separate from code, making the application more flexible, secure, and easier to manage.
Many components rely on these configurations. For instance, when an agent needs to execute code safely, it might use a DockerSandbox
. The settings for this sandbox – like which Docker image to use or how much memory to allow – are read directly from the configuration we just discussed.
Let’s move on to Chapter 8: DockerSandbox to see how OpenManus provides a secure environment for running code generated by agents, using settings managed by our Config
system.
Generated by AI Codebase Knowledge Builder