Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -448,3 +448,7 @@ pyrightconfig.json
.ionide

# End of https://www.toptal.com/developers/gitignore/api/python,direnv,visualstudiocode,pycharm,macos,jetbrains

# Mellea config files (may contain credentials)
mellea.toml
.mellea.toml
65 changes: 64 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,72 @@ uv run mypy . # Type check
## 2. Directory Structure
| Path | Contents |
|------|----------|
<<<<<<< user-config
| `mellea/stdlib` | Core: Sessions, Genslots, Requirements, Sampling, Context |
| `mellea/backends` | Providers: HF, OpenAI, Ollama, Watsonx, LiteLLM |
| `mellea/helpers` | Utilities, logging, model ID tables |
| `mellea/config.py` | Configuration file support (TOML) |
| `cli/` | CLI commands (`m serve`, `m alora`, `m decompose`, `m eval`, `m config`) |
=======
| `mellea/core/` | Core abstractions: Backend, Base, Formatter, Requirement, Sampling |
| `mellea/stdlib/` | Standard library: Sessions, Components, Context |
| `mellea/backends/` | Providers: HF, OpenAI, Ollama, Watsonx, LiteLLM |
| `mellea/formatters/` | Output formatters for different types |
| `mellea/templates/` | Jinja2 templates |
| `mellea/helpers/` | Utilities, logging, model ID tables |
| `cli/` | CLI commands (`m serve`, `m alora`, `m decompose`, `m eval`) |
>>>>>>> main
| `test/` | All tests (run from repo root) |
| `docs/examples/` | Example code (run as tests via pytest) |
| `scratchpad/` | Experiments (git-ignored) |

## 3. Test Markers
## 3. Configuration Files
Mellea supports TOML configuration files for setting default backends, models, and credentials.

**Config Location:** `./mellea.toml` (searched in current dir and parents)

**Value Precedence:** Explicit params > Project config > Defaults

**CLI Commands:**
```bash
m config init # Create project config
m config show # Display effective config
m config path # Show loaded config file
m config where # Show config location
```

**Development Usage:**
- If `mellea.toml` exists, it will be used; if not, defaults apply
- Store credentials in environment variables (never commit credentials)
- Config files are git-ignored by default (`mellea.toml`, `.mellea.toml`)

**Example Project Config** (`./mellea.toml`):
```toml
[backend]
name = "ollama"
model_id = "llama3.2:1b"

# Generic model options (apply to all backends)
[backend.model_options]
temperature = 0.7

# Per-backend model options (override generic for that backend)
[backend.model_options.ollama]
num_ctx = 4096

[backend.model_options.openai]
presence_penalty = 0.5

[credentials]
# openai_api_key = "sk-..." # Better: use env vars
```

**Testing with Config:**
- Tests use temporary config directories (see `test/config/test_config.py`)
- Integration tests verify config precedence (see `test/config/test_config_integration.py`)
- Clear config cache in tests with `clear_config_cache()` from `mellea.config`

## 4. Test Markers
All tests and examples use markers to indicate requirements. The test infrastructure automatically skips tests based on system capabilities.

**Backend Markers:**
Expand Down Expand Up @@ -107,12 +161,21 @@ Pre-commit runs: ruff, mypy, uv-lock, codespell
| Ollama refused | Run `ollama serve` |

## 8. Self-Review (before notifying user)
<<<<<<< user-config
1. **Pre-commit checks pass?** Run `uv run pre-commit run --all-files` or at minimum:
- `uv run ruff format . && uv run ruff check .` (formatting & linting)
- `uv run mypy <changed-files>` (type checking)
2. `uv run pytest -m "not qualitative"` passes?
=======
1. `uv run pytest test/ -m "not qualitative"` passes?
2. `ruff format` and `ruff check` clean?
>>>>>>> main
3. New functions typed with concise docstrings?
4. Unit tests added for new functionality?
5. Avoided over-engineering?

**Note:** All pre-commit hooks (ruff, mypy, codespell, uv-lock) must pass before a task is considered complete.

## 9. Writing Tests
- Place tests in `test/` mirroring source structure
- Name files `test_*.py` (required for pydocstyle)
Expand Down
5 changes: 5 additions & 0 deletions cli/config/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Configuration management commands for Mellea CLI."""

from .commands import config_app

__all__ = ["config_app"]
158 changes: 158 additions & 0 deletions cli/config/commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
"""CLI commands for Mellea configuration management."""

from pathlib import Path

import typer
from rich.console import Console
from rich.syntax import Syntax
from rich.table import Table

from mellea.config import find_config_file, init_project_config, load_config

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


@config_app.command("init")
def init_project(
force: bool = typer.Option(
False, "--force", "-f", help="Overwrite existing config file"
),
) -> None:
"""Create a project configuration file at ./mellea.toml."""
try:
config_path = init_project_config(force=force)
console.print(f"[green]✓[/green] Created project config at: {config_path}")
console.print("\nEdit this file to set your backend, model, and other options.")
console.print(
"Run [cyan]m config show[/cyan] to view the current configuration."
)
except FileExistsError as e:
console.print(f"[red]✗[/red] {e}")
console.print("Use [cyan]--force[/cyan] to overwrite the existing file.")
raise typer.Exit(1)
except Exception as e:
console.print(f"[red]✗[/red] Error creating config: {e}")
raise typer.Exit(1)


@config_app.command("show")
def show_config() -> None:
"""Display the current effective configuration."""
try:
config, config_path = load_config()

# Display config source
if config_path:
console.print(f"[bold]Configuration loaded from:[/bold] {config_path}\n")
else:
console.print(
"[yellow]No configuration file found. Using defaults.[/yellow]\n"
)

# Create a table for the configuration
table = Table(
title="Effective Configuration", show_header=True, header_style="bold cyan"
)
table.add_column("Setting", style="dim")
table.add_column("Value")

# Backend settings
table.add_row(
"Backend Name", config.backend.name or "[dim](default: ollama)[/dim]"
)
table.add_row(
"Model ID",
config.backend.model_id or "[dim](default: granite-4-micro:3b)[/dim]",
)

# Model options
if config.backend.model_options:
for key, value in config.backend.model_options.items():
table.add_row(f" {key}", str(value))

# Backend kwargs
if config.backend.kwargs:
for key, value in config.backend.kwargs.items():
table.add_row(f" backend.{key}", str(value))

# Credentials (masked)
if config.credentials.openai_api_key:
table.add_row("OpenAI API Key", "[dim]***configured***[/dim]")
if config.credentials.watsonx_api_key:
table.add_row("Watsonx API Key", "[dim]***configured***[/dim]")
if config.credentials.watsonx_project_id:
table.add_row("Watsonx Project ID", config.credentials.watsonx_project_id)
if config.credentials.watsonx_url:
table.add_row("Watsonx URL", config.credentials.watsonx_url)

# General settings
table.add_row(
"Context Type", config.context_type or "[dim](default: simple)[/dim]"
)
table.add_row("Log Level", config.log_level or "[dim](default: INFO)[/dim]")

console.print(table)

console.print(
"\n[dim]Explicit parameters in code override config file values.[/dim]"
)

except Exception as e:
console.print(f"[red]✗[/red] Error loading config: {e}")
raise typer.Exit(1)


@config_app.command("path")
def show_path() -> None:
"""Show the path to the currently loaded configuration file."""
try:
config_path = find_config_file()

if config_path:
console.print(f"[green]✓[/green] Using config file: {config_path}")

# Show the file content
console.print("\n[bold]File contents:[/bold]")
with open(config_path) as f:
content = f.read()
syntax = Syntax(content, "toml", theme="monokai", line_numbers=True)
console.print(syntax)
else:
console.print("[yellow]No configuration file found.[/yellow]")
console.print("\nSearched: ./mellea.toml (current dir and parents)")
console.print(
"\nRun [cyan]m config init[/cyan] to create a project config."
)
except Exception as e:
console.print(f"[red]✗[/red] Error: {e}")
raise typer.Exit(1)


@config_app.command("where")
def show_locations() -> None:
"""Show configuration file location."""
project_config_path = Path.cwd() / "mellea.toml"

console.print("[bold]Configuration file location:[/bold]\n")

# Project config
console.print(f"[cyan]Project config:[/cyan] {project_config_path}")
if project_config_path.exists():
console.print(" [green]✓ exists[/green]")
else:
console.print(" [dim]✗ not found[/dim]")
console.print(" Run [cyan]m config init[/cyan] to create")

console.print()

# Currently loaded (might be in parent dir)
current = find_config_file()
if current:
console.print(f"[bold green]Currently loaded:[/bold green] {current}")
if current != project_config_path:
console.print(" [dim](found in parent directory)[/dim]")
else:
console.print(
"[yellow]No config file currently loaded (using defaults)[/yellow]"
)
7 changes: 6 additions & 1 deletion cli/eval/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def pass_rate(self) -> float:


def create_session(
backend: str, model: str | None, max_tokens: int | None
backend: str | None, model: str | None, max_tokens: int | None
) -> mellea.MelleaSession:
"""Create a mellea session with the specified backend and model."""
model_id = None
Expand All @@ -92,6 +92,11 @@ def create_session(
model_id = mellea.model_ids.IBM_GRANITE_4_MICRO_3B

try:
from mellea.core.backend import Backend

if backend is None:
raise ValueError("Backend must be specified")

backend_lower = backend.lower()
backend_instance: Backend

Expand Down
2 changes: 2 additions & 0 deletions cli/m.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import typer

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

cli.add_typer(eval_app)
Loading