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
16 changes: 13 additions & 3 deletions src/modeldock/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down
31 changes: 23 additions & 8 deletions src/modeldock/core/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -233,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:
Expand All @@ -244,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()
Expand Down
37 changes: 34 additions & 3 deletions src/modeldock/core/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -54,4 +85,4 @@ def by_category(self, category: Category) -> List[ModelSpec]:
return self._registry.by_category(category)


__all__ = ["RegistryService"]
__all__ = ["RegistryService"]
35 changes: 35 additions & 0 deletions tests/unit/test_search_filters.py
Original file line number Diff line number Diff line change
@@ -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