Skip to content

Commit cf211e7

Browse files
committed
feat: config files for backend options
Signed-off-by: Paul S. Schweigert <paul@paulschweigert.com>
1 parent 6d19086 commit cf211e7

File tree

15 files changed

+1869
-6
lines changed

15 files changed

+1869
-6
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,3 +448,7 @@ pyrightconfig.json
448448
.ionide
449449

450450
# End of https://www.toptal.com/developers/gitignore/api/python,direnv,visualstudiocode,pycharm,macos,jetbrains
451+
452+
# Mellea config files (may contain credentials)
453+
mellea.toml
454+
.mellea.toml

AGENTS.md

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,55 @@ uv run ruff format . && uv run ruff check . # Lint & format
3232
| `mellea/stdlib` | Core: Sessions, Genslots, Requirements, Sampling, Context |
3333
| `mellea/backends` | Providers: HF, OpenAI, Ollama, Watsonx, LiteLLM |
3434
| `mellea/helpers` | Utilities, logging, model ID tables |
35-
| `cli/` | CLI commands (`m serve`, `m alora`, `m decompose`, `m eval`) |
35+
| `mellea/config.py` | Configuration file support (TOML) |
36+
| `cli/` | CLI commands (`m serve`, `m alora`, `m decompose`, `m eval`, `m config`) |
3637
| `test/` | All tests (run from repo root) |
3738
| `scratchpad/` | Experiments (git-ignored) |
3839

39-
## 3. Test Markers
40+
## 3. Configuration Files
41+
Mellea supports TOML configuration files for setting default backends, models, and credentials.
42+
43+
**Config Locations (precedence order):**
44+
1. Project config: `./mellea.toml` (current dir and parents)
45+
2. User config: `~/.config/mellea/config.toml` (Linux/macOS) or `%APPDATA%\mellea\config.toml` (Windows)
46+
47+
**Value Precedence:** Explicit params > Project config > User config > Defaults
48+
49+
**CLI Commands:**
50+
```bash
51+
m config init # Create user config
52+
m config init-project # Create project config
53+
m config show # Display effective config
54+
m config path # Show loaded config file
55+
m config where # Show all config locations
56+
```
57+
58+
**Development Usage:**
59+
- Set your preferred backend/model in user config for convenience
60+
- Use project config for project-specific settings (safe to commit without credentials)
61+
- Store credentials in user config or environment variables (never commit)
62+
- Config files with credentials are git-ignored by default (`mellea.toml`, `.mellea.toml`)
63+
64+
**Example User Config** (`~/.config/mellea/config.toml`):
65+
```toml
66+
[backend]
67+
name = "ollama"
68+
model_id = "llama3.2:1b"
69+
70+
[backend.model_options]
71+
temperature = 0.7
72+
max_tokens = 2048
73+
74+
[credentials]
75+
# openai_api_key = "sk-..." # Better: use env vars
76+
```
77+
78+
**Testing with Config:**
79+
- Tests use temporary config directories (see `test/config/test_config.py`)
80+
- Integration tests verify config precedence (see `test/config/test_config_integration.py`)
81+
- Clear config cache in tests with `clear_config_cache()` from `mellea.config`
82+
83+
## 4. Test Markers
4084
All tests and examples use markers to indicate requirements. The test infrastructure automatically skips tests based on system capabilities.
4185

4286
**Backend Markers:**

README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,66 @@ If you want to contribute, ensure that you install the precommit hooks:
149149
pre-commit install
150150
```
151151
152+
153+
## Configuration
154+
155+
Mellea supports configuration files to set default backends, models, and credentials without hardcoding them in your code.
156+
157+
### Quick Start
158+
159+
Create a user configuration file:
160+
161+
```bash
162+
m config init
163+
```
164+
165+
This creates `~/.config/mellea/config.toml` (Linux/macOS) or `%APPDATA%\mellea\config.toml` (Windows) with example settings.
166+
167+
For project-specific settings:
168+
169+
```bash
170+
m config init-project
171+
```
172+
173+
### Example Configuration
174+
175+
```toml
176+
# ~/.config/mellea/config.toml
177+
[backend]
178+
name = "ollama"
179+
model_id = "granite-4-micro:3b"
180+
181+
[backend.model_options]
182+
temperature = 0.7
183+
max_tokens = 2048
184+
185+
[credentials]
186+
# API keys (environment variables take precedence)
187+
# openai_api_key = "sk-..."
188+
189+
context_type = "simple" # or "chat"
190+
log_level = "INFO"
191+
```
192+
193+
### Configuration Hierarchy
194+
195+
Configuration files are searched in this order:
196+
1. Project config: `./mellea.toml` (current directory and parents)
197+
2. User config: `~/.config/mellea/config.toml`
198+
199+
Values are applied with precedence: **explicit parameters > project config > user config > defaults**
200+
201+
### CLI Commands
202+
203+
```bash
204+
m config show # Display current configuration
205+
m config path # Show which config file is loaded
206+
m config where # Show all config file locations
207+
```
208+
209+
For detailed configuration options and security best practices, see the [Configuration Guide](docs/configuration.md).
210+
211+
152212
### `conda`/`mamba`-based installation from source
153213
154214
Fork and clone the repository:

cli/config/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Configuration management commands for Mellea CLI."""
2+
3+
from .commands import config_app
4+
5+
__all__ = ["config_app"]

cli/config/commands.py

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
"""CLI commands for Mellea configuration management."""
2+
3+
from pathlib import Path
4+
5+
import typer
6+
from rich.console import Console
7+
from rich.syntax import Syntax
8+
from rich.table import Table
9+
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+
)
18+
19+
config_app = typer.Typer(name="config", help="Manage Mellea configuration files")
20+
console = Console()
21+
22+
23+
@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")
49+
def init_project(
50+
force: bool = typer.Option(
51+
False, "--force", "-f", help="Overwrite existing config file"
52+
),
53+
) -> None:
54+
"""Create a project configuration file at ./mellea.toml."""
55+
try:
56+
config_path = init_project_config(force=force)
57+
console.print(f"[green]✓[/green] Created project config at: {config_path}")
58+
console.print("\nThis config will override user settings for this project.")
59+
console.print(
60+
"Run [cyan]m config show[/cyan] to view the effective configuration."
61+
)
62+
except FileExistsError as e:
63+
console.print(f"[red]✗[/red] {e}")
64+
console.print("Use [cyan]--force[/cyan] to overwrite the existing file.")
65+
raise typer.Exit(1)
66+
except Exception as e:
67+
console.print(f"[red]✗[/red] Error creating config: {e}")
68+
raise typer.Exit(1)
69+
70+
71+
@config_app.command("show")
72+
def show_config() -> None:
73+
"""Display the current effective configuration."""
74+
try:
75+
config, config_path = load_config()
76+
77+
# Display config source
78+
if config_path:
79+
console.print(f"[bold]Configuration loaded from:[/bold] {config_path}\n")
80+
else:
81+
console.print(
82+
"[yellow]No configuration file found. Using defaults.[/yellow]\n"
83+
)
84+
85+
# Create a table for the configuration
86+
table = Table(
87+
title="Effective Configuration", show_header=True, header_style="bold cyan"
88+
)
89+
table.add_column("Setting", style="dim")
90+
table.add_column("Value")
91+
92+
# Backend settings
93+
table.add_row(
94+
"Backend Name", config.backend.name or "[dim](default: ollama)[/dim]"
95+
)
96+
table.add_row(
97+
"Model ID",
98+
config.backend.model_id or "[dim](default: granite-4-micro:3b)[/dim]",
99+
)
100+
101+
# Model options
102+
if config.backend.model_options:
103+
for key, value in config.backend.model_options.items():
104+
table.add_row(f" {key}", str(value))
105+
106+
# Backend kwargs
107+
if config.backend.kwargs:
108+
for key, value in config.backend.kwargs.items():
109+
table.add_row(f" backend.{key}", str(value))
110+
111+
# Credentials (masked)
112+
if config.credentials.openai_api_key:
113+
table.add_row("OpenAI API Key", "[dim]***configured***[/dim]")
114+
if config.credentials.watsonx_api_key:
115+
table.add_row("Watsonx API Key", "[dim]***configured***[/dim]")
116+
if config.credentials.watsonx_project_id:
117+
table.add_row("Watsonx Project ID", config.credentials.watsonx_project_id)
118+
if config.credentials.watsonx_url:
119+
table.add_row("Watsonx URL", config.credentials.watsonx_url)
120+
121+
# General settings
122+
table.add_row(
123+
"Context Type", config.context_type or "[dim](default: simple)[/dim]"
124+
)
125+
table.add_row("Log Level", config.log_level or "[dim](default: INFO)[/dim]")
126+
127+
console.print(table)
128+
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'}")
133+
console.print(
134+
"\n[dim]Explicit parameters in code override config file values.[/dim]"
135+
)
136+
137+
except Exception as e:
138+
console.print(f"[red]✗[/red] Error loading config: {e}")
139+
raise typer.Exit(1)
140+
141+
142+
@config_app.command("path")
143+
def show_path() -> None:
144+
"""Show the path to the currently loaded configuration file."""
145+
try:
146+
config_path = find_config_file()
147+
148+
if config_path:
149+
console.print(f"[green]✓[/green] Using config file: {config_path}")
150+
151+
# Show the file content
152+
console.print("\n[bold]File contents:[/bold]")
153+
with open(config_path) as f:
154+
content = f.read()
155+
syntax = Syntax(content, "toml", theme="monokai", line_numbers=True)
156+
console.print(syntax)
157+
else:
158+
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.")
163+
console.print(
164+
"Run [cyan]m config init-project[/cyan] to create a project config."
165+
)
166+
except Exception as e:
167+
console.print(f"[red]✗[/red] Error: {e}")
168+
raise typer.Exit(1)
169+
170+
171+
@config_app.command("where")
172+
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"
176+
project_config_path = Path.cwd() / "mellea.toml"
177+
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()
189+
190+
# Project config
191+
console.print(f"[cyan]Project config:[/cyan] {project_config_path}")
192+
if project_config_path.exists():
193+
console.print(" [green]✓ exists[/green]")
194+
else:
195+
console.print(" [dim]✗ not found[/dim]")
196+
console.print(" Run [cyan]m config init-project[/cyan] to create")
197+
198+
console.print()
199+
200+
# Currently loaded
201+
current = find_config_file()
202+
if current:
203+
console.print(f"[bold green]Currently loaded:[/bold green] {current}")
204+
else:
205+
console.print(
206+
"[yellow]No config file currently loaded (using defaults)[/yellow]"
207+
)

cli/eval/runner.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def pass_rate(self) -> float:
7676

7777

7878
def create_session(
79-
backend: str, model: str | None, max_tokens: int | None
79+
backend: str | None, model: str | None, max_tokens: int | None
8080
) -> mellea.MelleaSession:
8181
"""Create a mellea session with the specified backend and model."""
8282
model_id = None
@@ -92,6 +92,11 @@ def create_session(
9292
model_id = mellea.model_ids.IBM_GRANITE_4_MICRO_3B
9393

9494
try:
95+
from mellea.core.backend import Backend
96+
97+
if backend is None:
98+
raise ValueError("Backend must be specified")
99+
95100
backend_lower = backend.lower()
96101
backend_instance: Backend
97102

cli/m.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import typer
44

55
from cli.alora.commands import alora_app
6+
from cli.config.commands import config_app
67
from cli.decompose import app as decompose_app
78
from cli.eval.commands import eval_app
89
from cli.serve.app import serve
@@ -25,6 +26,7 @@ def callback() -> None:
2526
# Add new subcommand groups by importing and adding with `cli.add_typer()`
2627
# as documented: https://typer.tiangolo.com/tutorial/subcommands/add-typer/#put-them-together.
2728
cli.add_typer(alora_app)
29+
cli.add_typer(config_app)
2830
cli.add_typer(decompose_app)
2931

3032
cli.add_typer(eval_app)

0 commit comments

Comments
 (0)