kweaver-dolphin 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.
- DolphinLanguageSDK/__init__.py +58 -0
- dolphin/__init__.py +62 -0
- dolphin/cli/__init__.py +20 -0
- dolphin/cli/args/__init__.py +9 -0
- dolphin/cli/args/parser.py +567 -0
- dolphin/cli/builtin_agents/__init__.py +22 -0
- dolphin/cli/commands/__init__.py +4 -0
- dolphin/cli/interrupt/__init__.py +8 -0
- dolphin/cli/interrupt/handler.py +205 -0
- dolphin/cli/interrupt/keyboard.py +82 -0
- dolphin/cli/main.py +49 -0
- dolphin/cli/multimodal/__init__.py +34 -0
- dolphin/cli/multimodal/clipboard.py +327 -0
- dolphin/cli/multimodal/handler.py +249 -0
- dolphin/cli/multimodal/image_processor.py +214 -0
- dolphin/cli/multimodal/input_parser.py +149 -0
- dolphin/cli/runner/__init__.py +8 -0
- dolphin/cli/runner/runner.py +989 -0
- dolphin/cli/ui/__init__.py +10 -0
- dolphin/cli/ui/console.py +2795 -0
- dolphin/cli/ui/input.py +340 -0
- dolphin/cli/ui/layout.py +425 -0
- dolphin/cli/ui/stream_renderer.py +302 -0
- dolphin/cli/utils/__init__.py +8 -0
- dolphin/cli/utils/helpers.py +135 -0
- dolphin/cli/utils/version.py +49 -0
- dolphin/core/__init__.py +107 -0
- dolphin/core/agent/__init__.py +10 -0
- dolphin/core/agent/agent_state.py +69 -0
- dolphin/core/agent/base_agent.py +970 -0
- dolphin/core/code_block/__init__.py +0 -0
- dolphin/core/code_block/agent_init_block.py +0 -0
- dolphin/core/code_block/assign_block.py +98 -0
- dolphin/core/code_block/basic_code_block.py +1865 -0
- dolphin/core/code_block/explore_block.py +1327 -0
- dolphin/core/code_block/explore_block_v2.py +712 -0
- dolphin/core/code_block/explore_strategy.py +672 -0
- dolphin/core/code_block/judge_block.py +220 -0
- dolphin/core/code_block/prompt_block.py +32 -0
- dolphin/core/code_block/skill_call_deduplicator.py +291 -0
- dolphin/core/code_block/tool_block.py +129 -0
- dolphin/core/common/__init__.py +17 -0
- dolphin/core/common/constants.py +176 -0
- dolphin/core/common/enums.py +1173 -0
- dolphin/core/common/exceptions.py +133 -0
- dolphin/core/common/multimodal.py +539 -0
- dolphin/core/common/object_type.py +165 -0
- dolphin/core/common/output_format.py +432 -0
- dolphin/core/common/types.py +36 -0
- dolphin/core/config/__init__.py +16 -0
- dolphin/core/config/global_config.py +1289 -0
- dolphin/core/config/ontology_config.py +133 -0
- dolphin/core/context/__init__.py +12 -0
- dolphin/core/context/context.py +1580 -0
- dolphin/core/context/context_manager.py +161 -0
- dolphin/core/context/var_output.py +82 -0
- dolphin/core/context/variable_pool.py +356 -0
- dolphin/core/context_engineer/__init__.py +41 -0
- dolphin/core/context_engineer/config/__init__.py +5 -0
- dolphin/core/context_engineer/config/settings.py +402 -0
- dolphin/core/context_engineer/core/__init__.py +7 -0
- dolphin/core/context_engineer/core/budget_manager.py +327 -0
- dolphin/core/context_engineer/core/context_assembler.py +583 -0
- dolphin/core/context_engineer/core/context_manager.py +637 -0
- dolphin/core/context_engineer/core/tokenizer_service.py +260 -0
- dolphin/core/context_engineer/example/incremental_example.py +267 -0
- dolphin/core/context_engineer/example/traditional_example.py +334 -0
- dolphin/core/context_engineer/services/__init__.py +5 -0
- dolphin/core/context_engineer/services/compressor.py +399 -0
- dolphin/core/context_engineer/utils/__init__.py +6 -0
- dolphin/core/context_engineer/utils/context_utils.py +441 -0
- dolphin/core/context_engineer/utils/message_formatter.py +270 -0
- dolphin/core/context_engineer/utils/token_utils.py +139 -0
- dolphin/core/coroutine/__init__.py +15 -0
- dolphin/core/coroutine/context_snapshot.py +154 -0
- dolphin/core/coroutine/context_snapshot_profile.py +922 -0
- dolphin/core/coroutine/context_snapshot_store.py +268 -0
- dolphin/core/coroutine/execution_frame.py +145 -0
- dolphin/core/coroutine/execution_state_registry.py +161 -0
- dolphin/core/coroutine/resume_handle.py +101 -0
- dolphin/core/coroutine/step_result.py +101 -0
- dolphin/core/executor/__init__.py +18 -0
- dolphin/core/executor/debug_controller.py +630 -0
- dolphin/core/executor/dolphin_executor.py +1063 -0
- dolphin/core/executor/executor.py +624 -0
- dolphin/core/flags/__init__.py +27 -0
- dolphin/core/flags/definitions.py +49 -0
- dolphin/core/flags/manager.py +113 -0
- dolphin/core/hook/__init__.py +95 -0
- dolphin/core/hook/expression_evaluator.py +499 -0
- dolphin/core/hook/hook_dispatcher.py +380 -0
- dolphin/core/hook/hook_types.py +248 -0
- dolphin/core/hook/isolated_variable_pool.py +284 -0
- dolphin/core/interfaces.py +53 -0
- dolphin/core/llm/__init__.py +0 -0
- dolphin/core/llm/llm.py +495 -0
- dolphin/core/llm/llm_call.py +100 -0
- dolphin/core/llm/llm_client.py +1285 -0
- dolphin/core/llm/message_sanitizer.py +120 -0
- dolphin/core/logging/__init__.py +20 -0
- dolphin/core/logging/logger.py +526 -0
- dolphin/core/message/__init__.py +8 -0
- dolphin/core/message/compressor.py +749 -0
- dolphin/core/parser/__init__.py +8 -0
- dolphin/core/parser/parser.py +405 -0
- dolphin/core/runtime/__init__.py +10 -0
- dolphin/core/runtime/runtime_graph.py +926 -0
- dolphin/core/runtime/runtime_instance.py +446 -0
- dolphin/core/skill/__init__.py +14 -0
- dolphin/core/skill/context_retention.py +157 -0
- dolphin/core/skill/skill_function.py +686 -0
- dolphin/core/skill/skill_matcher.py +282 -0
- dolphin/core/skill/skillkit.py +700 -0
- dolphin/core/skill/skillset.py +72 -0
- dolphin/core/trajectory/__init__.py +10 -0
- dolphin/core/trajectory/recorder.py +189 -0
- dolphin/core/trajectory/trajectory.py +522 -0
- dolphin/core/utils/__init__.py +9 -0
- dolphin/core/utils/cache_kv.py +212 -0
- dolphin/core/utils/tools.py +340 -0
- dolphin/lib/__init__.py +93 -0
- dolphin/lib/debug/__init__.py +8 -0
- dolphin/lib/debug/visualizer.py +409 -0
- dolphin/lib/memory/__init__.py +28 -0
- dolphin/lib/memory/async_processor.py +220 -0
- dolphin/lib/memory/llm_calls.py +195 -0
- dolphin/lib/memory/manager.py +78 -0
- dolphin/lib/memory/sandbox.py +46 -0
- dolphin/lib/memory/storage.py +245 -0
- dolphin/lib/memory/utils.py +51 -0
- dolphin/lib/ontology/__init__.py +12 -0
- dolphin/lib/ontology/basic/__init__.py +0 -0
- dolphin/lib/ontology/basic/base.py +102 -0
- dolphin/lib/ontology/basic/concept.py +130 -0
- dolphin/lib/ontology/basic/object.py +11 -0
- dolphin/lib/ontology/basic/relation.py +63 -0
- dolphin/lib/ontology/datasource/__init__.py +27 -0
- dolphin/lib/ontology/datasource/datasource.py +66 -0
- dolphin/lib/ontology/datasource/oracle_datasource.py +338 -0
- dolphin/lib/ontology/datasource/sql.py +845 -0
- dolphin/lib/ontology/mapping.py +177 -0
- dolphin/lib/ontology/ontology.py +733 -0
- dolphin/lib/ontology/ontology_context.py +16 -0
- dolphin/lib/ontology/ontology_manager.py +107 -0
- dolphin/lib/skill_results/__init__.py +31 -0
- dolphin/lib/skill_results/cache_backend.py +559 -0
- dolphin/lib/skill_results/result_processor.py +181 -0
- dolphin/lib/skill_results/result_reference.py +179 -0
- dolphin/lib/skill_results/skillkit_hook.py +324 -0
- dolphin/lib/skill_results/strategies.py +328 -0
- dolphin/lib/skill_results/strategy_registry.py +150 -0
- dolphin/lib/skillkits/__init__.py +44 -0
- dolphin/lib/skillkits/agent_skillkit.py +155 -0
- dolphin/lib/skillkits/cognitive_skillkit.py +82 -0
- dolphin/lib/skillkits/env_skillkit.py +250 -0
- dolphin/lib/skillkits/mcp_adapter.py +616 -0
- dolphin/lib/skillkits/mcp_skillkit.py +771 -0
- dolphin/lib/skillkits/memory_skillkit.py +650 -0
- dolphin/lib/skillkits/noop_skillkit.py +31 -0
- dolphin/lib/skillkits/ontology_skillkit.py +89 -0
- dolphin/lib/skillkits/plan_act_skillkit.py +452 -0
- dolphin/lib/skillkits/resource/__init__.py +52 -0
- dolphin/lib/skillkits/resource/models/__init__.py +6 -0
- dolphin/lib/skillkits/resource/models/skill_config.py +109 -0
- dolphin/lib/skillkits/resource/models/skill_meta.py +127 -0
- dolphin/lib/skillkits/resource/resource_skillkit.py +393 -0
- dolphin/lib/skillkits/resource/skill_cache.py +215 -0
- dolphin/lib/skillkits/resource/skill_loader.py +395 -0
- dolphin/lib/skillkits/resource/skill_validator.py +406 -0
- dolphin/lib/skillkits/resource_skillkit.py +11 -0
- dolphin/lib/skillkits/search_skillkit.py +163 -0
- dolphin/lib/skillkits/sql_skillkit.py +274 -0
- dolphin/lib/skillkits/system_skillkit.py +509 -0
- dolphin/lib/skillkits/vm_skillkit.py +65 -0
- dolphin/lib/utils/__init__.py +9 -0
- dolphin/lib/utils/data_process.py +207 -0
- dolphin/lib/utils/handle_progress.py +178 -0
- dolphin/lib/utils/security.py +139 -0
- dolphin/lib/utils/text_retrieval.py +462 -0
- dolphin/lib/vm/__init__.py +11 -0
- dolphin/lib/vm/env_executor.py +895 -0
- dolphin/lib/vm/python_session_manager.py +453 -0
- dolphin/lib/vm/vm.py +610 -0
- dolphin/sdk/__init__.py +60 -0
- dolphin/sdk/agent/__init__.py +12 -0
- dolphin/sdk/agent/agent_factory.py +236 -0
- dolphin/sdk/agent/dolphin_agent.py +1106 -0
- dolphin/sdk/api/__init__.py +4 -0
- dolphin/sdk/runtime/__init__.py +8 -0
- dolphin/sdk/runtime/env.py +363 -0
- dolphin/sdk/skill/__init__.py +10 -0
- dolphin/sdk/skill/global_skills.py +706 -0
- dolphin/sdk/skill/traditional_toolkit.py +260 -0
- kweaver_dolphin-0.1.0.dist-info/METADATA +521 -0
- kweaver_dolphin-0.1.0.dist-info/RECORD +199 -0
- kweaver_dolphin-0.1.0.dist-info/WHEEL +5 -0
- kweaver_dolphin-0.1.0.dist-info/entry_points.txt +27 -0
- kweaver_dolphin-0.1.0.dist-info/licenses/LICENSE.txt +201 -0
- kweaver_dolphin-0.1.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""Validation utilities for ResourceSkillkit.
|
|
2
|
+
|
|
3
|
+
This module provides validation for skill packages, including:
|
|
4
|
+
- SKILL.md format validation
|
|
5
|
+
- Path security validation (prevent path traversal)
|
|
6
|
+
- Size limit validation
|
|
7
|
+
- File type validation
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import List, Optional, Tuple, Set, Union
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
from .models.skill_config import ResourceSkillConfig
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class ValidationResult:
|
|
21
|
+
"""Result of a validation check.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
is_valid: Whether validation passed
|
|
25
|
+
errors: List of error messages
|
|
26
|
+
warnings: List of warning messages
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
is_valid: bool
|
|
30
|
+
errors: List[str]
|
|
31
|
+
warnings: List[str]
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def success(cls, warnings: Optional[List[str]] = None) -> "ValidationResult":
|
|
35
|
+
"""Create a successful validation result."""
|
|
36
|
+
return cls(is_valid=True, errors=[], warnings=warnings or [])
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def failure(cls, errors: List[str], warnings: Optional[List[str]] = None) -> "ValidationResult":
|
|
40
|
+
"""Create a failed validation result."""
|
|
41
|
+
return cls(is_valid=False, errors=errors, warnings=warnings or [])
|
|
42
|
+
|
|
43
|
+
def merge(self, other: "ValidationResult") -> "ValidationResult":
|
|
44
|
+
"""Merge two validation results."""
|
|
45
|
+
return ValidationResult(
|
|
46
|
+
is_valid=self.is_valid and other.is_valid,
|
|
47
|
+
errors=self.errors + other.errors,
|
|
48
|
+
warnings=self.warnings + other.warnings,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class SkillValidator:
|
|
53
|
+
"""Validator for skill packages and content.
|
|
54
|
+
|
|
55
|
+
Performs various validation checks including:
|
|
56
|
+
- Frontmatter validation (required fields, format)
|
|
57
|
+
- Path security validation (no traversal attacks)
|
|
58
|
+
- Size limit validation
|
|
59
|
+
- File type whitelisting
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
# Required fields in SKILL.md frontmatter
|
|
63
|
+
REQUIRED_FRONTMATTER_FIELDS = {"name", "description"}
|
|
64
|
+
|
|
65
|
+
# Optional but recognized frontmatter fields
|
|
66
|
+
OPTIONAL_FRONTMATTER_FIELDS = {"version", "tags", "author", "license"}
|
|
67
|
+
|
|
68
|
+
# Pattern for valid skill names (alphanumeric, hyphens, underscores)
|
|
69
|
+
SKILL_NAME_PATTERN = re.compile(r"^[a-zA-Z][a-zA-Z0-9_-]*$")
|
|
70
|
+
|
|
71
|
+
def __init__(self, config: Optional[ResourceSkillConfig] = None):
|
|
72
|
+
"""Initialize the validator.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
config: Optional configuration, uses defaults if not provided
|
|
76
|
+
"""
|
|
77
|
+
self.config = config or ResourceSkillConfig()
|
|
78
|
+
self._allowed_extensions: Set[str] = set(self.config.allowed_extensions)
|
|
79
|
+
|
|
80
|
+
def validate_frontmatter(self, frontmatter: dict) -> ValidationResult:
|
|
81
|
+
"""Validate SKILL.md frontmatter.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
frontmatter: Parsed YAML frontmatter dictionary
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
ValidationResult with any errors/warnings
|
|
88
|
+
"""
|
|
89
|
+
errors = []
|
|
90
|
+
warnings = []
|
|
91
|
+
|
|
92
|
+
# Check required fields
|
|
93
|
+
for field in self.REQUIRED_FRONTMATTER_FIELDS:
|
|
94
|
+
if field not in frontmatter:
|
|
95
|
+
errors.append(f"Missing required field in frontmatter: '{field}'")
|
|
96
|
+
elif not frontmatter[field]:
|
|
97
|
+
errors.append(f"Empty required field in frontmatter: '{field}'")
|
|
98
|
+
|
|
99
|
+
# Validate skill name format
|
|
100
|
+
name = frontmatter.get("name", "")
|
|
101
|
+
if name and not self.SKILL_NAME_PATTERN.match(name):
|
|
102
|
+
errors.append(
|
|
103
|
+
f"Invalid skill name '{name}': must start with letter and contain only "
|
|
104
|
+
"alphanumeric characters, hyphens, and underscores"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Check description length
|
|
108
|
+
description = frontmatter.get("description", "")
|
|
109
|
+
if description and len(description) > 500:
|
|
110
|
+
warnings.append(
|
|
111
|
+
f"Description is {len(description)} characters, "
|
|
112
|
+
"consider keeping it under 500 for better system prompt efficiency"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Validate tags if present
|
|
116
|
+
tags = frontmatter.get("tags", [])
|
|
117
|
+
if tags and not isinstance(tags, list):
|
|
118
|
+
errors.append("'tags' field must be a list")
|
|
119
|
+
|
|
120
|
+
# Validate version format if present
|
|
121
|
+
version = frontmatter.get("version")
|
|
122
|
+
if version and not self._is_valid_version(version):
|
|
123
|
+
warnings.append(
|
|
124
|
+
f"Version '{version}' doesn't follow semantic versioning (x.y.z)"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Check for unknown fields
|
|
128
|
+
known_fields = self.REQUIRED_FRONTMATTER_FIELDS | self.OPTIONAL_FRONTMATTER_FIELDS
|
|
129
|
+
unknown_fields = set(frontmatter.keys()) - known_fields
|
|
130
|
+
if unknown_fields:
|
|
131
|
+
warnings.append(f"Unknown frontmatter fields: {unknown_fields}")
|
|
132
|
+
|
|
133
|
+
if errors:
|
|
134
|
+
return ValidationResult.failure(errors, warnings)
|
|
135
|
+
return ValidationResult.success(warnings)
|
|
136
|
+
|
|
137
|
+
def validate_path_security(
|
|
138
|
+
self, requested_path: str, skill_base_path: Path
|
|
139
|
+
) -> ValidationResult:
|
|
140
|
+
"""Validate that a requested path is safe (no path traversal).
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
requested_path: The path requested (e.g., "scripts/etl.py")
|
|
144
|
+
skill_base_path: The base path of the skill directory
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
ValidationResult with any security errors
|
|
148
|
+
"""
|
|
149
|
+
errors = []
|
|
150
|
+
|
|
151
|
+
skill_base = skill_base_path.resolve()
|
|
152
|
+
full_path, error = resolve_safe_path(requested_path, skill_base)
|
|
153
|
+
if error:
|
|
154
|
+
return ValidationResult.failure([error])
|
|
155
|
+
|
|
156
|
+
# Check that resolved path is under skill base
|
|
157
|
+
try:
|
|
158
|
+
full_path.relative_to(skill_base)
|
|
159
|
+
except ValueError:
|
|
160
|
+
errors.append(
|
|
161
|
+
f"Path traversal detected: '{requested_path}' "
|
|
162
|
+
f"resolves outside skill directory"
|
|
163
|
+
)
|
|
164
|
+
return ValidationResult.failure(errors)
|
|
165
|
+
|
|
166
|
+
return ValidationResult.success()
|
|
167
|
+
|
|
168
|
+
def validate_file_type(self, file_path: Path) -> ValidationResult:
|
|
169
|
+
"""Validate that a file type is allowed.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
file_path: Path to the file to check
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
ValidationResult with any errors
|
|
176
|
+
"""
|
|
177
|
+
suffix = file_path.suffix.lower()
|
|
178
|
+
|
|
179
|
+
if suffix not in self._allowed_extensions:
|
|
180
|
+
return ValidationResult.failure(
|
|
181
|
+
[
|
|
182
|
+
f"File type '{suffix}' is not allowed. "
|
|
183
|
+
f"Allowed types: {sorted(self._allowed_extensions)}"
|
|
184
|
+
]
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
return ValidationResult.success()
|
|
188
|
+
|
|
189
|
+
# Directories to exclude from size calculation
|
|
190
|
+
SIZE_EXCLUDE_DIRS = {
|
|
191
|
+
"node_modules",
|
|
192
|
+
".git",
|
|
193
|
+
".venv",
|
|
194
|
+
"venv",
|
|
195
|
+
"__pycache__",
|
|
196
|
+
".cache",
|
|
197
|
+
"tmp",
|
|
198
|
+
"temp",
|
|
199
|
+
"profiles",
|
|
200
|
+
"build",
|
|
201
|
+
"dist",
|
|
202
|
+
".next",
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
def validate_size(self, path: Path) -> ValidationResult:
|
|
206
|
+
"""Validate that a file or directory doesn't exceed size limits.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
path: Path to file or directory to check
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
ValidationResult with any size errors
|
|
213
|
+
"""
|
|
214
|
+
max_bytes = self.config.max_skill_size_mb * 1024 * 1024
|
|
215
|
+
|
|
216
|
+
if path.is_file():
|
|
217
|
+
size = path.stat().st_size
|
|
218
|
+
if size > max_bytes:
|
|
219
|
+
return ValidationResult.failure(
|
|
220
|
+
[
|
|
221
|
+
f"File '{path.name}' is {size / 1024 / 1024:.2f}MB, "
|
|
222
|
+
f"exceeds limit of {self.config.max_skill_size_mb}MB"
|
|
223
|
+
]
|
|
224
|
+
)
|
|
225
|
+
elif path.is_dir():
|
|
226
|
+
# Calculate size excluding common large directories
|
|
227
|
+
total_size = 0
|
|
228
|
+
for f in path.rglob("*"):
|
|
229
|
+
if f.is_file():
|
|
230
|
+
# Check if any parent directory is in exclude list
|
|
231
|
+
parts = f.relative_to(path).parts
|
|
232
|
+
if any(part in self.SIZE_EXCLUDE_DIRS for part in parts):
|
|
233
|
+
continue
|
|
234
|
+
total_size += f.stat().st_size
|
|
235
|
+
|
|
236
|
+
if total_size > max_bytes:
|
|
237
|
+
return ValidationResult.failure(
|
|
238
|
+
[
|
|
239
|
+
f"Skill package is {total_size / 1024 / 1024:.2f}MB, "
|
|
240
|
+
f"exceeds limit of {self.config.max_skill_size_mb}MB"
|
|
241
|
+
]
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return ValidationResult.success()
|
|
245
|
+
|
|
246
|
+
def validate_skill_directory(self, skill_dir: Path) -> ValidationResult:
|
|
247
|
+
"""Validate an entire skill directory structure.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
skill_dir: Path to the skill directory
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
ValidationResult with any errors/warnings
|
|
254
|
+
"""
|
|
255
|
+
errors = []
|
|
256
|
+
warnings = []
|
|
257
|
+
|
|
258
|
+
# Check directory exists
|
|
259
|
+
if not skill_dir.is_dir():
|
|
260
|
+
return ValidationResult.failure(
|
|
261
|
+
[f"Skill directory does not exist: {skill_dir}"]
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Check SKILL.md exists
|
|
265
|
+
skill_md = skill_dir / "SKILL.md"
|
|
266
|
+
if not skill_md.is_file():
|
|
267
|
+
return ValidationResult.failure(
|
|
268
|
+
[f"SKILL.md not found in {skill_dir}"]
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Validate size
|
|
272
|
+
size_result = self.validate_size(skill_dir)
|
|
273
|
+
if not size_result.is_valid:
|
|
274
|
+
errors.extend(size_result.errors)
|
|
275
|
+
warnings.extend(size_result.warnings)
|
|
276
|
+
|
|
277
|
+
# Check for optional directories
|
|
278
|
+
scripts_dir = skill_dir / "scripts"
|
|
279
|
+
refs_dir = skill_dir / "references"
|
|
280
|
+
|
|
281
|
+
if scripts_dir.exists() and not scripts_dir.is_dir():
|
|
282
|
+
errors.append("'scripts' exists but is not a directory")
|
|
283
|
+
|
|
284
|
+
if refs_dir.exists() and not refs_dir.is_dir():
|
|
285
|
+
errors.append("'references' exists but is not a directory")
|
|
286
|
+
|
|
287
|
+
# Check for AGENTS.md (Codex compatibility)
|
|
288
|
+
agents_md = skill_dir / "AGENTS.md"
|
|
289
|
+
if agents_md.is_file():
|
|
290
|
+
warnings.append("AGENTS.md found - Codex compatibility mode available")
|
|
291
|
+
|
|
292
|
+
if errors:
|
|
293
|
+
return ValidationResult.failure(errors, warnings)
|
|
294
|
+
return ValidationResult.success(warnings)
|
|
295
|
+
|
|
296
|
+
def _is_valid_version(self, version: str) -> bool:
|
|
297
|
+
"""Check if version string follows semantic versioning.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
version: Version string to check
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
True if valid semver format
|
|
304
|
+
"""
|
|
305
|
+
semver_pattern = re.compile(
|
|
306
|
+
r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)"
|
|
307
|
+
r"(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)"
|
|
308
|
+
r"(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?"
|
|
309
|
+
r"(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
|
|
310
|
+
)
|
|
311
|
+
return bool(semver_pattern.match(version))
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def validate_skill_name(name: str) -> Tuple[bool, Optional[str]]:
|
|
315
|
+
"""Validate a skill name.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
name: The skill name to validate
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Tuple of (is_valid, error_message)
|
|
322
|
+
"""
|
|
323
|
+
if not name:
|
|
324
|
+
return False, "Skill name cannot be empty"
|
|
325
|
+
|
|
326
|
+
if not SkillValidator.SKILL_NAME_PATTERN.match(name):
|
|
327
|
+
return False, (
|
|
328
|
+
f"Invalid skill name '{name}': must start with letter and contain only "
|
|
329
|
+
"alphanumeric characters, hyphens, and underscores"
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
return True, None
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def is_safe_path(requested_path: str, base_path: Path) -> bool:
|
|
336
|
+
"""Quick check if a path is safe (no traversal).
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
requested_path: The relative path requested
|
|
340
|
+
base_path: The base directory path
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
True if path is safe, False otherwise
|
|
344
|
+
"""
|
|
345
|
+
resolved, error = resolve_safe_path(requested_path, base_path)
|
|
346
|
+
return resolved is not None and error is None
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def resolve_safe_path(
|
|
350
|
+
requested_path: str, base_path: Union[Path, str]
|
|
351
|
+
) -> Tuple[Optional[Path], Optional[str]]:
|
|
352
|
+
"""Resolve a requested path under a base directory safely.
|
|
353
|
+
|
|
354
|
+
This helper prevents path traversal and rejects any symlink in the path.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
(resolved_path, error_message)
|
|
358
|
+
"""
|
|
359
|
+
try:
|
|
360
|
+
base = Path(base_path).resolve()
|
|
361
|
+
except (OSError, ValueError) as e:
|
|
362
|
+
return None, f"Invalid base path: {e}"
|
|
363
|
+
|
|
364
|
+
if not requested_path:
|
|
365
|
+
return None, "Invalid path: empty"
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
rel = Path(requested_path)
|
|
369
|
+
except Exception as e:
|
|
370
|
+
return None, f"Invalid path: {e}"
|
|
371
|
+
|
|
372
|
+
if rel.is_absolute():
|
|
373
|
+
return None, f"Invalid path: '{requested_path}' - absolute paths not allowed"
|
|
374
|
+
|
|
375
|
+
candidate = base / rel
|
|
376
|
+
|
|
377
|
+
# Reject any symlink in the path components (including the final file)
|
|
378
|
+
current = base
|
|
379
|
+
for part in rel.parts:
|
|
380
|
+
if part in (".", ""):
|
|
381
|
+
continue
|
|
382
|
+
if part == "..":
|
|
383
|
+
return None, f"Invalid path: '{requested_path}' - path traversal not allowed"
|
|
384
|
+
current = current / part
|
|
385
|
+
try:
|
|
386
|
+
if current.exists() and current.is_symlink():
|
|
387
|
+
return (
|
|
388
|
+
None,
|
|
389
|
+
f"Invalid path: '{requested_path}' - symlinks are not allowed",
|
|
390
|
+
)
|
|
391
|
+
except OSError as e:
|
|
392
|
+
return None, f"Invalid path: {e}"
|
|
393
|
+
|
|
394
|
+
try:
|
|
395
|
+
resolved = candidate.resolve(strict=True)
|
|
396
|
+
except FileNotFoundError:
|
|
397
|
+
return None, f"Resource file not found: '{requested_path}'"
|
|
398
|
+
except (OSError, ValueError) as e:
|
|
399
|
+
return None, f"Invalid path: {e}"
|
|
400
|
+
|
|
401
|
+
try:
|
|
402
|
+
resolved.relative_to(base)
|
|
403
|
+
except ValueError:
|
|
404
|
+
return None, f"Invalid path: '{requested_path}' - path traversal not allowed"
|
|
405
|
+
|
|
406
|
+
return resolved, None
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Compatibility wrapper for ResourceSkillkit.
|
|
2
|
+
|
|
3
|
+
GlobalSkills' file-based loader scans only top-level `skill/installed/*.py`.
|
|
4
|
+
This module re-exports the package implementation so ResourceSkillkit can be
|
|
5
|
+
discovered in both entry-point and fallback modes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dolphin.lib.skillkits.resource import ResourceSkillkit
|
|
9
|
+
|
|
10
|
+
__all__ = ["ResourceSkillkit"]
|
|
11
|
+
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import List
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
from dolphin.core.skill.skill_function import SkillFunction
|
|
7
|
+
from dolphin.core.skill.skillkit import Skillkit
|
|
8
|
+
from dolphin.core.utils.cache_kv import GlobalCacheKVCenter
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
from requests.exceptions import RequestException
|
|
12
|
+
from dolphin.core.common.constants import SEARCH_TIMEOUT, SEARCH_RETRY_COUNT
|
|
13
|
+
from dolphin.core.logging.logger import get_logger
|
|
14
|
+
|
|
15
|
+
logger = get_logger("skill")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SearchSkillkit(Skillkit):
|
|
19
|
+
MAX_KEYWORDS = 5
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
super().__init__()
|
|
23
|
+
self.cacheMgr = None
|
|
24
|
+
|
|
25
|
+
def getName(self) -> str:
|
|
26
|
+
return "search_skillkit"
|
|
27
|
+
|
|
28
|
+
def setGlobalConfig(self, globalConfig):
|
|
29
|
+
"""Set global context"""
|
|
30
|
+
super().setGlobalConfig(globalConfig)
|
|
31
|
+
|
|
32
|
+
if self.cacheMgr is None:
|
|
33
|
+
self.cacheMgr = GlobalCacheKVCenter.getCacheMgr(
|
|
34
|
+
"data/cache/", category="web_search", expireTimeByDay=10
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def _search(
|
|
38
|
+
self, query: str, maxResults: int = 10, site: str = "", **kwargs
|
|
39
|
+
) -> str:
|
|
40
|
+
"""Search for information on the internet using Chinese. If the keywords exceed 5, the subsequent content will be truncated.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
query (str): The query string to search for, no more than 5 keywords
|
|
44
|
+
maxResults (int): Maximum number of search results
|
|
45
|
+
site (str): Specify the website domain to search, such as "github.com" or "stackoverflow.com"
|
|
46
|
+
**kwargs: Additional properties passed to the tool.
|
|
47
|
+
|
|
48
|
+
Returns: JSON string of search results
|
|
49
|
+
"""
|
|
50
|
+
# Lazy initialization: If the global context is not set, an error is thrown.
|
|
51
|
+
if self.globalConfig is None:
|
|
52
|
+
raise ValueError(
|
|
53
|
+
"Global context not set. Please call setGlobalConfig() first."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Delayed Initialization Cache Manager
|
|
57
|
+
if self.cacheMgr is None:
|
|
58
|
+
self.cacheMgr = GlobalCacheKVCenter.getCacheMgr(
|
|
59
|
+
"data/cache/", category="web_search", expireTimeByDay=10
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
query = self._query_preprocess(query)
|
|
63
|
+
|
|
64
|
+
modelName = "zhipu_search"
|
|
65
|
+
cacheKey = [{"query": query, "maxResults": maxResults, "site": site}]
|
|
66
|
+
|
|
67
|
+
cachedResult = self.cacheMgr.getValue(modelName, cacheKey)
|
|
68
|
+
if cachedResult:
|
|
69
|
+
return json.dumps(cachedResult, ensure_ascii=False)
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
# If the site parameter is specified, add the site: operator to the query string.
|
|
73
|
+
searchQuery = query
|
|
74
|
+
if site:
|
|
75
|
+
searchQuery = f"site:{site} {query}"
|
|
76
|
+
|
|
77
|
+
# Get API key from configuration file
|
|
78
|
+
zhipuConfig = self.globalConfig.all_clouds_config.get_cloud_config("zhipu")
|
|
79
|
+
if not zhipuConfig or not zhipuConfig.api_key:
|
|
80
|
+
raise ValueError("Zhipu API key not found in configuration")
|
|
81
|
+
|
|
82
|
+
# Configure the Zhipu API request, using the tools interface according to the official documentation.
|
|
83
|
+
url = f"{zhipuConfig.api}/api/paas/v4/tools"
|
|
84
|
+
headers = {
|
|
85
|
+
"Authorization": f"{zhipuConfig.api_key}",
|
|
86
|
+
"Content-Type": "application/json",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# Formatting requests according to the Zhipu AI documentation
|
|
90
|
+
data = {
|
|
91
|
+
"tool": "web-search-pro",
|
|
92
|
+
"messages": [{"role": "user", "content": searchQuery}],
|
|
93
|
+
"request_id": str(uuid.uuid4()),
|
|
94
|
+
"stream": False,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for attempt in range(SEARCH_RETRY_COUNT):
|
|
98
|
+
try:
|
|
99
|
+
response = requests.post(
|
|
100
|
+
url, headers=headers, json=data, timeout=SEARCH_TIMEOUT
|
|
101
|
+
)
|
|
102
|
+
response.raise_for_status()
|
|
103
|
+
break
|
|
104
|
+
except RequestException:
|
|
105
|
+
if attempt == SEARCH_RETRY_COUNT - 1:
|
|
106
|
+
raise
|
|
107
|
+
time.sleep(2**attempt) # exponential backoff
|
|
108
|
+
|
|
109
|
+
# Parse response results
|
|
110
|
+
responseData = response.json()
|
|
111
|
+
|
|
112
|
+
# Check response format
|
|
113
|
+
if (
|
|
114
|
+
"choices" not in responseData
|
|
115
|
+
or len(responseData["choices"]) == 0
|
|
116
|
+
or "message" not in responseData["choices"][0]
|
|
117
|
+
or "tool_calls" not in responseData["choices"][0]["message"]
|
|
118
|
+
):
|
|
119
|
+
raise ValueError("Invalid response format from Zhipu API")
|
|
120
|
+
|
|
121
|
+
# Extract search results
|
|
122
|
+
toolCalls = responseData["choices"][0]["message"]["tool_calls"]
|
|
123
|
+
searchResults = []
|
|
124
|
+
|
|
125
|
+
for toolCall in toolCalls:
|
|
126
|
+
if (
|
|
127
|
+
toolCall.get("type") == "search_result"
|
|
128
|
+
and "search_result" in toolCall
|
|
129
|
+
):
|
|
130
|
+
searchResults.extend(toolCall["search_result"])
|
|
131
|
+
|
|
132
|
+
# Convert the result to a unified format
|
|
133
|
+
responses = []
|
|
134
|
+
for i, result in enumerate(searchResults[:maxResults], start=1):
|
|
135
|
+
response = {
|
|
136
|
+
"resultId": i,
|
|
137
|
+
"title": result.get("title", ""),
|
|
138
|
+
"description": result.get("content", ""),
|
|
139
|
+
"url": result.get("link", ""),
|
|
140
|
+
}
|
|
141
|
+
responses.append(response)
|
|
142
|
+
|
|
143
|
+
if len(responses) > 0:
|
|
144
|
+
self.cacheMgr.setValue(modelName, cacheKey, responses)
|
|
145
|
+
return json.dumps(responses, ensure_ascii=False)
|
|
146
|
+
|
|
147
|
+
except Exception as e:
|
|
148
|
+
logger.error(f"zhipu search failed: {str(e)}")
|
|
149
|
+
return json.dumps(
|
|
150
|
+
[{"error": f"zhipu search failed: {str(e)}"}], ensure_ascii=False
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def _createSkills(self) -> List[SkillFunction]:
|
|
154
|
+
return [
|
|
155
|
+
SkillFunction(self._search),
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
def _query_preprocess(self, query: str) -> str:
|
|
159
|
+
query = query.strip()
|
|
160
|
+
items = query.split(" ")
|
|
161
|
+
if len(items) > self.MAX_KEYWORDS:
|
|
162
|
+
return " ".join(items[: self.MAX_KEYWORDS])
|
|
163
|
+
return query
|