Coverage for /home/admin/Documents/AI/applications/lexigram-dev/lexigram/experimental/ai/lexigram-ai-skills/src/lexigram/ai/skills/di/provider.py: 52%
95 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"""SkillsProvider — DI provider registering the skills subsystem."""
3from __future__ import annotations
5from pathlib import Path
6from typing import TYPE_CHECKING, Any, cast
8from lexigram.ai.skills.config import SkillsConfig
9from lexigram.contracts.core.health import HealthCheckResult, HealthStatus
10from lexigram.contracts.core.provider import ProviderPriority
11from lexigram.contracts.infra.cache.protocols import CacheBackendProtocol
12from lexigram.di.provider import Provider
13from lexigram.logging import (
14 get_logger,
15)
17if TYPE_CHECKING:
18 from lexigram.contracts.core.di import (
19 ContainerRegistrarProtocol,
20 ContainerResolverProtocol,
21 )
23logger = get_logger(__name__)
25_BUILTIN_MAP: dict[str, str] = {
26 "current_datetime": "lexigram.ai.skills.builtin.datetime_skill.DateTimeSkill",
27 "math_calculate": "lexigram.ai.skills.builtin.math_skill.MathSkill",
28 "text_summarize": "lexigram.ai.skills.builtin.text_processing.TextSummarizeSkill",
29 "text_translate": "lexigram.ai.skills.builtin.text_processing.TextTranslateSkill",
30 "web_search": "lexigram.ai.skills.builtin.web_search.WebSearchSkill",
31 "http_request": "lexigram.ai.skills.builtin.http_request.HTTPRequestSkill",
32 "file_read": "lexigram.ai.skills.builtin.file_operations.FileReadSkill",
33 "file_write": "lexigram.ai.skills.builtin.file_operations.FileWriteSkill",
34 "code_execute": "lexigram.ai.skills.builtin.code_execution.CodeExecutionSkill",
35}
38def _import_class(dotted_path: str) -> type[Any]:
39 """Import a class from a fully qualified dotted path."""
40 import importlib
42 module_path, _, class_name = dotted_path.rpartition(".")
43 module = importlib.import_module(module_path)
44 return getattr(module, class_name)
47class SkillsProvider(Provider):
48 """Register and bootstrap the skills subsystem."""
50 name = "skills"
51 priority = ProviderPriority.DOMAIN
52 config_key: str | None = "ai_skills"
53 config_model: type | None = SkillsConfig
55 def __init__(
56 self,
57 config: SkillsConfig | None = None,
58 enable_tool_bridge: bool = False,
59 **kwargs: Any,
60 ) -> None:
61 """Initialise the provider with optional configuration.
63 Args:
64 config: Skills configuration. Defaults to ``SkillsConfig()``.
65 enable_tool_bridge: Register the tool-bridge adapter so agent tool
66 registries can invoke skills as first-class tools.
67 **kwargs: Reserved for future keyword arguments.
68 """
69 super().__init__()
70 self._requested_config = config
71 self._config = config or SkillsConfig()
72 self._enable_tool_bridge = enable_tool_bridge
74 async def register(self, container: ContainerRegistrarProtocol) -> None:
75 """Register all skills services into the container."""
76 from lexigram.ai.skills.caching.skill_cache import SkillResultCache
77 from lexigram.ai.skills.executor import SkillExecutor
78 from lexigram.ai.skills.permissions.permission_checker import PermissionChecker
79 from lexigram.ai.skills.registry import SkillRegistry
81 self._config = self._requested_config or (
82 self.config
83 if isinstance(getattr(self, "config", None), SkillsConfig)
84 else self._config
85 )
86 container.singleton(SkillsConfig, instance=self._config)
87 registry = SkillRegistry()
88 container.singleton(SkillRegistry, instance=registry)
89 logger.debug("skills_provider_registered", service="SkillRegistry")
91 cache: SkillResultCache | None = None
92 if self._config.cache_enabled:
93 cache = SkillResultCache(ttl_seconds=self._config.cache_ttl_seconds)
94 container.singleton(SkillResultCache, instance=cache)
95 logger.debug("skills_provider_registered", service="SkillResultCache")
97 permission_checker: PermissionChecker | None = None
98 if self._config.enforce_permissions:
99 permission_checker = PermissionChecker()
100 container.singleton(PermissionChecker, instance=permission_checker)
101 logger.debug("skills_provider_registered", service="PermissionChecker")
103 executor = SkillExecutor(
104 registry=registry,
105 cache=cast("CacheBackendProtocol | None", cache),
106 permission_checker=permission_checker,
107 )
108 container.singleton(SkillExecutor, instance=executor)
109 logger.debug("skills_provider_registered", service="SkillExecutor")
111 async def boot(self, container: ContainerResolverProtocol) -> None:
112 """Register built-in and auto-discovered skills into the registry."""
113 from lexigram.ai.skills.registry import SkillRegistry
115 registry = await container.resolve_optional(SkillRegistry)
116 if registry is None:
117 logger.warning("skills_registry_not_available")
118 return
120 if self._config.enable_builtin:
121 for skill_name in self._config.builtin_skills:
122 dotted = _BUILTIN_MAP.get(skill_name)
123 if dotted is None:
124 logger.warning("skills_unknown_builtin", skill=skill_name)
125 continue
126 try:
127 cls = _import_class(dotted)
128 registry.register(cls())
129 logger.debug("skills_builtin_registered", skill=skill_name)
130 except (
131 ImportError,
132 AttributeError,
133 TypeError,
134 ValueError,
135 ) as exc:
136 logger.warning(
137 "skills_builtin_register_error",
138 skill=skill_name,
139 error=str(exc),
140 )
142 if self._config.auto_discover:
143 from lexigram.ai.skills.discovery.module_scanner import ModuleScanner
145 scanner = ModuleScanner()
146 for pkg in self._config.scan_packages:
147 try:
148 count = await scanner.scan_package(registry, pkg)
149 logger.info("skills_auto_discovered", package=pkg, count=count)
150 except (
151 ImportError,
152 AttributeError,
153 RuntimeError,
154 ValueError,
155 ) as exc:
156 logger.warning(
157 "skills_discovery_error", package=pkg, error=str(exc)
158 )
160 # Skill sources (SKILL.md files from various directories)
161 if self._config.enable_skill_sources:
162 from lexigram.ai.skills.discovery.skill_loader import SkillLoader
163 from lexigram.ai.skills.discovery.skill_source_scanner import (
164 SkillSourceScanner,
165 )
167 # Scan all configured paths
168 for path_str in self._config.skill_paths:
169 path = Path(path_str).expanduser()
170 if not path.exists():
171 logger.debug("skill_source_path_not_found", path=path_str)
172 continue
174 loader = SkillLoader(
175 timeout_seconds=self._config.script_timeout_seconds,
176 skill_root=path,
177 allowed_script_types=tuple(self._config.allowed_script_types),
178 )
179 source_scanner = SkillSourceScanner(loader=loader)
181 try:
182 count = await source_scanner.scan(registry, str(path))
183 logger.info("skill_sources_discovered", path=path_str, count=count)
184 except Exception as exc: # noqa: BLE001 # skill source scanning is best-effort; one failure must not stop others
185 logger.warning(
186 "skill_source_discovery_error",
187 path=path_str,
188 error=str(exc),
189 )
191 async def shutdown(self) -> None:
192 """Perform any cleanup on shutdown (no-op for the skills provider)."""
194 async def health_check(self, timeout: float = 5.0) -> HealthCheckResult:
195 """Health check — always healthy (in-process domain provider).
197 No external backend to ping.
199 Args:
200 timeout: Ignored for in-process providers.
202 Returns:
203 Always HEALTHY — no external backend to ping.
204 """
205 return HealthCheckResult(
206 component=self.name,
207 status=HealthStatus.HEALTHY,
208 details={"status": "operational"},
209 )
212__all__ = ["SkillsProvider"]