diff --git a/.env.example b/.env.example index 77475a6..0533e54 100644 --- a/.env.example +++ b/.env.example @@ -9,7 +9,7 @@ SEARXNG_URL=http://192.168.86.149:8080 REDIS_HOST=192.168.86.149 PAPERLESS_URL=http://192.168.86.149:8091 -OLLAMA_MODEL=mistral-nemo-large:latest +OLLAMA_LLM_MODEL=gemma4:e2b OLLAMA_EMBEDDING_MODEL=nomic-embed-text # Wiki.js auth diff --git a/CHANGELOG.md b/CHANGELOG.md index f384c98..b4b4a4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to Library Desk will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed + +- **Ollama generation model env collision** - Renamed the generation-model setting `ollama_model` to `ollama_llm_model` (env: `OLLAMA_LLM_MODEL`, default `gemma4:e2b`). The container env `OLLAMA_MODEL=nomic-embed-text` (meant for embeddings) was shadowing the generation model, breaking Phase 0 keyword extraction and Phase 4 LLM re-ranking on every request. Startup now logs the resolved generation model. +- **LLM call timeouts** - Phase 0 keyword extraction and Phase 4 re-ranking are wrapped in a 12s `asyncio.wait_for` with graceful fallback, so a hung Ollama call can no longer gate retrieval for the full 120s client timeout. + ## [1.7.3] - 2026-01-07 ### Fixed diff --git a/src/config.py b/src/config.py index 4eac3c5..e629bcb 100644 --- a/src/config.py +++ b/src/config.py @@ -66,7 +66,9 @@ class Settings(BaseSettings): # Ollama Configuration ollama_url: str = Field(default="http://ollama:11434", description="Ollama URL") - ollama_model: str = Field(default="mistral-nemo-large:latest", description="Ollama LLM model") + # Named ollama_llm_model (env: OLLAMA_LLM_MODEL) to avoid collision with the + # OLLAMA_MODEL container env var, which is used for the embedding model. + ollama_llm_model: str = Field(default="gemma4:e2b", description="Ollama LLM model for generation (keyword extraction, re-ranking, consolidation)") ollama_embedding_model: str = Field(default="nomic-embed-text", description="Ollama embedding model") # HybridRAG Configuration diff --git a/src/main.py b/src/main.py index 34f1b3a..6996f38 100644 --- a/src/main.py +++ b/src/main.py @@ -142,7 +142,7 @@ async def health(settings: Settings = Depends(get_settings)) -> HealthResponse: }, "ollama": { "url": settings.ollama_url, - "model": settings.ollama_model, + "model": settings.ollama_llm_model, "healthy": service_health.get("ollama", False) } } @@ -412,6 +412,7 @@ async def startup_event(): logger.info(f"Wiki.js: {settings.wikijs_url}") logger.info(f"SearXNG: {settings.searxng_url}") logger.info(f"Ollama: {settings.ollama_url}") + logger.info(f"Ollama generation model: {settings.ollama_llm_model} (embedding model: {settings.ollama_embedding_model})") # Initialize all service clients await startup_clients() diff --git a/src/services/consolidation_service.py b/src/services/consolidation_service.py index 7811ff2..c01235d 100644 --- a/src/services/consolidation_service.py +++ b/src/services/consolidation_service.py @@ -505,7 +505,7 @@ JSON:""" # Call Ollama for analysis (temperature=0.0 for consistent classification) response = await self.ollama.generate_text( prompt=prompt, - model=self.settings.ollama_model, + model=self.settings.ollama_llm_model, stream=False, temperature=0.0 ) @@ -1079,7 +1079,7 @@ JSON:""" try: response = await self.ollama.generate_text( prompt=prompt, - model=self.settings.ollama_model, + model=self.settings.ollama_llm_model, stream=False, temperature=0.0 ) diff --git a/src/services/hybrid_rag_service.py b/src/services/hybrid_rag_service.py index acfc94d..074d4fb 100644 --- a/src/services/hybrid_rag_service.py +++ b/src/services/hybrid_rag_service.py @@ -34,6 +34,10 @@ from src.core.multi_tenancy import get_neo4j_user_base_label, get_neo4j_user_lab logger = logging.getLogger(__name__) +# Timeout for auxiliary LLM calls (keyword extraction, re-ranking). +# A hung Ollama call must not gate retrieval for the full client timeout. +LLM_CALL_TIMEOUT_SECONDS = 12.0 + class HybridRAGService: """ @@ -69,7 +73,7 @@ class HybridRAGService: self.content_extractor = content_extractor self.settings = settings self.volatile = volatile_service - self.reranker_model = settings.ollama_model + self.reranker_model = settings.ollama_llm_model async def search( self, @@ -224,10 +228,13 @@ Return format: JSON:""" try: - response = await self.ollama.generate_text( - prompt=prompt, - model=self.reranker_model, - temperature=0.0 # Deterministic for consistent extraction + response = await asyncio.wait_for( + self.ollama.generate_text( + prompt=prompt, + model=self.reranker_model, + temperature=0.0 # Deterministic for consistent extraction + ), + timeout=LLM_CALL_TIMEOUT_SECONDS ) # Parse JSON response (handle potential extra text) @@ -261,6 +268,16 @@ JSON:""" "synonyms": {}, "expansions": {} } + except asyncio.TimeoutError: + logger.warning( + f"Keyword extraction timed out after {LLM_CALL_TIMEOUT_SECONDS}s, using fallback" + ) + return { + "core_keywords": query.split(), + "entities": [], + "synonyms": {}, + "expansions": {} + } except Exception as e: logger.error(f"Keyword extraction failed: {e}", exc_info=True) return { @@ -772,10 +789,13 @@ Example output: 3,1,5,2,4 Ranking:""" - response = await self.ollama.generate_text( - prompt=prompt, - model=self.reranker_model, - temperature=0.0 # Deterministic for consistent rankings + response = await asyncio.wait_for( + self.ollama.generate_text( + prompt=prompt, + model=self.reranker_model, + temperature=0.0 # Deterministic for consistent rankings + ), + timeout=LLM_CALL_TIMEOUT_SECONDS ) # Parse response: "3,1,5,2,4" → [2, 0, 4, 1, 3] (0-indexed) @@ -796,6 +816,11 @@ Ranking:""" logger.info(f"LLM re-ranking: reordered {len(reranked)} results") return reranked + except asyncio.TimeoutError: + logger.warning( + f"LLM re-ranking timed out after {LLM_CALL_TIMEOUT_SECONDS}s, using RRF order" + ) + return results # Fallback to RRF order except Exception as e: logger.warning(f"LLM re-ranking failed: {e}, using RRF order") return results # Fallback to RRF order diff --git a/src/services/wiki_page_writer.py b/src/services/wiki_page_writer.py index e402a4c..102475c 100644 --- a/src/services/wiki_page_writer.py +++ b/src/services/wiki_page_writer.py @@ -34,7 +34,7 @@ class WikiPageWriter: settings: Application settings """ self.ollama = ollama_client - self.model = settings.ollama_model + self.model = settings.ollama_llm_model async def create_page( self, diff --git a/tests/test_consolidation.py b/tests/test_consolidation.py index dd06f2d..49970de 100644 --- a/tests/test_consolidation.py +++ b/tests/test_consolidation.py @@ -38,7 +38,7 @@ def settings(): """Get mocked application settings for testing.""" mock_settings = MagicMock() mock_settings.reranker_model = "mistral-nemo" - mock_settings.ollama_model = "mistral-nemo" + mock_settings.ollama_llm_model = "mistral-nemo" return mock_settings