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,1106 @@
|
|
|
1
|
+
"""Dolphin Language Agent - A concrete implementation based on BaseAgent"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import inspect
|
|
6
|
+
from dolphin.core.context_engineer.config.settings import BuildInBucket
|
|
7
|
+
import aiofiles
|
|
8
|
+
import logging
|
|
9
|
+
from typing import AsyncGenerator, Any, Dict, Optional
|
|
10
|
+
import asyncio
|
|
11
|
+
|
|
12
|
+
from dolphin.core.context.context import Context
|
|
13
|
+
from dolphin.core.skill.skillkit import Skillkit
|
|
14
|
+
from dolphin.core.common.object_type import ObjectTypeFactory
|
|
15
|
+
from dolphin.core.parser.parser import Parser
|
|
16
|
+
|
|
17
|
+
from dolphin.core.agent.base_agent import BaseAgent
|
|
18
|
+
from dolphin.core.agent.agent_state import AgentState
|
|
19
|
+
from dolphin.core.config.global_config import GlobalConfig
|
|
20
|
+
from dolphin.core.coroutine.step_result import StepResult
|
|
21
|
+
import dolphin.core.executor.dolphin_executor as dolphin_language
|
|
22
|
+
from dolphin.core.executor.dolphin_executor import DolphinExecutor
|
|
23
|
+
from dolphin.core.common.exceptions import DolphinAgentException
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DolphinAgent(BaseAgent):
|
|
27
|
+
"""DPH file execution Agent implementation based on BaseAgent
|
|
28
|
+
Supports full lifecycle management and state control
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
name: Optional[str] = None,
|
|
34
|
+
description: Optional[str] = None,
|
|
35
|
+
skillkit: Optional[Skillkit] = None,
|
|
36
|
+
variables: Optional[Dict[str, Any]] = None,
|
|
37
|
+
global_skills=None,
|
|
38
|
+
file_path: Optional[str] = None,
|
|
39
|
+
content: Optional[str] = None,
|
|
40
|
+
global_config: Optional[GlobalConfig] = None,
|
|
41
|
+
global_config_path: str = "",
|
|
42
|
+
global_types: ObjectTypeFactory = ObjectTypeFactory(),
|
|
43
|
+
verbose: bool = False,
|
|
44
|
+
is_cli: bool = False,
|
|
45
|
+
log_level: int = logging.INFO,
|
|
46
|
+
output_variables: Optional[list] = None,
|
|
47
|
+
):
|
|
48
|
+
"""Initialize Dolphin Agent
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
name: Agent name, automatically generated if not provided
|
|
52
|
+
description: Agent description
|
|
53
|
+
skillkit: Skillkit instance
|
|
54
|
+
variables: Initial variables
|
|
55
|
+
file_path: DPH file path (optional, mutually exclusive with content)
|
|
56
|
+
content: DPH content as a string (optional, mutually exclusive with file_path)
|
|
57
|
+
global_config: Global configuration
|
|
58
|
+
global_config_path: Path to the global configuration file
|
|
59
|
+
global_skills: Global skills
|
|
60
|
+
global_types: Global type definitions
|
|
61
|
+
verbose: Whether to enable verbose output mode (detailed logging)
|
|
62
|
+
is_cli: Whether running in CLI mode (controls Rich/terminal beautification)
|
|
63
|
+
log_level: Log level, such as logging.DEBUG, logging.INFO, etc.
|
|
64
|
+
output_variables: List of variable names to return, corresponding to VariablePool variable names.
|
|
65
|
+
If specified, only these variables are returned; if empty list or None, all variables are returned
|
|
66
|
+
"""
|
|
67
|
+
# Parameter Validation
|
|
68
|
+
if file_path is None and content is None:
|
|
69
|
+
raise DolphinAgentException(
|
|
70
|
+
"INVALID_ARGUMENT", "必须提供 file_path 或 content 参数"
|
|
71
|
+
)
|
|
72
|
+
if file_path is not None and content is not None:
|
|
73
|
+
raise DolphinAgentException(
|
|
74
|
+
"INVALID_ARGUMENT", "不能同时提供 file_path 和 content 参数"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Set name according to content source
|
|
78
|
+
if file_path is not None:
|
|
79
|
+
# File Mode
|
|
80
|
+
self.content_source = "file"
|
|
81
|
+
agent_name = name or os.path.splitext(os.path.basename(file_path))[0]
|
|
82
|
+
|
|
83
|
+
# Verify file existence
|
|
84
|
+
if not os.path.exists(file_path):
|
|
85
|
+
raise DolphinAgentException(
|
|
86
|
+
"FILE_NOT_FOUND", f"DPH file not found: {file_path}"
|
|
87
|
+
)
|
|
88
|
+
else:
|
|
89
|
+
# Content Mode
|
|
90
|
+
self.content_source = "content"
|
|
91
|
+
agent_name = name or "content_agent"
|
|
92
|
+
|
|
93
|
+
super().__init__(
|
|
94
|
+
name=agent_name, description=description, global_config=global_config
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
self.content = content
|
|
98
|
+
self.skillkit = skillkit
|
|
99
|
+
self.variables = variables
|
|
100
|
+
self.file_path = file_path
|
|
101
|
+
self.global_config = global_config
|
|
102
|
+
self.global_config_path = global_config_path
|
|
103
|
+
self.global_skills = global_skills
|
|
104
|
+
self.global_types = global_types
|
|
105
|
+
self.verbose = verbose # Store the verbose parameter
|
|
106
|
+
self.is_cli = is_cli # Store CLI mode flag
|
|
107
|
+
self.log_level = log_level # Storage log level parameter
|
|
108
|
+
self.executor: Optional[DolphinExecutor] = None
|
|
109
|
+
|
|
110
|
+
self.execution_context = None
|
|
111
|
+
# Normalize output variable parameters to avoid sharing issues caused by mutable default arguments
|
|
112
|
+
self.output_variables = output_variables or [] # Store output variable list
|
|
113
|
+
self.header_info = {} # Store parsed header information
|
|
114
|
+
|
|
115
|
+
# Initialize components (completed in the _initialize method)
|
|
116
|
+
|
|
117
|
+
# Set log level
|
|
118
|
+
from dolphin.core.logging.logger import set_log_level
|
|
119
|
+
|
|
120
|
+
set_log_level(self.log_level)
|
|
121
|
+
|
|
122
|
+
async def achat(self, message = None, **kwargs) -> AsyncGenerator[Any, None]:
|
|
123
|
+
"""Interactive dialogue mode execution, used to continue multi-turn dialogues after the Agent is initialized and running.
|
|
124
|
+
|
|
125
|
+
.. deprecated:: 2.1
|
|
126
|
+
Use :meth:`continue_chat` instead. The achat method returns raw progress data
|
|
127
|
+
without wrapping, which is inconsistent with arun. The new continue_chat method
|
|
128
|
+
provides a consistent API by wrapping results in _progress list.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
message: The message input by the user. Can be:
|
|
132
|
+
- str: Plain text message
|
|
133
|
+
- List[Dict]: Multimodal content (e.g., [{"type": "text", "text": "..."}, {"type": "image_url", ...}])
|
|
134
|
+
- None: Assume the message has been added to the Context.
|
|
135
|
+
**kwargs: Other parameters
|
|
136
|
+
|
|
137
|
+
Yields:
|
|
138
|
+
Execution results
|
|
139
|
+
"""
|
|
140
|
+
import warnings
|
|
141
|
+
warnings.warn(
|
|
142
|
+
"achat() is deprecated and will be removed in v3.0. "
|
|
143
|
+
"Use continue_chat() instead for consistent API with arun().",
|
|
144
|
+
DeprecationWarning,
|
|
145
|
+
stacklevel=2
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if self.executor is None:
|
|
149
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
150
|
+
|
|
151
|
+
# Pass message through kwargs to continue_exploration
|
|
152
|
+
# Note: Do NOT call reset_for_block() here - continue_exploration will handle it
|
|
153
|
+
# and we need to pass content via kwargs, not bucket (which would be cleared)
|
|
154
|
+
if message:
|
|
155
|
+
kwargs["content"] = message
|
|
156
|
+
|
|
157
|
+
# Handled by the continue_exploration method of the executor
|
|
158
|
+
# continue_exploration will automatically get the previously used model (if available) from context
|
|
159
|
+
# This can reduce the coupling between DolphinAgent and internal implementation classes.
|
|
160
|
+
async for result in self.executor.continue_exploration(
|
|
161
|
+
**kwargs
|
|
162
|
+
):
|
|
163
|
+
yield result
|
|
164
|
+
|
|
165
|
+
async def continue_chat(
|
|
166
|
+
self,
|
|
167
|
+
message: str = None,
|
|
168
|
+
stream_variables: bool = True,
|
|
169
|
+
stream_mode: str = "full",
|
|
170
|
+
**kwargs
|
|
171
|
+
) -> AsyncGenerator[Any, None]:
|
|
172
|
+
"""Continue multi-turn dialogue with consistent API format as arun().
|
|
173
|
+
|
|
174
|
+
This is the recommended method for multi-turn conversations, replacing achat().
|
|
175
|
+
Returns results wrapped in _progress list for consistency with arun().
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
message: The message input by the user. Can be:
|
|
179
|
+
- str: Plain text message
|
|
180
|
+
- List[Dict]: Multimodal content
|
|
181
|
+
- None: Assume the message has been added to the Context
|
|
182
|
+
stream_variables: Whether to enable streaming variable output (default: True)
|
|
183
|
+
stream_mode: Streaming mode:
|
|
184
|
+
- "full": Return full accumulated text (default)
|
|
185
|
+
- "delta": Return only incremental text changes
|
|
186
|
+
**kwargs: Other parameters
|
|
187
|
+
|
|
188
|
+
Yields:
|
|
189
|
+
Results in the same format as arun():
|
|
190
|
+
{
|
|
191
|
+
'_status': 'running' | 'completed' | 'interrupted',
|
|
192
|
+
'_progress': [
|
|
193
|
+
{
|
|
194
|
+
'stage': 'llm' | 'tool_call' | ...,
|
|
195
|
+
'status': 'running' | 'completed' | 'failed',
|
|
196
|
+
'answer': str, # Full text (stream_mode='full') or delta (stream_mode='delta')
|
|
197
|
+
'delta': str, # Only present when stream_mode='delta'
|
|
198
|
+
...
|
|
199
|
+
}
|
|
200
|
+
],
|
|
201
|
+
'_interrupt': {...} # Only present when _status='interrupted'
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
Raises:
|
|
205
|
+
DolphinAgentException: If EXPLORE_BLOCK_V2 flag is not disabled or agent not initialized
|
|
206
|
+
|
|
207
|
+
Example:
|
|
208
|
+
# First run
|
|
209
|
+
async for result in agent.arun(query="Hello"):
|
|
210
|
+
process(result)
|
|
211
|
+
|
|
212
|
+
# Continue conversation
|
|
213
|
+
async for result in agent.continue_chat(message="Continue..."):
|
|
214
|
+
process(result) # Same format as arun!
|
|
215
|
+
"""
|
|
216
|
+
# Fail-fast check: EXPLORE_BLOCK_V2 must be disabled
|
|
217
|
+
from dolphin.core import flags
|
|
218
|
+
if flags.is_enabled(flags.EXPLORE_BLOCK_V2):
|
|
219
|
+
raise DolphinAgentException(
|
|
220
|
+
"INVALID_FLAG_STATE",
|
|
221
|
+
"continue_chat() requires EXPLORE_BLOCK_V2 flag to be disabled. "
|
|
222
|
+
"Set flags.set_flag(flags.EXPLORE_BLOCK_V2, False) before using continue_chat()."
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# Lazy initialization if needed
|
|
226
|
+
if self.executor is None:
|
|
227
|
+
await self.initialize()
|
|
228
|
+
|
|
229
|
+
# Pass message through kwargs
|
|
230
|
+
if message:
|
|
231
|
+
kwargs["content"] = message
|
|
232
|
+
|
|
233
|
+
# Validate stream_mode
|
|
234
|
+
if stream_mode not in ("full", "delta"):
|
|
235
|
+
raise DolphinAgentException(
|
|
236
|
+
"INVALID_PARAMETER",
|
|
237
|
+
f"stream_mode must be 'full' or 'delta', got '{stream_mode}'"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Track last answer for delta calculation
|
|
241
|
+
last_answer = {}
|
|
242
|
+
|
|
243
|
+
# Direct passthrough pattern (like achat), with optional wrapping
|
|
244
|
+
# This ensures streaming works correctly by yielding each result immediately
|
|
245
|
+
async for result in self.executor.continue_exploration(**kwargs):
|
|
246
|
+
# Interrupt: wrap in consistent format with _status="interrupted"
|
|
247
|
+
if isinstance(result, dict) and result.get("status") == "interrupted":
|
|
248
|
+
yield {
|
|
249
|
+
"_status": "interrupted",
|
|
250
|
+
"_progress": [],
|
|
251
|
+
"_interrupt": result # Preserve original interrupt info
|
|
252
|
+
}
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
# Streaming mode: get context variables (contains _progress)
|
|
256
|
+
ctx = self.executor.context if self.executor else None
|
|
257
|
+
if ctx is not None and stream_variables:
|
|
258
|
+
if self.output_variables is not None and len(self.output_variables) > 0:
|
|
259
|
+
data = ctx.get_variables_values(self.output_variables)
|
|
260
|
+
else:
|
|
261
|
+
data = ctx.get_all_variables_values()
|
|
262
|
+
|
|
263
|
+
# Apply delta mode if requested
|
|
264
|
+
if stream_mode == "delta" and "_progress" in data:
|
|
265
|
+
data = self._apply_delta_mode(data, last_answer)
|
|
266
|
+
|
|
267
|
+
# data already contains _status and _progress from context
|
|
268
|
+
yield data
|
|
269
|
+
else:
|
|
270
|
+
# Non-streaming or no context: wrap raw result
|
|
271
|
+
yield {
|
|
272
|
+
"_status": "running",
|
|
273
|
+
"_progress": [result] if isinstance(result, dict) else []
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
# Final state: mark as completed
|
|
277
|
+
try:
|
|
278
|
+
ctx = self.executor.context if self.executor else None
|
|
279
|
+
if ctx is not None:
|
|
280
|
+
if self.output_variables is not None and len(self.output_variables) > 0:
|
|
281
|
+
final_data = ctx.get_variables_values(self.output_variables)
|
|
282
|
+
else:
|
|
283
|
+
final_data = ctx.get_all_variables_values()
|
|
284
|
+
|
|
285
|
+
# Explicitly mark completion status
|
|
286
|
+
final_data["_status"] = "completed"
|
|
287
|
+
|
|
288
|
+
# Apply delta mode to final data
|
|
289
|
+
if stream_mode == "delta" and "_progress" in final_data:
|
|
290
|
+
final_data = self._apply_delta_mode(final_data, last_answer)
|
|
291
|
+
|
|
292
|
+
yield final_data
|
|
293
|
+
except (AttributeError, KeyError) as e:
|
|
294
|
+
# Expected errors when context/variables are not fully initialized
|
|
295
|
+
self._logger.debug(f"Could not get final variables in continue_chat: {e}")
|
|
296
|
+
|
|
297
|
+
def _apply_delta_mode(self, data: dict, last_answer: dict) -> dict:
|
|
298
|
+
"""Apply delta mode to progress data by calculating incremental changes.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
data: Current data with _progress list
|
|
302
|
+
last_answer: Dictionary tracking last answer text per stage
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Modified data with delta field added to each progress item
|
|
306
|
+
"""
|
|
307
|
+
if "_progress" not in data or not isinstance(data["_progress"], list):
|
|
308
|
+
return data
|
|
309
|
+
|
|
310
|
+
for prog in data["_progress"]:
|
|
311
|
+
if not isinstance(prog, dict):
|
|
312
|
+
continue
|
|
313
|
+
|
|
314
|
+
stage = prog.get("stage", "")
|
|
315
|
+
stage_id = prog.get("id", stage) # Use ID if available
|
|
316
|
+
current_answer = prog.get("answer", "")
|
|
317
|
+
|
|
318
|
+
# Calculate delta
|
|
319
|
+
last_text = last_answer.get(stage_id, "")
|
|
320
|
+
if not last_text:
|
|
321
|
+
# First time: full text is delta
|
|
322
|
+
delta = current_answer
|
|
323
|
+
elif current_answer.startswith(last_text):
|
|
324
|
+
# Normal case: extract new portion
|
|
325
|
+
delta = current_answer[len(last_text):]
|
|
326
|
+
else:
|
|
327
|
+
# Text changed unexpectedly: reset
|
|
328
|
+
delta = current_answer
|
|
329
|
+
|
|
330
|
+
# Update tracking and add delta field
|
|
331
|
+
last_answer[stage_id] = current_answer
|
|
332
|
+
prog["delta"] = delta
|
|
333
|
+
|
|
334
|
+
return data
|
|
335
|
+
|
|
336
|
+
async def _on_initialize(self):
|
|
337
|
+
"""Initialize Agent component"""
|
|
338
|
+
try:
|
|
339
|
+
# Get content according to the source
|
|
340
|
+
if self.content_source == "file" and self.file_path is not None:
|
|
341
|
+
# File mode: Asynchronously read file contents to avoid blocking the event loop
|
|
342
|
+
async with aiofiles.open(self.file_path, "r", encoding="utf-8") as f:
|
|
343
|
+
self.content = await f.read()
|
|
344
|
+
# Content mode: Content has been set in the constructor
|
|
345
|
+
|
|
346
|
+
# Parse and remove the header information block (such as @DESC ... @DESC)
|
|
347
|
+
self._parse_header_info()
|
|
348
|
+
|
|
349
|
+
# Create Executor
|
|
350
|
+
self.executor = dolphin_language.DolphinExecutor(
|
|
351
|
+
global_configpath=self.global_config_path,
|
|
352
|
+
global_config=self.global_config,
|
|
353
|
+
global_skills=self.global_skills,
|
|
354
|
+
global_types=self.global_types,
|
|
355
|
+
verbose=self.verbose,
|
|
356
|
+
is_cli=self.is_cli,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
await self.executor.executor_init(
|
|
360
|
+
{
|
|
361
|
+
"skillkit": self.skillkit,
|
|
362
|
+
"variables": self.variables,
|
|
363
|
+
}
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
# Validate DPH syntax
|
|
367
|
+
self._validate_syntax()
|
|
368
|
+
|
|
369
|
+
self._logger.debug(f"Dolphin Agent '{self.name}' initialized successfully")
|
|
370
|
+
|
|
371
|
+
except Exception as e:
|
|
372
|
+
raise DolphinAgentException(
|
|
373
|
+
"INIT_FAILED", f"Failed to initialize agent: {str(e)}"
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
def _parse_header_info(self):
|
|
377
|
+
"""Parse the header information block of DPH files, supporting the general @XX ... @XX format.
|
|
378
|
+
同时从content中移除这些header块,避免干扰后续解析
|
|
379
|
+
|
|
380
|
+
Examples:
|
|
381
|
+
@DESC
|
|
382
|
+
This is the agent description
|
|
383
|
+
@DESC
|
|
384
|
+
|
|
385
|
+
@VERSION
|
|
386
|
+
1.0.0
|
|
387
|
+
@VERSION
|
|
388
|
+
"""
|
|
389
|
+
if self.content is None:
|
|
390
|
+
return
|
|
391
|
+
|
|
392
|
+
self.header_info = {}
|
|
393
|
+
|
|
394
|
+
# Use regular expressions to match information blocks in the format @XX ... @XX
|
|
395
|
+
# Pattern: @-prefixed tags (allowing only uppercase letters, numbers, and underscores), non-greedy match until the closing tag of the same type
|
|
396
|
+
pattern = r"@([A-Z][A-Z_0-9]+)\s*(.*?)\s*@\1"
|
|
397
|
+
|
|
398
|
+
matches = re.findall(pattern, self.content, re.DOTALL)
|
|
399
|
+
|
|
400
|
+
for tag_name, content in matches:
|
|
401
|
+
# Clean up extra blank lines in the content
|
|
402
|
+
clean_content = re.sub(r"\n\s*\n", "\n", content.strip())
|
|
403
|
+
self.header_info[tag_name] = clean_content
|
|
404
|
+
|
|
405
|
+
# Remove all matching header blocks from self.content
|
|
406
|
+
self.content = re.sub(pattern, "", self.content, flags=re.DOTALL)
|
|
407
|
+
|
|
408
|
+
# Clean up extra blank lines that may be generated after removing the header
|
|
409
|
+
self.content = re.sub(r"\n\s*\n\s*\n", "\n\n", self.content).strip()
|
|
410
|
+
|
|
411
|
+
# If DESC is parsed and description is not provided during construction, use the parsed DESC
|
|
412
|
+
if "DESC" in self.header_info and not self.description:
|
|
413
|
+
self.description = self.header_info["DESC"]
|
|
414
|
+
|
|
415
|
+
def _validate_syntax(self):
|
|
416
|
+
"""Validate DPH file syntax"""
|
|
417
|
+
if self.content is None:
|
|
418
|
+
raise DolphinAgentException("INVALID_CONTENT", "DPH content is empty")
|
|
419
|
+
|
|
420
|
+
is_valid, error_message = Parser.validate_syntax(self.content)
|
|
421
|
+
if not is_valid:
|
|
422
|
+
raise DolphinAgentException("SYNTAX_ERROR", error_message)
|
|
423
|
+
|
|
424
|
+
async def _on_execute(self, **kwargs) -> AsyncGenerator[Any, None]:
|
|
425
|
+
"""Execute DPH file content
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
**kwargs: Additional parameters passed to the executor
|
|
429
|
+
|
|
430
|
+
Yields:
|
|
431
|
+
Execution results
|
|
432
|
+
"""
|
|
433
|
+
if self.executor is None:
|
|
434
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
435
|
+
|
|
436
|
+
try:
|
|
437
|
+
# Initialize executor parameters
|
|
438
|
+
if kwargs:
|
|
439
|
+
init_params = {"variables": kwargs}
|
|
440
|
+
await self.executor.executor_init(init_params)
|
|
441
|
+
|
|
442
|
+
# Set the current Agent
|
|
443
|
+
self.executor.context.set_cur_agent(self)
|
|
444
|
+
|
|
445
|
+
# Execute DPH content
|
|
446
|
+
async for result in self.executor.run(
|
|
447
|
+
content=self.content, output_variables=self.output_variables, **kwargs
|
|
448
|
+
):
|
|
449
|
+
yield result
|
|
450
|
+
|
|
451
|
+
except Exception as e:
|
|
452
|
+
self._logger.error(f"Execution error: {e}")
|
|
453
|
+
raise DolphinAgentException("EXECUTION_ERROR", str(e))
|
|
454
|
+
|
|
455
|
+
async def _on_pause(self):
|
|
456
|
+
"""Pause Agent execution"""
|
|
457
|
+
self._logger.debug(f"Agent '{self.name}' paused")
|
|
458
|
+
# Here you can add cleanup logic when paused
|
|
459
|
+
# For example: save the current execution state, release resources, etc.
|
|
460
|
+
|
|
461
|
+
async def _on_resume(self):
|
|
462
|
+
"""Restore Agent Execution"""
|
|
463
|
+
self._logger.debug(f"Agent '{self.name}' resumed")
|
|
464
|
+
# Here you can add logic for recovery.
|
|
465
|
+
# For example: restoring execution state, re-initializing resources, etc.
|
|
466
|
+
|
|
467
|
+
async def _on_terminate(self):
|
|
468
|
+
"""Terminate Agent execution"""
|
|
469
|
+
self._logger.debug(f"Agent '{self.name}' terminated")
|
|
470
|
+
# Clean up resources
|
|
471
|
+
if self.executor:
|
|
472
|
+
try:
|
|
473
|
+
self.executor.shutdown()
|
|
474
|
+
except Exception as e:
|
|
475
|
+
self._logger.warning(f"Error during executor shutdown: {e}")
|
|
476
|
+
|
|
477
|
+
# === Coroutine Series Method Implementation ===
|
|
478
|
+
async def _on_start_coroutine(self, **kwargs):
|
|
479
|
+
"""Start coroutine execution"""
|
|
480
|
+
if self.executor is None:
|
|
481
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
482
|
+
|
|
483
|
+
try:
|
|
484
|
+
# Initialize executor parameters
|
|
485
|
+
if kwargs:
|
|
486
|
+
init_params = {"variables": kwargs}
|
|
487
|
+
await self.executor.executor_init(init_params)
|
|
488
|
+
|
|
489
|
+
# Set the current Agent
|
|
490
|
+
self.executor.context.set_cur_agent(self)
|
|
491
|
+
|
|
492
|
+
# Start coroutine execution (compatible with MagicMock scenarios)
|
|
493
|
+
start_result = self.executor.start_coroutine(content=self.content, **kwargs)
|
|
494
|
+
frame = (
|
|
495
|
+
await start_result
|
|
496
|
+
if inspect.isawaitable(start_result)
|
|
497
|
+
else start_result
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
# Inject interrupt event into executor context for user interrupt detection
|
|
501
|
+
# This enables Context.check_user_interrupt() to work correctly
|
|
502
|
+
if self.executor and self.executor.context:
|
|
503
|
+
self.executor.context.set_interrupt_event(self._interrupt_event)
|
|
504
|
+
self._logger.debug("Interrupt event injected into executor context")
|
|
505
|
+
|
|
506
|
+
return frame
|
|
507
|
+
|
|
508
|
+
except Exception as e:
|
|
509
|
+
self._logger.error(f"Start coroutine error: {e}")
|
|
510
|
+
raise DolphinAgentException("START_COROUTINE_FAILED", str(e)) from e
|
|
511
|
+
|
|
512
|
+
async def _on_step_coroutine(self) -> StepResult:
|
|
513
|
+
"""Execute one step"""
|
|
514
|
+
if self.executor is None:
|
|
515
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
516
|
+
|
|
517
|
+
if self._current_frame is None:
|
|
518
|
+
raise DolphinAgentException("NO_FRAME", "No execution frame available")
|
|
519
|
+
|
|
520
|
+
try:
|
|
521
|
+
# Execute one step, return StepResult
|
|
522
|
+
result = await self.executor.step_coroutine(self._current_frame.frame_id)
|
|
523
|
+
|
|
524
|
+
# Return StepResult directly, the caller can use is_interrupted/is_completed/is_running to determine.
|
|
525
|
+
return result
|
|
526
|
+
|
|
527
|
+
except Exception as e:
|
|
528
|
+
self._logger.error(f"Step coroutine error: {e}")
|
|
529
|
+
raise DolphinAgentException("STEP_COROUTINE_FAILED", str(e)) from e
|
|
530
|
+
|
|
531
|
+
async def _on_run_coroutine(self):
|
|
532
|
+
"""Continuously execute until the tool interrupts or completes (fast mode)"""
|
|
533
|
+
if self.executor is None:
|
|
534
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
535
|
+
|
|
536
|
+
if self._current_frame is None:
|
|
537
|
+
raise DolphinAgentException("NO_FRAME", "No execution frame available")
|
|
538
|
+
|
|
539
|
+
try:
|
|
540
|
+
# If a progress callback exists (for streaming variable output), pass it to the executor.
|
|
541
|
+
progress_cb = getattr(self, "_progress_callback", None)
|
|
542
|
+
result = await self.executor.run_coroutine(
|
|
543
|
+
self._current_frame.frame_id, progress_callback=progress_cb
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
# Return StepResult directly, the caller can use is_interrupted/is_completed to determine.
|
|
547
|
+
return result
|
|
548
|
+
|
|
549
|
+
except Exception as e:
|
|
550
|
+
self._logger.error(f"Run coroutine error: {e}")
|
|
551
|
+
raise DolphinAgentException("RUN_COROUTINE_FAILED", str(e)) from e
|
|
552
|
+
|
|
553
|
+
async def arun(
|
|
554
|
+
self,
|
|
555
|
+
run_mode: bool = True,
|
|
556
|
+
stream_variables: bool = True,
|
|
557
|
+
stream_mode: str = "full",
|
|
558
|
+
**kwargs,
|
|
559
|
+
) -> AsyncGenerator[Any, None]:
|
|
560
|
+
"""Run the Agent asynchronously.
|
|
561
|
+
|
|
562
|
+
- The default behavior is consistent with BaseAgent.arun, producing a run status/completion/interrupt structure.
|
|
563
|
+
- When stream_variables=True, execute in fast mode (run_mode=True) to avoid snapshot overhead at each step,
|
|
564
|
+
while streaming current context variables at each block's progress point (filtered as needed if output_variables is set).
|
|
565
|
+
|
|
566
|
+
Important: When stream_variables=True, fast mode is enforced (equivalent to run_mode=True),
|
|
567
|
+
the run_mode passed by the caller will be ignored and only effective in this branch.
|
|
568
|
+
|
|
569
|
+
Args:
|
|
570
|
+
run_mode: Whether to run in fast mode (default: True)
|
|
571
|
+
stream_variables: Whether to enable streaming variable output (default: True)
|
|
572
|
+
stream_mode: Streaming mode (default: "full"):
|
|
573
|
+
- "full": Return full accumulated text
|
|
574
|
+
- "delta": Return only incremental text changes (framework calculates automatically)
|
|
575
|
+
**kwargs: Additional runtime variables
|
|
576
|
+
|
|
577
|
+
Note: Tool interruption handling when stream_variables=True:
|
|
578
|
+
1. During streaming variable views (dict), if a tool interruption occurs, an interruption information dictionary is produced:
|
|
579
|
+
{"status": "interrupted", "handle": ResumeHandle}
|
|
580
|
+
2. The Agent state becomes PAUSED
|
|
581
|
+
3. The caller must handle the tool interruption, then call agent.resume(updates) to resume execution
|
|
582
|
+
4. After resuming, arun(stream_variables=True) can be called again to continue streaming
|
|
583
|
+
5. On normal completion, the final variable snapshot is produced, which may be identical to the penultimate output (ensuring no final state is missed)
|
|
584
|
+
|
|
585
|
+
Queue strategy:
|
|
586
|
+
- Use a bounded queue (maxsize=32) + discard old, keep new to prevent memory bloat (balancing high-frequency updates and memory usage)
|
|
587
|
+
- The caller always receives the latest variable state
|
|
588
|
+
- If execution is too fast, some intermediate states may be skipped (only the latest is retained)
|
|
589
|
+
|
|
590
|
+
Example:
|
|
591
|
+
async for data in agent.arun(stream_variables=True):
|
|
592
|
+
if isinstance(data, dict) and data.get("status") == "interrupted":
|
|
593
|
+
# Handle tool interruption
|
|
594
|
+
handle = data["handle"]
|
|
595
|
+
updates = handle_tool_interrupt(handle)
|
|
596
|
+
await agent.resume(updates)
|
|
597
|
+
# Continue execution
|
|
598
|
+
async for data in agent.arun(stream_variables=True):
|
|
599
|
+
process_variable_data(data)
|
|
600
|
+
else:
|
|
601
|
+
# Normal variable view data
|
|
602
|
+
process_variable_data(data)
|
|
603
|
+
"""
|
|
604
|
+
if not stream_variables:
|
|
605
|
+
# Keep the original arun semantics
|
|
606
|
+
async for item in super().arun(run_mode=run_mode, **kwargs):
|
|
607
|
+
yield item
|
|
608
|
+
return
|
|
609
|
+
|
|
610
|
+
# Validate stream_mode
|
|
611
|
+
if stream_mode not in ("full", "delta"):
|
|
612
|
+
raise DolphinAgentException(
|
|
613
|
+
"INVALID_PARAMETER",
|
|
614
|
+
f"stream_mode must be 'full' or 'delta', got '{stream_mode}'"
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
# Streaming variable pattern: capture execution progress via progress_callback, while using fast mode to avoid frequent snapshots
|
|
618
|
+
if self.executor is None:
|
|
619
|
+
# If not yet initialized, trigger initialization (lazy loading)
|
|
620
|
+
await self.initialize()
|
|
621
|
+
|
|
622
|
+
# Queue for callbacks (bounded, discard old, keep new, to avoid memory bloat)
|
|
623
|
+
# maxsize=32 can buffer high-frequency data for 1-2 seconds, balancing memory usage and data integrity
|
|
624
|
+
queue: asyncio.Queue = asyncio.Queue(maxsize=32)
|
|
625
|
+
# Used to pass tool interrupt information
|
|
626
|
+
interrupt_info = None
|
|
627
|
+
# Track last answer for delta calculation
|
|
628
|
+
last_answer = {} if stream_mode == "delta" else None
|
|
629
|
+
|
|
630
|
+
def _queue_put_latest(payload):
|
|
631
|
+
try:
|
|
632
|
+
queue.put_nowait(payload)
|
|
633
|
+
except asyncio.QueueFull:
|
|
634
|
+
try:
|
|
635
|
+
queue.get_nowait() # Drop old items
|
|
636
|
+
except asyncio.QueueEmpty:
|
|
637
|
+
pass
|
|
638
|
+
try:
|
|
639
|
+
queue.put_nowait(payload)
|
|
640
|
+
except asyncio.QueueFull:
|
|
641
|
+
# Ignore this update in extreme cases to avoid blocking
|
|
642
|
+
pass
|
|
643
|
+
|
|
644
|
+
def _progress_cb(_resp):
|
|
645
|
+
# Self-check: Only deliver when the current callback is still this closure, to avoid incorrect delivery caused by concurrent overwriting.
|
|
646
|
+
if getattr(self, "_progress_callback", None) is not _progress_cb:
|
|
647
|
+
return
|
|
648
|
+
try:
|
|
649
|
+
ctx = self.executor.context if self.executor else None
|
|
650
|
+
if ctx is None:
|
|
651
|
+
return
|
|
652
|
+
if self.output_variables is not None and len(self.output_variables) > 0:
|
|
653
|
+
data = ctx.get_variables_values(self.output_variables)
|
|
654
|
+
else:
|
|
655
|
+
data = ctx.get_all_variables_values()
|
|
656
|
+
|
|
657
|
+
# Apply delta mode if requested
|
|
658
|
+
if stream_mode == "delta" and last_answer is not None:
|
|
659
|
+
data = self._apply_delta_mode(data, last_answer)
|
|
660
|
+
|
|
661
|
+
_queue_put_latest(data)
|
|
662
|
+
except Exception:
|
|
663
|
+
# Callback exceptions must not affect main execution
|
|
664
|
+
pass
|
|
665
|
+
|
|
666
|
+
# Install callback
|
|
667
|
+
setattr(self, "_progress_callback", _progress_cb)
|
|
668
|
+
|
|
669
|
+
async def _drive_base_arun():
|
|
670
|
+
nonlocal interrupt_info
|
|
671
|
+
# Reuse base class state machine/event: Fast mode execution
|
|
672
|
+
try:
|
|
673
|
+
async for item in super(DolphinAgent, self).arun(
|
|
674
|
+
run_mode=True, **kwargs
|
|
675
|
+
):
|
|
676
|
+
# Check if it is an interruption (unified status, type in interrupt_type field)
|
|
677
|
+
if isinstance(item, dict) and item.get("status") == "interrupted":
|
|
678
|
+
interrupt_info = item
|
|
679
|
+
# Do not push to the queue, output uniformly by the outer layer (to avoid duplication)
|
|
680
|
+
return # Stop execution and let the outer layer pass interrupt information
|
|
681
|
+
finally:
|
|
682
|
+
# Clean up callbacks to avoid leakage into subsequent runs
|
|
683
|
+
if hasattr(self, "_progress_callback"):
|
|
684
|
+
delattr(self, "_progress_callback")
|
|
685
|
+
|
|
686
|
+
# Start Execution
|
|
687
|
+
runner = asyncio.create_task(_drive_base_arun())
|
|
688
|
+
|
|
689
|
+
try:
|
|
690
|
+
# Consume the progress queue until execution ends
|
|
691
|
+
while True:
|
|
692
|
+
try:
|
|
693
|
+
# 优先从队列Get数据,使用较短的超时避免runner完成后检测延迟过大
|
|
694
|
+
# Timeout values balance responsiveness and CPU efficiency: even if the item interval > 0.1s, it will still be fetched in the next loop iteration.
|
|
695
|
+
item = await asyncio.wait_for(queue.get(), timeout=0.1)
|
|
696
|
+
yield item
|
|
697
|
+
except asyncio.TimeoutError:
|
|
698
|
+
# Check runner status only after timeout to avoid race conditions
|
|
699
|
+
if runner.done():
|
|
700
|
+
# Clear the remaining data in the queue, using exception handling instead of empty() checks
|
|
701
|
+
while True:
|
|
702
|
+
try:
|
|
703
|
+
yield queue.get_nowait()
|
|
704
|
+
except asyncio.QueueEmpty:
|
|
705
|
+
break
|
|
706
|
+
break
|
|
707
|
+
finally:
|
|
708
|
+
# Ensure backend tasks are completed
|
|
709
|
+
if not runner.done():
|
|
710
|
+
runner.cancel()
|
|
711
|
+
try:
|
|
712
|
+
# Always await, regardless of whether done or not, to re-raise any possible exceptions.
|
|
713
|
+
await runner
|
|
714
|
+
except asyncio.CancelledError:
|
|
715
|
+
# Active cancellation is expected behavior
|
|
716
|
+
pass
|
|
717
|
+
except Exception:
|
|
718
|
+
# Log and re-raise the exception to inform the caller that an error has occurred.
|
|
719
|
+
self._logger.exception(
|
|
720
|
+
"Background execution failed in stream_variables mode"
|
|
721
|
+
)
|
|
722
|
+
raise
|
|
723
|
+
|
|
724
|
+
# At the end:
|
|
725
|
+
if interrupt_info is not None:
|
|
726
|
+
# If there is tool interruption information, ensure the caller definitely obtains it again (even with queue consumption race conditions)
|
|
727
|
+
yield interrupt_info
|
|
728
|
+
else:
|
|
729
|
+
# Complete normally by capturing one final frame of variable snapshots to avoid missing the last state.
|
|
730
|
+
try:
|
|
731
|
+
ctx = self.executor.context if self.executor else None
|
|
732
|
+
if ctx is not None:
|
|
733
|
+
if (
|
|
734
|
+
self.output_variables is not None
|
|
735
|
+
and len(self.output_variables) > 0
|
|
736
|
+
):
|
|
737
|
+
final_data = ctx.get_variables_values(self.output_variables)
|
|
738
|
+
else:
|
|
739
|
+
final_data = ctx.get_all_variables_values()
|
|
740
|
+
|
|
741
|
+
# Apply delta mode to final data
|
|
742
|
+
if stream_mode == "delta" and last_answer is not None:
|
|
743
|
+
final_data = self._apply_delta_mode(final_data, last_answer)
|
|
744
|
+
|
|
745
|
+
yield final_data
|
|
746
|
+
except (AttributeError, KeyError) as e:
|
|
747
|
+
# Expected errors when context/variables are not fully initialized
|
|
748
|
+
self._logger.debug(f"Could not get final variables in arun: {e}")
|
|
749
|
+
|
|
750
|
+
async def _on_pause_coroutine(self):
|
|
751
|
+
"""Pause coroutine"""
|
|
752
|
+
if self.executor is None:
|
|
753
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
754
|
+
|
|
755
|
+
if self._current_frame is None:
|
|
756
|
+
raise DolphinAgentException("NO_FRAME", "No execution frame available")
|
|
757
|
+
|
|
758
|
+
try:
|
|
759
|
+
handle = await self.executor.pause_coroutine(self._current_frame.frame_id)
|
|
760
|
+
return handle
|
|
761
|
+
|
|
762
|
+
except Exception as e:
|
|
763
|
+
self._logger.error(f"Pause coroutine error: {e}")
|
|
764
|
+
raise DolphinAgentException("PAUSE_COROUTINE_FAILED", str(e))
|
|
765
|
+
|
|
766
|
+
async def _on_resume_coroutine(self, updates: Optional[Dict[str, Any]] = None):
|
|
767
|
+
"""Resume coroutine"""
|
|
768
|
+
if self.executor is None:
|
|
769
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
770
|
+
|
|
771
|
+
if self._resume_handle is None:
|
|
772
|
+
raise DolphinAgentException(
|
|
773
|
+
"NO_RESUME_HANDLE", "No resume handle available"
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
try:
|
|
777
|
+
frame = await self.executor.resume_coroutine(self._resume_handle, updates)
|
|
778
|
+
return frame
|
|
779
|
+
|
|
780
|
+
except Exception as e:
|
|
781
|
+
self._logger.error(f"Resume coroutine error: {e}")
|
|
782
|
+
raise DolphinAgentException("RESUME_COROUTINE_FAILED", str(e))
|
|
783
|
+
|
|
784
|
+
async def _on_terminate_coroutine(self):
|
|
785
|
+
"""Terminate coroutine"""
|
|
786
|
+
if self.executor is None:
|
|
787
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
788
|
+
|
|
789
|
+
if self._current_frame is None:
|
|
790
|
+
# No frame executed, return directly
|
|
791
|
+
return
|
|
792
|
+
|
|
793
|
+
try:
|
|
794
|
+
await self.executor.terminate_coroutine(self._current_frame.frame_id)
|
|
795
|
+
|
|
796
|
+
except Exception as e:
|
|
797
|
+
self._logger.error(f"Terminate coroutine error: {e}")
|
|
798
|
+
raise DolphinAgentException("TERMINATE_COROUTINE_FAILED", str(e))
|
|
799
|
+
|
|
800
|
+
def get_content_source(self) -> str:
|
|
801
|
+
"""Get content source type"""
|
|
802
|
+
return getattr(self, "content_source", "unknown")
|
|
803
|
+
|
|
804
|
+
def get_file_path(self) -> Optional[str]:
|
|
805
|
+
"""Get DPH file path"""
|
|
806
|
+
return self.file_path
|
|
807
|
+
|
|
808
|
+
def get_execution_trace(self, title=None):
|
|
809
|
+
"""Get Execution Trace"""
|
|
810
|
+
if self.executor:
|
|
811
|
+
return self.executor.get_execution_trace(title)
|
|
812
|
+
else:
|
|
813
|
+
return "Execution trace not available: Agent not initialized"
|
|
814
|
+
|
|
815
|
+
# Backward compatibility: retain old method names
|
|
816
|
+
def get_profile(self, title=None):
|
|
817
|
+
"""[Deprecated] Please use get_execution_trace() instead"""
|
|
818
|
+
import warnings
|
|
819
|
+
warnings.warn("get_profile() 已废弃,请使用 get_execution_trace()", DeprecationWarning, stacklevel=2)
|
|
820
|
+
return self.get_execution_trace(title)
|
|
821
|
+
|
|
822
|
+
def get_content(self) -> Optional[str]:
|
|
823
|
+
"""Get Agent content"""
|
|
824
|
+
return self.content
|
|
825
|
+
|
|
826
|
+
def get_header_info(self) -> Dict[str, str]:
|
|
827
|
+
"""Get all parsed header information
|
|
828
|
+
|
|
829
|
+
Returns:
|
|
830
|
+
A dictionary containing all header information, for example {'DESC': '...', 'VERSION': '...'}
|
|
831
|
+
"""
|
|
832
|
+
return self.header_info.copy()
|
|
833
|
+
|
|
834
|
+
def get_desc(self) -> str:
|
|
835
|
+
"""Get Agent description (from @DESC header or constructor parameters)
|
|
836
|
+
|
|
837
|
+
Returns:
|
|
838
|
+
Agent description string
|
|
839
|
+
"""
|
|
840
|
+
return self.description or self.header_info.get("DESC", "")
|
|
841
|
+
|
|
842
|
+
async def append_incremental_message(self, payload: dict):
|
|
843
|
+
"""Append an incremental message to the current coroutine (using prefix cache)
|
|
844
|
+
|
|
845
|
+
Args:
|
|
846
|
+
payload: Status or event information, such as {"event_type": "...", "content": "...", "metadata": {...}}
|
|
847
|
+
"""
|
|
848
|
+
if self.executor is None:
|
|
849
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
850
|
+
|
|
851
|
+
await self.executor.append_incremental_message(payload)
|
|
852
|
+
|
|
853
|
+
async def append_round_message(self, round_info: dict):
|
|
854
|
+
"""Compatible with old names: Please use append_incremental_message instead."""
|
|
855
|
+
await self.append_incremental_message(round_info)
|
|
856
|
+
|
|
857
|
+
def set_context(self, context: Context):
|
|
858
|
+
"""Set the Agent's context"""
|
|
859
|
+
if self.executor:
|
|
860
|
+
self.executor.set_context(context)
|
|
861
|
+
|
|
862
|
+
def sync_variables(self, context: Context):
|
|
863
|
+
"""Synchronize variables to the specified context"""
|
|
864
|
+
if self.executor:
|
|
865
|
+
# Synchronize the variables of the current execution context to the target context
|
|
866
|
+
context.sync_variables(self.executor.context)
|
|
867
|
+
|
|
868
|
+
def get_context(self) -> Optional[Context]:
|
|
869
|
+
"""Get the execution context of the Agent"""
|
|
870
|
+
if self.executor is None:
|
|
871
|
+
return None
|
|
872
|
+
return self.executor.context
|
|
873
|
+
|
|
874
|
+
def set_user_id(self, user_id: str):
|
|
875
|
+
"""Set user ID"""
|
|
876
|
+
if self.executor is None:
|
|
877
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
878
|
+
self.executor.context.set_user_id(user_id)
|
|
879
|
+
|
|
880
|
+
def set_session_id(self, session_id: str):
|
|
881
|
+
"""Set session ID"""
|
|
882
|
+
if self.executor is None:
|
|
883
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
884
|
+
self.executor.context.set_session_id(session_id)
|
|
885
|
+
|
|
886
|
+
def add_user_message(self, content: str, bucket: str = None):
|
|
887
|
+
"""Add user message"""
|
|
888
|
+
if self.executor is None:
|
|
889
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
890
|
+
self.executor.context.add_user_message(content, bucket=bucket)
|
|
891
|
+
|
|
892
|
+
def get_messages(self):
|
|
893
|
+
"""Get message list"""
|
|
894
|
+
if self.executor is None:
|
|
895
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
896
|
+
return self.executor.context.get_messages()
|
|
897
|
+
|
|
898
|
+
def get_skillkit(self, skillNames: Optional[list] = None):
|
|
899
|
+
"""Get Skill Package"""
|
|
900
|
+
if self.executor is None:
|
|
901
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
902
|
+
return self.executor.context.get_skillkit(skillNames)
|
|
903
|
+
|
|
904
|
+
def get_config(self):
|
|
905
|
+
"""Get configuration"""
|
|
906
|
+
if self.executor is None:
|
|
907
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
908
|
+
return self.executor.context.get_config()
|
|
909
|
+
|
|
910
|
+
def save_trajectory(self, agent_name: str = None, force_save: bool = False, trajectory_path: str = None):
|
|
911
|
+
"""Save execution trace"""
|
|
912
|
+
if self.executor is None:
|
|
913
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
914
|
+
self.executor.context.save_trajectory(
|
|
915
|
+
agent_name=agent_name or self.name,
|
|
916
|
+
force_save=force_save,
|
|
917
|
+
trajectory_path=trajectory_path
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
def get_snapshot_analysis(self, title=None, format='markdown', options=None):
|
|
921
|
+
"""Get snapshot analysis"""
|
|
922
|
+
if self.executor is None:
|
|
923
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
924
|
+
return self.executor.context.get_snapshot_analysis(title=title, format=format, options=options)
|
|
925
|
+
|
|
926
|
+
def get_all_variables(self):
|
|
927
|
+
"""Get all variables"""
|
|
928
|
+
if self.executor is None:
|
|
929
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
930
|
+
return self.executor.context.get_all_variables()
|
|
931
|
+
|
|
932
|
+
def get_var_value(self, name, default_value=None):
|
|
933
|
+
"""Get variable value"""
|
|
934
|
+
if self.executor is None:
|
|
935
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
936
|
+
return self.executor.context.get_var_value(name, default_value)
|
|
937
|
+
|
|
938
|
+
def add_bucket(self, bucket_name: str, content, priority: float = 1.0, allocated_tokens: Optional[int] = None, message_role=None):
|
|
939
|
+
"""Add context buckets"""
|
|
940
|
+
if self.executor is None:
|
|
941
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
942
|
+
self.executor.context.add_bucket(
|
|
943
|
+
bucket_name=bucket_name,
|
|
944
|
+
content=content,
|
|
945
|
+
priority=priority,
|
|
946
|
+
allocated_tokens=allocated_tokens,
|
|
947
|
+
message_role=message_role
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
def set_trajectorypath(self, trajectorypath: str):
|
|
951
|
+
"""Set trajectory path"""
|
|
952
|
+
if self.executor is None:
|
|
953
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
954
|
+
self.executor.context.trajectorypath = trajectorypath
|
|
955
|
+
|
|
956
|
+
def set_agent_name(self, agent_name: str):
|
|
957
|
+
"""Set agent name (in context)"""
|
|
958
|
+
if self.executor is None:
|
|
959
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
960
|
+
self.executor.context.agent_name = agent_name
|
|
961
|
+
|
|
962
|
+
def set_cur_agent(self, agent):
|
|
963
|
+
"""Set the current agent (in context)"""
|
|
964
|
+
if self.executor is None:
|
|
965
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
966
|
+
self.executor.context.set_cur_agent(agent)
|
|
967
|
+
|
|
968
|
+
def set_skills(self, skillkit):
|
|
969
|
+
"""Set Skill Pack"""
|
|
970
|
+
if self.executor is None:
|
|
971
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
972
|
+
self.executor.context.set_skills(skillkit)
|
|
973
|
+
|
|
974
|
+
def get_skillkit_raw(self):
|
|
975
|
+
"""Get the original skillkit object (for scenarios where direct access to skillkit is required)"""
|
|
976
|
+
if self.executor is None:
|
|
977
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
978
|
+
return self.executor.context.skillkit
|
|
979
|
+
|
|
980
|
+
def get_context_messages_dict(self):
|
|
981
|
+
"""Get the messages dictionary of context (for debugging)"""
|
|
982
|
+
if self.executor is None:
|
|
983
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
984
|
+
return self.executor.context.messages
|
|
985
|
+
|
|
986
|
+
def get_context_manager(self):
|
|
987
|
+
"""Get context_manager (for debugging)"""
|
|
988
|
+
if self.executor is None:
|
|
989
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
990
|
+
return self.executor.context.context_manager
|
|
991
|
+
|
|
992
|
+
async def start_coroutine(self, **kwargs):
|
|
993
|
+
"""Public method: explicitly start coroutine execution
|
|
994
|
+
|
|
995
|
+
Used in scenarios where manual control over coroutine startup is required
|
|
996
|
+
"""
|
|
997
|
+
# Keep consistent with the state machine of BaseAgent
|
|
998
|
+
if self.state == AgentState.CREATED:
|
|
999
|
+
await self.initialize()
|
|
1000
|
+
|
|
1001
|
+
# If the previous one has been completed, clean up and return to INITIALIZED
|
|
1002
|
+
if self.state == AgentState.COMPLETED:
|
|
1003
|
+
try:
|
|
1004
|
+
if self._current_frame is not None:
|
|
1005
|
+
await self._on_terminate_coroutine()
|
|
1006
|
+
except Exception:
|
|
1007
|
+
pass
|
|
1008
|
+
self._current_frame = None
|
|
1009
|
+
self._resume_handle = None
|
|
1010
|
+
self._pause_type = None
|
|
1011
|
+
await self._change_state(
|
|
1012
|
+
AgentState.INITIALIZED, "Agent reinitialized for new run"
|
|
1013
|
+
)
|
|
1014
|
+
|
|
1015
|
+
# Explicit start is only allowed in the INITIALIZED state; explicit start in RUNNING/PAUSED states would create ambiguity, hence rejected.
|
|
1016
|
+
if self.state != AgentState.INITIALIZED:
|
|
1017
|
+
raise DolphinAgentException(
|
|
1018
|
+
"INVALID_STATE",
|
|
1019
|
+
f"start_coroutine() not allowed from state {self.state.value}",
|
|
1020
|
+
)
|
|
1021
|
+
|
|
1022
|
+
# Clean up control signals and switch to RUNNING
|
|
1023
|
+
self._terminate_event.clear()
|
|
1024
|
+
self._pause_event.set()
|
|
1025
|
+
await self._change_state(
|
|
1026
|
+
AgentState.RUNNING, "Agent started via start_coroutine()"
|
|
1027
|
+
)
|
|
1028
|
+
|
|
1029
|
+
# Start Execution Frame
|
|
1030
|
+
self._current_frame = await self._on_start_coroutine(**kwargs)
|
|
1031
|
+
return self._current_frame
|
|
1032
|
+
|
|
1033
|
+
async def resume_coroutine(self, handle, updates=None):
|
|
1034
|
+
"""Resume coroutine execution (resume from tool interruption)
|
|
1035
|
+
|
|
1036
|
+
Args:
|
|
1037
|
+
handle: ResumeHandle resume handle
|
|
1038
|
+
updates: variable updates to inject
|
|
1039
|
+
|
|
1040
|
+
Returns:
|
|
1041
|
+
ExecutionFrame: the execution frame after resumption
|
|
1042
|
+
"""
|
|
1043
|
+
if self.executor is None:
|
|
1044
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
1045
|
+
|
|
1046
|
+
# Explicit resume API is only allowed when in PAUSED state
|
|
1047
|
+
if self.state != AgentState.PAUSED:
|
|
1048
|
+
raise DolphinAgentException(
|
|
1049
|
+
"INVALID_STATE",
|
|
1050
|
+
f"resume_coroutine() not allowed from state {self.state.value}",
|
|
1051
|
+
)
|
|
1052
|
+
|
|
1053
|
+
frame = await self.executor.resume_coroutine(handle, updates)
|
|
1054
|
+
self._current_frame = frame
|
|
1055
|
+
self._resume_handle = None
|
|
1056
|
+
|
|
1057
|
+
# Keep the same behavior as BaseAgent.resume(): resume the pause, switch to RUNNING, and trigger on_resume
|
|
1058
|
+
self._pause_event.set()
|
|
1059
|
+
await self._change_state(
|
|
1060
|
+
AgentState.RUNNING, "Agent resumed via resume_coroutine()"
|
|
1061
|
+
)
|
|
1062
|
+
await self._on_resume()
|
|
1063
|
+
return frame
|
|
1064
|
+
|
|
1065
|
+
def get_intervention_data(self) -> dict:
|
|
1066
|
+
"""Get interrupt data (such as tool call parameters)
|
|
1067
|
+
|
|
1068
|
+
Returns:
|
|
1069
|
+
dict: Interrupt-related data
|
|
1070
|
+
"""
|
|
1071
|
+
if self.executor is None:
|
|
1072
|
+
raise DolphinAgentException("NOT_INITIALIZED", "Agent not initialized")
|
|
1073
|
+
|
|
1074
|
+
return self.executor.get_intervention_data()
|
|
1075
|
+
|
|
1076
|
+
def get_execution_info(self) -> Dict[str, Any]:
|
|
1077
|
+
"""Get execution information"""
|
|
1078
|
+
info = {
|
|
1079
|
+
"name": self.name,
|
|
1080
|
+
"description": self.description,
|
|
1081
|
+
"state": self.state.value,
|
|
1082
|
+
"execution_mode": "coroutine", # Explicitly identified as coroutine mode
|
|
1083
|
+
"content_source": self.get_content_source(),
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
if self._current_frame is not None:
|
|
1087
|
+
info["frame_id"] = self._current_frame.frame_id
|
|
1088
|
+
info["frame_status"] = self._current_frame.status.value
|
|
1089
|
+
info["block_pointer"] = self._current_frame.block_pointer
|
|
1090
|
+
|
|
1091
|
+
if self._resume_handle is not None:
|
|
1092
|
+
info["has_resume_handle"] = True
|
|
1093
|
+
|
|
1094
|
+
return info
|
|
1095
|
+
|
|
1096
|
+
def __str__(self) -> str:
|
|
1097
|
+
if self.get_content_source() == "file":
|
|
1098
|
+
return f"DolphinAgent(name={self.name}, file={self.file_path}, state={self.state.value})"
|
|
1099
|
+
else:
|
|
1100
|
+
if self.content is None:
|
|
1101
|
+
return f"DolphinAgent(name={self.name}, state={self.state.value})"
|
|
1102
|
+
|
|
1103
|
+
content_preview = (
|
|
1104
|
+
self.content[:50] + "..." if len(self.content) > 50 else self.content
|
|
1105
|
+
)
|
|
1106
|
+
return f"DolphinAgent(name={self.name}, content_source={self.get_content_source()}, content_preview='{content_preview}', state={self.state.value})"
|