Initial commit: library-desk service extraction from portainer-core
Build and Push / build (release) Successful in 36s

This commit is contained in:
2025-12-11 17:28:23 +01:00
commit 95852190ba
59 changed files with 17146 additions and 0 deletions
+384
View File
@@ -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