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,1580 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Dict, List, Optional, Any, Union, TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from dolphin.core.common.enums import MessageRole, SkillInfo, Messages, SkillType
|
|
8
|
+
from dolphin.core.config.global_config import GlobalConfig
|
|
9
|
+
from dolphin.core.common.constants import (
|
|
10
|
+
KEY_MAX_ANSWER_CONTENT_LENGTH,
|
|
11
|
+
KEY_SESSION_ID,
|
|
12
|
+
KEY_USER_ID,
|
|
13
|
+
MAX_ANSWER_CONTENT_LENGTH,
|
|
14
|
+
MAX_LOG_LENGTH,
|
|
15
|
+
)
|
|
16
|
+
from dolphin.core.context_engineer.core.context_manager import (
|
|
17
|
+
ContextManager,
|
|
18
|
+
)
|
|
19
|
+
from dolphin.core.runtime.runtime_graph import RuntimeGraph
|
|
20
|
+
from dolphin.core.skill.skill_function import SkillFunction
|
|
21
|
+
from dolphin.core.skill.skillkit import Skillkit
|
|
22
|
+
from dolphin.core.skill.skillset import Skillset
|
|
23
|
+
from dolphin.core.skill.skill_matcher import SkillMatcher
|
|
24
|
+
|
|
25
|
+
from dolphin.core.common.types import Var
|
|
26
|
+
from dolphin.core.context.var_output import VarOutput, SourceType
|
|
27
|
+
from dolphin.core.context.variable_pool import VariablePool
|
|
28
|
+
from dolphin.core.logging.logger import get_logger
|
|
29
|
+
from dolphin.core.trajectory.trajectory import Trajectory
|
|
30
|
+
|
|
31
|
+
# Import sdk/lib modules under TYPE_CHECKING to avoid circular imports
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from dolphin.sdk.skill.global_skills import GlobalSkills
|
|
34
|
+
from dolphin.lib.memory.manager import MemoryManager
|
|
35
|
+
from dolphin.lib.skill_results.skillkit_hook import SkillkitHook
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
logger = get_logger("context")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Context:
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
config: Optional[GlobalConfig] = None,
|
|
45
|
+
global_skills: Optional["GlobalSkills"] = None,
|
|
46
|
+
memory_manager: Optional["MemoryManager"] = None,
|
|
47
|
+
global_types=None,
|
|
48
|
+
skillkit_hook: Optional["SkillkitHook"] = None,
|
|
49
|
+
context_manager: Optional[ContextManager] = None,
|
|
50
|
+
verbose: bool = False,
|
|
51
|
+
is_cli: bool = False,
|
|
52
|
+
):
|
|
53
|
+
self.config = config
|
|
54
|
+
self.global_skills = global_skills
|
|
55
|
+
self.memory_manager = memory_manager
|
|
56
|
+
self.global_types = global_types
|
|
57
|
+
self.variable_pool = VariablePool()
|
|
58
|
+
self.skillkit = Skillkit()
|
|
59
|
+
self.messages: Dict[str, Messages] = (
|
|
60
|
+
dict()
|
|
61
|
+
) # Use Messages class instead of list
|
|
62
|
+
self.messages_dirty: bool = True # Initially dirty, ensure synchronization on first use
|
|
63
|
+
self.user_id = None
|
|
64
|
+
self.session_id = None
|
|
65
|
+
self.cur_agent = None
|
|
66
|
+
self.max_answer_len = MAX_ANSWER_CONTENT_LENGTH
|
|
67
|
+
self.verbose = verbose # Control log detail level
|
|
68
|
+
self.is_cli = is_cli # Control CLI rendering (Rich/Live Markdown)
|
|
69
|
+
|
|
70
|
+
self.time_extracted_knowledge = None
|
|
71
|
+
|
|
72
|
+
self.runtime_graph = RuntimeGraph()
|
|
73
|
+
|
|
74
|
+
# Initialize all_skills to avoid AttributeError
|
|
75
|
+
self.all_skills = Skillset()
|
|
76
|
+
|
|
77
|
+
# Initialize skillkit_hook
|
|
78
|
+
if skillkit_hook is not None:
|
|
79
|
+
self.skillkit_hook = skillkit_hook
|
|
80
|
+
else:
|
|
81
|
+
self.skillkit_hook = None
|
|
82
|
+
|
|
83
|
+
# Initialize context_manager
|
|
84
|
+
self.context_manager = context_manager or ContextManager()
|
|
85
|
+
|
|
86
|
+
# Initialize trajectory (disabled by default, enabled via init_trajectory)
|
|
87
|
+
self.trajectory: Optional[Trajectory] = None
|
|
88
|
+
|
|
89
|
+
# Historical injection idempotency flag: avoid re-injecting variable history within the same context lifecycle
|
|
90
|
+
self.history_injected: bool = False
|
|
91
|
+
|
|
92
|
+
# The name of the model used last (to maintain model consistency in multi-turn conversations)
|
|
93
|
+
self._last_model_name: Optional[str] = None
|
|
94
|
+
|
|
95
|
+
# The final skills configuration used (inherited skill filtering configuration for multi-turn conversations)
|
|
96
|
+
self._last_skills: Optional[List[str]] = None
|
|
97
|
+
|
|
98
|
+
# The final exploration mode used (when conducting multi-turn conversations, inherits the mode configuration)
|
|
99
|
+
self._last_explore_mode: Optional[str] = None
|
|
100
|
+
|
|
101
|
+
# The last system prompt used (to maintain consistent system bucket across multi-turn conversations)
|
|
102
|
+
self._last_system_prompt: Optional[str] = None
|
|
103
|
+
|
|
104
|
+
# User interrupt event (injected by Agent layer for cooperative cancellation)
|
|
105
|
+
self._interrupt_event: Optional[asyncio.Event] = None
|
|
106
|
+
|
|
107
|
+
def set_skillkit_hook(self, skillkit_hook: "SkillkitHook"):
|
|
108
|
+
"""Set skillkit_hook"""
|
|
109
|
+
self.skillkit_hook = skillkit_hook
|
|
110
|
+
logger.debug("skillkit_hook has been set")
|
|
111
|
+
|
|
112
|
+
def get_skillkit_hook(self) -> Optional["SkillkitHook"]:
|
|
113
|
+
"""Get skillkit_hook"""
|
|
114
|
+
return self.skillkit_hook
|
|
115
|
+
|
|
116
|
+
def has_skillkit_hook(self) -> bool:
|
|
117
|
+
"""Check if skillkit_hook exists"""
|
|
118
|
+
return self.skillkit_hook is not None
|
|
119
|
+
|
|
120
|
+
def init_trajectory(self, trajectory_path: str, overwrite: bool = True):
|
|
121
|
+
"""Initialize trajectory recording
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
trajectory_path: Path to save the trajectory file
|
|
125
|
+
overwrite: Whether to overwrite existing trajectory files (default is True)
|
|
126
|
+
"""
|
|
127
|
+
self.trajectory = Trajectory(trajectory_path, overwrite=overwrite)
|
|
128
|
+
logger.debug(f"Trajectory initialized: {trajectory_path} (overwrite={overwrite})")
|
|
129
|
+
|
|
130
|
+
def copy(self):
|
|
131
|
+
copied = Context(
|
|
132
|
+
config=self.config,
|
|
133
|
+
global_skills=self.global_skills,
|
|
134
|
+
memory_manager=self.memory_manager,
|
|
135
|
+
global_types=self.global_types,
|
|
136
|
+
verbose=self.verbose,
|
|
137
|
+
is_cli=self.is_cli,
|
|
138
|
+
)
|
|
139
|
+
copied.variable_pool = self.variable_pool.copy()
|
|
140
|
+
copied.skillkit = self.skillkit
|
|
141
|
+
# Copy Messages object
|
|
142
|
+
copied.messages = {}
|
|
143
|
+
for agent_name, messages in self.messages.items():
|
|
144
|
+
copied.messages[agent_name] = messages.copy()
|
|
145
|
+
# copied.compressed_messages = self.compressed_messages # Commented out this line because the attribute does not exist
|
|
146
|
+
copied.cur_agent = self.cur_agent
|
|
147
|
+
copied.user_id = self.user_id
|
|
148
|
+
copied.session_id = self.session_id
|
|
149
|
+
copied.cur_agent = self.cur_agent
|
|
150
|
+
copied.max_answer_len = self.max_answer_len
|
|
151
|
+
copied.verbose = self.verbose
|
|
152
|
+
copied.is_cli = self.is_cli
|
|
153
|
+
copied.runtime_graph = self.runtime_graph.copy()
|
|
154
|
+
return copied
|
|
155
|
+
|
|
156
|
+
def set_cur_agent(self, agent):
|
|
157
|
+
self.cur_agent = agent
|
|
158
|
+
self.runtime_graph.set_agent(agent)
|
|
159
|
+
|
|
160
|
+
def get_cur_agent(self):
|
|
161
|
+
return self.cur_agent
|
|
162
|
+
|
|
163
|
+
def get_cur_agent_name(self):
|
|
164
|
+
return self.cur_agent.get_name() if self.cur_agent else None
|
|
165
|
+
|
|
166
|
+
def set_user_id(self, user_id: str):
|
|
167
|
+
self.user_id = user_id
|
|
168
|
+
|
|
169
|
+
def get_user_id(self):
|
|
170
|
+
return self.user_id
|
|
171
|
+
|
|
172
|
+
def set_session_id(self, session_id: str):
|
|
173
|
+
self.session_id = session_id
|
|
174
|
+
|
|
175
|
+
def get_session_id(self):
|
|
176
|
+
return self.session_id
|
|
177
|
+
|
|
178
|
+
def set_max_answer_len(self, max_answer_len: int):
|
|
179
|
+
self.max_answer_len = max_answer_len
|
|
180
|
+
|
|
181
|
+
def get_max_answer_len(self):
|
|
182
|
+
return self.max_answer_len
|
|
183
|
+
|
|
184
|
+
def set_verbose(self, verbose: bool):
|
|
185
|
+
"""Set verbose mode"""
|
|
186
|
+
self.verbose = verbose
|
|
187
|
+
|
|
188
|
+
def get_verbose(self) -> bool:
|
|
189
|
+
"""Get verbose mode status"""
|
|
190
|
+
return self.verbose
|
|
191
|
+
|
|
192
|
+
def is_verbose(self) -> bool:
|
|
193
|
+
"""Check if verbose mode is enabled (detailed logging)"""
|
|
194
|
+
return self.verbose
|
|
195
|
+
|
|
196
|
+
def set_cli_mode(self, is_cli: bool):
|
|
197
|
+
"""Set CLI mode (controls Rich/terminal beautification)"""
|
|
198
|
+
self.is_cli = is_cli
|
|
199
|
+
|
|
200
|
+
def is_cli_mode(self) -> bool:
|
|
201
|
+
"""Check if running in CLI mode (controls Rich/terminal beautification)
|
|
202
|
+
|
|
203
|
+
This is separate from verbose:
|
|
204
|
+
- verbose: controls log detail level
|
|
205
|
+
- is_cli: controls terminal beautification (Rich Live Markdown, colors, etc.)
|
|
206
|
+
|
|
207
|
+
Use cases:
|
|
208
|
+
- verbose=True, is_cli=True: CLI with detailed beautiful output
|
|
209
|
+
- verbose=True, is_cli=False: API/script with detailed plain text logs
|
|
210
|
+
- verbose=False, is_cli=True: CLI with quiet but beautiful output
|
|
211
|
+
- verbose=False, is_cli=False: API/script in silent mode
|
|
212
|
+
"""
|
|
213
|
+
return self.is_cli
|
|
214
|
+
|
|
215
|
+
def get_config(self):
|
|
216
|
+
return self.config
|
|
217
|
+
|
|
218
|
+
def get_memory_manager(self):
|
|
219
|
+
return self.memory_manager
|
|
220
|
+
|
|
221
|
+
def get_global_types(self):
|
|
222
|
+
return self.global_types
|
|
223
|
+
|
|
224
|
+
def get_history_messages(self, normalize: bool = True):
|
|
225
|
+
"""Get history, default normalized to Messages without modifying the original storage structure of the variable pool.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
normalize: Whether to convert history into a Messages object
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Messages or the original stored history
|
|
232
|
+
"""
|
|
233
|
+
history_raw = self.get_var_value("history")
|
|
234
|
+
if not normalize:
|
|
235
|
+
return history_raw
|
|
236
|
+
|
|
237
|
+
if isinstance(history_raw, Messages):
|
|
238
|
+
return history_raw.copy()
|
|
239
|
+
|
|
240
|
+
normalized = Messages()
|
|
241
|
+
if history_raw is None:
|
|
242
|
+
return normalized
|
|
243
|
+
if isinstance(history_raw, list):
|
|
244
|
+
normalized.extend_plain_messages(history_raw)
|
|
245
|
+
else:
|
|
246
|
+
raise ValueError(f"Invalid history format: {type(history_raw)}, expected list or Messages object.")
|
|
247
|
+
return normalized
|
|
248
|
+
|
|
249
|
+
def set_variable(self, name, value):
|
|
250
|
+
if name == KEY_USER_ID:
|
|
251
|
+
self.set_user_id(value)
|
|
252
|
+
elif name == KEY_SESSION_ID:
|
|
253
|
+
self.set_session_id(value)
|
|
254
|
+
elif name == KEY_MAX_ANSWER_CONTENT_LENGTH:
|
|
255
|
+
self.set_max_answer_len(int(value))
|
|
256
|
+
|
|
257
|
+
self.variable_pool.set_var(name, value)
|
|
258
|
+
|
|
259
|
+
def set_var_output(
|
|
260
|
+
self,
|
|
261
|
+
name,
|
|
262
|
+
value,
|
|
263
|
+
source_type=SourceType.OTHER,
|
|
264
|
+
skill_info: Optional[SkillInfo] = None,
|
|
265
|
+
):
|
|
266
|
+
"""Set variable
|
|
267
|
+
:param name: variable name
|
|
268
|
+
:param value: variable value
|
|
269
|
+
:param source_type: variable source type
|
|
270
|
+
:param skill_info: skill information
|
|
271
|
+
"""
|
|
272
|
+
self.variable_pool.set_var_output(name, value, source_type, skill_info)
|
|
273
|
+
|
|
274
|
+
def init_variables(self, variables):
|
|
275
|
+
"""Initialize variable pool
|
|
276
|
+
:param variables: variable dictionary
|
|
277
|
+
"""
|
|
278
|
+
self.variable_pool.init_variables(variables)
|
|
279
|
+
|
|
280
|
+
def init_skillkit(self, skillkit: Skillkit):
|
|
281
|
+
self.set_skills(skillkit)
|
|
282
|
+
|
|
283
|
+
def init_agents(self, agents):
|
|
284
|
+
self.agents = agents
|
|
285
|
+
|
|
286
|
+
def add_agents(self, agents):
|
|
287
|
+
for key, value in agents.items():
|
|
288
|
+
self.agents[key] = value
|
|
289
|
+
|
|
290
|
+
def get_agent(self, agent_name):
|
|
291
|
+
return self.agents[agent_name]
|
|
292
|
+
|
|
293
|
+
def get_agents(self):
|
|
294
|
+
return self.agents
|
|
295
|
+
|
|
296
|
+
def has_agent(self, agent_name):
|
|
297
|
+
"""Check if the specified agent skill exists
|
|
298
|
+
:param agent_name: agent name
|
|
299
|
+
:return: whether it exists
|
|
300
|
+
"""
|
|
301
|
+
# Support direct use of agent_name and skill names with prefixes
|
|
302
|
+
return (
|
|
303
|
+
self.skillkit.hasSkill(agent_name)
|
|
304
|
+
or self.skillkit.hasSkill(f"run_{agent_name}")
|
|
305
|
+
or self.skillkit.hasSkill(f"arun_{agent_name}")
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
def exec_agent(self, agent_name, **kwargs):
|
|
309
|
+
"""Synchronous execution of agent (via skill system)
|
|
310
|
+
:param agent_name: agent name
|
|
311
|
+
:param kwargs: parameters passed to the agent
|
|
312
|
+
:return: execution result
|
|
313
|
+
"""
|
|
314
|
+
# First try directly using agent_name, then try the run_ prefix
|
|
315
|
+
skill_name = agent_name
|
|
316
|
+
if not self.skillkit.hasSkill(skill_name):
|
|
317
|
+
skill_name = f"run_{agent_name}"
|
|
318
|
+
|
|
319
|
+
if not self.skillkit.hasSkill(skill_name):
|
|
320
|
+
raise ValueError(
|
|
321
|
+
f"Agent skill not found: {agent_name} (tried: {agent_name}, run_{agent_name})"
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
result = self.skillkit.exec(skill_name, **kwargs)
|
|
325
|
+
return result.result if hasattr(result, "result") else result
|
|
326
|
+
|
|
327
|
+
async def aexec_agent(self, agent_name, **kwargs):
|
|
328
|
+
"""Asynchronously execute agent (via skill system)
|
|
329
|
+
:param agent_name: agent name
|
|
330
|
+
:param kwargs: parameters to pass to the agent
|
|
331
|
+
:return: execution result
|
|
332
|
+
"""
|
|
333
|
+
# First try directly using agent_name, then try the arun_ prefix
|
|
334
|
+
skill_name = agent_name
|
|
335
|
+
if not self.skillkit.hasSkill(skill_name):
|
|
336
|
+
skill_name = f"arun_{agent_name}"
|
|
337
|
+
|
|
338
|
+
if not self.skillkit.hasSkill(skill_name):
|
|
339
|
+
raise ValueError(
|
|
340
|
+
f"Agent skill not found: {agent_name} (tried: {agent_name}, arun_{agent_name})"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
result = await self.skillkit.aexec(skill_name, **kwargs)
|
|
344
|
+
return result.result if hasattr(result, "result") else result
|
|
345
|
+
|
|
346
|
+
def get_var_value(self, name, default_value=None):
|
|
347
|
+
"""Get variable
|
|
348
|
+
:param name: variable name
|
|
349
|
+
:return: variable value, or None if it does not exist
|
|
350
|
+
"""
|
|
351
|
+
# Compatibility layer: If it's a known flag, redirect to the new system
|
|
352
|
+
# Return string ("true"/"false") or boolean (True/False) based on the caller's default value type
|
|
353
|
+
# - If default_value is None or str: return the string "true"/"false" (compatible with the old string comparison style)
|
|
354
|
+
# - Otherwise: return boolean True/False (compatible with scenarios where it's used directly as a boolean)
|
|
355
|
+
# Lazy import to avoid circular dependencies; use definitions.DEFAULT_VALUES as the true source
|
|
356
|
+
try:
|
|
357
|
+
from dolphin.core.flags.definitions import DEFAULT_VALUES
|
|
358
|
+
|
|
359
|
+
if name in DEFAULT_VALUES:
|
|
360
|
+
from dolphin.core import flags
|
|
361
|
+
|
|
362
|
+
enabled = bool(flags.is_enabled(name))
|
|
363
|
+
if default_value is None or isinstance(default_value, str):
|
|
364
|
+
return "true" if enabled else "false"
|
|
365
|
+
return enabled
|
|
366
|
+
except Exception:
|
|
367
|
+
pass
|
|
368
|
+
return self.variable_pool.get_var_value(name, default_value)
|
|
369
|
+
|
|
370
|
+
def get_var_path_value(self, varpath, default_value=None):
|
|
371
|
+
"""Get variable path value
|
|
372
|
+
:param varpath: variable path
|
|
373
|
+
:return: variable path value, returns None if it does not exist
|
|
374
|
+
"""
|
|
375
|
+
return self.variable_pool.get_var_path_value(varpath, default_value)
|
|
376
|
+
|
|
377
|
+
def get_var_obj(self, name):
|
|
378
|
+
"""Get the variable object
|
|
379
|
+
:param name: variable name
|
|
380
|
+
:return: Variable object, or None if it does not exist
|
|
381
|
+
"""
|
|
382
|
+
return self.variable_pool.get_var(name)
|
|
383
|
+
|
|
384
|
+
def get_all_variables(self):
|
|
385
|
+
"""Get all variables
|
|
386
|
+
:return: Dictionary mapping variable names to variable values
|
|
387
|
+
"""
|
|
388
|
+
return self.variable_pool.get_all_variables()
|
|
389
|
+
|
|
390
|
+
def get_user_variables(self, include_system_context_vars=False):
|
|
391
|
+
"""Get user-defined variables, excluding internal variables.
|
|
392
|
+
|
|
393
|
+
Parameters:
|
|
394
|
+
include_system_context_vars: Whether to include system context variables (e.g., _user_id, _session_id, etc.)
|
|
395
|
+
Default is False for backward compatibility
|
|
396
|
+
|
|
397
|
+
:return: Dictionary mapping user variable names to their values
|
|
398
|
+
"""
|
|
399
|
+
return self.variable_pool.get_user_variables(include_system_context_vars)
|
|
400
|
+
|
|
401
|
+
def get_all_variables_values(self):
|
|
402
|
+
"""Get all variable values
|
|
403
|
+
:return: List of variable values
|
|
404
|
+
"""
|
|
405
|
+
return self.variable_pool.get_all_variables_values()
|
|
406
|
+
|
|
407
|
+
def get_variables_values(self, variable_names: list[str]):
|
|
408
|
+
"""Get the variable dictionary for the specified variable names to improve performance and avoid retrieving all variables.
|
|
409
|
+
:param variable_names: List of variable names to retrieve
|
|
410
|
+
:return: Dictionary containing the specified variables, in the format {variable name: variable value}
|
|
411
|
+
"""
|
|
412
|
+
if not variable_names:
|
|
413
|
+
return {}
|
|
414
|
+
|
|
415
|
+
result = {}
|
|
416
|
+
for var_name in variable_names:
|
|
417
|
+
if self.variable_pool.contain_var(var_name):
|
|
418
|
+
var = self.variable_pool.get_var(var_name)
|
|
419
|
+
result[var_name] = (
|
|
420
|
+
var.value if var is not None and hasattr(var, "value") else None
|
|
421
|
+
)
|
|
422
|
+
return result
|
|
423
|
+
|
|
424
|
+
def get_skillkit(self, skillNames: Optional[List[str]] = None):
|
|
425
|
+
"""Get the skill set, supporting wildcard (glob), exact matching, and optional skillkit namespace.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
skillNames: List of skill patterns.
|
|
429
|
+
- Plain tool name/pattern: "_python", "*_resource*"
|
|
430
|
+
- Namespaced pattern: "<skillkit>.<pattern>", e.g. "resource_skillkit.*"
|
|
431
|
+
If None, return the merged skill set;
|
|
432
|
+
If empty list [], indicates no skills are enabled
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
Skillset: The matched skill set, or the merged skill set if no skills match
|
|
436
|
+
"""
|
|
437
|
+
|
|
438
|
+
# If no matching pattern is provided, return the merged skill set
|
|
439
|
+
if skillNames is None:
|
|
440
|
+
return self.all_skills
|
|
441
|
+
|
|
442
|
+
# When an explicit empty list is passed in, it indicates that the caller does not wish to expose any tools.
|
|
443
|
+
# For example, the scenario where tools=[] is configured in DPH
|
|
444
|
+
if isinstance(skillNames, list) and len(skillNames) == 0:
|
|
445
|
+
return Skillset()
|
|
446
|
+
|
|
447
|
+
skills = self.all_skills.getSkills()
|
|
448
|
+
owner_names = SkillMatcher.get_owner_skillkits(skills)
|
|
449
|
+
|
|
450
|
+
# Use optimized batch matching (pre-parses patterns, deduplicates results)
|
|
451
|
+
matched_skills, any_namespaced_pattern = SkillMatcher.match_skills_batch(
|
|
452
|
+
skills, skillNames, owner_names
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
# If no skills match:
|
|
456
|
+
# - if namespaced patterns were used, return an empty set (safer default)
|
|
457
|
+
# - otherwise keep the historical behavior and return the merged skill set
|
|
458
|
+
if not matched_skills:
|
|
459
|
+
if any_namespaced_pattern:
|
|
460
|
+
return Skillset()
|
|
461
|
+
return self.all_skills
|
|
462
|
+
|
|
463
|
+
# Return the matching skill set
|
|
464
|
+
result_skillset = Skillset()
|
|
465
|
+
for skill in matched_skills:
|
|
466
|
+
result_skillset.addSkill(skill)
|
|
467
|
+
|
|
468
|
+
# Auto-inject _get_result_detail if needed
|
|
469
|
+
self._inject_detail_skill_if_needed(result_skillset)
|
|
470
|
+
|
|
471
|
+
return result_skillset
|
|
472
|
+
|
|
473
|
+
def _inject_detail_skill_if_needed(self, skillset: Skillset):
|
|
474
|
+
"""Auto-inject _get_result_detail if any skill uses omitting modes (SUMMARY/REFERENCE)"""
|
|
475
|
+
try:
|
|
476
|
+
from dolphin.core.skill.context_retention import ContextRetentionMode
|
|
477
|
+
from dolphin.lib.skillkits.system_skillkit import SystemFunctions
|
|
478
|
+
except ImportError:
|
|
479
|
+
return
|
|
480
|
+
|
|
481
|
+
skills = skillset.getSkills()
|
|
482
|
+
should_inject = False
|
|
483
|
+
|
|
484
|
+
for skill in skills:
|
|
485
|
+
# Check for configured retention strategy
|
|
486
|
+
config = getattr(skill.func, '_context_retention', None)
|
|
487
|
+
if config and config.mode in (
|
|
488
|
+
ContextRetentionMode.SUMMARY,
|
|
489
|
+
ContextRetentionMode.REFERENCE,
|
|
490
|
+
):
|
|
491
|
+
should_inject = True
|
|
492
|
+
break
|
|
493
|
+
|
|
494
|
+
if should_inject:
|
|
495
|
+
# Check if already exists in skillset
|
|
496
|
+
has_detail_skill = any(
|
|
497
|
+
"_get_result_detail" in s.get_function_name()
|
|
498
|
+
for s in skills
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
if not has_detail_skill:
|
|
502
|
+
# Try to get the skill from SystemFunctions
|
|
503
|
+
# We try different name variations just in case
|
|
504
|
+
detail_skill = SystemFunctions.getSkill("_get_result_detail")
|
|
505
|
+
if not detail_skill:
|
|
506
|
+
detail_skill = SystemFunctions.getSkill("system_functions._get_result_detail")
|
|
507
|
+
|
|
508
|
+
# If still not found (e.g. SystemFunctions not initialized with it), we can pick it manually if possible
|
|
509
|
+
# But typically SystemFunctions singleton has it.
|
|
510
|
+
if detail_skill:
|
|
511
|
+
skillset.addSkill(detail_skill)
|
|
512
|
+
else:
|
|
513
|
+
# Fallback: look through SystemFunctions.getSkills() manually
|
|
514
|
+
for s in SystemFunctions.getSkills():
|
|
515
|
+
if "_get_result_detail" in s.get_function_name():
|
|
516
|
+
skillset.addSkill(s)
|
|
517
|
+
break
|
|
518
|
+
|
|
519
|
+
def get_skill(self, name):
|
|
520
|
+
return self.skillkit.getSkill(name)
|
|
521
|
+
|
|
522
|
+
def get_skill_type(self, skill_name: str) -> SkillType:
|
|
523
|
+
"""Get the type of a skill
|
|
524
|
+
|
|
525
|
+
Args:
|
|
526
|
+
skill_name: The name of the skill
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
SkillType: The type of the skill
|
|
530
|
+
"""
|
|
531
|
+
skill = self.get_skill(skill_name)
|
|
532
|
+
if skill:
|
|
533
|
+
# Try to get the tool type, return the default type if not available
|
|
534
|
+
return getattr(skill, "tool_type", SkillType.TOOL)
|
|
535
|
+
else:
|
|
536
|
+
# Default returns TOOL type
|
|
537
|
+
return SkillType.TOOL
|
|
538
|
+
|
|
539
|
+
def is_skillkit_empty(self):
|
|
540
|
+
return self.skillkit is None or self.skillkit.isEmpty()
|
|
541
|
+
|
|
542
|
+
def exec_skill(self, name, **kwargs):
|
|
543
|
+
return self.skillkit.exec(name, **kwargs)
|
|
544
|
+
|
|
545
|
+
async def aexec_skill(self, name, **kwargs):
|
|
546
|
+
return self.skillkit.aexec(name, **kwargs)
|
|
547
|
+
|
|
548
|
+
def get_agent_skill(self, skill_function: SkillFunction):
|
|
549
|
+
if self.global_skills is None:
|
|
550
|
+
return None
|
|
551
|
+
return self.global_skills.getAgent(skill_function.get_function_name())
|
|
552
|
+
|
|
553
|
+
def sync_variables(self, context: "Context"):
|
|
554
|
+
self.variable_pool.sync_variables(context.variable_pool)
|
|
555
|
+
|
|
556
|
+
def delete_variable(self, name):
|
|
557
|
+
"""Delete variable
|
|
558
|
+
:param name: variable name
|
|
559
|
+
"""
|
|
560
|
+
self.variable_pool.delete_var(name)
|
|
561
|
+
|
|
562
|
+
def clear_variables(self):
|
|
563
|
+
"""Clear all variables"""
|
|
564
|
+
self.variable_pool.clear()
|
|
565
|
+
|
|
566
|
+
def list_variables(self):
|
|
567
|
+
"""List all variables
|
|
568
|
+
:return: A list containing all variable names
|
|
569
|
+
"""
|
|
570
|
+
return self.variable_pool.keys()
|
|
571
|
+
|
|
572
|
+
def set_skills(self, skillkit):
|
|
573
|
+
self.skillkit = skillkit
|
|
574
|
+
self._calc_all_skills()
|
|
575
|
+
|
|
576
|
+
def append_var_output(
|
|
577
|
+
self, name, value, source_type=SourceType.OTHER, skill_info=None
|
|
578
|
+
):
|
|
579
|
+
var = self.variable_pool.get_var(name)
|
|
580
|
+
if not var:
|
|
581
|
+
var = VarOutput(
|
|
582
|
+
name=name, value=[], source_type=SourceType.LIST, skill_info=skill_info
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
if not isinstance(var, VarOutput):
|
|
586
|
+
if isinstance(var, Var):
|
|
587
|
+
if var.value:
|
|
588
|
+
if isinstance(var.value, list):
|
|
589
|
+
init_var = VarOutput(
|
|
590
|
+
name=name,
|
|
591
|
+
value=[],
|
|
592
|
+
source_type=SourceType.LIST,
|
|
593
|
+
skill_info=skill_info,
|
|
594
|
+
)
|
|
595
|
+
for item in var.value:
|
|
596
|
+
init_var.add(
|
|
597
|
+
VarOutput(
|
|
598
|
+
name=name,
|
|
599
|
+
value=item,
|
|
600
|
+
source_type=source_type,
|
|
601
|
+
skill_info=skill_info,
|
|
602
|
+
)
|
|
603
|
+
)
|
|
604
|
+
var = init_var
|
|
605
|
+
else:
|
|
606
|
+
var = VarOutput(
|
|
607
|
+
name=name,
|
|
608
|
+
value=[var],
|
|
609
|
+
source_type=SourceType.LIST,
|
|
610
|
+
skill_info=skill_info,
|
|
611
|
+
)
|
|
612
|
+
else:
|
|
613
|
+
var = VarOutput(
|
|
614
|
+
name=name,
|
|
615
|
+
value=[],
|
|
616
|
+
source_type=SourceType.LIST,
|
|
617
|
+
skill_info=skill_info,
|
|
618
|
+
)
|
|
619
|
+
else:
|
|
620
|
+
var = VarOutput(
|
|
621
|
+
name=name,
|
|
622
|
+
value=[var],
|
|
623
|
+
source_type=SourceType.LIST,
|
|
624
|
+
skill_info=skill_info,
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
new_var = var.add(
|
|
628
|
+
VarOutput(
|
|
629
|
+
name=name, value=value, source_type=source_type, skill_info=skill_info
|
|
630
|
+
)
|
|
631
|
+
)
|
|
632
|
+
self.variable_pool.set_var(name, new_var)
|
|
633
|
+
|
|
634
|
+
def set_last_var_output(
|
|
635
|
+
self, name, value, source_type=SourceType.OTHER, skill_info=None
|
|
636
|
+
):
|
|
637
|
+
var = self.variable_pool.get_var(name)
|
|
638
|
+
if var:
|
|
639
|
+
var.set_last(
|
|
640
|
+
VarOutput(
|
|
641
|
+
name=name,
|
|
642
|
+
value=value,
|
|
643
|
+
source_type=source_type,
|
|
644
|
+
skill_info=skill_info,
|
|
645
|
+
)
|
|
646
|
+
)
|
|
647
|
+
self.variable_pool.set_var(name, var)
|
|
648
|
+
|
|
649
|
+
def update_var_output(
|
|
650
|
+
self, name, value, source_type=SourceType.OTHER, skill_info=None
|
|
651
|
+
):
|
|
652
|
+
"""Update variable
|
|
653
|
+
:param name: Variable name
|
|
654
|
+
:param value: Variable value
|
|
655
|
+
:param source_type: Variable source type
|
|
656
|
+
:param skill_info: Skill information
|
|
657
|
+
"""
|
|
658
|
+
var = self.variable_pool.get_var(name)
|
|
659
|
+
if var:
|
|
660
|
+
# Check if the variable has the corresponding attribute and attempt to update it
|
|
661
|
+
if hasattr(var, "value"):
|
|
662
|
+
if isinstance(getattr(var, "value", None), list):
|
|
663
|
+
var.value[-1] = value # type: ignore
|
|
664
|
+
else:
|
|
665
|
+
setattr(var, "value", value)
|
|
666
|
+
# Try to update source_type and skill_info
|
|
667
|
+
if hasattr(var, "source_type"):
|
|
668
|
+
setattr(var, "source_type", source_type)
|
|
669
|
+
if hasattr(var, "skill_info"):
|
|
670
|
+
setattr(var, "skill_info", skill_info)
|
|
671
|
+
else:
|
|
672
|
+
self.set_var_output(name, value, source_type, skill_info)
|
|
673
|
+
|
|
674
|
+
def recognize_variable(self, dolphin_str):
|
|
675
|
+
# Identify the positions of all variables in a string - Simple variables: `$variableName` - Array indices: `$variableName[index]` - Nested properties: `$variableName.key1.key2`
|
|
676
|
+
"""Identify the positions of all variables in a string
|
|
677
|
+
:param dolphin_str: The string to be identified
|
|
678
|
+
:return: A list of tuples containing variable names and their positions [('variable name', (start position, end position)), ...]
|
|
679
|
+
"""
|
|
680
|
+
return self.variable_pool.recognize_variable(dolphin_str)
|
|
681
|
+
|
|
682
|
+
def get_variable_type(self, variable_str):
|
|
683
|
+
return self.variable_pool.get_variable_type(variable_str)
|
|
684
|
+
|
|
685
|
+
def reset_messages(self):
|
|
686
|
+
agent_name = self.get_cur_agent_name()
|
|
687
|
+
if agent_name is None:
|
|
688
|
+
agent_name = "default"
|
|
689
|
+
self.messages[agent_name] = Messages()
|
|
690
|
+
# Note: This only clears the image, messages in context_manager still exist
|
|
691
|
+
# If you need to clear the context_manager, you should call context_manager.clear_bucket()
|
|
692
|
+
# Here maintain the original behavior, only clear the image
|
|
693
|
+
self.messages_dirty = False # The mirror has already been explicitly set, no synchronization is needed.
|
|
694
|
+
|
|
695
|
+
# Reset the historical injection flag to allow new sessions to re-inject history.
|
|
696
|
+
self.history_injected = False
|
|
697
|
+
|
|
698
|
+
def reset_for_block(self):
|
|
699
|
+
"""Reset context state for new code blocks.
|
|
700
|
+
|
|
701
|
+
This method uniformly handles the reset logic before executing a code block, including:
|
|
702
|
+
1. Mark trajectory stage baseline (needs to be done before clearing, as current state is required)
|
|
703
|
+
2. Reset message mirroring
|
|
704
|
+
3. Clean up temporary buckets (SCRATCHPAD, SYSTEM, QUERY)
|
|
705
|
+
|
|
706
|
+
Note: This method is specifically designed for resetting before executing code blocks and should not be used in other scenarios.
|
|
707
|
+
"""
|
|
708
|
+
# Step 1: Mark stage baseline BEFORE clearing (trajectory needs current state)
|
|
709
|
+
# The baseline captures the message count before this block starts executing
|
|
710
|
+
if getattr(self, "trajectory", None):
|
|
711
|
+
try:
|
|
712
|
+
self.trajectory.begin_stage(self.context_manager)
|
|
713
|
+
logger.debug("Trajectory stage baseline marked")
|
|
714
|
+
except (AttributeError, TypeError) as e:
|
|
715
|
+
logger.debug(f"Failed to mark trajectory baseline: {e}")
|
|
716
|
+
|
|
717
|
+
# Step 2: Reset message mirror for fresh block execution
|
|
718
|
+
# This prevents message accumulation across blocks and ensures clean state
|
|
719
|
+
self.reset_messages()
|
|
720
|
+
|
|
721
|
+
# Step 3: Clear transient buckets that should not persist across blocks:
|
|
722
|
+
# - SCRATCHPAD: temporary working memory for current block only
|
|
723
|
+
# - SYSTEM: system prompt may be different per block
|
|
724
|
+
# - QUERY: user query changes per block
|
|
725
|
+
# Note: These buckets will be re-populated by specific blocks (e.g., llm_chat)
|
|
726
|
+
try:
|
|
727
|
+
from dolphin.core.context_engineer.config.settings import BuildInBucket
|
|
728
|
+
|
|
729
|
+
cm = self.context_manager
|
|
730
|
+
removed_buckets = []
|
|
731
|
+
for bucket in [BuildInBucket.SCRATCHPAD.value,
|
|
732
|
+
BuildInBucket.SYSTEM.value,
|
|
733
|
+
BuildInBucket.QUERY.value]:
|
|
734
|
+
try:
|
|
735
|
+
cm.remove_bucket(bucket)
|
|
736
|
+
removed_buckets.append(bucket)
|
|
737
|
+
except (AttributeError, KeyError):
|
|
738
|
+
pass # Bucket doesn't exist, which is fine
|
|
739
|
+
|
|
740
|
+
if removed_buckets:
|
|
741
|
+
logger.debug(f"Cleared buckets for fresh block state: {removed_buckets}")
|
|
742
|
+
except Exception as e:
|
|
743
|
+
# Log unexpected errors but don't fail block execution
|
|
744
|
+
logger.warning(f"Unexpected error during bucket cleanup: {e}")
|
|
745
|
+
|
|
746
|
+
def clear_messages(self):
|
|
747
|
+
"""Clear message history - alias of reset_messages"""
|
|
748
|
+
self.reset_messages()
|
|
749
|
+
|
|
750
|
+
def set_messages(self, messages: Messages):
|
|
751
|
+
self.get_messages().set_messages(messages) # Use Messages.set_messages
|
|
752
|
+
|
|
753
|
+
def sync_messages_from_llm_context(self, force: bool = False) -> Messages:
|
|
754
|
+
"""Core synchronization method: synchronize messages from the context_manager single data source to the current Agent's mirror.
|
|
755
|
+
|
|
756
|
+
Args:
|
|
757
|
+
force: Whether to force synchronization (ignore dirty flag optimization)
|
|
758
|
+
|
|
759
|
+
Returns:
|
|
760
|
+
Messages: The updated Messages object
|
|
761
|
+
|
|
762
|
+
Raises:
|
|
763
|
+
SyncError: An error occurred during synchronization
|
|
764
|
+
|
|
765
|
+
Note:
|
|
766
|
+
1. Uses a lazy loading strategy: synchronization is performed only when the dirty flag is True or force=True
|
|
767
|
+
2. Synchronization scope: Only updates the current Agent's mirror (self.messages[self.agent_name])
|
|
768
|
+
3. Thread safety: Ensured by the locking mechanism inside context_manager
|
|
769
|
+
"""
|
|
770
|
+
from dolphin.core.common.exceptions import SyncError
|
|
771
|
+
|
|
772
|
+
agent_name = self.get_cur_agent_name() or "default"
|
|
773
|
+
|
|
774
|
+
# Performance optimization: Check dirty flags
|
|
775
|
+
if not force and not self.messages_dirty:
|
|
776
|
+
return self.messages.get(agent_name, Messages())
|
|
777
|
+
|
|
778
|
+
try:
|
|
779
|
+
# 1. Obtain the authoritative, deduplicated, and policy-sorted final message list from context_manager
|
|
780
|
+
llm_messages = self.context_manager.to_dph_messages()
|
|
781
|
+
|
|
782
|
+
# 2. Get the current agent's image container (create one if it does not exist)
|
|
783
|
+
if agent_name not in self.messages:
|
|
784
|
+
self.messages[agent_name] = Messages()
|
|
785
|
+
target_mirror = self.messages[agent_name]
|
|
786
|
+
|
|
787
|
+
# 3. Update the image content in place, keeping references unchanged
|
|
788
|
+
# Messages.set_messages() already exists (common.py line 366-368)
|
|
789
|
+
target_mirror.set_messages(llm_messages)
|
|
790
|
+
|
|
791
|
+
# 4. Clear dirty marks
|
|
792
|
+
self.messages_dirty = False
|
|
793
|
+
|
|
794
|
+
logger.debug(f"Synced messages for agent '{agent_name}': {len(llm_messages.get_messages())} messages")
|
|
795
|
+
|
|
796
|
+
return target_mirror
|
|
797
|
+
|
|
798
|
+
except Exception as e:
|
|
799
|
+
logger.error(f"Failed to sync messages from context_manager: {e}")
|
|
800
|
+
raise SyncError(f"Message synchronization failed: {e}") from e
|
|
801
|
+
|
|
802
|
+
def get_messages(self):
|
|
803
|
+
"""Get the message mirror of the current Agent (auto-sync)"""
|
|
804
|
+
agent_name = self.get_cur_agent_name()
|
|
805
|
+
if agent_name is None:
|
|
806
|
+
agent_name = "default"
|
|
807
|
+
|
|
808
|
+
# Auto Sync
|
|
809
|
+
self.sync_messages_from_llm_context()
|
|
810
|
+
|
|
811
|
+
if agent_name not in self.messages:
|
|
812
|
+
self.messages[agent_name] = Messages()
|
|
813
|
+
|
|
814
|
+
return self.messages[agent_name]
|
|
815
|
+
|
|
816
|
+
def get_messages_with_tool_calls(self):
|
|
817
|
+
"""Get all messages that have tool calls"""
|
|
818
|
+
return self.get_messages().get_messages_with_tool_calls()
|
|
819
|
+
|
|
820
|
+
def get_tool_response_messages(self):
|
|
821
|
+
"""Get all tool response messages"""
|
|
822
|
+
return self.get_messages().get_tool_response_messages()
|
|
823
|
+
|
|
824
|
+
# ============ Added: Unified message management convenience methods ============
|
|
825
|
+
def add_user_message(self, content, bucket: str = None):
|
|
826
|
+
"""Add user message (unified interface)
|
|
827
|
+
|
|
828
|
+
Args:
|
|
829
|
+
content: Message content, can be:
|
|
830
|
+
- str: Plain text message
|
|
831
|
+
- List[Dict]: Multimodal content (e.g., [{"type": "text", "text": "..."}, {"type": "image_url", ...}])
|
|
832
|
+
bucket: Bucket name, default is SCRATCHPAD
|
|
833
|
+
|
|
834
|
+
Note:
|
|
835
|
+
Only written to context_manager (single data source),
|
|
836
|
+
Mirrors are synchronized on-demand via sync_messages_from_llm_context().
|
|
837
|
+
"""
|
|
838
|
+
from dolphin.core.context_engineer.config.settings import BuildInBucket
|
|
839
|
+
|
|
840
|
+
if bucket is None:
|
|
841
|
+
bucket = BuildInBucket.SCRATCHPAD.value
|
|
842
|
+
|
|
843
|
+
# Unique write path: added to context_manager
|
|
844
|
+
messages = Messages()
|
|
845
|
+
messages.add_message(content, MessageRole.USER)
|
|
846
|
+
self.context_manager.add_bucket(bucket, messages)
|
|
847
|
+
|
|
848
|
+
# Mark as dirty (remove double writing)
|
|
849
|
+
self.messages_dirty = True
|
|
850
|
+
|
|
851
|
+
def add_assistant_message(self, content: str, bucket: str = None):
|
|
852
|
+
"""Add assistant message (unified interface)
|
|
853
|
+
|
|
854
|
+
Args:
|
|
855
|
+
content: Message content
|
|
856
|
+
bucket: Bucket name, default is SCRATCHPAD
|
|
857
|
+
|
|
858
|
+
Note:
|
|
859
|
+
Only written to context_manager (single data source),
|
|
860
|
+
Mirrors are synchronized on-demand via sync_messages_from_llm_context().
|
|
861
|
+
"""
|
|
862
|
+
from dolphin.core.context_engineer.config.settings import BuildInBucket
|
|
863
|
+
|
|
864
|
+
if bucket is None:
|
|
865
|
+
bucket = BuildInBucket.SCRATCHPAD.value
|
|
866
|
+
|
|
867
|
+
# Unique write path: added to context_manager
|
|
868
|
+
messages = Messages()
|
|
869
|
+
messages.add_message(content, MessageRole.ASSISTANT)
|
|
870
|
+
self.context_manager.add_bucket(bucket, messages)
|
|
871
|
+
|
|
872
|
+
# Mark as dirty (remove double writing)
|
|
873
|
+
self.messages_dirty = True
|
|
874
|
+
|
|
875
|
+
def add_system_message(self, content: str, bucket: str = None):
|
|
876
|
+
"""Add system message (unified interface)
|
|
877
|
+
|
|
878
|
+
Args:
|
|
879
|
+
content: Message content
|
|
880
|
+
bucket: Bucket name, default is SYSTEM
|
|
881
|
+
|
|
882
|
+
Note:
|
|
883
|
+
Only written to context_manager (single data source),
|
|
884
|
+
Mirrors are synchronized on-demand via sync_messages_from_llm_context().
|
|
885
|
+
"""
|
|
886
|
+
from dolphin.core.context_engineer.config.settings import BuildInBucket
|
|
887
|
+
|
|
888
|
+
if bucket is None:
|
|
889
|
+
bucket = BuildInBucket.SYSTEM.value
|
|
890
|
+
|
|
891
|
+
messages = Messages()
|
|
892
|
+
messages.add_message(content, MessageRole.SYSTEM)
|
|
893
|
+
# If the corresponding bucket already exists, replace its content directly; otherwise, create a new bucket.
|
|
894
|
+
if self.context_manager is not None:
|
|
895
|
+
if bucket in self.context_manager.state.buckets:
|
|
896
|
+
# Directly replace the content, avoid merging Messages again
|
|
897
|
+
self.context_manager.replace_bucket_content(bucket, messages)
|
|
898
|
+
# Mark the message mirror as dirty to ensure subsequent synchronization.
|
|
899
|
+
self.messages_dirty = True
|
|
900
|
+
else:
|
|
901
|
+
# The initial creation still uses context_manager.add_bucket, with message_role set to SYSTEM
|
|
902
|
+
self.context_manager.add_bucket(
|
|
903
|
+
bucket_name=bucket,
|
|
904
|
+
content=messages,
|
|
905
|
+
message_role=MessageRole.SYSTEM,
|
|
906
|
+
)
|
|
907
|
+
self.messages_dirty = True
|
|
908
|
+
|
|
909
|
+
def add_tool_call_message_v2(self, content: str, tool_calls: list, bucket: str = None):
|
|
910
|
+
"""Add tool call messages (unified interface)
|
|
911
|
+
|
|
912
|
+
Args:
|
|
913
|
+
content: Message content
|
|
914
|
+
tool_calls: List of tool calls
|
|
915
|
+
bucket: Bucket name, default is SCRATCHPAD
|
|
916
|
+
|
|
917
|
+
Note:
|
|
918
|
+
Only write to context_manager (single data source),
|
|
919
|
+
Mirrors are synchronized on-demand via sync_messages_from_llm_context().
|
|
920
|
+
"""
|
|
921
|
+
from dolphin.core.context_engineer.config.settings import BuildInBucket
|
|
922
|
+
|
|
923
|
+
if bucket is None:
|
|
924
|
+
bucket = BuildInBucket.SCRATCHPAD.value
|
|
925
|
+
|
|
926
|
+
# Unique write path: added to context_manager
|
|
927
|
+
messages = Messages()
|
|
928
|
+
messages.add_tool_call_message(content=content, tool_calls=tool_calls)
|
|
929
|
+
self.context_manager.add_bucket(bucket, messages)
|
|
930
|
+
|
|
931
|
+
# Mark as dirty (remove double writing)
|
|
932
|
+
self.messages_dirty = True
|
|
933
|
+
|
|
934
|
+
def add_tool_response_message_v2(self, content: str, tool_call_id: str, bucket: str = None):
|
|
935
|
+
"""Add tool response message (unified interface)
|
|
936
|
+
|
|
937
|
+
Args:
|
|
938
|
+
content: Message content
|
|
939
|
+
tool_call_id: Tool call ID
|
|
940
|
+
bucket: Bucket name, default is SCRATCHPAD
|
|
941
|
+
|
|
942
|
+
Note:
|
|
943
|
+
Only write to context_manager (single data source),
|
|
944
|
+
Mirrors are synchronized on-demand via sync_messages_from_llm_context().
|
|
945
|
+
"""
|
|
946
|
+
from dolphin.core.context_engineer.config.settings import BuildInBucket
|
|
947
|
+
|
|
948
|
+
if bucket is None:
|
|
949
|
+
bucket = BuildInBucket.SCRATCHPAD.value
|
|
950
|
+
|
|
951
|
+
# Unique write path: added to context_manager
|
|
952
|
+
messages = Messages()
|
|
953
|
+
messages.add_tool_response_message(content=content, tool_call_id=tool_call_id)
|
|
954
|
+
self.context_manager.add_bucket(bucket, messages)
|
|
955
|
+
|
|
956
|
+
# Mark as dirty (remove double writing)
|
|
957
|
+
self.messages_dirty = True
|
|
958
|
+
|
|
959
|
+
def set_messages_batch(self, messages: Messages, bucket: str = None):
|
|
960
|
+
"""Batch set messages (uniform interface)
|
|
961
|
+
Used to restore the previous messages state
|
|
962
|
+
|
|
963
|
+
Args:
|
|
964
|
+
messages: Messages object
|
|
965
|
+
bucket: bucket name, default is SCRATCHPAD
|
|
966
|
+
"""
|
|
967
|
+
from dolphin.core.context_engineer.config.settings import BuildInBucket
|
|
968
|
+
|
|
969
|
+
if bucket is None:
|
|
970
|
+
bucket = BuildInBucket.SCRATCHPAD.value
|
|
971
|
+
|
|
972
|
+
# Empty the current bucket
|
|
973
|
+
self.context_manager.clear_bucket(bucket)
|
|
974
|
+
# Add new messages
|
|
975
|
+
self.context_manager.add_bucket(bucket, messages)
|
|
976
|
+
|
|
977
|
+
# Mark as dirty
|
|
978
|
+
self.messages_dirty = True
|
|
979
|
+
|
|
980
|
+
def set_history_bucket(self, messages: Messages):
|
|
981
|
+
"""Set or override the content of the history bucket to always remain consistent with the history snapshot in the variable pool.
|
|
982
|
+
|
|
983
|
+
Args:
|
|
984
|
+
messages: Normalized historical messages (Messages)
|
|
985
|
+
"""
|
|
986
|
+
from dolphin.core.context_engineer.config.settings import BuildInBucket
|
|
987
|
+
|
|
988
|
+
if not self.context_manager:
|
|
989
|
+
return
|
|
990
|
+
|
|
991
|
+
bucket_name = BuildInBucket.HISTORY.value
|
|
992
|
+
|
|
993
|
+
# If the bucket already exists, directly replace the content to avoid duplicate merge at the Messages level
|
|
994
|
+
if bucket_name in self.context_manager.state.buckets:
|
|
995
|
+
self.context_manager.replace_bucket_content(bucket_name, messages)
|
|
996
|
+
self.messages_dirty = True
|
|
997
|
+
else:
|
|
998
|
+
# First created, unified through add_bucket to maintain consistent configuration
|
|
999
|
+
self.add_bucket(
|
|
1000
|
+
bucket_name=bucket_name,
|
|
1001
|
+
content=messages,
|
|
1002
|
+
message_role=MessageRole.USER,
|
|
1003
|
+
)
|
|
1004
|
+
|
|
1005
|
+
def add_bucket(
|
|
1006
|
+
self,
|
|
1007
|
+
bucket_name: str,
|
|
1008
|
+
content: Union[str, Messages],
|
|
1009
|
+
priority: float = 1.0,
|
|
1010
|
+
allocated_tokens: Optional[int] = None,
|
|
1011
|
+
message_role: Optional[MessageRole] = None,
|
|
1012
|
+
) -> None:
|
|
1013
|
+
"""Unified bucket addition interface (wraps context_manager.add_bucket)
|
|
1014
|
+
|
|
1015
|
+
This is the recommended way to add a bucket, automatically handling the messages_dirty flag.
|
|
1016
|
+
External direct calls to self.context_manager.add_bucket() are prohibited.
|
|
1017
|
+
|
|
1018
|
+
Args:
|
|
1019
|
+
bucket_name: Bucket name
|
|
1020
|
+
content: Content, supports string or Messages type
|
|
1021
|
+
priority: Priority
|
|
1022
|
+
allocated_tokens: Number of allocated tokens
|
|
1023
|
+
message_role: Message role
|
|
1024
|
+
|
|
1025
|
+
Note:
|
|
1026
|
+
This method ensures the messages_dirty flag is correctly set,
|
|
1027
|
+
guaranteeing the message synchronization mechanism works properly.
|
|
1028
|
+
"""
|
|
1029
|
+
self.context_manager.add_bucket(
|
|
1030
|
+
bucket_name=bucket_name,
|
|
1031
|
+
content=content,
|
|
1032
|
+
priority=priority,
|
|
1033
|
+
allocated_tokens=allocated_tokens,
|
|
1034
|
+
message_role=message_role,
|
|
1035
|
+
)
|
|
1036
|
+
# Automatically mark as dirty to ensure message synchronization
|
|
1037
|
+
self.messages_dirty = True
|
|
1038
|
+
|
|
1039
|
+
# ============ End: Unified Message Management Convenience Methods ============
|
|
1040
|
+
|
|
1041
|
+
async def update_usage(self, final_chunk):
|
|
1042
|
+
if not self.get_var_value("usage"):
|
|
1043
|
+
default_uasge = {
|
|
1044
|
+
"prompt_tokens": 0,
|
|
1045
|
+
"total_tokens": 0,
|
|
1046
|
+
"completion_tokens": 0,
|
|
1047
|
+
}
|
|
1048
|
+
self.set_variable("usage", default_uasge)
|
|
1049
|
+
|
|
1050
|
+
if not hasattr(final_chunk, "usage") or (
|
|
1051
|
+
hasattr(final_chunk, "usage") and not final_chunk.usage
|
|
1052
|
+
):
|
|
1053
|
+
return
|
|
1054
|
+
|
|
1055
|
+
try:
|
|
1056
|
+
usage = (
|
|
1057
|
+
final_chunk.usage if final_chunk.usage
|
|
1058
|
+
else (final_chunk.choices[0].usage if final_chunk.choices else None)
|
|
1059
|
+
) # SDK after AD packaging and DeepSeek native interface
|
|
1060
|
+
except:
|
|
1061
|
+
usage = (
|
|
1062
|
+
final_chunk["usage"]
|
|
1063
|
+
if final_chunk.get("usage", 0)
|
|
1064
|
+
else (final_chunk["choices"][0]["usage"] if final_chunk.get("choices") and len(final_chunk["choices"]) > 0 else None)
|
|
1065
|
+
) # Current API request method for obtaining usage
|
|
1066
|
+
# If usage information cannot be obtained, return directly.
|
|
1067
|
+
if usage is None:
|
|
1068
|
+
return
|
|
1069
|
+
|
|
1070
|
+
llm_tokens = self.get_var_value("usage")
|
|
1071
|
+
if llm_tokens is None:
|
|
1072
|
+
llm_tokens = {
|
|
1073
|
+
"prompt_tokens": 0,
|
|
1074
|
+
"total_tokens": 0,
|
|
1075
|
+
"completion_tokens": 0,
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
if isinstance(usage, dict):
|
|
1079
|
+
llm_tokens["prompt_tokens"] += usage.get("prompt_tokens", 0)
|
|
1080
|
+
llm_tokens["total_tokens"] += usage.get("total_tokens", 0)
|
|
1081
|
+
llm_tokens["completion_tokens"] += usage.get("completion_tokens", 0)
|
|
1082
|
+
else:
|
|
1083
|
+
llm_tokens["prompt_tokens"] += usage.prompt_tokens
|
|
1084
|
+
llm_tokens["total_tokens"] += usage.total_tokens
|
|
1085
|
+
llm_tokens["completion_tokens"] += usage.completion_tokens
|
|
1086
|
+
self.set_variable("usage", llm_tokens)
|
|
1087
|
+
|
|
1088
|
+
def get_runtime_graph(self):
|
|
1089
|
+
return self.runtime_graph
|
|
1090
|
+
|
|
1091
|
+
def set_last_model_name(self, model_name: str):
|
|
1092
|
+
"""
|
|
1093
|
+
Set the last used model name.
|
|
1094
|
+
This should be called when making LLM calls to maintain model consistency across multiple rounds.
|
|
1095
|
+
|
|
1096
|
+
Args:
|
|
1097
|
+
model_name: The model name to store
|
|
1098
|
+
"""
|
|
1099
|
+
if model_name:
|
|
1100
|
+
self._last_model_name = model_name
|
|
1101
|
+
|
|
1102
|
+
def get_last_model_name(self) -> Optional[str]:
|
|
1103
|
+
"""
|
|
1104
|
+
Get the last used model name.
|
|
1105
|
+
This is useful for maintaining model consistency across multiple rounds of conversation.
|
|
1106
|
+
|
|
1107
|
+
Returns:
|
|
1108
|
+
Optional[str]: The model name if found, None otherwise
|
|
1109
|
+
"""
|
|
1110
|
+
return self._last_model_name
|
|
1111
|
+
|
|
1112
|
+
def set_last_skills(self, skills: Optional[List[str]]):
|
|
1113
|
+
"""
|
|
1114
|
+
Set the last used skills configuration.
|
|
1115
|
+
This should be called when executing explore blocks to maintain skills consistency across multiple rounds.
|
|
1116
|
+
|
|
1117
|
+
Args:
|
|
1118
|
+
skills: The skills list to store (can be None to clear)
|
|
1119
|
+
"""
|
|
1120
|
+
self._last_skills = skills
|
|
1121
|
+
|
|
1122
|
+
def get_last_skills(self) -> Optional[List[str]]:
|
|
1123
|
+
"""
|
|
1124
|
+
Get the last used skills configuration.
|
|
1125
|
+
This is useful for maintaining skills consistency across multiple rounds of conversation.
|
|
1126
|
+
|
|
1127
|
+
Returns:
|
|
1128
|
+
Optional[List[str]]: The skills list if found, None otherwise
|
|
1129
|
+
"""
|
|
1130
|
+
return self._last_skills
|
|
1131
|
+
|
|
1132
|
+
def set_last_explore_mode(self, mode: Optional[str]):
|
|
1133
|
+
"""
|
|
1134
|
+
Set the last used explore mode.
|
|
1135
|
+
This should be called when executing explore blocks to maintain mode consistency across multiple rounds.
|
|
1136
|
+
|
|
1137
|
+
Args:
|
|
1138
|
+
mode: The explore mode to store ('prompt' or 'tool_call', can be None to clear)
|
|
1139
|
+
"""
|
|
1140
|
+
self._last_explore_mode = mode
|
|
1141
|
+
|
|
1142
|
+
def get_last_explore_mode(self) -> Optional[str]:
|
|
1143
|
+
"""
|
|
1144
|
+
Get the last used explore mode.
|
|
1145
|
+
This is useful for maintaining mode consistency across multiple rounds of conversation.
|
|
1146
|
+
|
|
1147
|
+
Returns:
|
|
1148
|
+
Optional[str]: The explore mode if found ('prompt' or 'tool_call'), None otherwise
|
|
1149
|
+
"""
|
|
1150
|
+
return self._last_explore_mode
|
|
1151
|
+
|
|
1152
|
+
def set_last_system_prompt(self, system_prompt: str):
|
|
1153
|
+
"""
|
|
1154
|
+
Set the last used system prompt.
|
|
1155
|
+
This is useful for restoring the _system bucket during multi-turn conversations (e.g., continue_exploration).
|
|
1156
|
+
|
|
1157
|
+
Args:
|
|
1158
|
+
system_prompt: The system prompt to store
|
|
1159
|
+
"""
|
|
1160
|
+
if system_prompt and str(system_prompt).strip():
|
|
1161
|
+
self._last_system_prompt = str(system_prompt)
|
|
1162
|
+
|
|
1163
|
+
def get_last_system_prompt(self) -> Optional[str]:
|
|
1164
|
+
"""
|
|
1165
|
+
Get the last used system prompt.
|
|
1166
|
+
|
|
1167
|
+
Returns:
|
|
1168
|
+
Optional[str]: The system prompt if found, otherwise None
|
|
1169
|
+
"""
|
|
1170
|
+
return self._last_system_prompt
|
|
1171
|
+
|
|
1172
|
+
def get_execution_trace(self, title=None) -> Dict[str, Any]:
|
|
1173
|
+
"""Generate and return runtime execution trace information (Execution Trace)
|
|
1174
|
+
|
|
1175
|
+
The execution trace records the complete execution flow of the Agent, including:
|
|
1176
|
+
- Execution order and duration of each code block
|
|
1177
|
+
- LLM call details (input/output tokens, model, etc.)
|
|
1178
|
+
- Variable changes and state transitions
|
|
1179
|
+
|
|
1180
|
+
Args:
|
|
1181
|
+
title (str, optional): Trace title. If not provided, a default title is used.
|
|
1182
|
+
|
|
1183
|
+
Returns:
|
|
1184
|
+
Dict[str, Any]: Execution trace information containing call_chain and LLM details
|
|
1185
|
+
"""
|
|
1186
|
+
return self.runtime_graph.profile(title or "")
|
|
1187
|
+
|
|
1188
|
+
# Backward compatibility: retain old method names
|
|
1189
|
+
def get_profile(self, title=None):
|
|
1190
|
+
"""[Deprecated] Please use get_execution_trace() instead"""
|
|
1191
|
+
import warnings
|
|
1192
|
+
warnings.warn("get_profile() 已废弃,请使用 get_execution_trace()", DeprecationWarning, stacklevel=2)
|
|
1193
|
+
return self.get_execution_trace(title)
|
|
1194
|
+
|
|
1195
|
+
def get_snapshot_analysis(self, title=None, format='markdown', options=None):
|
|
1196
|
+
"""Generate and return a visualization analysis report for ContextSnapshot (Snapshot Analysis)
|
|
1197
|
+
|
|
1198
|
+
Snapshot analysis creates a snapshot of the current context and generates a detailed analysis report, including:
|
|
1199
|
+
- Message statistics (bucketed by role, size, type)
|
|
1200
|
+
- Variable statistics (bucketed by type, size, namespace)
|
|
1201
|
+
- Memory usage analysis (original size, compressed size, compression ratio)
|
|
1202
|
+
- Optimization suggestions
|
|
1203
|
+
|
|
1204
|
+
Args:
|
|
1205
|
+
title (str, optional): Title of the analysis report
|
|
1206
|
+
format (str): Output format, either 'markdown' or 'json'
|
|
1207
|
+
options (dict, optional): Configuration options, including thresholds, rendering options, etc.
|
|
1208
|
+
|
|
1209
|
+
Returns:
|
|
1210
|
+
str or dict: Markdown-formatted report (if format='markdown') or JSON-formatted data (if format='json')
|
|
1211
|
+
|
|
1212
|
+
Examples:
|
|
1213
|
+
# Get Markdown report
|
|
1214
|
+
analysis = context.get_snapshot_analysis(title="Step 5 Analysis")
|
|
1215
|
+
print(analysis)
|
|
1216
|
+
|
|
1217
|
+
# Get JSON data
|
|
1218
|
+
analysis_data = context.get_snapshot_analysis(format='json')
|
|
1219
|
+
print(f"Compression: {analysis_data['compression_ratio']:.1%}")
|
|
1220
|
+
"""
|
|
1221
|
+
# Create Snapshot
|
|
1222
|
+
snapshot = self.export_runtime_state(frame_id="analysis_snapshot")
|
|
1223
|
+
|
|
1224
|
+
# Generate analysis report
|
|
1225
|
+
return snapshot.profile(format=format, title=title, options=options)
|
|
1226
|
+
|
|
1227
|
+
def save_trajectory(
|
|
1228
|
+
self,
|
|
1229
|
+
agent_name: str = "main",
|
|
1230
|
+
trajectory_path: Optional[str] = None,
|
|
1231
|
+
force_save: bool = False,
|
|
1232
|
+
pretty_format: bool = False,
|
|
1233
|
+
stage: Optional[str] = None,
|
|
1234
|
+
):
|
|
1235
|
+
"""
|
|
1236
|
+
Save dialog messages to file (legacy/simple mode).
|
|
1237
|
+
|
|
1238
|
+
Note: Stage-based trajectory saving is now handled by the Trajectory class.
|
|
1239
|
+
This method delegates to Trajectory.save_simple() for consistency.
|
|
1240
|
+
|
|
1241
|
+
Args:
|
|
1242
|
+
agent_name: The agent name, defaults to "main"
|
|
1243
|
+
trajectory_path: Custom trajectory path
|
|
1244
|
+
force_save: Force save even if memory is not enabled
|
|
1245
|
+
pretty_format: Save in pretty formatted text instead of JSON
|
|
1246
|
+
stage: [Deprecated] Use context.trajectory.finalize_stage()
|
|
1247
|
+
"""
|
|
1248
|
+
# Stage-based saving should use Trajectory class
|
|
1249
|
+
if stage is not None:
|
|
1250
|
+
logger.warning(
|
|
1251
|
+
"Stage-based trajectory saving via save_trajectory() is deprecated. "
|
|
1252
|
+
"Use context.trajectory.finalize_stage() instead."
|
|
1253
|
+
)
|
|
1254
|
+
return
|
|
1255
|
+
|
|
1256
|
+
if not (force_save or (self.config and self.config.memory_config and self.config.memory_config.enabled)):
|
|
1257
|
+
return
|
|
1258
|
+
|
|
1259
|
+
# Determine trajectory file path
|
|
1260
|
+
if not trajectory_path:
|
|
1261
|
+
current_date = datetime.now().strftime("%Y%m%d%H%M")
|
|
1262
|
+
dialog_base_path = (
|
|
1263
|
+
getattr(self.config.memory_config, "dialog_path", "data/dialog/")
|
|
1264
|
+
if self.config and self.config.memory_config
|
|
1265
|
+
else "data/dialog/"
|
|
1266
|
+
)
|
|
1267
|
+
user_id = self.user_id or "_default_user_"
|
|
1268
|
+
dialog_dir = f"{dialog_base_path}{agent_name}/user_{user_id}"
|
|
1269
|
+
trajectory_path = f"{dialog_dir}/dialog_{current_date}.json"
|
|
1270
|
+
os.makedirs(dialog_dir, exist_ok=True)
|
|
1271
|
+
|
|
1272
|
+
# Delegate to Trajectory class for actual saving
|
|
1273
|
+
Trajectory.save_simple(
|
|
1274
|
+
messages=self.get_messages().get_messages(),
|
|
1275
|
+
tools=self.skillkit.getSkillsSchema(),
|
|
1276
|
+
file_path=trajectory_path,
|
|
1277
|
+
pretty_format=pretty_format,
|
|
1278
|
+
user_id=self.user_id
|
|
1279
|
+
)
|
|
1280
|
+
|
|
1281
|
+
def info(self, log_str):
|
|
1282
|
+
logger.info(self._make_log(log_str))
|
|
1283
|
+
|
|
1284
|
+
def debug(self, log_str):
|
|
1285
|
+
logger.debug(self._make_log(log_str))
|
|
1286
|
+
|
|
1287
|
+
def warn(self, log_str):
|
|
1288
|
+
logger.warning(self._make_log(log_str))
|
|
1289
|
+
|
|
1290
|
+
def error(self, log_str):
|
|
1291
|
+
logger.error(self._make_log(log_str))
|
|
1292
|
+
|
|
1293
|
+
def _calc_all_skills(self):
|
|
1294
|
+
"""
|
|
1295
|
+
Calculate all skills
|
|
1296
|
+
"""
|
|
1297
|
+
self.all_skills = Skillset()
|
|
1298
|
+
|
|
1299
|
+
# Add skills from self.skillkit
|
|
1300
|
+
if self.skillkit and not self.skillkit.isEmpty():
|
|
1301
|
+
self.all_skills.addSkillkit(self.skillkit)
|
|
1302
|
+
|
|
1303
|
+
# Add skills from global_skills
|
|
1304
|
+
if self.global_skills is not None:
|
|
1305
|
+
self.all_skills.addSkillkit(self.global_skills.getAllSkills())
|
|
1306
|
+
|
|
1307
|
+
def _make_log(self, log_str):
|
|
1308
|
+
if len(log_str) < MAX_LOG_LENGTH:
|
|
1309
|
+
return "session[{}] {}".format(
|
|
1310
|
+
self.session_id, log_str.replace("\n", "\\n")
|
|
1311
|
+
)
|
|
1312
|
+
else:
|
|
1313
|
+
log_str = (
|
|
1314
|
+
log_str[: int(MAX_LOG_LENGTH * 1 / 3)]
|
|
1315
|
+
+ "..."
|
|
1316
|
+
+ log_str[-int(MAX_LOG_LENGTH * 2 / 3) :]
|
|
1317
|
+
)
|
|
1318
|
+
return "session[{}] {}".format(
|
|
1319
|
+
self.session_id, log_str.replace("\n", "\\n")
|
|
1320
|
+
)
|
|
1321
|
+
|
|
1322
|
+
def _export_context_manager_state(self) -> Dict[str, Any]:
|
|
1323
|
+
"""Export the complete state of context_manager
|
|
1324
|
+
|
|
1325
|
+
Returns:
|
|
1326
|
+
A dictionary containing information about all buckets
|
|
1327
|
+
"""
|
|
1328
|
+
buckets_state = []
|
|
1329
|
+
|
|
1330
|
+
for bucket_name, bucket in self.context_manager.state.buckets.items():
|
|
1331
|
+
# Serializing ContextBucket
|
|
1332
|
+
bucket_data = {
|
|
1333
|
+
"name": bucket_name,
|
|
1334
|
+
"priority": bucket.priority,
|
|
1335
|
+
"allocated_tokens": bucket.allocated_tokens,
|
|
1336
|
+
"message_role": bucket.message_role.value,
|
|
1337
|
+
"is_compressed": bucket.is_compressed,
|
|
1338
|
+
"messages": []
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
# Extract message content
|
|
1342
|
+
if isinstance(bucket.content, Messages):
|
|
1343
|
+
for msg in bucket.content.get_messages():
|
|
1344
|
+
bucket_data["messages"].append({
|
|
1345
|
+
"role": msg.role.value,
|
|
1346
|
+
"content": msg.content,
|
|
1347
|
+
"timestamp": msg.timestamp,
|
|
1348
|
+
"user_id": msg.user_id,
|
|
1349
|
+
"tool_calls": msg.tool_calls,
|
|
1350
|
+
"tool_call_id": msg.tool_call_id,
|
|
1351
|
+
"metadata": msg.metadata,
|
|
1352
|
+
})
|
|
1353
|
+
elif isinstance(bucket.content, str):
|
|
1354
|
+
# If it is a string type, save as a single message
|
|
1355
|
+
bucket_data["messages"].append({
|
|
1356
|
+
"role": bucket.message_role.value,
|
|
1357
|
+
"content": bucket.content,
|
|
1358
|
+
"timestamp": None,
|
|
1359
|
+
"user_id": "",
|
|
1360
|
+
"tool_calls": None,
|
|
1361
|
+
"tool_call_id": None,
|
|
1362
|
+
"metadata": {},
|
|
1363
|
+
})
|
|
1364
|
+
|
|
1365
|
+
buckets_state.append(bucket_data)
|
|
1366
|
+
|
|
1367
|
+
return {
|
|
1368
|
+
"buckets": buckets_state,
|
|
1369
|
+
"layout_policy": self.context_manager.state.layout_policy,
|
|
1370
|
+
"bucket_order": self.context_manager.state.bucket_order,
|
|
1371
|
+
"total_tokens": self.context_manager.state.total_tokens,
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
def export_runtime_state(self, frame_id: str) -> "ContextSnapshot":
|
|
1375
|
+
"""Export runtime state as a snapshot"""
|
|
1376
|
+
from dolphin.core.coroutine.context_snapshot import ContextSnapshot
|
|
1377
|
+
|
|
1378
|
+
# Ensure the image is up to date
|
|
1379
|
+
self.sync_messages_from_llm_context(force=True)
|
|
1380
|
+
|
|
1381
|
+
# Export variable state
|
|
1382
|
+
variables = {}
|
|
1383
|
+
for name, var_obj in self.variable_pool.get_all_variables().items():
|
|
1384
|
+
if isinstance(var_obj, VarOutput):
|
|
1385
|
+
# VarOutput knows how to serialize itself into a dict
|
|
1386
|
+
variables[name] = var_obj.to_dict()
|
|
1387
|
+
elif isinstance(var_obj, Var):
|
|
1388
|
+
# It's a simple Var wrapper. We only want to store the value.
|
|
1389
|
+
value = var_obj.value
|
|
1390
|
+
if isinstance(value, Messages):
|
|
1391
|
+
# This is the problematic type. Serialize it.
|
|
1392
|
+
variables[name] = value.to_dict()
|
|
1393
|
+
else:
|
|
1394
|
+
# Assume other values are primitives and store them directly.
|
|
1395
|
+
variables[name] = value
|
|
1396
|
+
# This case is for values stored in the pool without a Var wrapper.
|
|
1397
|
+
elif isinstance(var_obj, Messages):
|
|
1398
|
+
variables[name] = var_obj.get_messages_as_dict()
|
|
1399
|
+
else:
|
|
1400
|
+
variables[name] = var_obj
|
|
1401
|
+
|
|
1402
|
+
# Export message history (export from the synchronized mirror)
|
|
1403
|
+
messages = []
|
|
1404
|
+
for agent_name, agent_messages in self.messages.items():
|
|
1405
|
+
for msg in agent_messages.get_messages():
|
|
1406
|
+
messages.append(
|
|
1407
|
+
{
|
|
1408
|
+
"agent": agent_name,
|
|
1409
|
+
"role": msg.role.value,
|
|
1410
|
+
"content": msg.content,
|
|
1411
|
+
"timestamp": msg.timestamp,
|
|
1412
|
+
"user_id": msg.user_id,
|
|
1413
|
+
"tool_calls": msg.tool_calls,
|
|
1414
|
+
"tool_call_id": msg.tool_call_id,
|
|
1415
|
+
"metadata": msg.metadata,
|
|
1416
|
+
}
|
|
1417
|
+
)
|
|
1418
|
+
|
|
1419
|
+
# Export runtime state
|
|
1420
|
+
runtime_state = {
|
|
1421
|
+
"user_id": self.user_id,
|
|
1422
|
+
"session_id": self.session_id,
|
|
1423
|
+
"cur_agent": self.cur_agent.getName() if self.cur_agent else None,
|
|
1424
|
+
"max_answer_len": self.max_answer_len,
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
# Export skill set status
|
|
1428
|
+
skillkit_state = {}
|
|
1429
|
+
if self.skillkit and not self.skillkit.isEmpty():
|
|
1430
|
+
try:
|
|
1431
|
+
skillkit_state = {
|
|
1432
|
+
"skills_schema": self.skillkit.getSkillsSchema(),
|
|
1433
|
+
"skill_count": len(self.skillkit.getSkills()),
|
|
1434
|
+
}
|
|
1435
|
+
except Exception as e:
|
|
1436
|
+
logger.warning(f"Failed to export skillkit state: {e}")
|
|
1437
|
+
|
|
1438
|
+
# Export the complete state of context_manager (including bucket structure)
|
|
1439
|
+
context_manager_state = self._export_context_manager_state()
|
|
1440
|
+
return ContextSnapshot.create_snapshot(
|
|
1441
|
+
frame_id=frame_id,
|
|
1442
|
+
variables=variables,
|
|
1443
|
+
messages=messages,
|
|
1444
|
+
runtime_state=runtime_state,
|
|
1445
|
+
skillkit_state=skillkit_state,
|
|
1446
|
+
context_manager_state=context_manager_state,
|
|
1447
|
+
)
|
|
1448
|
+
|
|
1449
|
+
def apply_runtime_state(self, snapshot: "ContextSnapshot"):
|
|
1450
|
+
"""Restore runtime state from snapshot"""
|
|
1451
|
+
from dolphin.core.context_engineer.config.settings import BuildInBucket
|
|
1452
|
+
|
|
1453
|
+
# Restore variable state
|
|
1454
|
+
self.variable_pool.clear()
|
|
1455
|
+
for name, value in snapshot.variables.items():
|
|
1456
|
+
# Support restoring VarOutput structure from snapshot
|
|
1457
|
+
try:
|
|
1458
|
+
from dolphin.core.context.var_output import VarOutput
|
|
1459
|
+
|
|
1460
|
+
if VarOutput.is_serialized_dict(value):
|
|
1461
|
+
# This is the serialized structure of VarOutput
|
|
1462
|
+
self.variable_pool.set_var(name, VarOutput.from_dict(value))
|
|
1463
|
+
else:
|
|
1464
|
+
# Other simple types are restored by value
|
|
1465
|
+
self.variable_pool.set_var(name, value)
|
|
1466
|
+
except Exception:
|
|
1467
|
+
# Fallback: Set to original value to avoid execution interruption due to restore failure
|
|
1468
|
+
self.variable_pool.set_var(name, value)
|
|
1469
|
+
|
|
1470
|
+
# Restore message history to context_manager (single data source)
|
|
1471
|
+
self.context_manager.state.buckets.clear()
|
|
1472
|
+
|
|
1473
|
+
# Check if there is a complete context_manager_state (new version snapshot)
|
|
1474
|
+
# Note: Even if buckets is an empty list, use the new version restore logic as long as context_manager_state exists
|
|
1475
|
+
# Full restoration: Reconstruct the complete bucket structure from context_manager_state
|
|
1476
|
+
bucket_count = len(snapshot.context_manager_state.get('buckets', []))
|
|
1477
|
+
logger.info(f"Restoring {bucket_count} buckets from context_manager_state")
|
|
1478
|
+
|
|
1479
|
+
for bucket_data in snapshot.context_manager_state['buckets']:
|
|
1480
|
+
# Reconstruct Messages object
|
|
1481
|
+
messages = Messages()
|
|
1482
|
+
for msg_data in bucket_data['messages']:
|
|
1483
|
+
messages.append_message(
|
|
1484
|
+
role=MessageRole(msg_data['role']),
|
|
1485
|
+
content=msg_data['content'],
|
|
1486
|
+
user_id=msg_data.get('user_id', ''),
|
|
1487
|
+
tool_calls=msg_data.get('tool_calls'),
|
|
1488
|
+
tool_call_id=msg_data.get('tool_call_id'),
|
|
1489
|
+
metadata=msg_data.get('metadata', {}),
|
|
1490
|
+
)
|
|
1491
|
+
|
|
1492
|
+
# Reconstruct ContextBucket (preserving original priority and other attributes)
|
|
1493
|
+
if messages.get_messages(): # Only add non-empty messages
|
|
1494
|
+
self.context_manager.add_bucket(
|
|
1495
|
+
bucket_name=bucket_data['name'],
|
|
1496
|
+
content=messages,
|
|
1497
|
+
priority=bucket_data['priority'],
|
|
1498
|
+
allocated_tokens=bucket_data['allocated_tokens'],
|
|
1499
|
+
message_role=MessageRole(bucket_data['message_role']),
|
|
1500
|
+
)
|
|
1501
|
+
|
|
1502
|
+
# Restore bucket_order and layout_policy
|
|
1503
|
+
self.context_manager.state.bucket_order = snapshot.context_manager_state.get('bucket_order', [])
|
|
1504
|
+
self.context_manager.state.layout_policy = snapshot.context_manager_state.get('layout_policy', 'default')
|
|
1505
|
+
|
|
1506
|
+
logger.info("Successfully restored complete bucket structure from snapshot")
|
|
1507
|
+
|
|
1508
|
+
# Synchronize from context_manager to mirror (ensure symmetry)
|
|
1509
|
+
self.messages.clear()
|
|
1510
|
+
self.messages_dirty = True # Force Synchronization
|
|
1511
|
+
self.sync_messages_from_llm_context(force=True)
|
|
1512
|
+
|
|
1513
|
+
# Resume runtime state
|
|
1514
|
+
runtime_state = snapshot.runtime_state
|
|
1515
|
+
if runtime_state:
|
|
1516
|
+
self.user_id = runtime_state.get("user_id")
|
|
1517
|
+
self.session_id = runtime_state.get("session_id")
|
|
1518
|
+
self.max_answer_len = runtime_state.get(
|
|
1519
|
+
"max_answer_len", MAX_ANSWER_CONTENT_LENGTH
|
|
1520
|
+
)
|
|
1521
|
+
|
|
1522
|
+
# Resume the current agent (to be handled in the calling function above)
|
|
1523
|
+
# The restoration of self.cur_agent requires external coordination
|
|
1524
|
+
|
|
1525
|
+
# Verify recovery results
|
|
1526
|
+
bucket_count = len(self.context_manager.state.buckets)
|
|
1527
|
+
total_messages = sum(
|
|
1528
|
+
len(bucket.content.get_messages()) if isinstance(bucket.content, Messages) else 1
|
|
1529
|
+
for bucket in self.context_manager.state.buckets.values()
|
|
1530
|
+
)
|
|
1531
|
+
|
|
1532
|
+
logger.info(
|
|
1533
|
+
f"Restored context state from snapshot {snapshot.snapshot_id}: "
|
|
1534
|
+
f"{bucket_count} buckets, {total_messages} total messages, "
|
|
1535
|
+
f"schema_version={snapshot.schema_version}"
|
|
1536
|
+
)
|
|
1537
|
+
|
|
1538
|
+
# === User Interrupt API ===
|
|
1539
|
+
|
|
1540
|
+
def set_interrupt_event(self, interrupt_event: asyncio.Event) -> None:
|
|
1541
|
+
"""Set the user interrupt event (injected by Agent layer).
|
|
1542
|
+
|
|
1543
|
+
Args:
|
|
1544
|
+
interrupt_event: asyncio.Event that will be set when user requests interrupt
|
|
1545
|
+
"""
|
|
1546
|
+
self._interrupt_event = interrupt_event
|
|
1547
|
+
|
|
1548
|
+
def get_interrupt_event(self) -> Optional[asyncio.Event]:
|
|
1549
|
+
"""Get the user interrupt event.
|
|
1550
|
+
|
|
1551
|
+
Returns:
|
|
1552
|
+
The interrupt event, or None if not set
|
|
1553
|
+
"""
|
|
1554
|
+
return self._interrupt_event
|
|
1555
|
+
|
|
1556
|
+
def is_interrupted(self) -> bool:
|
|
1557
|
+
"""Check if user has requested an interrupt.
|
|
1558
|
+
|
|
1559
|
+
Returns:
|
|
1560
|
+
True if interrupt event is set, False otherwise
|
|
1561
|
+
"""
|
|
1562
|
+
return self._interrupt_event is not None and self._interrupt_event.is_set()
|
|
1563
|
+
|
|
1564
|
+
def check_user_interrupt(self) -> None:
|
|
1565
|
+
"""Check user interrupt status and raise exception if interrupted.
|
|
1566
|
+
|
|
1567
|
+
This is the primary checkpoint method to be called at strategic locations
|
|
1568
|
+
during execution (e.g., LLM streaming loop, before skill execution).
|
|
1569
|
+
|
|
1570
|
+
Raises:
|
|
1571
|
+
UserInterrupt: If user has requested interrupt
|
|
1572
|
+
"""
|
|
1573
|
+
if self.is_interrupted():
|
|
1574
|
+
from dolphin.core.common.exceptions import UserInterrupt
|
|
1575
|
+
raise UserInterrupt("User interrupted execution")
|
|
1576
|
+
|
|
1577
|
+
def clear_interrupt(self) -> None:
|
|
1578
|
+
"""Clear the interrupt status (called when resuming execution)."""
|
|
1579
|
+
if self._interrupt_event is not None:
|
|
1580
|
+
self._interrupt_event.clear()
|