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 +159 -0
- skilllite/analyzer.py +391 -0
- skilllite/builtin_tools.py +240 -0
- skilllite/cli.py +217 -0
- skilllite/core/__init__.py +65 -0
- skilllite/core/executor.py +182 -0
- skilllite/core/handler.py +332 -0
- skilllite/core/loops.py +770 -0
- skilllite/core/manager.py +507 -0
- skilllite/core/metadata.py +338 -0
- skilllite/core/prompt_builder.py +321 -0
- skilllite/core/registry.py +185 -0
- skilllite/core/skill_info.py +181 -0
- skilllite/core/tool_builder.py +338 -0
- skilllite/core/tools.py +253 -0
- skilllite/mcp/__init__.py +45 -0
- skilllite/mcp/server.py +734 -0
- skilllite/quick.py +420 -0
- skilllite/sandbox/__init__.py +36 -0
- skilllite/sandbox/base.py +93 -0
- skilllite/sandbox/config.py +229 -0
- skilllite/sandbox/skillbox/__init__.py +44 -0
- skilllite/sandbox/skillbox/binary.py +421 -0
- skilllite/sandbox/skillbox/executor.py +608 -0
- skilllite/sandbox/utils.py +77 -0
- skilllite/validation.py +137 -0
- skilllite-0.1.0.dist-info/METADATA +293 -0
- skilllite-0.1.0.dist-info/RECORD +32 -0
- skilllite-0.1.0.dist-info/WHEEL +5 -0
- skilllite-0.1.0.dist-info/entry_points.txt +3 -0
- skilllite-0.1.0.dist-info/licenses/LICENSE +21 -0
- skilllite-0.1.0.dist-info/top_level.txt +1 -0
skilllite/core/loops.py
ADDED
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agentic Loops - Continuous tool execution loops for LLM interactions.
|
|
3
|
+
|
|
4
|
+
This module provides a unified agentic loop implementation that supports
|
|
5
|
+
both OpenAI-compatible APIs and Claude's native API through a single interface.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any, List, Optional, TYPE_CHECKING, Dict, Callable
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .manager import SkillManager
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ApiFormat(Enum):
|
|
17
|
+
"""Supported API formats."""
|
|
18
|
+
OPENAI = "openai"
|
|
19
|
+
CLAUDE_NATIVE = "claude_native"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AgenticLoop:
|
|
23
|
+
"""
|
|
24
|
+
Unified agentic loop for LLM-tool interactions.
|
|
25
|
+
|
|
26
|
+
Supports both OpenAI-compatible APIs and Claude's native API through
|
|
27
|
+
a single interface. Handles the back-and-forth between the LLM and
|
|
28
|
+
tool execution until completion.
|
|
29
|
+
|
|
30
|
+
Works with:
|
|
31
|
+
- OpenAI (GPT-4, GPT-3.5, etc.)
|
|
32
|
+
- Azure OpenAI
|
|
33
|
+
- Anthropic Claude (both OpenAI-compatible and native)
|
|
34
|
+
- Ollama, vLLM, LMStudio
|
|
35
|
+
- DeepSeek, Qwen, Moonshot, etc.
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
```python
|
|
39
|
+
# OpenAI-compatible (default)
|
|
40
|
+
loop = AgenticLoop(manager, client, model="gpt-4")
|
|
41
|
+
|
|
42
|
+
# Claude native API
|
|
43
|
+
loop = AgenticLoop(manager, client, model="claude-3-opus",
|
|
44
|
+
api_format=ApiFormat.CLAUDE_NATIVE)
|
|
45
|
+
```
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
manager: "SkillManager",
|
|
51
|
+
client: Any,
|
|
52
|
+
model: str,
|
|
53
|
+
system_prompt: Optional[str] = None,
|
|
54
|
+
max_iterations: int = 10,
|
|
55
|
+
api_format: ApiFormat = ApiFormat.OPENAI,
|
|
56
|
+
custom_tool_handler: Optional[Callable] = None,
|
|
57
|
+
enable_task_planning: bool = True,
|
|
58
|
+
verbose: bool = True,
|
|
59
|
+
**kwargs
|
|
60
|
+
):
|
|
61
|
+
"""
|
|
62
|
+
Initialize the agentic loop.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
manager: SkillManager instance
|
|
66
|
+
client: LLM client (OpenAI or Anthropic)
|
|
67
|
+
model: Model name to use
|
|
68
|
+
system_prompt: Optional system prompt
|
|
69
|
+
max_iterations: Maximum number of iterations
|
|
70
|
+
api_format: API format to use (OPENAI or CLAUDE_NATIVE)
|
|
71
|
+
custom_tool_handler: Optional custom tool handler function
|
|
72
|
+
enable_task_planning: Whether to generate task list before execution
|
|
73
|
+
verbose: Whether to print detailed logs
|
|
74
|
+
**kwargs: Additional arguments passed to the LLM
|
|
75
|
+
"""
|
|
76
|
+
self.manager = manager
|
|
77
|
+
self.client = client
|
|
78
|
+
self.model = model
|
|
79
|
+
self.system_prompt = system_prompt
|
|
80
|
+
self.max_iterations = max_iterations
|
|
81
|
+
self.api_format = api_format
|
|
82
|
+
self.custom_tool_handler = custom_tool_handler
|
|
83
|
+
self.enable_task_planning = enable_task_planning
|
|
84
|
+
self.verbose = verbose
|
|
85
|
+
self.extra_kwargs = kwargs
|
|
86
|
+
self.task_list: List[Dict] = []
|
|
87
|
+
|
|
88
|
+
def _log(self, message: str) -> None:
|
|
89
|
+
"""Print log message if verbose mode is enabled."""
|
|
90
|
+
if self.verbose:
|
|
91
|
+
print(message)
|
|
92
|
+
|
|
93
|
+
def _get_execution_system_prompt(self) -> str:
|
|
94
|
+
"""
|
|
95
|
+
Generate the main execution system prompt for skill selection and file operations.
|
|
96
|
+
|
|
97
|
+
This prompt guides the LLM to:
|
|
98
|
+
1. Analyze tasks and select appropriate skills
|
|
99
|
+
2. Determine when to use built-in file read/write capabilities
|
|
100
|
+
3. Execute tasks step by step
|
|
101
|
+
"""
|
|
102
|
+
# Get available skills info
|
|
103
|
+
skills_info = []
|
|
104
|
+
for skill in self.manager.list_skills():
|
|
105
|
+
skill_desc = {
|
|
106
|
+
"name": skill.name,
|
|
107
|
+
"description": skill.description or "No description",
|
|
108
|
+
"executable": self.manager.is_executable(skill.name),
|
|
109
|
+
"path": str(skill.path) if hasattr(skill, 'path') else ""
|
|
110
|
+
}
|
|
111
|
+
skills_info.append(skill_desc)
|
|
112
|
+
|
|
113
|
+
skills_list_str = "\n".join([
|
|
114
|
+
f" - **{s['name']}**: {s['description']} {'[Executable]' if s['executable'] else '[Reference Only]'}"
|
|
115
|
+
for s in skills_info
|
|
116
|
+
])
|
|
117
|
+
|
|
118
|
+
# Determine skills directory
|
|
119
|
+
skills_dir = ".skills" # Default
|
|
120
|
+
if skills_info and skills_info[0].get("path"):
|
|
121
|
+
# Extract skills directory from first skill path
|
|
122
|
+
first_path = skills_info[0]["path"]
|
|
123
|
+
if ".skills" in first_path:
|
|
124
|
+
skills_dir = ".skills"
|
|
125
|
+
elif "skills" in first_path:
|
|
126
|
+
skills_dir = "skills"
|
|
127
|
+
|
|
128
|
+
return f"""You are an intelligent task execution assistant responsible for planning and executing tasks based on user requirements.
|
|
129
|
+
|
|
130
|
+
## Project Structure
|
|
131
|
+
|
|
132
|
+
**Skills Directory**: `{skills_dir}/`
|
|
133
|
+
|
|
134
|
+
All skills are stored in the `{skills_dir}/` directory, each skill is an independent subdirectory.
|
|
135
|
+
|
|
136
|
+
## Available Skills
|
|
137
|
+
|
|
138
|
+
{skills_list_str}
|
|
139
|
+
|
|
140
|
+
## Built-in File Operations
|
|
141
|
+
|
|
142
|
+
In addition to the above Skills, you have the following built-in file operation capabilities:
|
|
143
|
+
|
|
144
|
+
1. **read_file**: Read file content
|
|
145
|
+
- Used to view existing files, understand project structure, read configurations, etc.
|
|
146
|
+
- Parameter: `file_path` (string, file path)
|
|
147
|
+
|
|
148
|
+
2. **write_file**: Write/create files
|
|
149
|
+
- Used to create new files or modify existing file content
|
|
150
|
+
- Parameters: `file_path` (string, file path), `content` (string, file content)
|
|
151
|
+
|
|
152
|
+
3. **list_directory**: List directory contents
|
|
153
|
+
- Used to view directory structure, understand project layout
|
|
154
|
+
- Parameter: `directory_path` (string, directory path, e.g., "." or ".skills")
|
|
155
|
+
|
|
156
|
+
4. **file_exists**: Check if file exists
|
|
157
|
+
- Used to confirm file status before operations
|
|
158
|
+
- Parameter: `file_path` (string, file path)
|
|
159
|
+
|
|
160
|
+
**Note**: Parameter names must be used exactly as defined above, otherwise errors will occur.
|
|
161
|
+
|
|
162
|
+
## Task Execution Strategy
|
|
163
|
+
|
|
164
|
+
### 1. Task Analysis
|
|
165
|
+
- Carefully analyze user requirements and understand the final goal
|
|
166
|
+
- Break down complex tasks into executable sub-steps
|
|
167
|
+
- Identify the tools needed for each step (Skill or built-in file operations)
|
|
168
|
+
|
|
169
|
+
### 2. Tool Selection Principles
|
|
170
|
+
|
|
171
|
+
**When to prioritize Skills:**
|
|
172
|
+
- Tasks involve specialized domain processing (e.g., data analysis, text processing, HTTP requests)
|
|
173
|
+
- Skills have encapsulated complex business logic
|
|
174
|
+
- Need to call external services or APIs
|
|
175
|
+
|
|
176
|
+
**When to use built-in file operations:**
|
|
177
|
+
- Need to read existing files to understand content or structure
|
|
178
|
+
- Need to create new files or modify existing files
|
|
179
|
+
- Need to view directory structure to locate files
|
|
180
|
+
- Need to prepare input data before calling Skills
|
|
181
|
+
- Need to save output results after calling Skills
|
|
182
|
+
|
|
183
|
+
### 3. Execution Order
|
|
184
|
+
|
|
185
|
+
1. **Information Gathering Phase**: Use read_file, list_directory to understand current state
|
|
186
|
+
2. **Planning Phase**: Determine which Skills to use and operation order
|
|
187
|
+
3. **Execution Phase**: Call Skills and file operations in sequence
|
|
188
|
+
4. **Verification Phase**: Check execution results, make corrections if necessary
|
|
189
|
+
|
|
190
|
+
### 4. Error Handling
|
|
191
|
+
|
|
192
|
+
- If Skill execution fails, analyze the error cause and try to fix it
|
|
193
|
+
- If file operation fails, check if the path is correct
|
|
194
|
+
- When encountering unsolvable problems, explain the situation to the user and request help
|
|
195
|
+
|
|
196
|
+
## Output Guidelines
|
|
197
|
+
|
|
198
|
+
- After completing each task step, explicitly declare: "Task X completed"
|
|
199
|
+
- Provide clear execution process explanations
|
|
200
|
+
- Give a complete summary of execution results at the end
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
def _generate_task_list(self, user_message: str) -> List[Dict]:
|
|
204
|
+
"""Generate task list from user message using LLM."""
|
|
205
|
+
# Get available skills for context
|
|
206
|
+
skills_names = self.manager.skill_names()
|
|
207
|
+
skills_info = ", ".join(skills_names) if skills_names else "None"
|
|
208
|
+
|
|
209
|
+
planning_prompt = f"""You are a task planning assistant. Based on user requirements, determine whether tools are needed and generate a task list.
|
|
210
|
+
|
|
211
|
+
## Core Principle: Minimize Tool Usage
|
|
212
|
+
|
|
213
|
+
**Important**: Not all tasks require tools! Follow these principles:
|
|
214
|
+
|
|
215
|
+
1. **Complete simple tasks directly**: If a task can be completed directly by the LLM (such as writing, translation, Q&A, creative generation, etc.), return an empty task list `[]` and let the LLM answer directly
|
|
216
|
+
2. **Use tools only when necessary**: Only plan tool-using tasks when the task truly requires external capabilities (such as calculations, HTTP requests, file operations, data analysis, etc.)
|
|
217
|
+
|
|
218
|
+
## Examples of tasks that DON'T need tools (return empty list `[]`)
|
|
219
|
+
|
|
220
|
+
- Writing poems, articles, stories
|
|
221
|
+
- Translating text
|
|
222
|
+
- Answering knowledge-based questions
|
|
223
|
+
- Code explanation, code review suggestions
|
|
224
|
+
- Creative generation, brainstorming
|
|
225
|
+
- Summarizing, rewriting, polishing text
|
|
226
|
+
|
|
227
|
+
## Examples of tasks that NEED tools
|
|
228
|
+
|
|
229
|
+
- Precise calculations (use calculator)
|
|
230
|
+
- Sending HTTP requests (use http-request)
|
|
231
|
+
- Reading/writing files (use built-in file operations)
|
|
232
|
+
- Querying real-time weather (use weather)
|
|
233
|
+
- Creating new Skills (use skill-creator)
|
|
234
|
+
|
|
235
|
+
## Available Resources
|
|
236
|
+
|
|
237
|
+
**Available Skills**: {skills_info}
|
|
238
|
+
|
|
239
|
+
**Built-in capabilities**: read_file (read files), write_file (write files), list_directory (list directory), file_exists (check file existence)
|
|
240
|
+
|
|
241
|
+
## Planning Principles
|
|
242
|
+
|
|
243
|
+
1. **Task decomposition**: Break down user requirements into specific, executable steps
|
|
244
|
+
2. **Tool matching**: Select appropriate tools for each step (Skill or built-in file operations)
|
|
245
|
+
3. **Dependency order**: Ensure tasks are arranged in correct dependency order
|
|
246
|
+
4. **Verifiability**: Each task should have clear completion criteria
|
|
247
|
+
|
|
248
|
+
## Output Format
|
|
249
|
+
|
|
250
|
+
Must return pure JSON format, no other text.
|
|
251
|
+
Task list is an array, each task contains:
|
|
252
|
+
- id: Task ID (number)
|
|
253
|
+
- description: Task description (concise and clear, stating what to do)
|
|
254
|
+
- tool_hint: Suggested tool (skill name or "file_operation" or "analysis")
|
|
255
|
+
- completed: Whether completed (initially false)
|
|
256
|
+
|
|
257
|
+
Example format:
|
|
258
|
+
[
|
|
259
|
+
{{"id": 1, "description": "Use list_directory to view project structure", "tool_hint": "file_operation", "completed": false}},
|
|
260
|
+
{{"id": 2, "description": "Use skill-creator to create basic skill structure", "tool_hint": "skill-creator", "completed": false}},
|
|
261
|
+
{{"id": 3, "description": "Use write_file to write main skill code", "tool_hint": "file_operation", "completed": false}},
|
|
262
|
+
{{"id": 4, "description": "Verify the created skill is correct", "tool_hint": "analysis", "completed": false}}
|
|
263
|
+
]
|
|
264
|
+
- If task can be completed directly by LLM, return: `[]`
|
|
265
|
+
- If tools are needed, return task array, each task contains:
|
|
266
|
+
- id: Task ID (number)
|
|
267
|
+
- description: Task description
|
|
268
|
+
- tool_hint: Suggested tool (skill name or "file_operation")
|
|
269
|
+
- completed: false
|
|
270
|
+
|
|
271
|
+
Example 1 - Simple task (writing poetry):
|
|
272
|
+
User request: "Write a poem praising spring"
|
|
273
|
+
Return: []
|
|
274
|
+
|
|
275
|
+
Example 2 - Task requiring tools:
|
|
276
|
+
User request: "Calculate 123 * 456 + 789 for me"
|
|
277
|
+
Return: [{{"id": 1, "description": "Use calculator to compute expression", "tool_hint": "calculator", "completed": false}}]
|
|
278
|
+
|
|
279
|
+
Return only JSON, no other content."""
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
if self.api_format == ApiFormat.OPENAI:
|
|
283
|
+
response = self.client.chat.completions.create(
|
|
284
|
+
model=self.model,
|
|
285
|
+
messages=[
|
|
286
|
+
{"role": "system", "content": planning_prompt},
|
|
287
|
+
{"role": "user", "content": f"User request:\n{user_message}\n\nPlease generate task list:"}
|
|
288
|
+
],
|
|
289
|
+
temperature=0.3
|
|
290
|
+
)
|
|
291
|
+
result = response.choices[0].message.content.strip()
|
|
292
|
+
else: # CLAUDE_NATIVE
|
|
293
|
+
response = self.client.messages.create(
|
|
294
|
+
model=self.model,
|
|
295
|
+
max_tokens=2048,
|
|
296
|
+
system=planning_prompt,
|
|
297
|
+
messages=[
|
|
298
|
+
{"role": "user", "content": f"User request:\n{user_message}\n\nPlease generate task list:"}
|
|
299
|
+
]
|
|
300
|
+
)
|
|
301
|
+
result = response.content[0].text.strip()
|
|
302
|
+
|
|
303
|
+
# Parse JSON
|
|
304
|
+
if result.startswith("```json"):
|
|
305
|
+
result = result[7:]
|
|
306
|
+
if result.startswith("```"):
|
|
307
|
+
result = result[3:]
|
|
308
|
+
if result.endswith("```"):
|
|
309
|
+
result = result[:-3]
|
|
310
|
+
|
|
311
|
+
task_list = json.loads(result.strip())
|
|
312
|
+
|
|
313
|
+
for task in task_list:
|
|
314
|
+
if "completed" not in task:
|
|
315
|
+
task["completed"] = False
|
|
316
|
+
|
|
317
|
+
# Check if any task involves creating a skill using skill-creator
|
|
318
|
+
# If so, automatically add a task to write SKILL.md content (if not already present)
|
|
319
|
+
has_skill_creation = any(
|
|
320
|
+
"skill-creator" in task.get("description", "").lower() or
|
|
321
|
+
"skill-creator" in task.get("tool_hint", "").lower()
|
|
322
|
+
for task in task_list
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# Check if SKILL.md writing task already exists
|
|
326
|
+
has_skillmd_task = any(
|
|
327
|
+
"skill.md" in task.get("description", "").lower() or
|
|
328
|
+
"skill.md" in task.get("tool_hint", "").lower()
|
|
329
|
+
for task in task_list
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
if has_skill_creation and not has_skillmd_task:
|
|
333
|
+
# Add task to write SKILL.md actual content
|
|
334
|
+
max_id = max((task["id"] for task in task_list), default=0)
|
|
335
|
+
new_task = {
|
|
336
|
+
"id": max_id + 1,
|
|
337
|
+
"description": "Use write_file to write actual SKILL.md content (skill description, usage, parameter documentation, etc.)",
|
|
338
|
+
"tool_hint": "file_operation",
|
|
339
|
+
"completed": False
|
|
340
|
+
}
|
|
341
|
+
task_list.append(new_task)
|
|
342
|
+
self._log(f"\nš” Detected skill creation task, automatically adding SKILL.md writing task")
|
|
343
|
+
|
|
344
|
+
self._log(f"\nš Generated task list ({len(task_list)} tasks):")
|
|
345
|
+
for task in task_list:
|
|
346
|
+
status = "ā
" if task["completed"] else "ā¬"
|
|
347
|
+
self._log(f" {status} [{task['id']}] {task['description']}")
|
|
348
|
+
|
|
349
|
+
return task_list
|
|
350
|
+
|
|
351
|
+
except Exception as e:
|
|
352
|
+
self._log(f"ā ļø Failed to generate task list: {e}")
|
|
353
|
+
return [{"id": 1, "description": user_message, "completed": False}]
|
|
354
|
+
|
|
355
|
+
def _update_task_list(self, completed_task_id: Optional[int] = None) -> None:
|
|
356
|
+
"""Update task list display."""
|
|
357
|
+
if completed_task_id is not None:
|
|
358
|
+
for task in self.task_list:
|
|
359
|
+
if task["id"] == completed_task_id:
|
|
360
|
+
task["completed"] = True
|
|
361
|
+
break
|
|
362
|
+
|
|
363
|
+
completed = sum(1 for t in self.task_list if t["completed"])
|
|
364
|
+
self._log(f"\nš Current task progress ({completed}/{len(self.task_list)}):")
|
|
365
|
+
for task in self.task_list:
|
|
366
|
+
status = "ā
" if task["completed"] else "ā¬"
|
|
367
|
+
self._log(f" {status} [{task['id']}] {task['description']}")
|
|
368
|
+
|
|
369
|
+
def _check_all_tasks_completed(self) -> bool:
|
|
370
|
+
"""Check if all tasks are completed."""
|
|
371
|
+
return all(task["completed"] for task in self.task_list)
|
|
372
|
+
|
|
373
|
+
def _check_task_completion_in_content(self, content: str) -> Optional[int]:
|
|
374
|
+
"""Check if any task was completed based on LLM response content."""
|
|
375
|
+
if not content:
|
|
376
|
+
return None
|
|
377
|
+
content_lower = content.lower()
|
|
378
|
+
for task in self.task_list:
|
|
379
|
+
if not task["completed"]:
|
|
380
|
+
if f"task {task['id']} completed" in content_lower or f"task{task['id']} completed" in content_lower:
|
|
381
|
+
return task["id"]
|
|
382
|
+
return None
|
|
383
|
+
|
|
384
|
+
def _get_task_system_prompt(self) -> str:
|
|
385
|
+
"""Generate system prompt with task list and execution guidance."""
|
|
386
|
+
# Get the main execution system prompt
|
|
387
|
+
execution_prompt = self._get_execution_system_prompt()
|
|
388
|
+
|
|
389
|
+
# Format task list
|
|
390
|
+
task_list_str = json.dumps(self.task_list, ensure_ascii=False, indent=2)
|
|
391
|
+
current_task = next((t for t in self.task_list if not t["completed"]), None)
|
|
392
|
+
current_task_info = ""
|
|
393
|
+
if current_task:
|
|
394
|
+
tool_hint = current_task.get("tool_hint", "")
|
|
395
|
+
hint_str = f"(Suggested tool: {tool_hint})" if tool_hint else ""
|
|
396
|
+
current_task_info = f"\n\nšÆ **Current task to execute**: Task {current_task['id']} - {current_task['description']} {hint_str}"
|
|
397
|
+
|
|
398
|
+
task_rules = f"""
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## Current Task List
|
|
402
|
+
|
|
403
|
+
{task_list_str}
|
|
404
|
+
|
|
405
|
+
## Execution Rules
|
|
406
|
+
|
|
407
|
+
1. **Strict sequential execution**: Must execute tasks in order, do not skip tasks
|
|
408
|
+
2. **Focus on current task**: Focus only on executing the current task at a time
|
|
409
|
+
3. **Explicit completion declaration**: After completing a task, must explicitly declare in response: "Task X completed" (X is task ID)
|
|
410
|
+
4. **Sequential progression**: Can only start next task after current task is completed
|
|
411
|
+
5. **Avoid repetition**: Do not repeat already completed tasks
|
|
412
|
+
6. **Multi-step tasks**: If a task requires multiple tool calls to complete, continue calling tools until the task is truly completed before declaring
|
|
413
|
+
{current_task_info}
|
|
414
|
+
|
|
415
|
+
ā ļø **Important**: You must explicitly declare after completing each task so the system can track progress and know when to end.
|
|
416
|
+
"""
|
|
417
|
+
|
|
418
|
+
return execution_prompt + task_rules
|
|
419
|
+
|
|
420
|
+
def _get_skill_docs_for_tools(self, tool_calls: List[Any]) -> Optional[str]:
|
|
421
|
+
"""
|
|
422
|
+
Get full SKILL.md documentation for the tools being called.
|
|
423
|
+
|
|
424
|
+
This implements progressive disclosure - the LLM only gets the full
|
|
425
|
+
documentation when it decides to use a specific skill.
|
|
426
|
+
|
|
427
|
+
Tracks which skills have already been documented to avoid duplicates.
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
tool_calls: List of tool calls from LLM response
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
Formatted string with full SKILL.md content for each skill,
|
|
434
|
+
or None if no new skill documentation is available
|
|
435
|
+
"""
|
|
436
|
+
# Initialize the set to track documented skills if not exists
|
|
437
|
+
if not hasattr(self, '_documented_skills'):
|
|
438
|
+
self._documented_skills = set()
|
|
439
|
+
|
|
440
|
+
docs_parts = []
|
|
441
|
+
|
|
442
|
+
for tc in tool_calls:
|
|
443
|
+
tool_name = tc.function.name if hasattr(tc, 'function') else tc.get('function', {}).get('name', '')
|
|
444
|
+
|
|
445
|
+
# Skip built-in tools (read_file, write_file, etc.)
|
|
446
|
+
if tool_name in ['read_file', 'write_file', 'list_directory', 'file_exists']:
|
|
447
|
+
continue
|
|
448
|
+
|
|
449
|
+
# Skip if already documented in this session
|
|
450
|
+
if tool_name in self._documented_skills:
|
|
451
|
+
continue
|
|
452
|
+
|
|
453
|
+
# Get skill info - handle both regular skills and multi-script tools
|
|
454
|
+
skill_info = self.manager.get_skill(tool_name)
|
|
455
|
+
if not skill_info:
|
|
456
|
+
# Try to get parent skill for multi-script tools (e.g., "skill-creator:init-skill")
|
|
457
|
+
if ':' in tool_name:
|
|
458
|
+
parent_name = tool_name.split(':')[0]
|
|
459
|
+
skill_info = self.manager.get_skill(parent_name)
|
|
460
|
+
# Mark both the parent and the specific tool as documented
|
|
461
|
+
if skill_info:
|
|
462
|
+
self._documented_skills.add(parent_name)
|
|
463
|
+
|
|
464
|
+
if skill_info:
|
|
465
|
+
full_content = skill_info.get_full_content()
|
|
466
|
+
if full_content:
|
|
467
|
+
# Mark this skill as documented
|
|
468
|
+
self._documented_skills.add(tool_name)
|
|
469
|
+
|
|
470
|
+
docs_parts.append(f"""
|
|
471
|
+
## š Skill Documentation: {tool_name}
|
|
472
|
+
|
|
473
|
+
Below is the complete documentation for `{tool_name}`. Please read the documentation to understand how to use this tool correctly:
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
{full_content}
|
|
477
|
+
---
|
|
478
|
+
""")
|
|
479
|
+
|
|
480
|
+
if docs_parts:
|
|
481
|
+
header = """
|
|
482
|
+
# š Skill Detailed Documentation
|
|
483
|
+
|
|
484
|
+
You are calling the following Skills. Here is their complete documentation. Please read carefully to understand:
|
|
485
|
+
1. The functionality and purpose of this Skill
|
|
486
|
+
2. What parameters need to be passed
|
|
487
|
+
3. The format and type of parameters
|
|
488
|
+
4. Usage examples
|
|
489
|
+
|
|
490
|
+
Based on the documentation, call the tools with correct parameters.
|
|
491
|
+
"""
|
|
492
|
+
return header + "\n".join(docs_parts)
|
|
493
|
+
|
|
494
|
+
return None
|
|
495
|
+
|
|
496
|
+
# ==================== OpenAI-compatible API ====================
|
|
497
|
+
|
|
498
|
+
def _run_openai(
|
|
499
|
+
self,
|
|
500
|
+
user_message: str,
|
|
501
|
+
allow_network: Optional[bool] = None,
|
|
502
|
+
timeout: Optional[int] = None
|
|
503
|
+
) -> Any:
|
|
504
|
+
"""Run loop using OpenAI-compatible API."""
|
|
505
|
+
messages = []
|
|
506
|
+
|
|
507
|
+
if self.system_prompt:
|
|
508
|
+
messages.append({"role": "system", "content": self.system_prompt})
|
|
509
|
+
|
|
510
|
+
if self.enable_task_planning and self.task_list:
|
|
511
|
+
messages.append({"role": "system", "content": self._get_task_system_prompt()})
|
|
512
|
+
|
|
513
|
+
messages.append({"role": "user", "content": user_message})
|
|
514
|
+
|
|
515
|
+
tools = self.manager.get_tools()
|
|
516
|
+
response = None
|
|
517
|
+
|
|
518
|
+
for iteration in range(self.max_iterations):
|
|
519
|
+
self._log(f"\nš Iteration #{iteration + 1}/{self.max_iterations}")
|
|
520
|
+
|
|
521
|
+
self._log("ā³ Calling LLM...")
|
|
522
|
+
response = self.client.chat.completions.create(
|
|
523
|
+
model=self.model,
|
|
524
|
+
messages=messages,
|
|
525
|
+
tools=tools if tools else None,
|
|
526
|
+
**self.extra_kwargs
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
message = response.choices[0].message
|
|
530
|
+
finish_reason = response.choices[0].finish_reason
|
|
531
|
+
|
|
532
|
+
self._log(f"ā
LLM response completed (finish_reason: {finish_reason})")
|
|
533
|
+
|
|
534
|
+
# No tool calls
|
|
535
|
+
if not message.tool_calls:
|
|
536
|
+
self._log("š LLM did not call any tools")
|
|
537
|
+
|
|
538
|
+
if self.enable_task_planning:
|
|
539
|
+
completed_id = self._check_task_completion_in_content(message.content)
|
|
540
|
+
if completed_id:
|
|
541
|
+
self._update_task_list(completed_id)
|
|
542
|
+
|
|
543
|
+
if self._check_all_tasks_completed():
|
|
544
|
+
self._log("šÆ All tasks completed, ending iteration")
|
|
545
|
+
return response
|
|
546
|
+
else:
|
|
547
|
+
return response
|
|
548
|
+
|
|
549
|
+
# Handle tool calls
|
|
550
|
+
self._log(f"\nš§ LLM decided to call {len(message.tool_calls)} tools:")
|
|
551
|
+
for idx, tc in enumerate(message.tool_calls, 1):
|
|
552
|
+
self._log(f" {idx}. {tc.function.name}")
|
|
553
|
+
self._log(f" Arguments: {tc.function.arguments}")
|
|
554
|
+
|
|
555
|
+
# Get full SKILL.md content for tools that haven't been documented yet
|
|
556
|
+
skill_docs = self._get_skill_docs_for_tools(message.tool_calls)
|
|
557
|
+
|
|
558
|
+
# If we have new skill docs, inject them into the prompt first
|
|
559
|
+
# and ask LLM to re-call with correct parameters
|
|
560
|
+
if skill_docs:
|
|
561
|
+
self._log(f"\nš Injecting Skill documentation into prompt...")
|
|
562
|
+
messages.append({
|
|
563
|
+
"role": "system",
|
|
564
|
+
"content": skill_docs
|
|
565
|
+
})
|
|
566
|
+
messages.append({
|
|
567
|
+
"role": "user",
|
|
568
|
+
"content": "Please re-call the tools with correct parameters based on the complete Skill documentation above."
|
|
569
|
+
})
|
|
570
|
+
continue
|
|
571
|
+
|
|
572
|
+
messages.append(message)
|
|
573
|
+
|
|
574
|
+
self._log(f"\nāļø Executing tools...")
|
|
575
|
+
if self.custom_tool_handler:
|
|
576
|
+
tool_results = self.custom_tool_handler(
|
|
577
|
+
response, self.manager, allow_network, timeout
|
|
578
|
+
)
|
|
579
|
+
else:
|
|
580
|
+
tool_results = self.manager.handle_tool_calls(
|
|
581
|
+
response, allow_network=allow_network, timeout=timeout
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
self._log(f"\nš Tool execution results:")
|
|
585
|
+
for idx, (result, tc) in enumerate(zip(tool_results, message.tool_calls), 1):
|
|
586
|
+
output = result.content
|
|
587
|
+
if len(output) > 500:
|
|
588
|
+
output = output[:500] + "... (truncated)"
|
|
589
|
+
self._log(f" {idx}. {tc.function.name}")
|
|
590
|
+
self._log(f" Result: {output}")
|
|
591
|
+
|
|
592
|
+
for result in tool_results:
|
|
593
|
+
messages.append(result.to_openai_format())
|
|
594
|
+
|
|
595
|
+
# Check task completion
|
|
596
|
+
if self.enable_task_planning:
|
|
597
|
+
if message.content:
|
|
598
|
+
completed_id = self._check_task_completion_in_content(message.content)
|
|
599
|
+
if completed_id:
|
|
600
|
+
self._update_task_list(completed_id)
|
|
601
|
+
|
|
602
|
+
if self._check_all_tasks_completed():
|
|
603
|
+
self._log("šÆ All tasks completed, ending iteration")
|
|
604
|
+
final_response = self.client.chat.completions.create(
|
|
605
|
+
model=self.model, messages=messages, tools=None
|
|
606
|
+
)
|
|
607
|
+
return final_response
|
|
608
|
+
|
|
609
|
+
# Update task focus
|
|
610
|
+
current_task = next((t for t in self.task_list if not t["completed"]), None)
|
|
611
|
+
if current_task:
|
|
612
|
+
task_list_str = json.dumps(self.task_list, ensure_ascii=False, indent=2)
|
|
613
|
+
messages.append({
|
|
614
|
+
"role": "system",
|
|
615
|
+
"content": f"Task progress update:\n{task_list_str}\n\nCurrent task to execute: Task {current_task['id']} - {current_task['description']}\n\nPlease continue to focus on completing the current task."
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
self._log(f"\nā ļø Reached maximum iterations ({self.max_iterations}), stopping execution")
|
|
619
|
+
return response
|
|
620
|
+
|
|
621
|
+
# ==================== Claude Native API ====================
|
|
622
|
+
|
|
623
|
+
def _run_claude_native(
|
|
624
|
+
self,
|
|
625
|
+
user_message: str,
|
|
626
|
+
allow_network: Optional[bool] = None,
|
|
627
|
+
timeout: Optional[int] = None
|
|
628
|
+
) -> Any:
|
|
629
|
+
"""Run loop using Claude's native API."""
|
|
630
|
+
messages = [{"role": "user", "content": user_message}]
|
|
631
|
+
tools = self.manager.get_tools_for_claude_native()
|
|
632
|
+
|
|
633
|
+
# Build system prompt
|
|
634
|
+
system = self.system_prompt or ""
|
|
635
|
+
if self.enable_task_planning and self.task_list:
|
|
636
|
+
system = (system + "\n\n" if system else "") + self._get_task_system_prompt()
|
|
637
|
+
|
|
638
|
+
response = None
|
|
639
|
+
|
|
640
|
+
for iteration in range(self.max_iterations):
|
|
641
|
+
self._log(f"\nš Iteration #{iteration + 1}/{self.max_iterations}")
|
|
642
|
+
|
|
643
|
+
self._log("ā³ Calling LLM...")
|
|
644
|
+
|
|
645
|
+
kwargs = {
|
|
646
|
+
"model": self.model,
|
|
647
|
+
"max_tokens": self.extra_kwargs.get("max_tokens", 4096),
|
|
648
|
+
"tools": tools,
|
|
649
|
+
"messages": messages,
|
|
650
|
+
**{k: v for k, v in self.extra_kwargs.items() if k != "max_tokens"}
|
|
651
|
+
}
|
|
652
|
+
if system:
|
|
653
|
+
kwargs["system"] = system
|
|
654
|
+
|
|
655
|
+
response = self.client.messages.create(**kwargs)
|
|
656
|
+
|
|
657
|
+
self._log(f"ā
LLM response completed (stop_reason: {response.stop_reason})")
|
|
658
|
+
|
|
659
|
+
# No tool use
|
|
660
|
+
if response.stop_reason != "tool_use":
|
|
661
|
+
self._log("š LLM did not call any tools")
|
|
662
|
+
|
|
663
|
+
if self.enable_task_planning:
|
|
664
|
+
# Extract text content
|
|
665
|
+
text_content = ""
|
|
666
|
+
for block in response.content:
|
|
667
|
+
if hasattr(block, 'text'):
|
|
668
|
+
text_content += block.text
|
|
669
|
+
|
|
670
|
+
completed_id = self._check_task_completion_in_content(text_content)
|
|
671
|
+
if completed_id:
|
|
672
|
+
self._update_task_list(completed_id)
|
|
673
|
+
|
|
674
|
+
if self._check_all_tasks_completed():
|
|
675
|
+
self._log("šÆ All tasks completed, ending iteration")
|
|
676
|
+
return response
|
|
677
|
+
else:
|
|
678
|
+
self._log("ā³ There are still pending tasks, continuing execution...")
|
|
679
|
+
messages.append({"role": "assistant", "content": response.content})
|
|
680
|
+
messages.append({"role": "user", "content": "Please continue to complete the remaining tasks."})
|
|
681
|
+
continue
|
|
682
|
+
else:
|
|
683
|
+
return response
|
|
684
|
+
|
|
685
|
+
# Handle tool calls
|
|
686
|
+
tool_use_blocks = [b for b in response.content if hasattr(b, 'type') and b.type == 'tool_use']
|
|
687
|
+
self._log(f"\nš§ LLM decided to call {len(tool_use_blocks)} tools:")
|
|
688
|
+
for idx, block in enumerate(tool_use_blocks, 1):
|
|
689
|
+
self._log(f" {idx}. {block.name}")
|
|
690
|
+
self._log(f" Arguments: {json.dumps(block.input, ensure_ascii=False)}")
|
|
691
|
+
|
|
692
|
+
messages.append({"role": "assistant", "content": response.content})
|
|
693
|
+
|
|
694
|
+
self._log(f"\nāļø Executing tools...")
|
|
695
|
+
tool_results = self.manager.handle_tool_calls_claude_native(
|
|
696
|
+
response, allow_network=allow_network, timeout=timeout
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
self._log(f"\nš Tool execution results:")
|
|
700
|
+
for idx, result in enumerate(tool_results, 1):
|
|
701
|
+
output = result.content
|
|
702
|
+
if len(output) > 500:
|
|
703
|
+
output = output[:500] + "... (truncated)"
|
|
704
|
+
self._log(f" {idx}. Result: {output}")
|
|
705
|
+
|
|
706
|
+
formatted_results = self.manager.format_tool_results_claude_native(tool_results)
|
|
707
|
+
messages.append({"role": "user", "content": formatted_results})
|
|
708
|
+
|
|
709
|
+
# Check task completion
|
|
710
|
+
if self.enable_task_planning:
|
|
711
|
+
text_content = ""
|
|
712
|
+
for block in response.content:
|
|
713
|
+
if hasattr(block, 'text'):
|
|
714
|
+
text_content += block.text
|
|
715
|
+
|
|
716
|
+
completed_id = self._check_task_completion_in_content(text_content)
|
|
717
|
+
if completed_id:
|
|
718
|
+
self._update_task_list(completed_id)
|
|
719
|
+
|
|
720
|
+
if self._check_all_tasks_completed():
|
|
721
|
+
self._log("šÆ All tasks completed, ending iteration")
|
|
722
|
+
final_response = self.client.messages.create(
|
|
723
|
+
model=self.model,
|
|
724
|
+
max_tokens=self.extra_kwargs.get("max_tokens", 4096),
|
|
725
|
+
system=system if system else None,
|
|
726
|
+
messages=messages
|
|
727
|
+
)
|
|
728
|
+
return final_response
|
|
729
|
+
|
|
730
|
+
self._log(f"\nā ļø Reached maximum iterations ({self.max_iterations}), stopping execution")
|
|
731
|
+
return response
|
|
732
|
+
|
|
733
|
+
# ==================== Public API ====================
|
|
734
|
+
|
|
735
|
+
def run(
|
|
736
|
+
self,
|
|
737
|
+
user_message: str,
|
|
738
|
+
allow_network: Optional[bool] = None,
|
|
739
|
+
timeout: Optional[int] = None
|
|
740
|
+
) -> Any:
|
|
741
|
+
"""
|
|
742
|
+
Run the agentic loop until completion.
|
|
743
|
+
|
|
744
|
+
Args:
|
|
745
|
+
user_message: The user's message
|
|
746
|
+
allow_network: Override default network setting for skill execution
|
|
747
|
+
timeout: Execution timeout per tool call in seconds
|
|
748
|
+
|
|
749
|
+
Returns:
|
|
750
|
+
The final LLM response
|
|
751
|
+
"""
|
|
752
|
+
# Generate task list if enabled
|
|
753
|
+
if self.enable_task_planning:
|
|
754
|
+
self.task_list = self._generate_task_list(user_message)
|
|
755
|
+
|
|
756
|
+
# If task list is empty, the task can be completed by LLM directly
|
|
757
|
+
# Disable task planning mode for this run
|
|
758
|
+
if not self.task_list:
|
|
759
|
+
self._log("\nš” Task can be completed directly by LLM, no tools needed")
|
|
760
|
+
self.enable_task_planning = False
|
|
761
|
+
|
|
762
|
+
# Dispatch to appropriate implementation
|
|
763
|
+
if self.api_format == ApiFormat.OPENAI:
|
|
764
|
+
return self._run_openai(user_message, allow_network, timeout)
|
|
765
|
+
else:
|
|
766
|
+
return self._run_claude_native(user_message, allow_network, timeout)
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
# Backward compatibility alias
|
|
770
|
+
AgenticLoopClaudeNative = AgenticLoop
|