camel-ai 0.2.73a4__py3-none-any.whl → 0.2.80a2__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.
- camel/__init__.py +1 -1
- camel/agents/_utils.py +38 -0
- camel/agents/chat_agent.py +2217 -519
- camel/agents/mcp_agent.py +30 -27
- camel/configs/__init__.py +15 -0
- camel/configs/aihubmix_config.py +88 -0
- camel/configs/amd_config.py +70 -0
- camel/configs/cometapi_config.py +104 -0
- camel/configs/minimax_config.py +93 -0
- camel/configs/nebius_config.py +103 -0
- camel/data_collectors/alpaca_collector.py +15 -6
- camel/datasets/base_generator.py +39 -10
- camel/environments/single_step.py +28 -3
- camel/environments/tic_tac_toe.py +1 -1
- camel/interpreters/__init__.py +2 -0
- camel/interpreters/docker/Dockerfile +3 -12
- camel/interpreters/e2b_interpreter.py +34 -1
- camel/interpreters/microsandbox_interpreter.py +395 -0
- camel/loaders/__init__.py +11 -2
- camel/loaders/chunkr_reader.py +9 -0
- camel/memories/agent_memories.py +48 -4
- camel/memories/base.py +26 -0
- camel/memories/blocks/chat_history_block.py +122 -4
- camel/memories/context_creators/score_based.py +25 -384
- camel/memories/records.py +88 -8
- camel/messages/base.py +153 -34
- camel/models/__init__.py +10 -0
- camel/models/aihubmix_model.py +83 -0
- camel/models/aiml_model.py +1 -16
- camel/models/amd_model.py +101 -0
- camel/models/anthropic_model.py +6 -19
- camel/models/aws_bedrock_model.py +2 -33
- camel/models/azure_openai_model.py +114 -89
- camel/models/base_audio_model.py +3 -1
- camel/models/base_model.py +32 -14
- camel/models/cohere_model.py +1 -16
- camel/models/cometapi_model.py +83 -0
- camel/models/crynux_model.py +1 -16
- camel/models/deepseek_model.py +1 -16
- camel/models/fish_audio_model.py +6 -0
- camel/models/gemini_model.py +36 -18
- camel/models/groq_model.py +1 -17
- camel/models/internlm_model.py +1 -16
- camel/models/litellm_model.py +1 -16
- camel/models/lmstudio_model.py +1 -17
- camel/models/minimax_model.py +83 -0
- camel/models/mistral_model.py +1 -16
- camel/models/model_factory.py +27 -1
- camel/models/modelscope_model.py +1 -16
- camel/models/moonshot_model.py +105 -24
- camel/models/nebius_model.py +83 -0
- camel/models/nemotron_model.py +0 -5
- camel/models/netmind_model.py +1 -16
- camel/models/novita_model.py +1 -16
- camel/models/nvidia_model.py +1 -16
- camel/models/ollama_model.py +4 -19
- camel/models/openai_compatible_model.py +62 -41
- camel/models/openai_model.py +62 -57
- camel/models/openrouter_model.py +1 -17
- camel/models/ppio_model.py +1 -16
- camel/models/qianfan_model.py +1 -16
- camel/models/qwen_model.py +1 -16
- camel/models/reka_model.py +1 -16
- camel/models/samba_model.py +34 -47
- camel/models/sglang_model.py +64 -31
- camel/models/siliconflow_model.py +1 -16
- camel/models/stub_model.py +0 -4
- camel/models/togetherai_model.py +1 -16
- camel/models/vllm_model.py +1 -16
- camel/models/volcano_model.py +0 -17
- camel/models/watsonx_model.py +1 -16
- camel/models/yi_model.py +1 -16
- camel/models/zhipuai_model.py +60 -16
- camel/parsers/__init__.py +18 -0
- camel/parsers/mcp_tool_call_parser.py +176 -0
- camel/retrievers/auto_retriever.py +1 -0
- camel/runtimes/daytona_runtime.py +11 -12
- camel/societies/__init__.py +2 -0
- camel/societies/workforce/__init__.py +2 -0
- camel/societies/workforce/events.py +122 -0
- camel/societies/workforce/prompts.py +146 -66
- camel/societies/workforce/role_playing_worker.py +15 -11
- camel/societies/workforce/single_agent_worker.py +302 -65
- camel/societies/workforce/structured_output_handler.py +30 -18
- camel/societies/workforce/task_channel.py +163 -27
- camel/societies/workforce/utils.py +107 -13
- camel/societies/workforce/workflow_memory_manager.py +772 -0
- camel/societies/workforce/workforce.py +1949 -579
- camel/societies/workforce/workforce_callback.py +74 -0
- camel/societies/workforce/workforce_logger.py +168 -145
- camel/societies/workforce/workforce_metrics.py +33 -0
- camel/storages/key_value_storages/json.py +15 -2
- camel/storages/key_value_storages/mem0_cloud.py +48 -47
- camel/storages/object_storages/google_cloud.py +1 -1
- camel/storages/vectordb_storages/oceanbase.py +13 -13
- camel/storages/vectordb_storages/qdrant.py +3 -3
- camel/storages/vectordb_storages/tidb.py +8 -6
- camel/tasks/task.py +4 -3
- camel/toolkits/__init__.py +20 -7
- camel/toolkits/aci_toolkit.py +45 -0
- camel/toolkits/base.py +6 -4
- camel/toolkits/code_execution.py +28 -1
- camel/toolkits/context_summarizer_toolkit.py +684 -0
- camel/toolkits/dappier_toolkit.py +5 -1
- camel/toolkits/dingtalk.py +1135 -0
- camel/toolkits/edgeone_pages_mcp_toolkit.py +11 -31
- camel/toolkits/excel_toolkit.py +1 -1
- camel/toolkits/{file_write_toolkit.py → file_toolkit.py} +430 -36
- camel/toolkits/function_tool.py +13 -3
- camel/toolkits/github_toolkit.py +104 -17
- camel/toolkits/gmail_toolkit.py +1839 -0
- camel/toolkits/google_calendar_toolkit.py +38 -4
- camel/toolkits/google_drive_mcp_toolkit.py +12 -31
- camel/toolkits/hybrid_browser_toolkit/config_loader.py +15 -0
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +77 -8
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +884 -88
- camel/toolkits/hybrid_browser_toolkit/installer.py +203 -0
- camel/toolkits/hybrid_browser_toolkit/ts/package-lock.json +5 -612
- camel/toolkits/hybrid_browser_toolkit/ts/package.json +0 -1
- camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +959 -89
- camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +9 -2
- camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +281 -213
- camel/toolkits/hybrid_browser_toolkit/ts/src/parent-child-filter.ts +226 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/snapshot-parser.ts +219 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/som-screenshot-injected.ts +543 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +23 -3
- camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +72 -7
- camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +582 -132
- camel/toolkits/hybrid_browser_toolkit_py/actions.py +158 -0
- camel/toolkits/hybrid_browser_toolkit_py/browser_session.py +55 -8
- camel/toolkits/hybrid_browser_toolkit_py/config_loader.py +43 -0
- camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +321 -8
- camel/toolkits/hybrid_browser_toolkit_py/snapshot.py +10 -4
- camel/toolkits/hybrid_browser_toolkit_py/unified_analyzer.js +45 -4
- camel/toolkits/{openai_image_toolkit.py → image_generation_toolkit.py} +151 -53
- camel/toolkits/klavis_toolkit.py +5 -1
- camel/toolkits/markitdown_toolkit.py +27 -1
- camel/toolkits/math_toolkit.py +64 -10
- camel/toolkits/mcp_toolkit.py +366 -71
- camel/toolkits/memory_toolkit.py +5 -1
- camel/toolkits/message_integration.py +18 -13
- camel/toolkits/minimax_mcp_toolkit.py +195 -0
- camel/toolkits/note_taking_toolkit.py +19 -10
- camel/toolkits/notion_mcp_toolkit.py +16 -26
- camel/toolkits/openbb_toolkit.py +5 -1
- camel/toolkits/origene_mcp_toolkit.py +8 -49
- camel/toolkits/playwright_mcp_toolkit.py +12 -31
- camel/toolkits/resend_toolkit.py +168 -0
- camel/toolkits/search_toolkit.py +264 -91
- camel/toolkits/slack_toolkit.py +64 -10
- camel/toolkits/terminal_toolkit/__init__.py +18 -0
- camel/toolkits/terminal_toolkit/terminal_toolkit.py +957 -0
- camel/toolkits/terminal_toolkit/utils.py +532 -0
- camel/toolkits/vertex_ai_veo_toolkit.py +590 -0
- camel/toolkits/video_analysis_toolkit.py +17 -11
- camel/toolkits/wechat_official_toolkit.py +483 -0
- camel/toolkits/zapier_toolkit.py +5 -1
- camel/types/__init__.py +2 -2
- camel/types/enums.py +274 -7
- camel/types/openai_types.py +2 -2
- camel/types/unified_model_type.py +15 -0
- camel/utils/commons.py +36 -5
- camel/utils/constants.py +3 -0
- camel/utils/context_utils.py +1003 -0
- camel/utils/mcp.py +138 -4
- camel/utils/token_counting.py +43 -20
- {camel_ai-0.2.73a4.dist-info → camel_ai-0.2.80a2.dist-info}/METADATA +223 -83
- {camel_ai-0.2.73a4.dist-info → camel_ai-0.2.80a2.dist-info}/RECORD +170 -141
- camel/loaders/pandas_reader.py +0 -368
- camel/toolkits/openai_agent_toolkit.py +0 -135
- camel/toolkits/terminal_toolkit.py +0 -1550
- {camel_ai-0.2.73a4.dist-info → camel_ai-0.2.80a2.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.73a4.dist-info → camel_ai-0.2.80a2.dist-info}/licenses/LICENSE +0 -0
|
@@ -11,41 +11,24 @@
|
|
|
11
11
|
# See the License for the specific language governing permissions and
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
14
|
-
from collections import defaultdict
|
|
15
|
-
from typing import Dict, List, Optional, Tuple
|
|
16
14
|
|
|
17
|
-
from
|
|
15
|
+
from typing import List, Optional, Tuple
|
|
18
16
|
|
|
19
|
-
from camel.logger import get_logger
|
|
20
17
|
from camel.memories.base import BaseContextCreator
|
|
21
18
|
from camel.memories.records import ContextRecord
|
|
22
|
-
from camel.messages import
|
|
19
|
+
from camel.messages import OpenAIMessage
|
|
23
20
|
from camel.types.enums import OpenAIBackendRole
|
|
24
21
|
from camel.utils import BaseTokenCounter
|
|
25
22
|
|
|
26
|
-
logger = get_logger(__name__)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class _ContextUnit(BaseModel):
|
|
30
|
-
idx: int
|
|
31
|
-
record: ContextRecord
|
|
32
|
-
num_tokens: int
|
|
33
|
-
|
|
34
23
|
|
|
35
24
|
class ScoreBasedContextCreator(BaseContextCreator):
|
|
36
|
-
r"""A
|
|
37
|
-
from :obj:`BaseContextCreator`.
|
|
38
|
-
|
|
39
|
-
This class provides a strategy to generate a conversational context from
|
|
40
|
-
a list of chat history records while ensuring the total token count of
|
|
41
|
-
the context does not exceed a specified limit. It prunes messages based
|
|
42
|
-
on their score if the total token count exceeds the limit.
|
|
25
|
+
r"""A context creation strategy that orders records chronologically.
|
|
43
26
|
|
|
44
27
|
Args:
|
|
45
|
-
token_counter (BaseTokenCounter):
|
|
46
|
-
|
|
47
|
-
token_limit (int):
|
|
48
|
-
|
|
28
|
+
token_counter (BaseTokenCounter): Token counter instance used to
|
|
29
|
+
compute the combined token count of the returned messages.
|
|
30
|
+
token_limit (int): Retained for API compatibility. No longer used to
|
|
31
|
+
filter records.
|
|
49
32
|
"""
|
|
50
33
|
|
|
51
34
|
def __init__(
|
|
@@ -66,376 +49,34 @@ class ScoreBasedContextCreator(BaseContextCreator):
|
|
|
66
49
|
self,
|
|
67
50
|
records: List[ContextRecord],
|
|
68
51
|
) -> Tuple[List[OpenAIMessage], int]:
|
|
69
|
-
|
|
70
|
-
token limits.
|
|
71
|
-
|
|
72
|
-
Key strategies:
|
|
73
|
-
1. System message is always prioritized and preserved
|
|
74
|
-
2. Truncation removes low-score messages first
|
|
75
|
-
3. Final output maintains chronological order and in history memory,
|
|
76
|
-
the score of each message decreases according to keep_rate. The
|
|
77
|
-
newer the message, the higher the score.
|
|
78
|
-
4. Tool calls and their responses are kept together to maintain
|
|
79
|
-
API compatibility
|
|
80
|
-
|
|
81
|
-
Args:
|
|
82
|
-
records (List[ContextRecord]): List of context records with scores
|
|
83
|
-
and timestamps.
|
|
84
|
-
|
|
85
|
-
Returns:
|
|
86
|
-
Tuple[List[OpenAIMessage], int]:
|
|
87
|
-
- Ordered list of OpenAI messages
|
|
88
|
-
- Total token count of the final context
|
|
89
|
-
|
|
90
|
-
Raises:
|
|
91
|
-
RuntimeError: If system message alone exceeds token limit
|
|
92
|
-
"""
|
|
93
|
-
# ======================
|
|
94
|
-
# 1. System Message Handling
|
|
95
|
-
# ======================
|
|
96
|
-
system_unit, regular_units = self._extract_system_message(records)
|
|
97
|
-
system_tokens = system_unit.num_tokens if system_unit else 0
|
|
52
|
+
"""Returns messages sorted by timestamp and their total token count."""
|
|
98
53
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
raise RuntimeError(
|
|
102
|
-
f"System message alone exceeds token limit"
|
|
103
|
-
f": {system_tokens} > {self.token_limit}",
|
|
104
|
-
system_tokens,
|
|
105
|
-
)
|
|
54
|
+
system_record: Optional[ContextRecord] = None
|
|
55
|
+
remaining_records: List[ContextRecord] = []
|
|
106
56
|
|
|
107
|
-
|
|
108
|
-
# 2. Deduplication & Initial Processing
|
|
109
|
-
# ======================
|
|
110
|
-
seen_uuids = set()
|
|
111
|
-
if system_unit:
|
|
112
|
-
seen_uuids.add(system_unit.record.memory_record.uuid)
|
|
113
|
-
|
|
114
|
-
# Process non-system messages with deduplication
|
|
115
|
-
for idx, record in enumerate(records):
|
|
57
|
+
for record in records:
|
|
116
58
|
if (
|
|
117
|
-
|
|
59
|
+
system_record is None
|
|
60
|
+
and record.memory_record.role_at_backend
|
|
118
61
|
== OpenAIBackendRole.SYSTEM
|
|
119
62
|
):
|
|
63
|
+
system_record = record
|
|
120
64
|
continue
|
|
121
|
-
|
|
122
|
-
continue
|
|
123
|
-
seen_uuids.add(record.memory_record.uuid)
|
|
124
|
-
|
|
125
|
-
token_count = self.token_counter.count_tokens_from_messages(
|
|
126
|
-
[record.memory_record.to_openai_message()]
|
|
127
|
-
)
|
|
128
|
-
regular_units.append(
|
|
129
|
-
_ContextUnit(
|
|
130
|
-
idx=idx,
|
|
131
|
-
record=record,
|
|
132
|
-
num_tokens=token_count,
|
|
133
|
-
)
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
# ======================
|
|
137
|
-
# 3. Tool Call Relationship Mapping
|
|
138
|
-
# ======================
|
|
139
|
-
tool_call_groups = self._group_tool_calls_and_responses(regular_units)
|
|
140
|
-
|
|
141
|
-
# ======================
|
|
142
|
-
# 4. Token Calculation
|
|
143
|
-
# ======================
|
|
144
|
-
total_tokens = system_tokens + sum(u.num_tokens for u in regular_units)
|
|
145
|
-
|
|
146
|
-
# ======================
|
|
147
|
-
# 5. Early Return if Within Limit
|
|
148
|
-
# ======================
|
|
149
|
-
if total_tokens <= self.token_limit:
|
|
150
|
-
sorted_units = sorted(
|
|
151
|
-
regular_units, key=self._conversation_sort_key
|
|
152
|
-
)
|
|
153
|
-
return self._assemble_output(sorted_units, system_unit)
|
|
154
|
-
|
|
155
|
-
# ======================
|
|
156
|
-
# 6. Truncation Logic with Tool Call Awareness
|
|
157
|
-
# ======================
|
|
158
|
-
remaining_units = self._truncate_with_tool_call_awareness(
|
|
159
|
-
regular_units, tool_call_groups, system_tokens
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
# Log only after truncation is actually performed so that both
|
|
163
|
-
# the original and the final token counts are visible.
|
|
164
|
-
tokens_after = system_tokens + sum(
|
|
165
|
-
u.num_tokens for u in remaining_units
|
|
166
|
-
)
|
|
167
|
-
logger.warning(
|
|
168
|
-
"Context truncation performed: "
|
|
169
|
-
f"before={total_tokens}, after={tokens_after}, "
|
|
170
|
-
f"limit={self.token_limit}"
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
# ======================
|
|
174
|
-
# 7. Output Assembly
|
|
175
|
-
# ======================
|
|
176
|
-
|
|
177
|
-
# In case system message is the only message in memory when sorted
|
|
178
|
-
# units are empty, raise an error
|
|
179
|
-
if system_unit and len(remaining_units) == 0 and len(records) > 1:
|
|
180
|
-
raise RuntimeError(
|
|
181
|
-
"System message and current message exceeds token limit ",
|
|
182
|
-
total_tokens,
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
# Sort remaining units chronologically
|
|
186
|
-
final_units = sorted(remaining_units, key=self._conversation_sort_key)
|
|
187
|
-
return self._assemble_output(final_units, system_unit)
|
|
188
|
-
|
|
189
|
-
def _group_tool_calls_and_responses(
|
|
190
|
-
self, units: List[_ContextUnit]
|
|
191
|
-
) -> Dict[str, List[_ContextUnit]]:
|
|
192
|
-
r"""Groups tool calls with their corresponding responses based on
|
|
193
|
-
`tool_call_id`.
|
|
194
|
-
|
|
195
|
-
This improved logic robustly gathers all messages (assistant requests
|
|
196
|
-
and tool responses, including chunks) that share a `tool_call_id`.
|
|
197
|
-
|
|
198
|
-
Args:
|
|
199
|
-
units (List[_ContextUnit]): List of context units to analyze.
|
|
200
|
-
|
|
201
|
-
Returns:
|
|
202
|
-
Dict[str, List[_ContextUnit]]: Mapping from `tool_call_id` to a
|
|
203
|
-
list of related units.
|
|
204
|
-
"""
|
|
205
|
-
tool_call_groups: Dict[str, List[_ContextUnit]] = defaultdict(list)
|
|
206
|
-
|
|
207
|
-
for unit in units:
|
|
208
|
-
# FunctionCallingMessage stores tool_call_id.
|
|
209
|
-
message = unit.record.memory_record.message
|
|
210
|
-
tool_call_id = getattr(message, 'tool_call_id', None)
|
|
211
|
-
|
|
212
|
-
if tool_call_id:
|
|
213
|
-
tool_call_groups[tool_call_id].append(unit)
|
|
214
|
-
|
|
215
|
-
# Filter out empty or incomplete groups if necessary,
|
|
216
|
-
# though defaultdict and getattr handle this gracefully.
|
|
217
|
-
return dict(tool_call_groups)
|
|
218
|
-
|
|
219
|
-
def _truncate_with_tool_call_awareness(
|
|
220
|
-
self,
|
|
221
|
-
regular_units: List[_ContextUnit],
|
|
222
|
-
tool_call_groups: Dict[str, List[_ContextUnit]],
|
|
223
|
-
system_tokens: int,
|
|
224
|
-
) -> List[_ContextUnit]:
|
|
225
|
-
r"""Truncates messages while preserving tool call-response pairs.
|
|
226
|
-
This method implements a more sophisticated truncation strategy:
|
|
227
|
-
1. It treats tool call groups (request + responses) and standalone
|
|
228
|
-
messages as individual items to be included.
|
|
229
|
-
2. It sorts all items by score and greedily adds them to the context.
|
|
230
|
-
3. **Partial Truncation**: If a complete tool group is too large to
|
|
231
|
-
fit,it attempts to add the request message and as many of the most
|
|
232
|
-
recent response chunks as the token budget allows.
|
|
233
|
-
|
|
234
|
-
Args:
|
|
235
|
-
regular_units (List[_ContextUnit]): All regular message units.
|
|
236
|
-
tool_call_groups (Dict[str, List[_ContextUnit]]): Grouped tool
|
|
237
|
-
calls.
|
|
238
|
-
system_tokens (int): Tokens used by the system message.
|
|
239
|
-
|
|
240
|
-
Returns:
|
|
241
|
-
List[_ContextUnit]: A list of units that fit within the token
|
|
242
|
-
limit.
|
|
243
|
-
"""
|
|
244
|
-
|
|
245
|
-
# Create a set for quick lookup of units belonging to any tool call
|
|
246
|
-
tool_call_unit_ids = {
|
|
247
|
-
unit.record.memory_record.uuid
|
|
248
|
-
for group in tool_call_groups.values()
|
|
249
|
-
for unit in group
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
# Separate standalone units from tool call groups
|
|
253
|
-
standalone_units = [
|
|
254
|
-
u
|
|
255
|
-
for u in regular_units
|
|
256
|
-
if u.record.memory_record.uuid not in tool_call_unit_ids
|
|
257
|
-
]
|
|
258
|
-
|
|
259
|
-
# Prepare all items (standalone units and groups) for sorting
|
|
260
|
-
all_potential_items: List[Dict] = []
|
|
261
|
-
for unit in standalone_units:
|
|
262
|
-
all_potential_items.append(
|
|
263
|
-
{
|
|
264
|
-
"type": "standalone",
|
|
265
|
-
"score": unit.record.score,
|
|
266
|
-
"timestamp": unit.record.timestamp,
|
|
267
|
-
"tokens": unit.num_tokens,
|
|
268
|
-
"item": unit,
|
|
269
|
-
}
|
|
270
|
-
)
|
|
271
|
-
for group in tool_call_groups.values():
|
|
272
|
-
all_potential_items.append(
|
|
273
|
-
{
|
|
274
|
-
"type": "group",
|
|
275
|
-
"score": max(u.record.score for u in group),
|
|
276
|
-
"timestamp": max(u.record.timestamp for u in group),
|
|
277
|
-
"tokens": sum(u.num_tokens for u in group),
|
|
278
|
-
"item": group,
|
|
279
|
-
}
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
# Sort all potential items by score (high to low), then timestamp
|
|
283
|
-
all_potential_items.sort(key=lambda x: (-x["score"], -x["timestamp"]))
|
|
284
|
-
|
|
285
|
-
remaining_units: List[_ContextUnit] = []
|
|
286
|
-
current_tokens = system_tokens
|
|
287
|
-
|
|
288
|
-
for item_dict in all_potential_items:
|
|
289
|
-
item_type = item_dict["type"]
|
|
290
|
-
item = item_dict["item"]
|
|
291
|
-
item_tokens = item_dict["tokens"]
|
|
292
|
-
|
|
293
|
-
if current_tokens + item_tokens <= self.token_limit:
|
|
294
|
-
# The whole item (standalone or group) fits, so add it
|
|
295
|
-
if item_type == "standalone":
|
|
296
|
-
remaining_units.append(item)
|
|
297
|
-
else: # item_type == "group"
|
|
298
|
-
remaining_units.extend(item)
|
|
299
|
-
current_tokens += item_tokens
|
|
300
|
-
|
|
301
|
-
elif item_type == "group":
|
|
302
|
-
# The group does not fit completely; try partial inclusion.
|
|
303
|
-
request_unit: Optional[_ContextUnit] = None
|
|
304
|
-
response_units: List[_ContextUnit] = []
|
|
305
|
-
|
|
306
|
-
for unit in item:
|
|
307
|
-
# Assistant msg with `args` is the request
|
|
308
|
-
if (
|
|
309
|
-
isinstance(
|
|
310
|
-
unit.record.memory_record.message,
|
|
311
|
-
FunctionCallingMessage,
|
|
312
|
-
)
|
|
313
|
-
and unit.record.memory_record.message.args is not None
|
|
314
|
-
):
|
|
315
|
-
request_unit = unit
|
|
316
|
-
else:
|
|
317
|
-
response_units.append(unit)
|
|
318
|
-
|
|
319
|
-
# A group must have a request to be considered for inclusion.
|
|
320
|
-
if request_unit is None:
|
|
321
|
-
continue
|
|
322
|
-
|
|
323
|
-
# Check if we can at least fit the request.
|
|
324
|
-
if (
|
|
325
|
-
current_tokens + request_unit.num_tokens
|
|
326
|
-
<= self.token_limit
|
|
327
|
-
):
|
|
328
|
-
units_to_add = [request_unit]
|
|
329
|
-
tokens_to_add = request_unit.num_tokens
|
|
330
|
-
|
|
331
|
-
# Sort responses by timestamp to add newest chunks first
|
|
332
|
-
response_units.sort(
|
|
333
|
-
key=lambda u: u.record.timestamp, reverse=True
|
|
334
|
-
)
|
|
65
|
+
remaining_records.append(record)
|
|
335
66
|
|
|
336
|
-
|
|
337
|
-
if (
|
|
338
|
-
current_tokens
|
|
339
|
-
+ tokens_to_add
|
|
340
|
-
+ resp_unit.num_tokens
|
|
341
|
-
<= self.token_limit
|
|
342
|
-
):
|
|
343
|
-
units_to_add.append(resp_unit)
|
|
344
|
-
tokens_to_add += resp_unit.num_tokens
|
|
67
|
+
remaining_records.sort(key=lambda record: record.timestamp)
|
|
345
68
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
current_tokens += tokens_to_add
|
|
69
|
+
messages: List[OpenAIMessage] = []
|
|
70
|
+
if system_record is not None:
|
|
71
|
+
messages.append(system_record.memory_record.to_openai_message())
|
|
350
72
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
self, records: List[ContextRecord]
|
|
355
|
-
) -> Tuple[Optional[_ContextUnit], List[_ContextUnit]]:
|
|
356
|
-
r"""Extracts the system message from records and validates it.
|
|
357
|
-
|
|
358
|
-
Args:
|
|
359
|
-
records (List[ContextRecord]): List of context records
|
|
360
|
-
representing conversation history.
|
|
361
|
-
|
|
362
|
-
Returns:
|
|
363
|
-
Tuple[Optional[_ContextUnit], List[_ContextUnit]]: containing:
|
|
364
|
-
- The system message as a `_ContextUnit`, if valid; otherwise,
|
|
365
|
-
`None`.
|
|
366
|
-
- An empty list, serving as the initial container for regular
|
|
367
|
-
messages.
|
|
368
|
-
"""
|
|
369
|
-
if not records:
|
|
370
|
-
return None, []
|
|
371
|
-
|
|
372
|
-
first_record = records[0]
|
|
373
|
-
if (
|
|
374
|
-
first_record.memory_record.role_at_backend
|
|
375
|
-
!= OpenAIBackendRole.SYSTEM
|
|
376
|
-
):
|
|
377
|
-
return None, []
|
|
378
|
-
|
|
379
|
-
message = first_record.memory_record.to_openai_message()
|
|
380
|
-
tokens = self.token_counter.count_tokens_from_messages([message])
|
|
381
|
-
system_message_unit = _ContextUnit(
|
|
382
|
-
idx=0,
|
|
383
|
-
record=first_record,
|
|
384
|
-
num_tokens=tokens,
|
|
73
|
+
messages.extend(
|
|
74
|
+
record.memory_record.to_openai_message()
|
|
75
|
+
for record in remaining_records
|
|
385
76
|
)
|
|
386
|
-
return system_message_unit, []
|
|
387
|
-
|
|
388
|
-
def _conversation_sort_key(
|
|
389
|
-
self, unit: _ContextUnit
|
|
390
|
-
) -> Tuple[float, float]:
|
|
391
|
-
r"""Defines the sorting key for assembling the final output.
|
|
392
|
-
|
|
393
|
-
Sorting priority:
|
|
394
|
-
- Primary: Sort by timestamp in ascending order (chronological order).
|
|
395
|
-
- Secondary: Sort by score in descending order (higher scores first
|
|
396
|
-
when timestamps are equal).
|
|
397
|
-
|
|
398
|
-
Args:
|
|
399
|
-
unit (_ContextUnit): A `_ContextUnit` representing a conversation
|
|
400
|
-
record.
|
|
401
|
-
|
|
402
|
-
Returns:
|
|
403
|
-
Tuple[float, float]:
|
|
404
|
-
- Timestamp for chronological sorting.
|
|
405
|
-
- Negative score for descending order sorting.
|
|
406
|
-
"""
|
|
407
|
-
return (unit.record.timestamp, -unit.record.score)
|
|
408
|
-
|
|
409
|
-
def _assemble_output(
|
|
410
|
-
self,
|
|
411
|
-
context_units: List[_ContextUnit],
|
|
412
|
-
system_unit: Optional[_ContextUnit],
|
|
413
|
-
) -> Tuple[List[OpenAIMessage], int]:
|
|
414
|
-
r"""Assembles final message list with proper ordering and token count.
|
|
415
|
-
|
|
416
|
-
Args:
|
|
417
|
-
context_units (List[_ContextUnit]): Sorted list of regular message
|
|
418
|
-
units.
|
|
419
|
-
system_unit (Optional[_ContextUnit]): System message unit (if
|
|
420
|
-
present).
|
|
421
|
-
|
|
422
|
-
Returns:
|
|
423
|
-
Tuple[List[OpenAIMessage], int]: Tuple of (ordered messages, total
|
|
424
|
-
tokens)
|
|
425
|
-
"""
|
|
426
|
-
messages = []
|
|
427
|
-
total_tokens = 0
|
|
428
|
-
|
|
429
|
-
# Add system message first if present
|
|
430
|
-
if system_unit:
|
|
431
|
-
messages.append(
|
|
432
|
-
system_unit.record.memory_record.to_openai_message()
|
|
433
|
-
)
|
|
434
|
-
total_tokens += system_unit.num_tokens
|
|
435
77
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
messages.append(unit.record.memory_record.to_openai_message())
|
|
439
|
-
total_tokens += unit.num_tokens
|
|
78
|
+
if not messages:
|
|
79
|
+
return [], 0
|
|
440
80
|
|
|
81
|
+
total_tokens = self.token_counter.count_tokens_from_messages(messages)
|
|
441
82
|
return messages, total_tokens
|
camel/memories/records.py
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
# Enables postponed evaluation of annotations (for string-based type hints)
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
|
+
import inspect
|
|
18
19
|
import time
|
|
19
|
-
from dataclasses import asdict
|
|
20
20
|
from typing import Any, ClassVar, Dict
|
|
21
21
|
from uuid import UUID, uuid4
|
|
22
22
|
|
|
@@ -63,6 +63,20 @@ class MemoryRecord(BaseModel):
|
|
|
63
63
|
"FunctionCallingMessage": FunctionCallingMessage,
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
# Cache for constructor parameters (performance optimization)
|
|
67
|
+
_constructor_params_cache: ClassVar[Dict[str, set]] = {}
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def _get_constructor_params(cls, message_cls) -> set:
|
|
71
|
+
"""Get constructor parameters for a message class with caching."""
|
|
72
|
+
cls_name = message_cls.__name__
|
|
73
|
+
if cls_name not in cls._constructor_params_cache:
|
|
74
|
+
sig = inspect.signature(message_cls.__init__)
|
|
75
|
+
cls._constructor_params_cache[cls_name] = set(
|
|
76
|
+
sig.parameters.keys()
|
|
77
|
+
) - {'self'}
|
|
78
|
+
return cls._constructor_params_cache[cls_name]
|
|
79
|
+
|
|
66
80
|
@classmethod
|
|
67
81
|
def from_dict(cls, record_dict: Dict[str, Any]) -> "MemoryRecord":
|
|
68
82
|
r"""Reconstruct a :obj:`MemoryRecord` from the input dict.
|
|
@@ -70,14 +84,78 @@ class MemoryRecord(BaseModel):
|
|
|
70
84
|
Args:
|
|
71
85
|
record_dict(Dict[str, Any]): A dict generated by :meth:`to_dict`.
|
|
72
86
|
"""
|
|
87
|
+
from camel.types import OpenAIBackendRole, RoleType
|
|
88
|
+
|
|
73
89
|
message_cls = cls._MESSAGE_TYPES[record_dict["message"]["__class__"]]
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
90
|
+
data = record_dict["message"].copy()
|
|
91
|
+
data.pop("__class__")
|
|
92
|
+
|
|
93
|
+
# Convert role_type string to enum
|
|
94
|
+
if "role_type" in data and isinstance(data["role_type"], str):
|
|
95
|
+
data["role_type"] = RoleType(data["role_type"])
|
|
96
|
+
|
|
97
|
+
# Deserialize image_list from base64 strings/URLs back to PIL Images/
|
|
98
|
+
# URLs
|
|
99
|
+
if "image_list" in data and data["image_list"] is not None:
|
|
100
|
+
import base64
|
|
101
|
+
from io import BytesIO
|
|
102
|
+
|
|
103
|
+
from PIL import Image
|
|
104
|
+
|
|
105
|
+
image_objects = []
|
|
106
|
+
for img_item in data["image_list"]:
|
|
107
|
+
if isinstance(img_item, dict):
|
|
108
|
+
# New format with type indicator
|
|
109
|
+
if img_item["type"] == "url":
|
|
110
|
+
# URL string, keep as-is
|
|
111
|
+
image_objects.append(img_item["data"])
|
|
112
|
+
else: # type == "base64"
|
|
113
|
+
# Base64 encoded image, convert to PIL Image
|
|
114
|
+
img_bytes = base64.b64decode(img_item["data"])
|
|
115
|
+
img = Image.open(BytesIO(img_bytes))
|
|
116
|
+
# Restore the format attribute if it was saved
|
|
117
|
+
if "format" in img_item:
|
|
118
|
+
img.format = img_item["format"]
|
|
119
|
+
image_objects.append(img)
|
|
120
|
+
else:
|
|
121
|
+
# Legacy format: assume it's a base64 string
|
|
122
|
+
img_bytes = base64.b64decode(img_item)
|
|
123
|
+
img = Image.open(BytesIO(img_bytes))
|
|
124
|
+
image_objects.append(img)
|
|
125
|
+
data["image_list"] = image_objects
|
|
126
|
+
|
|
127
|
+
# Deserialize video_bytes from base64 string
|
|
128
|
+
if "video_bytes" in data and data["video_bytes"] is not None:
|
|
129
|
+
import base64
|
|
130
|
+
|
|
131
|
+
data["video_bytes"] = base64.b64decode(data["video_bytes"])
|
|
132
|
+
|
|
133
|
+
# Get valid constructor parameters (cached)
|
|
134
|
+
valid_params = cls._get_constructor_params(message_cls)
|
|
135
|
+
|
|
136
|
+
# Separate constructor args from extra fields
|
|
137
|
+
kwargs = {k: v for k, v in data.items() if k in valid_params}
|
|
138
|
+
extra_fields = {k: v for k, v in data.items() if k not in valid_params}
|
|
139
|
+
|
|
140
|
+
# Handle meta_dict properly: merge existing meta_dict with extra fields
|
|
141
|
+
existing_meta = kwargs.get("meta_dict", {}) or {}
|
|
142
|
+
if extra_fields:
|
|
143
|
+
# Extra fields take precedence, but preserve existing meta_dict
|
|
144
|
+
# structure
|
|
145
|
+
merged_meta = {**existing_meta, **extra_fields}
|
|
146
|
+
kwargs["meta_dict"] = merged_meta
|
|
147
|
+
elif not existing_meta:
|
|
148
|
+
kwargs["meta_dict"] = None
|
|
149
|
+
|
|
150
|
+
# Convert role_at_backend
|
|
151
|
+
role_at_backend = record_dict["role_at_backend"]
|
|
152
|
+
if isinstance(role_at_backend, str):
|
|
153
|
+
role_at_backend = OpenAIBackendRole(role_at_backend)
|
|
154
|
+
|
|
77
155
|
return cls(
|
|
78
156
|
uuid=UUID(record_dict["uuid"]),
|
|
79
|
-
message=
|
|
80
|
-
role_at_backend=
|
|
157
|
+
message=message_cls(**kwargs),
|
|
158
|
+
role_at_backend=role_at_backend,
|
|
81
159
|
extra_info=record_dict["extra_info"],
|
|
82
160
|
timestamp=record_dict["timestamp"],
|
|
83
161
|
agent_id=record_dict["agent_id"],
|
|
@@ -91,9 +169,11 @@ class MemoryRecord(BaseModel):
|
|
|
91
169
|
"uuid": str(self.uuid),
|
|
92
170
|
"message": {
|
|
93
171
|
"__class__": self.message.__class__.__name__,
|
|
94
|
-
**
|
|
172
|
+
**self.message.to_dict(),
|
|
95
173
|
},
|
|
96
|
-
"role_at_backend": self.role_at_backend
|
|
174
|
+
"role_at_backend": self.role_at_backend.value
|
|
175
|
+
if hasattr(self.role_at_backend, "value")
|
|
176
|
+
else self.role_at_backend,
|
|
97
177
|
"extra_info": self.extra_info,
|
|
98
178
|
"timestamp": self.timestamp,
|
|
99
179
|
"agent_id": self.agent_id,
|