camel-ai 0.2.75a6__py3-none-any.whl → 0.2.76__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.

Potentially problematic release.


This version of camel-ai might be problematic. Click here for more details.

Files changed (97) hide show
  1. camel/__init__.py +1 -1
  2. camel/agents/chat_agent.py +1001 -205
  3. camel/agents/mcp_agent.py +30 -27
  4. camel/configs/__init__.py +6 -0
  5. camel/configs/amd_config.py +70 -0
  6. camel/configs/cometapi_config.py +104 -0
  7. camel/data_collectors/alpaca_collector.py +15 -6
  8. camel/environments/tic_tac_toe.py +1 -1
  9. camel/interpreters/__init__.py +2 -0
  10. camel/interpreters/docker/Dockerfile +3 -12
  11. camel/interpreters/microsandbox_interpreter.py +395 -0
  12. camel/loaders/__init__.py +11 -2
  13. camel/loaders/chunkr_reader.py +9 -0
  14. camel/memories/__init__.py +2 -1
  15. camel/memories/agent_memories.py +3 -1
  16. camel/memories/blocks/chat_history_block.py +21 -3
  17. camel/memories/records.py +88 -8
  18. camel/messages/base.py +127 -34
  19. camel/models/__init__.py +4 -0
  20. camel/models/amd_model.py +101 -0
  21. camel/models/azure_openai_model.py +0 -6
  22. camel/models/base_model.py +30 -0
  23. camel/models/cometapi_model.py +83 -0
  24. camel/models/model_factory.py +4 -0
  25. camel/models/openai_compatible_model.py +0 -6
  26. camel/models/openai_model.py +0 -6
  27. camel/models/zhipuai_model.py +61 -2
  28. camel/parsers/__init__.py +18 -0
  29. camel/parsers/mcp_tool_call_parser.py +176 -0
  30. camel/retrievers/auto_retriever.py +1 -0
  31. camel/runtimes/daytona_runtime.py +11 -12
  32. camel/societies/workforce/prompts.py +131 -50
  33. camel/societies/workforce/single_agent_worker.py +434 -49
  34. camel/societies/workforce/structured_output_handler.py +30 -18
  35. camel/societies/workforce/task_channel.py +43 -0
  36. camel/societies/workforce/utils.py +105 -12
  37. camel/societies/workforce/workforce.py +1322 -311
  38. camel/societies/workforce/workforce_logger.py +24 -5
  39. camel/storages/key_value_storages/json.py +15 -2
  40. camel/storages/object_storages/google_cloud.py +1 -1
  41. camel/storages/vectordb_storages/oceanbase.py +10 -11
  42. camel/storages/vectordb_storages/tidb.py +8 -6
  43. camel/tasks/task.py +4 -3
  44. camel/toolkits/__init__.py +18 -5
  45. camel/toolkits/aci_toolkit.py +45 -0
  46. camel/toolkits/code_execution.py +28 -1
  47. camel/toolkits/context_summarizer_toolkit.py +684 -0
  48. camel/toolkits/dingtalk.py +1135 -0
  49. camel/toolkits/edgeone_pages_mcp_toolkit.py +11 -31
  50. camel/toolkits/{file_write_toolkit.py → file_toolkit.py} +194 -34
  51. camel/toolkits/function_tool.py +6 -1
  52. camel/toolkits/google_drive_mcp_toolkit.py +12 -31
  53. camel/toolkits/hybrid_browser_toolkit/config_loader.py +12 -0
  54. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +79 -2
  55. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +95 -59
  56. camel/toolkits/hybrid_browser_toolkit/installer.py +203 -0
  57. camel/toolkits/hybrid_browser_toolkit/ts/package-lock.json +5 -612
  58. camel/toolkits/hybrid_browser_toolkit/ts/package.json +0 -1
  59. camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +619 -95
  60. camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +7 -2
  61. camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +115 -219
  62. camel/toolkits/hybrid_browser_toolkit/ts/src/parent-child-filter.ts +226 -0
  63. camel/toolkits/hybrid_browser_toolkit/ts/src/snapshot-parser.ts +219 -0
  64. camel/toolkits/hybrid_browser_toolkit/ts/src/som-screenshot-injected.ts +543 -0
  65. camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +1 -0
  66. camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +39 -6
  67. camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +405 -131
  68. camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +9 -5
  69. camel/toolkits/{openai_image_toolkit.py → image_generation_toolkit.py} +98 -31
  70. camel/toolkits/markitdown_toolkit.py +27 -1
  71. camel/toolkits/mcp_toolkit.py +348 -348
  72. camel/toolkits/message_integration.py +3 -0
  73. camel/toolkits/minimax_mcp_toolkit.py +195 -0
  74. camel/toolkits/note_taking_toolkit.py +18 -8
  75. camel/toolkits/notion_mcp_toolkit.py +16 -26
  76. camel/toolkits/origene_mcp_toolkit.py +8 -49
  77. camel/toolkits/playwright_mcp_toolkit.py +12 -31
  78. camel/toolkits/resend_toolkit.py +168 -0
  79. camel/toolkits/slack_toolkit.py +50 -1
  80. camel/toolkits/terminal_toolkit/__init__.py +18 -0
  81. camel/toolkits/terminal_toolkit/terminal_toolkit.py +924 -0
  82. camel/toolkits/terminal_toolkit/utils.py +532 -0
  83. camel/toolkits/vertex_ai_veo_toolkit.py +590 -0
  84. camel/toolkits/video_analysis_toolkit.py +17 -11
  85. camel/toolkits/wechat_official_toolkit.py +483 -0
  86. camel/types/enums.py +124 -1
  87. camel/types/unified_model_type.py +5 -0
  88. camel/utils/commons.py +17 -0
  89. camel/utils/context_utils.py +804 -0
  90. camel/utils/mcp.py +136 -2
  91. camel/utils/token_counting.py +25 -17
  92. {camel_ai-0.2.75a6.dist-info → camel_ai-0.2.76.dist-info}/METADATA +158 -59
  93. {camel_ai-0.2.75a6.dist-info → camel_ai-0.2.76.dist-info}/RECORD +95 -76
  94. camel/loaders/pandas_reader.py +0 -368
  95. camel/toolkits/terminal_toolkit.py +0 -1788
  96. {camel_ai-0.2.75a6.dist-info → camel_ai-0.2.76.dist-info}/WHEEL +0 -0
  97. {camel_ai-0.2.75a6.dist-info → camel_ai-0.2.76.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,395 @@
1
+ # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software
9
+ # distributed under the License is distributed on an "AS IS" BASIS,
10
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ # See the License for the specific language governing permissions and
12
+ # limitations under the License.
13
+ # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
14
+ import asyncio
15
+ from typing import Any, ClassVar, Dict, List, Optional, Tuple, Union
16
+
17
+ from camel.interpreters.base import BaseInterpreter
18
+ from camel.interpreters.interpreter_error import InterpreterError
19
+ from camel.logger import get_logger
20
+
21
+ logger = get_logger(__name__)
22
+
23
+
24
+ class MicrosandboxInterpreter(BaseInterpreter):
25
+ r"""Microsandbox Code Interpreter implementation.
26
+
27
+ This interpreter provides secure code execution using microsandbox,
28
+ a self-hosted platform for secure execution of untrusted user/AI code.
29
+ It supports Python code execution via PythonSandbox, JavaScript/Node.js
30
+ code execution via NodeSandbox, and shell commands via the command
31
+ interface.
32
+
33
+ Args:
34
+ require_confirm (bool, optional): If True, prompt user before running
35
+ code strings for security. (default: :obj:`True`)
36
+ server_url (str, optional): URL of the microsandbox server. If not
37
+ provided, will use MSB_SERVER_URL environment variable, then
38
+ fall back to http://127.0.0.1:5555. (default: :obj:`None`)
39
+ api_key (str, optional): API key for microsandbox authentication.
40
+ If not provided, will use MSB_API_KEY environment variable.
41
+ (default: :obj:`None`)
42
+ namespace (str, optional): Namespace for the sandbox.
43
+ (default: :obj:`"default"`)
44
+ sandbox_name (str, optional): Name of the sandbox instance. If not
45
+ provided, a random name will be generated by the SDK.
46
+ (default: :obj:`None`)
47
+ timeout (int, optional): Default timeout for code execution in seconds.
48
+ (default: :obj:`30`)
49
+
50
+ Environment Variables:
51
+ MSB_SERVER_URL: URL of the microsandbox server.
52
+ MSB_API_KEY: API key for microsandbox authentication.
53
+
54
+ Note:
55
+ The SDK handles parameter priority as: user parameter > environment
56
+ variable > default value.
57
+ """
58
+
59
+ _CODE_TYPE_MAPPING: ClassVar[Dict[str, str]] = {
60
+ # Python code - uses PythonSandbox
61
+ "python": "python_sandbox",
62
+ "py3": "python_sandbox",
63
+ "python3": "python_sandbox",
64
+ "py": "python_sandbox",
65
+ # JavaScript/Node.js code - uses NodeSandbox
66
+ "javascript": "node_sandbox",
67
+ "js": "node_sandbox",
68
+ "node": "node_sandbox",
69
+ "typescript": "node_sandbox",
70
+ "ts": "node_sandbox",
71
+ # Shell commands - uses command.run()
72
+ "bash": "shell_command",
73
+ "shell": "shell_command",
74
+ "sh": "shell_command",
75
+ }
76
+
77
+ def __init__(
78
+ self,
79
+ require_confirm: bool = True,
80
+ server_url: Optional[str] = None,
81
+ api_key: Optional[str] = None,
82
+ namespace: str = "default",
83
+ sandbox_name: Optional[str] = None,
84
+ timeout: int = 30,
85
+ ) -> None:
86
+ from microsandbox import (
87
+ NodeSandbox,
88
+ PythonSandbox,
89
+ )
90
+
91
+ # Store parameters, let SDK handle defaults and environment variables
92
+ self.require_confirm = require_confirm
93
+ self.server_url = server_url # None means use SDK default logic
94
+ self.api_key = api_key # None means use SDK default logic
95
+ self.namespace = namespace
96
+ self.sandbox_name = (
97
+ sandbox_name # None means SDK generates random name
98
+ )
99
+ self.timeout = timeout
100
+
101
+ # Store sandbox configuration
102
+ self._sandbox_config = {
103
+ "server_url": self.server_url,
104
+ "namespace": self.namespace,
105
+ "name": self.sandbox_name,
106
+ "api_key": self.api_key,
107
+ }
108
+
109
+ # Store sandbox classes for reuse
110
+ self._PythonSandbox = PythonSandbox
111
+ self._NodeSandbox = NodeSandbox
112
+
113
+ # Log initialization info
114
+ logger.info("Initialized MicrosandboxInterpreter")
115
+ logger.info(f"Namespace: {self.namespace}")
116
+ if self.sandbox_name:
117
+ logger.info(f"Sandbox name: {self.sandbox_name}")
118
+ else:
119
+ logger.info("Sandbox name: will be auto-generated by SDK")
120
+
121
+ def run(
122
+ self,
123
+ code: str,
124
+ code_type: str = "python",
125
+ ) -> str:
126
+ r"""Executes the given code in the microsandbox.
127
+
128
+ Args:
129
+ code (str): The code string to execute.
130
+ code_type (str): The type of code to execute. Supported types:
131
+ 'python', 'javascript', 'bash'. (default: :obj:`python`)
132
+
133
+ Returns:
134
+ str: The string representation of the output of the executed code.
135
+
136
+ Raises:
137
+ InterpreterError: If the `code_type` is not supported or if any
138
+ runtime error occurs during the execution of the code.
139
+ """
140
+ if code_type not in self._CODE_TYPE_MAPPING:
141
+ raise InterpreterError(
142
+ f"Unsupported code type {code_type}. "
143
+ f"`{self.__class__.__name__}` only supports "
144
+ f"{', '.join(list(self._CODE_TYPE_MAPPING.keys()))}."
145
+ )
146
+
147
+ # Print code for security checking
148
+ if self.require_confirm:
149
+ logger.info(
150
+ f"The following {code_type} code will run on "
151
+ f"microsandbox: {code}"
152
+ )
153
+ self._confirm_execution("code")
154
+
155
+ # Run the code asynchronously
156
+ return asyncio.run(self._run_async(code, code_type))
157
+
158
+ async def _run_async(self, code: str, code_type: str) -> str:
159
+ r"""Asynchronously executes code in microsandbox.
160
+
161
+ Args:
162
+ code (str): The code to execute.
163
+ code_type (str): The type of code to execute.
164
+
165
+ Returns:
166
+ str: The output of the executed code.
167
+
168
+ Raises:
169
+ InterpreterError: If execution fails.
170
+ """
171
+ try:
172
+ execution_method = self._CODE_TYPE_MAPPING[code_type]
173
+
174
+ if execution_method == "python_sandbox":
175
+ return await self._run_python_code(code)
176
+ elif execution_method == "node_sandbox":
177
+ return await self._run_node_code(code)
178
+ elif execution_method == "shell_command":
179
+ return await self._run_shell_command(code)
180
+ else:
181
+ raise InterpreterError(
182
+ f"Unsupported execution method: {execution_method}"
183
+ )
184
+
185
+ except Exception as e:
186
+ raise InterpreterError(
187
+ f"Error executing code in microsandbox: {e}"
188
+ )
189
+
190
+ async def _run_python_code(self, code: str) -> str:
191
+ r"""Execute Python code using PythonSandbox.
192
+
193
+ Args:
194
+ code (str): Python code to execute.
195
+
196
+ Returns:
197
+ str: Execution output.
198
+ """
199
+ async with self._PythonSandbox.create(
200
+ **self._sandbox_config
201
+ ) as sandbox:
202
+ execution = await asyncio.wait_for(
203
+ sandbox.run(code), timeout=self.timeout
204
+ )
205
+ return await self._get_execution_output(execution)
206
+
207
+ async def _run_node_code(self, code: str) -> str:
208
+ r"""Execute JavaScript/Node.js code using NodeSandbox.
209
+
210
+ Args:
211
+ code (str): JavaScript/Node.js code to execute.
212
+
213
+ Returns:
214
+ str: Execution output.
215
+ """
216
+ async with self._NodeSandbox.create(**self._sandbox_config) as sandbox:
217
+ execution = await asyncio.wait_for(
218
+ sandbox.run(code), timeout=self.timeout
219
+ )
220
+ return await self._get_execution_output(execution)
221
+
222
+ async def _run_shell_command(self, code: str) -> str:
223
+ r"""Execute shell commands directly.
224
+
225
+ Args:
226
+ code (str): Shell command to execute.
227
+
228
+ Returns:
229
+ str: Command execution output.
230
+ """
231
+ # Use any sandbox for shell commands
232
+ async with self._PythonSandbox.create(
233
+ **self._sandbox_config
234
+ ) as sandbox:
235
+ execution = await asyncio.wait_for(
236
+ sandbox.command.run("bash", ["-c", code]), timeout=self.timeout
237
+ )
238
+ return await self._get_command_output(execution)
239
+
240
+ async def _get_execution_output(self, execution) -> str:
241
+ r"""Get output from code execution.
242
+
243
+ Args:
244
+ execution: Execution object from sandbox.run().
245
+
246
+ Returns:
247
+ str: Formatted execution output.
248
+ """
249
+ output = await execution.output()
250
+ error = await execution.error()
251
+
252
+ result_parts = []
253
+ if output and output.strip():
254
+ result_parts.append(output.strip())
255
+ if error and error.strip():
256
+ result_parts.append(f"STDERR: {error.strip()}")
257
+
258
+ return (
259
+ "\n".join(result_parts)
260
+ if result_parts
261
+ else "Code executed successfully (no output)"
262
+ )
263
+
264
+ async def _get_command_output(self, execution) -> str:
265
+ r"""Get output from command execution.
266
+
267
+ Args:
268
+ execution: CommandExecution object from sandbox.command.run().
269
+
270
+ Returns:
271
+ str: Formatted command output.
272
+ """
273
+ output = await execution.output()
274
+ error = await execution.error()
275
+
276
+ result_parts = []
277
+ if output and output.strip():
278
+ result_parts.append(output.strip())
279
+ if error and error.strip():
280
+ result_parts.append(f"STDERR: {error.strip()}")
281
+ if hasattr(execution, 'exit_code') and execution.exit_code != 0:
282
+ result_parts.append(f"Exit code: {execution.exit_code}")
283
+
284
+ return (
285
+ "\n".join(result_parts)
286
+ if result_parts
287
+ else "Command executed successfully (no output)"
288
+ )
289
+
290
+ def _confirm_execution(self, execution_type: str) -> None:
291
+ r"""Prompt user for confirmation before executing code or commands.
292
+
293
+ Args:
294
+ execution_type (str): Type of execution ('code' or 'command').
295
+
296
+ Raises:
297
+ InterpreterError: If user declines to run the code/command.
298
+ """
299
+ while True:
300
+ choice = input(f"Running {execution_type}? [Y/n]:").lower()
301
+ if choice in ["y", "yes", "ye"]:
302
+ break
303
+ elif choice not in ["no", "n"]:
304
+ continue
305
+ raise InterpreterError(
306
+ f"Execution halted: User opted not to run the "
307
+ f"{execution_type}. "
308
+ f"This choice stops the current operation and any "
309
+ f"further {execution_type} execution."
310
+ )
311
+
312
+ def supported_code_types(self) -> List[str]:
313
+ r"""Provides supported code types by the interpreter."""
314
+ return list(self._CODE_TYPE_MAPPING.keys())
315
+
316
+ def update_action_space(self, action_space: Dict[str, Any]) -> None:
317
+ r"""Updates action space for interpreter.
318
+
319
+ Args:
320
+ action_space: Action space dictionary (unused in microsandbox).
321
+
322
+ Note:
323
+ Microsandbox doesn't support action space updates as it runs
324
+ in isolated environments for each execution.
325
+ """
326
+ # Explicitly acknowledge the parameter to avoid linting warnings
327
+ _ = action_space
328
+ logger.warning(
329
+ "Microsandbox doesn't support action space updates. "
330
+ "Code runs in isolated environments for each execution."
331
+ )
332
+
333
+ def execute_command(self, command: str) -> Union[str, Tuple[str, str]]:
334
+ r"""Execute a shell command in the microsandbox.
335
+
336
+ This method is designed for package management and system
337
+ administration tasks. It executes shell commands directly
338
+ using the microsandbox command interface.
339
+
340
+ Args:
341
+ command (str): The shell command to execute (e.g.,
342
+ "pip install numpy", "ls -la", "apt-get update").
343
+
344
+ Returns:
345
+ Union[str, Tuple[str, str]]: The output of the command.
346
+
347
+ Examples:
348
+ >>> interpreter.execute_command("pip install numpy")
349
+ >>> interpreter.execute_command("npm install express")
350
+ >>> interpreter.execute_command("ls -la /tmp")
351
+ """
352
+ # Print command for security checking
353
+ if self.require_confirm:
354
+ logger.info(
355
+ f"The following shell command will run on "
356
+ f"microsandbox: {command}"
357
+ )
358
+ self._confirm_execution("command")
359
+
360
+ return asyncio.run(self._execute_command_async(command))
361
+
362
+ async def _execute_command_async(self, command: str) -> str:
363
+ r"""Asynchronously executes a shell command in microsandbox.
364
+
365
+ Args:
366
+ command (str): The shell command to execute.
367
+
368
+ Returns:
369
+ str: The output of the command execution.
370
+
371
+ Raises:
372
+ InterpreterError: If execution fails.
373
+ """
374
+ try:
375
+ async with self._PythonSandbox.create(
376
+ **self._sandbox_config
377
+ ) as sandbox:
378
+ execution = await asyncio.wait_for(
379
+ sandbox.command.run("bash", ["-c", command]),
380
+ timeout=self.timeout,
381
+ )
382
+ return await self._get_command_output(execution)
383
+
384
+ except Exception as e:
385
+ raise InterpreterError(
386
+ f"Error executing command in microsandbox: {e}"
387
+ )
388
+
389
+ def __del__(self) -> None:
390
+ r"""Destructor for the MicrosandboxInterpreter class.
391
+
392
+ Microsandbox uses context managers for resource management,
393
+ so no explicit cleanup is needed.
394
+ """
395
+ logger.debug("MicrosandboxInterpreter cleaned up")
camel/loaders/__init__.py CHANGED
@@ -21,7 +21,6 @@ from .jina_url_reader import JinaURLReader
21
21
  from .markitdown import MarkItDownLoader
22
22
  from .mineru_extractor import MinerU
23
23
  from .mistral_reader import MistralReader
24
- from .pandas_reader import PandasReader
25
24
  from .scrapegraph_reader import ScrapeGraphAI
26
25
  from .unstructured_io import UnstructuredIO
27
26
 
@@ -33,7 +32,6 @@ __all__ = [
33
32
  'JinaURLReader',
34
33
  'Firecrawl',
35
34
  'Apify',
36
- 'PandasReader',
37
35
  'ChunkrReader',
38
36
  'ChunkrReaderConfig',
39
37
  'MinerU',
@@ -42,3 +40,14 @@ __all__ = [
42
40
  'ScrapeGraphAI',
43
41
  'MistralReader',
44
42
  ]
43
+
44
+
45
+ def __getattr__(name: str):
46
+ if name == 'PandasReader':
47
+ raise ImportError(
48
+ "PandasReader has been removed from camel.loaders. "
49
+ "The pandasai dependency limited pandas to version 1.5.3. "
50
+ "Please use ExcelToolkit from camel.toolkits instead for "
51
+ "handling structured data."
52
+ )
53
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
@@ -34,6 +34,12 @@ class ChunkrReaderConfig:
34
34
  high_resolution (bool, optional): Whether to use high resolution OCR.
35
35
  (default: :obj:`True`)
36
36
  ocr_strategy (str, optional): The OCR strategy. Defaults to 'Auto'.
37
+ **kwargs: Additional keyword arguments to pass to the Chunkr
38
+ Configuration. This accepts all other Configuration parameters
39
+ such as expires_in, pipeline, segment_processing,
40
+ segmentation_strategy, etc.
41
+ See: https://github.com/lumina-ai-inc/chunkr/blob/main/core/src/
42
+ models/task.rs#L749
37
43
  """
38
44
 
39
45
  def __init__(
@@ -41,10 +47,12 @@ class ChunkrReaderConfig:
41
47
  chunk_processing: int = 512,
42
48
  high_resolution: bool = True,
43
49
  ocr_strategy: str = "Auto",
50
+ **kwargs,
44
51
  ):
45
52
  self.chunk_processing = chunk_processing
46
53
  self.high_resolution = high_resolution
47
54
  self.ocr_strategy = ocr_strategy
55
+ self.kwargs = kwargs
48
56
 
49
57
 
50
58
  class ChunkrReader:
@@ -190,4 +198,5 @@ class ChunkrReader:
190
198
  "Auto": OcrStrategy.AUTO,
191
199
  "All": OcrStrategy.ALL,
192
200
  }.get(chunkr_config.ocr_strategy, OcrStrategy.ALL),
201
+ **chunkr_config.kwargs,
193
202
  )
@@ -18,7 +18,7 @@ from .agent_memories import (
18
18
  VectorDBMemory,
19
19
  )
20
20
  from .base import AgentMemory, BaseContextCreator, MemoryBlock
21
- from .blocks.chat_history_block import ChatHistoryBlock
21
+ from .blocks.chat_history_block import ChatHistoryBlock, EmptyMemoryWarning
22
22
  from .blocks.vectordb_block import VectorDBBlock
23
23
  from .context_creators.score_based import ScoreBasedContextCreator
24
24
  from .records import ContextRecord, MemoryRecord
@@ -35,4 +35,5 @@ __all__ = [
35
35
  'ChatHistoryBlock',
36
36
  'VectorDBBlock',
37
37
  'LongtermAgentMemory',
38
+ 'EmptyMemoryWarning',
38
39
  ]
@@ -51,7 +51,9 @@ class ChatHistoryMemory(AgentMemory):
51
51
  raise ValueError("`window_size` must be non-negative.")
52
52
  self._context_creator = context_creator
53
53
  self._window_size = window_size
54
- self._chat_history_block = ChatHistoryBlock(storage=storage)
54
+ self._chat_history_block = ChatHistoryBlock(
55
+ storage=storage,
56
+ )
55
57
  self._agent_id = agent_id
56
58
 
57
59
  @property
@@ -21,6 +21,17 @@ from camel.storages.key_value_storages.in_memory import InMemoryKeyValueStorage
21
21
  from camel.types import OpenAIBackendRole
22
22
 
23
23
 
24
+ class EmptyMemoryWarning(UserWarning):
25
+ """Warning raised when attempting to access an empty memory.
26
+
27
+ This warning is raised when operations are performed on memory
28
+ that contains no records. It can be safely caught and suppressed
29
+ in contexts where empty memory is expected.
30
+ """
31
+
32
+ pass
33
+
34
+
24
35
  class ChatHistoryBlock(MemoryBlock):
25
36
  r"""An implementation of the :obj:`MemoryBlock` abstract base class for
26
37
  maintaining a record of chat histories.
@@ -39,7 +50,7 @@ class ChatHistoryBlock(MemoryBlock):
39
50
  last message is 1.0, and with each step taken backward, the score
40
51
  of the message is multiplied by the `keep_rate`. Higher `keep_rate`
41
52
  leads to high possibility to keep history messages during context
42
- creation.
53
+ creation. (default: :obj:`0.9`)
43
54
  """
44
55
 
45
56
  def __init__(
@@ -70,7 +81,11 @@ class ChatHistoryBlock(MemoryBlock):
70
81
  """
71
82
  record_dicts = self.storage.load()
72
83
  if len(record_dicts) == 0:
73
- warnings.warn("The `ChatHistoryMemory` is empty.")
84
+ warnings.warn(
85
+ "The `ChatHistoryMemory` is empty.",
86
+ EmptyMemoryWarning,
87
+ stacklevel=1,
88
+ )
74
89
  return list()
75
90
 
76
91
  if window_size is not None and window_size >= 0:
@@ -81,7 +96,10 @@ class ChatHistoryBlock(MemoryBlock):
81
96
  if (
82
97
  record_dicts
83
98
  and record_dicts[0]['role_at_backend']
84
- in {OpenAIBackendRole.SYSTEM, OpenAIBackendRole.DEVELOPER}
99
+ in {
100
+ OpenAIBackendRole.SYSTEM.value,
101
+ OpenAIBackendRole.DEVELOPER.value,
102
+ }
85
103
  )
86
104
  else 0
87
105
  )
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
- kwargs: Dict = record_dict["message"].copy()
75
- kwargs.pop("__class__")
76
- reconstructed_message = message_cls(**kwargs)
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=reconstructed_message,
80
- role_at_backend=record_dict["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
- **asdict(self.message),
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,