From ceb15343546641f831b982ff36be9806794d1814 Mon Sep 17 00:00:00 2001 From: Aainee Sinha Date: Fri, 5 Jun 2026 14:24:37 +0530 Subject: [PATCH 1/8] chore: split heavy ML stack into optional mmory extras --- README.md | 12 +++++++++--- requirements-memory.txt | 3 +++ requirements.txt | 3 --- 3 files changed, 12 insertions(+), 6 deletions(-) create mode 100644 requirements-memory.txt diff --git a/README.md b/README.md index 8002908..69cbfdd 100644 --- a/README.md +++ b/README.md @@ -74,9 +74,10 @@ Rooms/ ├── tests/ # Unit Tests │ └── test_session.py # Logic Verification ├── outputs/ # Session Transcripts -├── cli.py # Interactive Wizard Entry Point +├── cli.py # Interactive Wizard Entry Point ├── rooms.settings.example.yaml # Settings template (commit this) -├── requirements.txt # Project Dependencies +├── requirements.txt # Core Project Dependencies +└── requirements-memory.txt # Optional Vector Memory Dependencies # Project Dependencies ``` `rooms.settings.yaml` is gitignored — create it locally with `python cli.py config init` or by copying the example file. @@ -94,9 +95,14 @@ cd Rooms python -m venv venv venv\Scripts\activate # Windows: venv\Scripts\activate | Unix: source venv/bin/activate -# Install Dependencies +# Install Core Dependencies pip install -r requirements.txt ``` +#### Optional: Long-Term Memory & RAG Support +If you plan to use vector memory features (such as long-term agent memory across sessions), you will need to install the heavier machine learning dependencies separately: + +```bash +pip install -r requirements-memory.txt ### 2. Configure defaults (optional) diff --git a/requirements-memory.txt b/requirements-memory.txt new file mode 100644 index 0000000..545764d --- /dev/null +++ b/requirements-memory.txt @@ -0,0 +1,3 @@ +chromadb>=0.4.0 +sentence-transformers>=2.2.2 +torch>=2.0.0 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 17035f7..0381f69 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,4 @@ litellm>=1.20.0 -chromadb>=0.4.0 -sentence-transformers>=2.2.2 -torch>=2.0.0 rich>=13.0.0 pydantic>=2.0.0 prompt_toolkit>=3.0.0 From 4d54509ad861f9f436e41a42f36d8755b64e8731 Mon Sep 17 00:00:00 2001 From: Aainee Sinha Date: Sun, 7 Jun 2026 11:40:26 +0530 Subject: [PATCH 2/8] docs: fix formatting, clean up tree layout, and reference issue 13 in readme --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 69cbfdd..8bd4ae6 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ Rooms/ ├── cli.py # Interactive Wizard Entry Point ├── rooms.settings.example.yaml # Settings template (commit this) ├── requirements.txt # Core Project Dependencies -└── requirements-memory.txt # Optional Vector Memory Dependencies # Project Dependencies +└── requirements-memory.txt # Optional Vector Memory Dependencies ``` `rooms.settings.yaml` is gitignored — create it locally with `python cli.py config init` or by copying the example file. @@ -99,8 +99,7 @@ venv\Scripts\activate # Windows: venv\Scripts\activate | Unix: source venv/bin/ pip install -r requirements.txt ``` #### Optional: Long-Term Memory & RAG Support -If you plan to use vector memory features (such as long-term agent memory across sessions), you will need to install the heavier machine learning dependencies separately: - +If you plan to use vector memory features (such as long-term agent memory across sessions (#13)), you will need to install the heavier machine learning dependencies separately: ```bash pip install -r requirements-memory.txt From a0a95e047f9e6f20b29df5c04b1cc27973974a01 Mon Sep 17 00:00:00 2001 From: Aainee Sinha Date: Sun, 7 Jun 2026 15:23:47 +0530 Subject: [PATCH 3/8] docs: close code block fence before configure defaults heading --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8bd4ae6..57fcb2d 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,7 @@ pip install -r requirements.txt If you plan to use vector memory features (such as long-term agent memory across sessions (#13)), you will need to install the heavier machine learning dependencies separately: ```bash pip install -r requirements-memory.txt +``` ### 2. Configure defaults (optional) From ff0c6e4f6577f18ef5d24421bb68b981e24adfac Mon Sep 17 00:00:00 2001 From: Aainee Sinha Date: Sun, 7 Jun 2026 17:55:17 +0530 Subject: [PATCH 4/8] fix: resolve ollama preflight imports and directory path breakdown in smoke tests --- cli.py | 12 ++++++++++++ tests/test_cli_settings_smoke.py | 6 ++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/cli.py b/cli.py index 129104f..a8428e0 100644 --- a/cli.py +++ b/cli.py @@ -306,6 +306,14 @@ def build_parser() -> argparse.ArgumentParser: return parser +def check_ollama_preflight(settings) -> bool: + """ + Runs an optional preflight check for Ollama reachability and model availability + if the default configured model relies on a local Ollama instance. + """ + from rooms import ollama_preflight + return ollama_preflight.run_ollama_preflight(settings) + def main(argv: Optional[List[str]] = None) -> int: parser = build_parser() args = parser.parse_args(argv) @@ -330,6 +338,10 @@ def main(argv: Optional[List[str]] = None) -> int: if found: console.print(f"[dim]Using settings: {found}[/dim]") + # Run the Ollama preflight check. If it fails, exit cleanly without starting the wizard. + if not check_ollama_preflight(settings): + return 1 + main_menu(settings) return 0 diff --git a/tests/test_cli_settings_smoke.py b/tests/test_cli_settings_smoke.py index 35b80aa..fb409ac 100644 --- a/tests/test_cli_settings_smoke.py +++ b/tests/test_cli_settings_smoke.py @@ -56,11 +56,13 @@ def test_cli_main_loads_explicit_config_without_wizard(tmp_path, monkeypatch): encoding="utf-8", ) - with patch.object(cli, "main_menu") as mock_menu: + with patch.object(cli, "main_menu") as mock_menu, \ + patch.object(cli, "check_ollama_preflight", return_value=True) as mock_preflight: rc = cli.main(["--config", str(cfg)]) assert rc == 0 mock_menu.assert_called_once() + mock_preflight.assert_called_once() settings = mock_menu.call_args[0][0] assert settings.defaults.litellm_model == "ollama/smoke:1b" assert settings.defaults.timeout == 99 @@ -88,4 +90,4 @@ def test_settings_error_from_init_guard(tmp_path, monkeypatch): cli.main(["config", "init"]) with pytest.raises(SettingsError): from rooms.settings import init_settings_file - init_settings_file() + init_settings_file() \ No newline at end of file From 44a5140bcb2f13cd4199976c606ce42b480edb26 Mon Sep 17 00:00:00 2001 From: Aainee Sinha Date: Sun, 7 Jun 2026 18:04:51 +0530 Subject: [PATCH 5/8] docs: document --skip-preflight flag in EXAMPLES.md --- docs/EXAMPLES.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md index 2601778..a90d483 100644 --- a/docs/EXAMPLES.md +++ b/docs/EXAMPLES.md @@ -219,3 +219,14 @@ The quality of your agents is entirely determined by the quality of their system | Agent addresses you directly | Session auto-triggers HITL early | This is by design — respond or type `continue` | | Agent has nothing to add | Returns `PASS` | Turn is silently skipped; visible in logs only | | Topic is very broad | Agents go off-scope | Add Orchestrator with *"steer back to [topic] if agents drift"* | + +--- + +## Advanced CLI Reference + +### Skipping Preflight Checks +If you are running the application in a CI/CD automation environment, running automated test configurations, or simply wish to bypass the local Ollama connectivity and model verification sequence, append the `--skip-preflight` flag alongside your execution statement: + +```bash +python cli.py --skip-preflight +``` From 798cd4f2615235392ce16162cefecf9d59d72f22 Mon Sep 17 00:00:00 2001 From: Aainee Sinha Date: Mon, 8 Jun 2026 14:46:13 +0530 Subject: [PATCH 6/8] feat: implement preflight validation check module, wire up cli parser flag, and add comprehensive unit tests --- cli.py | 13 +++++-- rooms/ollama_preflight.py | 69 ++++++++++++++++++++++++++++++++++ tests/test_ollama_preflight.py | 48 +++++++++++++++++++++++ 3 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 rooms/ollama_preflight.py create mode 100644 tests/test_ollama_preflight.py diff --git a/cli.py b/cli.py index a8428e0..e6f6b1c 100644 --- a/cli.py +++ b/cli.py @@ -303,6 +303,12 @@ def build_parser() -> argparse.ArgumentParser: reset_p.add_argument("--path", help="Specific settings file to remove") reset_p.add_argument("-y", "--yes", action="store_true", help="Skip confirmation") + parser.add_argument( + "--skip-preflight", + action="store_true", + help="Skip Ollama preflight connectivity and model validation checks" + ) + return parser @@ -338,9 +344,10 @@ def main(argv: Optional[List[str]] = None) -> int: if found: console.print(f"[dim]Using settings: {found}[/dim]") - # Run the Ollama preflight check. If it fails, exit cleanly without starting the wizard. - if not check_ollama_preflight(settings): - return 1 + # Run the Ollama preflight check unless --skip-preflight is passed. + if not args.skip_preflight: + if not check_ollama_preflight(settings): + return 1 main_menu(settings) return 0 diff --git a/rooms/ollama_preflight.py b/rooms/ollama_preflight.py new file mode 100644 index 0000000..c5594ef --- /dev/null +++ b/rooms/ollama_preflight.py @@ -0,0 +1,69 @@ +import urllib.request +import urllib.error +import json +import sys +from rich.console import Console +from rich.panel import Panel + +def run_ollama_preflight(settings) -> bool: + """ + Verifies if the configured local Ollama instance is running + and contains the requested model tag. + """ + model_string = getattr(settings.defaults, "litellm_model", "") + if not model_string.startswith("ollama/"): + return True + + # Extract the tag name (e.g., 'ollama/gemma4:e2b' -> 'gemma4:e2b') + configured_tag = model_string.split("/", 1)[1] + base_url = getattr(settings.ollama, "base_url", "http://localhost:11434").rstrip("/") + tags_url = f"{base_url}/api/tags" + + console = Console() + + try: + req = urllib.request.Request(tags_url, method="GET") + with urllib.request.urlopen(req, timeout=3.0) as response: + if response.status != 200: + raise urllib.error.URLError(f"HTTP Status {response.status}") + + data = json.loads(response.read().decode("utf-8")) + models = data.get("models", []) + + available_tags = [] + for m in models: + if "name" in m: + available_tags.append(m["name"]) + if "model" in m: + available_tags.append(m["model"]) + + if configured_tag in available_tags or f"{configured_tag}:latest" in available_tags: + return True + + # Server is up, but model tag is missing + panel = Panel( + f"[bold yellow]Warning:[/bold yellow] Configured Ollama model [bold cyan]'{configured_tag}'[/bold cyan] was not found locally.\n\n" + f"[bold white]Actionable Fixes:[/bold white]\n" + f" • Run: [green]ollama pull {configured_tag}[/green]\n" + f" • Edit your configuration file to use an available tag.\n" + f" • Run with [green]python cli.py --skip-preflight[/green] to bypass.", + title="[bold red]Ollama Preflight Verification Failed[/bold red]", + expand=False + ) + console.print(panel) + return False + + except (urllib.error.URLError, TimeoutError, ConnectionError) as e: + # Ollama service is completely unreachable + panel = Panel( + f"[bold yellow]Warning:[/bold yellow] Could not connect to Ollama server at [cyan]{base_url}[/cyan]\n" + f"Error Details: {str(e)}\n\n" + f"[bold white]Actionable Fixes:[/bold white]\n" + f" • Ensure Ollama is running by executing: [green]ollama serve[/green]\n" + f" • Verify your [magenta]ollama.base_url[/magenta] settings match your active instance.\n" + f" • Run with [green]python cli.py --skip-preflight[/green] to bypass.", + title="[bold red]Ollama Server Unreachable[/bold red]", + expand=False + ) + console.print(panel) + return False \ No newline at end of file diff --git a/tests/test_ollama_preflight.py b/tests/test_ollama_preflight.py new file mode 100644 index 0000000..6f754d0 --- /dev/null +++ b/tests/test_ollama_preflight.py @@ -0,0 +1,48 @@ +import unittest +from unittest.mock import patch, MagicMock +import urllib.error +import io + +from rooms.ollama_preflight import run_ollama_preflight + +class TestOllamaPreflightLogic(unittest.TestCase): + def setUp(self): + # Setup clean configuration mock structure + self.mock_settings = MagicMock() + self.mock_settings.defaults.litellm_model = "ollama/gemma4:e2b" + self.mock_settings.ollama.base_url = "http://localhost:11434" + + def test_skips_non_ollama_models(self): + self.mock_settings.defaults.litellm_model = "openai/gpt-4" + result = run_ollama_preflight(self.mock_settings) + self.assertTrue(result) + + @patch("urllib.request.urlopen") + def test_preflight_success_exact_match(self, mock_urlopen): + # Simulate clean API json payload back from Ollama + mock_response = MagicMock() + mock_response.status = 200 + mock_response.read.return_value = b'{"models": [{"name": "gemma4:e2b"}, {"name": "llama3:latest"}]}' + mock_urlopen.return_value.__enter__.return_value = mock_response + + result = run_ollama_preflight(self.mock_settings) + self.assertTrue(result) + + @patch("urllib.request.urlopen") + @patch("sys.stdout", new_callable=io.StringIO) + def test_preflight_missing_model_tag(self, mock_stdout, mock_urlopen): + mock_response = MagicMock() + mock_response.status = 200 + mock_response.read.return_value = b'{"models": [{"name": "llama3:latest"}]}' + mock_urlopen.return_value.__enter__.return_value = mock_response + + result = run_ollama_preflight(self.mock_settings) + self.assertFalse(result) + + @patch("urllib.request.urlopen") + @patch("sys.stdout", new_callable=io.StringIO) + def test_preflight_server_unreachable(self, mock_stdout, mock_urlopen): + mock_urlopen.side_effect = urllib.error.URLError("Connection refused") + + result = run_ollama_preflight(self.mock_settings) + self.assertFalse(result) \ No newline at end of file From b2db8659606ca6317acf3195e8b574b717116cb6 Mon Sep 17 00:00:00 2001 From: Chirag04-bit Date: Tue, 9 Jun 2026 13:35:09 +0530 Subject: [PATCH 7/8] feat: add ollama auto model selection support --- cli.py | 4 +++- rooms/settings.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/cli.py b/cli.py index e6f6b1c..7648025 100644 --- a/cli.py +++ b/cli.py @@ -21,8 +21,10 @@ init_settings_file, reset_settings_file, find_settings_file, + resolve_ollama_model, EXAMPLE_SETTINGS_FILENAME, USER_SETTINGS_FILENAME, + ) console = Console() @@ -79,7 +81,7 @@ def create_custom_agent_wizard(settings: RoomsSettings, tracked_env_keys: Option config.custom_function_name = Prompt.ask("Enter the exact function name to call (e.g. process_inference)") else: config.model_type = ModelType.LITELLM - default_model = defaults.litellm_model + default_model = resolve_ollama_model(settings) console.print( "[dim]Hint: For local Ollama use your tag from `ollama list` (e.g. " f"'{default_model}'). For OpenAI use 'gpt-4o'.[/dim]" diff --git a/rooms/settings.py b/rooms/settings.py index 53ccf13..42290b4 100644 --- a/rooms/settings.py +++ b/rooms/settings.py @@ -4,8 +4,12 @@ import os import shutil +import json + from pathlib import Path from typing import Dict, List, Optional +from urllib.request import urlopen +from urllib.error import URLError import yaml from pydantic import BaseModel, Field, ValidationError @@ -134,6 +138,30 @@ def _apply_ollama_env(settings: RoomsSettings) -> None: if settings.ollama.base_url: os.environ.setdefault("OLLAMA_API_BASE", settings.ollama.base_url) +def resolve_ollama_model(settings: RoomsSettings) -> str: + model = settings.defaults.litellm_model + + if not settings.ollama.auto_select_first: + return model + + if model != "ollama/auto": + return model + + try: + url = f"{settings.ollama.base_url}/api/tags" + + with urlopen(url, timeout=3) as response: + data = json.loads(response.read().decode("utf-8")) + + models = data.get("models", []) + + if models: + return f"ollama/{models[0]['name']}" + + except (URLError, KeyError, IndexError, json.JSONDecodeError): + pass + + return model def load_settings(explicit_path: Optional[str] = None, *, required: bool = False) -> RoomsSettings: """Load settings from the first matching file, or return built-in defaults.""" From ccb107617da128ff611edce409ccada7fd15fd28 Mon Sep 17 00:00:00 2001 From: Chirag04-bit Date: Fri, 12 Jun 2026 14:41:23 +0530 Subject: [PATCH 8/8] refactor: address reviewer feedback, unify preflight logic, and add comprehensive tests --- rooms.settings.example.yaml | 4 +- rooms/ollama_preflight.py | 75 ++++++++++++++++++++----------------- rooms/settings.py | 26 +++++++++---- tests/test_settings.py | 45 +++++++++++++++++++++- 4 files changed, 106 insertions(+), 44 deletions(-) diff --git a/rooms.settings.example.yaml b/rooms.settings.example.yaml index 413d217..45c7ba9 100644 --- a/rooms.settings.example.yaml +++ b/rooms.settings.example.yaml @@ -2,6 +2,7 @@ # See: https://github.com/ARPAHLS/rooms/issues/26 and #27 defaults: + # Set to "ollama/auto" to automatically pick the first model returned by your local engine litellm_model: "ollama/gemma4:e2b" orchestrator_model: "ollama/gemma4:e2b" temperature: 0.7 @@ -15,6 +16,7 @@ presets: api_key_env: "OPENAI_API_KEY" ollama: + # When litellm_model is set to "ollama/auto", true selects the first active model from `ollama list` auto_select_first: false base_url: "http://localhost:11434" @@ -32,4 +34,4 @@ use_shipped_personas: true # expertise: ["law", "contracts"] # model: null # temperature: null -# color: "magenta" +# # color: "magenta" \ No newline at end of file diff --git a/rooms/ollama_preflight.py b/rooms/ollama_preflight.py index c5594ef..9efd237 100644 --- a/rooms/ollama_preflight.py +++ b/rooms/ollama_preflight.py @@ -5,6 +5,25 @@ from rich.console import Console from rich.panel import Panel +def fetch_local_ollama_tags(base_url: str) -> list: + """Fetches and handles the raw array of tags available from the local Ollama instance.""" + tags_url = f"{base_url.rstrip('/')}/api/tags" + req = urllib.request.Request(tags_url, method="GET") + with urllib.request.urlopen(req, timeout=3.0) as response: + if response.status != 200: + raise urllib.error.URLError(f"HTTP Status {response.status}") + + data = json.loads(response.read().decode("utf-8")) + models = data.get("models", []) + + available_tags = [] + for m in models: + if "name" in m: + available_tags.append(m["name"]) + if "model" in m: + available_tags.append(m["model"]) + return available_tags + def run_ollama_preflight(settings) -> bool: """ Verifies if the configured local Ollama instance is running @@ -16,42 +35,30 @@ def run_ollama_preflight(settings) -> bool: # Extract the tag name (e.g., 'ollama/gemma4:e2b' -> 'gemma4:e2b') configured_tag = model_string.split("/", 1)[1] - base_url = getattr(settings.ollama, "base_url", "http://localhost:11434").rstrip("/") - tags_url = f"{base_url}/api/tags" - + base_url = getattr(settings.ollama, "base_url", "http://localhost:11434") console = Console() try: - req = urllib.request.Request(tags_url, method="GET") - with urllib.request.urlopen(req, timeout=3.0) as response: - if response.status != 200: - raise urllib.error.URLError(f"HTTP Status {response.status}") - - data = json.loads(response.read().decode("utf-8")) - models = data.get("models", []) - - available_tags = [] - for m in models: - if "name" in m: - available_tags.append(m["name"]) - if "model" in m: - available_tags.append(m["model"]) + available_tags = fetch_local_ollama_tags(base_url) - if configured_tag in available_tags or f"{configured_tag}:latest" in available_tags: - return True + # Flexible matching checking both string formats directly + if (configured_tag in available_tags or + f"{configured_tag}:latest" in available_tags or + (configured_tag.endswith(":latest") and configured_tag[:-7] in available_tags)): + return True - # Server is up, but model tag is missing - panel = Panel( - f"[bold yellow]Warning:[/bold yellow] Configured Ollama model [bold cyan]'{configured_tag}'[/bold cyan] was not found locally.\n\n" - f"[bold white]Actionable Fixes:[/bold white]\n" - f" • Run: [green]ollama pull {configured_tag}[/green]\n" - f" • Edit your configuration file to use an available tag.\n" - f" • Run with [green]python cli.py --skip-preflight[/green] to bypass.", - title="[bold red]Ollama Preflight Verification Failed[/bold red]", - expand=False - ) - console.print(panel) - return False + # Server is up, but model tag is missing + panel = Panel( + f"[bold yellow]Warning:[/bold yellow] Configured Ollama model [bold cyan]'{configured_tag}'[/bold cyan] was not found locally.\n\n" + f"[bold white]Actionable Fixes:[/bold white]\n" + f" • Run: [green]ollama pull {configured_tag}[/green]\n" + f" • Edit your configuration file to use an available tag.\n" + f" • Run with [green]python cli.py --skip-preflight[/green] to bypass.", + title="[bold red]Ollama Preflight Verification Failed[/bold red]", + expand=False + ) + console.print(panel) + return False except (urllib.error.URLError, TimeoutError, ConnectionError) as e: # Ollama service is completely unreachable @@ -59,9 +66,9 @@ def run_ollama_preflight(settings) -> bool: f"[bold yellow]Warning:[/bold yellow] Could not connect to Ollama server at [cyan]{base_url}[/cyan]\n" f"Error Details: {str(e)}\n\n" f"[bold white]Actionable Fixes:[/bold white]\n" - f" • Ensure Ollama is running by executing: [green]ollama serve[/green]\n" - f" • Verify your [magenta]ollama.base_url[/magenta] settings match your active instance.\n" - f" • Run with [green]python cli.py --skip-preflight[/green] to bypass.", + f" • Ensure Ollama is running by executing: [green]ollama serve[/green]\n" + f" • Verify your [magenta]ollama.base_url[/magenta] settings match your active instance.\n" + f" • Run with [green]python cli.py --skip-preflight[/green] to bypass.", title="[bold red]Ollama Server Unreachable[/bold red]", expand=False ) diff --git a/rooms/settings.py b/rooms/settings.py index 42290b4..e337d8d 100644 --- a/rooms/settings.py +++ b/rooms/settings.py @@ -6,10 +6,13 @@ import shutil import json +from typing import List, Optional + from pathlib import Path from typing import Dict, List, Optional from urllib.request import urlopen from urllib.error import URLError +from rooms.ollama_preflight import fetch_local_ollama_tags import yaml from pydantic import BaseModel, Field, ValidationError @@ -148,17 +151,18 @@ def resolve_ollama_model(settings: RoomsSettings) -> str: return model try: - url = f"{settings.ollama.base_url}/api/tags" - - with urlopen(url, timeout=3) as response: - data = json.loads(response.read().decode("utf-8")) + # Local import handles the circular dependency beautifully + from rooms.ollama_preflight import fetch_local_ollama_tags - models = data.get("models", []) + # Delegate the API fetch to your shared preflight helper + available_tags = fetch_local_ollama_tags(settings.ollama.base_url) - if models: - return f"ollama/{models[0]['name']}" + if available_tags: + # Fall back safely to the first active local model tag + return f"ollama/{available_tags[0]}" - except (URLError, KeyError, IndexError, json.JSONDecodeError): + except Exception: + # Fall back to 'ollama/auto' if the server is down/unreachable pass return model @@ -174,6 +178,8 @@ def load_settings(explicit_path: Optional[str] = None, *, required: bool = False ) settings = RoomsSettings() _apply_ollama_env(settings) + # Globally resolve ollama/auto for built-in defaults + settings.defaults.litellm_model = resolve_ollama_model(settings) return settings try: @@ -186,6 +192,8 @@ def load_settings(explicit_path: Optional[str] = None, *, required: bool = False ) from e _apply_ollama_env(settings) + # Globally resolve ollama/auto for loaded custom files + settings.defaults.litellm_model = resolve_ollama_model(settings) return settings @@ -194,6 +202,7 @@ def persona_settings_to_agent_config(persona: PersonaSettings, defaults: Default name=persona.name, system_prompt=persona.system_prompt, expertise=persona.expertise, + custom_instructions="", # Use an empty string to satisfy Pylance's field check model=persona.model or defaults.litellm_model, temperature=persona.temperature if persona.temperature is not None else defaults.temperature, timeout=defaults.timeout, @@ -209,6 +218,7 @@ def _shipped_persona_dicts_to_configs(defaults: DefaultsSettings) -> List[AgentC name=data["name"], system_prompt=data["system_prompt"], expertise=data["expertise"], + custom_instructions="", # Use an empty string here too model=defaults.litellm_model, temperature=defaults.temperature, timeout=defaults.timeout, diff --git a/tests/test_settings.py b/tests/test_settings.py index 095a55f..667372e 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -22,8 +22,14 @@ def test_builtin_defaults_without_file(tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) + # Mock preflight tag fetch to match baseline defaults during testing + monkeypatch.setattr( + "rooms.ollama_preflight.fetch_local_ollama_tags", + lambda base_url: ["gemma4:e2b"] + ) settings = load_settings() - assert settings.defaults.litellm_model == "ollama/gemma4:e2b" + # If defaults fall back or resolve, ensure we handle the test smoothly + assert settings.defaults.litellm_model in ["ollama/gemma4:e2b", "ollama/auto"] assert settings.user.name == "User" @@ -101,3 +107,40 @@ def test_explicit_config_required_missing(tmp_path): missing = tmp_path / "nope.yaml" with pytest.raises(SettingsError): load_settings(str(missing), required=True) + + +def test_resolve_ollama_model_auto_success(monkeypatch): + """Verifies ollama/auto successfully resolves to the first available engine tag.""" + from rooms.settings import resolve_ollama_model + + monkeypatch.setattr( + "rooms.ollama_preflight.fetch_local_ollama_tags", + lambda base_url: ["llama3:latest", "gemma:7b"] + ) + + settings = RoomsSettings() + settings.defaults.litellm_model = "ollama/auto" + settings.ollama.auto_select_first = True + + resolved = resolve_ollama_model(settings) + assert resolved == "ollama/llama3:latest" + + +def test_resolve_ollama_model_auto_server_down(monkeypatch): + """Verifies resolution falls back gracefully to 'ollama/auto' when server is unreachable.""" + from rooms.settings import resolve_ollama_model + + def mock_fetch_failed(base_url): + raise ConnectionError("Server completely unreachable") + + monkeypatch.setattr( + "rooms.ollama_preflight.fetch_local_ollama_tags", + mock_fetch_failed + ) + + settings = RoomsSettings() + settings.defaults.litellm_model = "ollama/auto" + settings.ollama.auto_select_first = True + + resolved = resolve_ollama_model(settings) + assert resolved == "ollama/auto" \ No newline at end of file