Coverage for /home/admin/Documents/AI/applications/lexigram-dev/lexigram/experimental/ai/lexigram-ai-session/src/lexigram/ai/session/di/provider.py: 56%
71 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 07:19 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 07:19 +0800
1"""DI provider for lexigram-ai-session."""
3from __future__ import annotations
5from typing import TYPE_CHECKING
7from lexigram.ai.session.config import SessionConfig
8from lexigram.ai.session.context.pruner import RelevanceContextPruner
9from lexigram.ai.session.context.session_context import SessionContext
10from lexigram.ai.session.manager import SessionCleanupScheduler, SessionManagerImpl
11from lexigram.ai.session.stores.in_memory import InMemorySessionStore
12from lexigram.contracts.ai.session import (
13 ContextPrunerProtocol,
14 SessionContextProtocol,
15 SessionManagerProtocol,
16 SessionStoreProtocol,
17)
18from lexigram.contracts.core.health import HealthCheckResult, HealthStatus
19from lexigram.contracts.core.provider import ProviderPriority
20from lexigram.di.provider import Provider
21from lexigram.logging import (
22 get_logger,
23)
25if TYPE_CHECKING:
26 from lexigram.contracts.core.di import (
27 BootContainerProtocol,
28 ContainerRegistrarProtocol,
29 )
31logger = get_logger(__name__)
34class SessionProvider(Provider):
35 """Registers session management services in the DI container.
37 Wires ``SessionStoreProtocol`` and ``SessionManagerProtocol`` based on the
38 configured backend. Background cleanup is started during ``boot()``.
39 """
41 name = "session"
42 priority = ProviderPriority.INFRASTRUCTURE
43 config_key: str | None = "ai_session"
44 config_model: type | None = SessionConfig
46 def __init__(
47 self,
48 config: SessionConfig | dict | None = None,
49 *,
50 enable_cleanup_scheduler: bool = True,
51 ) -> None:
52 """Initialise the provider."""
53 super().__init__()
54 if isinstance(config, dict):
55 config = SessionConfig(**config)
56 self._requested_config = config
57 self._config = config or SessionConfig()
58 self._enable_cleanup_scheduler = enable_cleanup_scheduler
59 self._scheduler: SessionCleanupScheduler | None = None
61 async def register(self, container: ContainerRegistrarProtocol) -> None:
62 """Register session services in the container."""
63 self._config = self._requested_config or (
64 self.config
65 if isinstance(getattr(self, "config", None), SessionConfig)
66 else self._config
67 )
68 container.singleton(SessionConfig, instance=self._config)
70 if not self._config.enabled:
71 logger.info("session_disabled", reason="SessionConfig.enabled=False")
72 return
74 if self._config.backend == "in_memory":
75 container.singleton(SessionStoreProtocol, instance=InMemorySessionStore())
76 elif self._config.backend == "cache":
77 from lexigram.ai.session.stores.cache import CacheSessionStore
79 container.singleton(SessionStoreProtocol, factory=CacheSessionStore)
80 elif self._config.backend == "database":
81 from lexigram.ai.session.stores.database import DatabaseSessionStore
83 container.singleton(SessionStoreProtocol, factory=DatabaseSessionStore)
84 else:
85 logger.warning(
86 "unknown_session_backend",
87 backend=self._config.backend,
88 fallback="in_memory",
89 )
90 container.singleton(SessionStoreProtocol, instance=InMemorySessionStore())
92 container.singleton(SessionManagerProtocol, factory=SessionManagerImpl)
93 container.singleton(SessionCleanupScheduler, factory=SessionCleanupScheduler)
94 container.transient(SessionContextProtocol, factory=SessionContext) # type: ignore[type-abstract]
95 container.singleton(ContextPrunerProtocol, factory=RelevanceContextPruner)
97 logger.info("session_services_registered", backend=self._config.backend)
99 async def boot(self, container: BootContainerProtocol) -> None:
100 """Pre-warm session services and start the cleanup scheduler."""
101 if not self._config.enabled:
102 return
104 store = await container.resolve(SessionStoreProtocol)
105 initialize = getattr(store, "initialize", None)
106 if callable(initialize):
107 await initialize()
109 if self._enable_cleanup_scheduler:
110 try:
111 scheduler = await container.resolve(SessionCleanupScheduler)
112 await scheduler.start()
113 self._scheduler = scheduler
114 except (LookupError, RuntimeError, AttributeError, OSError) as exc:
115 logger.warning("failed_to_start_session_cleanup", error=str(exc))
117 async def shutdown(self) -> None:
118 """Stop the background cleanup loop."""
119 if self._scheduler is None:
120 return
122 try:
123 await self._scheduler.stop()
124 except (RuntimeError, AttributeError, OSError) as exc:
125 logger.warning("failed_to_stop_session_cleanup", error=str(exc))
127 async def health_check(self, timeout: float = 5.0) -> HealthCheckResult:
128 """Health check — always healthy (in-process domain provider).
130 No external backend to ping.
132 Args:
133 timeout: Ignored for in-process providers.
135 Returns:
136 Always HEALTHY — no external backend to ping.
137 """
138 return HealthCheckResult(
139 component=self.name,
140 status=HealthStatus.HEALTHY,
141 details={"status": "operational"},
142 )
145__all__ = ["SessionProvider"]