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.
Files changed (199) hide show
  1. DolphinLanguageSDK/__init__.py +58 -0
  2. dolphin/__init__.py +62 -0
  3. dolphin/cli/__init__.py +20 -0
  4. dolphin/cli/args/__init__.py +9 -0
  5. dolphin/cli/args/parser.py +567 -0
  6. dolphin/cli/builtin_agents/__init__.py +22 -0
  7. dolphin/cli/commands/__init__.py +4 -0
  8. dolphin/cli/interrupt/__init__.py +8 -0
  9. dolphin/cli/interrupt/handler.py +205 -0
  10. dolphin/cli/interrupt/keyboard.py +82 -0
  11. dolphin/cli/main.py +49 -0
  12. dolphin/cli/multimodal/__init__.py +34 -0
  13. dolphin/cli/multimodal/clipboard.py +327 -0
  14. dolphin/cli/multimodal/handler.py +249 -0
  15. dolphin/cli/multimodal/image_processor.py +214 -0
  16. dolphin/cli/multimodal/input_parser.py +149 -0
  17. dolphin/cli/runner/__init__.py +8 -0
  18. dolphin/cli/runner/runner.py +989 -0
  19. dolphin/cli/ui/__init__.py +10 -0
  20. dolphin/cli/ui/console.py +2795 -0
  21. dolphin/cli/ui/input.py +340 -0
  22. dolphin/cli/ui/layout.py +425 -0
  23. dolphin/cli/ui/stream_renderer.py +302 -0
  24. dolphin/cli/utils/__init__.py +8 -0
  25. dolphin/cli/utils/helpers.py +135 -0
  26. dolphin/cli/utils/version.py +49 -0
  27. dolphin/core/__init__.py +107 -0
  28. dolphin/core/agent/__init__.py +10 -0
  29. dolphin/core/agent/agent_state.py +69 -0
  30. dolphin/core/agent/base_agent.py +970 -0
  31. dolphin/core/code_block/__init__.py +0 -0
  32. dolphin/core/code_block/agent_init_block.py +0 -0
  33. dolphin/core/code_block/assign_block.py +98 -0
  34. dolphin/core/code_block/basic_code_block.py +1865 -0
  35. dolphin/core/code_block/explore_block.py +1327 -0
  36. dolphin/core/code_block/explore_block_v2.py +712 -0
  37. dolphin/core/code_block/explore_strategy.py +672 -0
  38. dolphin/core/code_block/judge_block.py +220 -0
  39. dolphin/core/code_block/prompt_block.py +32 -0
  40. dolphin/core/code_block/skill_call_deduplicator.py +291 -0
  41. dolphin/core/code_block/tool_block.py +129 -0
  42. dolphin/core/common/__init__.py +17 -0
  43. dolphin/core/common/constants.py +176 -0
  44. dolphin/core/common/enums.py +1173 -0
  45. dolphin/core/common/exceptions.py +133 -0
  46. dolphin/core/common/multimodal.py +539 -0
  47. dolphin/core/common/object_type.py +165 -0
  48. dolphin/core/common/output_format.py +432 -0
  49. dolphin/core/common/types.py +36 -0
  50. dolphin/core/config/__init__.py +16 -0
  51. dolphin/core/config/global_config.py +1289 -0
  52. dolphin/core/config/ontology_config.py +133 -0
  53. dolphin/core/context/__init__.py +12 -0
  54. dolphin/core/context/context.py +1580 -0
  55. dolphin/core/context/context_manager.py +161 -0
  56. dolphin/core/context/var_output.py +82 -0
  57. dolphin/core/context/variable_pool.py +356 -0
  58. dolphin/core/context_engineer/__init__.py +41 -0
  59. dolphin/core/context_engineer/config/__init__.py +5 -0
  60. dolphin/core/context_engineer/config/settings.py +402 -0
  61. dolphin/core/context_engineer/core/__init__.py +7 -0
  62. dolphin/core/context_engineer/core/budget_manager.py +327 -0
  63. dolphin/core/context_engineer/core/context_assembler.py +583 -0
  64. dolphin/core/context_engineer/core/context_manager.py +637 -0
  65. dolphin/core/context_engineer/core/tokenizer_service.py +260 -0
  66. dolphin/core/context_engineer/example/incremental_example.py +267 -0
  67. dolphin/core/context_engineer/example/traditional_example.py +334 -0
  68. dolphin/core/context_engineer/services/__init__.py +5 -0
  69. dolphin/core/context_engineer/services/compressor.py +399 -0
  70. dolphin/core/context_engineer/utils/__init__.py +6 -0
  71. dolphin/core/context_engineer/utils/context_utils.py +441 -0
  72. dolphin/core/context_engineer/utils/message_formatter.py +270 -0
  73. dolphin/core/context_engineer/utils/token_utils.py +139 -0
  74. dolphin/core/coroutine/__init__.py +15 -0
  75. dolphin/core/coroutine/context_snapshot.py +154 -0
  76. dolphin/core/coroutine/context_snapshot_profile.py +922 -0
  77. dolphin/core/coroutine/context_snapshot_store.py +268 -0
  78. dolphin/core/coroutine/execution_frame.py +145 -0
  79. dolphin/core/coroutine/execution_state_registry.py +161 -0
  80. dolphin/core/coroutine/resume_handle.py +101 -0
  81. dolphin/core/coroutine/step_result.py +101 -0
  82. dolphin/core/executor/__init__.py +18 -0
  83. dolphin/core/executor/debug_controller.py +630 -0
  84. dolphin/core/executor/dolphin_executor.py +1063 -0
  85. dolphin/core/executor/executor.py +624 -0
  86. dolphin/core/flags/__init__.py +27 -0
  87. dolphin/core/flags/definitions.py +49 -0
  88. dolphin/core/flags/manager.py +113 -0
  89. dolphin/core/hook/__init__.py +95 -0
  90. dolphin/core/hook/expression_evaluator.py +499 -0
  91. dolphin/core/hook/hook_dispatcher.py +380 -0
  92. dolphin/core/hook/hook_types.py +248 -0
  93. dolphin/core/hook/isolated_variable_pool.py +284 -0
  94. dolphin/core/interfaces.py +53 -0
  95. dolphin/core/llm/__init__.py +0 -0
  96. dolphin/core/llm/llm.py +495 -0
  97. dolphin/core/llm/llm_call.py +100 -0
  98. dolphin/core/llm/llm_client.py +1285 -0
  99. dolphin/core/llm/message_sanitizer.py +120 -0
  100. dolphin/core/logging/__init__.py +20 -0
  101. dolphin/core/logging/logger.py +526 -0
  102. dolphin/core/message/__init__.py +8 -0
  103. dolphin/core/message/compressor.py +749 -0
  104. dolphin/core/parser/__init__.py +8 -0
  105. dolphin/core/parser/parser.py +405 -0
  106. dolphin/core/runtime/__init__.py +10 -0
  107. dolphin/core/runtime/runtime_graph.py +926 -0
  108. dolphin/core/runtime/runtime_instance.py +446 -0
  109. dolphin/core/skill/__init__.py +14 -0
  110. dolphin/core/skill/context_retention.py +157 -0
  111. dolphin/core/skill/skill_function.py +686 -0
  112. dolphin/core/skill/skill_matcher.py +282 -0
  113. dolphin/core/skill/skillkit.py +700 -0
  114. dolphin/core/skill/skillset.py +72 -0
  115. dolphin/core/trajectory/__init__.py +10 -0
  116. dolphin/core/trajectory/recorder.py +189 -0
  117. dolphin/core/trajectory/trajectory.py +522 -0
  118. dolphin/core/utils/__init__.py +9 -0
  119. dolphin/core/utils/cache_kv.py +212 -0
  120. dolphin/core/utils/tools.py +340 -0
  121. dolphin/lib/__init__.py +93 -0
  122. dolphin/lib/debug/__init__.py +8 -0
  123. dolphin/lib/debug/visualizer.py +409 -0
  124. dolphin/lib/memory/__init__.py +28 -0
  125. dolphin/lib/memory/async_processor.py +220 -0
  126. dolphin/lib/memory/llm_calls.py +195 -0
  127. dolphin/lib/memory/manager.py +78 -0
  128. dolphin/lib/memory/sandbox.py +46 -0
  129. dolphin/lib/memory/storage.py +245 -0
  130. dolphin/lib/memory/utils.py +51 -0
  131. dolphin/lib/ontology/__init__.py +12 -0
  132. dolphin/lib/ontology/basic/__init__.py +0 -0
  133. dolphin/lib/ontology/basic/base.py +102 -0
  134. dolphin/lib/ontology/basic/concept.py +130 -0
  135. dolphin/lib/ontology/basic/object.py +11 -0
  136. dolphin/lib/ontology/basic/relation.py +63 -0
  137. dolphin/lib/ontology/datasource/__init__.py +27 -0
  138. dolphin/lib/ontology/datasource/datasource.py +66 -0
  139. dolphin/lib/ontology/datasource/oracle_datasource.py +338 -0
  140. dolphin/lib/ontology/datasource/sql.py +845 -0
  141. dolphin/lib/ontology/mapping.py +177 -0
  142. dolphin/lib/ontology/ontology.py +733 -0
  143. dolphin/lib/ontology/ontology_context.py +16 -0
  144. dolphin/lib/ontology/ontology_manager.py +107 -0
  145. dolphin/lib/skill_results/__init__.py +31 -0
  146. dolphin/lib/skill_results/cache_backend.py +559 -0
  147. dolphin/lib/skill_results/result_processor.py +181 -0
  148. dolphin/lib/skill_results/result_reference.py +179 -0
  149. dolphin/lib/skill_results/skillkit_hook.py +324 -0
  150. dolphin/lib/skill_results/strategies.py +328 -0
  151. dolphin/lib/skill_results/strategy_registry.py +150 -0
  152. dolphin/lib/skillkits/__init__.py +44 -0
  153. dolphin/lib/skillkits/agent_skillkit.py +155 -0
  154. dolphin/lib/skillkits/cognitive_skillkit.py +82 -0
  155. dolphin/lib/skillkits/env_skillkit.py +250 -0
  156. dolphin/lib/skillkits/mcp_adapter.py +616 -0
  157. dolphin/lib/skillkits/mcp_skillkit.py +771 -0
  158. dolphin/lib/skillkits/memory_skillkit.py +650 -0
  159. dolphin/lib/skillkits/noop_skillkit.py +31 -0
  160. dolphin/lib/skillkits/ontology_skillkit.py +89 -0
  161. dolphin/lib/skillkits/plan_act_skillkit.py +452 -0
  162. dolphin/lib/skillkits/resource/__init__.py +52 -0
  163. dolphin/lib/skillkits/resource/models/__init__.py +6 -0
  164. dolphin/lib/skillkits/resource/models/skill_config.py +109 -0
  165. dolphin/lib/skillkits/resource/models/skill_meta.py +127 -0
  166. dolphin/lib/skillkits/resource/resource_skillkit.py +393 -0
  167. dolphin/lib/skillkits/resource/skill_cache.py +215 -0
  168. dolphin/lib/skillkits/resource/skill_loader.py +395 -0
  169. dolphin/lib/skillkits/resource/skill_validator.py +406 -0
  170. dolphin/lib/skillkits/resource_skillkit.py +11 -0
  171. dolphin/lib/skillkits/search_skillkit.py +163 -0
  172. dolphin/lib/skillkits/sql_skillkit.py +274 -0
  173. dolphin/lib/skillkits/system_skillkit.py +509 -0
  174. dolphin/lib/skillkits/vm_skillkit.py +65 -0
  175. dolphin/lib/utils/__init__.py +9 -0
  176. dolphin/lib/utils/data_process.py +207 -0
  177. dolphin/lib/utils/handle_progress.py +178 -0
  178. dolphin/lib/utils/security.py +139 -0
  179. dolphin/lib/utils/text_retrieval.py +462 -0
  180. dolphin/lib/vm/__init__.py +11 -0
  181. dolphin/lib/vm/env_executor.py +895 -0
  182. dolphin/lib/vm/python_session_manager.py +453 -0
  183. dolphin/lib/vm/vm.py +610 -0
  184. dolphin/sdk/__init__.py +60 -0
  185. dolphin/sdk/agent/__init__.py +12 -0
  186. dolphin/sdk/agent/agent_factory.py +236 -0
  187. dolphin/sdk/agent/dolphin_agent.py +1106 -0
  188. dolphin/sdk/api/__init__.py +4 -0
  189. dolphin/sdk/runtime/__init__.py +8 -0
  190. dolphin/sdk/runtime/env.py +363 -0
  191. dolphin/sdk/skill/__init__.py +10 -0
  192. dolphin/sdk/skill/global_skills.py +706 -0
  193. dolphin/sdk/skill/traditional_toolkit.py +260 -0
  194. kweaver_dolphin-0.1.0.dist-info/METADATA +521 -0
  195. kweaver_dolphin-0.1.0.dist-info/RECORD +199 -0
  196. kweaver_dolphin-0.1.0.dist-info/WHEEL +5 -0
  197. kweaver_dolphin-0.1.0.dist-info/entry_points.txt +27 -0
  198. kweaver_dolphin-0.1.0.dist-info/licenses/LICENSE.txt +201 -0
  199. 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