kweaver-dolphin 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. DolphinLanguageSDK/__init__.py +58 -0
  2. dolphin/__init__.py +62 -0
  3. dolphin/cli/__init__.py +20 -0
  4. dolphin/cli/args/__init__.py +9 -0
  5. dolphin/cli/args/parser.py +567 -0
  6. dolphin/cli/builtin_agents/__init__.py +22 -0
  7. dolphin/cli/commands/__init__.py +4 -0
  8. dolphin/cli/interrupt/__init__.py +8 -0
  9. dolphin/cli/interrupt/handler.py +205 -0
  10. dolphin/cli/interrupt/keyboard.py +82 -0
  11. dolphin/cli/main.py +49 -0
  12. dolphin/cli/multimodal/__init__.py +34 -0
  13. dolphin/cli/multimodal/clipboard.py +327 -0
  14. dolphin/cli/multimodal/handler.py +249 -0
  15. dolphin/cli/multimodal/image_processor.py +214 -0
  16. dolphin/cli/multimodal/input_parser.py +149 -0
  17. dolphin/cli/runner/__init__.py +8 -0
  18. dolphin/cli/runner/runner.py +989 -0
  19. dolphin/cli/ui/__init__.py +10 -0
  20. dolphin/cli/ui/console.py +2795 -0
  21. dolphin/cli/ui/input.py +340 -0
  22. dolphin/cli/ui/layout.py +425 -0
  23. dolphin/cli/ui/stream_renderer.py +302 -0
  24. dolphin/cli/utils/__init__.py +8 -0
  25. dolphin/cli/utils/helpers.py +135 -0
  26. dolphin/cli/utils/version.py +49 -0
  27. dolphin/core/__init__.py +107 -0
  28. dolphin/core/agent/__init__.py +10 -0
  29. dolphin/core/agent/agent_state.py +69 -0
  30. dolphin/core/agent/base_agent.py +970 -0
  31. dolphin/core/code_block/__init__.py +0 -0
  32. dolphin/core/code_block/agent_init_block.py +0 -0
  33. dolphin/core/code_block/assign_block.py +98 -0
  34. dolphin/core/code_block/basic_code_block.py +1865 -0
  35. dolphin/core/code_block/explore_block.py +1327 -0
  36. dolphin/core/code_block/explore_block_v2.py +712 -0
  37. dolphin/core/code_block/explore_strategy.py +672 -0
  38. dolphin/core/code_block/judge_block.py +220 -0
  39. dolphin/core/code_block/prompt_block.py +32 -0
  40. dolphin/core/code_block/skill_call_deduplicator.py +291 -0
  41. dolphin/core/code_block/tool_block.py +129 -0
  42. dolphin/core/common/__init__.py +17 -0
  43. dolphin/core/common/constants.py +176 -0
  44. dolphin/core/common/enums.py +1173 -0
  45. dolphin/core/common/exceptions.py +133 -0
  46. dolphin/core/common/multimodal.py +539 -0
  47. dolphin/core/common/object_type.py +165 -0
  48. dolphin/core/common/output_format.py +432 -0
  49. dolphin/core/common/types.py +36 -0
  50. dolphin/core/config/__init__.py +16 -0
  51. dolphin/core/config/global_config.py +1289 -0
  52. dolphin/core/config/ontology_config.py +133 -0
  53. dolphin/core/context/__init__.py +12 -0
  54. dolphin/core/context/context.py +1580 -0
  55. dolphin/core/context/context_manager.py +161 -0
  56. dolphin/core/context/var_output.py +82 -0
  57. dolphin/core/context/variable_pool.py +356 -0
  58. dolphin/core/context_engineer/__init__.py +41 -0
  59. dolphin/core/context_engineer/config/__init__.py +5 -0
  60. dolphin/core/context_engineer/config/settings.py +402 -0
  61. dolphin/core/context_engineer/core/__init__.py +7 -0
  62. dolphin/core/context_engineer/core/budget_manager.py +327 -0
  63. dolphin/core/context_engineer/core/context_assembler.py +583 -0
  64. dolphin/core/context_engineer/core/context_manager.py +637 -0
  65. dolphin/core/context_engineer/core/tokenizer_service.py +260 -0
  66. dolphin/core/context_engineer/example/incremental_example.py +267 -0
  67. dolphin/core/context_engineer/example/traditional_example.py +334 -0
  68. dolphin/core/context_engineer/services/__init__.py +5 -0
  69. dolphin/core/context_engineer/services/compressor.py +399 -0
  70. dolphin/core/context_engineer/utils/__init__.py +6 -0
  71. dolphin/core/context_engineer/utils/context_utils.py +441 -0
  72. dolphin/core/context_engineer/utils/message_formatter.py +270 -0
  73. dolphin/core/context_engineer/utils/token_utils.py +139 -0
  74. dolphin/core/coroutine/__init__.py +15 -0
  75. dolphin/core/coroutine/context_snapshot.py +154 -0
  76. dolphin/core/coroutine/context_snapshot_profile.py +922 -0
  77. dolphin/core/coroutine/context_snapshot_store.py +268 -0
  78. dolphin/core/coroutine/execution_frame.py +145 -0
  79. dolphin/core/coroutine/execution_state_registry.py +161 -0
  80. dolphin/core/coroutine/resume_handle.py +101 -0
  81. dolphin/core/coroutine/step_result.py +101 -0
  82. dolphin/core/executor/__init__.py +18 -0
  83. dolphin/core/executor/debug_controller.py +630 -0
  84. dolphin/core/executor/dolphin_executor.py +1063 -0
  85. dolphin/core/executor/executor.py +624 -0
  86. dolphin/core/flags/__init__.py +27 -0
  87. dolphin/core/flags/definitions.py +49 -0
  88. dolphin/core/flags/manager.py +113 -0
  89. dolphin/core/hook/__init__.py +95 -0
  90. dolphin/core/hook/expression_evaluator.py +499 -0
  91. dolphin/core/hook/hook_dispatcher.py +380 -0
  92. dolphin/core/hook/hook_types.py +248 -0
  93. dolphin/core/hook/isolated_variable_pool.py +284 -0
  94. dolphin/core/interfaces.py +53 -0
  95. dolphin/core/llm/__init__.py +0 -0
  96. dolphin/core/llm/llm.py +495 -0
  97. dolphin/core/llm/llm_call.py +100 -0
  98. dolphin/core/llm/llm_client.py +1285 -0
  99. dolphin/core/llm/message_sanitizer.py +120 -0
  100. dolphin/core/logging/__init__.py +20 -0
  101. dolphin/core/logging/logger.py +526 -0
  102. dolphin/core/message/__init__.py +8 -0
  103. dolphin/core/message/compressor.py +749 -0
  104. dolphin/core/parser/__init__.py +8 -0
  105. dolphin/core/parser/parser.py +405 -0
  106. dolphin/core/runtime/__init__.py +10 -0
  107. dolphin/core/runtime/runtime_graph.py +926 -0
  108. dolphin/core/runtime/runtime_instance.py +446 -0
  109. dolphin/core/skill/__init__.py +14 -0
  110. dolphin/core/skill/context_retention.py +157 -0
  111. dolphin/core/skill/skill_function.py +686 -0
  112. dolphin/core/skill/skill_matcher.py +282 -0
  113. dolphin/core/skill/skillkit.py +700 -0
  114. dolphin/core/skill/skillset.py +72 -0
  115. dolphin/core/trajectory/__init__.py +10 -0
  116. dolphin/core/trajectory/recorder.py +189 -0
  117. dolphin/core/trajectory/trajectory.py +522 -0
  118. dolphin/core/utils/__init__.py +9 -0
  119. dolphin/core/utils/cache_kv.py +212 -0
  120. dolphin/core/utils/tools.py +340 -0
  121. dolphin/lib/__init__.py +93 -0
  122. dolphin/lib/debug/__init__.py +8 -0
  123. dolphin/lib/debug/visualizer.py +409 -0
  124. dolphin/lib/memory/__init__.py +28 -0
  125. dolphin/lib/memory/async_processor.py +220 -0
  126. dolphin/lib/memory/llm_calls.py +195 -0
  127. dolphin/lib/memory/manager.py +78 -0
  128. dolphin/lib/memory/sandbox.py +46 -0
  129. dolphin/lib/memory/storage.py +245 -0
  130. dolphin/lib/memory/utils.py +51 -0
  131. dolphin/lib/ontology/__init__.py +12 -0
  132. dolphin/lib/ontology/basic/__init__.py +0 -0
  133. dolphin/lib/ontology/basic/base.py +102 -0
  134. dolphin/lib/ontology/basic/concept.py +130 -0
  135. dolphin/lib/ontology/basic/object.py +11 -0
  136. dolphin/lib/ontology/basic/relation.py +63 -0
  137. dolphin/lib/ontology/datasource/__init__.py +27 -0
  138. dolphin/lib/ontology/datasource/datasource.py +66 -0
  139. dolphin/lib/ontology/datasource/oracle_datasource.py +338 -0
  140. dolphin/lib/ontology/datasource/sql.py +845 -0
  141. dolphin/lib/ontology/mapping.py +177 -0
  142. dolphin/lib/ontology/ontology.py +733 -0
  143. dolphin/lib/ontology/ontology_context.py +16 -0
  144. dolphin/lib/ontology/ontology_manager.py +107 -0
  145. dolphin/lib/skill_results/__init__.py +31 -0
  146. dolphin/lib/skill_results/cache_backend.py +559 -0
  147. dolphin/lib/skill_results/result_processor.py +181 -0
  148. dolphin/lib/skill_results/result_reference.py +179 -0
  149. dolphin/lib/skill_results/skillkit_hook.py +324 -0
  150. dolphin/lib/skill_results/strategies.py +328 -0
  151. dolphin/lib/skill_results/strategy_registry.py +150 -0
  152. dolphin/lib/skillkits/__init__.py +44 -0
  153. dolphin/lib/skillkits/agent_skillkit.py +155 -0
  154. dolphin/lib/skillkits/cognitive_skillkit.py +82 -0
  155. dolphin/lib/skillkits/env_skillkit.py +250 -0
  156. dolphin/lib/skillkits/mcp_adapter.py +616 -0
  157. dolphin/lib/skillkits/mcp_skillkit.py +771 -0
  158. dolphin/lib/skillkits/memory_skillkit.py +650 -0
  159. dolphin/lib/skillkits/noop_skillkit.py +31 -0
  160. dolphin/lib/skillkits/ontology_skillkit.py +89 -0
  161. dolphin/lib/skillkits/plan_act_skillkit.py +452 -0
  162. dolphin/lib/skillkits/resource/__init__.py +52 -0
  163. dolphin/lib/skillkits/resource/models/__init__.py +6 -0
  164. dolphin/lib/skillkits/resource/models/skill_config.py +109 -0
  165. dolphin/lib/skillkits/resource/models/skill_meta.py +127 -0
  166. dolphin/lib/skillkits/resource/resource_skillkit.py +393 -0
  167. dolphin/lib/skillkits/resource/skill_cache.py +215 -0
  168. dolphin/lib/skillkits/resource/skill_loader.py +395 -0
  169. dolphin/lib/skillkits/resource/skill_validator.py +406 -0
  170. dolphin/lib/skillkits/resource_skillkit.py +11 -0
  171. dolphin/lib/skillkits/search_skillkit.py +163 -0
  172. dolphin/lib/skillkits/sql_skillkit.py +274 -0
  173. dolphin/lib/skillkits/system_skillkit.py +509 -0
  174. dolphin/lib/skillkits/vm_skillkit.py +65 -0
  175. dolphin/lib/utils/__init__.py +9 -0
  176. dolphin/lib/utils/data_process.py +207 -0
  177. dolphin/lib/utils/handle_progress.py +178 -0
  178. dolphin/lib/utils/security.py +139 -0
  179. dolphin/lib/utils/text_retrieval.py +462 -0
  180. dolphin/lib/vm/__init__.py +11 -0
  181. dolphin/lib/vm/env_executor.py +895 -0
  182. dolphin/lib/vm/python_session_manager.py +453 -0
  183. dolphin/lib/vm/vm.py +610 -0
  184. dolphin/sdk/__init__.py +60 -0
  185. dolphin/sdk/agent/__init__.py +12 -0
  186. dolphin/sdk/agent/agent_factory.py +236 -0
  187. dolphin/sdk/agent/dolphin_agent.py +1106 -0
  188. dolphin/sdk/api/__init__.py +4 -0
  189. dolphin/sdk/runtime/__init__.py +8 -0
  190. dolphin/sdk/runtime/env.py +363 -0
  191. dolphin/sdk/skill/__init__.py +10 -0
  192. dolphin/sdk/skill/global_skills.py +706 -0
  193. dolphin/sdk/skill/traditional_toolkit.py +260 -0
  194. kweaver_dolphin-0.1.0.dist-info/METADATA +521 -0
  195. kweaver_dolphin-0.1.0.dist-info/RECORD +199 -0
  196. kweaver_dolphin-0.1.0.dist-info/WHEEL +5 -0
  197. kweaver_dolphin-0.1.0.dist-info/entry_points.txt +27 -0
  198. kweaver_dolphin-0.1.0.dist-info/licenses/LICENSE.txt +201 -0
  199. kweaver_dolphin-0.1.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,749 @@
1
+ from abc import ABC, abstractmethod
2
+ from datetime import datetime
3
+ from typing import List, Dict, Any, Optional, TYPE_CHECKING
4
+ from dataclasses import dataclass, field
5
+
6
+ from dolphin.core.common.enums import (
7
+ KnowledgePoint,
8
+ MessageRole,
9
+ Messages,
10
+ CompressLevel,
11
+ )
12
+ from dolphin.core.config.global_config import (
13
+ ContextEngineerConfig,
14
+ )
15
+ from dolphin.core.common.constants import (
16
+ estimate_chars_from_tokens,
17
+ estimate_tokens_from_chars,
18
+ CHINESE_CHAR_TO_TOKEN_RATIO,
19
+ )
20
+ from dolphin.core.context.context import Context
21
+ from dolphin.core.common.multimodal import (
22
+ estimate_image_tokens,
23
+ ImageTokenConfig,
24
+ )
25
+
26
+ if TYPE_CHECKING:
27
+ from dolphin.core.config.global_config import (
28
+ ContextConstraints,
29
+ LLMInstanceConfig,
30
+ )
31
+
32
+ from dolphin.core.logging.logger import get_logger
33
+
34
+ logger = get_logger("context_engineer")
35
+
36
+ # Default image token config for estimation
37
+ _default_image_config = ImageTokenConfig()
38
+
39
+
40
+ @dataclass
41
+ class CompressionResult:
42
+ """Compressed result"""
43
+
44
+ compressed_messages: Messages
45
+ original_token_count: int
46
+ compressed_token_count: int
47
+ compression_ratio: float
48
+ strategy_used: str
49
+ metadata: Optional[Dict[str, Any]] = field(default_factory=dict)
50
+
51
+
52
+ class CompressionStrategy(ABC):
53
+ """Compress strategy abstract base class"""
54
+
55
+ MEMORY_PREFIX = "Here are some knowledge points: "
56
+ DATE_PREFIX = "今天日期是 "
57
+
58
+ @abstractmethod
59
+ def get_name(self) -> str:
60
+ """Get strategy name"""
61
+ pass
62
+
63
+ @abstractmethod
64
+ def estimate_tokens(self, messages: Messages) -> int:
65
+ """Estimate the number of tokens in a message"""
66
+ pass
67
+
68
+ def preparation(
69
+ self,
70
+ context: Context,
71
+ messages: Messages,
72
+ constraints: "ContextConstraints",
73
+ **kwargs,
74
+ ) -> tuple[Messages, Messages, Messages]:
75
+ """Preprocess message list
76
+
77
+ Returns:
78
+ Tuple of (system_messages, first_user_message, other_messages)
79
+ - system_messages: All system messages (must be first)
80
+ - first_user_message: First user message after system (must be preserved for GLM API)
81
+ - other_messages: Remaining messages for truncation/compression
82
+ """
83
+ system_messages = (
84
+ messages.extract_system_messages()
85
+ if constraints.preserve_system
86
+ else Messages()
87
+ )
88
+ if (
89
+ not self._date_in_system_message(system_messages)
90
+ and context.get_var_value("_no_date_in_system_message", "false") != "true"
91
+ ):
92
+ system_messages.prepend_message(
93
+ MessageRole.SYSTEM,
94
+ f"{self.DATE_PREFIX} {datetime.now().strftime('%Y-%m-%d')}",
95
+ )
96
+
97
+ if context.get_config().context_engineer_config.import_mem and not kwargs.get(
98
+ "knowledge_extraction", False
99
+ ):
100
+ knowledge_points = context.get_memory_manager().retrieve_relevant_memory(
101
+ context=context, user_id=context.user_id
102
+ )
103
+ if knowledge_points and not self._knowledge_in_system_message(
104
+ system_messages
105
+ ):
106
+ system_messages.append_message(
107
+ MessageRole.SYSTEM,
108
+ f"{self.MEMORY_PREFIX} {KnowledgePoint.to_prompt(knowledge_points)}",
109
+ )
110
+ non_system_messages = messages.extract_non_system_messages()
111
+
112
+ # to meet the requirement of the deepseek-chat llm, remove the prefix of the messages if it is not the last message
113
+ for i in range(len(non_system_messages)):
114
+ if (
115
+ non_system_messages[i].role == MessageRole.ASSISTANT
116
+ and i != len(non_system_messages) - 1
117
+ ):
118
+ non_system_messages[i].metadata.pop("prefix", None)
119
+
120
+ # Extract first user message (required by GLM API - first non-system must be user)
121
+ # This prevents truncation from dropping the only user message
122
+ first_user_message = Messages()
123
+ other_messages = Messages()
124
+ found_first_user = False
125
+
126
+ for msg in non_system_messages:
127
+ if not found_first_user and msg.role == MessageRole.USER:
128
+ first_user_message.add_message(content=msg)
129
+ found_first_user = True
130
+ else:
131
+ other_messages.add_message(content=msg)
132
+
133
+ return system_messages, first_user_message, other_messages
134
+
135
+ @abstractmethod
136
+ def compress(
137
+ self,
138
+ context: Context,
139
+ messages: Messages,
140
+ constraints: "ContextConstraints",
141
+ **kwargs,
142
+ ) -> CompressionResult:
143
+ """Compress message list
144
+
145
+ Args:
146
+ context: Context
147
+ constraints: Compression constraints
148
+ messages: Original message list
149
+
150
+ Returns:
151
+ CompressionResult: Compression result
152
+ """
153
+ pass
154
+
155
+ def _date_in_system_message(self, system_messages: Messages) -> bool:
156
+ """Determine whether the system message contains a date. If it does, return True; otherwise, return False."""
157
+ for msg in system_messages:
158
+ if self.DATE_PREFIX in msg.content:
159
+ return True
160
+ return False
161
+
162
+ def _knowledge_in_system_message(self, system_messages: Messages) -> bool:
163
+ """Determine whether the system message contains knowledge. If it does, return True; otherwise, return False."""
164
+ for msg in system_messages:
165
+ if self.MEMORY_PREFIX in msg.content:
166
+ return True
167
+ return False
168
+
169
+ def _group_messages(self, messages: Messages) -> List[List]:
170
+ """Group messages to preserve tool_calls/tool pairing.
171
+
172
+ This method groups messages such that:
173
+ - An assistant message with tool_calls is grouped with all subsequent tool messages
174
+ that correspond to those tool_calls
175
+ - Other messages (user, assistant without tools) form single-message groups
176
+
177
+ This ensures that compression strategies never break the tool_calls/tool pairing
178
+ which would cause API errors like:
179
+ "messages with role 'tool' must be a response to a preceeding message with 'tool_calls'"
180
+
181
+ Args:
182
+ messages: The messages to group
183
+
184
+ Returns:
185
+ List of message groups, where each group is a list of SingleMessage objects.
186
+ Groups should be kept together or dropped together during compression.
187
+ """
188
+ groups = []
189
+ current_group = []
190
+ expecting_tools = False
191
+ expected_tool_ids = set()
192
+
193
+ for msg in messages:
194
+ is_tool_caller = (
195
+ msg.role == MessageRole.ASSISTANT and
196
+ (msg.tool_calls or getattr(msg, 'function_call', None))
197
+ )
198
+
199
+ if is_tool_caller:
200
+ # If we have a pending group, finalize it
201
+ if current_group:
202
+ groups.append(current_group)
203
+
204
+ # Start a new group with this assistant message
205
+ current_group = [msg]
206
+ expecting_tools = True
207
+
208
+ # Collect expected tool_call_ids
209
+ expected_tool_ids = set()
210
+ if msg.tool_calls:
211
+ for tc in msg.tool_calls:
212
+ tc_id = tc.get("id") if isinstance(tc, dict) else getattr(tc, "id", None)
213
+ if tc_id:
214
+ expected_tool_ids.add(tc_id)
215
+
216
+ elif msg.role == MessageRole.TOOL and expecting_tools:
217
+ # Add tool message to current group
218
+ current_group.append(msg)
219
+
220
+ # Check if we've received all expected tools
221
+ tool_call_id = msg.tool_call_id
222
+ if tool_call_id in expected_tool_ids:
223
+ expected_tool_ids.discard(tool_call_id)
224
+
225
+ # If all tools received, finalize the group
226
+ if not expected_tool_ids:
227
+ groups.append(current_group)
228
+ current_group = []
229
+ expecting_tools = False
230
+
231
+ else:
232
+ # Regular message (user, assistant without tools, etc.)
233
+ # Finalize any pending group first
234
+ if current_group:
235
+ groups.append(current_group)
236
+ current_group = []
237
+ expecting_tools = False
238
+ expected_tool_ids = set()
239
+
240
+ # Single-message group
241
+ groups.append([msg])
242
+
243
+ # Don't forget the last group if any
244
+ if current_group:
245
+ groups.append(current_group)
246
+
247
+ return groups
248
+
249
+ def _flatten_groups(self, groups: List[List]) -> Messages:
250
+ """Flatten message groups back into a Messages object.
251
+
252
+ Args:
253
+ groups: List of message groups
254
+
255
+ Returns:
256
+ Messages object containing all messages in order
257
+ """
258
+ result = Messages()
259
+ for group in groups:
260
+ for msg in group:
261
+ result.add_message(content=msg)
262
+ return result
263
+
264
+ def _estimate_group_tokens(self, group: List) -> int:
265
+ """Estimate the total tokens for a message group.
266
+
267
+ Args:
268
+ group: A list of SingleMessage objects
269
+
270
+ Returns:
271
+ Estimated token count for the entire group
272
+ """
273
+ temp_messages = Messages()
274
+ for msg in group:
275
+ temp_messages.add_message(content=msg)
276
+ return self.estimate_tokens(temp_messages)
277
+
278
+ def _count_images_in_messages(self, messages: Messages) -> int:
279
+ """Count total number of images in a Messages object.
280
+
281
+ Args:
282
+ messages: Messages object to count images in
283
+
284
+ Returns:
285
+ Total number of images
286
+ """
287
+ total_images = 0
288
+ for msg in messages:
289
+ if isinstance(msg.content, list):
290
+ total_images += sum(
291
+ 1 for block in msg.content
292
+ if block.get("type") == "image_url"
293
+ )
294
+ return total_images
295
+
296
+
297
+ class TruncationStrategy(CompressionStrategy):
298
+ """Simple truncation strategy: keep the latest messages"""
299
+
300
+ def get_name(self) -> str:
301
+ return "truncation"
302
+
303
+ def estimate_tokens(self, messages: Messages) -> int:
304
+ """Estimate the number of tokens, including both text and image content."""
305
+ total_tokens = 0
306
+ for message in messages:
307
+ content = message.content
308
+ if isinstance(content, str):
309
+ total_tokens += estimate_tokens_from_chars(content)
310
+ elif isinstance(content, list):
311
+ # Multimodal content
312
+ for block in content:
313
+ if block.get("type") == "text":
314
+ total_tokens += estimate_tokens_from_chars(block.get("text", ""))
315
+ elif block.get("type") == "image_url":
316
+ detail = block.get("image_url", {}).get("detail", "auto")
317
+ total_tokens += _default_image_config.estimate_tokens(detail=detail)
318
+ return total_tokens
319
+
320
+ def compress(
321
+ self,
322
+ context: Context,
323
+ constraints: "ContextConstraints",
324
+ messages: Messages,
325
+ **kwargs,
326
+ ) -> CompressionResult:
327
+ """Simple Truncation Compression"""
328
+ if not messages:
329
+ return CompressionResult(
330
+ compressed_messages=Messages(),
331
+ original_token_count=0,
332
+ compressed_token_count=0,
333
+ compression_ratio=1.0,
334
+ strategy_used=self.get_name(),
335
+ )
336
+
337
+ system_messages, first_user_message, other_messages = self.preparation(
338
+ context, messages, constraints, **kwargs
339
+ )
340
+ # Combine for original token count calculation
341
+ all_messages = Messages.combine_messages(
342
+ Messages.combine_messages(system_messages, first_user_message),
343
+ other_messages
344
+ )
345
+
346
+ original_tokens = self.estimate_tokens(all_messages)
347
+ max_tokens = constraints.max_input_tokens - constraints.reserve_output_tokens
348
+
349
+ if original_tokens <= max_tokens:
350
+ return CompressionResult(
351
+ compressed_messages=all_messages,
352
+ original_token_count=original_tokens,
353
+ compressed_token_count=original_tokens,
354
+ compression_ratio=1.0,
355
+ strategy_used=self.get_name(),
356
+ )
357
+
358
+ logger.debug(
359
+ f"TruncationStrategy compress: original_tokens={original_tokens}, max_tokens={max_tokens} max_input_tokens={constraints.max_input_tokens} reserve_output_tokens={constraints.reserve_output_tokens}"
360
+ )
361
+
362
+ # Calculate the number of tokens in system messages and first user message (both preserved)
363
+ system_tokens = self.estimate_tokens(system_messages)
364
+ first_user_tokens = self.estimate_tokens(first_user_message)
365
+ remaining_tokens = max_tokens - system_tokens - first_user_tokens
366
+
367
+ # Group messages to preserve tool_calls/tool pairing
368
+ groups = self._group_messages(other_messages)
369
+
370
+ # Keep from the latest groups backwards
371
+ compressed_groups = []
372
+ current_tokens = 0
373
+
374
+ # Iterate backwards through groups
375
+ for group in reversed(groups):
376
+ group_tokens = self._estimate_group_tokens(group)
377
+
378
+ if current_tokens + group_tokens <= remaining_tokens:
379
+ compressed_groups.insert(0, group)
380
+ current_tokens += group_tokens
381
+ elif len(compressed_groups) == 0:
382
+ # First group (latest) doesn't fit - try to truncate if it's a single text message
383
+ if len(group) == 1 and isinstance(group[0].content, str):
384
+ chars_to_keep = estimate_chars_from_tokens(remaining_tokens)
385
+ if chars_to_keep > 0:
386
+ compressed_msg = group[0].copy()
387
+ compressed_msg.content = group[0].content[:chars_to_keep]
388
+ compressed_groups.insert(0, [compressed_msg])
389
+ current_tokens += remaining_tokens
390
+ # Can't truncate (tool group or multimodal) - skip and stop
391
+ break
392
+ else:
393
+ # Doesn't fit and not the first one -> stop
394
+ break
395
+
396
+ # Flatten groups back to messages
397
+ compressed_other = self._flatten_groups(compressed_groups)
398
+
399
+ # Merge Results: system + first_user + compressed_other
400
+ len_system_messages = len(system_messages)
401
+ compressed_messages = system_messages.copy()
402
+ # Add first user message (preserved)
403
+ for msg in first_user_message:
404
+ compressed_messages.add_message(content=msg)
405
+ # Add compressed other messages
406
+ for msg in compressed_other:
407
+ compressed_messages.add_message(
408
+ content=msg
409
+ ) # Preserve original message with all attributes
410
+ final_tokens = self.estimate_tokens(compressed_messages)
411
+
412
+ # Count images for logging
413
+ original_images = self._count_images_in_messages(all_messages)
414
+ compressed_images = self._count_images_in_messages(compressed_messages)
415
+ dropped_images = original_images - compressed_images
416
+
417
+ # Enhanced logging with image information
418
+ if dropped_images > 0:
419
+ logger.info(
420
+ f"Compression dropped {dropped_images} image(s) "
421
+ f"(kept {compressed_images}/{original_images}) due to token limit"
422
+ )
423
+
424
+ return CompressionResult(
425
+ compressed_messages=compressed_messages,
426
+ original_token_count=original_tokens,
427
+ compressed_token_count=final_tokens,
428
+ compression_ratio=(
429
+ final_tokens / original_tokens if original_tokens > 0 else 1.0
430
+ ),
431
+ strategy_used=self.get_name(),
432
+ metadata={
433
+ "truncated_messages": len(other_messages) - len(compressed_other),
434
+ "preserved_system_messages": len_system_messages,
435
+ "preserved_first_user": len(first_user_message) > 0,
436
+ "original_images": original_images,
437
+ "compressed_images": compressed_images,
438
+ "dropped_images": dropped_images,
439
+ },
440
+ )
441
+
442
+
443
+ class SlidingWindowStrategy(CompressionStrategy):
444
+ """Sliding window strategy: retain a fixed number of recent messages"""
445
+
446
+ def __init__(self, window_size: int = 10):
447
+ self.window_size = window_size
448
+
449
+ def get_name(self) -> str:
450
+ return f"sliding_window_{self.window_size}"
451
+
452
+ def estimate_tokens(self, messages: Messages) -> int:
453
+ """Estimate the number of tokens, including both text and image content."""
454
+ total_tokens = 0
455
+ for message in messages:
456
+ content = message.content
457
+ if isinstance(content, str):
458
+ total_tokens += int(len(content) / CHINESE_CHAR_TO_TOKEN_RATIO)
459
+ elif isinstance(content, list):
460
+ # Multimodal content
461
+ for block in content:
462
+ if block.get("type") == "text":
463
+ total_tokens += int(len(block.get("text", "")) / CHINESE_CHAR_TO_TOKEN_RATIO)
464
+ elif block.get("type") == "image_url":
465
+ detail = block.get("image_url", {}).get("detail", "auto")
466
+ total_tokens += _default_image_config.estimate_tokens(detail=detail)
467
+ return total_tokens
468
+
469
+ def compress(
470
+ self,
471
+ context: Context,
472
+ constraints: "ContextConstraints",
473
+ messages: Messages,
474
+ **kwargs,
475
+ ) -> CompressionResult:
476
+ """Sliding Window Compression"""
477
+ if not messages:
478
+ return CompressionResult(
479
+ compressed_messages=Messages(),
480
+ original_token_count=0,
481
+ compressed_token_count=0,
482
+ compression_ratio=1.0,
483
+ strategy_used=self.get_name(),
484
+ )
485
+
486
+ original_tokens = self.estimate_tokens(messages)
487
+
488
+ # Separation system message and first user message
489
+ system_messages, first_user_message, other_messages = self.preparation(
490
+ context, messages, constraints, **kwargs
491
+ )
492
+
493
+ # Group messages to preserve tool_calls/tool pairing
494
+ groups = self._group_messages(other_messages)
495
+
496
+ # Get the last window_size GROUPS (not individual messages)
497
+ # This ensures we never break a tool_calls/tool pair
498
+ windowed_groups = (
499
+ groups[-self.window_size :]
500
+ if len(groups) > self.window_size
501
+ else groups
502
+ )
503
+
504
+ # Flatten groups back to messages
505
+ windowed_messages = self._flatten_groups(windowed_groups)
506
+
507
+ # Merge Results: system + first_user + windowed
508
+ compressed_messages = Messages.combine_messages(
509
+ Messages.combine_messages(system_messages, first_user_message),
510
+ windowed_messages
511
+ )
512
+ final_tokens = self.estimate_tokens(compressed_messages)
513
+
514
+ # Count images for logging
515
+ all_messages = Messages.combine_messages(
516
+ Messages.combine_messages(system_messages, first_user_message),
517
+ other_messages
518
+ )
519
+ original_images = self._count_images_in_messages(all_messages)
520
+ compressed_images = self._count_images_in_messages(compressed_messages)
521
+ dropped_images = original_images - compressed_images
522
+
523
+ # Enhanced logging with image information
524
+ if dropped_images > 0:
525
+ logger.info(
526
+ f"Sliding window dropped {dropped_images} image(s) "
527
+ f"(kept {compressed_images}/{original_images}) due to window size limit"
528
+ )
529
+
530
+ return CompressionResult(
531
+ compressed_messages=compressed_messages,
532
+ original_token_count=original_tokens,
533
+ compressed_token_count=final_tokens,
534
+ compression_ratio=(
535
+ final_tokens / original_tokens if original_tokens > 0 else 1.0
536
+ ),
537
+ strategy_used=self.get_name(),
538
+ metadata={
539
+ "window_size": self.window_size,
540
+ "groups_in_window": len(windowed_groups),
541
+ "messages_in_window": len(windowed_messages),
542
+ "preserved_system_messages": len(system_messages),
543
+ "preserved_first_user": len(first_user_message) > 0,
544
+ "original_images": original_images,
545
+ "compressed_images": compressed_images,
546
+ "dropped_images": dropped_images,
547
+ },
548
+ )
549
+
550
+
551
+ class LevelStrategy(CompressionStrategy):
552
+ """Level compression strategy: compress all messages except the last one using normal compression level"""
553
+
554
+ def get_name(self) -> str:
555
+ return "level"
556
+
557
+ def estimate_tokens(self, messages: Messages) -> int:
558
+ """Estimate the number of tokens, including both text and image content."""
559
+ total_tokens = 0
560
+ for message in messages:
561
+ content = message.content
562
+ if isinstance(content, str):
563
+ total_tokens += estimate_tokens_from_chars(content)
564
+ elif isinstance(content, list):
565
+ # Multimodal content
566
+ for block in content:
567
+ if block.get("type") == "text":
568
+ total_tokens += estimate_tokens_from_chars(block.get("text", ""))
569
+ elif block.get("type") == "image_url":
570
+ detail = block.get("image_url", {}).get("detail", "auto")
571
+ total_tokens += _default_image_config.estimate_tokens(detail=detail)
572
+ return total_tokens
573
+
574
+ def compress(
575
+ self,
576
+ context: Context,
577
+ constraints: "ContextConstraints",
578
+ messages: Messages,
579
+ **kwargs,
580
+ ) -> CompressionResult:
581
+ """Level compression: apply normal compression to all messages except the last one"""
582
+ if not messages:
583
+ return CompressionResult(
584
+ compressed_messages=Messages(),
585
+ original_token_count=0,
586
+ compressed_token_count=0,
587
+ compression_ratio=1.0,
588
+ strategy_used=self.get_name(),
589
+ )
590
+
591
+ original_tokens = self.estimate_tokens(messages)
592
+
593
+ # Get system, first user, and other messages
594
+ system_messages, first_user_message, other_messages = self.preparation(
595
+ context, messages, constraints, **kwargs
596
+ )
597
+
598
+ # Apply compression to all other messages except the last two
599
+ compressed_other = Messages()
600
+ for i, msg in enumerate(other_messages):
601
+ if i < len(other_messages) - 2: # Not the last two messages
602
+ # Create a copy and compress it
603
+ compressed_msg = msg.copy()
604
+ compressed_msg.compress(CompressLevel.NORMAL)
605
+ compressed_other.add_message(content=compressed_msg)
606
+ else: # Last two messages, keep original
607
+ compressed_other.add_message(content=msg)
608
+
609
+ # Combine results: system + first_user + compressed_other
610
+ compressed_messages = Messages.combine_messages(
611
+ Messages.combine_messages(system_messages, first_user_message),
612
+ compressed_other
613
+ )
614
+ final_tokens = self.estimate_tokens(compressed_messages)
615
+
616
+ return CompressionResult(
617
+ compressed_messages=compressed_messages,
618
+ original_token_count=original_tokens,
619
+ compressed_token_count=final_tokens,
620
+ compression_ratio=(
621
+ final_tokens / original_tokens if original_tokens > 0 else 1.0
622
+ ),
623
+ strategy_used=self.get_name(),
624
+ metadata={
625
+ "compressed_messages": (
626
+ len(other_messages) - 1 if len(other_messages) > 0 else 0
627
+ ),
628
+ "preserved_system_messages": len(system_messages),
629
+ "preserved_first_user": len(first_user_message) > 0,
630
+ "preserved_last_message": True,
631
+ },
632
+ )
633
+
634
+
635
+ class MessageCompressor:
636
+ """Message compressor: responsible for compressing and pruning messages under constraints (original ContextEngineer)."""
637
+
638
+ def __init__(
639
+ self,
640
+ config: Optional[ContextEngineerConfig] = None,
641
+ context: Context = None,
642
+ ):
643
+ # Compatible with two types of configuration
644
+ self.config = config or ContextEngineerConfig()
645
+ self.context = context
646
+ self.strategies = self._register_default_strategies()
647
+ logger.debug(
648
+ f"MessageCompressor initialized with strategy: {self.config.default_strategy}"
649
+ )
650
+
651
+ def _register_default_strategies(self) -> Dict[str, CompressionStrategy]:
652
+ """Register default compression strategy"""
653
+ strategies = {
654
+ "level": LevelStrategy(),
655
+ "truncation": TruncationStrategy(),
656
+ "sliding_window_5": SlidingWindowStrategy(5),
657
+ "sliding_window_10": SlidingWindowStrategy(10),
658
+ "sliding_window_20": SlidingWindowStrategy(20),
659
+ }
660
+ # Merge user-defined policy configurations
661
+ for name, strategy_config in self.config.strategy_configs.items():
662
+ # Here, corresponding strategy instances can be created based on strategy_config
663
+ # Skip temporarily, keep extension interface
664
+ pass
665
+ return strategies
666
+
667
+ def compress_messages(
668
+ self,
669
+ messages: Messages,
670
+ strategy_name: Optional[str] = None,
671
+ constraints: Optional["ContextConstraints"] = None,
672
+ model_config: Optional["LLMInstanceConfig"] = None,
673
+ **kwargs,
674
+ ) -> CompressionResult:
675
+ """Compress message context
676
+
677
+ Args:
678
+ messages: Original message list
679
+ strategy_name: Specifies the compression strategy to use, default uses the default strategy in configuration
680
+ constraints: Compression constraints, default uses constraints in configuration
681
+ model_config: Model configuration, used to automatically adjust constraints
682
+
683
+ Returns:
684
+ CompressionResult: Compression result
685
+ """
686
+ # Select Strategy
687
+ strategy_name = strategy_name or self.config.default_strategy
688
+ if strategy_name not in self.strategies:
689
+ logger.warning(f"Strategy '{strategy_name}' not found, using 'truncation'")
690
+ strategy_name = "truncation"
691
+ if strategy_name not in self.strategies:
692
+ # If there is no truncation at all, create a default one.
693
+ self.strategies["truncation"] = TruncationStrategy()
694
+
695
+ strategy = self.strategies[strategy_name]
696
+
697
+ # Select constraint conditions; if model_config is provided, adjust constraints according to model capabilities.
698
+ if constraints is None:
699
+ constraints = self.config.constraints
700
+
701
+ # Automatically adjust constraints according to model_config
702
+ if model_config is not None:
703
+ from dolphin.core.config.global_config import ContextConstraints
704
+
705
+ # Dynamically create constraints suitable for the current model
706
+ adjusted_constraints = ContextConstraints(
707
+ max_input_tokens=constraints.max_input_tokens,
708
+ reserve_output_tokens=model_config.max_tokens, # Use the model's max_tokens as reserved output
709
+ preserve_system=constraints.preserve_system,
710
+ )
711
+ constraints = adjusted_constraints
712
+
713
+ logger.debug(
714
+ f"Adjusted constraints for model {model_config.model_name}: "
715
+ f"max_input={constraints.max_input_tokens}, "
716
+ f"reserve_output={constraints.reserve_output_tokens}"
717
+ )
718
+
719
+ # Perform compression
720
+ result = strategy.compress(
721
+ context=self.context, messages=messages, constraints=constraints, **kwargs
722
+ )
723
+
724
+ # Log records
725
+ if result.compression_ratio < 1.0:
726
+ self.context.info(
727
+ f"Context compressed using {strategy_name}: "
728
+ f"{result.original_token_count} -> {result.compressed_token_count} tokens "
729
+ f"(ratio: {result.compression_ratio:.2f})"
730
+ )
731
+
732
+ return result
733
+
734
+ def register_strategy(self, name: str, strategy: CompressionStrategy):
735
+ """Register a new compression strategy"""
736
+ self.strategies[name] = strategy
737
+ logger.debug(f"Registered new compression strategy: {name}")
738
+
739
+ def get_available_strategies(self) -> List[str]:
740
+ """Get the list of available compression strategies"""
741
+ return list(self.strategies.keys())
742
+
743
+ def estimate_tokens(self, messages: Messages) -> int:
744
+ """Estimate the number of tokens in a message"""
745
+ # Token estimation method using default strategy
746
+ default_strategy = self.strategies.get(
747
+ self.config.default_strategy, TruncationStrategy()
748
+ )
749
+ return default_strategy.estimate_tokens(messages)