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,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)
|