kweaver-dolphin 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- DolphinLanguageSDK/__init__.py +58 -0
- dolphin/__init__.py +62 -0
- dolphin/cli/__init__.py +20 -0
- dolphin/cli/args/__init__.py +9 -0
- dolphin/cli/args/parser.py +567 -0
- dolphin/cli/builtin_agents/__init__.py +22 -0
- dolphin/cli/commands/__init__.py +4 -0
- dolphin/cli/interrupt/__init__.py +8 -0
- dolphin/cli/interrupt/handler.py +205 -0
- dolphin/cli/interrupt/keyboard.py +82 -0
- dolphin/cli/main.py +49 -0
- dolphin/cli/multimodal/__init__.py +34 -0
- dolphin/cli/multimodal/clipboard.py +327 -0
- dolphin/cli/multimodal/handler.py +249 -0
- dolphin/cli/multimodal/image_processor.py +214 -0
- dolphin/cli/multimodal/input_parser.py +149 -0
- dolphin/cli/runner/__init__.py +8 -0
- dolphin/cli/runner/runner.py +989 -0
- dolphin/cli/ui/__init__.py +10 -0
- dolphin/cli/ui/console.py +2795 -0
- dolphin/cli/ui/input.py +340 -0
- dolphin/cli/ui/layout.py +425 -0
- dolphin/cli/ui/stream_renderer.py +302 -0
- dolphin/cli/utils/__init__.py +8 -0
- dolphin/cli/utils/helpers.py +135 -0
- dolphin/cli/utils/version.py +49 -0
- dolphin/core/__init__.py +107 -0
- dolphin/core/agent/__init__.py +10 -0
- dolphin/core/agent/agent_state.py +69 -0
- dolphin/core/agent/base_agent.py +970 -0
- dolphin/core/code_block/__init__.py +0 -0
- dolphin/core/code_block/agent_init_block.py +0 -0
- dolphin/core/code_block/assign_block.py +98 -0
- dolphin/core/code_block/basic_code_block.py +1865 -0
- dolphin/core/code_block/explore_block.py +1327 -0
- dolphin/core/code_block/explore_block_v2.py +712 -0
- dolphin/core/code_block/explore_strategy.py +672 -0
- dolphin/core/code_block/judge_block.py +220 -0
- dolphin/core/code_block/prompt_block.py +32 -0
- dolphin/core/code_block/skill_call_deduplicator.py +291 -0
- dolphin/core/code_block/tool_block.py +129 -0
- dolphin/core/common/__init__.py +17 -0
- dolphin/core/common/constants.py +176 -0
- dolphin/core/common/enums.py +1173 -0
- dolphin/core/common/exceptions.py +133 -0
- dolphin/core/common/multimodal.py +539 -0
- dolphin/core/common/object_type.py +165 -0
- dolphin/core/common/output_format.py +432 -0
- dolphin/core/common/types.py +36 -0
- dolphin/core/config/__init__.py +16 -0
- dolphin/core/config/global_config.py +1289 -0
- dolphin/core/config/ontology_config.py +133 -0
- dolphin/core/context/__init__.py +12 -0
- dolphin/core/context/context.py +1580 -0
- dolphin/core/context/context_manager.py +161 -0
- dolphin/core/context/var_output.py +82 -0
- dolphin/core/context/variable_pool.py +356 -0
- dolphin/core/context_engineer/__init__.py +41 -0
- dolphin/core/context_engineer/config/__init__.py +5 -0
- dolphin/core/context_engineer/config/settings.py +402 -0
- dolphin/core/context_engineer/core/__init__.py +7 -0
- dolphin/core/context_engineer/core/budget_manager.py +327 -0
- dolphin/core/context_engineer/core/context_assembler.py +583 -0
- dolphin/core/context_engineer/core/context_manager.py +637 -0
- dolphin/core/context_engineer/core/tokenizer_service.py +260 -0
- dolphin/core/context_engineer/example/incremental_example.py +267 -0
- dolphin/core/context_engineer/example/traditional_example.py +334 -0
- dolphin/core/context_engineer/services/__init__.py +5 -0
- dolphin/core/context_engineer/services/compressor.py +399 -0
- dolphin/core/context_engineer/utils/__init__.py +6 -0
- dolphin/core/context_engineer/utils/context_utils.py +441 -0
- dolphin/core/context_engineer/utils/message_formatter.py +270 -0
- dolphin/core/context_engineer/utils/token_utils.py +139 -0
- dolphin/core/coroutine/__init__.py +15 -0
- dolphin/core/coroutine/context_snapshot.py +154 -0
- dolphin/core/coroutine/context_snapshot_profile.py +922 -0
- dolphin/core/coroutine/context_snapshot_store.py +268 -0
- dolphin/core/coroutine/execution_frame.py +145 -0
- dolphin/core/coroutine/execution_state_registry.py +161 -0
- dolphin/core/coroutine/resume_handle.py +101 -0
- dolphin/core/coroutine/step_result.py +101 -0
- dolphin/core/executor/__init__.py +18 -0
- dolphin/core/executor/debug_controller.py +630 -0
- dolphin/core/executor/dolphin_executor.py +1063 -0
- dolphin/core/executor/executor.py +624 -0
- dolphin/core/flags/__init__.py +27 -0
- dolphin/core/flags/definitions.py +49 -0
- dolphin/core/flags/manager.py +113 -0
- dolphin/core/hook/__init__.py +95 -0
- dolphin/core/hook/expression_evaluator.py +499 -0
- dolphin/core/hook/hook_dispatcher.py +380 -0
- dolphin/core/hook/hook_types.py +248 -0
- dolphin/core/hook/isolated_variable_pool.py +284 -0
- dolphin/core/interfaces.py +53 -0
- dolphin/core/llm/__init__.py +0 -0
- dolphin/core/llm/llm.py +495 -0
- dolphin/core/llm/llm_call.py +100 -0
- dolphin/core/llm/llm_client.py +1285 -0
- dolphin/core/llm/message_sanitizer.py +120 -0
- dolphin/core/logging/__init__.py +20 -0
- dolphin/core/logging/logger.py +526 -0
- dolphin/core/message/__init__.py +8 -0
- dolphin/core/message/compressor.py +749 -0
- dolphin/core/parser/__init__.py +8 -0
- dolphin/core/parser/parser.py +405 -0
- dolphin/core/runtime/__init__.py +10 -0
- dolphin/core/runtime/runtime_graph.py +926 -0
- dolphin/core/runtime/runtime_instance.py +446 -0
- dolphin/core/skill/__init__.py +14 -0
- dolphin/core/skill/context_retention.py +157 -0
- dolphin/core/skill/skill_function.py +686 -0
- dolphin/core/skill/skill_matcher.py +282 -0
- dolphin/core/skill/skillkit.py +700 -0
- dolphin/core/skill/skillset.py +72 -0
- dolphin/core/trajectory/__init__.py +10 -0
- dolphin/core/trajectory/recorder.py +189 -0
- dolphin/core/trajectory/trajectory.py +522 -0
- dolphin/core/utils/__init__.py +9 -0
- dolphin/core/utils/cache_kv.py +212 -0
- dolphin/core/utils/tools.py +340 -0
- dolphin/lib/__init__.py +93 -0
- dolphin/lib/debug/__init__.py +8 -0
- dolphin/lib/debug/visualizer.py +409 -0
- dolphin/lib/memory/__init__.py +28 -0
- dolphin/lib/memory/async_processor.py +220 -0
- dolphin/lib/memory/llm_calls.py +195 -0
- dolphin/lib/memory/manager.py +78 -0
- dolphin/lib/memory/sandbox.py +46 -0
- dolphin/lib/memory/storage.py +245 -0
- dolphin/lib/memory/utils.py +51 -0
- dolphin/lib/ontology/__init__.py +12 -0
- dolphin/lib/ontology/basic/__init__.py +0 -0
- dolphin/lib/ontology/basic/base.py +102 -0
- dolphin/lib/ontology/basic/concept.py +130 -0
- dolphin/lib/ontology/basic/object.py +11 -0
- dolphin/lib/ontology/basic/relation.py +63 -0
- dolphin/lib/ontology/datasource/__init__.py +27 -0
- dolphin/lib/ontology/datasource/datasource.py +66 -0
- dolphin/lib/ontology/datasource/oracle_datasource.py +338 -0
- dolphin/lib/ontology/datasource/sql.py +845 -0
- dolphin/lib/ontology/mapping.py +177 -0
- dolphin/lib/ontology/ontology.py +733 -0
- dolphin/lib/ontology/ontology_context.py +16 -0
- dolphin/lib/ontology/ontology_manager.py +107 -0
- dolphin/lib/skill_results/__init__.py +31 -0
- dolphin/lib/skill_results/cache_backend.py +559 -0
- dolphin/lib/skill_results/result_processor.py +181 -0
- dolphin/lib/skill_results/result_reference.py +179 -0
- dolphin/lib/skill_results/skillkit_hook.py +324 -0
- dolphin/lib/skill_results/strategies.py +328 -0
- dolphin/lib/skill_results/strategy_registry.py +150 -0
- dolphin/lib/skillkits/__init__.py +44 -0
- dolphin/lib/skillkits/agent_skillkit.py +155 -0
- dolphin/lib/skillkits/cognitive_skillkit.py +82 -0
- dolphin/lib/skillkits/env_skillkit.py +250 -0
- dolphin/lib/skillkits/mcp_adapter.py +616 -0
- dolphin/lib/skillkits/mcp_skillkit.py +771 -0
- dolphin/lib/skillkits/memory_skillkit.py +650 -0
- dolphin/lib/skillkits/noop_skillkit.py +31 -0
- dolphin/lib/skillkits/ontology_skillkit.py +89 -0
- dolphin/lib/skillkits/plan_act_skillkit.py +452 -0
- dolphin/lib/skillkits/resource/__init__.py +52 -0
- dolphin/lib/skillkits/resource/models/__init__.py +6 -0
- dolphin/lib/skillkits/resource/models/skill_config.py +109 -0
- dolphin/lib/skillkits/resource/models/skill_meta.py +127 -0
- dolphin/lib/skillkits/resource/resource_skillkit.py +393 -0
- dolphin/lib/skillkits/resource/skill_cache.py +215 -0
- dolphin/lib/skillkits/resource/skill_loader.py +395 -0
- dolphin/lib/skillkits/resource/skill_validator.py +406 -0
- dolphin/lib/skillkits/resource_skillkit.py +11 -0
- dolphin/lib/skillkits/search_skillkit.py +163 -0
- dolphin/lib/skillkits/sql_skillkit.py +274 -0
- dolphin/lib/skillkits/system_skillkit.py +509 -0
- dolphin/lib/skillkits/vm_skillkit.py +65 -0
- dolphin/lib/utils/__init__.py +9 -0
- dolphin/lib/utils/data_process.py +207 -0
- dolphin/lib/utils/handle_progress.py +178 -0
- dolphin/lib/utils/security.py +139 -0
- dolphin/lib/utils/text_retrieval.py +462 -0
- dolphin/lib/vm/__init__.py +11 -0
- dolphin/lib/vm/env_executor.py +895 -0
- dolphin/lib/vm/python_session_manager.py +453 -0
- dolphin/lib/vm/vm.py +610 -0
- dolphin/sdk/__init__.py +60 -0
- dolphin/sdk/agent/__init__.py +12 -0
- dolphin/sdk/agent/agent_factory.py +236 -0
- dolphin/sdk/agent/dolphin_agent.py +1106 -0
- dolphin/sdk/api/__init__.py +4 -0
- dolphin/sdk/runtime/__init__.py +8 -0
- dolphin/sdk/runtime/env.py +363 -0
- dolphin/sdk/skill/__init__.py +10 -0
- dolphin/sdk/skill/global_skills.py +706 -0
- dolphin/sdk/skill/traditional_toolkit.py +260 -0
- kweaver_dolphin-0.1.0.dist-info/METADATA +521 -0
- kweaver_dolphin-0.1.0.dist-info/RECORD +199 -0
- kweaver_dolphin-0.1.0.dist-info/WHEEL +5 -0
- kweaver_dolphin-0.1.0.dist-info/entry_points.txt +27 -0
- kweaver_dolphin-0.1.0.dist-info/licenses/LICENSE.txt +201 -0
- kweaver_dolphin-0.1.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,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)]
|