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,700 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import inspect
5
+ import json
6
+ from typing import Any, Callable, List, Tuple, Dict, Optional
7
+
8
+ from dolphin.core.logging.logger import MaxLenLog
9
+ from dolphin.core.skill.skill_function import SkillFunction
10
+ from dolphin.core.skill.skill_matcher import SkillMatcher
11
+ from dolphin.core.logging.logger import get_logger
12
+
13
+ logger = get_logger("skill")
14
+
15
+
16
+ class SkillExecRecord:
17
+ """Skill Execution Log"""
18
+
19
+ def __init__(self, toolCall: Tuple[str, dict], tool: SkillFunction, result: Any):
20
+ self.toolCall = toolCall
21
+ self.tool = tool
22
+ self.result = result
23
+
24
+ def __str__(self) -> str:
25
+ return f"toolCall: {self.toolCall}, tool: {self.tool}, result: {self.result[:MaxLenLog]}"
26
+
27
+ def get_tool_call(self) -> Tuple[str, dict]:
28
+ return self.toolCall
29
+
30
+ def get_tool(self) -> SkillFunction:
31
+ return self.tool
32
+
33
+ def get_result(self) -> Any:
34
+ return self.result
35
+
36
+
37
+ class Skillkit:
38
+ def __init__(self) -> None:
39
+ self.records = []
40
+ self.queryAsArg = False
41
+ self._skills_cache: Optional[List[SkillFunction]] = None
42
+ """Skill result processing strategy configuration. The strategies used must be registered strategies in StrategyRegistry.
43
+ Example:
44
+ [
45
+ {
46
+ "strategy": "summary",
47
+ "category": "llm",
48
+ },
49
+ {
50
+ "strategy": "preview",
51
+ "category": "app",
52
+ },
53
+ ]
54
+ """
55
+ self.result_process_strategy_cfg: list[Dict[str, str]] = None
56
+
57
+ def getName(self) -> str:
58
+ return "skillkit"
59
+
60
+ # ─────────────────────────────────────────────────────────────
61
+ # UI Rendering Protocol (Custom UI Support)
62
+ # ─────────────────────────────────────────────────────────────
63
+
64
+ def has_custom_ui(self, skill_name: str) -> bool:
65
+ """Check if this skillkit provides custom UI rendering for a skill.
66
+
67
+ Subclasses can override this to indicate that they provide
68
+ custom UI rendering instead of the default skill_call box.
69
+
70
+ Args:
71
+ skill_name: Name of the skill being rendered
72
+
73
+ Returns:
74
+ True if custom UI is provided, False to use default rendering
75
+ """
76
+ return False
77
+
78
+ def render_skill_start(
79
+ self,
80
+ skill_name: str,
81
+ params: dict,
82
+ verbose: bool = True
83
+ ) -> None:
84
+ """Custom UI rendering for skill start (before execution).
85
+
86
+ Called instead of skill_call_start when has_custom_ui returns True.
87
+ Subclasses should override to provide custom rendering.
88
+
89
+ Args:
90
+ skill_name: Name of the skill being called
91
+ params: Parameters passed to the skill
92
+ verbose: Whether to render UI
93
+ """
94
+ pass # Default: no-op, subclasses implement
95
+
96
+ def render_skill_end(
97
+ self,
98
+ skill_name: str,
99
+ params: dict,
100
+ result: Any,
101
+ success: bool = True,
102
+ duration_ms: float = 0,
103
+ verbose: bool = True
104
+ ) -> None:
105
+ """Custom UI rendering for skill end (after execution).
106
+
107
+ Called instead of skill_call_end when has_custom_ui returns True.
108
+ Subclasses should override to provide custom rendering.
109
+
110
+ Args:
111
+ skill_name: Name of the skill that completed
112
+ params: Parameters that were passed to the skill
113
+ result: Result from the skill execution
114
+ success: Whether the skill succeeded
115
+ duration_ms: Execution duration in milliseconds
116
+ verbose: Whether to render UI
117
+ """
118
+ pass # Default: no-op, subclasses implement
119
+
120
+ def _createSkills(self) -> List[SkillFunction]:
121
+ """Subclasses override this method to create the skill list.
122
+
123
+ This is the template method pattern: subclasses implement skill creation,
124
+ and the base class handles owner binding in getSkills().
125
+
126
+ Returns:
127
+ List[SkillFunction]: List of skills created by this skillkit
128
+ """
129
+ return []
130
+
131
+ def getSkills(self) -> List[SkillFunction]:
132
+ """Get the skill list with owner_skillkit automatically bound.
133
+
134
+ This method caches the skills and ensures owner_skillkit is set
135
+ for all skills. Subclasses should override _createSkills() instead
136
+ of this method.
137
+
138
+ Returns:
139
+ List[SkillFunction]: List of skills with owner_skillkit set
140
+ """
141
+ if self._skills_cache is None:
142
+ self._skills_cache = self._createSkills()
143
+ self._bindOwnerToSkills(self._skills_cache)
144
+ return self._skills_cache
145
+
146
+ def _bindOwnerToSkills(self, skills: List[SkillFunction]) -> None:
147
+ """Bind owner_skillkit to all skills that don't have one set.
148
+
149
+ This passes the Skillkit object (self) so that metadata prompt
150
+ can be collected dynamically via skill.owner_skillkit.get_metadata_prompt().
151
+ """
152
+ for skill in skills:
153
+ if hasattr(skill, "set_owner_skillkit"):
154
+ current_owner = getattr(skill, "get_owner_skillkit", lambda: None)()
155
+ if current_owner is None:
156
+ skill.set_owner_skillkit(self)
157
+
158
+ def invalidateSkillsCache(self) -> None:
159
+ """Invalidate the skills cache, forcing recreation on next getSkills() call."""
160
+ self._skills_cache = None
161
+
162
+ def getResultProcessStrategyCfg(self) -> list[Dict[str, str]]:
163
+ return self.result_process_strategy_cfg
164
+
165
+ def setResultProcessStrategyCfg(
166
+ self, result_process_strategy_cfg: list[Dict[str, str]]
167
+ ) -> None:
168
+ self.result_process_strategy_cfg = result_process_strategy_cfg
169
+
170
+ def getSkillNames(self) -> List[str]:
171
+ return [skill.get_function_name() for skill in self.getSkills()]
172
+
173
+ def setGlobalConfig(self, globalConfig):
174
+ self.globalConfig = globalConfig
175
+
176
+ def getCertainSkills(
177
+ self, skillNames: List[str] | str | None
178
+ ) -> List[SkillFunction]:
179
+ if skillNames is None:
180
+ return self.getSkills()
181
+ elif isinstance(skillNames, str):
182
+ # Use SkillMatcher to support wildcard matching
183
+ return SkillMatcher.filter_skills_by_pattern(self.getSkills(), skillNames)
184
+ else:
185
+ # Use SkillMatcher to support wildcard matching
186
+ return SkillMatcher.filter_skills_by_patterns(self.getSkills(), skillNames)
187
+
188
+ def hasSkill(self, skillName: str) -> bool:
189
+ return SkillMatcher.get_skill_by_name(self.getSkills(), skillName) is not None
190
+
191
+ def getSkill(self, skillName: str) -> Optional[SkillFunction]:
192
+ return SkillMatcher.get_skill_by_name(self.getSkills(), skillName)
193
+
194
+ @staticmethod
195
+ def getSkillsWithSingleSkill(skill: Callable) -> List[SkillFunction]:
196
+ return [SkillFunction(skill)]
197
+
198
+ def getSkillsSchema(self) -> list:
199
+ return [skill.get_openai_tool_schema() for skill in self.getSkills()]
200
+
201
+ def getSkillsSchemaForCertainSkills(self, skillNames: List[str]) -> list:
202
+ return [
203
+ skill.get_openai_tool_schema()
204
+ for skill in self.getCertainSkills(skillNames)
205
+ ]
206
+
207
+ def getSkillsDict(self) -> dict:
208
+ return {skill.get_function_name(): skill for skill in self.getSkills()}
209
+
210
+ def getSchemas(self, skillNames: Optional[List[str]] = None) -> str:
211
+ skills = self.getCertainSkills(skillNames)
212
+ functionSchemas = [
213
+ json.dumps(skill.get_openai_tool_schema()["function"], ensure_ascii=False)
214
+ for skill in skills
215
+ ]
216
+ return "|".join(functionSchemas)
217
+
218
+ # =========================
219
+ # Compression (generic)
220
+ # =========================
221
+
222
+ # Default rules for compressing skill-call messages; subclasses can override
223
+ DEFAULT_COMPRESS_RULES: Dict[str, Dict[str, List[str]]] = {}
224
+
225
+ def get_compress_rules(self) -> Dict[str, Dict[str, List[str]]]:
226
+ """Return default compression rules for this skillkit instance."""
227
+ return self.DEFAULT_COMPRESS_RULES
228
+
229
+ @classmethod
230
+ def set_default_compress_rules(cls, rules: Dict[str, Dict[str, List[str]]]):
231
+ """Set default compression rules at class level."""
232
+ cls.DEFAULT_COMPRESS_RULES = rules or {}
233
+
234
+ @staticmethod
235
+ def compress_message_with_rules(
236
+ message: str,
237
+ rules: Optional[Dict[str, Dict[str, List[str]]]] = None,
238
+ marker_prefix: str = "=>#",
239
+ ) -> str:
240
+ """
241
+ Compress skill-call messages using include/exclude rules per skill name.
242
+
243
+ Args:
244
+ message: Raw message text containing markers like '=>#skillName:{...}'.
245
+ rules: Per-skill rules, e.g. {"_cog_think": {"include": ["action"]}}
246
+ marker_prefix: Prefix that denotes a skill-call marker.
247
+
248
+ Returns:
249
+ Compressed message text.
250
+ """
251
+ import json
252
+ import re
253
+ from dolphin.core.utils.tools import (
254
+ extract_json_from_response,
255
+ safe_json_loads,
256
+ )
257
+
258
+ active_rules: Dict[str, Dict[str, List[str]]] = rules or {}
259
+
260
+ def apply_rule(skill_name: str, data: dict) -> tuple[dict, bool]:
261
+ """Return (possibly_transformed_data, applied_flag)."""
262
+ rule = active_rules.get(skill_name) or active_rules.get("*")
263
+ if not rule:
264
+ return data, False
265
+ include_fields = (
266
+ rule.get("include") if isinstance(rule.get("include"), list) else []
267
+ )
268
+ exclude_fields = (
269
+ rule.get("exclude") if isinstance(rule.get("exclude"), list) else []
270
+ )
271
+ if include_fields:
272
+ return ({k: v for k, v in data.items() if k in include_fields}, True)
273
+ if exclude_fields:
274
+ return (
275
+ {k: v for k, v in data.items() if k not in exclude_fields},
276
+ True,
277
+ )
278
+ return data, False
279
+
280
+ # Regex to find markers and capture the skill name
281
+ pattern = re.compile(re.escape(marker_prefix) + r"([A-Za-z0-9_]+):")
282
+
283
+ idx = 0
284
+ out_parts: List[str] = []
285
+ for match in pattern.finditer(message):
286
+ start, end = match.start(), match.end()
287
+ skill_name = match.group(1)
288
+ # Append text before marker and the marker itself
289
+ out_parts.append(message[idx:start])
290
+ out_parts.append(message[start:end])
291
+
292
+ # Locate JSON object starting after ':' using shared util
293
+ brace_start = message.find("{", end)
294
+ if brace_start == -1:
295
+ idx = end
296
+ continue
297
+
298
+ json_text = extract_json_from_response(message[brace_start:])
299
+ if not json_text or not json_text.startswith("{"):
300
+ # Not a proper JSON, keep raw char and move one step
301
+ out_parts.append(message[end : brace_start + 1])
302
+ idx = brace_start + 1
303
+ continue
304
+
305
+ next_idx = brace_start + len(json_text)
306
+
307
+ try:
308
+ data = safe_json_loads(json_text, strict=False)
309
+ if isinstance(data, dict):
310
+ transformed, applied = apply_rule(skill_name, data)
311
+ if applied:
312
+ out_parts.append(json.dumps(transformed, ensure_ascii=False))
313
+ else:
314
+ # No rule applied: keep original JSON text unchanged
315
+ out_parts.append(json_text)
316
+ else:
317
+ # Non-dict payloads: keep original
318
+ out_parts.append(json_text)
319
+ except Exception:
320
+ out_parts.append(json_text)
321
+
322
+ idx = next_idx
323
+
324
+ out_parts.append(message[idx:])
325
+ return "".join(out_parts)
326
+
327
+ def getSkillsDescs(self) -> dict[str, str]:
328
+ return {
329
+ skill.get_function_name(): skill.get_function_description()
330
+ for skill in self.getSkills()
331
+ }
332
+
333
+ def getFormattedToolsDescription(self, format_type: str = "medium") -> str:
334
+ """
335
+ Get formatted tools description for LLM prompts
336
+
337
+ Args:
338
+ format_type (str): Format type - "concise", "medium", or "detailed"
339
+
340
+ Returns:
341
+ str: Formatted tools description
342
+ """
343
+ skills = self.getSkills()
344
+ if not skills:
345
+ return "No tools available"
346
+
347
+ if format_type.lower() == "concise":
348
+ return self._formatToolsConcise(skills)
349
+ elif format_type.lower() == "medium":
350
+ return self._formatToolsMedium(skills)
351
+ elif format_type.lower() == "detailed":
352
+ return self._formatToolsDetailed(skills)
353
+ else:
354
+ return self._formatToolsMedium(skills) # Default to medium format
355
+
356
+ def _formatToolsConcise(self, skills: List[SkillFunction]) -> str:
357
+ """
358
+ Format tools in concise style: toolName - brief description
359
+ """
360
+ formatted_tools = []
361
+ for skill in skills:
362
+ name = skill.get_function_name()
363
+ desc = skill.get_function_description()
364
+ # Extract first sentence as brief description
365
+ brief_desc = desc.split(".")[0] if desc else "Tool function"
366
+ formatted_tools.append(f"- {name}: {brief_desc}")
367
+
368
+ return "\n".join(formatted_tools)
369
+
370
+ def _formatToolsMedium(self, skills: List[SkillFunction]) -> str:
371
+ """
372
+ Format tools in medium style: toolName(key_params) - description + purpose
373
+ """
374
+ formatted_tools = []
375
+ for skill in skills:
376
+ name = skill.get_function_name()
377
+ desc = skill.get_function_description()
378
+
379
+ # Extract key parameters from schema
380
+ key_params = self._extractKeyParameters(skill)
381
+ param_str = f"({key_params})" if key_params else ""
382
+
383
+ # Format: toolName(params) - description
384
+ formatted_tools.append(f"- {name}{param_str}: {desc}")
385
+
386
+ return "\n".join(formatted_tools)
387
+
388
+ def _formatToolsDetailed(self, skills: List[SkillFunction]) -> str:
389
+ """
390
+ Format tools in detailed style: full schema with parameters and types
391
+ """
392
+ formatted_tools = []
393
+ for skill in skills:
394
+ name = skill.get_function_name()
395
+ desc = skill.get_function_description()
396
+
397
+ # Get parameter details from schema
398
+ param_details = self._extractParameterDetails(skill)
399
+
400
+ tool_block = [f"**{name}**"]
401
+ tool_block.append(f" Description: {desc}")
402
+
403
+ if param_details:
404
+ tool_block.append(" Parameters:")
405
+ for param_info in param_details:
406
+ tool_block.append(f" - {param_info}")
407
+ else:
408
+ tool_block.append(" Parameters: None")
409
+
410
+ formatted_tools.append("\n".join(tool_block))
411
+
412
+ return "\n\n".join(formatted_tools)
413
+
414
+ def _extractKeyParameters(self, skill: SkillFunction) -> str:
415
+ """
416
+ Extract key parameters from skill schema for medium format
417
+ """
418
+ try:
419
+ schema = skill.get_openai_tool_schema()
420
+ if "function" in schema and "parameters" in schema["function"]:
421
+ params = schema["function"]["parameters"]
422
+ if "properties" in params:
423
+ param_names = list(params["properties"].keys())
424
+ # Show first 3 key parameters
425
+ if len(param_names) <= 3:
426
+ return ", ".join(param_names)
427
+ else:
428
+ return ", ".join(param_names[:3]) + ", ..."
429
+ return ""
430
+ except Exception:
431
+ return ""
432
+
433
+ def _extractParameterDetails(self, skill: SkillFunction) -> List[str]:
434
+ """
435
+ Extract detailed parameter information for detailed format
436
+ """
437
+ try:
438
+ schema = skill.get_openai_tool_schema()
439
+ if "function" in schema and "parameters" in schema["function"]:
440
+ params = schema["function"]["parameters"]
441
+ if "properties" in params:
442
+ param_details = []
443
+ properties = params["properties"]
444
+ required = params.get("required", [])
445
+
446
+ for param_name, param_info in properties.items():
447
+ param_type = param_info.get("type", "any")
448
+ param_desc = param_info.get("description", "")
449
+ is_required = (
450
+ " (required)" if param_name in required else " (optional)"
451
+ )
452
+
453
+ param_line = f"{param_name} ({param_type}){is_required}"
454
+ if param_desc:
455
+ param_line += f": {param_desc}"
456
+ param_details.append(param_line)
457
+
458
+ return param_details
459
+ return []
460
+ except Exception:
461
+ return []
462
+
463
+ def getSessionId(
464
+ self,
465
+ session_id: Optional[str] = None,
466
+ props: Optional[Dict[str, Any]] = None,
467
+ **kwargs,
468
+ ) -> str:
469
+ # First try to get session_id directly
470
+ if session_id:
471
+ return session_id
472
+
473
+ # Fallback to the original method via props/gvp
474
+ if props:
475
+ context = props.get("gvp")
476
+ if context and hasattr(context, "get_session_id"):
477
+ session_id = context.get_session_id()
478
+ return session_id
479
+
480
+ def get_metadata_prompt(self) -> str:
481
+ """Return metadata prompt to inject into system prompt.
482
+
483
+ Subclasses can override this to provide fixed metadata content
484
+ that should be injected into the system prompt. This is useful
485
+ for resource/guidance type skillkits that need to expose
486
+ available resources to the LLM upfront.
487
+
488
+ By default, returns an empty string (no metadata injection).
489
+
490
+ Returns:
491
+ Markdown string to append to system prompt, or empty string
492
+ if no metadata injection is needed.
493
+ """
494
+ return ""
495
+
496
+ @staticmethod
497
+ def collect_metadata_from_skills(skillkit: "Skillkit") -> str:
498
+ """Collect metadata prompts from a skillkit via skill.owner_skillkit.
499
+
500
+ This static method traverses all skills in the given skillkit and
501
+ collects metadata prompts from their owner skillkits. Only skillkits
502
+ that override get_metadata_prompt() (like ResourceSkillkit) will
503
+ return non-empty metadata.
504
+
505
+ This is the central utility for metadata collection, used by
506
+ ExploreStrategy and ExploreBlockV2 to inject metadata into system prompt.
507
+
508
+ Args:
509
+ skillkit: The skillkit containing skills to inspect
510
+
511
+ Returns:
512
+ Combined metadata prompts separated by double newlines,
513
+ or empty string if none
514
+ """
515
+ if skillkit is None:
516
+ return ""
517
+
518
+ # Safely get skills list, handling various skillkit implementations
519
+ try:
520
+ skills = skillkit.getSkills() if hasattr(skillkit, 'getSkills') else []
521
+ if not skills:
522
+ return ""
523
+ except Exception:
524
+ return ""
525
+
526
+ seen_skillkit_ids = set()
527
+ prompts = []
528
+
529
+ for skill in skills:
530
+ owner = getattr(skill, 'owner_skillkit', None)
531
+ if owner is None:
532
+ continue
533
+
534
+ owner_id = id(owner)
535
+ if owner_id in seen_skillkit_ids:
536
+ continue
537
+ seen_skillkit_ids.add(owner_id)
538
+
539
+ if hasattr(owner, 'get_metadata_prompt'):
540
+ try:
541
+ prompt = owner.get_metadata_prompt()
542
+ if prompt:
543
+ prompts.append(prompt)
544
+ except Exception:
545
+ pass
546
+
547
+ return "\n\n".join(prompts)
548
+
549
+ def isEmpty(self) -> bool:
550
+ return len(self.getSkills()) == 0
551
+
552
+ def isQueryAsArg(self) -> bool:
553
+ return self.queryAsArg
554
+
555
+ def _logAndCreateRecord(
556
+ self, skillName: str, kwargs: dict, skill: SkillFunction, result: Any
557
+ ) -> SkillExecRecord:
558
+ """Log execution result and create SkillExecRecord"""
559
+ if result is None:
560
+ raise ValueError(f"funcCall func[{skillName}] result[{result}]")
561
+
562
+ logger.info(f"funcCall func[{skillName}] result[{str(result)[:MaxLenLog]}]")
563
+ return SkillExecRecord((skillName, kwargs), skill, result)
564
+
565
+ def exec(self, skillName: str, **kwargs) -> SkillExecRecord:
566
+ skill = self.getSkill(skillName)
567
+ if skill is None:
568
+ raise ValueError(f"skill[{skillName}] not found")
569
+
570
+ result = self.run(skill, **kwargs)
571
+ return self._logAndCreateRecord(skillName, kwargs, skill, result)
572
+
573
+ async def aexec(self, skillName: str, **kwargs) -> SkillExecRecord:
574
+ """
575
+ Execute a skill by name (async version)
576
+
577
+ Args:
578
+ skillName: Name of the skill to execute
579
+ **kwargs: Arguments to pass to the skill
580
+
581
+ Returns:
582
+ SkillExecRecord containing execution results
583
+ """
584
+ skill = self.getSkill(skillName)
585
+ if skill is None:
586
+ raise ValueError(f"skill[{skillName}] not found")
587
+
588
+ # Execute the function directly for better performance
589
+ if not hasattr(skill, "func"):
590
+ raise ValueError(
591
+ f"Expected SkillFunction object with 'func' attribute, got {type(skill)}"
592
+ )
593
+
594
+ if inspect.iscoroutinefunction(skill.func):
595
+ result = await skill.func(**kwargs)
596
+ else:
597
+ result = skill.func(**kwargs)
598
+
599
+ return self._logAndCreateRecord(skillName, kwargs, skill, result)
600
+
601
+ @staticmethod
602
+ def run(func: SkillFunction, **kwargs):
603
+ # Check if the function is async and handle accordingly
604
+ if inspect.iscoroutinefunction(func.func):
605
+ # Handle async function in sync context
606
+ try:
607
+ # Try to get the current event loop
608
+ loop = asyncio.get_event_loop()
609
+ if loop.is_running():
610
+ # If we're already in an event loop, we need to handle this specially
611
+ # This is typically for MCP skills that need to run in event loop context
612
+
613
+ # Create a task and wait for it using Future
614
+ future = asyncio.Future()
615
+
616
+ def on_task_complete(task):
617
+ try:
618
+ if task.cancelled():
619
+ future.set_exception(asyncio.CancelledError())
620
+ elif task.exception():
621
+ future.set_exception(task.exception())
622
+ else:
623
+ future.set_result(task.result())
624
+ except Exception as e:
625
+ future.set_exception(e)
626
+
627
+ # Schedule the async function as a task
628
+ task = loop.create_task(func.func(**kwargs))
629
+ task.add_done_callback(on_task_complete)
630
+
631
+ # Wait for the task to complete using a polling approach
632
+ import time
633
+
634
+ timeout = 30 # 30 second timeout
635
+ start_time = time.time()
636
+
637
+ while not future.done():
638
+ if time.time() - start_time > timeout:
639
+ task.cancel()
640
+ raise asyncio.TimeoutError(
641
+ f"Async skill function timeout after {timeout}s"
642
+ )
643
+ time.sleep(0.01) # Small sleep to avoid busy waiting
644
+
645
+ # Get the result
646
+ if future.cancelled():
647
+ raise asyncio.CancelledError(
648
+ "Async skill function was cancelled"
649
+ )
650
+ elif future.exception():
651
+ raise future.exception() # type: ignore
652
+ else:
653
+ return future.result()
654
+ else:
655
+ return loop.run_until_complete(func.func(**kwargs))
656
+ except RuntimeError:
657
+ # No event loop, create a new one
658
+ return asyncio.run(func.func(**kwargs))
659
+ else:
660
+ # Handle sync function directly
661
+ return func.func(**kwargs)
662
+
663
+ @staticmethod
664
+ async def arun(skill: SkillFunction, skill_params: Optional[dict] = None, **kwargs):
665
+ """
666
+ Execute a SkillFunction skill and yield results as an async generator
667
+
668
+ Args:
669
+ skill: SkillFunction object to execute
670
+ **kwargs: Arguments to pass to the function
671
+
672
+ Yields:
673
+ Execution results from the function
674
+ """
675
+ if not hasattr(skill, "func"):
676
+ raise ValueError(
677
+ f"Expected SkillFunction object with 'func' attribute, got {type(skill)}"
678
+ )
679
+
680
+ if inspect.isasyncgenfunction(skill.func):
681
+ # For async generator functions, yield each result
682
+ # Merge parameter dictionaries to avoid duplicate keyword arguments
683
+ merged_params = {**skill_params} if skill_params else {}
684
+ merged_params.update(kwargs)
685
+ async for result in skill.func(**merged_params):
686
+ yield result
687
+
688
+ elif inspect.iscoroutinefunction(skill.func):
689
+ # For regular async functions, await and yield single result
690
+ # Merge parameter dictionaries to avoid duplicate keyword arguments
691
+ merged_params = {**skill_params} if skill_params else {}
692
+ merged_params.update(kwargs)
693
+ result = await skill.func(**merged_params)
694
+ yield result
695
+ else:
696
+ # Merge parameter dictionaries to avoid duplicate keyword arguments
697
+ merged_params = {**skill_params} if skill_params else {}
698
+ merged_params.update(kwargs)
699
+ result = skill.func(**merged_params)
700
+ yield result