camel-ai 0.2.78__py3-none-any.whl → 0.2.79a0__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.

camel/__init__.py CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  from camel.logger import disable_logging, enable_logging, set_log_level
16
16
 
17
- __version__ = '0.2.78'
17
+ __version__ = '0.2.79a0'
18
18
 
19
19
  __all__ = [
20
20
  '__version__',
@@ -71,7 +71,6 @@ from camel.memories import (
71
71
  MemoryRecord,
72
72
  ScoreBasedContextCreator,
73
73
  )
74
- from camel.memories.blocks.chat_history_block import EmptyMemoryWarning
75
74
  from camel.messages import (
76
75
  BaseMessage,
77
76
  FunctionCallingMessage,
@@ -877,9 +876,7 @@ class ChatAgent(BaseAgent):
877
876
  [message.to_openai_message(role)]
878
877
  )
879
878
 
880
- with warnings.catch_warnings():
881
- warnings.filterwarnings("ignore", category=EmptyMemoryWarning)
882
- _, ctx_tokens = self.memory.get_context()
879
+ _, ctx_tokens = self.memory.get_context()
883
880
 
884
881
  remaining_budget = max(0, token_limit - ctx_tokens)
885
882
 
@@ -1077,6 +1074,10 @@ class ChatAgent(BaseAgent):
1077
1074
  r"""Summarize the agent's current conversation context and persist it
1078
1075
  to a markdown file.
1079
1076
 
1077
+ .. deprecated:: 0.2.80
1078
+ Use :meth:`asummarize` for async/await support and better
1079
+ performance in parallel summarization workflows.
1080
+
1080
1081
  Args:
1081
1082
  filename (Optional[str]): The base filename (without extension) to
1082
1083
  use for the markdown file. Defaults to a timestamped name when
@@ -1096,8 +1097,18 @@ class ChatAgent(BaseAgent):
1096
1097
  Dict[str, Any]: A dictionary containing the summary text, file
1097
1098
  path, status message, and optionally structured_summary if
1098
1099
  response_format was provided.
1100
+
1101
+ See Also:
1102
+ :meth:`asummarize`: Async version for non-blocking LLM calls.
1099
1103
  """
1100
1104
 
1105
+ warnings.warn(
1106
+ "summarize() is synchronous. Consider using asummarize() "
1107
+ "for async/await support and better performance.",
1108
+ DeprecationWarning,
1109
+ stacklevel=2,
1110
+ )
1111
+
1101
1112
  result: Dict[str, Any] = {
1102
1113
  "summary": "",
1103
1114
  "file_path": None,
@@ -1247,11 +1258,29 @@ class ChatAgent(BaseAgent):
1247
1258
  result["status"] = status_message
1248
1259
  return result
1249
1260
 
1250
- base_filename = (
1251
- filename
1252
- if filename
1253
- else f"context_summary_{datetime.now().strftime('%Y%m%d_%H%M%S')}" # noqa: E501
1254
- )
1261
+ # handle structured output if response_format was provided
1262
+ structured_output = None
1263
+ if response_format and response.msgs[-1].parsed:
1264
+ structured_output = response.msgs[-1].parsed
1265
+
1266
+ # determine filename: use provided filename, or extract from
1267
+ # structured output, or generate timestamp
1268
+ if filename:
1269
+ base_filename = filename
1270
+ elif structured_output and hasattr(
1271
+ structured_output, 'task_title'
1272
+ ):
1273
+ # use task_title from structured output for filename
1274
+ task_title = structured_output.task_title
1275
+ clean_title = ContextUtility.sanitize_workflow_filename(
1276
+ task_title
1277
+ )
1278
+ base_filename = (
1279
+ f"{clean_title}_workflow" if clean_title else "workflow"
1280
+ )
1281
+ else:
1282
+ base_filename = f"context_summary_{datetime.now().strftime('%Y%m%d_%H%M%S')}" # noqa: E501
1283
+
1255
1284
  base_filename = Path(base_filename).with_suffix("").name
1256
1285
 
1257
1286
  metadata = context_util.get_session_metadata()
@@ -1262,11 +1291,274 @@ class ChatAgent(BaseAgent):
1262
1291
  }
1263
1292
  )
1264
1293
 
1265
- # Handle structured output if response_format was provided
1294
+ # convert structured output to custom markdown if present
1295
+ if structured_output:
1296
+ # convert structured output to custom markdown
1297
+ summary_content = context_util.structured_output_to_markdown(
1298
+ structured_data=structured_output, metadata=metadata
1299
+ )
1300
+
1301
+ # Save the markdown (either custom structured or default)
1302
+ save_status = context_util.save_markdown_file(
1303
+ base_filename,
1304
+ summary_content,
1305
+ title="Conversation Summary"
1306
+ if not structured_output
1307
+ else None,
1308
+ metadata=metadata if not structured_output else None,
1309
+ )
1310
+
1311
+ file_path = (
1312
+ context_util.get_working_directory() / f"{base_filename}.md"
1313
+ )
1314
+
1315
+ # Prepare result dictionary
1316
+ result_dict = {
1317
+ "summary": summary_content,
1318
+ "file_path": str(file_path),
1319
+ "status": save_status,
1320
+ "structured_summary": structured_output,
1321
+ }
1322
+
1323
+ result.update(result_dict)
1324
+ logger.info("Conversation summary saved to %s", file_path)
1325
+ return result
1326
+
1327
+ except Exception as exc:
1328
+ error_message = f"Failed to summarize conversation context: {exc}"
1329
+ logger.error(error_message)
1330
+ result["status"] = error_message
1331
+ return result
1332
+
1333
+ async def asummarize(
1334
+ self,
1335
+ filename: Optional[str] = None,
1336
+ summary_prompt: Optional[str] = None,
1337
+ response_format: Optional[Type[BaseModel]] = None,
1338
+ working_directory: Optional[Union[str, Path]] = None,
1339
+ ) -> Dict[str, Any]:
1340
+ r"""Asynchronously summarize the agent's current conversation context
1341
+ and persist it to a markdown file.
1342
+
1343
+ This is the async version of summarize() that uses astep() for
1344
+ non-blocking LLM calls, enabling parallel summarization of multiple
1345
+ agents.
1346
+
1347
+ Args:
1348
+ filename (Optional[str]): The base filename (without extension) to
1349
+ use for the markdown file. Defaults to a timestamped name when
1350
+ not provided.
1351
+ summary_prompt (Optional[str]): Custom prompt for the summarizer.
1352
+ When omitted, a default prompt highlighting key decisions,
1353
+ action items, and open questions is used.
1354
+ response_format (Optional[Type[BaseModel]]): A Pydantic model
1355
+ defining the expected structure of the response. If provided,
1356
+ the summary will be generated as structured output and included
1357
+ in the result.
1358
+ working_directory (Optional[str|Path]): Optional directory to save
1359
+ the markdown summary file. If provided, overrides the default
1360
+ directory used by ContextUtility.
1361
+
1362
+ Returns:
1363
+ Dict[str, Any]: A dictionary containing the summary text, file
1364
+ path, status message, and optionally structured_summary if
1365
+ response_format was provided.
1366
+ """
1367
+
1368
+ result: Dict[str, Any] = {
1369
+ "summary": "",
1370
+ "file_path": None,
1371
+ "status": "",
1372
+ }
1373
+
1374
+ try:
1375
+ # Use external context if set, otherwise create local one
1376
+ if self._context_utility is None:
1377
+ if working_directory is not None:
1378
+ self._context_utility = ContextUtility(
1379
+ working_directory=str(working_directory)
1380
+ )
1381
+ else:
1382
+ self._context_utility = ContextUtility()
1383
+ context_util = self._context_utility
1384
+
1385
+ # Get conversation directly from agent's memory
1386
+ messages, _ = self.memory.get_context()
1387
+
1388
+ if not messages:
1389
+ status_message = (
1390
+ "No conversation context available to summarize."
1391
+ )
1392
+ result["status"] = status_message
1393
+ return result
1394
+
1395
+ # Convert messages to conversation text
1396
+ conversation_lines = []
1397
+ for message in messages:
1398
+ role = message.get('role', 'unknown')
1399
+ content = message.get('content', '')
1400
+
1401
+ # Handle tool call messages (assistant calling tools)
1402
+ tool_calls = message.get('tool_calls')
1403
+ if tool_calls and isinstance(tool_calls, (list, tuple)):
1404
+ for tool_call in tool_calls:
1405
+ # Handle both dict and object formats
1406
+ if isinstance(tool_call, dict):
1407
+ func_name = tool_call.get('function', {}).get(
1408
+ 'name', 'unknown_tool'
1409
+ )
1410
+ func_args_str = tool_call.get('function', {}).get(
1411
+ 'arguments', '{}'
1412
+ )
1413
+ else:
1414
+ # Handle object format (Pydantic or similar)
1415
+ func_name = getattr(
1416
+ getattr(tool_call, 'function', None),
1417
+ 'name',
1418
+ 'unknown_tool',
1419
+ )
1420
+ func_args_str = getattr(
1421
+ getattr(tool_call, 'function', None),
1422
+ 'arguments',
1423
+ '{}',
1424
+ )
1425
+
1426
+ # Parse and format arguments for readability
1427
+ try:
1428
+ import json
1429
+
1430
+ args_dict = json.loads(func_args_str)
1431
+ args_formatted = ', '.join(
1432
+ f"{k}={v}" for k, v in args_dict.items()
1433
+ )
1434
+ except (json.JSONDecodeError, ValueError, TypeError):
1435
+ args_formatted = func_args_str
1436
+
1437
+ conversation_lines.append(
1438
+ f"[TOOL CALL] {func_name}({args_formatted})"
1439
+ )
1440
+
1441
+ # Handle tool response messages
1442
+ elif role == 'tool':
1443
+ tool_name = message.get('name', 'unknown_tool')
1444
+ if not content:
1445
+ content = str(message.get('content', ''))
1446
+ conversation_lines.append(
1447
+ f"[TOOL RESULT] {tool_name} → {content}"
1448
+ )
1449
+
1450
+ # Handle regular content messages (user/assistant/system)
1451
+ elif content:
1452
+ conversation_lines.append(f"{role}: {content}")
1453
+
1454
+ conversation_text = "\n".join(conversation_lines).strip()
1455
+
1456
+ if not conversation_text:
1457
+ status_message = (
1458
+ "Conversation context is empty; skipping summary."
1459
+ )
1460
+ result["status"] = status_message
1461
+ return result
1462
+
1463
+ if self._context_summary_agent is None:
1464
+ self._context_summary_agent = ChatAgent(
1465
+ system_message=(
1466
+ "You are a helpful assistant that summarizes "
1467
+ "conversations"
1468
+ ),
1469
+ model=self.model_backend,
1470
+ agent_id=f"{self.agent_id}_context_summarizer",
1471
+ )
1472
+ else:
1473
+ self._context_summary_agent.reset()
1474
+
1475
+ if summary_prompt:
1476
+ prompt_text = (
1477
+ f"{summary_prompt.rstrip()}\n\n"
1478
+ f"AGENT CONVERSATION TO BE SUMMARIZED:\n"
1479
+ f"{conversation_text}"
1480
+ )
1481
+ else:
1482
+ prompt_text = (
1483
+ "Summarize the context information in concise markdown "
1484
+ "bullet points highlighting key decisions, action items.\n"
1485
+ f"Context information:\n{conversation_text}"
1486
+ )
1487
+
1488
+ try:
1489
+ # Use structured output if response_format is provided
1490
+ if response_format:
1491
+ response = await self._context_summary_agent.astep(
1492
+ prompt_text, response_format=response_format
1493
+ )
1494
+ else:
1495
+ response = await self._context_summary_agent.astep(
1496
+ prompt_text
1497
+ )
1498
+
1499
+ # Handle streaming response
1500
+ if isinstance(response, AsyncStreamingChatAgentResponse):
1501
+ # Collect final response
1502
+ final_response = await response
1503
+ response = final_response
1504
+
1505
+ except Exception as step_exc:
1506
+ error_message = (
1507
+ f"Failed to generate summary using model: {step_exc}"
1508
+ )
1509
+ logger.error(error_message)
1510
+ result["status"] = error_message
1511
+ return result
1512
+
1513
+ if not response.msgs:
1514
+ status_message = (
1515
+ "Failed to generate summary from model response."
1516
+ )
1517
+ result["status"] = status_message
1518
+ return result
1519
+
1520
+ summary_content = response.msgs[-1].content.strip()
1521
+ if not summary_content:
1522
+ status_message = "Generated summary is empty."
1523
+ result["status"] = status_message
1524
+ return result
1525
+
1526
+ # handle structured output if response_format was provided
1266
1527
  structured_output = None
1267
1528
  if response_format and response.msgs[-1].parsed:
1268
1529
  structured_output = response.msgs[-1].parsed
1269
- # Convert structured output to custom markdown
1530
+
1531
+ # determine filename: use provided filename, or extract from
1532
+ # structured output, or generate timestamp
1533
+ if filename:
1534
+ base_filename = filename
1535
+ elif structured_output and hasattr(
1536
+ structured_output, 'task_title'
1537
+ ):
1538
+ # use task_title from structured output for filename
1539
+ task_title = structured_output.task_title
1540
+ clean_title = ContextUtility.sanitize_workflow_filename(
1541
+ task_title
1542
+ )
1543
+ base_filename = (
1544
+ f"{clean_title}_workflow" if clean_title else "workflow"
1545
+ )
1546
+ else:
1547
+ base_filename = f"context_summary_{datetime.now().strftime('%Y%m%d_%H%M%S')}" # noqa: E501
1548
+
1549
+ base_filename = Path(base_filename).with_suffix("").name
1550
+
1551
+ metadata = context_util.get_session_metadata()
1552
+ metadata.update(
1553
+ {
1554
+ "agent_id": self.agent_id,
1555
+ "message_count": len(messages),
1556
+ }
1557
+ )
1558
+
1559
+ # convert structured output to custom markdown if present
1560
+ if structured_output:
1561
+ # convert structured output to custom markdown
1270
1562
  summary_content = context_util.structured_output_to_markdown(
1271
1563
  structured_data=structured_output, metadata=metadata
1272
1564
  )
@@ -1347,7 +1639,7 @@ class ChatAgent(BaseAgent):
1347
1639
  message.
1348
1640
  """
1349
1641
  self.memory.clear()
1350
- # avoid UserWarning: The `ChatHistoryMemory` is empty.
1642
+ # Write system message to memory if provided
1351
1643
  if self.system_message is not None:
1352
1644
  self.memory.write_record(
1353
1645
  MemoryRecord(
@@ -62,6 +62,7 @@ class BaseGenerator(abc.ABC, IterableDataset):
62
62
  self._buffer = buffer
63
63
  self._data: List[DataPoint] = []
64
64
  self._batch_to_save: List[DataPoint] = []
65
+ self._iter_position: int = 0
65
66
 
66
67
  if data_path:
67
68
  file_path = Path(data_path)
@@ -103,9 +104,9 @@ class BaseGenerator(abc.ABC, IterableDataset):
103
104
  r"""Async iterator that yields datapoints dynamically.
104
105
 
105
106
  If a `data_path` was provided during initialization, those datapoints
106
- are yielded first. When self._data is empty, 20 new datapoints
107
- are generated. Every 100 yields, the batch is appended to the
108
- JSONL file or discarded if `cache` is None.
107
+ are yielded first. When self._iter_position reaches the end of _data,
108
+ new datapoints are generated. Every 100 yields, the batch is appended
109
+ to the JSONL file or discarded if `cache` is None.
109
110
 
110
111
  Yields:
111
112
  DataPoint: A single datapoint.
@@ -113,9 +114,10 @@ class BaseGenerator(abc.ABC, IterableDataset):
113
114
 
114
115
  async def generator():
115
116
  while True:
116
- if not self._data:
117
+ if self._iter_position >= len(self._data):
117
118
  await self.generate_new(self._buffer)
118
- datapoint = self._data.pop(0)
119
+ datapoint = self._data[self._iter_position]
120
+ self._iter_position += 1
119
121
  yield datapoint
120
122
  self._batch_to_save.append(datapoint)
121
123
  if len(self._batch_to_save) == 100:
@@ -132,9 +134,9 @@ class BaseGenerator(abc.ABC, IterableDataset):
132
134
  r"""Synchronous iterator for PyTorch IterableDataset compatibility.
133
135
 
134
136
  If a `data_path` was provided during initialization, those datapoints
135
- are yielded first. When self._data is empty, 20 new datapoints
136
- are generated. Every 100 yields, the batch is appended to the
137
- JSONL file or discarded if `cache` is None.
137
+ are yielded first. When self._iter_position reaches the end of _data,
138
+ new datapoints are generated. Every 100 yields, the batch is appended
139
+ to the JSONL file or discarded if `cache` is None.
138
140
 
139
141
  Yields:
140
142
  DataPoint: A single datapoint.
@@ -150,9 +152,10 @@ class BaseGenerator(abc.ABC, IterableDataset):
150
152
  raise
151
153
 
152
154
  while True:
153
- if not self._data:
155
+ if self._iter_position >= len(self._data):
154
156
  asyncio.run(self.generate_new(self._buffer))
155
- datapoint = self._data.pop(0)
157
+ datapoint = self._data[self._iter_position]
158
+ self._iter_position += 1
156
159
  yield datapoint
157
160
  self._batch_to_save.append(datapoint)
158
161
  if len(self._batch_to_save) == 100:
@@ -248,6 +251,7 @@ class BaseGenerator(abc.ABC, IterableDataset):
248
251
 
249
252
  self.save_to_jsonl(file_path)
250
253
  self._data = []
254
+ self._iter_position = 0
251
255
  logger.info(f"Data flushed to {file_path} and cleared from the memory")
252
256
 
253
257
  def _init_from_jsonl(self, file_path: Path) -> List[Dict[str, Any]]:
@@ -290,3 +294,28 @@ class BaseGenerator(abc.ABC, IterableDataset):
290
294
  f"Successfully loaded {len(raw_data)} items from {file_path}"
291
295
  )
292
296
  return raw_data
297
+
298
+ def __getitem__(self, index: int) -> DataPoint:
299
+ r"""Get a datapoint by index without removing the datapoint from _data.
300
+
301
+ Args:
302
+ index (int): Index of the datapoint to retrieve.
303
+
304
+ Returns:
305
+ DataPoint: The datapoint at the specified index.
306
+
307
+ Raises:
308
+ IndexError: If the index is out of range.
309
+ """
310
+ if index < 0 or index >= len(self._data):
311
+ raise IndexError(f"Index {index} is out of range")
312
+
313
+ return self._data[index]
314
+
315
+ def __len__(self) -> int:
316
+ r"""Get the number of datapoints in the dataset.
317
+
318
+ Returns:
319
+ int: The number of datapoints.
320
+ """
321
+ return len(self._data)
@@ -218,9 +218,34 @@ class SingleStepEnv:
218
218
  return observations[0] if batch_size == 1 else observations
219
219
 
220
220
  elif isinstance(self.dataset, BaseGenerator):
221
- self._states = [
222
- await self.dataset.async_sample() for _ in range(batch_size)
223
- ]
221
+ # Generate more data if needed
222
+ if batch_size > len(self.dataset):
223
+ new_datapoints_needed = batch_size - len(self.dataset)
224
+ await self.dataset.generate_new(n=new_datapoints_needed)
225
+
226
+ # Verify that enough data was generated
227
+ if len(self.dataset) < batch_size:
228
+ raise RuntimeError(
229
+ f"Failed to generate enough datapoints. "
230
+ f"Requested {batch_size}, but only "
231
+ f"{len(self.dataset)} available after generation."
232
+ )
233
+
234
+ # Choose sampling strategy based on whether seed is provided
235
+ if seed is not None:
236
+ # Deterministic random sampling when seed is provided
237
+ random_indices = rng.sample(
238
+ range(len(self.dataset)), batch_size
239
+ )
240
+ self._states = [self.dataset[ind] for ind in random_indices]
241
+ else:
242
+ # Sequential sampling when no seed (backward compatible)
243
+ # Use async_sample to maintain sequential behavior
244
+ self._states = [
245
+ await self.dataset.async_sample()
246
+ for _ in range(batch_size)
247
+ ]
248
+
224
249
  self.current_batch_size = batch_size
225
250
  self._states_done = [False] * batch_size
226
251
 
@@ -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, EmptyMemoryWarning
21
+ from .blocks.chat_history_block import ChatHistoryBlock
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,5 +35,4 @@ __all__ = [
35
35
  'ChatHistoryBlock',
36
36
  'VectorDBBlock',
37
37
  'LongtermAgentMemory',
38
- 'EmptyMemoryWarning',
39
38
  ]
@@ -11,7 +11,6 @@
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
- import warnings
15
14
  from typing import List, Optional
16
15
 
17
16
  from camel.memories.base import MemoryBlock
@@ -21,17 +20,6 @@ from camel.storages.key_value_storages.in_memory import InMemoryKeyValueStorage
21
20
  from camel.types import OpenAIBackendRole
22
21
 
23
22
 
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
-
35
23
  class ChatHistoryBlock(MemoryBlock):
36
24
  r"""An implementation of the :obj:`MemoryBlock` abstract base class for
37
25
  maintaining a record of chat histories.
@@ -81,11 +69,8 @@ class ChatHistoryBlock(MemoryBlock):
81
69
  """
82
70
  record_dicts = self.storage.load()
83
71
  if len(record_dicts) == 0:
84
- warnings.warn(
85
- "The `ChatHistoryMemory` is empty.",
86
- EmptyMemoryWarning,
87
- stacklevel=1,
88
- )
72
+ # Empty memory is a valid state (e.g., during initialization).
73
+ # Users can check if memory is empty by checking the returned list.
89
74
  return list()
90
75
 
91
76
  if window_size is not None and window_size >= 0:
@@ -13,17 +13,11 @@
13
13
  # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
14
14
 
15
15
  import os
16
- from typing import Any, Dict, List, Optional, Type, Union
17
-
18
- from openai import AsyncStream
19
- from pydantic import BaseModel
16
+ from typing import Any, Dict, Optional, Union
20
17
 
21
18
  from camel.configs import BedrockConfig
22
- from camel.messages import OpenAIMessage
23
19
  from camel.models.openai_compatible_model import OpenAICompatibleModel
24
20
  from camel.types import (
25
- ChatCompletion,
26
- ChatCompletionChunk,
27
21
  ModelType,
28
22
  )
29
23
  from camel.utils import BaseTokenCounter, api_keys_required
@@ -93,13 +87,3 @@ class AWSBedrockModel(OpenAICompatibleModel):
93
87
  max_retries=max_retries,
94
88
  **kwargs,
95
89
  )
96
-
97
- async def _arun(
98
- self,
99
- messages: List[OpenAIMessage],
100
- response_format: Optional[Type[BaseModel]] = None,
101
- tools: Optional[List[Dict[str, Any]]] = None,
102
- ) -> Union[ChatCompletion, AsyncStream[ChatCompletionChunk]]:
103
- raise NotImplementedError(
104
- "AWS Bedrock does not support async inference."
105
- )