Initial commit: library-desk service extraction from portainer-core
Build and Push / build (release) Successful in 36s
Build and Push / build (release) Successful in 36s
This commit is contained in:
@@ -0,0 +1,384 @@
|
||||
"""
|
||||
Dependency injection for Library Desk.
|
||||
|
||||
Provides FastAPI dependencies for service clients with:
|
||||
- Singleton pattern via @lru_cache
|
||||
- Lazy initialization
|
||||
- Proper lifecycle management
|
||||
- Type aliases for clean endpoint signatures
|
||||
"""
|
||||
|
||||
from functools import lru_cache
|
||||
from typing import Annotated
|
||||
from fastapi import Depends
|
||||
import logging
|
||||
|
||||
from src.config import Settings, get_settings
|
||||
from src.clients.neo4j_client import Neo4jClient
|
||||
from src.clients.qdrant_client import QdrantClientWrapper
|
||||
from src.clients.wikijs_client import WikiJSClient
|
||||
from src.clients.searxng_client import SearXNGClient
|
||||
from src.clients.ollama_client import OllamaClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Settings dependency
|
||||
SettingsDep = Annotated[Settings, Depends(get_settings)]
|
||||
|
||||
|
||||
# Client factory functions with @lru_cache for singletons
|
||||
@lru_cache
|
||||
def get_neo4j_client() -> Neo4jClient:
|
||||
"""
|
||||
Get Neo4j client singleton.
|
||||
|
||||
Returns:
|
||||
Initialized Neo4j client (not yet connected)
|
||||
|
||||
Note: Call client.connect() during app startup
|
||||
"""
|
||||
settings = get_settings()
|
||||
client = Neo4jClient(
|
||||
uri=settings.neo4j_uri,
|
||||
user=settings.neo4j_user,
|
||||
password=settings.neo4j_password
|
||||
)
|
||||
logger.debug("Created Neo4j client instance")
|
||||
return client
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_qdrant_client() -> QdrantClientWrapper:
|
||||
"""
|
||||
Get Qdrant client singleton.
|
||||
|
||||
Returns:
|
||||
Initialized Qdrant client
|
||||
|
||||
Note: Collections are created lazily per-user
|
||||
"""
|
||||
settings = get_settings()
|
||||
client = QdrantClientWrapper(
|
||||
url=settings.qdrant_url,
|
||||
embedding_dim=768 # nomic-embed-text default
|
||||
)
|
||||
logger.debug("Created Qdrant client instance")
|
||||
return client
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_wikijs_client() -> WikiJSClient:
|
||||
"""
|
||||
Get Wiki.js client singleton.
|
||||
|
||||
Returns:
|
||||
Initialized Wiki.js GraphQL client with username/password auth
|
||||
"""
|
||||
settings = get_settings()
|
||||
client = WikiJSClient(
|
||||
base_url=settings.wikijs_url,
|
||||
username=settings.wikijs_username,
|
||||
password=settings.wikijs_password
|
||||
)
|
||||
logger.debug("Created Wiki.js client instance")
|
||||
return client
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_searxng_client() -> SearXNGClient:
|
||||
"""
|
||||
Get SearXNG client singleton.
|
||||
|
||||
Returns:
|
||||
Initialized SearXNG search client
|
||||
"""
|
||||
settings = get_settings()
|
||||
client = SearXNGClient(base_url=settings.searxng_url)
|
||||
logger.debug("Created SearXNG client instance")
|
||||
return client
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_ollama_client() -> OllamaClient:
|
||||
"""
|
||||
Get Ollama client singleton.
|
||||
|
||||
Returns:
|
||||
Initialized Ollama embeddings client
|
||||
"""
|
||||
settings = get_settings()
|
||||
client = OllamaClient(
|
||||
base_url=settings.ollama_url,
|
||||
model=settings.ollama_model
|
||||
)
|
||||
logger.debug("Created Ollama client instance")
|
||||
return client
|
||||
|
||||
|
||||
# Type aliases for FastAPI endpoint dependencies
|
||||
# Usage: def my_endpoint(neo4j: Neo4jDep):
|
||||
Neo4jDep = Annotated[Neo4jClient, Depends(get_neo4j_client)]
|
||||
QdrantDep = Annotated[QdrantClientWrapper, Depends(get_qdrant_client)]
|
||||
WikiJSDep = Annotated[WikiJSClient, Depends(get_wikijs_client)]
|
||||
SearXNGDep = Annotated[SearXNGClient, Depends(get_searxng_client)]
|
||||
OllamaDep = Annotated[OllamaClient, Depends(get_ollama_client)]
|
||||
|
||||
|
||||
# Lifecycle management functions
|
||||
async def startup_clients():
|
||||
"""
|
||||
Initialize all service clients at application startup.
|
||||
|
||||
Should be called in FastAPI lifespan or startup event.
|
||||
|
||||
Performs:
|
||||
- Neo4j connection pool initialization
|
||||
- Neo4j connectivity verification
|
||||
- Ollama model availability check
|
||||
"""
|
||||
logger.info("Starting up service clients...")
|
||||
|
||||
# Initialize Neo4j connection pool
|
||||
neo4j = get_neo4j_client()
|
||||
try:
|
||||
await neo4j.connect()
|
||||
logger.info("✓ Neo4j connected")
|
||||
except Exception as e:
|
||||
logger.error(f"✗ Neo4j connection failed: {e}")
|
||||
# Don't fail startup - allow degraded operation
|
||||
pass
|
||||
|
||||
# Check Ollama availability
|
||||
ollama = get_ollama_client()
|
||||
try:
|
||||
is_healthy = await ollama.health_check()
|
||||
if is_healthy:
|
||||
logger.info(f"✓ Ollama ready (model: {ollama.model})")
|
||||
else:
|
||||
logger.warning(f"✗ Ollama model '{ollama.model}' not available")
|
||||
except Exception as e:
|
||||
logger.error(f"✗ Ollama health check failed: {e}")
|
||||
pass
|
||||
|
||||
# Qdrant, Wiki.js, SearXNG are lazy-initialized
|
||||
logger.info("Service clients startup complete")
|
||||
|
||||
|
||||
async def shutdown_clients():
|
||||
"""
|
||||
Cleanup all service clients at application shutdown.
|
||||
|
||||
Should be called in FastAPI lifespan or shutdown event.
|
||||
|
||||
Performs:
|
||||
- Close Neo4j connection pool
|
||||
- Close HTTP clients
|
||||
"""
|
||||
logger.info("Shutting down service clients...")
|
||||
|
||||
# Close Neo4j driver
|
||||
neo4j = get_neo4j_client()
|
||||
try:
|
||||
await neo4j.close()
|
||||
logger.info("✓ Neo4j closed")
|
||||
except Exception as e:
|
||||
logger.error(f"Error closing Neo4j: {e}")
|
||||
|
||||
# Close HTTP clients
|
||||
clients_to_close = [
|
||||
("Wiki.js", get_wikijs_client()),
|
||||
("SearXNG", get_searxng_client()),
|
||||
("Ollama", get_ollama_client())
|
||||
]
|
||||
|
||||
for name, client in clients_to_close:
|
||||
try:
|
||||
await client.close()
|
||||
logger.info(f"✓ {name} client closed")
|
||||
except Exception as e:
|
||||
logger.error(f"Error closing {name} client: {e}")
|
||||
|
||||
logger.info("Service clients shutdown complete")
|
||||
|
||||
|
||||
async def check_service_health() -> dict:
|
||||
"""
|
||||
Check health of all service clients.
|
||||
|
||||
Returns:
|
||||
Dictionary with health status of each service:
|
||||
{
|
||||
"neo4j": bool,
|
||||
"qdrant": bool,
|
||||
"wikijs": bool,
|
||||
"searxng": bool,
|
||||
"ollama": bool
|
||||
}
|
||||
|
||||
Usage:
|
||||
>>> health = await check_service_health()
|
||||
>>> health["neo4j"]
|
||||
True
|
||||
"""
|
||||
health = {}
|
||||
|
||||
# Neo4j
|
||||
try:
|
||||
neo4j = get_neo4j_client()
|
||||
# Simple query to check connectivity
|
||||
await neo4j.execute_query("RETURN 1 as test", {})
|
||||
health["neo4j"] = True
|
||||
except Exception as e:
|
||||
logger.error(f"Neo4j health check failed: {e}")
|
||||
health["neo4j"] = False
|
||||
|
||||
# Qdrant
|
||||
try:
|
||||
qdrant = get_qdrant_client()
|
||||
# Check if we can list collections
|
||||
collections = qdrant.client.get_collections()
|
||||
health["qdrant"] = True
|
||||
except Exception as e:
|
||||
logger.error(f"Qdrant health check failed: {e}")
|
||||
health["qdrant"] = False
|
||||
|
||||
# Wiki.js
|
||||
try:
|
||||
wikijs = get_wikijs_client()
|
||||
# Try a simple query (list pages with limit 1)
|
||||
await wikijs.list_pages(limit=1)
|
||||
health["wikijs"] = True
|
||||
except Exception as e:
|
||||
logger.error(f"Wiki.js health check failed: {e}")
|
||||
health["wikijs"] = False
|
||||
|
||||
# SearXNG
|
||||
try:
|
||||
searxng = get_searxng_client()
|
||||
# Just check if service is up (no actual search)
|
||||
health["searxng"] = await searxng.health_check()
|
||||
except Exception as e:
|
||||
logger.error(f"SearXNG health check failed: {e}")
|
||||
health["searxng"] = False
|
||||
|
||||
# Ollama
|
||||
try:
|
||||
ollama = get_ollama_client()
|
||||
is_healthy = await ollama.health_check()
|
||||
health["ollama"] = is_healthy
|
||||
except Exception as e:
|
||||
logger.error(f"Ollama health check failed: {e}")
|
||||
health["ollama"] = False
|
||||
|
||||
return health
|
||||
|
||||
|
||||
# Service factory functions
|
||||
@lru_cache
|
||||
def get_vector_service() -> "VectorService":
|
||||
"""Get VectorService singleton."""
|
||||
from src.services.vector_service import VectorService
|
||||
return VectorService(
|
||||
qdrant_client=get_qdrant_client(),
|
||||
wikijs_client=get_wikijs_client(),
|
||||
ollama_client=get_ollama_client()
|
||||
)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_graph_service() -> "GraphService":
|
||||
"""Get GraphService singleton."""
|
||||
from src.services.graph_service import GraphService
|
||||
return GraphService(
|
||||
neo4j_client=get_neo4j_client(),
|
||||
wikijs_client=get_wikijs_client()
|
||||
)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_wiki_service() -> "WikiService":
|
||||
"""Get WikiService singleton."""
|
||||
from src.services.wiki_service import WikiService
|
||||
return WikiService(wiki_client=get_wikijs_client())
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_consolidation_service() -> "ConsolidationService":
|
||||
"""Get ConsolidationService singleton."""
|
||||
from src.services.consolidation_service import ConsolidationService
|
||||
return ConsolidationService(
|
||||
neo4j=get_neo4j_client(),
|
||||
ollama=get_ollama_client(),
|
||||
wiki=get_wikijs_client(),
|
||||
settings=get_settings(),
|
||||
ingestion_service=get_ingestion_service()
|
||||
)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_ingestion_service() -> "IngestionService":
|
||||
"""Get IngestionService singleton."""
|
||||
from src.services.ingestion_service import IngestionService
|
||||
return IngestionService(
|
||||
vector_service=get_vector_service(),
|
||||
graph_service=get_graph_service(),
|
||||
wiki_client=get_wikijs_client()
|
||||
)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_hybrid_rag_service() -> "HybridRAGService":
|
||||
"""Get HybridRAGService singleton."""
|
||||
from src.services.hybrid_rag_service import HybridRAGService
|
||||
return HybridRAGService(
|
||||
vector_service=get_vector_service(),
|
||||
graph_service=get_graph_service(),
|
||||
searxng_client=get_searxng_client(),
|
||||
ollama_client=get_ollama_client(),
|
||||
settings=get_settings()
|
||||
)
|
||||
|
||||
|
||||
# Utility: Get default user from settings or multi_tenancy
|
||||
def get_default_user() -> str:
|
||||
"""
|
||||
Get default user for operations.
|
||||
|
||||
Returns:
|
||||
Default user identifier
|
||||
"""
|
||||
from src.core.multi_tenancy import DEFAULT_USER
|
||||
return DEFAULT_USER
|
||||
|
||||
|
||||
# Authentication
|
||||
from fastapi import Security, HTTPException
|
||||
from fastapi.security import HTTPBearer
|
||||
|
||||
security = HTTPBearer()
|
||||
|
||||
|
||||
async def verify_api_key(
|
||||
credentials: Annotated[HTTPBearer, Security(security)],
|
||||
settings: SettingsDep
|
||||
) -> str:
|
||||
"""
|
||||
Verify API key from Bearer token.
|
||||
|
||||
Args:
|
||||
credentials: HTTP Bearer credentials
|
||||
settings: Application settings
|
||||
|
||||
Returns:
|
||||
API key if valid
|
||||
|
||||
Raises:
|
||||
HTTPException: If API key is invalid
|
||||
"""
|
||||
if credentials.credentials != settings.library_api_key:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Invalid API key"
|
||||
)
|
||||
return credentials.credentials
|
||||
Reference in New Issue
Block a user