skilllite 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,185 @@
1
+ """
2
+ Skill Registry - Skill registration and discovery.
3
+
4
+ This module handles:
5
+ - Scanning directories for skills
6
+ - Registering individual skills
7
+ - Skill lookup and listing
8
+ - Multi-script tool detection
9
+ """
10
+
11
+ from pathlib import Path
12
+ from typing import Dict, List, Optional, Set
13
+
14
+ from .metadata import SkillMetadata, parse_skill_metadata, detect_all_scripts
15
+ from .skill_info import SkillInfo
16
+
17
+
18
+ class SkillRegistry:
19
+ """
20
+ Registry for managing skill registration and discovery.
21
+
22
+ Handles skill scanning, registration, and lookup operations.
23
+ Multi-script analysis is performed lazily when needed.
24
+ """
25
+
26
+ def __init__(self):
27
+ self._skills: Dict[str, SkillInfo] = {}
28
+ # Maps tool_name -> tool info for multi-script skills
29
+ self._multi_script_tools: Dict[str, Dict[str, str]] = {}
30
+ # Track which skills have been analyzed for multi-script tools
31
+ self._analyzed_skills: Set[str] = set()
32
+
33
+ def scan_directory(self, directory: Path) -> int:
34
+ """
35
+ Scan a directory for skills.
36
+
37
+ Args:
38
+ directory: Directory to scan
39
+
40
+ Returns:
41
+ Number of skills registered
42
+
43
+ Raises:
44
+ FileNotFoundError: If directory does not exist
45
+ """
46
+ if not directory.exists():
47
+ raise FileNotFoundError(f"Skills directory does not exist: {directory}")
48
+
49
+ # Check if directory itself is a skill
50
+ if (directory / "SKILL.md").exists():
51
+ self.register_skill(directory)
52
+ return 1
53
+
54
+ # Scan subdirectories
55
+ count = 0
56
+ for path in directory.iterdir():
57
+ if path.is_dir() and (path / "SKILL.md").exists():
58
+ try:
59
+ self.register_skill(path)
60
+ count += 1
61
+ except Exception as e:
62
+ print(f"Warning: Failed to register skill at {path}: {e}")
63
+ return count
64
+
65
+ def register_skill(self, skill_dir: Path) -> SkillInfo:
66
+ """
67
+ Register a single skill from a directory.
68
+
69
+ Multi-script analysis is deferred until needed (lazy loading).
70
+
71
+ Args:
72
+ skill_dir: Path to skill directory
73
+
74
+ Returns:
75
+ Registered SkillInfo
76
+ """
77
+ metadata = parse_skill_metadata(skill_dir)
78
+ info = SkillInfo(metadata, skill_dir)
79
+ self._skills[metadata.name] = info
80
+ return info
81
+
82
+ def analyze_multi_script_skill(self, skill_name: str) -> None:
83
+ """
84
+ Analyze a skill for multiple scripts and register them as tools.
85
+
86
+ Called lazily when tool definitions are requested.
87
+
88
+ Args:
89
+ skill_name: Name of skill to analyze
90
+ """
91
+ if skill_name in self._analyzed_skills:
92
+ return
93
+
94
+ info = self._skills.get(skill_name)
95
+ if not info:
96
+ return
97
+
98
+ # Only analyze skills without a main entry point
99
+ if not info.metadata.entry_point:
100
+ scripts = detect_all_scripts(info.path)
101
+ if scripts:
102
+ for script in scripts:
103
+ # Create unique tool name: skill-name__script-name
104
+ # Use double underscore instead of colon for API compatibility
105
+ tool_name = f"{skill_name}__{script['name']}"
106
+ self._multi_script_tools[tool_name] = {
107
+ "skill_name": skill_name,
108
+ "script_path": script["path"],
109
+ "script_name": script["name"],
110
+ "language": script["language"],
111
+ "filename": script["filename"],
112
+ }
113
+
114
+ self._analyzed_skills.add(skill_name)
115
+
116
+ def analyze_all_multi_script_skills(self) -> None:
117
+ """Analyze all registered skills for multi-script tools."""
118
+ for skill_name in self._skills:
119
+ self.analyze_multi_script_skill(skill_name)
120
+
121
+ def get_skill(self, name: str) -> Optional[SkillInfo]:
122
+ """Get a skill by name."""
123
+ return self._skills.get(name)
124
+
125
+ def list_skills(self) -> List[SkillInfo]:
126
+ """Get all registered skills."""
127
+ return list(self._skills.values())
128
+
129
+ def skill_names(self) -> List[str]:
130
+ """Get names of all registered skills."""
131
+ return list(self._skills.keys())
132
+
133
+ def has_skill(self, name: str) -> bool:
134
+ """Check if a skill exists."""
135
+ return name in self._skills
136
+
137
+ def is_executable(self, name: str) -> bool:
138
+ """
139
+ Check if a skill or tool is executable.
140
+
141
+ Includes:
142
+ - Skills with a single entry_point
143
+ - Multi-script tools (skill-name:script-name format)
144
+ """
145
+ if name in self._multi_script_tools:
146
+ return True
147
+ info = self._skills.get(name)
148
+ return info is not None and bool(info.metadata.entry_point)
149
+
150
+ def list_executable_skills(self) -> List[SkillInfo]:
151
+ """Get all executable skills (with entry_point or multi-script tools)."""
152
+ executable = []
153
+ for info in self._skills.values():
154
+ if info.metadata.entry_point:
155
+ executable.append(info)
156
+ elif info.name in [t["skill_name"] for t in self._multi_script_tools.values()]:
157
+ executable.append(info)
158
+ return executable
159
+
160
+ def list_prompt_only_skills(self) -> List[SkillInfo]:
161
+ """Get all prompt-only skills (without entry_point and no multi-script tools)."""
162
+ prompt_only = []
163
+ multi_script_skill_names = set(t["skill_name"] for t in self._multi_script_tools.values())
164
+ for info in self._skills.values():
165
+ if not info.metadata.entry_point and info.name not in multi_script_skill_names:
166
+ prompt_only.append(info)
167
+ return prompt_only
168
+
169
+ def list_multi_script_tools(self) -> List[str]:
170
+ """Get all multi-script tool names."""
171
+ return list(self._multi_script_tools.keys())
172
+
173
+ def get_multi_script_tool_info(self, tool_name: str) -> Optional[Dict[str, str]]:
174
+ """Get info for a multi-script tool."""
175
+ return self._multi_script_tools.get(tool_name)
176
+
177
+ @property
178
+ def skills(self) -> Dict[str, SkillInfo]:
179
+ """Direct access to skills dict (for compatibility)."""
180
+ return self._skills
181
+
182
+ @property
183
+ def multi_script_tools(self) -> Dict[str, Dict[str, str]]:
184
+ """Direct access to multi-script tools dict (for compatibility)."""
185
+ return self._multi_script_tools
@@ -0,0 +1,181 @@
1
+ """
2
+ SkillInfo - Information container for a registered skill.
3
+
4
+ This is a CORE module - do not modify without explicit permission.
5
+
6
+ This module provides the SkillInfo class which encapsulates all information
7
+ about a skill including its metadata, content, references, and assets.
8
+ """
9
+
10
+ import json
11
+ from pathlib import Path
12
+ from typing import Any, Dict, List, Optional
13
+
14
+ from .metadata import SkillMetadata, detect_language, detect_all_scripts
15
+
16
+ class SkillInfo:
17
+ """Information about a registered skill."""
18
+
19
+ def __init__(self, metadata: SkillMetadata, path: Path):
20
+ self.metadata = metadata
21
+ self.path = path
22
+ self._full_content_cache: Optional[str] = None
23
+
24
+ @property
25
+ def name(self) -> str:
26
+ return self.metadata.name
27
+
28
+ @property
29
+ def description(self) -> Optional[str]:
30
+ return self.metadata.description
31
+
32
+ @property
33
+ def language(self) -> str:
34
+ return detect_language(self.path, self.metadata)
35
+
36
+ def get_full_content(self) -> str:
37
+ """
38
+ Get the full content of SKILL.md file.
39
+
40
+ Returns:
41
+ Complete content of SKILL.md including instructions and examples
42
+ """
43
+ if self._full_content_cache is not None:
44
+ return self._full_content_cache
45
+
46
+ skill_md_path = self.path / "SKILL.md"
47
+ if skill_md_path.exists():
48
+ self._full_content_cache = skill_md_path.read_text(encoding="utf-8")
49
+ return self._full_content_cache
50
+ return ""
51
+
52
+ def get_references(self) -> Dict[str, str]:
53
+ """
54
+ Get all reference documents from references/ directory.
55
+
56
+ Returns:
57
+ Dictionary mapping filename to file content
58
+ """
59
+ references = {}
60
+ references_dir = self.path / "references"
61
+
62
+ if not references_dir.exists():
63
+ return references
64
+
65
+ for file_path in references_dir.iterdir():
66
+ if file_path.is_file():
67
+ try:
68
+ content = file_path.read_text(encoding="utf-8")
69
+ references[file_path.name] = content
70
+ except Exception:
71
+ references[file_path.name] = f"[Error reading file: {file_path.name}]"
72
+
73
+ return references
74
+
75
+ def get_assets(self) -> Dict[str, Any]:
76
+ """
77
+ Get all asset files from assets/ directory.
78
+
79
+ For JSON files, returns parsed content.
80
+ For other files, returns file path for reference.
81
+
82
+ Returns:
83
+ Dictionary mapping filename to content or path
84
+ """
85
+ assets = {}
86
+ assets_dir = self.path / "assets"
87
+
88
+ if not assets_dir.exists():
89
+ return assets
90
+
91
+ for file_path in assets_dir.iterdir():
92
+ if file_path.is_file():
93
+ try:
94
+ if file_path.suffix.lower() == ".json":
95
+ content = file_path.read_text(encoding="utf-8")
96
+ assets[file_path.name] = json.loads(content)
97
+ elif file_path.suffix.lower() in [".txt", ".md", ".yaml", ".yml", ".toml", ".ini", ".cfg"]:
98
+ assets[file_path.name] = file_path.read_text(encoding="utf-8")
99
+ else:
100
+ assets[file_path.name] = {"_path": str(file_path), "_type": "binary"}
101
+ except Exception as e:
102
+ assets[file_path.name] = {"_error": str(e)}
103
+
104
+ return assets
105
+
106
+ def get_assets_dir(self) -> Optional[Path]:
107
+ """
108
+ Get the path to assets/ directory if it exists.
109
+
110
+ Returns:
111
+ Path to assets directory, or None if not exists
112
+ """
113
+ assets_dir = self.path / "assets"
114
+ return assets_dir if assets_dir.exists() else None
115
+
116
+ def get_references_dir(self) -> Optional[Path]:
117
+ """
118
+ Get the path to references/ directory if it exists.
119
+
120
+ Returns:
121
+ Path to references directory, or None if not exists
122
+ """
123
+ references_dir = self.path / "references"
124
+ return references_dir if references_dir.exists() else None
125
+
126
+ def get_all_scripts(self) -> List[Dict[str, str]]:
127
+ """
128
+ Get all executable scripts in this skill.
129
+
130
+ This is useful for skills with multiple entry points (like skill-creator
131
+ which has init_skill.py, package_skill.py, etc.)
132
+
133
+ Returns:
134
+ List of dicts with 'name', 'path', 'language', and 'filename' for each script
135
+ """
136
+ return detect_all_scripts(self.path)
137
+
138
+ def has_multiple_scripts(self) -> bool:
139
+ """
140
+ Check if this skill has multiple executable scripts.
141
+
142
+ Returns:
143
+ True if there are multiple scripts that could be entry points
144
+ """
145
+ scripts = self.get_all_scripts()
146
+ return len(scripts) > 1
147
+
148
+ def get_context(self, include_references: bool = True, include_assets: bool = True) -> Dict[str, Any]:
149
+ """
150
+ Get complete skill context for LLM consumption.
151
+
152
+ Args:
153
+ include_references: Whether to include reference documents
154
+ include_assets: Whether to include asset files
155
+
156
+ Returns:
157
+ Dictionary containing full skill context
158
+ """
159
+ context = {
160
+ "name": self.name,
161
+ "description": self.description,
162
+ "full_instructions": self.get_full_content(),
163
+ "skill_dir": str(self.path),
164
+ }
165
+
166
+ if include_references:
167
+ refs = self.get_references()
168
+ if refs:
169
+ context["references"] = refs
170
+
171
+ if include_assets:
172
+ assets = self.get_assets()
173
+ if assets:
174
+ context["assets"] = assets
175
+
176
+ # Include available scripts info for multi-script skills
177
+ scripts = self.get_all_scripts()
178
+ if scripts:
179
+ context["available_scripts"] = scripts
180
+
181
+ return context
@@ -0,0 +1,338 @@
1
+ """
2
+ Tool Builder - Tool definition generation and schema inference.
3
+
4
+ This module handles:
5
+ - Creating tool definitions from skills
6
+ - Schema inference from script content
7
+ - Argparse parsing for Python scripts
8
+ - Multi-script tool definition creation
9
+ """
10
+
11
+ import ast
12
+ import re
13
+ from pathlib import Path
14
+ from typing import Any, Dict, List, Optional, TYPE_CHECKING
15
+
16
+ from .skill_info import SkillInfo
17
+ from .tools import ToolDefinition
18
+
19
+ if TYPE_CHECKING:
20
+ from .registry import SkillRegistry
21
+
22
+
23
+ class ToolBuilder:
24
+ """
25
+ Builder for creating tool definitions from skills.
26
+
27
+ Handles tool definition generation and argparse parsing for Python scripts.
28
+ Uses progressive disclosure - tool definitions only contain name and description,
29
+ full SKILL.md content is injected when the tool is actually called.
30
+ """
31
+
32
+ def __init__(self, registry: "SkillRegistry"):
33
+ """
34
+ Initialize the tool builder.
35
+
36
+ Args:
37
+ registry: Skill registry for accessing skill info
38
+ """
39
+ self._registry = registry
40
+ # Cache for multi-script tool schemas (argparse-based)
41
+ self._multi_script_schemas: Dict[str, Dict[str, Any]] = {}
42
+
43
+ def get_tool_definitions(self, include_prompt_only: bool = False) -> List[ToolDefinition]:
44
+ """
45
+ Get tool definitions for registered skills.
46
+
47
+ Includes:
48
+ - Regular skills with a single entry_point
49
+ - Multi-script tools (each script as a separate tool)
50
+
51
+ Args:
52
+ include_prompt_only: Whether to include prompt-only skills
53
+
54
+ Returns:
55
+ List of tool definitions
56
+ """
57
+ # Lazily analyze all skills for multi-script tools
58
+ self._registry.analyze_all_multi_script_skills()
59
+
60
+ definitions = []
61
+ multi_script_skill_names = set(
62
+ t["skill_name"] for t in self._registry.multi_script_tools.values()
63
+ )
64
+
65
+ # Add regular skills with single entry_point
66
+ for info in self._registry.list_skills():
67
+ if info.metadata.entry_point:
68
+ definition = self._create_tool_definition(info)
69
+ definitions.append(definition)
70
+ elif info.name in multi_script_skill_names:
71
+ # Skip - will be handled by multi-script tools below
72
+ pass
73
+ elif include_prompt_only:
74
+ definition = self._create_tool_definition(info)
75
+ definitions.append(definition)
76
+
77
+ # Add multi-script tools
78
+ for tool_name, tool_info in self._registry.multi_script_tools.items():
79
+ skill_info = self._registry.get_skill(tool_info["skill_name"])
80
+ if skill_info:
81
+ definition = self._create_multi_script_tool_definition(
82
+ tool_name, tool_info, skill_info
83
+ )
84
+ definitions.append(definition)
85
+
86
+ return definitions
87
+
88
+ def get_tools_openai(self) -> List[Dict[str, Any]]:
89
+ """Get tool definitions in OpenAI-compatible format."""
90
+ return [d.to_openai_format() for d in self.get_tool_definitions()]
91
+
92
+ def get_tools_claude_native(self) -> List[Dict[str, Any]]:
93
+ """Get tool definitions in Claude's native API format."""
94
+ return [d.to_claude_format() for d in self.get_tool_definitions()]
95
+
96
+ def _create_tool_definition(self, info: SkillInfo) -> ToolDefinition:
97
+ """Create a tool definition from skill info.
98
+
99
+ Uses progressive disclosure:
100
+ 1. Tool definition only contains name and description (from YAML front matter)
101
+ 2. Uses a flexible schema that accepts any parameters
102
+ 3. Full SKILL.md content is injected when the tool is actually called,
103
+ letting the LLM understand the expected parameters from the documentation
104
+ """
105
+ description = info.description or f"Execute the {info.name} skill"
106
+
107
+ # Use a flexible schema that accepts any parameters
108
+ # The LLM will understand the expected format from the full SKILL.md
109
+ # content that is injected when the tool is called
110
+ input_schema = {
111
+ "type": "object",
112
+ "properties": {},
113
+ "additionalProperties": True
114
+ }
115
+
116
+ return ToolDefinition(
117
+ name=info.name,
118
+ description=description,
119
+ input_schema=input_schema
120
+ )
121
+
122
+ def _create_multi_script_tool_definition(
123
+ self,
124
+ tool_name: str,
125
+ tool_info: Dict[str, str],
126
+ skill_info: SkillInfo
127
+ ) -> ToolDefinition:
128
+ """Create a tool definition for a multi-script tool."""
129
+ script_name = tool_info["script_name"]
130
+ script_path = skill_info.path / tool_info["script_path"]
131
+ description = f"Execute {script_name} from {skill_info.name} skill"
132
+
133
+ if script_path.exists():
134
+ try:
135
+ script_content = script_path.read_text(encoding="utf-8")
136
+ docstring = self._extract_script_docstring(script_content)
137
+ if docstring:
138
+ first_line = docstring.strip().split('\n')[0].strip()
139
+ if first_line:
140
+ description = first_line
141
+ # Add action hint for common operations
142
+ if "init" in script_name.lower() or "create" in script_name.lower():
143
+ description += ". Call this tool directly to create a new skill."
144
+ elif "package" in script_name.lower():
145
+ description += ". Call this tool directly to package a skill."
146
+ elif "validate" in script_name.lower():
147
+ description += ". Call this tool directly to validate a skill."
148
+ except Exception:
149
+ pass
150
+
151
+ input_schema = self._infer_script_schema(tool_name, skill_info, tool_info)
152
+
153
+ return ToolDefinition(
154
+ name=tool_name,
155
+ description=description,
156
+ input_schema=input_schema
157
+ )
158
+
159
+ def _infer_script_schema(
160
+ self,
161
+ tool_name: str,
162
+ skill_info: SkillInfo,
163
+ tool_info: Dict[str, str]
164
+ ) -> Dict[str, Any]:
165
+ """Infer input schema for a multi-script tool using argparse parsing."""
166
+ if tool_name in self._multi_script_schemas:
167
+ return self._multi_script_schemas[tool_name]
168
+
169
+ script_path = skill_info.path / tool_info["script_path"]
170
+ script_code = None
171
+ if script_path.exists():
172
+ try:
173
+ script_code = script_path.read_text(encoding="utf-8")
174
+ except Exception:
175
+ pass
176
+
177
+ # Try argparse parsing for Python scripts
178
+ if script_code and tool_info.get("language") == "python":
179
+ schema = self._parse_argparse_schema(script_code)
180
+ if schema:
181
+ self._multi_script_schemas[tool_name] = schema
182
+ return schema
183
+
184
+ # Default schema for scripts without argparse
185
+ return {
186
+ "type": "object",
187
+ "properties": {
188
+ "args": {
189
+ "type": "array",
190
+ "items": {"type": "string"},
191
+ "description": "Command line arguments to pass to the script"
192
+ }
193
+ },
194
+ "required": []
195
+ }
196
+
197
+ def _extract_script_docstring(self, script_content: str) -> Optional[str]:
198
+ """Extract the module-level docstring from a Python script."""
199
+ try:
200
+ tree = ast.parse(script_content)
201
+ return ast.get_docstring(tree)
202
+ except Exception:
203
+ return None
204
+
205
+ def _parse_argparse_schema(self, script_code: str) -> Optional[Dict[str, Any]]:
206
+ """
207
+ Parse argparse argument definitions from Python script code.
208
+
209
+ Extracts add_argument calls and converts them to JSON schema format.
210
+ """
211
+ properties = {}
212
+ required = []
213
+
214
+ # Pattern to match add_argument calls
215
+ arg_pattern = re.compile(
216
+ r'\.add_argument\s*\(\s*["\']([^"\']+)["\']'
217
+ r'(?:\s*,\s*["\']([^"\']+)["\'])?'
218
+ r'([^)]*)\)',
219
+ re.MULTILINE | re.DOTALL
220
+ )
221
+
222
+ for match in arg_pattern.finditer(script_code):
223
+ arg_name = match.group(1)
224
+ second_arg = match.group(2)
225
+ kwargs_str = match.group(3)
226
+
227
+ # Determine parameter name
228
+ if arg_name.startswith('--'):
229
+ param_name = arg_name[2:].replace('-', '_')
230
+ is_positional = False
231
+ elif arg_name.startswith('-'):
232
+ if second_arg and second_arg.startswith('--'):
233
+ param_name = second_arg[2:].replace('-', '_')
234
+ else:
235
+ param_name = arg_name[1:]
236
+ is_positional = False
237
+ else:
238
+ param_name = arg_name.replace('-', '_')
239
+ is_positional = True
240
+
241
+ prop = {"type": "string"}
242
+
243
+ # Extract help text
244
+ help_match = re.search(r'help\s*=\s*["\']([^"\']+)["\']', kwargs_str)
245
+ if help_match:
246
+ prop["description"] = help_match.group(1)
247
+
248
+ # Extract type
249
+ type_match = re.search(r'type\s*=\s*(\w+)', kwargs_str)
250
+ if type_match:
251
+ type_name = type_match.group(1)
252
+ if type_name == 'int':
253
+ prop["type"] = "integer"
254
+ elif type_name == 'float':
255
+ prop["type"] = "number"
256
+ elif type_name == 'bool':
257
+ prop["type"] = "boolean"
258
+
259
+ # Check for action="store_true" or action="store_false"
260
+ action_match = re.search(r'action\s*=\s*["\'](\w+)["\']', kwargs_str)
261
+ if action_match:
262
+ action = action_match.group(1)
263
+ if action in ('store_true', 'store_false'):
264
+ prop["type"] = "boolean"
265
+
266
+ # Check for nargs
267
+ nargs_match = re.search(r'nargs\s*=\s*["\']?([^,\s\)]+)["\']?', kwargs_str)
268
+ if nargs_match:
269
+ nargs = nargs_match.group(1)
270
+ if nargs in ('*', '+') or nargs.isdigit():
271
+ prop["type"] = "array"
272
+ prop["items"] = {"type": "string"}
273
+
274
+ # Check for choices
275
+ choices_match = re.search(r'choices\s*=\s*\[([^\]]+)\]', kwargs_str)
276
+ if choices_match:
277
+ choices_str = choices_match.group(1)
278
+ choices = re.findall(r'["\']([^"\']+)["\']', choices_str)
279
+ if choices:
280
+ prop["enum"] = choices
281
+
282
+ # Check for default
283
+ default_match = re.search(r'default\s*=\s*([^,\)]+)', kwargs_str)
284
+ if default_match:
285
+ default_val = default_match.group(1).strip()
286
+ if default_val not in ('None', '""', "''"):
287
+ prop["default"] = default_val.strip('"\'')
288
+
289
+ # Check if required
290
+ required_match = re.search(r'required\s*=\s*True', kwargs_str)
291
+ if required_match or is_positional:
292
+ required.append(param_name)
293
+
294
+ properties[param_name] = prop
295
+
296
+ if not properties:
297
+ return None
298
+
299
+ return {
300
+ "type": "object",
301
+ "properties": properties,
302
+ "required": required
303
+ }
304
+
305
+ def infer_all_schemas(self, force: bool = False) -> Dict[str, Dict[str, Any]]:
306
+ """
307
+ Infer schemas for all skills that don't have one defined.
308
+
309
+ Args:
310
+ force: Force re-inference even if cached
311
+
312
+ Returns:
313
+ Dict mapping skill name to inferred schema
314
+ """
315
+ if not self._schema_inferrer:
316
+ raise RuntimeError("Schema inferrer not configured.")
317
+
318
+ results = {}
319
+ for info in self._registry.list_skills():
320
+ name = info.name
321
+ if info.metadata.input_schema:
322
+ results[name] = info.metadata.input_schema
323
+ continue
324
+ if not force and name in self._inferred_schemas:
325
+ results[name] = self._inferred_schemas[name]
326
+ continue
327
+
328
+ schema = self._infer_skill_schema(info)
329
+ if schema:
330
+ self._inferred_schemas[name] = schema
331
+ results[name] = schema
332
+
333
+ return results
334
+
335
+ @property
336
+ def inferred_schemas(self) -> Dict[str, Dict[str, Any]]:
337
+ """Access to inferred schemas cache."""
338
+ return self._inferred_schemas