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/quick.py ADDED
@@ -0,0 +1,420 @@
1
+ """
2
+ SkillLite Quick Start - Minimal wrapper for running Skills with one line of code.
3
+
4
+ Provides out-of-the-box convenience functions without manual LLM calls and tool calls handling.
5
+
6
+ Example:
7
+ ```python
8
+ from skilllite import quick_run
9
+
10
+ # Run with one line of code
11
+ result = quick_run("Calculate 15 times 27 for me")
12
+ print(result)
13
+ ```
14
+ """
15
+
16
+ import os
17
+ from pathlib import Path
18
+ from typing import Any, Callable, Dict, List, Optional, Union
19
+
20
+ from .core import SkillManager, AgenticLoop
21
+
22
+
23
+ def load_env(env_file: Optional[Union[str, Path]] = None) -> Dict[str, str]:
24
+ """
25
+ Load .env file into environment variables.
26
+
27
+ Args:
28
+ env_file: Path to .env file, defaults to .env in current directory
29
+
30
+ Returns:
31
+ Dictionary of loaded environment variables
32
+ """
33
+ if env_file is None:
34
+ env_file = Path.cwd() / ".env"
35
+ else:
36
+ env_file = Path(env_file)
37
+
38
+ loaded = {}
39
+ if env_file.exists():
40
+ for line in env_file.read_text().splitlines():
41
+ line = line.strip()
42
+ if line and not line.startswith("#") and "=" in line:
43
+ key, value = line.split("=", 1)
44
+ key, value = key.strip(), value.strip()
45
+ if value:
46
+ os.environ.setdefault(key, value)
47
+ loaded[key] = value
48
+ return loaded
49
+
50
+
51
+ class SkillRunner:
52
+ """
53
+ Minimal Skill Runner - Encapsulates all initialization and invocation logic.
54
+
55
+ Example:
56
+ ```python
57
+ from skilllite import SkillRunner
58
+
59
+ # Method 1: Use .env configuration
60
+ runner = SkillRunner()
61
+ result = runner.run("Calculate 15 times 27 for me")
62
+
63
+ # Method 2: Explicitly pass configuration
64
+ runner = SkillRunner(
65
+ base_url="https://api.deepseek.com",
66
+ api_key="sk-xxx",
67
+ model="deepseek-chat",
68
+ skills_dir="./.skills"
69
+ )
70
+ result = runner.run("Calculate 15 times 27 for me")
71
+ ```
72
+ """
73
+
74
+ def __init__(
75
+ self,
76
+ base_url: Optional[str] = None,
77
+ api_key: Optional[str] = None,
78
+ model: Optional[str] = None,
79
+ skills_dir: Optional[Union[str, Path]] = None,
80
+ env_file: Optional[Union[str, Path]] = None,
81
+ include_full_instructions: bool = True,
82
+ include_references: bool = True,
83
+ include_assets: bool = True,
84
+ context_mode: str = "full",
85
+ max_tokens_per_skill: Optional[int] = None,
86
+ max_iterations: int = 10,
87
+ verbose: bool = False,
88
+ custom_tools: Optional[List[Dict[str, Any]]] = None,
89
+ custom_tool_executor: Optional[Callable] = None,
90
+ use_enhanced_loop: bool = True,
91
+ enable_builtin_tools: bool = True,
92
+ allow_network: Optional[bool] = None,
93
+ enable_sandbox: Optional[bool] = None,
94
+ execution_timeout: Optional[int] = None,
95
+ max_memory_mb: Optional[int] = None
96
+ ):
97
+ """
98
+ Initialize SkillRunner.
99
+
100
+ Args:
101
+ base_url: LLM API URL, defaults to BASE_URL environment variable
102
+ api_key: API key, defaults to API_KEY environment variable
103
+ model: Model name, defaults to MODEL environment variable or "deepseek-chat"
104
+ skills_dir: Skills directory, defaults to "./.skills"
105
+ env_file: Path to .env file, defaults to .env in current directory
106
+ include_full_instructions: Whether to include full SKILL.md in system prompt (legacy)
107
+ include_references: Whether to include references directory content
108
+ include_assets: Whether to include assets directory content
109
+ context_mode: System prompt mode:
110
+ - "summary": Most minimal, only name, description and brief summary
111
+ - "standard": Balanced mode, includes input_schema and usage summary
112
+ - "full": Full mode, includes complete SKILL.md content
113
+ - "progressive": Progressive, summary + on-demand detail prompts
114
+ max_tokens_per_skill: Maximum tokens per skill (for truncation)
115
+ max_iterations: Maximum tool call iterations
116
+ verbose: Whether to output detailed logs
117
+ custom_tools: Custom tools list (e.g., file operation tools)
118
+ custom_tool_executor: Custom tool executor function
119
+ use_enhanced_loop: Whether to use enhanced AgenticLoop (default: True)
120
+ enable_builtin_tools: Whether to enable built-in file operation tools (default: True)
121
+ allow_network: Whether to allow skill network access (defaults from .env or False)
122
+ enable_sandbox: Whether to enable sandbox protection (defaults from .env or True)
123
+ execution_timeout: Skill execution timeout in seconds (defaults from .env or 120)
124
+ max_memory_mb: Maximum memory limit in MB (defaults from .env or 512)
125
+ """
126
+ # Load .env
127
+ load_env(env_file)
128
+
129
+ # Configuration
130
+ self.base_url = base_url or os.environ.get("BASE_URL")
131
+ self.api_key = api_key or os.environ.get("API_KEY")
132
+ self.model = model or os.environ.get("MODEL", "deepseek-chat")
133
+ self.skills_dir = skills_dir or "./.skills"
134
+ self.include_full_instructions = include_full_instructions
135
+ self.include_references = include_references
136
+ self.include_assets = include_assets
137
+ self.context_mode = context_mode
138
+ self.max_tokens_per_skill = max_tokens_per_skill
139
+ self.max_iterations = max_iterations
140
+ self.verbose = verbose
141
+ self.enable_builtin_tools = enable_builtin_tools
142
+
143
+ # Sandbox and network configuration (read from .env or use defaults)
144
+ self.allow_network = allow_network if allow_network is not None else \
145
+ (os.environ.get("ALLOW_NETWORK", "false").lower() == "true")
146
+ self.enable_sandbox = enable_sandbox if enable_sandbox is not None else \
147
+ (os.environ.get("ENABLE_SANDBOX", "true").lower() == "true")
148
+ self.execution_timeout = execution_timeout or \
149
+ int(os.environ.get("EXECUTION_TIMEOUT", "120"))
150
+ self.max_memory_mb = max_memory_mb or \
151
+ int(os.environ.get("MAX_MEMORY_MB", "512"))
152
+ # Read sandbox security level (from .env or default to 3)
153
+ self.sandbox_level = os.environ.get("SKILLBOX_SANDBOX_LEVEL", "3")
154
+
155
+ # Merge built-in tools and custom tools
156
+ self.custom_tools = custom_tools or []
157
+ if enable_builtin_tools:
158
+ from .builtin_tools import get_builtin_file_tools
159
+ builtin_tools = get_builtin_file_tools()
160
+ self.custom_tools = builtin_tools + self.custom_tools
161
+
162
+ self.custom_tool_executor = custom_tool_executor
163
+ self.use_enhanced_loop = use_enhanced_loop
164
+
165
+ # Lazy initialization
166
+ self._client = None
167
+ self._manager = None
168
+ self._system_context = None
169
+
170
+ @property
171
+ def client(self):
172
+ """Get OpenAI client (lazy initialization)"""
173
+ if self._client is None:
174
+ from openai import OpenAI
175
+ self._client = OpenAI(base_url=self.base_url, api_key=self.api_key)
176
+ return self._client
177
+
178
+ @property
179
+ def manager(self) -> SkillManager:
180
+ """Get SkillManager (lazy initialization)"""
181
+ if self._manager is None:
182
+ self._manager = SkillManager(
183
+ skills_dir=self.skills_dir,
184
+ allow_network=self.allow_network,
185
+ enable_sandbox=self.enable_sandbox,
186
+ execution_timeout=self.execution_timeout,
187
+ max_memory_mb=self.max_memory_mb,
188
+ sandbox_level=self.sandbox_level
189
+ )
190
+ if self.verbose:
191
+ print(f"📦 Loaded Skills: {self._manager.skill_names()}")
192
+ return self._manager
193
+
194
+ @property
195
+ def system_context(self) -> str:
196
+ """Get system prompt context"""
197
+ if self._system_context is None:
198
+ # Basic skill context, using new mode parameter
199
+ skill_context = self.manager.get_system_prompt_context(
200
+ include_full_instructions=self.include_full_instructions,
201
+ include_references=self.include_references,
202
+ include_assets=self.include_assets,
203
+ mode=self.context_mode,
204
+ max_tokens_per_skill=self.max_tokens_per_skill
205
+ )
206
+
207
+ # Add tool calling guidance
208
+ tool_guidance = """
209
+ # Tool Calling Guidelines
210
+
211
+ When calling tools, follow these rules:
212
+
213
+ 1. **Sequential Dependencies**: If a task depends on the result of a previous task, you MUST wait for the previous tool call to complete before making the next one. Do NOT use placeholders like `<result_of_xxx>` - always use actual values.
214
+
215
+ 2. **Parallel Independence**: If multiple tasks are independent of each other, you can call them in parallel in a single turn.
216
+
217
+ 3. **Always Use Real Values**: Tool parameters must be concrete values (numbers, strings, etc.), never references to other tool results.
218
+
219
+ Example of WRONG approach (don't do this):
220
+ - Task: "Calculate 100+200, then multiply by 3"
221
+ - Wrong: Call calculator(add, 100, 200) AND calculator(multiply, <result>, 3) in same turn
222
+
223
+ Example of CORRECT approach:
224
+ - Turn 1: Call calculator(add, 100, 200) → get result 300
225
+ - Turn 2: Call calculator(multiply, 300, 3) → get result 900
226
+
227
+ """
228
+ self._system_context = tool_guidance + skill_context
229
+
230
+ if self.verbose:
231
+ estimated_tokens = self.manager.estimate_context_tokens(
232
+ mode=self.context_mode,
233
+ include_references=self.include_references,
234
+ include_assets=self.include_assets
235
+ )
236
+ print(f"📊 System Prompt estimated tokens: ~{estimated_tokens}")
237
+ return self._system_context
238
+
239
+ @property
240
+ def tools(self) -> List[Dict[str, Any]]:
241
+ """Get tool definitions list"""
242
+ return self.manager.get_tools()
243
+
244
+ def run(self, user_message: str, stream: bool = False) -> str:
245
+ """
246
+ Run Skill and return final result.
247
+
248
+ Args:
249
+ user_message: User input message
250
+ stream: Whether to use streaming output (not supported yet)
251
+
252
+ Returns:
253
+ Final response content from LLM
254
+ """
255
+ if self.verbose:
256
+ print(f"👤 User: {user_message}")
257
+ print(f"⏳ Calling LLM...")
258
+
259
+ # Prepare tool executor
260
+ tool_executor = self.custom_tool_executor
261
+ if self.enable_builtin_tools and tool_executor is None:
262
+ # Create a combined executor that handles both built-in and custom tools
263
+ from .builtin_tools import execute_builtin_file_tool
264
+
265
+ def combined_executor(tool_input: Dict[str, Any]) -> str:
266
+ tool_name = tool_input.get("tool_name")
267
+ builtin_names = {"read_file", "write_file", "list_directory", "file_exists"}
268
+
269
+ if tool_name in builtin_names:
270
+ return execute_builtin_file_tool(tool_name, tool_input)
271
+ elif self.custom_tool_executor:
272
+ return self.custom_tool_executor(tool_input)
273
+ else:
274
+ return f"Error: No executor found for tool: {tool_name}"
275
+
276
+ tool_executor = combined_executor
277
+
278
+ # Use enhanced AgenticLoop to handle complete conversation flow
279
+ if self.use_enhanced_loop:
280
+ loop = self.manager.create_enhanced_agentic_loop(
281
+ client=self.client,
282
+ model=self.model,
283
+ max_iterations=self.max_iterations,
284
+ custom_tools=self.custom_tools if self.custom_tools else None,
285
+ custom_tool_executor=tool_executor
286
+ )
287
+ else:
288
+ # Use basic AgenticLoop (backward compatible)
289
+ loop = self.manager.create_agentic_loop(
290
+ client=self.client,
291
+ model=self.model,
292
+ system_prompt=self.system_context,
293
+ max_iterations=self.max_iterations
294
+ )
295
+
296
+ response = loop.run(user_message)
297
+ result = response.choices[0].message.content or ""
298
+
299
+ if self.verbose:
300
+ print(f"🤖 Assistant: {result}")
301
+
302
+ return result
303
+
304
+ def run_with_details(self, user_message: str) -> Dict[str, Any]:
305
+ """
306
+ Run Skill and return detailed results (including intermediate process).
307
+
308
+ Args:
309
+ user_message: User input message
310
+
311
+ Returns:
312
+ Dictionary containing complete information
313
+ """
314
+ messages = []
315
+ if self.system_context:
316
+ messages.append({"role": "system", "content": self.system_context})
317
+ messages.append({"role": "user", "content": user_message})
318
+
319
+ tools = self.tools
320
+ tool_calls_history = []
321
+ iterations = 0
322
+
323
+ for _ in range(self.max_iterations):
324
+ iterations += 1
325
+ response = self.client.chat.completions.create(
326
+ model=self.model,
327
+ tools=tools if tools else None,
328
+ messages=messages
329
+ )
330
+
331
+ message = response.choices[0].message
332
+
333
+ if not message.tool_calls:
334
+ return {
335
+ "content": message.content,
336
+ "iterations": iterations,
337
+ "tool_calls": tool_calls_history,
338
+ "final_response": response
339
+ }
340
+
341
+ # Record tool calls
342
+ messages.append(message)
343
+ results = self.manager.handle_tool_calls(response)
344
+
345
+ for tc, result in zip(message.tool_calls, results):
346
+ tool_calls_history.append({
347
+ "name": tc.function.name,
348
+ "arguments": tc.function.arguments,
349
+ "result": result.content
350
+ })
351
+ messages.append({
352
+ "role": "tool",
353
+ "tool_call_id": tc.id,
354
+ "content": result.content
355
+ })
356
+
357
+ return {
358
+ "content": message.content if message else None,
359
+ "iterations": iterations,
360
+ "tool_calls": tool_calls_history,
361
+ "final_response": response
362
+ }
363
+
364
+
365
+ # ==================== Convenience Functions ====================
366
+
367
+ _default_runner: Optional[SkillRunner] = None
368
+
369
+
370
+ def get_runner(**kwargs) -> SkillRunner:
371
+ """
372
+ Get default SkillRunner instance (singleton pattern).
373
+
374
+ Creates instance on first call, subsequent calls return the same instance.
375
+ Passed parameters will override default configuration.
376
+ """
377
+ global _default_runner
378
+ if _default_runner is None or kwargs:
379
+ _default_runner = SkillRunner(**kwargs)
380
+ return _default_runner
381
+
382
+
383
+ def quick_run(
384
+ user_message: str,
385
+ skills_dir: Optional[str] = None,
386
+ verbose: bool = False,
387
+ **kwargs
388
+ ) -> str:
389
+ """
390
+ Run Skill with one line of code.
391
+
392
+ Args:
393
+ user_message: User input message
394
+ skills_dir: Skills directory, defaults to "./.skills"
395
+ verbose: Whether to output detailed logs
396
+ **kwargs: Other parameters passed to SkillRunner
397
+
398
+ Returns:
399
+ Final response content from LLM
400
+
401
+ Example:
402
+ ```python
403
+ from skilllite import quick_run
404
+
405
+ # Simplest usage (requires .env configuration)
406
+ result = quick_run("Calculate 15 times 27 for me")
407
+
408
+ # With detailed output
409
+ result = quick_run("Calculate 15 times 27 for me", verbose=True)
410
+
411
+ # Specify skills directory
412
+ result = quick_run("Calculate 15 times 27 for me", skills_dir="./my_skills")
413
+ ```
414
+ """
415
+ if skills_dir:
416
+ kwargs["skills_dir"] = skills_dir
417
+ kwargs["verbose"] = verbose
418
+
419
+ runner = get_runner(**kwargs)
420
+ return runner.run(user_message)
@@ -0,0 +1,36 @@
1
+ """
2
+ Sandbox module - provides sandboxed execution environments.
3
+
4
+ This module abstracts different sandbox implementations, with skillbox
5
+ (Rust-based sandbox) as the primary implementation.
6
+ """
7
+
8
+ from .base import SandboxExecutor, ExecutionResult
9
+ from .config import (
10
+ SandboxConfig,
11
+ DEFAULT_EXECUTION_TIMEOUT,
12
+ DEFAULT_MAX_MEMORY_MB,
13
+ DEFAULT_SANDBOX_LEVEL,
14
+ DEFAULT_ALLOW_NETWORK,
15
+ DEFAULT_ENABLE_SANDBOX,
16
+ )
17
+ from .skillbox import SkillboxExecutor, install, uninstall, find_binary, ensure_installed
18
+
19
+ __all__ = [
20
+ # Base classes
21
+ "SandboxExecutor",
22
+ "ExecutionResult",
23
+ # Configuration
24
+ "SandboxConfig",
25
+ "DEFAULT_EXECUTION_TIMEOUT",
26
+ "DEFAULT_MAX_MEMORY_MB",
27
+ "DEFAULT_SANDBOX_LEVEL",
28
+ "DEFAULT_ALLOW_NETWORK",
29
+ "DEFAULT_ENABLE_SANDBOX",
30
+ # Skillbox implementation
31
+ "SkillboxExecutor",
32
+ "install",
33
+ "uninstall",
34
+ "find_binary",
35
+ "ensure_installed",
36
+ ]
@@ -0,0 +1,93 @@
1
+ """
2
+ Base classes for sandbox executors.
3
+
4
+ This module defines the abstract interface that all sandbox implementations
5
+ must follow, enabling easy switching between different sandbox backends.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Any, Dict, Optional
12
+
13
+
14
+ @dataclass
15
+ class ExecutionResult:
16
+ """Result of a sandbox execution."""
17
+ success: bool
18
+ output: Optional[Dict[str, Any]] = None
19
+ error: Optional[str] = None
20
+ exit_code: int = 0
21
+ stdout: str = ""
22
+ stderr: str = ""
23
+
24
+
25
+ class SandboxExecutor(ABC):
26
+ """
27
+ Abstract base class for sandbox executors.
28
+
29
+ All sandbox implementations (skillbox, docker, pyodide, etc.) should
30
+ inherit from this class and implement the required methods.
31
+ """
32
+
33
+ @abstractmethod
34
+ def execute(
35
+ self,
36
+ skill_dir: Path,
37
+ input_data: Dict[str, Any],
38
+ allow_network: Optional[bool] = None,
39
+ timeout: Optional[int] = None,
40
+ entry_point: Optional[str] = None
41
+ ) -> ExecutionResult:
42
+ """
43
+ Execute a skill with the given input.
44
+
45
+ Args:
46
+ skill_dir: Path to the skill directory
47
+ input_data: Input data for the skill
48
+ allow_network: Whether to allow network access
49
+ timeout: Execution timeout in seconds
50
+ entry_point: Optional specific script to execute
51
+
52
+ Returns:
53
+ ExecutionResult with the output or error
54
+ """
55
+ pass
56
+
57
+ @abstractmethod
58
+ def exec_script(
59
+ self,
60
+ skill_dir: Path,
61
+ script_path: str,
62
+ input_data: Dict[str, Any],
63
+ args: Optional[list] = None,
64
+ allow_network: Optional[bool] = None,
65
+ timeout: Optional[int] = None
66
+ ) -> ExecutionResult:
67
+ """
68
+ Execute a specific script directly.
69
+
70
+ Args:
71
+ skill_dir: Path to the skill directory
72
+ script_path: Relative path to the script
73
+ input_data: Input data for the script
74
+ args: Optional command line arguments
75
+ allow_network: Whether to allow network access
76
+ timeout: Execution timeout in seconds
77
+
78
+ Returns:
79
+ ExecutionResult with the output or error
80
+ """
81
+ pass
82
+
83
+ @property
84
+ @abstractmethod
85
+ def is_available(self) -> bool:
86
+ """Check if this sandbox executor is available and ready to use."""
87
+ pass
88
+
89
+ @property
90
+ @abstractmethod
91
+ def name(self) -> str:
92
+ """Return the name of this sandbox implementation."""
93
+ pass