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,1173 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import uuid
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, Dict, List, Literal, Optional, Union
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from dolphin.core.common.types import Var, SourceType
|
|
10
|
+
from dolphin.core.common.constants import (
|
|
11
|
+
estimate_tokens_from_chars,
|
|
12
|
+
MAX_ANSWER_COMPRESSION_LENGTH,
|
|
13
|
+
ANSWER_CONTENT_PREFIX,
|
|
14
|
+
ANSWER_CONTENT_SUFFIX,
|
|
15
|
+
TOOL_CALL_ID_PREFIX,
|
|
16
|
+
)
|
|
17
|
+
from dolphin.core import flags
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# Type alias for message content - can be plain text or multimodal (OpenAI format)
|
|
22
|
+
MessageContent = Union[str, List[Dict[str, Any]]]
|
|
23
|
+
|
|
24
|
+
PlainMessages = List[Dict[str, Any]]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MessageRole(Enum):
|
|
28
|
+
"""Enumeration type, defines message roles"""
|
|
29
|
+
|
|
30
|
+
USER = "user"
|
|
31
|
+
ASSISTANT = "assistant"
|
|
32
|
+
SYSTEM = "system"
|
|
33
|
+
TOOL = "tool"
|
|
34
|
+
|
|
35
|
+
def __str__(self):
|
|
36
|
+
"""Return the string value of the enumeration"""
|
|
37
|
+
return self.value
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CompressLevel(Enum):
|
|
41
|
+
NONE = "none"
|
|
42
|
+
NORMAL = "normal"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SingleMessage:
|
|
46
|
+
"""Single message in a conversation.
|
|
47
|
+
|
|
48
|
+
Supports both plain text content (str) and multimodal content (List[Dict]).
|
|
49
|
+
Multimodal content follows the OpenAI format:
|
|
50
|
+
[
|
|
51
|
+
{"type": "text", "text": "..."},
|
|
52
|
+
{"type": "image_url", "image_url": {"url": "...", "detail": "auto"}}
|
|
53
|
+
]
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
role: MessageRole,
|
|
59
|
+
content: MessageContent, # Union[str, List[Dict]]
|
|
60
|
+
timestamp: str = datetime.now().isoformat(),
|
|
61
|
+
user_id: str = "",
|
|
62
|
+
tool_calls: Optional[List[Dict[str, Any]]] = None,
|
|
63
|
+
tool_call_id: Optional[str] = None,
|
|
64
|
+
compress_level: CompressLevel = CompressLevel.NONE,
|
|
65
|
+
metadata: Dict[str, Any] = {},
|
|
66
|
+
):
|
|
67
|
+
self.role = role
|
|
68
|
+
self.content = content
|
|
69
|
+
self.timestamp = timestamp
|
|
70
|
+
self.user_id = user_id
|
|
71
|
+
self.tool_calls = tool_calls or []
|
|
72
|
+
self.tool_call_id = tool_call_id
|
|
73
|
+
self.compress_level = compress_level
|
|
74
|
+
self.metadata = metadata
|
|
75
|
+
|
|
76
|
+
def is_multimodal(self) -> bool:
|
|
77
|
+
"""Check if this message contains multimodal content."""
|
|
78
|
+
return isinstance(self.content, list)
|
|
79
|
+
|
|
80
|
+
def has_images(self) -> bool:
|
|
81
|
+
"""Check if this message contains any images."""
|
|
82
|
+
if not self.is_multimodal():
|
|
83
|
+
return False
|
|
84
|
+
return any(
|
|
85
|
+
block.get("type") == "image_url"
|
|
86
|
+
for block in self.content
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def get_image_count(self) -> int:
|
|
90
|
+
"""Get the number of images in this message."""
|
|
91
|
+
if not self.is_multimodal():
|
|
92
|
+
return 0
|
|
93
|
+
return sum(
|
|
94
|
+
1 for block in self.content
|
|
95
|
+
if block.get("type") == "image_url"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def extract_text(self) -> str:
|
|
99
|
+
"""Extract plain text from this message (excluding images)."""
|
|
100
|
+
if isinstance(self.content, str):
|
|
101
|
+
return self.content
|
|
102
|
+
return "".join(
|
|
103
|
+
block.get("text", "")
|
|
104
|
+
for block in self.content
|
|
105
|
+
if block.get("type") == "text"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def normalize_content(self) -> List[Dict[str, Any]]:
|
|
109
|
+
"""Normalize content to List[Dict] format."""
|
|
110
|
+
if isinstance(self.content, str):
|
|
111
|
+
return [{"type": "text", "text": self.content}]
|
|
112
|
+
return self.content
|
|
113
|
+
|
|
114
|
+
def append_content(self, new_content: MessageContent):
|
|
115
|
+
"""Append content to this message.
|
|
116
|
+
|
|
117
|
+
Append rules:
|
|
118
|
+
- str + str → str (simple concatenation)
|
|
119
|
+
- str + list → list (type upgrade)
|
|
120
|
+
- list + str → list (append text block)
|
|
121
|
+
- list + list → list (merge)
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
new_content: Content to append
|
|
125
|
+
"""
|
|
126
|
+
current = self.content
|
|
127
|
+
|
|
128
|
+
# Case 1: str + str → str
|
|
129
|
+
if isinstance(current, str) and isinstance(new_content, str):
|
|
130
|
+
self.content = current + new_content
|
|
131
|
+
|
|
132
|
+
# Case 2: str + list → list (type upgrade)
|
|
133
|
+
elif isinstance(current, str) and isinstance(new_content, list):
|
|
134
|
+
self.content = [{"type": "text", "text": current}] + new_content
|
|
135
|
+
|
|
136
|
+
# Case 3: list + str → list (append text block)
|
|
137
|
+
elif isinstance(current, list) and isinstance(new_content, str):
|
|
138
|
+
self.content = current + [{"type": "text", "text": new_content}]
|
|
139
|
+
|
|
140
|
+
# Case 4: list + list → list (merge)
|
|
141
|
+
elif isinstance(current, list) and isinstance(new_content, list):
|
|
142
|
+
self.content = current + new_content
|
|
143
|
+
|
|
144
|
+
def compress(self, compress_level: CompressLevel):
|
|
145
|
+
if compress_level == CompressLevel.NONE:
|
|
146
|
+
return
|
|
147
|
+
elif compress_level == CompressLevel.NORMAL:
|
|
148
|
+
self._compress_normal()
|
|
149
|
+
else:
|
|
150
|
+
raise ValueError(f"Invalid compress level: {compress_level}")
|
|
151
|
+
|
|
152
|
+
def to_dict(self):
|
|
153
|
+
# Handle None metadata defensively (should be {} but can be None from some code paths)
|
|
154
|
+
metadata = self.metadata if self.metadata is not None else {}
|
|
155
|
+
result = {"role": self.role.value, "content": self.content, **metadata}
|
|
156
|
+
if self.tool_calls:
|
|
157
|
+
result["tool_calls"] = self.tool_calls
|
|
158
|
+
if self.tool_call_id:
|
|
159
|
+
result["tool_call_id"] = self.tool_call_id
|
|
160
|
+
return result
|
|
161
|
+
|
|
162
|
+
def length(self) -> int:
|
|
163
|
+
"""Get the text length of this message (excluding images)."""
|
|
164
|
+
if isinstance(self.content, str):
|
|
165
|
+
return len(self.content)
|
|
166
|
+
# For multimodal, only count text content
|
|
167
|
+
return sum(
|
|
168
|
+
len(block.get("text", ""))
|
|
169
|
+
for block in self.content
|
|
170
|
+
if block.get("type") == "text"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def copy(self):
|
|
174
|
+
"""Create a deep copy of this message."""
|
|
175
|
+
# Deep copy content if it's a list
|
|
176
|
+
if isinstance(self.content, list):
|
|
177
|
+
content_copy = [
|
|
178
|
+
{**block} for block in self.content
|
|
179
|
+
]
|
|
180
|
+
else:
|
|
181
|
+
content_copy = self.content
|
|
182
|
+
|
|
183
|
+
return SingleMessage(
|
|
184
|
+
role=self.role,
|
|
185
|
+
content=content_copy,
|
|
186
|
+
timestamp=self.timestamp,
|
|
187
|
+
user_id=self.user_id,
|
|
188
|
+
metadata=self.metadata.copy() if self.metadata else {},
|
|
189
|
+
compress_level=self.compress_level,
|
|
190
|
+
tool_calls=self.tool_calls.copy() if self.tool_calls else None,
|
|
191
|
+
tool_call_id=self.tool_call_id,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def _compress_normal(self):
|
|
195
|
+
"""Apply normal compression to the message content.
|
|
196
|
+
|
|
197
|
+
Note: Compression is only applied to text content.
|
|
198
|
+
For multimodal messages, only text blocks are compressed.
|
|
199
|
+
"""
|
|
200
|
+
if self.compress_level == CompressLevel.NORMAL:
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
if isinstance(self.content, str):
|
|
204
|
+
self.content = self._compress_answer(self.content)
|
|
205
|
+
self.content = self._compress_cognitive(self.content)
|
|
206
|
+
else:
|
|
207
|
+
# For multimodal, compress each text block
|
|
208
|
+
for block in self.content:
|
|
209
|
+
if block.get("type") == "text" and "text" in block:
|
|
210
|
+
block["text"] = self._compress_answer(block["text"])
|
|
211
|
+
block["text"] = self._compress_cognitive(block["text"])
|
|
212
|
+
|
|
213
|
+
self.compress_level = CompressLevel.NORMAL
|
|
214
|
+
|
|
215
|
+
@staticmethod
|
|
216
|
+
def _compress_answer(content: str) -> str:
|
|
217
|
+
"""
|
|
218
|
+
Compress <answer>XXX</answer> content to maximum length, add '...' if truncated
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
content (str): The message content to compress
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
str: Compressed content
|
|
225
|
+
"""
|
|
226
|
+
import re
|
|
227
|
+
|
|
228
|
+
# Find all <answer>...</answer> blocks
|
|
229
|
+
answer_pattern = re.compile(
|
|
230
|
+
rf"{re.escape(ANSWER_CONTENT_PREFIX)}(.*?){re.escape(ANSWER_CONTENT_SUFFIX)}",
|
|
231
|
+
re.DOTALL,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def compress_answer_block(match):
|
|
235
|
+
answer_content = match.group(1)
|
|
236
|
+
if len(answer_content) <= MAX_ANSWER_COMPRESSION_LENGTH:
|
|
237
|
+
return match.group(0) # No compression needed
|
|
238
|
+
|
|
239
|
+
# Truncate and add ellipsis
|
|
240
|
+
compressed_content = answer_content[:MAX_ANSWER_COMPRESSION_LENGTH] + "..."
|
|
241
|
+
return f"{ANSWER_CONTENT_PREFIX}{compressed_content}{ANSWER_CONTENT_SUFFIX}"
|
|
242
|
+
|
|
243
|
+
return answer_pattern.sub(compress_answer_block, content)
|
|
244
|
+
|
|
245
|
+
@staticmethod
|
|
246
|
+
def _compress_cognitive(content: str) -> str:
|
|
247
|
+
"""
|
|
248
|
+
Compress cognitive skill call messages using CognitiveSkillkit.compress_msg
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
content (str): The message content to compress
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
str: Compressed content
|
|
255
|
+
"""
|
|
256
|
+
# Import here to avoid circular import
|
|
257
|
+
from dolphin.lib.skillkits.cognitive_skillkit import (
|
|
258
|
+
CognitiveSkillkit,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return CognitiveSkillkit.compress_msg(content)
|
|
262
|
+
|
|
263
|
+
def has_tool_calls(self) -> bool:
|
|
264
|
+
"""Check if this message has tool calls"""
|
|
265
|
+
return bool(self.tool_calls)
|
|
266
|
+
|
|
267
|
+
def get_tool_calls(self) -> List[Dict[str, Any]]:
|
|
268
|
+
"""Get tool calls"""
|
|
269
|
+
return self.tool_calls or []
|
|
270
|
+
|
|
271
|
+
def add_tool_call(self, tool_call: Dict[str, Any]):
|
|
272
|
+
"""Add a tool call to this message"""
|
|
273
|
+
if not self.tool_calls:
|
|
274
|
+
self.tool_calls = []
|
|
275
|
+
self.tool_calls.append(tool_call)
|
|
276
|
+
|
|
277
|
+
def set_tool_call_id(self, tool_call_id: str):
|
|
278
|
+
"""Set tool call ID for this message"""
|
|
279
|
+
self.tool_call_id = tool_call_id
|
|
280
|
+
|
|
281
|
+
def get_content_preview(self) -> Dict[str, Any]:
|
|
282
|
+
"""Get a preview of content for logging (avoiding sensitive data)."""
|
|
283
|
+
if isinstance(self.content, str):
|
|
284
|
+
return {"type": "text", "length": len(self.content)}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
"type": "multimodal",
|
|
288
|
+
"text_length": self.length(),
|
|
289
|
+
"image_count": self.get_image_count()
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
def str_preview(self, max_chars: int = 20) -> str:
|
|
293
|
+
"""Generate a compact preview of this message for debugging.
|
|
294
|
+
|
|
295
|
+
Shows role and content preview (head...tail if long, full content if short).
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
max_chars: Maximum characters to show from each end of content.
|
|
299
|
+
If content length <= max_chars * 2, show full content.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
A compact string like '[USER: Hello, how a...your help | 156c]'
|
|
303
|
+
"""
|
|
304
|
+
# Get text content
|
|
305
|
+
if isinstance(self.content, list):
|
|
306
|
+
# Multimodal: extract text content
|
|
307
|
+
text_parts = [
|
|
308
|
+
block.get("text", "")
|
|
309
|
+
for block in self.content
|
|
310
|
+
if block.get("type") == "text"
|
|
311
|
+
]
|
|
312
|
+
text = " ".join(text_parts)
|
|
313
|
+
image_count = self.get_image_count()
|
|
314
|
+
suffix = f"+{image_count}img" if image_count > 0 else ""
|
|
315
|
+
else:
|
|
316
|
+
text = self.content
|
|
317
|
+
suffix = ""
|
|
318
|
+
|
|
319
|
+
# Clean up whitespace for display
|
|
320
|
+
text = text.replace("\n", "↵").replace("\t", "→")
|
|
321
|
+
text_len = len(text)
|
|
322
|
+
|
|
323
|
+
# Generate content preview
|
|
324
|
+
if text_len <= max_chars * 2:
|
|
325
|
+
# Short content: show full
|
|
326
|
+
content_preview = text
|
|
327
|
+
else:
|
|
328
|
+
# Long content: show head...tail
|
|
329
|
+
head = text[:max_chars]
|
|
330
|
+
tail = text[-max_chars:]
|
|
331
|
+
content_preview = f"{head}...{tail}"
|
|
332
|
+
|
|
333
|
+
# Role abbreviation
|
|
334
|
+
role_abbr = {
|
|
335
|
+
MessageRole.SYSTEM: "SYS",
|
|
336
|
+
MessageRole.USER: "USR",
|
|
337
|
+
MessageRole.ASSISTANT: "AST",
|
|
338
|
+
MessageRole.TOOL: "TOL",
|
|
339
|
+
}.get(self.role, self.role.value[:3].upper())
|
|
340
|
+
|
|
341
|
+
return f"[{role_abbr}: {content_preview}{suffix} | {text_len}c]"
|
|
342
|
+
|
|
343
|
+
def __str__(self):
|
|
344
|
+
# For multimodal, show a summary instead of full content
|
|
345
|
+
if isinstance(self.content, list):
|
|
346
|
+
preview = self.get_content_preview()
|
|
347
|
+
content_str = f"[Multimodal: {preview['text_length']} chars, {preview['image_count']} images]"
|
|
348
|
+
else:
|
|
349
|
+
content_str = self.content
|
|
350
|
+
return f"<<{self.role.value}>> {content_str} tool_calls[{self.tool_calls}] tool_call_id[{self.tool_call_id}]"
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class KnowledgePoint:
|
|
354
|
+
def __init__(
|
|
355
|
+
self,
|
|
356
|
+
content: str,
|
|
357
|
+
score: int,
|
|
358
|
+
user_id: str,
|
|
359
|
+
type: Literal[
|
|
360
|
+
"WorldModel", "ExperientialKnowledge", "OtherKnowledge"
|
|
361
|
+
] = "OtherKnowledge",
|
|
362
|
+
metadata: Dict[str, Any] = {},
|
|
363
|
+
):
|
|
364
|
+
self.content = content
|
|
365
|
+
self.type = type
|
|
366
|
+
self.score = score
|
|
367
|
+
self.user_id = user_id
|
|
368
|
+
self.metadata = metadata
|
|
369
|
+
|
|
370
|
+
def to_dict(self):
|
|
371
|
+
return {
|
|
372
|
+
"content": self.content,
|
|
373
|
+
"type": self.type,
|
|
374
|
+
"score": self.score,
|
|
375
|
+
"user_id": self.user_id,
|
|
376
|
+
"metadata": self.metadata,
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
def from_dict(self, data: dict):
|
|
380
|
+
return KnowledgePoint(
|
|
381
|
+
content=data["content"],
|
|
382
|
+
score=data["score"],
|
|
383
|
+
user_id=data["user_id"],
|
|
384
|
+
type=data["type"],
|
|
385
|
+
metadata=data["metadata"] if "metadata" in data else {},
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
@staticmethod
|
|
389
|
+
def to_prompt(knowledge_points: List["KnowledgePoint"]) -> str:
|
|
390
|
+
return "\n".join([f"{kp.content}" for kp in knowledge_points])
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class Messages:
|
|
394
|
+
def __init__(self):
|
|
395
|
+
self.messages: List[SingleMessage] = []
|
|
396
|
+
self.max_tokens = -1
|
|
397
|
+
|
|
398
|
+
def set_max_tokens(self, max_tokens: int):
|
|
399
|
+
self.max_tokens = max_tokens
|
|
400
|
+
|
|
401
|
+
def get_max_tokens(self) -> int:
|
|
402
|
+
return self.max_tokens
|
|
403
|
+
|
|
404
|
+
def clear_messages(self):
|
|
405
|
+
"""Clear all messages"""
|
|
406
|
+
self.messages = []
|
|
407
|
+
|
|
408
|
+
def insert_messages(self, messages: "Messages"):
|
|
409
|
+
"""Insert messages at the beginning of the message list"""
|
|
410
|
+
converted_messages = messages.get_messages()
|
|
411
|
+
self.messages = converted_messages + self.messages
|
|
412
|
+
|
|
413
|
+
def add_message(
|
|
414
|
+
self,
|
|
415
|
+
content: Any,
|
|
416
|
+
role: MessageRole = MessageRole.USER,
|
|
417
|
+
user_id: str = "",
|
|
418
|
+
metadata: Dict[str, Any] = {},
|
|
419
|
+
tool_calls: Optional[List[Dict[str, Any]]] = None,
|
|
420
|
+
tool_call_id: Optional[str] = None,
|
|
421
|
+
):
|
|
422
|
+
"""Add a message to the conversation.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
content: Message content - can be:
|
|
426
|
+
- str: Plain text content
|
|
427
|
+
- List[Dict]: Multimodal content (OpenAI format)
|
|
428
|
+
- SingleMessage: Existing message object
|
|
429
|
+
- dict: Will be converted to str (for backward compatibility)
|
|
430
|
+
role: Message role (default: USER)
|
|
431
|
+
user_id: User identifier
|
|
432
|
+
metadata: Additional metadata
|
|
433
|
+
tool_calls: Tool call information
|
|
434
|
+
tool_call_id: Tool call ID for tool response messages
|
|
435
|
+
"""
|
|
436
|
+
assert content is not None, "content is required"
|
|
437
|
+
|
|
438
|
+
if isinstance(content, str):
|
|
439
|
+
message: SingleMessage = SingleMessage(
|
|
440
|
+
role=role,
|
|
441
|
+
content=content,
|
|
442
|
+
timestamp=datetime.now().isoformat(),
|
|
443
|
+
user_id=user_id,
|
|
444
|
+
metadata=metadata,
|
|
445
|
+
tool_calls=tool_calls,
|
|
446
|
+
tool_call_id=tool_call_id,
|
|
447
|
+
)
|
|
448
|
+
elif isinstance(content, list):
|
|
449
|
+
# Multimodal content (List[Dict])
|
|
450
|
+
message: SingleMessage = SingleMessage(
|
|
451
|
+
role=role,
|
|
452
|
+
content=content,
|
|
453
|
+
timestamp=datetime.now().isoformat(),
|
|
454
|
+
user_id=user_id,
|
|
455
|
+
metadata=metadata,
|
|
456
|
+
tool_calls=tool_calls,
|
|
457
|
+
tool_call_id=tool_call_id,
|
|
458
|
+
)
|
|
459
|
+
elif isinstance(content, SingleMessage):
|
|
460
|
+
message: SingleMessage = content
|
|
461
|
+
elif isinstance(content, dict):
|
|
462
|
+
message: SingleMessage = SingleMessage(
|
|
463
|
+
role=role,
|
|
464
|
+
content=str(content),
|
|
465
|
+
timestamp=datetime.now().isoformat(),
|
|
466
|
+
user_id=user_id,
|
|
467
|
+
metadata=metadata,
|
|
468
|
+
tool_calls=tool_calls,
|
|
469
|
+
tool_call_id=tool_call_id,
|
|
470
|
+
)
|
|
471
|
+
else:
|
|
472
|
+
raise ValueError(f"Invalid message content type: {type(content)}")
|
|
473
|
+
self.messages.append(message)
|
|
474
|
+
|
|
475
|
+
def insert_message(
|
|
476
|
+
self,
|
|
477
|
+
role: MessageRole,
|
|
478
|
+
content: Any,
|
|
479
|
+
user_id: str = "",
|
|
480
|
+
metadata: Dict[str, Any] = {},
|
|
481
|
+
tool_calls: Optional[List[Dict[str, Any]]] = None,
|
|
482
|
+
tool_call_id: Optional[str] = None,
|
|
483
|
+
):
|
|
484
|
+
"""Insert a message at the beginning of the conversation."""
|
|
485
|
+
assert content is not None, "content is required"
|
|
486
|
+
|
|
487
|
+
if isinstance(content, str) or isinstance(content, list):
|
|
488
|
+
# Both str and List[Dict] are valid content types
|
|
489
|
+
message: SingleMessage = SingleMessage(
|
|
490
|
+
role=role,
|
|
491
|
+
content=content,
|
|
492
|
+
timestamp=datetime.now().isoformat(),
|
|
493
|
+
user_id=user_id,
|
|
494
|
+
metadata=metadata,
|
|
495
|
+
tool_calls=tool_calls,
|
|
496
|
+
tool_call_id=tool_call_id,
|
|
497
|
+
)
|
|
498
|
+
elif isinstance(content, SingleMessage):
|
|
499
|
+
message = content
|
|
500
|
+
else:
|
|
501
|
+
raise ValueError(f"Invalid message content type: {type(content)}")
|
|
502
|
+
self.messages.insert(0, message)
|
|
503
|
+
|
|
504
|
+
def prepend_message(
|
|
505
|
+
self,
|
|
506
|
+
role: MessageRole,
|
|
507
|
+
content: Any,
|
|
508
|
+
user_id: str = "",
|
|
509
|
+
metadata: Dict[str, Any] = {},
|
|
510
|
+
tool_calls: Optional[List[Dict[str, Any]]] = None,
|
|
511
|
+
tool_call_id: Optional[str] = None,
|
|
512
|
+
):
|
|
513
|
+
"""Prepend content to the first message if same role, otherwise insert new message."""
|
|
514
|
+
assert content is not None, "content is required"
|
|
515
|
+
|
|
516
|
+
if isinstance(content, SingleMessage):
|
|
517
|
+
content = content.content
|
|
518
|
+
|
|
519
|
+
if len(self.messages) > 0 and self.messages[0].role == role:
|
|
520
|
+
# Use append_content to handle multimodal merging properly
|
|
521
|
+
first_msg = self.messages[0]
|
|
522
|
+
if isinstance(content, str) and isinstance(first_msg.content, str):
|
|
523
|
+
# Both are strings - prepend with newline
|
|
524
|
+
first_msg.content = content + "\n" + first_msg.content
|
|
525
|
+
elif isinstance(content, list) or isinstance(first_msg.content, list):
|
|
526
|
+
# At least one is multimodal - normalize and merge
|
|
527
|
+
new_normalized = content if isinstance(content, list) else [{"type": "text", "text": content}]
|
|
528
|
+
old_normalized = first_msg.normalize_content()
|
|
529
|
+
first_msg.content = new_normalized + old_normalized
|
|
530
|
+
else:
|
|
531
|
+
first_msg.content = content + "\n" + first_msg.content
|
|
532
|
+
else:
|
|
533
|
+
self.insert_message(
|
|
534
|
+
role, content, user_id, metadata, tool_calls, tool_call_id
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
def append_message(
|
|
538
|
+
self,
|
|
539
|
+
role: MessageRole,
|
|
540
|
+
content: Any,
|
|
541
|
+
user_id: str = "",
|
|
542
|
+
metadata: Dict[str, Any] = {},
|
|
543
|
+
tool_calls: Optional[List[Dict[str, Any]]] = None,
|
|
544
|
+
tool_call_id: Optional[str] = None,
|
|
545
|
+
):
|
|
546
|
+
"""Append content to the last message if same role, otherwise add new message."""
|
|
547
|
+
assert content is not None, "content is required"
|
|
548
|
+
|
|
549
|
+
if isinstance(content, SingleMessage):
|
|
550
|
+
content = content.content
|
|
551
|
+
|
|
552
|
+
if len(self.messages) > 0 and self.messages[-1].role == role:
|
|
553
|
+
# Use append_content to handle multimodal merging properly
|
|
554
|
+
last_msg = self.messages[-1]
|
|
555
|
+
if isinstance(content, str) and isinstance(last_msg.content, str):
|
|
556
|
+
# Both are strings - append with newline
|
|
557
|
+
last_msg.content += "\n" + content
|
|
558
|
+
elif isinstance(content, list) or isinstance(last_msg.content, list):
|
|
559
|
+
# At least one is multimodal - use append_content method
|
|
560
|
+
last_msg.append_content(content)
|
|
561
|
+
else:
|
|
562
|
+
last_msg.content += "\n" + content
|
|
563
|
+
else:
|
|
564
|
+
self.add_message(
|
|
565
|
+
role=role,
|
|
566
|
+
content=content,
|
|
567
|
+
user_id=user_id,
|
|
568
|
+
metadata=metadata,
|
|
569
|
+
tool_calls=tool_calls,
|
|
570
|
+
tool_call_id=tool_call_id,
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
def add_messages(
|
|
574
|
+
self,
|
|
575
|
+
role: MessageRole,
|
|
576
|
+
contents: List[Any],
|
|
577
|
+
user_id: str = "",
|
|
578
|
+
metadata: Dict[str, Any] = {},
|
|
579
|
+
):
|
|
580
|
+
"""Add a list of messages to the conversation"""
|
|
581
|
+
for content in contents:
|
|
582
|
+
if isinstance(content, str):
|
|
583
|
+
self.add_message(
|
|
584
|
+
role=role, content=content, user_id=user_id, metadata=metadata
|
|
585
|
+
)
|
|
586
|
+
elif isinstance(content, SingleMessage):
|
|
587
|
+
# Preserve all attributes from the original SingleMessage
|
|
588
|
+
self.add_message(
|
|
589
|
+
role=content.role, # Use original role instead of forcing new role
|
|
590
|
+
content=content.content,
|
|
591
|
+
user_id=content.user_id or user_id,
|
|
592
|
+
metadata={**content.metadata, **metadata},
|
|
593
|
+
tool_calls=content.tool_calls,
|
|
594
|
+
tool_call_id=content.tool_call_id,
|
|
595
|
+
)
|
|
596
|
+
else:
|
|
597
|
+
raise ValueError(f"Invalid message content type: {type(content)}")
|
|
598
|
+
return self
|
|
599
|
+
|
|
600
|
+
def set_messages(self, messages: "Messages"):
|
|
601
|
+
"""Set the entire message list"""
|
|
602
|
+
self.messages = messages.get_messages()
|
|
603
|
+
|
|
604
|
+
def get_messages(self) -> List[SingleMessage]:
|
|
605
|
+
"""Get all messages"""
|
|
606
|
+
return self.messages
|
|
607
|
+
|
|
608
|
+
def get_messages_as_dict(self) -> PlainMessages:
|
|
609
|
+
"""Get all messages as dictionary format for compatibility"""
|
|
610
|
+
return [msg.to_dict() for msg in self.messages]
|
|
611
|
+
|
|
612
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
613
|
+
"""Convert the Messages object to a serializable dictionary."""
|
|
614
|
+
return {
|
|
615
|
+
"messages": [msg.to_dict() for msg in self.messages],
|
|
616
|
+
"max_tokens": self.max_tokens,
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
def first(self) -> SingleMessage:
|
|
620
|
+
"""Get the first message"""
|
|
621
|
+
return self.messages[0]
|
|
622
|
+
|
|
623
|
+
def last(self) -> SingleMessage:
|
|
624
|
+
"""Get the last message"""
|
|
625
|
+
return self.messages[-1]
|
|
626
|
+
|
|
627
|
+
def empty(self) -> bool:
|
|
628
|
+
"""Check if the message list is empty"""
|
|
629
|
+
return len(self.messages) == 0
|
|
630
|
+
|
|
631
|
+
def length(self) -> int:
|
|
632
|
+
"""Get the total length of the message list"""
|
|
633
|
+
return sum(msg.length() for msg in self.messages)
|
|
634
|
+
|
|
635
|
+
def str_first(self) -> str:
|
|
636
|
+
"""Get the first message as string"""
|
|
637
|
+
return str(self.messages[0]).replace("\n", "\\n")
|
|
638
|
+
|
|
639
|
+
def str_last(self) -> str:
|
|
640
|
+
"""Get the last message as string"""
|
|
641
|
+
return str(self.messages[-1]).replace("\n", "\\n")
|
|
642
|
+
|
|
643
|
+
def str_summary(self, max_chars: int = 20) -> str:
|
|
644
|
+
"""Generate a compact summary of all messages for debugging.
|
|
645
|
+
|
|
646
|
+
Shows a one-line preview of each message with role and content preview.
|
|
647
|
+
|
|
648
|
+
Args:
|
|
649
|
+
max_chars: Maximum characters to show from each end of content.
|
|
650
|
+
|
|
651
|
+
Returns:
|
|
652
|
+
A compact string like:
|
|
653
|
+
'[SYS: You are a he...assistant | 120c] -> [USR: 请帮我分析...这段代码 | 45c] -> [AST: 好的,我来...完成了。 | 892c]'
|
|
654
|
+
"""
|
|
655
|
+
if not self.messages:
|
|
656
|
+
return "[Empty]"
|
|
657
|
+
|
|
658
|
+
previews = [msg.str_preview(max_chars) for msg in self.messages]
|
|
659
|
+
return " -> ".join(previews)
|
|
660
|
+
|
|
661
|
+
def have_system_message(self) -> bool:
|
|
662
|
+
"""Check if the message list has a system message"""
|
|
663
|
+
return any(msg.role == MessageRole.SYSTEM for msg in self.messages)
|
|
664
|
+
|
|
665
|
+
def extend_messages(self, messages: "Messages"):
|
|
666
|
+
"""Extend the message list"""
|
|
667
|
+
self.messages.extend(messages.get_messages())
|
|
668
|
+
|
|
669
|
+
def extend_plain_messages(self, messages: PlainMessages):
|
|
670
|
+
"""Extend the message list"""
|
|
671
|
+
for msg in messages:
|
|
672
|
+
# Convert string role to MessageRole enum if needed
|
|
673
|
+
role = MessageRole.USER
|
|
674
|
+
if msg["role"].lower() == "assistant":
|
|
675
|
+
role = MessageRole.ASSISTANT
|
|
676
|
+
elif msg["role"].lower() == "system":
|
|
677
|
+
role = MessageRole.SYSTEM
|
|
678
|
+
elif msg["role"].lower() == "tool":
|
|
679
|
+
role = MessageRole.TOOL
|
|
680
|
+
|
|
681
|
+
singleMsg = SingleMessage(
|
|
682
|
+
role=role,
|
|
683
|
+
content=msg["content"],
|
|
684
|
+
timestamp=(
|
|
685
|
+
msg["timestamp"]
|
|
686
|
+
if "timestamp" in msg
|
|
687
|
+
else datetime.now().isoformat()
|
|
688
|
+
),
|
|
689
|
+
user_id=msg["user_id"] if "user_id" in msg else "",
|
|
690
|
+
metadata=msg["metadata"] if "metadata" in msg else {},
|
|
691
|
+
tool_calls=msg.get("tool_calls"),
|
|
692
|
+
tool_call_id=msg.get("tool_call_id"),
|
|
693
|
+
)
|
|
694
|
+
self.messages.append(singleMsg)
|
|
695
|
+
|
|
696
|
+
def extract_system_messages(self) -> "Messages":
|
|
697
|
+
"""Extract system messages from the message list"""
|
|
698
|
+
system_messages = Messages()
|
|
699
|
+
for msg in self.messages:
|
|
700
|
+
if msg.role == MessageRole.SYSTEM:
|
|
701
|
+
system_messages.add_message(msg)
|
|
702
|
+
return system_messages
|
|
703
|
+
|
|
704
|
+
def extract_non_system_messages(self) -> "Messages":
|
|
705
|
+
"""Extract non-system messages from the message list"""
|
|
706
|
+
non_system_messages = Messages()
|
|
707
|
+
for msg in self.messages:
|
|
708
|
+
if msg.role != MessageRole.SYSTEM:
|
|
709
|
+
non_system_messages.add_message(msg)
|
|
710
|
+
return non_system_messages
|
|
711
|
+
|
|
712
|
+
def drop_last(self):
|
|
713
|
+
"""Drop the last message"""
|
|
714
|
+
self.messages = self.messages[:-1]
|
|
715
|
+
|
|
716
|
+
def estimated_tokens(self):
|
|
717
|
+
estimated = 0
|
|
718
|
+
for message in self.messages:
|
|
719
|
+
estimated += estimate_tokens_from_chars(message.content)
|
|
720
|
+
return estimated
|
|
721
|
+
|
|
722
|
+
def compress(self, compress_level: CompressLevel):
|
|
723
|
+
"""
|
|
724
|
+
Compress all messages in the list
|
|
725
|
+
|
|
726
|
+
Args:
|
|
727
|
+
compress_level (CompressLevel): The compression level to apply
|
|
728
|
+
"""
|
|
729
|
+
for message in self.messages:
|
|
730
|
+
message.compress(compress_level)
|
|
731
|
+
|
|
732
|
+
def make_new_messages(self, content: str):
|
|
733
|
+
if not content:
|
|
734
|
+
return self
|
|
735
|
+
|
|
736
|
+
new_messages = Messages()
|
|
737
|
+
new_messages.extend_messages(self)
|
|
738
|
+
new_messages.append_message(MessageRole.USER, content)
|
|
739
|
+
return new_messages
|
|
740
|
+
|
|
741
|
+
def copy(self):
|
|
742
|
+
new_messages = Messages()
|
|
743
|
+
new_messages.messages = [msg.copy() for msg in self.messages]
|
|
744
|
+
return new_messages
|
|
745
|
+
|
|
746
|
+
@staticmethod
|
|
747
|
+
def create_system_message(content: str) -> "SingleMessage":
|
|
748
|
+
"""Make a system message"""
|
|
749
|
+
return SingleMessage(
|
|
750
|
+
role=MessageRole.SYSTEM,
|
|
751
|
+
content=content,
|
|
752
|
+
timestamp=datetime.now().isoformat(),
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
@staticmethod
|
|
756
|
+
def combine_messages(
|
|
757
|
+
messages: "Messages", other_messages: "Messages"
|
|
758
|
+
) -> "Messages":
|
|
759
|
+
"""Combine two message lists"""
|
|
760
|
+
combined_messages = Messages()
|
|
761
|
+
combined_messages.extend_messages(messages)
|
|
762
|
+
combined_messages.extend_messages(other_messages)
|
|
763
|
+
return combined_messages
|
|
764
|
+
|
|
765
|
+
def __len__(self) -> int:
|
|
766
|
+
"""Return the number of messages"""
|
|
767
|
+
return len(self.messages)
|
|
768
|
+
|
|
769
|
+
def __iter__(self):
|
|
770
|
+
"""Allow iteration over messages"""
|
|
771
|
+
return iter(self.messages)
|
|
772
|
+
|
|
773
|
+
def __getitem__(self, index):
|
|
774
|
+
"""Allow indexing of messages"""
|
|
775
|
+
return self.messages[index]
|
|
776
|
+
|
|
777
|
+
def __add__(self, other: "Messages") -> "Messages":
|
|
778
|
+
"""Add two Messages objects together, preserving all message attributes"""
|
|
779
|
+
if not isinstance(other, Messages):
|
|
780
|
+
raise TypeError(f"Cannot add Messages with {type(other)}")
|
|
781
|
+
|
|
782
|
+
result = self.copy()
|
|
783
|
+
result.extend_messages(other)
|
|
784
|
+
return result
|
|
785
|
+
|
|
786
|
+
def add_tool_call_message(
|
|
787
|
+
self,
|
|
788
|
+
content: str,
|
|
789
|
+
tool_calls: List[Dict[str, Any]],
|
|
790
|
+
user_id: str = "",
|
|
791
|
+
metadata: Dict[str, Any] = {},
|
|
792
|
+
):
|
|
793
|
+
"""Add a message with tool calls (typically assistant role)"""
|
|
794
|
+
self.add_message(
|
|
795
|
+
role=MessageRole.ASSISTANT,
|
|
796
|
+
content=content,
|
|
797
|
+
user_id=user_id,
|
|
798
|
+
metadata=metadata,
|
|
799
|
+
tool_calls=tool_calls,
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
def add_tool_response_message(
|
|
803
|
+
self,
|
|
804
|
+
content: str,
|
|
805
|
+
tool_call_id: str,
|
|
806
|
+
user_id: str = "",
|
|
807
|
+
metadata: Dict[str, Any] = {},
|
|
808
|
+
):
|
|
809
|
+
"""Add a tool response message (tool role)"""
|
|
810
|
+
self.add_message(
|
|
811
|
+
role=MessageRole.TOOL,
|
|
812
|
+
content=content,
|
|
813
|
+
user_id=user_id,
|
|
814
|
+
metadata=metadata,
|
|
815
|
+
tool_call_id=tool_call_id,
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
def get_messages_with_tool_calls(self) -> List[SingleMessage]:
|
|
819
|
+
"""Get all messages that have tool calls"""
|
|
820
|
+
return [msg for msg in self.messages if msg.has_tool_calls()]
|
|
821
|
+
|
|
822
|
+
def get_tool_response_messages(self) -> List[SingleMessage]:
|
|
823
|
+
"""Get all tool response messages"""
|
|
824
|
+
return [msg for msg in self.messages if msg.tool_call_id is not None]
|
|
825
|
+
|
|
826
|
+
def __str__(self):
|
|
827
|
+
return "\n-----------------------------\n".join(
|
|
828
|
+
[str(msg) for msg in self.messages]
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
class CategoryBlock(Enum):
|
|
833
|
+
JUDGE = "judge"
|
|
834
|
+
EXPLORE = "explore"
|
|
835
|
+
PROMPT = "prompt"
|
|
836
|
+
TOOL = "tool"
|
|
837
|
+
ASSIGN = "assign"
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
class SkillType(Enum):
|
|
841
|
+
TOOL = "TOOL"
|
|
842
|
+
AGENT = "AGENT"
|
|
843
|
+
MCP = "MCP"
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
class SkillArg:
|
|
847
|
+
def __init__(self, name: str, type: str, value: Any):
|
|
848
|
+
self.name = name
|
|
849
|
+
self.type = type
|
|
850
|
+
self.value = value
|
|
851
|
+
|
|
852
|
+
def to_dict(self):
|
|
853
|
+
return {"name": self.name, "type": self.type, "value": self.value}
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
class SkillInfo:
|
|
857
|
+
def __init__(
|
|
858
|
+
self, type: SkillType, name: str, args: list[SkillArg], checked: bool = True
|
|
859
|
+
):
|
|
860
|
+
self.type = type
|
|
861
|
+
self.name = name
|
|
862
|
+
self.args = args
|
|
863
|
+
self.checked = checked
|
|
864
|
+
|
|
865
|
+
def to_dict(self):
|
|
866
|
+
return {
|
|
867
|
+
"type": self.type.value,
|
|
868
|
+
"name": self.name,
|
|
869
|
+
"args": [arg.to_dict() for arg in self.args],
|
|
870
|
+
"checked": self.checked,
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
def __str__(self):
|
|
874
|
+
return f"{self.type}: {self.name} {self.args}"
|
|
875
|
+
|
|
876
|
+
@staticmethod
|
|
877
|
+
def build(
|
|
878
|
+
skill_type: SkillType,
|
|
879
|
+
skill_name: str,
|
|
880
|
+
skill_args: dict = {},
|
|
881
|
+
checked: bool = True,
|
|
882
|
+
) -> "SkillInfo":
|
|
883
|
+
args = [SkillArg(k, type(v).__name__, v) for k, v in skill_args.items()]
|
|
884
|
+
return SkillInfo(skill_type, skill_name, args, checked)
|
|
885
|
+
|
|
886
|
+
@staticmethod
|
|
887
|
+
def from_dict(dict_data: dict) -> Optional["SkillInfo"]:
|
|
888
|
+
if not dict_data:
|
|
889
|
+
return None
|
|
890
|
+
return SkillInfo(
|
|
891
|
+
type=SkillType(dict_data["type"]),
|
|
892
|
+
name=dict_data["name"],
|
|
893
|
+
args=[
|
|
894
|
+
SkillArg(arg["name"], arg["type"], arg["value"])
|
|
895
|
+
for arg in dict_data["args"]
|
|
896
|
+
],
|
|
897
|
+
checked=dict_data["checked"],
|
|
898
|
+
)
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
class Status(Enum):
|
|
902
|
+
PROCESSING = "processing"
|
|
903
|
+
COMPLETED = "completed"
|
|
904
|
+
FAILED = "failed"
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
class TypeStage(Enum):
|
|
908
|
+
LLM = "llm"
|
|
909
|
+
SKILL = "skill"
|
|
910
|
+
ASSIGN = "assign"
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
def count_occurrences(str_list, target_str):
|
|
914
|
+
total_count = 0
|
|
915
|
+
for s in str_list:
|
|
916
|
+
total_count += target_str.count(s)
|
|
917
|
+
return total_count
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
@dataclass
|
|
921
|
+
class ToolCallInfo:
|
|
922
|
+
"""Single tool call information
|
|
923
|
+
|
|
924
|
+
Used to store parsed tool call data from LLM responses.
|
|
925
|
+
Supports both LLM-provided IDs and fallback ID generation.
|
|
926
|
+
|
|
927
|
+
Attributes:
|
|
928
|
+
id: Unique identifier for this tool call (LLM-provided or generated)
|
|
929
|
+
name: Name of the tool/function to call
|
|
930
|
+
arguments: Parsed arguments dict (None if parsing failed)
|
|
931
|
+
index: Position index in multi-tool-call response
|
|
932
|
+
raw_arguments: Original unparsed arguments string (for debugging)
|
|
933
|
+
is_complete: Whether the tool call arguments have been fully received
|
|
934
|
+
and successfully parsed. False if stream is incomplete or
|
|
935
|
+
JSON parsing failed.
|
|
936
|
+
"""
|
|
937
|
+
id: str
|
|
938
|
+
name: str
|
|
939
|
+
arguments: Optional[Dict[str, Any]] = None
|
|
940
|
+
index: int = 0
|
|
941
|
+
raw_arguments: str = ""
|
|
942
|
+
is_complete: bool = False
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
class StreamItem:
|
|
946
|
+
"""Streaming response item from LLM
|
|
947
|
+
|
|
948
|
+
Supports both single tool call (legacy) and multiple tool calls (new).
|
|
949
|
+
Feature flag ENABLE_PARALLEL_TOOL_CALLS controls which behavior is used.
|
|
950
|
+
"""
|
|
951
|
+
|
|
952
|
+
def __init__(self):
|
|
953
|
+
self.answer = ""
|
|
954
|
+
self.think = ""
|
|
955
|
+
# Legacy fields (preserved for backward compatibility)
|
|
956
|
+
self.tool_name = ""
|
|
957
|
+
self.tool_args: Optional[dict[str, Any]] = None
|
|
958
|
+
# Stable tool_call_id for single tool call flows
|
|
959
|
+
self.tool_call_id: Optional[str] = None
|
|
960
|
+
# New fields for multiple tool calls
|
|
961
|
+
self.tool_calls: List[ToolCallInfo] = []
|
|
962
|
+
self.finish_reason: Optional[str] = None
|
|
963
|
+
self.output_var_value = None
|
|
964
|
+
self.token_usage = {}
|
|
965
|
+
|
|
966
|
+
def has_tool_call(self) -> bool:
|
|
967
|
+
"""Check if there is a tool call (backward compatible).
|
|
968
|
+
|
|
969
|
+
Note: When ENABLE_PARALLEL_TOOL_CALLS flag is disabled, only checks
|
|
970
|
+
the legacy tool_name field to ensure backward compatibility.
|
|
971
|
+
This prevents accidentally triggering new code paths.
|
|
972
|
+
"""
|
|
973
|
+
if flags.is_enabled(flags.ENABLE_PARALLEL_TOOL_CALLS):
|
|
974
|
+
return self.tool_name != "" or len(self.tool_calls) > 0
|
|
975
|
+
else:
|
|
976
|
+
# Flag disabled: only check legacy field for backward compatibility
|
|
977
|
+
return self.tool_name != ""
|
|
978
|
+
|
|
979
|
+
def has_tool_calls(self) -> bool:
|
|
980
|
+
"""Check if there are multiple tool calls (new method)."""
|
|
981
|
+
return len(self.tool_calls) > 0
|
|
982
|
+
|
|
983
|
+
def has_complete_tool_call(self) -> bool:
|
|
984
|
+
"""Check if there is a complete tool call with parsed arguments."""
|
|
985
|
+
return self.tool_name != "" and self.tool_args is not None
|
|
986
|
+
|
|
987
|
+
def set_output_var_value(self, output_var_value):
|
|
988
|
+
self.output_var_value = output_var_value
|
|
989
|
+
|
|
990
|
+
def get_tool_call(self) -> dict[str, Any]:
|
|
991
|
+
"""Get single tool call info (legacy method)."""
|
|
992
|
+
return {
|
|
993
|
+
"name": self.tool_name,
|
|
994
|
+
"arguments": self.tool_args,
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
def get_tool_calls(self) -> List[ToolCallInfo]:
|
|
998
|
+
"""Get all tool calls.
|
|
999
|
+
|
|
1000
|
+
Returns:
|
|
1001
|
+
List of ToolCallInfo objects. If tool_calls is empty but
|
|
1002
|
+
legacy tool_name is set, returns a single-item list for compatibility.
|
|
1003
|
+
"""
|
|
1004
|
+
if self.tool_calls:
|
|
1005
|
+
return self.tool_calls
|
|
1006
|
+
# Fallback to legacy field for backward compatibility
|
|
1007
|
+
if self.tool_name:
|
|
1008
|
+
# Set is_complete=True when tool_args has been successfully parsed
|
|
1009
|
+
# This ensures detect_tool_calls() can properly detect legacy tool calls
|
|
1010
|
+
return [ToolCallInfo(
|
|
1011
|
+
id=f"{TOOL_CALL_ID_PREFIX}{self.tool_name}_0",
|
|
1012
|
+
name=self.tool_name,
|
|
1013
|
+
arguments=self.tool_args,
|
|
1014
|
+
index=0,
|
|
1015
|
+
raw_arguments=json.dumps(self.tool_args) if self.tool_args else "",
|
|
1016
|
+
is_complete=self.tool_args is not None, # Mark complete if args parsed
|
|
1017
|
+
)]
|
|
1018
|
+
return []
|
|
1019
|
+
|
|
1020
|
+
def parse_from_chunk(self, chunk: dict, session_counter: int | None = None):
|
|
1021
|
+
"""Parse streaming chunk from LLM response.
|
|
1022
|
+
|
|
1023
|
+
Args:
|
|
1024
|
+
chunk: The LLM response chunk containing content, tool calls, etc.
|
|
1025
|
+
session_counter: Optional session-level tool call batch counter for
|
|
1026
|
+
generating fallback tool_call_ids. If None, a short UUID
|
|
1027
|
+
will be used to ensure uniqueness across sessions.
|
|
1028
|
+
"""
|
|
1029
|
+
# Generate a unique batch ID if no session_counter provided
|
|
1030
|
+
batch_id = str(session_counter) if session_counter is not None else uuid.uuid4().hex[:8]
|
|
1031
|
+
self.answer = chunk.get("content", "")
|
|
1032
|
+
self.think = chunk.get("reasoning_content", "")
|
|
1033
|
+
self.token_usage = chunk.get("usage", {})
|
|
1034
|
+
self.finish_reason = chunk.get("finish_reason")
|
|
1035
|
+
self.tool_call_id = None
|
|
1036
|
+
|
|
1037
|
+
# Parse multiple tool calls from tool_calls_data (new format)
|
|
1038
|
+
tool_calls_data = chunk.get("tool_calls_data", {})
|
|
1039
|
+
if tool_calls_data:
|
|
1040
|
+
self.tool_calls = []
|
|
1041
|
+
# Sort by index to ensure correct execution order
|
|
1042
|
+
items = []
|
|
1043
|
+
for index, data in tool_calls_data.items():
|
|
1044
|
+
try:
|
|
1045
|
+
normalized_index = int(index)
|
|
1046
|
+
except Exception:
|
|
1047
|
+
normalized_index = 0
|
|
1048
|
+
items.append((normalized_index, data))
|
|
1049
|
+
|
|
1050
|
+
for normalized_index, data in sorted(items, key=lambda x: x[0]):
|
|
1051
|
+
if data.get("name"):
|
|
1052
|
+
args_str = "".join(data.get("arguments", []))
|
|
1053
|
+
parsed_args = None
|
|
1054
|
+
|
|
1055
|
+
is_complete = False
|
|
1056
|
+
if args_str:
|
|
1057
|
+
try:
|
|
1058
|
+
parsed_args = json.loads(args_str)
|
|
1059
|
+
if isinstance(parsed_args, str):
|
|
1060
|
+
# Double JSON encoding detected - log for debugging
|
|
1061
|
+
logger.debug(
|
|
1062
|
+
f"Double JSON encoding detected for tool call '{data.get('name')}', "
|
|
1063
|
+
f"performing second parse. Original: {args_str[:200]}..."
|
|
1064
|
+
if len(args_str) > 200 else
|
|
1065
|
+
f"Double JSON encoding detected for tool call '{data.get('name')}', "
|
|
1066
|
+
f"performing second parse. Original: {args_str}"
|
|
1067
|
+
)
|
|
1068
|
+
parsed_args = json.loads(parsed_args)
|
|
1069
|
+
is_complete = True # Successfully parsed
|
|
1070
|
+
except json.JSONDecodeError:
|
|
1071
|
+
# Arguments not yet complete, keep as None
|
|
1072
|
+
# is_complete remains False
|
|
1073
|
+
pass
|
|
1074
|
+
|
|
1075
|
+
# Generate or use tool_call_id
|
|
1076
|
+
# Priority: LLM-provided id > fallback with batch_id
|
|
1077
|
+
tool_call_id = data.get("id")
|
|
1078
|
+
if not tool_call_id:
|
|
1079
|
+
# Fallback: use batch_id and index for stable ID
|
|
1080
|
+
# Format: {TOOL_CALL_ID_PREFIX}{batch_id}_{index}
|
|
1081
|
+
tool_call_id = f"{TOOL_CALL_ID_PREFIX}{batch_id}_{normalized_index}"
|
|
1082
|
+
|
|
1083
|
+
self.tool_calls.append(ToolCallInfo(
|
|
1084
|
+
id=tool_call_id,
|
|
1085
|
+
name=data["name"],
|
|
1086
|
+
arguments=parsed_args,
|
|
1087
|
+
index=normalized_index,
|
|
1088
|
+
raw_arguments=args_str,
|
|
1089
|
+
is_complete=is_complete,
|
|
1090
|
+
))
|
|
1091
|
+
|
|
1092
|
+
# Provide a stable single tool_call_id (index 0) for legacy callers.
|
|
1093
|
+
if self.tool_calls:
|
|
1094
|
+
self.tool_call_id = self.tool_calls[0].id
|
|
1095
|
+
|
|
1096
|
+
# Parse legacy single tool call (backward compatibility)
|
|
1097
|
+
if "func_name" in chunk and chunk["func_name"]:
|
|
1098
|
+
func_args_list = chunk.get("func_args", [])
|
|
1099
|
+
func_args_str = "".join(func_args_list) if func_args_list else ""
|
|
1100
|
+
parsed_args: Optional[dict[str, Any]] = None
|
|
1101
|
+
if func_args_str:
|
|
1102
|
+
try:
|
|
1103
|
+
parsed_args = json.loads(func_args_str)
|
|
1104
|
+
if isinstance(parsed_args, str):
|
|
1105
|
+
# Double JSON encoding detected - log for debugging
|
|
1106
|
+
logger.debug(
|
|
1107
|
+
f"Double JSON encoding detected for legacy tool call '{chunk['func_name']}', "
|
|
1108
|
+
f"performing second parse. Original: {func_args_str[:200]}..."
|
|
1109
|
+
if len(func_args_str) > 200 else
|
|
1110
|
+
f"Double JSON encoding detected for legacy tool call '{chunk['func_name']}', "
|
|
1111
|
+
f"performing second parse. Original: {func_args_str}"
|
|
1112
|
+
)
|
|
1113
|
+
parsed_args = json.loads(parsed_args)
|
|
1114
|
+
except json.JSONDecodeError:
|
|
1115
|
+
# The parameters are not yet complete, parsed_args remains None
|
|
1116
|
+
pass
|
|
1117
|
+
|
|
1118
|
+
self.tool_name = chunk["func_name"]
|
|
1119
|
+
self.tool_args = parsed_args
|
|
1120
|
+
if not self.tool_call_id:
|
|
1121
|
+
self.tool_call_id = f"{TOOL_CALL_ID_PREFIX}{batch_id}_0"
|
|
1122
|
+
|
|
1123
|
+
def to_dict(self):
|
|
1124
|
+
result = {"answer": self.answer, "think": self.think}
|
|
1125
|
+
|
|
1126
|
+
if self.tool_name:
|
|
1127
|
+
result["tool_call"] = self.get_tool_call()
|
|
1128
|
+
|
|
1129
|
+
# Include multiple tool calls if present
|
|
1130
|
+
if self.tool_calls:
|
|
1131
|
+
result["tool_calls"] = [
|
|
1132
|
+
{
|
|
1133
|
+
"id": tc.id,
|
|
1134
|
+
"name": tc.name,
|
|
1135
|
+
"arguments": tc.arguments,
|
|
1136
|
+
"index": tc.index,
|
|
1137
|
+
}
|
|
1138
|
+
for tc in self.tool_calls
|
|
1139
|
+
]
|
|
1140
|
+
|
|
1141
|
+
if self.output_var_value is not None:
|
|
1142
|
+
result["output_var_value"] = self.output_var_value
|
|
1143
|
+
|
|
1144
|
+
if self.token_usage:
|
|
1145
|
+
result["token_usage"] = self.token_usage
|
|
1146
|
+
|
|
1147
|
+
if self.finish_reason:
|
|
1148
|
+
result["finish_reason"] = self.finish_reason
|
|
1149
|
+
|
|
1150
|
+
return result
|
|
1151
|
+
|
|
1152
|
+
|
|
1153
|
+
class DolphinSDKEncoder(json.JSONEncoder):
|
|
1154
|
+
"""Custom JSON encoder for Dolphin SDK objects.
|
|
1155
|
+
|
|
1156
|
+
This encoder handles serialization of Dolphin SDK custom objects that
|
|
1157
|
+
are not natively JSON serializable, such as Messages, VarOutput, etc.
|
|
1158
|
+
|
|
1159
|
+
Uses duck typing to avoid circular import issues.
|
|
1160
|
+
"""
|
|
1161
|
+
def default(self, obj):
|
|
1162
|
+
# Handle Enum objects first (most common)
|
|
1163
|
+
if isinstance(obj, Enum):
|
|
1164
|
+
return obj.value
|
|
1165
|
+
# Handle Var objects - extract their value
|
|
1166
|
+
if isinstance(obj, Var):
|
|
1167
|
+
return obj.value
|
|
1168
|
+
# Handle any object with a to_dict() method (duck typing)
|
|
1169
|
+
# This includes Messages, VarOutput, SkillInfo, SingleMessage, etc.
|
|
1170
|
+
if hasattr(obj, 'to_dict') and callable(getattr(obj, 'to_dict')):
|
|
1171
|
+
return obj.to_dict()
|
|
1172
|
+
# Let the base class default method raise the TypeError for other types
|
|
1173
|
+
return super().default(obj)
|