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,522 @@
1
+ """
2
+ Trajectory management for DolphinLanguage execution tracking.
3
+
4
+ This module provides the Trajectory class for recording and managing
5
+ execution trajectories with stage-based incremental saving.
6
+ """
7
+
8
+ from dolphin.core.context_engineer.core.context_manager import ContextManager
9
+ from dolphin.core.context_engineer.config.settings import BuildInBucket
10
+ import json
11
+ import os
12
+ from datetime import datetime
13
+ from typing import List, Dict, Any, Optional
14
+ from dolphin.core.common.enums import MessageRole, Messages
15
+ import logging
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class Trajectory:
21
+ """
22
+ Manages execution trajectory recording and persistence.
23
+
24
+ The Trajectory class is responsible for:
25
+ - Recording messages during execution
26
+ - Tracking execution stages (prompt, explore, tool, etc.)
27
+ - Maintaining message ranges for each stage
28
+
29
+ Attributes:
30
+ trajectory_path: Path to save the trajectory file
31
+ messages: Accumulated messages across all stages
32
+ stages: List of stage metadata with message ranges
33
+ """
34
+
35
+ def __init__(self, trajectory_path: Optional[str] = None, overwrite: bool = True):
36
+ """
37
+ Initialize a new Trajectory instance.
38
+
39
+ Args:
40
+ trajectory_path: Path where trajectory will be saved. If None,
41
+ trajectory recording is disabled.
42
+ overwrite: If True, delete existing trajectory file to start fresh.
43
+ If False, load and continue from existing trajectory.
44
+ """
45
+ self.trajectory_path = trajectory_path
46
+ self.messages: List[Dict[str, Any]] = []
47
+ self.stages: List[Dict[str, Any]] = []
48
+ self._loaded_from_file = False
49
+ self.current_stage_index: int = -1 # Track current stage index
50
+
51
+ # Handle existing trajectory file
52
+ if self.trajectory_path and os.path.exists(self.trajectory_path):
53
+ if overwrite:
54
+ # Delete existing file to start fresh
55
+ try:
56
+ os.remove(self.trajectory_path)
57
+ logger.debug(f"Removed existing trajectory file: {self.trajectory_path}")
58
+ except Exception as e:
59
+ logger.warning(f"Failed to remove existing trajectory file: {e}")
60
+ else:
61
+ # Load existing trajectory to continue
62
+ self._load_from_file()
63
+
64
+ def _load_from_file(self):
65
+ """Load existing trajectory from file to support continuation."""
66
+ try:
67
+ with open(self.trajectory_path, "r", encoding="utf-8") as f:
68
+ data = json.load(f)
69
+ self.messages = data.get("trajectory", [])
70
+ self.stages = data.get("stages", [])
71
+ self._loaded_from_file = True
72
+ # Restore current_stage_index from loaded stages
73
+ if self.stages:
74
+ self.current_stage_index = max(stage.get("index", -1) for stage in self.stages)
75
+ else:
76
+ self.current_stage_index = -1
77
+
78
+ logger.debug(f"Loaded existing trajectory with {len(self.messages)} messages and {len(self.stages)} stages")
79
+ except Exception as e:
80
+ logger.warning(f"Failed to load existing trajectory file: {e}")
81
+
82
+ def is_enabled(self) -> bool:
83
+ """Check if trajectory recording is enabled."""
84
+ return self.trajectory_path is not None
85
+
86
+ def begin_stage(self, context_manager) -> None:
87
+ """Mark the start of a stage.
88
+
89
+ This method is currently mainly used to maintain compatibility with the old interface, performing only type checking and logging,
90
+ and no longer maintaining the baseline message count required for "incremental slicing".
91
+ """
92
+ try:
93
+ from dolphin.core.context_engineer.core.context_manager import ContextManager
94
+ if not isinstance(context_manager, ContextManager):
95
+ logger.debug(f"begin_stage ignored for invalid context_manager: {type(context_manager)}")
96
+ return
97
+ # 保留接口以兼容旧逻辑,这里不再记录基线计数
98
+ logger.debug("Trajectory begin_stage called with valid ContextManager")
99
+ except Exception as e:
100
+ logger.warning(f"begin_stage failed: {e}")
101
+
102
+ def _get_message_signature(self, msg) -> str:
103
+ """
104
+ Generate a unique signature for a message based on its content.
105
+
106
+ This signature is used for deduplication and is based on key message fields
107
+ rather than Python object id, ensuring consistent identification across
108
+ message copies.
109
+
110
+ Args:
111
+ msg: Message object to generate signature for
112
+
113
+ Returns:
114
+ MD5 hash string representing the message signature
115
+ """
116
+ import hashlib
117
+ # Use key fields to generate signature
118
+ # Include role, content, timestamp, and tool_call_id for uniqueness
119
+ signature_data = f"{msg.role.value}|{msg.content}|{msg.timestamp}|{msg.tool_call_id or ''}"
120
+ return hashlib.md5(signature_data.encode()).hexdigest()
121
+
122
+ def _get_history_message_signatures(self, context_manager) -> set:
123
+ """
124
+ Identify message signatures from history buckets.
125
+
126
+ Uses content-based signatures instead of object ids to correctly identify
127
+ history messages even if they have been copied or recreated.
128
+
129
+ Args:
130
+ context_manager: ContextManager instance to check for history buckets
131
+
132
+ Returns:
133
+ Set of message signature strings from history buckets
134
+ """
135
+ history_signatures = set()
136
+ # Check both standard history and conversation_history buckets
137
+ for bucket_name in [BuildInBucket.HISTORY.value, "conversation_history"]:
138
+ bucket = context_manager.state.buckets.get(bucket_name)
139
+ if bucket and isinstance(bucket.content, Messages):
140
+ history_signatures.update(
141
+ self._get_message_signature(m) for m in bucket.content.get_messages()
142
+ )
143
+ return history_signatures
144
+
145
+ def _convert_message_to_dict(self, msg, stage_name: str, user_id: str, model: Optional[str] = None) -> Dict[str, Any]:
146
+ """Convert a single Message object to a dictionary."""
147
+ message_dict = {
148
+ "role": msg.role.value,
149
+ "content": msg.content,
150
+ "timestamp": msg.timestamp,
151
+ "user_id": msg.user_id or user_id,
152
+ "tool_calls": msg.tool_calls,
153
+ "tool_call_id": msg.tool_call_id,
154
+ "metadata": msg.metadata,
155
+ "stage": stage_name,
156
+ }
157
+ # If model info is present, add it only to assistant messages
158
+ if model and msg.role == MessageRole.ASSISTANT:
159
+ message_dict["model"] = model
160
+ return message_dict
161
+
162
+ def finalize_stage(self,
163
+ stage_name: str,
164
+ stage_index: int,
165
+ context_manager :ContextManager,
166
+ tools: List[Dict[str, Any]],
167
+ user_id: str = "",
168
+ model: Optional[str] = None):
169
+ """
170
+ Finalize a stage by adding messages to trajectory and saving to file.
171
+
172
+ This method:
173
+ 1. Gets merged messages from context_manager
174
+ 2. Converts messages to dictionaries
175
+ 3. Appends them to the accumulated trajectory
176
+ 4. Records stage metadata with message range
177
+ 5. Saves the updated trajectory to file
178
+
179
+ Args:
180
+ stage_name: Name of the stage (e.g., "prompt", "explore", "tool")
181
+ stage_index: Index of this stage execution (e.g., 1st prompt, 2nd prompt)
182
+ context_manager: ContextManager instance to get messages from
183
+ tools: Tool schemas available in this context
184
+ user_id: User ID for message attribution
185
+ """
186
+ if not self.is_enabled():
187
+ return
188
+
189
+ try:
190
+ # 1. Get all messages from context manager
191
+ # Use to_dph_messages() to get merged messages, consistent with LLM calls
192
+ all_messages_obj = context_manager.to_dph_messages()
193
+ all_messages = all_messages_obj.get_messages()
194
+
195
+ # 2. Determine stage messages (handle explore stage logic)
196
+ # Default: Record all messages in full according to the LLM's actual order
197
+ stage_messages = all_messages
198
+
199
+ # For explore: if the explore phase has already existed before,
200
+ # Then it is considered as a subsequent round of the same explore session, and system is no longer recorded repeatedly.
201
+ if stage_name == "explore":
202
+ has_prev_explore = any(
203
+ stage.get("stage") == "explore" for stage in self.stages
204
+ )
205
+ if has_prev_explore:
206
+ stage_messages = [
207
+ m for m in all_messages if m.role != MessageRole.SYSTEM
208
+ ]
209
+
210
+ # 3. Identify history messages using content-based signatures
211
+ history_signatures = self._get_history_message_signatures(context_manager)
212
+
213
+ # 4. Process messages: convert to dicts and separate new vs history
214
+ new_messages_data = []
215
+
216
+ for i, msg in enumerate(stage_messages):
217
+ msg_data = self._convert_message_to_dict(msg, stage_name, user_id, model)
218
+ msg_signature = self._get_message_signature(msg)
219
+ is_history = msg_signature in history_signatures
220
+
221
+ # Only new messages (not from history)
222
+ if not is_history:
223
+ new_messages_data.append(msg_data)
224
+
225
+ # 5. Update global trajectory state
226
+ # Calculate the message range of the current stage in the global trajectory
227
+ start_index = len(self.messages)
228
+ self.messages.extend(new_messages_data)
229
+
230
+ # message_range refers to the range within the global trajectory
231
+ # The range corresponds to the *new* messages added in this stage.
232
+ # If no new messages were added, range is empty [start, start].
233
+ message_range = [start_index, len(self.messages)]
234
+
235
+ # 6. Record stage metadata with message range
236
+ # stage.messages only contains new messages, matching message_range
237
+ stage_info = {
238
+ "stage": stage_name,
239
+ "index": stage_index,
240
+ "timestamp": datetime.now().isoformat(),
241
+ "message_range": message_range,
242
+ "messages": new_messages_data, # Only new messages, matches message_range
243
+ }
244
+ if model:
245
+ stage_info["model"] = model
246
+ self.stages.append(stage_info)
247
+
248
+ # Update current_stage_index to track the latest stage
249
+ self.current_stage_index = stage_index
250
+
251
+ logger.debug(f"Finalized stage {stage_name} {stage_index}: range {message_range}, new messages: {len(new_messages_data)}")
252
+
253
+ # 7. Save to file
254
+ self._save_to_file(tools)
255
+
256
+ except Exception as e:
257
+ logger.error(f"Failed to finalize stage {stage_name}: {e}", exc_info=True)
258
+
259
+ def _save_to_file(self, tools: List[Dict[str, Any]]):
260
+ """
261
+ Save trajectory to file.
262
+
263
+ Args:
264
+ tools: Tool schemas to include in the saved trajectory
265
+ """
266
+ if not self.trajectory_path:
267
+ return
268
+
269
+ try:
270
+ # Ensure directory exists
271
+ dir_name = os.path.dirname(self.trajectory_path)
272
+ if dir_name:
273
+ os.makedirs(dir_name, exist_ok=True)
274
+
275
+ # Build trajectory data with original contract format
276
+ trajectory_data = {
277
+ "trajectory": self.messages,
278
+ "tools": tools,
279
+ "stages": self.stages
280
+ }
281
+
282
+ # Write to file
283
+ with open(self.trajectory_path, "w", encoding="utf-8") as f:
284
+ json.dump(trajectory_data, f, ensure_ascii=False, indent=2)
285
+
286
+ logger.debug(f"Saved trajectory to {self.trajectory_path}: {len(self.messages)} messages, {len(self.stages)} stages")
287
+
288
+ except Exception as e:
289
+ logger.error(f"Failed to save trajectory to {self.trajectory_path}: {e}", exc_info=True)
290
+
291
+ def get_summary(self) -> Dict[str, Any]:
292
+ """
293
+ Get a summary of the current trajectory state.
294
+
295
+ Returns:
296
+ Dictionary containing trajectory statistics
297
+ """
298
+ return {
299
+ "total_messages": len(self.messages),
300
+ "total_stages": len(self.stages),
301
+ "trajectory_path": self.trajectory_path,
302
+ "loaded_from_file": self._loaded_from_file,
303
+ "current_stage_index": self.current_stage_index,
304
+ }
305
+
306
+ @staticmethod
307
+ def save_simple(messages: List,
308
+ tools: List[Dict[str, Any]],
309
+ file_path: str,
310
+ pretty_format: bool = False,
311
+ user_id: str = ""):
312
+ """
313
+ Save a simple trajectory without stages (static method for legacy compatibility).
314
+
315
+ This is a utility method for saving messages in a simple format without
316
+ stage metadata. Used primarily for backward compatibility.
317
+
318
+ Args:
319
+ messages: List of Message objects to save
320
+ tools: Tool schemas to include
321
+ file_path: Path where to save the trajectory
322
+ pretty_format: If True, save in human-readable text format
323
+ user_id: User ID for message attribution
324
+
325
+ Raises:
326
+ Exception: If saving fails
327
+ """
328
+ try:
329
+ # Convert Message objects to dictionaries
330
+ messages_data = []
331
+ for msg in messages:
332
+ messages_data.append({
333
+ "role": msg.role.value,
334
+ "content": msg.content,
335
+ "timestamp": msg.timestamp,
336
+ "user_id": msg.user_id or user_id,
337
+ "tool_calls": msg.tool_calls,
338
+ "tool_call_id": msg.tool_call_id,
339
+ "metadata": msg.metadata,
340
+ })
341
+
342
+ if not messages_data:
343
+ logger.warning("No messages to save")
344
+ return
345
+
346
+ # Build trajectory data
347
+ trajectory_data = {
348
+ "trajectory": messages_data,
349
+ "tools": tools,
350
+ }
351
+
352
+ # Ensure directory exists
353
+ dir_name = os.path.dirname(file_path)
354
+ if dir_name:
355
+ os.makedirs(dir_name, exist_ok=True)
356
+
357
+ # Write to file
358
+ with open(file_path, "w", encoding="utf-8") as f:
359
+ if pretty_format:
360
+ # Save in human-readable text format
361
+ formatted_text = Trajectory._format_trajectory_pretty(trajectory_data)
362
+ f.write(formatted_text)
363
+ else:
364
+ json.dump(trajectory_data, f, ensure_ascii=False, indent=2)
365
+
366
+ logger.debug(f"Saved {len(messages_data)} messages to {file_path}")
367
+
368
+ except Exception as e:
369
+ logger.error(f"Failed to save simple trajectory to {file_path}: {e}")
370
+ raise
371
+
372
+ @staticmethod
373
+ def _format_trajectory_pretty(trajectory_data: Dict[str, Any]) -> str:
374
+ """Format trajectory data in a human-readable text format"""
375
+ lines = []
376
+
377
+ # Header
378
+ lines.append("=" * 80)
379
+ lines.append("TRAJECTORY SECTION")
380
+ lines.append("=" * 80)
381
+ lines.append("")
382
+
383
+ # Process each message
384
+ for i, message in enumerate(trajectory_data["trajectory"]):
385
+ lines.append(f"[{i+1:3d}] {message['role'].upper()}")
386
+ lines.append(f" Timestamp: {Trajectory._format_timestamp(message['timestamp'])}")
387
+ lines.append(f" User ID: {message.get('user_id', 'N/A')}")
388
+
389
+ # Format content based on role
390
+ if message['role'] == 'system':
391
+ lines.append(" Content:")
392
+ lines.extend(Trajectory._format_content_lines(message['content']))
393
+ elif message['role'] in ['user', 'assistant']:
394
+ lines.append(" Content:")
395
+ lines.extend(Trajectory._format_content_lines(message['content']))
396
+
397
+ # Tool calls for assistant
398
+ if message['role'] == 'assistant' and message.get('tool_calls'):
399
+ for tool_call in message['tool_calls']:
400
+ lines.extend(Trajectory._format_tool_call_lines(tool_call))
401
+
402
+ # Tool response
403
+ if message['role'] == 'tool':
404
+ lines.extend(Trajectory._format_tool_response_lines(message['content']))
405
+
406
+ # Tool call ID
407
+ if message.get('tool_call_id'):
408
+ lines.append(f" Tool Call ID: {message['tool_call_id']}")
409
+
410
+ # Metadata
411
+ if message.get('metadata'):
412
+ lines.append(" Metadata:")
413
+ metadata = message['metadata']
414
+ if isinstance(metadata, dict):
415
+ for key, value in metadata.items():
416
+ lines.append(f" {key}: {value}")
417
+
418
+ lines.append("-" * 60)
419
+
420
+ # Tools section
421
+ lines.append("")
422
+ lines.append("=" * 80)
423
+ lines.append("TOOLS SECTION")
424
+ lines.append("=" * 80)
425
+ lines.append("")
426
+
427
+ for i, tool in enumerate(trajectory_data.get("tools", [])):
428
+ function = tool.get('function', {})
429
+ lines.append(f"[{i+1}] {tool.get('type', 'unknown').upper()} Tool")
430
+ lines.append(f" Name: {function.get('name', 'N/A')}")
431
+ lines.append(f" Description: {function.get('description', 'N/A')}")
432
+
433
+ # Parameters
434
+ parameters = function.get('parameters', {})
435
+ if parameters:
436
+ lines.append(" Parameters:")
437
+ properties = parameters.get('properties', {})
438
+ required = parameters.get('required', [])
439
+
440
+ for param_name, param_info in properties.items():
441
+ required_mark = " (required)" if param_name in required else ""
442
+ param_type = param_info.get('type', 'unknown')
443
+ param_desc = param_info.get('description', 'N/A')
444
+ lines.append(f" - {param_name}{required_mark}: {param_type}")
445
+ lines.append(f" {param_desc}")
446
+
447
+ lines.append("-" * 60)
448
+
449
+ return "\n".join(lines)
450
+
451
+ @staticmethod
452
+ def _format_timestamp(timestamp: str) -> str:
453
+ """Format timestamp to HH:MM:SS"""
454
+ if 'T' in timestamp:
455
+ return timestamp.split('T')[1].split('.')[0]
456
+ return timestamp
457
+
458
+ @staticmethod
459
+ def _format_content_lines(content: str) -> List[str]:
460
+ """Format content into readable lines"""
461
+ try:
462
+ # Try to parse as JSON
463
+ parsed = json.loads(content)
464
+ if isinstance(parsed, dict):
465
+ lines = []
466
+ for key, value in parsed.items():
467
+ if key == 'content':
468
+ # Game state information, keep as is
469
+ lines.append(f" {key}: {value}")
470
+ elif key == 'metadata':
471
+ lines.append(f" {key}:")
472
+ if isinstance(value, dict):
473
+ for k, v in value.items():
474
+ lines.append(f" {k}: {v}")
475
+ else:
476
+ lines.append(f" {key}: {value}")
477
+ return lines
478
+ else:
479
+ return [f" {content}"]
480
+ except json.JSONDecodeError:
481
+ # Not JSON, keep as is
482
+ return [f" {content}"]
483
+
484
+ @staticmethod
485
+ def _format_tool_call_lines(tool_call: Dict[str, Any]) -> List[str]:
486
+ """Format tool call into readable lines"""
487
+ lines = [" Tool Call:"]
488
+ if tool_call:
489
+ lines.append(f" ID: {tool_call.get('id', 'N/A')}")
490
+ lines.append(f" Type: {tool_call.get('type', 'N/A')}")
491
+ function = tool_call.get('function', {})
492
+ lines.append(f" Function: {function.get('name', 'N/A')}")
493
+ try:
494
+ args = json.loads(function.get('arguments', '{}'))
495
+ lines.append(" Arguments:")
496
+ if isinstance(args, dict):
497
+ for k, v in args.items():
498
+ if k == 'cards' and isinstance(v, list):
499
+ lines.append(f" {k}: [{', '.join(v)}]")
500
+ else:
501
+ lines.append(f" {k}: {v}")
502
+ else:
503
+ lines.append(f" {args}")
504
+ except json.JSONDecodeError:
505
+ lines.append(f" Arguments: {function.get('arguments', 'N/A')}")
506
+ return lines
507
+
508
+ @staticmethod
509
+ def _format_tool_response_lines(content: str) -> List[str]:
510
+ """Format tool response into readable lines"""
511
+ lines = [" Tool Response:"]
512
+ if content:
513
+ try:
514
+ parsed = json.loads(content)
515
+ if isinstance(parsed, dict):
516
+ for key, value in parsed.items():
517
+ lines.append(f" {key}: {value}")
518
+ else:
519
+ lines.append(f" {content}")
520
+ except json.JSONDecodeError:
521
+ lines.append(f" {content}")
522
+ return lines
@@ -0,0 +1,9 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Utils 模块 - 核心工具"""
3
+
4
+ from dolphin.core.utils.cache_kv import CacheKV
5
+ from dolphin.core.utils.tools import *
6
+
7
+ __all__ = [
8
+ "CacheKV",
9
+ ]