-
Notifications
You must be signed in to change notification settings - Fork 6.2k
fix(knowledge_base): correct Ollama embedding URL and validate storage type #12412
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
chon3806
wants to merge
1
commit into
mindsdb:main
Choose a base branch
from
chon3806:fix/kb-ollama-embedding-and-storage-validation
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+225
−0
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
56 changes: 56 additions & 0 deletions
56
tests/unit/interfaces/knowledge_base/test_ollama_base_url.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| """Tests for ``LLMClient._normalize_ollama_base_url`` (gh-11910). | ||
|
|
||
| When a knowledge base is configured with an Ollama embedding model, the | ||
| OpenAI Python SDK appends route paths (``/embeddings``, ``/chat/completions``) | ||
| directly onto whatever ``base_url`` the client was constructed with. Ollama | ||
| serves its OpenAI-compatible API only under the ``/v1`` prefix, so a value | ||
| like ``http://localhost:11434`` resolves to ``http://localhost:11434/embeddings`` | ||
| which Ollama does not expose and answers with ``404 Not Found``. | ||
|
|
||
| These tests pin the normalization rules so future refactors cannot silently | ||
| re-introduce the original bug. | ||
| """ | ||
|
|
||
| import pytest | ||
|
|
||
| from mindsdb.interfaces.knowledge_base.llm_client import LLMClient | ||
|
|
||
|
|
||
| OLLAMA_DEFAULT = "http://localhost:11434/v1" | ||
|
|
||
|
|
||
| class TestNormalizeOllamaBaseUrl: | ||
| @pytest.mark.parametrize("missing", [None, ""]) | ||
| def test_missing_base_url_falls_back_to_local_default(self, missing): | ||
| assert LLMClient._normalize_ollama_base_url(missing) == OLLAMA_DEFAULT | ||
|
|
||
| def test_user_url_without_v1_gets_v1_appended(self): | ||
| # The exact case reported in gh-11910 — first user attempt. | ||
| assert LLMClient._normalize_ollama_base_url("http://localhost:11434") == "http://localhost:11434/v1" | ||
|
|
||
| def test_trailing_slash_is_collapsed_before_appending(self): | ||
| assert LLMClient._normalize_ollama_base_url("http://localhost:11434/") == "http://localhost:11434/v1" | ||
|
|
||
| def test_explicit_v1_is_left_unchanged(self): | ||
| assert LLMClient._normalize_ollama_base_url("http://localhost:11434/v1") == "http://localhost:11434/v1" | ||
|
|
||
| def test_explicit_v1_with_trailing_slash_is_normalized(self): | ||
| # rstrip("/") collapses trailing slashes; presence of /v1 still detected. | ||
| assert LLMClient._normalize_ollama_base_url("http://localhost:11434/v1/") == "http://localhost:11434/v1" | ||
|
|
||
| def test_remote_host_without_v1_gets_v1_appended(self): | ||
| assert ( | ||
| LLMClient._normalize_ollama_base_url("https://ollama.example.com:8080") | ||
| == "https://ollama.example.com:8080/v1" | ||
| ) | ||
|
|
||
| def test_url_with_v1_in_subpath_is_preserved(self): | ||
| # Some users front Ollama with a reverse proxy under a sub-path. | ||
| assert ( | ||
| LLMClient._normalize_ollama_base_url("http://gateway.local/ollama/v1") == "http://gateway.local/ollama/v1" | ||
| ) | ||
|
|
||
| def test_v10_is_not_mistaken_for_v1(self): | ||
| # Path-segment match must be exact: "v10" must not satisfy the "v1" check, | ||
| # otherwise we would silently drop the append and break custom deployments. | ||
| assert LLMClient._normalize_ollama_base_url("http://host/v10") == "http://host/v10/v1" |
132 changes: 132 additions & 0 deletions
132
tests/unit/interfaces/knowledge_base/test_storage_handler_validation.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| """Tests for KB storage handler validation (gh-11910). | ||
|
|
||
| Before this fix, ``KnowledgeBaseController.add`` happily accepted any | ||
| integration as ``storage`` and only blew up later inside | ||
| ``vector_store_handler.create_table(...)`` with an opaque | ||
| ``AttributeError: 'FileHandler' object has no attribute 'create_table'``. | ||
| The user has no way to tell from that message that they need to point | ||
| ``storage`` at a vector database. | ||
|
|
||
| This test pins the friendly, up-front ``ValueError`` so the bad message | ||
| cannot regress. | ||
| """ | ||
|
|
||
| from types import SimpleNamespace | ||
| from unittest.mock import MagicMock, patch | ||
|
|
||
| import pytest | ||
|
|
||
| from mindsdb_sql_parser.ast import Identifier | ||
|
|
||
| from mindsdb.integrations.libs.vectordatabase_handler import VectorStoreHandler | ||
| from mindsdb.interfaces.knowledge_base.controller import KnowledgeBaseController | ||
|
|
||
|
|
||
| class _NotAVectorStore: | ||
| """Stand-in for an integration handler that is not a vector database | ||
| (e.g. a FileHandler or a SQL handler).""" | ||
|
|
||
|
|
||
| class _FakeVectorStore(VectorStoreHandler): | ||
| """Concrete subclass so isinstance(_, VectorStoreHandler) is True.""" | ||
|
|
||
| def __init__(self): # pragma: no cover - trivial | ||
| # Intentionally bypass BaseHandler.__init__ — we only need the type. | ||
| pass | ||
|
|
||
|
|
||
| def _build_controller_with_storage(handler): | ||
| """Wire up just enough of the controller to reach the storage validation.""" | ||
| project = SimpleNamespace(id=1, name="mindsdb") | ||
| database_controller = MagicMock() | ||
| database_controller.get_project.return_value = project | ||
|
|
||
| data_node = SimpleNamespace(integration_handler=handler) | ||
| datahub = MagicMock() | ||
| datahub.get.return_value = data_node | ||
|
|
||
| integration_controller = MagicMock() | ||
| integration_controller.get.return_value = {"id": 42} | ||
|
|
||
| session = SimpleNamespace( | ||
| database_controller=database_controller, | ||
| datahub=datahub, | ||
| integration_controller=integration_controller, | ||
| model_controller=MagicMock(), | ||
| ) | ||
| return KnowledgeBaseController(session) | ||
|
|
||
|
|
||
| def _embedding_params(): | ||
| return { | ||
| "embedding_model": { | ||
| "provider": "openai", | ||
| "model_name": "text-embedding-3-small", | ||
| "api_key": "sk-test", | ||
| } | ||
| } | ||
|
|
||
|
|
||
| def test_non_vector_storage_raises_clear_error_before_create_table(): | ||
| """A non-vector handler must be rejected with an actionable message.""" | ||
| controller = _build_controller_with_storage(_NotAVectorStore()) | ||
|
|
||
| with ( | ||
| patch.object( | ||
| KnowledgeBaseController, | ||
| "_check_embedding_model", | ||
| return_value={"dimension": 1536}, | ||
| ), | ||
| patch.object(KnowledgeBaseController, "get", return_value=None), | ||
| ): | ||
| with pytest.raises(ValueError) as exc_info: | ||
| controller.add( | ||
| name="kb_bad_storage", | ||
| project_name="mindsdb", | ||
| storage=Identifier(parts=["files", "test_knowledge1"]), | ||
| params=_embedding_params(), | ||
| ) | ||
|
|
||
| message = str(exc_info.value) | ||
| assert "files" in message | ||
| assert "vector database" in message | ||
| assert "pgvector" in message # message must point user at valid alternatives | ||
| # The original AttributeError leak must not be what the user sees. | ||
| assert "create_table" not in message | ||
| assert "_NotAVectorStore" in message | ||
|
|
||
|
|
||
| def test_vector_storage_passes_validation(): | ||
| """A real VectorStoreHandler subclass is accepted (proves the guard | ||
| is not over-eager).""" | ||
| handler = _FakeVectorStore() | ||
| handler.create_table = MagicMock() | ||
| handler.drop_table = MagicMock() | ||
| handler.add_full_text_index = MagicMock() | ||
| controller = _build_controller_with_storage(handler) | ||
|
|
||
| with ( | ||
| patch.object( | ||
| KnowledgeBaseController, | ||
| "_check_embedding_model", | ||
| return_value={"dimension": 1536}, | ||
| ), | ||
| patch.object(KnowledgeBaseController, "_check_vector_table", return_value=None), | ||
| patch.object(KnowledgeBaseController, "get", return_value=None), | ||
| patch("mindsdb.interfaces.knowledge_base.controller.db.session"), | ||
| ): | ||
| # We don't care about the return value here; we only care that the | ||
| # storage-type guard does not reject a real vector handler. Any | ||
| # downstream failure should NOT be the ValueError we are protecting | ||
| # against in the negative test above. | ||
| try: | ||
| controller.add( | ||
| name="kb_good_storage", | ||
| project_name="mindsdb", | ||
| storage=Identifier(parts=["pgvector_db", "kb_table"]), | ||
| params=_embedding_params(), | ||
| ) | ||
| except ValueError as e: | ||
| assert "is not a vector database" not in str(e) | ||
|
|
||
| handler.create_table.assert_called_once_with("kb_table") |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correctness: The
str | Noneunion type syntax requires Python 3.10+; the rest of the file importsListfromtyping(Python 3.8/3.9 style), so this annotation will raise aTypeErrorat class-definition time on any runtime below 3.10. UseOptional[str]fromtypinginstead (or addfrom __future__ import annotationsto defer annotation evaluation).🤖 AI Agent Prompt for Cursor/Windsurf
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PEP 604 concern is a false positive:
setup.pypinspython_requires=">=3.10,<3.14", wherestr | Noneis native. The same syntax is already used throughout the repo, including the files this PR touches — e.g.controller.py:52 (id_column: str | None = None)anddefault_storage_resolver.py:7 (def _normalize_engine_name(engine: str | None) -> str | None). The annotation is consistent with existing convention, not a regression.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@chon3806 Got it, I'll keep that in mind for future reviews. Since the repo targets Python 3.10+ and already uses PEP 604 syntax consistently, I won't flag
str | Nonestyle annotations as issues.✍️ Learning notes added: