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,650 @@
1
+ import json
2
+ import threading
3
+ import time
4
+ import re
5
+ from typing import Dict, List, Optional, Tuple, Any
6
+
7
+ from dolphin.core.skill.skill_function import SkillFunction
8
+ from dolphin.core.skill.skillkit import Skillkit
9
+ from dolphin.lib.memory.sandbox import MemorySandbox
10
+
11
+
12
+ # -----------------------------
13
+ # Read-Write Lock
14
+ # -----------------------------
15
+
16
+
17
+ class RWLock:
18
+ def __init__(self) -> None:
19
+ self._cond = threading.Condition(threading.Lock())
20
+ self._readers = 0
21
+ self._writer = False
22
+
23
+ def acquire_read(self) -> None:
24
+ with self._cond:
25
+ while self._writer:
26
+ self._cond.wait()
27
+ self._readers += 1
28
+
29
+ def release_read(self) -> None:
30
+ with self._cond:
31
+ self._readers -= 1
32
+ if self._readers == 0:
33
+ self._cond.notify_all()
34
+
35
+ def acquire_write(self) -> None:
36
+ with self._cond:
37
+ while self._writer or self._readers > 0:
38
+ self._cond.wait()
39
+ self._writer = True
40
+
41
+ def release_write(self) -> None:
42
+ with self._cond:
43
+ self._writer = False
44
+ self._cond.notify_all()
45
+
46
+ def rlocked(self):
47
+ class _Ctx:
48
+ def __enter__(_self):
49
+ self.acquire_read()
50
+ return _self
51
+
52
+ def __exit__(_self, exc_type, exc, tb):
53
+ self.release_read()
54
+
55
+ return _Ctx()
56
+
57
+ def wlocked(self):
58
+ class _Ctx:
59
+ def __enter__(_self):
60
+ self.acquire_write()
61
+ return _self
62
+
63
+ def __exit__(_self, exc_type, exc, tb):
64
+ self.release_write()
65
+
66
+ return _Ctx()
67
+
68
+
69
+ # -----------------------------
70
+ # Memory bucket per session
71
+ # -----------------------------
72
+
73
+
74
+ # Doc class no longer needed - using direct tree storage
75
+
76
+
77
+ class MemoryBucket:
78
+ def __init__(self) -> None:
79
+ # Hierarchical store: nested dicts; leaves are dict with keys: _value, _ts
80
+ self.root: Dict[str, Any] = {}
81
+ self.lock = RWLock()
82
+
83
+ def _ensure_path(self, path: str) -> Tuple[Dict[str, Any], str]:
84
+ parts = [p for p in path.split(".") if p]
85
+ if not parts:
86
+ raise ValueError("path must not be empty")
87
+ node = self.root
88
+ for p in parts[:-1]:
89
+ if p not in node or not isinstance(node[p], dict):
90
+ node[p] = {}
91
+ node = node[p]
92
+ return node, parts[-1]
93
+
94
+ def _node_to_text(self, path: str, value: str) -> str:
95
+ # Index both key path and value
96
+ return f"{path}\n{value or ''}"
97
+
98
+ def set_value(self, path: str, value: str) -> None:
99
+ with self.lock.wlocked():
100
+ parent, leaf = self._ensure_path(path)
101
+ ts = time.time()
102
+ # update tree
103
+ parent[leaf] = {"_value": value, "_ts": ts}
104
+
105
+ # No index needed - direct storage only
106
+
107
+ def set_dict(self, value_dict: Dict[str, Any], prefix: str = "") -> int:
108
+ """Set values from nested dict. Returns number of leaves set."""
109
+ count = 0
110
+ # Use a stack to avoid recursion depth issues on deep trees
111
+ stack: List[Tuple[str, Any]] = [(prefix, value_dict)]
112
+ while stack:
113
+ cur_prefix, obj = stack.pop()
114
+ if isinstance(obj, dict):
115
+ for k, v in obj.items():
116
+ new_prefix = f"{cur_prefix}.{k}" if cur_prefix else str(k)
117
+ if isinstance(v, dict):
118
+ stack.append((new_prefix, v))
119
+ else:
120
+ # Convert leaves to string as required
121
+ if not isinstance(v, str):
122
+ try:
123
+ v = json.dumps(v, ensure_ascii=False)
124
+ except Exception:
125
+ v = str(v)
126
+ self.set_value(new_prefix, v)
127
+ count += 1
128
+ else:
129
+ # Prefix points directly to non-dict leaf
130
+ v = obj
131
+ if not isinstance(v, str):
132
+ try:
133
+ v = json.dumps(v, ensure_ascii=False)
134
+ except Exception:
135
+ v = str(v)
136
+ if cur_prefix:
137
+ self.set_value(cur_prefix, v)
138
+ count += 1
139
+ return count
140
+
141
+ def get_value(self, path: str) -> Optional[str]:
142
+ with self.lock.rlocked():
143
+ parts = [p for p in path.split(".") if p]
144
+ node = self.root
145
+ for p in parts:
146
+ if not isinstance(node, dict) or p not in node:
147
+ return None
148
+ node = node[p]
149
+ if isinstance(node, dict) and "_value" in node:
150
+ return node.get("_value")
151
+ return None
152
+
153
+ def _collect_doc_ids_under(self, path: str) -> set:
154
+ if not path:
155
+ return {doc.doc_id for doc in self.docs_by_path.values()}
156
+ prefix = path + "."
157
+ ids = set()
158
+ for p, d in self.docs_by_path.items():
159
+ if p == path or p.startswith(prefix):
160
+ ids.add(d.doc_id)
161
+ return ids
162
+
163
+ def grep(self, path: str, pattern: str, topk: int = 10) -> List[Dict[str, Any]]:
164
+ """Search for pattern in paths and values using simple string/regex matching"""
165
+ with self.lock.rlocked():
166
+ results = []
167
+
168
+ # Determine search strategy
169
+ if pattern.startswith("/") and pattern.endswith("/") and len(pattern) > 2:
170
+ # Regex pattern
171
+ try:
172
+ regex_pattern = pattern[1:-1] # strip / /
173
+ compiled_regex = re.compile(regex_pattern, re.IGNORECASE)
174
+ use_regex = True
175
+ except re.error:
176
+ # Invalid regex, fall back to substring
177
+ use_regex = False
178
+ pattern = pattern.lower()
179
+ else:
180
+ # Simple substring matching
181
+ use_regex = False
182
+ pattern = pattern.lower()
183
+
184
+ # Scan all entries under the specified path
185
+ for entry_path, entry_data in self._iter_leaves_under_path(path):
186
+ if "_value" not in entry_data or "_ts" not in entry_data:
187
+ continue
188
+
189
+ entry_value = str(entry_data["_value"])
190
+ entry_ts = entry_data["_ts"]
191
+
192
+ # Search in both path and value
193
+ search_text = f"{entry_path}\n{entry_value}"
194
+
195
+ matched = False
196
+ if use_regex:
197
+ if compiled_regex.search(search_text):
198
+ matched = True
199
+ else:
200
+ if pattern in search_text.lower():
201
+ matched = True
202
+
203
+ if matched:
204
+ # Calculate simple relevance score
205
+ score = self._calculate_simple_score(
206
+ entry_path, entry_value, pattern, use_regex
207
+ )
208
+ results.append(
209
+ {
210
+ "path": entry_path,
211
+ "value": entry_value,
212
+ "score": score,
213
+ "ts": entry_ts,
214
+ }
215
+ )
216
+
217
+ # Sort by score (descending) and limit results
218
+ results.sort(key=lambda x: x["score"], reverse=True)
219
+ return results[:topk]
220
+
221
+ def _iter_leaves_under_path(self, path: str) -> List[Tuple[str, Dict[str, Any]]]:
222
+ """Iterate over all leaf nodes under a given path"""
223
+
224
+ def _traverse(
225
+ node: Dict[str, Any], current_path: str
226
+ ) -> List[Tuple[str, Dict[str, Any]]]:
227
+ leaves = []
228
+ for key, value in node.items():
229
+ if key.startswith("_"): # Skip metadata keys like _value, _ts
230
+ continue
231
+
232
+ new_path = f"{current_path}.{key}" if current_path else key
233
+
234
+ if isinstance(value, dict):
235
+ if "_value" in value and "_ts" in value:
236
+ # This is a leaf node
237
+ leaves.append((new_path, value))
238
+ else:
239
+ # This is an intermediate node, recurse
240
+ leaves.extend(_traverse(value, new_path))
241
+ return leaves
242
+
243
+ if path:
244
+ # Start from a specific subtree
245
+ parts = [p for p in path.split(".") if p]
246
+ node = self.root
247
+ try:
248
+ for part in parts:
249
+ node = node[part]
250
+ return _traverse(node, path)
251
+ except (KeyError, TypeError):
252
+ return []
253
+ else:
254
+ # Start from root
255
+ return _traverse(self.root, "")
256
+
257
+ def _calculate_simple_score(
258
+ self, entry_path: str, entry_value: str, pattern: str, use_regex: bool
259
+ ) -> float:
260
+ """Calculate a simple relevance score for string matching"""
261
+ if use_regex:
262
+ # For regex, just return 1.0 (binary match)
263
+ return 1.0
264
+
265
+ # For substring matching, prefer exact matches and path matches
266
+ pattern_lower = pattern.lower()
267
+ path_lower = entry_path.lower()
268
+ value_lower = entry_value.lower()
269
+
270
+ score = 0.0
271
+
272
+ # Exact value match gets highest score
273
+ if pattern_lower == value_lower:
274
+ score += 10.0
275
+ elif pattern_lower in value_lower:
276
+ # Substring in value
277
+ score += 5.0 * (len(pattern) / len(entry_value))
278
+
279
+ # Path matching gets medium score
280
+ if pattern_lower == path_lower:
281
+ score += 8.0
282
+ elif pattern_lower in path_lower:
283
+ score += 3.0 * (len(pattern) / len(entry_path))
284
+
285
+ # Bonus for shorter paths (more specific)
286
+ score += max(0, 2.0 - len(entry_path.split(".")) * 0.1)
287
+
288
+ return round(score, 6)
289
+
290
+ def remove_path(self, path: str) -> bool:
291
+ """Remove a specific path from the tree structure."""
292
+ with self.lock.wlocked():
293
+ parts = [p for p in path.split(".") if p]
294
+ if not parts:
295
+ return False
296
+
297
+ # Navigate to parent and remove leaf
298
+ try:
299
+ node = self.root
300
+ for p in parts[:-1]:
301
+ if not isinstance(node, dict) or p not in node:
302
+ return False
303
+ node = node[p]
304
+
305
+ if isinstance(node, dict) and parts[-1] in node:
306
+ node.pop(parts[-1], None)
307
+ return True
308
+ except (KeyError, TypeError):
309
+ pass
310
+
311
+ return False
312
+
313
+ def expire_old_entries(self, max_age_seconds: float) -> int:
314
+ """Remove entries older than max_age_seconds. Returns number of removed entries."""
315
+ cutoff_time = time.time() - max_age_seconds
316
+ expired_paths = []
317
+
318
+ # Find expired entries
319
+ with self.lock.rlocked():
320
+ for entry_path, entry_data in self._iter_leaves_under_path(""):
321
+ if "_ts" in entry_data and entry_data["_ts"] < cutoff_time:
322
+ expired_paths.append(entry_path)
323
+
324
+ # Remove expired entries (acquire write lock separately to avoid deadlock)
325
+ removed_count = 0
326
+ for path in expired_paths:
327
+ if self.remove_path(path):
328
+ removed_count += 1
329
+
330
+ return removed_count
331
+
332
+ def get_stats(self) -> Dict[str, Any]:
333
+ """Get bucket statistics."""
334
+ with self.lock.rlocked():
335
+ entries = list(self._iter_leaves_under_path(""))
336
+ return {
337
+ "total_entries": len(entries),
338
+ "storage_type": "simple_tree",
339
+ "search_method": "string_matching",
340
+ }
341
+
342
+ def export_dict(self) -> Dict[str, Any]:
343
+ # Export the hierarchical dict including timestamps
344
+ with self.lock.rlocked():
345
+ return json.loads(json.dumps(self.root, ensure_ascii=False))
346
+
347
+
348
+ class MemoryStore:
349
+ """Singleton-like in-process memory store keyed by session_id."""
350
+
351
+ def __init__(self) -> None:
352
+ self._buckets: Dict[str, MemoryBucket] = {}
353
+ self._lock = threading.Lock()
354
+
355
+ def get_bucket(self, session_id: str) -> MemoryBucket:
356
+ if not session_id:
357
+ raise ValueError("session_id must not be empty")
358
+ # Double-checked locking to minimize contention
359
+ bucket = self._buckets.get(session_id)
360
+ if bucket is not None:
361
+ return bucket
362
+ with self._lock:
363
+ bucket = self._buckets.get(session_id)
364
+ if bucket is None:
365
+ bucket = MemoryBucket()
366
+ self._buckets[session_id] = bucket
367
+ return bucket
368
+
369
+
370
+ _GLOBAL_STORE = MemoryStore()
371
+
372
+
373
+ class MemorySkillkit(Skillkit):
374
+ """In-memory key-value store per session with hierarchical paths and intelligent string matching.
375
+
376
+ Data structure:
377
+ - Bucketed by session_id, with a tree structure (dot-separated paths) inside each bucket.
378
+ - Each leaf node stores {'_value': str, '_ts': float} for easy expiration strategy later.
379
+ - Uses efficient string matching and regular expressions, optimized for small data scenarios, supports intelligent scoring.
380
+ - Each session bucket uses RWLock, read-shared and write-exclusive, ensuring concurrent safety and high read performance.
381
+ """
382
+
383
+ def getName(self) -> str:
384
+ return "memory_skillkit"
385
+
386
+ # -----------------------------
387
+ # Private helpers
388
+ # -----------------------------
389
+
390
+ def _get_storage_base(self) -> str:
391
+ """Get storage base path from config or use default."""
392
+ memory_config = getattr(
393
+ getattr(self, "globalConfig", None), "memory_config", None
394
+ )
395
+ return memory_config.storage_path if memory_config else "data/memory/"
396
+
397
+ # -----------------------------
398
+ # Core APIs
399
+ # -----------------------------
400
+
401
+ def _mem_set(self, path: str, value: str, **kwargs) -> str:
402
+ """Set the string value at the specified path in mem.
403
+
404
+ Args:
405
+ path (str): The path separated by dots, for example "user.profile.name".
406
+ value (str): The string value to set.
407
+
408
+ Returns:
409
+ str: A JSON string, for example {"success": true}
410
+ """
411
+ session_id = self.getSessionId(
412
+ session_id=kwargs.get("session_id"), props=kwargs.get("props")
413
+ )
414
+ bucket = _GLOBAL_STORE.get_bucket(session_id)
415
+ bucket.set_value(path, value)
416
+ return json.dumps({"success": True}, ensure_ascii=False)
417
+
418
+ def _mem_set_dict(self, value_dict: Dict[str, Any], **kwargs) -> str:
419
+ """Batch set multiple path values in mem, supporting nested dictionaries; leaf values will be converted to strings.
420
+
421
+ Args:
422
+ value_dict (dict): Nested dictionary structure, leaves are of any type (will be automatically converted to strings).
423
+
424
+ Returns:
425
+ str: JSON string, for example {"success": true, "updated": 3}
426
+ """
427
+ if not isinstance(value_dict, dict):
428
+ raise ValueError("value_dict must be a dict")
429
+ session_id = self.getSessionId(
430
+ session_id=kwargs.get("session_id"), props=kwargs.get("props")
431
+ )
432
+ bucket = _GLOBAL_STORE.get_bucket(session_id)
433
+ updated = bucket.set_dict(value_dict)
434
+ return json.dumps({"success": True, "updated": updated}, ensure_ascii=False)
435
+
436
+ def _mem_get(self, path: str, **kwargs) -> str:
437
+ """Get the string value at the specified path from mem.
438
+
439
+ Args:
440
+ path (str): Path separated by dots.
441
+
442
+ Returns:
443
+ str: JSON string, for example {"success": true, "found": true, "value": "..."}
444
+ """
445
+ session_id = self.getSessionId(
446
+ session_id=kwargs.get("session_id"), props=kwargs.get("props")
447
+ )
448
+ bucket = _GLOBAL_STORE.get_bucket(session_id)
449
+ val = bucket.get_value(path)
450
+ return json.dumps(
451
+ {"success": True, "found": val is not None, "value": val or ""},
452
+ ensure_ascii=False,
453
+ )
454
+
455
+ def _mem_grep(self, path: str, pattern: str, **kwargs) -> str:
456
+ """Perform intelligent pattern matching and recall under the specified path (subtree) in mem.
457
+
458
+ Args:
459
+ path (str): The root path of the search scope; an empty string indicates the entire session bucket.
460
+ pattern (str): The retrieval pattern. Plain strings perform substring matching, while patterns wrapped in /.../ are matched as regular expressions.
461
+
462
+ Returns:
463
+ str: A JSON string in the format {"success": true, "results": [{path, value, score, ts}]}
464
+ Results are sorted by intelligent scoring: exact match > path match > value contains > path contains.
465
+ """
466
+ session_id = self.getSessionId(
467
+ session_id=kwargs.get("session_id"), props=kwargs.get("props")
468
+ )
469
+ bucket = _GLOBAL_STORE.get_bucket(session_id)
470
+ results = bucket.grep(path or "", pattern, topk=10)
471
+ return json.dumps({"success": True, "results": results}, ensure_ascii=False)
472
+
473
+ def _mem_save(self, local_filepath: str, **kwargs) -> str:
474
+ """Save the mem dictionary of the current session to a JSON file in the session sandbox.
475
+
476
+ Args:
477
+ local_filepath (str): File path relative to the session sandbox (must be .json).
478
+
479
+ Returns:
480
+ str: JSON string, for example {"success": true, "path": "..."}
481
+ """
482
+ session_id = self.getSessionId(
483
+ session_id=kwargs.get("session_id"), props=kwargs.get("props")
484
+ )
485
+ try:
486
+ bucket = _GLOBAL_STORE.get_bucket(session_id)
487
+ data = bucket.export_dict()
488
+
489
+ # Resolve sandbox path
490
+ storage_base = self._get_storage_base()
491
+ sandbox = MemorySandbox(storage_base)
492
+ safe_path = sandbox.resolve_session_path(session_id, local_filepath)
493
+
494
+ payload = json.dumps(data, ensure_ascii=False, indent=2)
495
+ sandbox.check_size_bytes(len(payload.encode("utf-8")))
496
+
497
+ with open(safe_path, "w", encoding="utf-8") as f:
498
+ f.write(payload)
499
+ return json.dumps(
500
+ {"success": True, "path": str(safe_path)}, ensure_ascii=False
501
+ )
502
+ except Exception as e:
503
+ return json.dumps({"success": False, "error": str(e)}, ensure_ascii=False)
504
+
505
+ def _mem_remove(self, path: str, **kwargs) -> str:
506
+ """Remove data at the specified path from mem.
507
+
508
+ Args:
509
+ path (str): Dot-separated path to be removed.
510
+
511
+ Returns:
512
+ str: Operation result as a JSON string, for example {"success": true, "removed": true}
513
+ """
514
+ session_id = self.getSessionId(
515
+ session_id=kwargs.get("session_id"), props=kwargs.get("props")
516
+ )
517
+ bucket = _GLOBAL_STORE.get_bucket(session_id)
518
+ removed = bucket.remove_path(path)
519
+ return json.dumps({"success": True, "removed": removed}, ensure_ascii=False)
520
+
521
+ def _mem_expire(self, max_age_seconds: float, **kwargs) -> str:
522
+ """Clean up expired data in mem for a specified session that exceeds a specified time.
523
+
524
+ Args:
525
+ max_age_seconds (float): Maximum age (in seconds), data older than this will be deleted.
526
+
527
+ Returns:
528
+ str: Operation result, JSON string, e.g., {"success": true, "expired_count": 5}
529
+ """
530
+ session_id = self.getSessionId(
531
+ session_id=kwargs.get("session_id"), props=kwargs.get("props")
532
+ )
533
+ bucket = _GLOBAL_STORE.get_bucket(session_id)
534
+ expired_count = bucket.expire_old_entries(max_age_seconds)
535
+ return json.dumps(
536
+ {"success": True, "expired_count": expired_count}, ensure_ascii=False
537
+ )
538
+
539
+ def _mem_stats(self, **kwargs) -> str:
540
+ """Get the storage statistics for a specified session in mem.
541
+
542
+ Args:
543
+ None
544
+
545
+ Returns:
546
+ str: Statistics in JSON format, containing {success, total_entries, storage_type, search_method}
547
+ """
548
+ session_id = self.getSessionId(
549
+ session_id=kwargs.get("session_id"), props=kwargs.get("props")
550
+ )
551
+ bucket = _GLOBAL_STORE.get_bucket(session_id)
552
+ stats = bucket.get_stats()
553
+ stats_out = {"success": True}
554
+ stats_out.update(stats)
555
+ return json.dumps(stats_out, ensure_ascii=False)
556
+
557
+ def _mem_view(self, path: str = "", **kwargs) -> str:
558
+ """View the contents or directory structure at the specified path.
559
+
560
+ Args:
561
+ path: Dot-separated path, empty string represents the root directory
562
+
563
+ Returns:
564
+ str: JSON string
565
+ - File: {"success": true, "type": "file", "value": "..."}
566
+ - Directory: {"success": true, "type": "directory", "children": ["name", "age", "profile"]}
567
+ """
568
+ session_id = self.getSessionId(
569
+ session_id=kwargs.get("session_id"), props=kwargs.get("props")
570
+ )
571
+ bucket = _GLOBAL_STORE.get_bucket(session_id)
572
+
573
+ if not path:
574
+ keys = [k for k in bucket.root.keys() if not str(k).startswith("_")]
575
+ return json.dumps(
576
+ {"success": True, "type": "directory", "children": keys},
577
+ ensure_ascii=False,
578
+ )
579
+
580
+ parts = [p for p in path.split(".") if p]
581
+ node = bucket.root
582
+ for p in parts:
583
+ if not isinstance(node, dict) or p not in node:
584
+ return json.dumps(
585
+ {"success": False, "error": "path not found"}, ensure_ascii=False
586
+ )
587
+ node = node[p]
588
+
589
+ if isinstance(node, dict) and "_value" in node:
590
+ return json.dumps(
591
+ {"success": True, "type": "file", "value": node.get("_value", "")},
592
+ ensure_ascii=False,
593
+ )
594
+ if isinstance(node, dict):
595
+ keys = [k for k in node.keys() if not str(k).startswith("_")]
596
+ return json.dumps(
597
+ {"success": True, "type": "directory", "children": keys},
598
+ ensure_ascii=False,
599
+ )
600
+ return json.dumps(
601
+ {"success": False, "error": "invalid node"}, ensure_ascii=False
602
+ )
603
+
604
+ def _mem_load(self, local_filepath: str, **kwargs) -> str:
605
+ """Load data from a JSON file in the session sandbox into memory (overwriting import).
606
+
607
+ Args:
608
+ local_filepath: Relative JSON file path to the session sandbox
609
+
610
+ Returns:
611
+ str: JSON string, for example {"success": true, "entries_loaded": 10}
612
+ """
613
+ session_id = self.getSessionId(
614
+ session_id=kwargs.get("session_id"), props=kwargs.get("props")
615
+ )
616
+ try:
617
+ bucket = _GLOBAL_STORE.get_bucket(session_id)
618
+ storage_base = self._get_storage_base()
619
+ sandbox = MemorySandbox(storage_base)
620
+ safe_path = sandbox.resolve_session_path(session_id, local_filepath)
621
+ with open(safe_path, "r", encoding="utf-8") as f:
622
+ content = f.read()
623
+ sandbox.check_size_bytes(len(content.encode("utf-8")))
624
+ data = json.loads(content)
625
+ # IMPORTANT: Complete overwrite (not merge) - this replaces all existing data
626
+ bucket.root = data if isinstance(data, dict) else {}
627
+ entries_loaded = len(bucket._iter_leaves_under_path(""))
628
+ return json.dumps(
629
+ {"success": True, "entries_loaded": entries_loaded}, ensure_ascii=False
630
+ )
631
+ except Exception as e:
632
+ return json.dumps({"success": False, "error": str(e)}, ensure_ascii=False)
633
+
634
+ # -----------------------------
635
+ # Skill exports
636
+ # -----------------------------
637
+
638
+ def _createSkills(self) -> List[SkillFunction]:
639
+ return [
640
+ SkillFunction(self._mem_set),
641
+ SkillFunction(self._mem_set_dict),
642
+ SkillFunction(self._mem_get),
643
+ SkillFunction(self._mem_grep),
644
+ SkillFunction(self._mem_view),
645
+ SkillFunction(self._mem_load),
646
+ SkillFunction(self._mem_save),
647
+ SkillFunction(self._mem_remove),
648
+ SkillFunction(self._mem_expire),
649
+ SkillFunction(self._mem_stats),
650
+ ]
@@ -0,0 +1,31 @@
1
+ from typing import List
2
+ from dolphin.core.skill.skill_function import SkillFunction
3
+ from dolphin.core.skill.skillkit import Skillkit
4
+
5
+
6
+ class NoopSkillkit(Skillkit):
7
+ """
8
+ just for test
9
+ """
10
+
11
+ def __init__(self):
12
+ super().__init__()
13
+ self.globalContext = None
14
+
15
+ def getName(self) -> str:
16
+ return " noop_skillkit"
17
+
18
+ def noop_calling(self, **kwargs) -> str:
19
+ """Do nothing, for testing
20
+
21
+ Args:
22
+ None
23
+
24
+ Returns:
25
+ str: Do nothing, for testing
26
+ """
27
+ print("do nothing")
28
+ return "do nothing"
29
+
30
+ def _createSkills(self) -> List[SkillFunction]:
31
+ return [SkillFunction(self.noop_calling)]