Coverage for /home/admin/Documents/AI/applications/lexigram-dev/lexigram/experimental/ai/lexigram-ai-prompt/src/lexigram/ai/prompt/di/provider.py: 75%
79 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"""Prompt DI provider — registers the PromptRegistry and PromptService with the container."""
3from __future__ import annotations
5from pathlib import Path
6from typing import TYPE_CHECKING, Any
8from lexigram.ai.prompt.assembly.assembler import CacheAwarePromptAssembler
9from lexigram.ai.prompt.assembly.cache_strategies import ProviderCacheStrategyRegistry
10from lexigram.ai.prompt.config import PromptConfig
11from lexigram.ai.prompt.registry.registry import PromptRegistry
12from lexigram.ai.prompt.rendering.sanitizer import InputSanitizer
13from lexigram.ai.prompt.service.loader import DictPromptLoader, DirectoryPromptLoader
14from lexigram.ai.prompt.service.models import PromptTemplate
15from lexigram.ai.prompt.service.observer import PromptObserverProtocol
16from lexigram.ai.prompt.service.service import PromptService, PromptServiceProtocol
17from lexigram.contracts.ai.llm import PromptAssemblerProtocol
18from lexigram.contracts.core.health import HealthCheckResult, HealthStatus
19from lexigram.contracts.core.hooks import HookRegistryProtocol
20from lexigram.contracts.core.provider import ProviderPriority
21from lexigram.contracts.exceptions.container import UnresolvableDependencyError
22from lexigram.contracts.exceptions.provider import ModuleVisibilityError
23from lexigram.di.provider import Provider
24from lexigram.logging import (
25 get_logger,
26)
28if TYPE_CHECKING:
29 from lexigram.contracts.core.di import (
30 ContainerRegistrarProtocol,
31 ContainerResolverProtocol,
32 )
34logger = get_logger(__name__)
37class PromptProvider(Provider):
38 """Provider for the prompt registry and prompt service.
40 Reads :class:`~lexigram.ai.prompt.config.PromptConfig`, builds a
41 :class:`~lexigram.ai.prompt.registry.registry.PromptRegistry` and a
42 :class:`~lexigram.ai.prompt.service.service.PromptService`, and registers
43 both as singletons.
45 The ``PromptService`` is configured via the keyword arguments:
47 * ``template_dir`` — path to a directory of YAML/JSON template files.
48 * ``inline_templates`` — list of raw template dicts (see
49 :class:`~lexigram.ai.prompt.service.loader.DictPromptLoader`).
50 * ``observer`` — an object implementing
51 :class:`~lexigram.ai.prompt.service.observer.PromptObserverProtocol`.
53 At least one of ``template_dir`` or ``inline_templates`` may be supplied;
54 both are combined if provided. When neither is supplied the
55 ``PromptService`` starts empty (useful in tests).
56 """
58 name = "prompt"
59 priority = ProviderPriority.DOMAIN
60 config_key: str | None = "ai_prompt"
61 config_model: type | None = PromptConfig
63 def __init__(
64 self,
65 config: PromptConfig | None = None,
66 *,
67 template_dir: str | Path | None = None,
68 inline_templates: list[dict[str, Any]] | None = None,
69 observer: PromptObserverProtocol | None = None,
70 ) -> None:
71 """Initialize with optional config and template sources.
73 Args:
74 config: Optional :class:`~lexigram.ai.prompt.config.PromptConfig`.
75 template_dir: Optional path to a directory of YAML/JSON files.
76 inline_templates: Optional list of raw template dicts.
77 observer: Optional observability hook for ``PromptService``.
78 """
79 super().__init__()
80 self._requested_config = config
81 self._config = config or PromptConfig()
82 self._template_dir = Path(template_dir) if template_dir else None
83 self._inline_templates: list[dict[str, Any]] = inline_templates or []
84 self._observer = observer
86 def _load_templates(self) -> list[PromptTemplate]:
87 """Load all templates from configured sources."""
88 templates: list[PromptTemplate] = []
90 if self._template_dir is not None:
91 templates.extend(
92 DirectoryPromptLoader(
93 self._template_dir, default_format=self._config.default_format
94 ).load()
95 )
97 if self._inline_templates:
98 templates.extend(
99 DictPromptLoader(
100 self._inline_templates, default_format=self._config.default_format
101 ).load()
102 )
104 return templates
106 async def register(self, container: ContainerRegistrarProtocol) -> None:
107 """Register the PromptConfig, PromptRegistry, PromptService, and CacheAwarePromptAssembler."""
108 self._config = self._requested_config or (
109 self.config
110 if isinstance(getattr(self, "config", None), PromptConfig)
111 else self._config
112 )
113 container.singleton(PromptConfig, self._config)
115 if not self._config.enabled:
116 logger.info("prompt_disabled", reason="PromptConfig.enabled=False")
117 return
119 registry = PromptRegistry()
120 container.singleton(PromptRegistry, registry)
122 # Register PromptService with loaded templates
123 templates = self._load_templates()
124 sanitizer: InputSanitizer | None = None
125 if self._config.sanitize_inputs:
126 sanitizer = InputSanitizer(strict=self._config.strict_sanitizer)
127 service = PromptService(
128 templates=templates,
129 observer=self._observer,
130 sanitizer=sanitizer,
131 )
132 container.singleton(PromptService, service)
133 container.singleton(PromptServiceProtocol, service)
135 # Register cache-aware assembler components
136 strategy_registry = ProviderCacheStrategyRegistry.with_defaults()
137 container.singleton(ProviderCacheStrategyRegistry, strategy_registry)
139 # Create assembler without token counter initially — will be enhanced in boot()
140 assembler = CacheAwarePromptAssembler(
141 strategy_registry=strategy_registry,
142 token_counter=None,
143 )
144 container.singleton(PromptAssemblerProtocol, assembler)
146 logger.info(
147 "prompt_provider_registered",
148 format=self._config.default_format,
149 sanitize_inputs=self._config.sanitize_inputs,
150 service_templates=len(templates),
151 )
153 async def boot(self, container: ContainerResolverProtocol) -> None:
154 """Boot phase — inject optional TokenCounterProtocol and hook registry."""
155 hook_registry = await container.resolve_optional(HookRegistryProtocol)
156 service = await container.resolve(PromptService)
157 service.attach_hook_registry(hook_registry)
159 try:
160 from lexigram.contracts.ai.llm import TokenCounterProtocol
162 token_counter = await container.resolve(TokenCounterProtocol)
163 assembler = await container.resolve(PromptAssemblerProtocol)
164 if isinstance(assembler, CacheAwarePromptAssembler):
165 assembler.set_token_counter(token_counter)
166 logger.debug("prompt_assembler_token_counter_injected")
167 else:
168 logger.warning(
169 "prompt_assembler_token_counter_skipped",
170 assembler_type=type(assembler).__name__,
171 )
172 except (UnresolvableDependencyError, ModuleVisibilityError):
173 logger.debug("prompt_assembler_token_counter_unavailable")
175 logger.debug("prompt_provider_booted")
177 async def shutdown(self) -> None:
178 """Shutdown phase — no cleanup required for prompt registry."""
179 logger.debug("prompt_provider_shutdown")
181 async def health_check(self, timeout: float = 5.0) -> HealthCheckResult:
182 """Health check — always healthy (in-process domain provider).
184 No external backend to ping.
186 Args:
187 timeout: Ignored for in-process providers.
189 Returns:
190 Always HEALTHY — no external backend to ping.
191 """
192 return HealthCheckResult(
193 component=self.name,
194 status=HealthStatus.HEALTHY,
195 details={"status": "operational"},
196 )