Coverage for /home/admin/Documents/AI/applications/lexigram-dev/lexigram/experimental/ai/lexigram-ai-feedback/src/lexigram/ai/feedback/di/provider.py: 53%

70 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-25 07:19 +0800

1"""Feedback DI provider.""" 

2 

3from __future__ import annotations 

4 

5import inspect 

6from typing import TYPE_CHECKING 

7 

8from lexigram.ai.feedback.config import FeedbackConfig 

9from lexigram.ai.feedback.processors.processor_registry import FeedbackProcessorRegistry 

10from lexigram.ai.feedback.services.collector import FeedbackCollector 

11from lexigram.ai.feedback.services.feedback_service import FeedbackService 

12from lexigram.contracts.ai.feedback import FeedbackProtocol 

13from lexigram.contracts.core.health import HealthCheckResult, HealthStatus 

14from lexigram.contracts.core.provider import ProviderPriority 

15from lexigram.di.provider import Provider 

16from lexigram.logging import ( 

17 get_logger, 

18) 

19 

20if TYPE_CHECKING: 

21 from lexigram.ai.feedback.storage.protocols import FeedbackStoreProtocol 

22 from lexigram.contracts.core.di import ( 

23 ContainerRegistrarProtocol, 

24 ContainerResolverProtocol, 

25 ) 

26 

27logger = get_logger(__name__) 

28 

29 

30class FeedbackProvider(Provider): 

31 """Provider for AI Feedback. 

32 

33 Registers :class:`~lexigram.ai.feedback.collector.FeedbackCollector` and 

34 :class:`~lexigram.ai.feedback.processor_registry.FeedbackProcessorRegistry`. 

35 Wires a durable storage backend (DB-backed, optionally cached) into the 

36 collector during :meth:`boot`. 

37 """ 

38 

39 name = "feedback" 

40 priority = ProviderPriority.DOMAIN 

41 config_key: str | None = "ai_feedback" 

42 config_model: type | None = FeedbackConfig 

43 

44 def __init__(self, config: FeedbackConfig | dict | None = None) -> None: 

45 super().__init__() 

46 if isinstance(config, dict): 

47 config = FeedbackConfig(**config) 

48 self._requested_config = config 

49 self._config = config or FeedbackConfig() 

50 

51 @classmethod 

52 def from_config(cls, config: FeedbackConfig, **context: object) -> FeedbackProvider: 

53 """Factory method for DI container setup.""" 

54 del context 

55 return cls(config) 

56 

57 async def register(self, container: ContainerRegistrarProtocol) -> None: 

58 """Register the feedback services.""" 

59 self._config = self._requested_config or ( 

60 self.config 

61 if isinstance(getattr(self, "config", None), FeedbackConfig) 

62 else self._config 

63 ) 

64 container.singleton(FeedbackConfig, self._config) 

65 

66 if not self._config.enabled: 

67 logger.info("feedback_disabled", reason="FeedbackConfig.enabled=False") 

68 return 

69 

70 container.singleton(FeedbackProcessorRegistry) 

71 container.singleton(FeedbackCollector) 

72 container.singleton(FeedbackProtocol, FeedbackService) 

73 

74 logger.info("feedback_registered") 

75 

76 async def boot(self, container: ContainerResolverProtocol) -> None: 

77 """Wire a durable storage backend into :class:`FeedbackCollector`.""" 

78 if not self._config.enabled: 

79 return 

80 

81 from lexigram.ai.feedback.storage.cache import CachedFeedbackStore 

82 from lexigram.ai.feedback.storage.database import DatabaseFeedbackStore 

83 from lexigram.contracts.data import DatabaseProviderProtocol 

84 from lexigram.contracts.infra.cache import CacheBackendProtocol 

85 

86 db_provider = await container.resolve_optional(DatabaseProviderProtocol) 

87 if db_provider is None: 

88 logger.debug( 

89 "feedback_store_not_wired", 

90 reason="DatabaseProviderProtocol unavailable", 

91 ) 

92 return 

93 

94 store: FeedbackStoreProtocol = DatabaseFeedbackStore(db_provider) # type: ignore[assignment] 

95 

96 cache_backend = await container.resolve_optional(CacheBackendProtocol) 

97 if cache_backend is not None: 

98 store = CachedFeedbackStore(store, cache_backend) 

99 logger.info("feedback_cached_store_wired") 

100 else: 

101 logger.info("feedback_database_store_wired") 

102 

103 collector_resolved = container.resolve(FeedbackCollector) 

104 if inspect.isawaitable(collector_resolved): 

105 collector = await collector_resolved 

106 else: 

107 collector = collector_resolved 

108 collector.storage = store 

109 

110 service_resolved = container.resolve(FeedbackProtocol) 

111 if inspect.isawaitable(service_resolved): 

112 service = await service_resolved 

113 else: 

114 service = service_resolved 

115 service._store = store 

116 

117 async def shutdown(self) -> None: 

118 """Shutdown phase.""" 

119 logger.debug("feedback_shutdown") 

120 

121 async def health_check(self, timeout: float = 5.0) -> HealthCheckResult: 

122 """Health check — always healthy (in-process domain provider). 

123 

124 No external backend to ping. 

125 

126 Args: 

127 timeout: Ignored for in-process providers. 

128 

129 Returns: 

130 Always HEALTHY — no external backend to ping. 

131 """ 

132 return HealthCheckResult( 

133 component=self.name, 

134 status=HealthStatus.HEALTHY, 

135 details={"status": "operational"}, 

136 ) 

137 

138 

139__all__ = ["FeedbackProvider"]