|
|
"""Service loader utility for safe, lazy loading of optional services. |
|
|
|
|
|
This module handles the import and initialization of services that may |
|
|
have missing optional dependencies (like Modal or Sentence Transformers), |
|
|
preventing the application from crashing if they are not available. |
|
|
|
|
|
Design Patterns: |
|
|
- Factory Method: get_embedding_service() creates appropriate service |
|
|
- Strategy Pattern: Selects between EmbeddingService and LlamaIndexRAGService |
|
|
""" |
|
|
|
|
|
import threading |
|
|
from typing import TYPE_CHECKING |
|
|
|
|
|
import structlog |
|
|
|
|
|
from src.utils.config import settings |
|
|
|
|
|
if TYPE_CHECKING: |
|
|
from src.services.embedding_protocol import EmbeddingServiceProtocol |
|
|
from src.services.statistical_analyzer import StatisticalAnalyzer |
|
|
|
|
|
logger = structlog.get_logger() |
|
|
|
|
|
|
|
|
def warmup_services() -> None: |
|
|
"""Pre-warm expensive services in a background thread. |
|
|
|
|
|
This reduces the "cold start" latency for the first user request by |
|
|
loading heavy models (like SentenceTransformer or LlamaIndex) into memory |
|
|
during application startup. |
|
|
""" |
|
|
|
|
|
def _warmup() -> None: |
|
|
logger.info("π₯ Warmup: Starting background service initialization...") |
|
|
try: |
|
|
|
|
|
get_embedding_service_if_available() |
|
|
logger.info("π₯ Warmup: Embedding service ready") |
|
|
except Exception as e: |
|
|
logger.warning("π₯ Warmup: Failed to warm up services", error=str(e)) |
|
|
|
|
|
|
|
|
thread = threading.Thread(target=_warmup, daemon=True) |
|
|
thread.start() |
|
|
|
|
|
|
|
|
def get_embedding_service() -> "EmbeddingServiceProtocol": |
|
|
"""Get the best available embedding service. |
|
|
|
|
|
Strategy selection (ordered by preference): |
|
|
1. LlamaIndexRAGService if OPENAI_API_KEY present (better quality + persistence) |
|
|
2. EmbeddingService (free, local, in-memory) as fallback |
|
|
|
|
|
Design Pattern: Factory Method + Strategy Pattern |
|
|
- Factory Method: Creates service instance |
|
|
- Strategy Pattern: Selects between implementations at runtime |
|
|
|
|
|
Returns: |
|
|
EmbeddingServiceProtocol: Either LlamaIndexRAGService or EmbeddingService |
|
|
|
|
|
Raises: |
|
|
ImportError: If no embedding service dependencies are available |
|
|
|
|
|
Example: |
|
|
```python |
|
|
service = get_embedding_service() |
|
|
await service.add_evidence("id", "content", {"source": "pubmed"}) |
|
|
results = await service.search_similar("query", n_results=5) |
|
|
unique = await service.deduplicate(evidence_list) |
|
|
``` |
|
|
""" |
|
|
|
|
|
if settings.has_openai_key: |
|
|
try: |
|
|
from src.services.llamaindex_rag import get_rag_service |
|
|
|
|
|
service = get_rag_service() |
|
|
logger.info( |
|
|
"Using LlamaIndex RAG service", |
|
|
tier="premium", |
|
|
persistence="enabled", |
|
|
embeddings="openai", |
|
|
) |
|
|
return service |
|
|
except ImportError as e: |
|
|
logger.info( |
|
|
"LlamaIndex deps not installed, falling back to local embeddings", |
|
|
missing=str(e), |
|
|
) |
|
|
except Exception as e: |
|
|
logger.warning( |
|
|
"LlamaIndex service failed to initialize, falling back", |
|
|
error=str(e), |
|
|
error_type=type(e).__name__, |
|
|
) |
|
|
|
|
|
|
|
|
try: |
|
|
from src.services.embeddings import get_embedding_service as get_local_service |
|
|
|
|
|
local_service = get_local_service() |
|
|
logger.info( |
|
|
"Using local embedding service", |
|
|
tier="free", |
|
|
persistence="disabled", |
|
|
embeddings="sentence-transformers", |
|
|
) |
|
|
return local_service |
|
|
except ImportError as e: |
|
|
logger.error( |
|
|
"No embedding service available", |
|
|
error=str(e), |
|
|
) |
|
|
raise ImportError( |
|
|
"No embedding service available. Install either:\n" |
|
|
" - uv sync --extra embeddings (for local embeddings)\n" |
|
|
" - uv sync --extra modal (for LlamaIndex with OpenAI)" |
|
|
) from e |
|
|
|
|
|
|
|
|
def get_embedding_service_if_available() -> "EmbeddingServiceProtocol | None": |
|
|
"""Safely attempt to load and initialize an embedding service. |
|
|
|
|
|
Unlike get_embedding_service(), this function returns None instead of |
|
|
raising ImportError when no service is available. |
|
|
|
|
|
Returns: |
|
|
EmbeddingServiceProtocol instance if dependencies are met, else None. |
|
|
""" |
|
|
try: |
|
|
return get_embedding_service() |
|
|
except ImportError as e: |
|
|
logger.info( |
|
|
"Embedding service not available (optional dependencies missing)", |
|
|
missing_dependency=str(e), |
|
|
) |
|
|
except Exception as e: |
|
|
logger.warning( |
|
|
"Embedding service initialization failed unexpectedly", |
|
|
error=str(e), |
|
|
error_type=type(e).__name__, |
|
|
) |
|
|
return None |
|
|
|
|
|
|
|
|
def get_analyzer_if_available() -> "StatisticalAnalyzer | None": |
|
|
"""Safely attempt to load and initialize the StatisticalAnalyzer. |
|
|
|
|
|
Returns: |
|
|
StatisticalAnalyzer instance if Modal is available, else None. |
|
|
""" |
|
|
try: |
|
|
from src.services.statistical_analyzer import get_statistical_analyzer |
|
|
|
|
|
analyzer = get_statistical_analyzer() |
|
|
logger.info("StatisticalAnalyzer initialized successfully") |
|
|
return analyzer |
|
|
except ImportError as e: |
|
|
logger.info( |
|
|
"StatisticalAnalyzer not available (Modal dependencies missing)", |
|
|
missing_dependency=str(e), |
|
|
) |
|
|
except Exception as e: |
|
|
logger.warning( |
|
|
"StatisticalAnalyzer initialization failed unexpectedly", |
|
|
error=str(e), |
|
|
error_type=type(e).__name__, |
|
|
) |
|
|
return None |
|
|
|