33This module provides support for TOML configuration files to set default
44backends, models, credentials, and other options without hardcoding them.
55
6- Configuration files are searched in the following order:
7- 1. Project-specific: ./mellea.toml (current dir and parents)
8- 2. User config: ~/.config/mellea/config.toml (Linux/macOS) or
9- %APPDATA%\mellea\config.toml (Windows)
6+ Configuration files are searched for in the current directory and parent
7+ directories (./mellea.toml). If found, the config is used; if not, defaults
8+ apply.
109
1110Values are applied with the following precedence:
12111. Explicit parameters passed to start_session()
13122. Project config file (if exists)
14- 3. User config file (if exists)
15- 4. Built-in defaults
13+ 3. Built-in defaults
1614"""
1715
1816import os
1917import sys
2018from pathlib import Path
21- from typing import Any , Optional
19+ from typing import Any
2220
2321from pydantic import BaseModel , Field
2422
3533
3634
3735class BackendConfig (BaseModel ):
38- """Configuration for backend settings."""
36+ """Configuration for backend settings.
37+
38+ Model options can be specified generically or per-backend:
39+
40+ ```toml
41+ [backend.model_options]
42+ temperature = 0.7 # applies to all backends
43+
44+ [backend.model_options.ollama]
45+ num_ctx = 4096 # ollama-specific
46+
47+ [backend.model_options.openai]
48+ presence_penalty = 0.5 # openai-specific
49+ ```
50+ """
3951
4052 name : str | None = None
4153 model_id : str | None = None
4254 model_options : dict [str , Any ] = Field (default_factory = dict )
4355 kwargs : dict [str , Any ] = Field (default_factory = dict )
4456
57+ # Known backend names for detecting per-backend options
58+ _BACKEND_NAMES = {
59+ "ollama" ,
60+ "hf" ,
61+ "huggingface" ,
62+ "openai" ,
63+ "watsonx" ,
64+ "litellm" ,
65+ "vllm" ,
66+ }
67+
68+ def get_model_options_for_backend (self , backend_name : str ) -> dict [str , Any ]:
69+ """Get merged model options for a specific backend.
70+
71+ Merges generic options with backend-specific options.
72+ Backend-specific options override generic ones.
73+
74+ Args:
75+ backend_name: The backend name (e.g., "ollama", "openai")
76+
77+ Returns:
78+ Merged model options dictionary
79+ """
80+ result = {}
81+
82+ # First, add generic options (non-dict values that aren't backend names)
83+ for key , value in self .model_options .items ():
84+ if key not in self ._BACKEND_NAMES and not isinstance (value , dict ):
85+ result [key ] = value
86+
87+ # Then, merge backend-specific options (overrides generic)
88+ backend_specific = self .model_options .get (backend_name )
89+ if isinstance (backend_specific , dict ):
90+ result .update (backend_specific )
91+
92+ return result
93+
4594
4695class CredentialsConfig (BaseModel ):
4796 """Configuration for API credentials."""
@@ -65,50 +114,21 @@ class MelleaConfig(BaseModel):
65114_config_cache : tuple [MelleaConfig , Path | None ] | None = None
66115
67116
68- def get_user_config_dir () -> Path :
69- r"""Get the user configuration directory following XDG Base Directory spec.
70-
71- Returns:
72- Path to user config directory (~/.config/mellea on Linux/macOS,
73- %APPDATA%\mellea on Windows)
74- """
75- if sys .platform == "win32" :
76- # Windows: use APPDATA
77- appdata = os .environ .get ("APPDATA" )
78- if appdata :
79- return Path (appdata ) / "mellea"
80- # Fallback to user home
81- return Path .home () / "AppData" / "Roaming" / "mellea"
82- else :
83- # Linux/macOS: use XDG_CONFIG_HOME or ~/.config
84- xdg_config = os .environ .get ("XDG_CONFIG_HOME" )
85- if xdg_config :
86- return Path (xdg_config ) / "mellea"
87- return Path .home () / ".config" / "mellea"
88-
89-
90117def find_config_file () -> Path | None :
91- """Find configuration file in standard locations .
118+ """Find configuration file in current directory or parent directories .
92119
93- Searches in order:
94- 1. ./mellea.toml (current directory and parents)
95- 2. ~/.config/mellea/config.toml (or Windows equivalent)
120+ Searches for ./mellea.toml starting from current directory and walking
121+ up to parent directories.
96122
97123 Returns:
98124 Path to config file if found, None otherwise
99125 """
100- # Search for project config (current dir and parents)
101126 current = Path .cwd ()
102127 for parent in [current , * current .parents ]:
103128 project_config = parent / "mellea.toml"
104129 if project_config .exists ():
105130 return project_config
106131
107- # Search for user config
108- user_config = get_user_config_dir () / "config.toml"
109- if user_config .exists ():
110- return user_config
111-
112132 return None
113133
114134
@@ -189,8 +209,8 @@ def apply_credentials_to_env(config: MelleaConfig) -> None:
189209 os .environ [env_var ] = value
190210
191211
192- def init_user_config (force : bool = False ) -> Path :
193- """Create example user configuration file.
212+ def init_project_config (force : bool = False ) -> Path :
213+ """Create example project configuration file.
194214
195215 Args:
196216 force: If True, overwrite existing config file
@@ -201,35 +221,37 @@ def init_user_config(force: bool = False) -> Path:
201221 Raises:
202222 FileExistsError: If config file exists and force=False
203223 """
204- config_dir = get_user_config_dir ()
205- config_path = config_dir / "config.toml"
224+ config_path = Path .cwd () / "mellea.toml"
206225
207226 if config_path .exists () and not force :
208227 raise FileExistsError (
209228 f"Config file already exists at { config_path } . Use --force to overwrite."
210229 )
211230
212- # Create config directory if it doesn't exist
213- config_dir .mkdir (parents = True , exist_ok = True )
214-
215- # Example config content
216- example_config = """# Mellea User Configuration
217- # This file sets global defaults for all projects.
218- # Project-specific configs (./mellea.toml) override these settings.
231+ # Example project config content
232+ example_config = """# Mellea Project Configuration
233+ # If this file exists, it will be used to configure start_session() defaults.
234+ # Explicit parameters passed to start_session() override these settings.
219235
220236[backend]
221- # Default backend to use (ollama, openai, huggingface, vllm, watsonx, litellm)
237+ # Backend to use (ollama, openai, huggingface, vllm, watsonx, litellm)
222238name = "ollama"
223239
224- # Default model ID
225- model_id = "granite-4-micro:3b "
240+ # Model ID
241+ model_id = "llama3.2:1b "
226242
227- # Default model options (temperature, max_tokens, etc. )
243+ # Generic model options (apply to all backends )
228244[backend.model_options]
229245temperature = 0.7
230- max_tokens = 2048
231246
232- # Backend-specific options
247+ # Per-backend model options (override generic options for that backend)
248+ # [backend.model_options.ollama]
249+ # num_ctx = 4096
250+
251+ # [backend.model_options.openai]
252+ # presence_penalty = 0.5
253+
254+ # Backend-specific constructor options
233255[backend.kwargs]
234256# base_url = "http://localhost:11434" # For Ollama
235257
@@ -252,47 +274,6 @@ def init_user_config(force: bool = False) -> Path:
252274 return config_path
253275
254276
255- def init_project_config (force : bool = False ) -> Path :
256- """Create example project configuration file.
257-
258- Args:
259- force: If True, overwrite existing config file
260-
261- Returns:
262- Path to created config file
263-
264- Raises:
265- FileExistsError: If config file exists and force=False
266- """
267- config_path = Path .cwd () / "mellea.toml"
268-
269- if config_path .exists () and not force :
270- raise FileExistsError (
271- f"Config file already exists at { config_path } . Use --force to overwrite."
272- )
273-
274- # Example project config content
275- example_config = """# Mellea Project Configuration
276- # This file overrides user config (~/.config/mellea/config.toml) for this project.
277-
278- [backend]
279- # Project-specific model
280- model_id = "llama3.2:1b"
281-
282- [backend.model_options]
283- temperature = 0.9
284-
285- # Project-specific context type
286- context_type = "chat"
287- """
288-
289- # Write config file
290- with open (config_path , "w" ) as f :
291- f .write (example_config )
292-
293- return config_path
294-
295-
296277def clear_config_cache () -> None :
297278 """Clear the cached configuration.
298279
0 commit comments