diff --git a/list_issues.py b/list_issues.py new file mode 100644 index 000000000..8a51d8611 --- /dev/null +++ b/list_issues.py @@ -0,0 +1,10 @@ +import urllib.request, json +url = "https://api.github.com/repos/repowise-dev/repowise/issues?state=open&per_page=15" +req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) +with urllib.request.urlopen(req) as response: + data = json.loads(response.read().decode()) + +for item in data: + if "pull_request" not in item: + labels = [l["name"] for l in item["labels"]] + print(f"#{item['number']}: {item['title']} (Labels: {', '.join(labels)})") diff --git a/packages/cli/src/repowise/cli/providers/embedders.py b/packages/cli/src/repowise/cli/providers/embedders.py index 17bf2a451..41922644f 100644 --- a/packages/cli/src/repowise/cli/providers/embedders.py +++ b/packages/cli/src/repowise/cli/providers/embedders.py @@ -5,6 +5,8 @@ import os from typing import Any +from repowise.core.providers.embedding.base import EmbedderConfigError, parse_numeric_env + def _embedder_kwargs(embedder_name: str) -> dict[str, Any]: kwargs: dict[str, Any] = {} @@ -21,13 +23,15 @@ def _embedder_kwargs(embedder_name: str) -> dict[str, Any]: if base_url: kwargs["base_url"] = base_url if dimensions: - kwargs["dimensions"] = int(dimensions) + env_name = "OLLAMA_EMBEDDING_DIMS" if "OLLAMA_EMBEDDING_DIMS" in os.environ else "REPOWISE_EMBEDDING_DIMS" + kwargs["dimensions"] = parse_numeric_env(dimensions, env_name, is_int=True) if timeout: - kwargs["timeout"] = float(timeout) + env_name = "OLLAMA_EMBEDDING_TIMEOUT" if "OLLAMA_EMBEDDING_TIMEOUT" in os.environ else "REPOWISE_EMBEDDING_TIMEOUT" + kwargs["timeout"] = parse_numeric_env(timeout, env_name) elif embedder_name == "gemini": dimensions = os.environ.get("REPOWISE_EMBEDDING_DIMS") if dimensions: - kwargs["output_dimensionality"] = int(dimensions) + kwargs["output_dimensionality"] = parse_numeric_env(dimensions, "REPOWISE_EMBEDDING_DIMS", is_int=True) if model: kwargs["model"] = model return kwargs @@ -79,6 +83,10 @@ def build_embedder(embedder_name_resolved: str) -> Any: if embedder_name_resolved == "mock": return MockEmbedder() try: - return get_embedder(embedder_name_resolved, **_embedder_kwargs(embedder_name_resolved)) + kwargs = _embedder_kwargs(embedder_name_resolved) + except EmbedderConfigError: + raise + try: + return get_embedder(embedder_name_resolved, **kwargs) except Exception: return MockEmbedder() diff --git a/packages/core/src/repowise/core/providers/embedding/base.py b/packages/core/src/repowise/core/providers/embedding/base.py index 8fd26cffc..61acc64a1 100644 --- a/packages/core/src/repowise/core/providers/embedding/base.py +++ b/packages/core/src/repowise/core/providers/embedding/base.py @@ -13,7 +13,27 @@ import hashlib import math import struct -from typing import Protocol, runtime_checkable +from typing import Any, Protocol, runtime_checkable + + +class EmbedderConfigError(ValueError): + """Raised when an embedder configuration value is malformed.""" + pass + + +def parse_numeric_env(value: Any, name: str, is_int: bool = False) -> float | int: + """Parse a numeric environment variable, requiring it to be finite and > 0.""" + try: + parsed = int(value) if is_int else float(value) + except (ValueError, TypeError): + raise EmbedderConfigError( + f"Invalid {name}: {value!r} (must be a positive {'integer' if is_int else 'number'})" + ) + if math.isnan(parsed) or math.isinf(parsed) or parsed <= 0: + raise EmbedderConfigError( + f"Invalid {name}: {value!r} (must be a positive {'integer' if is_int else 'number'})" + ) + return parsed @runtime_checkable diff --git a/packages/core/src/repowise/core/providers/embedding/ollama.py b/packages/core/src/repowise/core/providers/embedding/ollama.py index bf09ceabd..c28ae48cc 100644 --- a/packages/core/src/repowise/core/providers/embedding/ollama.py +++ b/packages/core/src/repowise/core/providers/embedding/ollama.py @@ -69,19 +69,31 @@ def __init__( self._base_url = _normalize_base_url( base_url or os.environ.get("OLLAMA_BASE_URL") or _DEFAULT_BASE_URL ) + from repowise.core.providers.embedding.base import parse_numeric_env + env_dimensions = os.environ.get("OLLAMA_EMBEDDING_DIMS") or os.environ.get( "REPOWISE_EMBEDDING_DIMS" ) - self._requested_dimensions = dimensions or (int(env_dimensions) if env_dimensions else None) + if dimensions is not None: + self._requested_dimensions = dimensions + elif env_dimensions: + env_name = "OLLAMA_EMBEDDING_DIMS" if "OLLAMA_EMBEDDING_DIMS" in os.environ else "REPOWISE_EMBEDDING_DIMS" + self._requested_dimensions = parse_numeric_env(env_dimensions, env_name, is_int=True) + else: + self._requested_dimensions = None + self._dimensions = self._requested_dimensions or _infer_dimensions(self._model) + env_timeout = os.environ.get("OLLAMA_EMBEDDING_TIMEOUT") or os.environ.get( "REPOWISE_EMBEDDING_TIMEOUT" ) - self._timeout = ( - timeout - if timeout is not None - else (float(env_timeout) if env_timeout else _DEFAULT_TIMEOUT) - ) + if timeout is not None: + self._timeout = timeout + elif env_timeout: + env_name = "OLLAMA_EMBEDDING_TIMEOUT" if "OLLAMA_EMBEDDING_TIMEOUT" in os.environ else "REPOWISE_EMBEDDING_TIMEOUT" + self._timeout = parse_numeric_env(env_timeout, env_name) + else: + self._timeout = _DEFAULT_TIMEOUT @property def dimensions(self) -> int: diff --git a/tests/unit/cli/test_shared_helpers.py b/tests/unit/cli/test_shared_helpers.py index e6f716609..1694ba526 100644 --- a/tests/unit/cli/test_shared_helpers.py +++ b/tests/unit/cli/test_shared_helpers.py @@ -96,6 +96,24 @@ def test_build_embedder_ollama_timeout_from_env(monkeypatch: pytest.MonkeyPatch) assert embedder._timeout == 300.0 +def test_build_embedder_ollama_timeout_invalid(monkeypatch: pytest.MonkeyPatch) -> None: + from repowise.core.providers.embedding.base import EmbedderConfigError + + monkeypatch.setenv("OLLAMA_EMBEDDING_MODEL", "qwen3-embedding:0.6b") + monkeypatch.setenv("OLLAMA_EMBEDDING_TIMEOUT", "invalid") + with pytest.raises(EmbedderConfigError, match="Invalid OLLAMA_EMBEDDING_TIMEOUT: 'invalid'"): + providers.build_embedder("ollama") + + +def test_build_embedder_ollama_dimensions_invalid(monkeypatch: pytest.MonkeyPatch) -> None: + from repowise.core.providers.embedding.base import EmbedderConfigError + + monkeypatch.setenv("OLLAMA_EMBEDDING_MODEL", "qwen3-embedding:0.6b") + monkeypatch.setenv("OLLAMA_EMBEDDING_DIMS", "-5") + with pytest.raises(EmbedderConfigError, match="Invalid OLLAMA_EMBEDDING_DIMS: '-5'"): + providers.build_embedder("ollama") + + def test_build_vector_store_returns_a_store(tmp_path) -> None: from repowise.core.providers.embedding.base import MockEmbedder diff --git a/tests/unit/test_persistence/test_ollama_embedder.py b/tests/unit/test_persistence/test_ollama_embedder.py index f0ff13c09..04353abcd 100644 --- a/tests/unit/test_persistence/test_ollama_embedder.py +++ b/tests/unit/test_persistence/test_ollama_embedder.py @@ -149,3 +149,19 @@ async def test_env_timeout_is_applied_to_the_request(monkeypatch: pytest.MonkeyP await embedder.embed(["one"]) assert _FakeAsyncClient.calls[0]["timeout"] == 300.0 + + +def test_timeout_invalid_raises_error(monkeypatch: pytest.MonkeyPatch) -> None: + from repowise.core.providers.embedding.base import EmbedderConfigError + + monkeypatch.setenv("OLLAMA_EMBEDDING_TIMEOUT", "invalid") + with pytest.raises(EmbedderConfigError, match="Invalid OLLAMA_EMBEDDING_TIMEOUT: 'invalid'"): + OllamaEmbedder(model="embeddinggemma") + + +def test_dimensions_invalid_raises_error(monkeypatch: pytest.MonkeyPatch) -> None: + from repowise.core.providers.embedding.base import EmbedderConfigError + + monkeypatch.setenv("OLLAMA_EMBEDDING_DIMS", "-5") + with pytest.raises(EmbedderConfigError, match="Invalid OLLAMA_EMBEDDING_DIMS: '-5'"): + OllamaEmbedder(model="embeddinggemma")