From bf3946178694aa833105312d90cd535d45be5a05 Mon Sep 17 00:00:00 2001 From: Harshal Sawant Date: Mon, 21 Sep 2026 19:56:54 +0530 Subject: [PATCH 1/3] feat: add keyword filters to md.search (fixes #54) --- src/modeldock/__init__.py | 16 ++++++++++--- src/modeldock/core/registry.py | 37 ++++++++++++++++++++++++++++--- tests/unit/test_search_filters.py | 35 +++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 tests/unit/test_search_filters.py diff --git a/src/modeldock/__init__.py b/src/modeldock/__init__.py index ad850bc..9ca93c7 100644 --- a/src/modeldock/__init__.py +++ b/src/modeldock/__init__.py @@ -67,9 +67,19 @@ def list() -> List[Any]: return _manager().list() -def search(query: str) -> List[Any]: - """Search by name / capability / category.""" - return _manager().search(query) +def search( + query: str = "", + category: Optional[str] = None, + capability: Optional[str] = None, + min_ram: Optional[int] = None, +) -> List[Any]: + """Search by name / capability / category / RAM.""" + return _manager().search( + query=query, + category=category, + capability=capability, + min_ram=min_ram + ) def installed() -> List[ModelRef]: diff --git a/src/modeldock/core/registry.py b/src/modeldock/core/registry.py index bdf947f..e6c399a 100644 --- a/src/modeldock/core/registry.py +++ b/src/modeldock/core/registry.py @@ -18,9 +18,40 @@ class RegistryService: def __init__(self, registry: RegistryPort) -> None: self._registry = registry - def search(self, query: str) -> List[ModelSpec]: + def search( + self, + query: str = "", + category: str | None = None, + capability: str | None = None, + min_ram: int | None = None + ) -> List[ModelSpec]: """Search the catalog by name/alias/capability/category.""" - return self._registry.search(query) + # 1. Base search: use query if provided, otherwise grab all to filter + if query: + results = self._registry.search(query) + else: + results = self._registry.list_all() + + # 2. Apply filters safely + if category is not None: + results = [ + m for m in results + if m.category == category or (hasattr(m.category, 'value') and m.category.value == category) + ] + + if capability is not None: + results = [ + m for m in results + if capability in getattr(m, 'capabilities', []) + ] + + if min_ram is not None: + results = [ + m for m in results + if getattr(m, 'ram', 0) >= min_ram + ] + + return results def info(self, name: str, installed_tags: List[str] | None = None) -> ModelInfo: """Return metadata for a model, enriched with installed tags. @@ -54,4 +85,4 @@ def by_category(self, category: Category) -> List[ModelSpec]: return self._registry.by_category(category) -__all__ = ["RegistryService"] +__all__ = ["RegistryService"] \ No newline at end of file diff --git a/tests/unit/test_search_filters.py b/tests/unit/test_search_filters.py new file mode 100644 index 0000000..e70af44 --- /dev/null +++ b/tests/unit/test_search_filters.py @@ -0,0 +1,35 @@ +from unittest.mock import Mock +from modeldock.core.registry import RegistryService +from modeldock.domain.model import ModelSpec + +def test_search_backward_compatibility(): + mock_port = Mock() + mock_port.search.return_value = [Mock(spec=ModelSpec)] + service = RegistryService(registry=mock_port) + + results = service.search("llama") + + assert len(results) == 1 + mock_port.search.assert_called_once_with("llama") + +def test_search_with_keyword_filters(): + mock_port = Mock() + + # Create fake model specs for filtering + m1 = Mock(spec=ModelSpec, category="text", capabilities=["chat"], ram=8) + m2 = Mock(spec=ModelSpec, category="image", capabilities=["vision"], ram=16) + + # When query is empty, it calls list_all + mock_port.list_all.return_value = [m1, m2] + service = RegistryService(registry=mock_port) + + # Test category filter + assert len(service.search(category="image")) == 1 + assert service.search(category="image")[0] == m2 + + # Test capability filter + assert len(service.search(capability="chat")) == 1 + + # Test min_ram filter + assert len(service.search(min_ram=10)) == 1 + assert service.search(min_ram=10)[0] == m2 \ No newline at end of file From 0ad1b26ada8bf3a8fc0239137bf0d14a4e4388bc Mon Sep 17 00:00:00 2001 From: Harshal Sawant Date: Tue, 22 Sep 2026 20:43:16 +0530 Subject: [PATCH 2/3] ci: update tests and fix cli output --- src/modeldock/core/manager.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/modeldock/core/manager.py b/src/modeldock/core/manager.py index 070136c..7c2d20c 100644 --- a/src/modeldock/core/manager.py +++ b/src/modeldock/core/manager.py @@ -216,10 +216,20 @@ def list(self) -> List[Any]: """List all known models in the catalog.""" return self._registry.list_all() - def search(self, query: str) -> List[Any]: - """Search the catalog by name/alias/capability/category.""" - return self._registry.search(query) - + def search( + self, + query: str = "", + category: Optional[str] = None, + capability: Optional[str] = None, + min_ram: Optional[int] = None, + ) -> List[Any]: + """Search models via registry service.""" + return self._registry.search( + query=query, + category=category, + capability=capability, + min_ram=min_ram, + ) def installed(self) -> List[ModelRef]: """Return models present locally in the active runtime.""" return self._runtime.list_installed() From b74ab8bcc4d6562b6a9206d6191e02042b5a4c0e Mon Sep 17 00:00:00 2001 From: Harshal Sawant Date: Tue, 22 Sep 2026 21:04:28 +0530 Subject: [PATCH 3/3] fix(core): handle offline runtime gracefully in ModelManager.info --- src/modeldock/core/manager.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/modeldock/core/manager.py b/src/modeldock/core/manager.py index 7c2d20c..a8fd78f 100644 --- a/src/modeldock/core/manager.py +++ b/src/modeldock/core/manager.py @@ -243,9 +243,14 @@ def info(self, name: str) -> Any: back to a minimal ``ModelInfo`` built from the local reference. """ ref = ModelRef.parse(name) - installed_tags = [ - existing.tag for existing in self._runtime.list_installed() if existing.name == ref.name - ] + try: + installed_tags = [ + existing.tag for existing in self._runtime.list_installed() if existing.name == ref.name + ] + except Exception: + # Runtime is offline or unreachable - default to no installed tags + installed_tags = [] + try: return self._registry.info(name, installed_tags=installed_tags) except ModelNotFoundError: @@ -254,7 +259,7 @@ def info(self, name: str) -> Any: if installed_tags: return ModelInfo.from_ref(ref, installed_tags) raise - + def categories(self) -> List[Category]: """Return all catalog categories.""" return self._registry.categories()