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 src/everos/component/embedding/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
- :class:`EmbeddingError` — backward-compat alias for ``EmbeddingServiceError``.
- :class:`OpenAIEmbeddingProvider` — concrete provider for any
OpenAI-protocol embeddings endpoint (DeepInfra, vLLM, OpenAI, …).
- :class:`OllamaEmbeddingProvider` — local Ollama embedding provider
with sensible defaults (``http://localhost:11434/v1``, dim=768).
- :func:`build_embedding_provider` — settings-driven factory.

External usage::
Expand All @@ -22,6 +24,7 @@
from .accessor import EmbeddingNotConfiguredError as EmbeddingNotConfiguredError
from .accessor import get_embedder as get_embedder
from .factory import build_embedding_provider as build_embedding_provider
from .ollama_provider import OllamaEmbeddingProvider as OllamaEmbeddingProvider
from .openai_provider import OpenAIEmbeddingProvider as OpenAIEmbeddingProvider
from .protocol import EmbeddingError as EmbeddingError
from .protocol import EmbeddingProvider as EmbeddingProvider
Expand All @@ -31,6 +34,7 @@
"EmbeddingNotConfiguredError",
"EmbeddingProvider",
"EmbeddingServiceError",
"OllamaEmbeddingProvider",
"OpenAIEmbeddingProvider",
"build_embedding_provider",
"get_embedder",
Expand Down
66 changes: 66 additions & 0 deletions src/everos/component/embedding/ollama_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Ollama embedding provider.

Ollama exposes an OpenAI-compatible ``/v1/embeddings`` endpoint at
``http://localhost:11434/v1``. This provider applies Ollama-specific
defaults so callers get a working configuration out of the box — no
need to remember the base URL or supply a dummy API key.

Usage::
from everos.component.embedding.ollama_provider import OllamaEmbeddingProvider

provider = OllamaEmbeddingProvider(
model="nomic-embed-text",
dim=768,
)
vec = await provider.embed("hello world")
"""

from __future__ import annotations

from .openai_provider import OpenAIEmbeddingProvider


_OLLAMA_DEFAULT_BASE_URL = "http://localhost:11434/v1"
_OLLAMA_PLACEHOLDER_KEY = "ollama"


class OllamaEmbeddingProvider(OpenAIEmbeddingProvider):
"""Ollama embedding provider — OpenAI-compatible with local defaults.

Same semantics as :class:`OpenAIEmbeddingProvider` except
``api_key`` and ``base_url`` fall back to Ollama's conventional
values when omitted.

Args:
model: Ollama model id (e.g. ``"nomic-embed-text"``).
api_key: API key; defaults to ``"ollama"``.
base_url: Endpoint; defaults to ``http://localhost:11434/v1``.
dim: Target vector dimension; defaults to 768 (Ollama's typical).
timeout: Per-request timeout, seconds.
max_retries: Retry budget.
batch_size: Embeddings per ``/embeddings`` call.
max_concurrent: Cap on in-flight chunked requests.
"""

def __init__(
self,
*,
model: str,
api_key: str = _OLLAMA_PLACEHOLDER_KEY,
base_url: str = _OLLAMA_DEFAULT_BASE_URL,
dim: int = 768,
timeout: float = 30.0,
max_retries: int = 3,
batch_size: int = 10,
max_concurrent: int = 5,
) -> None:
super().__init__(
model=model,
api_key=api_key,
base_url=base_url,
dim=dim,
timeout=timeout,
max_retries=max_retries,
batch_size=batch_size,
max_concurrent=max_concurrent,
)
4 changes: 4 additions & 0 deletions src/everos/component/llm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
- :class:`LLMNotConfiguredError` — raised when credentials are missing.
- :class:`OpenAIProvider` — concrete provider wrapping
``openai.AsyncOpenAI`` against any OpenAI-compatible endpoint.
- :class:`OllamaProvider` — local Ollama LLM provider with sensible
defaults (``http://localhost:11434/v1``, ``"ollama"`` key).
- :func:`build_llm_provider` — settings-driven factory.
- :func:`get_llm_client` — process-wide lazy singleton accessor.

Expand All @@ -24,6 +26,7 @@
from .client import get_llm_client as get_llm_client
from .client import get_multimodal_llm_client as get_multimodal_llm_client
from .factory import build_llm_provider as build_llm_provider
from .ollama_provider import OllamaProvider as OllamaProvider
from .openai_provider import OpenAIProvider as OpenAIProvider
from .protocol import ChatMessage as ChatMessage
from .protocol import ChatResponse as ChatResponse
Expand All @@ -37,6 +40,7 @@
"LLMClient",
"LLMError",
"LLMNotConfiguredError",
"OllamaProvider",
"OpenAIProvider",
"Usage",
"build_llm_provider",
Expand Down
61 changes: 61 additions & 0 deletions src/everos/component/llm/ollama_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Ollama LLM provider for everos.

Ollama exposes an OpenAI-compatible API at ``http://localhost:11434/v1``
by default, with ``api-key`` accepting any non-empty value. This
provider extends :class:`OpenAIProvider` with Ollama-specific defaults
so callers don't need to remember the canonical base URL or dummy key.

Usage::
settings = LLMSettings(
model="llama3.1",
base_url="http://localhost:11434/v1", # or omit — Ollama default
api_key=SecretStr("ollama"), # or omit — Ollama default
)
provider = OllamaProvider(model=settings.model)
"""

from __future__ import annotations

from typing import Any

from .openai_provider import OpenAIProvider
from .protocol import ChatMessage, ChatResponse


_OLLAMA_DEFAULT_BASE_URL = "http://localhost:11434/v1"
_OLLAMA_PLACEHOLDER_KEY = "ollama"


class OllamaProvider(OpenAIProvider):
"""Ollama LLM provider — OpenAI-compatible with local defaults.

Identical to :class:`OpenAIProvider` except that ``api_key`` and
``base_url`` fall back to Ollama's conventional values when omitted.

Args:
model: Ollama model id (e.g. ``"llama3.1"``, ``"qwen2.5"``).
api_key: API key; defaults to ``"ollama"`` (Ollama's placeholder).
base_url: Endpoint; defaults to ``http://localhost:11434/v1``.
timeout: Per-request timeout in seconds.
temperature: Default sampling temperature.
max_tokens: Default max-tokens cap.
"""

def __init__(
self,
*,
model: str,
api_key: str = _OLLAMA_PLACEHOLDER_KEY,
base_url: str | None = _OLLAMA_DEFAULT_BASE_URL,
timeout: float = 60.0,
temperature: float = 0.0,
max_tokens: int | None = None,
) -> None:
super().__init__(
model=model,
api_key=api_key,
base_url=base_url,
timeout=timeout,
temperature=temperature,
max_tokens=max_tokens,
)
106 changes: 106 additions & 0 deletions tests/unit/test_component/test_ollama_providers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Tests for Ollama LLM and Embedding providers."""

from __future__ import annotations

import sys
from unittest.mock import AsyncMock, patch

import pytest

# ── LLM Provider ────────────────────────────────────────────────────


class TestOllamaProvider:
"""Ollama LLM provider tests."""

def test_defaults(self):
"""OllamaProvider applies localhost defaults when omitted."""
from everos.component.llm.ollama_provider import OllamaProvider

with patch("openai.AsyncOpenAI"):
p = OllamaProvider(model="llama3.1")

assert p._model == "llama3.1"

def test_custom_base_url(self):
"""Custom base_url and api_key are respected."""
from everos.component.llm.ollama_provider import OllamaProvider

with patch("openai.AsyncOpenAI"):
p = OllamaProvider(
model="qwen2.5",
api_key="custom-key",
base_url="http://192.168.1.100:11434/v1",
)

assert p._model == "qwen2.5"

def test_extends_openai_provider(self):
"""OllamaProvider inherits from OpenAIProvider."""
from everos.component.llm.ollama_provider import OllamaProvider
from everos.component.llm.openai_provider import OpenAIProvider

assert issubclass(OllamaProvider, OpenAIProvider)


# ── Embedding Provider ──────────────────────────────────────────────


class TestOllamaEmbeddingProvider:
"""Ollama Embedding provider tests."""

def test_defaults(self):
"""OllamaEmbeddingProvider applies localhost + dim=768 defaults."""
from everos.component.embedding.ollama_provider import (
OllamaEmbeddingProvider,
)

with patch("openai.AsyncOpenAI"):
p = OllamaEmbeddingProvider(model="nomic-embed-text")

assert p._model == "nomic-embed-text"
assert p.dim == 768

def test_custom_dim(self):
"""Custom dim overrides the default 768."""
from everos.component.embedding.ollama_provider import (
OllamaEmbeddingProvider,
)

with patch("openai.AsyncOpenAI"):
p = OllamaEmbeddingProvider(
model="mxbai-embed-large",
dim=1024,
)

assert p.dim == 1024

def test_extends_openai_provider(self):
"""OllamaEmbeddingProvider inherits from OpenAIEmbeddingProvider."""
from everos.component.embedding.ollama_provider import (
OllamaEmbeddingProvider,
)
from everos.component.embedding.openai_provider import (
OpenAIEmbeddingProvider,
)

assert issubclass(OllamaEmbeddingProvider, OpenAIEmbeddingProvider)


# ── Import surface ──────────────────────────────────────────────────


class TestImportSurface:
"""Verify public imports work from the package level."""

def test_llm_ollama_importable(self):
"""OllamaProvider is importable from everos.component.llm."""
from everos.component.llm import OllamaProvider

assert OllamaProvider is not None

def test_embedding_ollama_importable(self):
"""OllamaEmbeddingProvider is importable from everos.component.embedding."""
from everos.component.embedding import OllamaEmbeddingProvider

assert OllamaEmbeddingProvider is not None