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