Skip to content

Commit e8865b4

Browse files
committed
review comments
Signed-off-by: Paul S. Schweigert <paul@paulschweigert.com>
1 parent cf211e7 commit e8865b4

File tree

5 files changed

+259
-413
lines changed

5 files changed

+259
-413
lines changed

cli/config/commands.py

Lines changed: 11 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -7,45 +7,13 @@
77
from rich.syntax import Syntax
88
from rich.table import Table
99

10-
from mellea.config import (
11-
find_config_file,
12-
get_config_path,
13-
get_user_config_dir,
14-
init_project_config,
15-
init_user_config,
16-
load_config,
17-
)
10+
from mellea.config import find_config_file, init_project_config, load_config
1811

1912
config_app = typer.Typer(name="config", help="Manage Mellea configuration files")
2013
console = Console()
2114

2215

2316
@config_app.command("init")
24-
def init_user(
25-
force: bool = typer.Option(
26-
False, "--force", "-f", help="Overwrite existing config file"
27-
),
28-
) -> None:
29-
"""Create a user configuration file at ~/.config/mellea/config.toml."""
30-
try:
31-
config_path = init_user_config(force=force)
32-
console.print(f"[green]✓[/green] Created user config at: {config_path}")
33-
console.print(
34-
"\nEdit this file to set your default backend, model, and credentials."
35-
)
36-
console.print(
37-
"Run [cyan]m config show[/cyan] to view the current configuration."
38-
)
39-
except FileExistsError as e:
40-
console.print(f"[red]✗[/red] {e}")
41-
console.print("Use [cyan]--force[/cyan] to overwrite the existing file.")
42-
raise typer.Exit(1)
43-
except Exception as e:
44-
console.print(f"[red]✗[/red] Error creating config: {e}")
45-
raise typer.Exit(1)
46-
47-
48-
@config_app.command("init-project")
4917
def init_project(
5018
force: bool = typer.Option(
5119
False, "--force", "-f", help="Overwrite existing config file"
@@ -55,9 +23,9 @@ def init_project(
5523
try:
5624
config_path = init_project_config(force=force)
5725
console.print(f"[green]✓[/green] Created project config at: {config_path}")
58-
console.print("\nThis config will override user settings for this project.")
26+
console.print("\nEdit this file to set your backend, model, and other options.")
5927
console.print(
60-
"Run [cyan]m config show[/cyan] to view the effective configuration."
28+
"Run [cyan]m config show[/cyan] to view the current configuration."
6129
)
6230
except FileExistsError as e:
6331
console.print(f"[red]✗[/red] {e}")
@@ -126,10 +94,6 @@ def show_config() -> None:
12694

12795
console.print(table)
12896

129-
# Show search order
130-
console.print("\n[bold]Configuration search order:[/bold]")
131-
console.print("1. Project config: ./mellea.toml (current dir and parents)")
132-
console.print(f"2. User config: {get_user_config_dir() / 'config.toml'}")
13397
console.print(
13498
"\n[dim]Explicit parameters in code override config file values.[/dim]"
13599
)
@@ -156,12 +120,9 @@ def show_path() -> None:
156120
console.print(syntax)
157121
else:
158122
console.print("[yellow]No configuration file found.[/yellow]")
159-
console.print("\nSearched locations:")
160-
console.print("1. ./mellea.toml (current dir and parents)")
161-
console.print(f"2. {get_user_config_dir() / 'config.toml'}")
162-
console.print("\nRun [cyan]m config init[/cyan] to create a user config.")
123+
console.print("\nSearched: ./mellea.toml (current dir and parents)")
163124
console.print(
164-
"Run [cyan]m config init-project[/cyan] to create a project config."
125+
"\nRun [cyan]m config init[/cyan] to create a project config."
165126
)
166127
except Exception as e:
167128
console.print(f"[red]✗[/red] Error: {e}")
@@ -170,37 +131,27 @@ def show_path() -> None:
170131

171132
@config_app.command("where")
172133
def show_locations() -> None:
173-
"""Show all possible configuration file locations."""
174-
user_config_dir = get_user_config_dir()
175-
user_config_path = user_config_dir / "config.toml"
134+
"""Show configuration file location."""
176135
project_config_path = Path.cwd() / "mellea.toml"
177136

178-
console.print("[bold]Configuration file locations:[/bold]\n")
179-
180-
# User config
181-
console.print(f"[cyan]User config:[/cyan] {user_config_path}")
182-
if user_config_path.exists():
183-
console.print(" [green]✓ exists[/green]")
184-
else:
185-
console.print(" [dim]✗ not found[/dim]")
186-
console.print(" Run [cyan]m config init[/cyan] to create")
187-
188-
console.print()
137+
console.print("[bold]Configuration file location:[/bold]\n")
189138

190139
# Project config
191140
console.print(f"[cyan]Project config:[/cyan] {project_config_path}")
192141
if project_config_path.exists():
193142
console.print(" [green]✓ exists[/green]")
194143
else:
195144
console.print(" [dim]✗ not found[/dim]")
196-
console.print(" Run [cyan]m config init-project[/cyan] to create")
145+
console.print(" Run [cyan]m config init[/cyan] to create")
197146

198147
console.print()
199148

200-
# Currently loaded
149+
# Currently loaded (might be in parent dir)
201150
current = find_config_file()
202151
if current:
203152
console.print(f"[bold green]Currently loaded:[/bold green] {current}")
153+
if current != project_config_path:
154+
console.print(" [dim](found in parent directory)[/dim]")
204155
else:
205156
console.print(
206157
"[yellow]No config file currently loaded (using defaults)[/yellow]"

mellea/config.py

Lines changed: 79 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,20 @@
33
This module provides support for TOML configuration files to set default
44
backends, 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
1110
Values are applied with the following precedence:
1211
1. Explicit parameters passed to start_session()
1312
2. Project config file (if exists)
14-
3. User config file (if exists)
15-
4. Built-in defaults
13+
3. Built-in defaults
1614
"""
1715

1816
import os
1917
import sys
2018
from pathlib import Path
21-
from typing import Any, Optional
19+
from typing import Any
2220

2321
from pydantic import BaseModel, Field
2422

@@ -35,13 +33,64 @@
3533

3634

3735
class 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

4695
class 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-
90117
def 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)
222238
name = "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]
229245
temperature = 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-
296277
def clear_config_cache() -> None:
297278
"""Clear the cached configuration.
298279

0 commit comments

Comments
 (0)