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.
skilllite/__init__.py ADDED
@@ -0,0 +1,159 @@
1
+ """
2
+ SkillLite - A lightweight Skills execution engine with LLM integration.
3
+
4
+ This package provides a Python interface to the SkillLite execution engine,
5
+ using OpenAI-compatible API format as the unified interface.
6
+
7
+ Supported LLM providers:
8
+ - OpenAI (GPT-4, GPT-3.5, etc.)
9
+ - Azure OpenAI
10
+ - Anthropic Claude (via OpenAI-compatible endpoint or native)
11
+ - Google Gemini (via OpenAI-compatible endpoint)
12
+ - Local models (Ollama, vLLM, LMStudio, etc.)
13
+ - DeepSeek, Qwen, Moonshot, Zhipu, and other providers
14
+
15
+ Quick Start (Enhanced):
16
+ ```python
17
+ from skilllite import SkillRunner
18
+
19
+ # One-line execution with intelligent features
20
+ runner = SkillRunner()
21
+ result = runner.run("Create a data analysis skill for me")
22
+ print(result)
23
+ ```
24
+
25
+ Advanced Usage:
26
+ ```python
27
+ from openai import OpenAI
28
+ from skilllite import SkillManager
29
+
30
+ # Works with any OpenAI-compatible client
31
+ client = OpenAI() # or OpenAI(base_url="...", api_key="...")
32
+ manager = SkillManager(skills_dir="./my_skills")
33
+
34
+ # Enhanced agentic loop with task list based execution
35
+ loop = manager.create_enhanced_agentic_loop(
36
+ client=client,
37
+ model="gpt-4"
38
+ )
39
+ response = loop.run("Analyze this data and generate a report")
40
+ ```
41
+
42
+ Legacy Usage:
43
+ ```python
44
+ from openai import OpenAI
45
+ from skilllite import SkillManager
46
+
47
+ client = OpenAI()
48
+ manager = SkillManager(skills_dir="./my_skills")
49
+
50
+ # Get tools (OpenAI-compatible format)
51
+ tools = manager.get_tools()
52
+
53
+ # Call any OpenAI-compatible API
54
+ response = client.chat.completions.create(
55
+ model="gpt-4",
56
+ tools=tools,
57
+ messages=[{"role": "user", "content": "..."}]
58
+ )
59
+
60
+ # Handle tool calls
61
+ if response.choices[0].message.tool_calls:
62
+ results = manager.handle_tool_calls(response)
63
+ ```
64
+ """
65
+
66
+ # Import from core module (protected core functionality)
67
+ from .core import (
68
+ SkillManager,
69
+ SkillInfo,
70
+ AgenticLoop,
71
+ AgenticLoopClaudeNative,
72
+ ApiFormat,
73
+ ToolDefinition,
74
+ ToolUseRequest,
75
+ ToolResult,
76
+ SkillExecutor,
77
+ ExecutionResult,
78
+ SkillMetadata,
79
+ NetworkPolicy,
80
+ parse_skill_metadata,
81
+ )
82
+
83
+ # Import from non-core modules (utilities, quick start, etc.)
84
+ from .quick import SkillRunner, quick_run, load_env, get_runner
85
+ from .core.metadata import get_skill_summary
86
+ from .sandbox.skillbox import (
87
+ install as install_binary,
88
+ uninstall as uninstall_binary,
89
+ is_installed as is_binary_installed,
90
+ find_binary,
91
+ ensure_installed,
92
+ get_installed_version,
93
+ BINARY_VERSION,
94
+ )
95
+ from .analyzer import (
96
+ ScriptAnalyzer,
97
+ ScriptInfo,
98
+ SkillScanResult,
99
+ ExecutionRecommendation,
100
+ scan_skill,
101
+ analyze_skill,
102
+ )
103
+ from .builtin_tools import (
104
+ get_builtin_file_tools,
105
+ execute_builtin_file_tool,
106
+ create_builtin_tool_executor,
107
+ )
108
+
109
+ # Try to import MCP module (optional dependency)
110
+ try:
111
+ from .mcp import MCPServer, SandboxExecutor
112
+ MCP_AVAILABLE = True
113
+ except ImportError:
114
+ MCP_AVAILABLE = False
115
+
116
+ __version__ = "0.1.0"
117
+ __all__ = [
118
+ # Core
119
+ "SkillManager",
120
+ "SkillInfo",
121
+ "AgenticLoop",
122
+ "AgenticLoopClaudeNative",
123
+ "ApiFormat",
124
+ "ToolDefinition",
125
+ "ToolUseRequest",
126
+ "ToolResult",
127
+ "SkillExecutor",
128
+ "ExecutionResult",
129
+ # Script Analysis
130
+ "ScriptAnalyzer",
131
+ "ScriptInfo",
132
+ "SkillScanResult",
133
+ "ExecutionRecommendation",
134
+ "scan_skill",
135
+ "analyze_skill",
136
+ # Schema Inference
137
+ "get_skill_summary",
138
+ # Quick Start
139
+ "SkillRunner",
140
+ "quick_run",
141
+ "load_env",
142
+ "get_runner",
143
+ # Binary Management
144
+ "install_binary",
145
+ "uninstall_binary",
146
+ "is_binary_installed",
147
+ "find_binary",
148
+ "ensure_installed",
149
+ "get_installed_version",
150
+ "BINARY_VERSION",
151
+ # Built-in Tools
152
+ "get_builtin_file_tools",
153
+ "execute_builtin_file_tool",
154
+ "create_builtin_tool_executor",
155
+ # MCP Integration (optional)
156
+ "MCPServer",
157
+ "SandboxExecutor",
158
+ "MCP_AVAILABLE",
159
+ ]
skilllite/analyzer.py ADDED
@@ -0,0 +1,391 @@
1
+ """
2
+ Script Analyzer - Analyzes skill scripts and generates execution recommendations for LLM.
3
+
4
+ This module provides tools to scan skill directories, analyze scripts, and generate
5
+ structured information that can be used by LLMs to decide how to execute skills.
6
+ """
7
+
8
+ import json
9
+ import subprocess
10
+ from dataclasses import dataclass, field
11
+ from pathlib import Path
12
+ from typing import Any, Dict, List, Optional
13
+
14
+ from .sandbox.skillbox import ensure_installed
15
+
16
+
17
+ @dataclass
18
+ class ScriptInfo:
19
+ """Information about a single script file."""
20
+ path: str
21
+ language: str
22
+ total_lines: int
23
+ preview: str
24
+ description: Optional[str]
25
+ has_main_entry: bool
26
+ uses_argparse: bool
27
+ uses_stdio: bool
28
+ file_size_bytes: int
29
+
30
+ @classmethod
31
+ def from_dict(cls, data: Dict[str, Any]) -> "ScriptInfo":
32
+ return cls(
33
+ path=data.get("path", ""),
34
+ language=data.get("language", ""),
35
+ total_lines=data.get("total_lines", 0),
36
+ preview=data.get("preview", ""),
37
+ description=data.get("description"),
38
+ has_main_entry=data.get("has_main_entry", False),
39
+ uses_argparse=data.get("uses_argparse", False),
40
+ uses_stdio=data.get("uses_stdio", False),
41
+ file_size_bytes=data.get("file_size_bytes", 0),
42
+ )
43
+
44
+
45
+ @dataclass
46
+ class SkillScanResult:
47
+ """Result of scanning a skill directory."""
48
+ skill_dir: str
49
+ has_skill_md: bool
50
+ skill_metadata: Optional[Dict[str, Any]]
51
+ scripts: List[ScriptInfo]
52
+ directories: Dict[str, bool]
53
+
54
+ @classmethod
55
+ def from_dict(cls, data: Dict[str, Any]) -> "SkillScanResult":
56
+ scripts = [ScriptInfo.from_dict(s) for s in data.get("scripts", [])]
57
+ return cls(
58
+ skill_dir=data.get("skill_dir", ""),
59
+ has_skill_md=data.get("has_skill_md", False),
60
+ skill_metadata=data.get("skill_metadata"),
61
+ scripts=scripts,
62
+ directories=data.get("directories", {}),
63
+ )
64
+
65
+
66
+ @dataclass
67
+ class ExecutionRecommendation:
68
+ """Recommendation for how to execute a script."""
69
+ script_path: str
70
+ language: str
71
+ execution_method: str # "stdin_json", "argparse", "direct"
72
+ confidence: float # 0.0 to 1.0
73
+ reasoning: str
74
+ suggested_command: str
75
+ input_format: str # "json_stdin", "cli_args", "none"
76
+ output_format: str # "json_stdout", "text_stdout", "file"
77
+
78
+
79
+ class ScriptAnalyzer:
80
+ """
81
+ Analyzes skill directories and scripts to provide execution recommendations.
82
+
83
+ This class uses the skillbox binary to scan directories and then provides
84
+ additional analysis and recommendations for LLM-based execution decisions.
85
+ """
86
+
87
+ def __init__(self, binary_path: Optional[str] = None, auto_install: bool = False):
88
+ """
89
+ Initialize the analyzer.
90
+
91
+ Args:
92
+ binary_path: Path to the skillbox binary. If None, auto-detect.
93
+ auto_install: Automatically download and install binary if not found.
94
+ """
95
+ self.binary_path = binary_path or ensure_installed(auto_install=auto_install)
96
+
97
+ def scan(self, skill_dir: Path, preview_lines: int = 10) -> SkillScanResult:
98
+ """
99
+ Scan a skill directory and return information about all scripts.
100
+
101
+ Args:
102
+ skill_dir: Path to the skill directory
103
+ preview_lines: Number of lines to include in script preview
104
+
105
+ Returns:
106
+ SkillScanResult with information about the skill and its scripts
107
+ """
108
+ cmd = [
109
+ self.binary_path,
110
+ "scan",
111
+ str(skill_dir),
112
+ "--preview-lines",
113
+ str(preview_lines),
114
+ ]
115
+
116
+ result = subprocess.run(cmd, capture_output=True, text=True)
117
+
118
+ if result.returncode != 0:
119
+ raise RuntimeError(f"Failed to scan skill directory: {result.stderr}")
120
+
121
+ data = json.loads(result.stdout)
122
+ return SkillScanResult.from_dict(data)
123
+
124
+ def analyze_for_execution(
125
+ self,
126
+ skill_dir: Path,
127
+ task_description: Optional[str] = None
128
+ ) -> Dict[str, Any]:
129
+ """
130
+ Analyze a skill directory and generate execution recommendations.
131
+
132
+ This method is designed to produce output that can be directly used
133
+ by an LLM to decide how to execute scripts in the skill.
134
+
135
+ Args:
136
+ skill_dir: Path to the skill directory
137
+ task_description: Optional description of what the user wants to do
138
+
139
+ Returns:
140
+ Dictionary with analysis results suitable for LLM consumption
141
+ """
142
+ scan_result = self.scan(skill_dir, preview_lines=15)
143
+
144
+ recommendations = []
145
+ for script in scan_result.scripts:
146
+ rec = self._analyze_script(script, scan_result.skill_metadata)
147
+ recommendations.append(rec)
148
+
149
+ # Sort by confidence
150
+ recommendations.sort(key=lambda r: r.confidence, reverse=True)
151
+
152
+ return {
153
+ "skill_dir": str(skill_dir),
154
+ "skill_name": scan_result.skill_metadata.get("name") if scan_result.skill_metadata else None,
155
+ "skill_description": scan_result.skill_metadata.get("description") if scan_result.skill_metadata else None,
156
+ "has_skill_md": scan_result.has_skill_md,
157
+ "total_scripts": len(scan_result.scripts),
158
+ "directories": scan_result.directories,
159
+ "recommendations": [
160
+ {
161
+ "script_path": r.script_path,
162
+ "language": r.language,
163
+ "execution_method": r.execution_method,
164
+ "confidence": r.confidence,
165
+ "reasoning": r.reasoning,
166
+ "suggested_command": r.suggested_command,
167
+ "input_format": r.input_format,
168
+ "output_format": r.output_format,
169
+ }
170
+ for r in recommendations
171
+ ],
172
+ "scripts_detail": [
173
+ {
174
+ "path": s.path,
175
+ "language": s.language,
176
+ "description": s.description,
177
+ "total_lines": s.total_lines,
178
+ "has_main_entry": s.has_main_entry,
179
+ "uses_argparse": s.uses_argparse,
180
+ "uses_stdio": s.uses_stdio,
181
+ }
182
+ for s in scan_result.scripts
183
+ ],
184
+ "llm_prompt_hint": self._generate_llm_hint(scan_result, task_description),
185
+ }
186
+
187
+ def _analyze_script(
188
+ self,
189
+ script: ScriptInfo,
190
+ skill_metadata: Optional[Dict[str, Any]]
191
+ ) -> ExecutionRecommendation:
192
+ """Analyze a single script and generate execution recommendation."""
193
+
194
+ confidence = 0.5
195
+ reasoning_parts = []
196
+
197
+ # Determine execution method based on script characteristics
198
+ if script.uses_stdio and not script.uses_argparse:
199
+ execution_method = "stdin_json"
200
+ input_format = "json_stdin"
201
+ confidence += 0.3
202
+ reasoning_parts.append("Script uses stdin/stdout for I/O")
203
+ elif script.uses_argparse:
204
+ execution_method = "argparse"
205
+ input_format = "cli_args"
206
+ confidence += 0.2
207
+ reasoning_parts.append("Script uses argument parsing")
208
+ else:
209
+ execution_method = "direct"
210
+ input_format = "none"
211
+ reasoning_parts.append("Script appears to run directly without input")
212
+
213
+ # Boost confidence for scripts with main entry
214
+ if script.has_main_entry:
215
+ confidence += 0.1
216
+ reasoning_parts.append("Has main entry point")
217
+
218
+ # Boost confidence for scripts in scripts/ directory
219
+ if script.path.startswith("scripts/"):
220
+ confidence += 0.1
221
+ reasoning_parts.append("Located in scripts/ directory")
222
+
223
+ # Check if this matches the skill's entry_point
224
+ if skill_metadata and skill_metadata.get("entry_point") == script.path:
225
+ confidence = 1.0
226
+ reasoning_parts.insert(0, "Matches skill entry_point")
227
+
228
+ # Determine output format
229
+ if script.uses_stdio:
230
+ output_format = "json_stdout" if "json" in script.preview.lower() else "text_stdout"
231
+ else:
232
+ output_format = "text_stdout"
233
+
234
+ # Generate suggested command
235
+ suggested_command = self._generate_command(script, execution_method)
236
+
237
+ return ExecutionRecommendation(
238
+ script_path=script.path,
239
+ language=script.language,
240
+ execution_method=execution_method,
241
+ confidence=min(confidence, 1.0),
242
+ reasoning="; ".join(reasoning_parts),
243
+ suggested_command=suggested_command,
244
+ input_format=input_format,
245
+ output_format=output_format,
246
+ )
247
+
248
+ def _generate_command(self, script: ScriptInfo, execution_method: str) -> str:
249
+ """Generate a suggested command for executing the script."""
250
+
251
+ if execution_method == "stdin_json":
252
+ if script.language == "python":
253
+ return f'echo \'{{"input": "value"}}\' | python {script.path}'
254
+ elif script.language == "node":
255
+ return f'echo \'{{"input": "value"}}\' | node {script.path}'
256
+ elif script.language == "shell":
257
+ return f'echo \'{{"input": "value"}}\' | bash {script.path}'
258
+ elif execution_method == "argparse":
259
+ if script.language == "python":
260
+ return f'python {script.path} --help'
261
+ elif script.language == "node":
262
+ return f'node {script.path} --help'
263
+ elif script.language == "shell":
264
+ return f'bash {script.path} --help'
265
+ else:
266
+ if script.language == "python":
267
+ return f'python {script.path}'
268
+ elif script.language == "node":
269
+ return f'node {script.path}'
270
+ elif script.language == "shell":
271
+ return f'bash {script.path}'
272
+
273
+ return f'# Unknown execution method for {script.path}'
274
+
275
+ def _generate_llm_hint(
276
+ self,
277
+ scan_result: SkillScanResult,
278
+ task_description: Optional[str]
279
+ ) -> str:
280
+ """Generate a hint for LLM to understand how to use this skill."""
281
+
282
+ hints = []
283
+
284
+ if scan_result.skill_metadata:
285
+ meta = scan_result.skill_metadata
286
+ if meta.get("description"):
287
+ hints.append(f"Skill purpose: {meta['description']}")
288
+ if meta.get("entry_point"):
289
+ hints.append(f"Primary entry point: {meta['entry_point']}")
290
+
291
+ if not scan_result.scripts:
292
+ hints.append("No executable scripts found. This may be a prompt-only skill.")
293
+ else:
294
+ script_types = {}
295
+ for s in scan_result.scripts:
296
+ script_types[s.language] = script_types.get(s.language, 0) + 1
297
+
298
+ type_str = ", ".join(f"{count} {lang}" for lang, count in script_types.items())
299
+ hints.append(f"Available scripts: {type_str}")
300
+
301
+ # Highlight scripts with descriptions
302
+ described = [s for s in scan_result.scripts if s.description]
303
+ if described:
304
+ hints.append("Scripts with descriptions:")
305
+ for s in described[:3]: # Limit to 3
306
+ hints.append(f" - {s.path}: {s.description[:100]}")
307
+
308
+ if task_description:
309
+ hints.append(f"User task: {task_description}")
310
+
311
+ return "\n".join(hints)
312
+
313
+ def get_execution_context(self, skill_dir: Path) -> Dict[str, Any]:
314
+ """
315
+ Get execution context for a skill, suitable for passing to skillbox exec.
316
+
317
+ Returns a dictionary with all information needed to execute scripts
318
+ in the skill directory.
319
+ """
320
+ scan_result = self.scan(skill_dir)
321
+
322
+ return {
323
+ "skill_dir": str(skill_dir.absolute()),
324
+ "has_skill_md": scan_result.has_skill_md,
325
+ "network_enabled": (
326
+ scan_result.skill_metadata.get("network_enabled", False)
327
+ if scan_result.skill_metadata else False
328
+ ),
329
+ "compatibility": (
330
+ scan_result.skill_metadata.get("compatibility")
331
+ if scan_result.skill_metadata else None
332
+ ),
333
+ "available_scripts": [
334
+ {
335
+ "path": s.path,
336
+ "language": s.language,
337
+ "has_main": s.has_main_entry,
338
+ "uses_argparse": s.uses_argparse,
339
+ "uses_stdio": s.uses_stdio,
340
+ }
341
+ for s in scan_result.scripts
342
+ ],
343
+ }
344
+
345
+
346
+ def scan_skill(skill_dir: str, preview_lines: int = 10) -> Dict[str, Any]:
347
+ """
348
+ Convenience function to scan a skill directory.
349
+
350
+ Args:
351
+ skill_dir: Path to the skill directory
352
+ preview_lines: Number of lines to include in script preview
353
+
354
+ Returns:
355
+ Dictionary with scan results
356
+ """
357
+ analyzer = ScriptAnalyzer(auto_install=True)
358
+ result = analyzer.scan(Path(skill_dir), preview_lines)
359
+ return {
360
+ "skill_dir": result.skill_dir,
361
+ "has_skill_md": result.has_skill_md,
362
+ "skill_metadata": result.skill_metadata,
363
+ "scripts": [
364
+ {
365
+ "path": s.path,
366
+ "language": s.language,
367
+ "description": s.description,
368
+ "total_lines": s.total_lines,
369
+ "has_main_entry": s.has_main_entry,
370
+ "uses_argparse": s.uses_argparse,
371
+ "uses_stdio": s.uses_stdio,
372
+ }
373
+ for s in result.scripts
374
+ ],
375
+ "directories": result.directories,
376
+ }
377
+
378
+
379
+ def analyze_skill(skill_dir: str, task_description: Optional[str] = None) -> Dict[str, Any]:
380
+ """
381
+ Convenience function to analyze a skill for execution.
382
+
383
+ Args:
384
+ skill_dir: Path to the skill directory
385
+ task_description: Optional description of what the user wants to do
386
+
387
+ Returns:
388
+ Dictionary with analysis results and recommendations
389
+ """
390
+ analyzer = ScriptAnalyzer(auto_install=True)
391
+ return analyzer.analyze_for_execution(Path(skill_dir), task_description)