camel-ai 0.2.76a9__py3-none-any.whl → 0.2.76a12__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.76a9'
17
+ __version__ = '0.2.76a12'
18
18
 
19
19
  __all__ = [
20
20
  '__version__',
@@ -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
  )
camel/memories/records.py CHANGED
@@ -94,6 +94,42 @@ class MemoryRecord(BaseModel):
94
94
  if "role_type" in data and isinstance(data["role_type"], str):
95
95
  data["role_type"] = RoleType(data["role_type"])
96
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
+
97
133
  # Get valid constructor parameters (cached)
98
134
  valid_params = cls._get_constructor_params(message_cls)
99
135
 
camel/messages/base.py CHANGED
@@ -64,8 +64,9 @@ class BaseMessage:
64
64
  content (str): The content of the message.
65
65
  video_bytes (Optional[bytes]): Optional bytes of a video associated
66
66
  with the message. (default: :obj:`None`)
67
- image_list (Optional[List[Image.Image]]): Optional list of PIL Image
68
- objects associated with the message. (default: :obj:`None`)
67
+ image_list (Optional[List[Union[Image.Image, str]]]): Optional list of
68
+ PIL Image objects or image URLs (strings) associated with the
69
+ message. (default: :obj:`None`)
69
70
  image_detail (Literal["auto", "low", "high"]): Detail level of the
70
71
  images associated with the message. (default: :obj:`auto`)
71
72
  video_detail (Literal["auto", "low", "high"]): Detail level of the
@@ -80,7 +81,7 @@ class BaseMessage:
80
81
  content: str
81
82
 
82
83
  video_bytes: Optional[bytes] = None
83
- image_list: Optional[List[Image.Image]] = None
84
+ image_list: Optional[List[Union[Image.Image, str]]] = None
84
85
  image_detail: Literal["auto", "low", "high"] = "auto"
85
86
  video_detail: Literal["auto", "low", "high"] = "auto"
86
87
  parsed: Optional[Union[BaseModel, dict]] = None
@@ -92,7 +93,7 @@ class BaseMessage:
92
93
  content: str,
93
94
  meta_dict: Optional[Dict[str, str]] = None,
94
95
  video_bytes: Optional[bytes] = None,
95
- image_list: Optional[List[Image.Image]] = None,
96
+ image_list: Optional[List[Union[Image.Image, str]]] = None,
96
97
  image_detail: Union[
97
98
  OpenAIVisionDetailType, str
98
99
  ] = OpenAIVisionDetailType.AUTO,
@@ -109,8 +110,9 @@ class BaseMessage:
109
110
  dictionary for the message.
110
111
  video_bytes (Optional[bytes]): Optional bytes of a video
111
112
  associated with the message.
112
- image_list (Optional[List[Image.Image]]): Optional list of PIL
113
- Image objects associated with the message.
113
+ image_list (Optional[List[Union[Image.Image, str]]]): Optional list
114
+ of PIL Image objects or image URLs (strings) associated with
115
+ the message.
114
116
  image_detail (Union[OpenAIVisionDetailType, str]): Detail level of
115
117
  the images associated with the message.
116
118
  video_detail (Union[OpenAIVisionDetailType, str]): Detail level of
@@ -137,7 +139,7 @@ class BaseMessage:
137
139
  content: str,
138
140
  meta_dict: Optional[Dict[str, str]] = None,
139
141
  video_bytes: Optional[bytes] = None,
140
- image_list: Optional[List[Image.Image]] = None,
142
+ image_list: Optional[List[Union[Image.Image, str]]] = None,
141
143
  image_detail: Union[
142
144
  OpenAIVisionDetailType, str
143
145
  ] = OpenAIVisionDetailType.AUTO,
@@ -154,8 +156,9 @@ class BaseMessage:
154
156
  dictionary for the message.
155
157
  video_bytes (Optional[bytes]): Optional bytes of a video
156
158
  associated with the message.
157
- image_list (Optional[List[Image.Image]]): Optional list of PIL
158
- Image objects associated with the message.
159
+ image_list (Optional[List[Union[Image.Image, str]]]): Optional list
160
+ of PIL Image objects or image URLs (strings) associated with
161
+ the message.
159
162
  image_detail (Union[OpenAIVisionDetailType, str]): Detail level of
160
163
  the images associated with the message.
161
164
  video_detail (Union[OpenAIVisionDetailType, str]): Detail level of
@@ -436,31 +439,64 @@ class BaseMessage:
436
439
  )
437
440
  if self.image_list and len(self.image_list) > 0:
438
441
  for image in self.image_list:
439
- if image.format is None:
440
- # Set default format to PNG as fallback
441
- image.format = 'PNG'
442
-
443
- image_type: str = image.format.lower()
444
- if image_type not in OpenAIImageType:
445
- raise ValueError(
446
- f"Image type {image.format} "
447
- f"is not supported by OpenAI vision model"
442
+ # Check if image is a URL string or PIL Image
443
+ if isinstance(image, str):
444
+ # Image is a URL string
445
+ hybrid_content.append(
446
+ {
447
+ "type": "image_url",
448
+ "image_url": {
449
+ "url": image,
450
+ "detail": self.image_detail,
451
+ },
452
+ }
448
453
  )
449
- with io.BytesIO() as buffer:
450
- image.save(fp=buffer, format=image.format)
451
- encoded_image = base64.b64encode(buffer.getvalue()).decode(
452
- "utf-8"
454
+ else:
455
+ # Image is a PIL Image object
456
+ if image.format is None:
457
+ # Set default format to PNG as fallback
458
+ image.format = 'PNG'
459
+
460
+ image_type: str = image.format.lower()
461
+ if image_type not in OpenAIImageType:
462
+ raise ValueError(
463
+ f"Image type {image.format} "
464
+ f"is not supported by OpenAI vision model"
465
+ )
466
+
467
+ # Convert RGBA to RGB for formats that don't support
468
+ # transparency or when the image has transparency channel
469
+ img_to_save = image
470
+ if image.mode in ('RGBA', 'LA', 'P') and image_type in (
471
+ 'jpeg',
472
+ 'jpg',
473
+ ):
474
+ # JPEG doesn't support transparency, convert to RGB
475
+ img_to_save = image.convert('RGB')
476
+ elif (
477
+ image.mode in ('RGBA', 'LA', 'P')
478
+ and image_type == 'png'
479
+ ):
480
+ # For PNG with transparency, convert to RGBA if needed
481
+ if image.mode in ('LA', 'P'):
482
+ img_to_save = image.convert('RGBA')
483
+ # else: RGBA mode, keep as-is
484
+
485
+ with io.BytesIO() as buffer:
486
+ img_to_save.save(fp=buffer, format=image.format)
487
+ encoded_image = base64.b64encode(
488
+ buffer.getvalue()
489
+ ).decode("utf-8")
490
+ image_prefix = f"data:image/{image_type};base64,"
491
+ hybrid_content.append(
492
+ {
493
+ "type": "image_url",
494
+ "image_url": {
495
+ "url": f"{image_prefix}{encoded_image}",
496
+ "detail": self.image_detail,
497
+ },
498
+ }
453
499
  )
454
- image_prefix = f"data:image/{image_type};base64,"
455
- hybrid_content.append(
456
- {
457
- "type": "image_url",
458
- "image_url": {
459
- "url": f"{image_prefix}{encoded_image}",
460
- "detail": self.image_detail,
461
- },
462
- }
463
- )
464
500
 
465
501
  if self.video_bytes:
466
502
  import imageio.v3 as iio
@@ -552,9 +588,66 @@ class BaseMessage:
552
588
  Returns:
553
589
  dict: The converted dictionary.
554
590
  """
555
- return {
591
+ result = {
556
592
  "role_name": self.role_name,
557
593
  "role_type": self.role_type.value,
558
594
  **(self.meta_dict or {}),
559
595
  "content": self.content,
560
596
  }
597
+
598
+ # Include image/video fields if present
599
+ if self.image_list is not None:
600
+ # Handle both PIL Images and URL strings
601
+ import base64
602
+ from io import BytesIO
603
+
604
+ image_data_list = []
605
+ for img in self.image_list:
606
+ if isinstance(img, str):
607
+ # Image is a URL string, store as-is
608
+ image_data_list.append({"type": "url", "data": img})
609
+ else:
610
+ # Image is a PIL Image, convert to base64
611
+ # Preserve format, default to PNG if not set
612
+ img_format = img.format if img.format else "PNG"
613
+
614
+ # Handle transparency for different formats
615
+ img_to_save = img
616
+ if img.mode in (
617
+ 'RGBA',
618
+ 'LA',
619
+ 'P',
620
+ ) and img_format.upper() in ('JPEG', 'JPG'):
621
+ # JPEG doesn't support transparency, convert to RGB
622
+ img_to_save = img.convert('RGB')
623
+ elif (
624
+ img.mode in ('LA', 'P') and img_format.upper() == 'PNG'
625
+ ):
626
+ # For PNG with transparency, convert to RGBA if needed
627
+ img_to_save = img.convert('RGBA')
628
+ # else: keep as-is for other combinations
629
+
630
+ buffered = BytesIO()
631
+ img_to_save.save(buffered, format=img_format)
632
+ img_str = base64.b64encode(buffered.getvalue()).decode()
633
+ image_data_list.append(
634
+ {
635
+ "type": "base64",
636
+ "data": img_str,
637
+ "format": img_format, # Preserve format
638
+ }
639
+ )
640
+ result["image_list"] = image_data_list
641
+
642
+ if self.video_bytes is not None:
643
+ import base64
644
+
645
+ result["video_bytes"] = base64.b64encode(self.video_bytes).decode()
646
+
647
+ if self.image_detail is not None:
648
+ result["image_detail"] = self.image_detail
649
+
650
+ if self.video_detail is not None:
651
+ result["video_detail"] = self.video_detail
652
+
653
+ return result
@@ -16,8 +16,11 @@ from collections import defaultdict, deque
16
16
  from enum import Enum
17
17
  from typing import Dict, List, Optional, Set
18
18
 
19
+ from camel.logger import get_logger
19
20
  from camel.tasks import Task
20
21
 
22
+ logger = get_logger(__name__)
23
+
21
24
 
22
25
  class PacketStatus(Enum):
23
26
  r"""The status of a packet. The packet can be in one of the following
@@ -269,6 +272,46 @@ class TaskChannel:
269
272
  async with self._condition:
270
273
  return list(self._task_by_status[PacketStatus.ARCHIVED])
271
274
 
275
+ async def get_in_flight_tasks(self, publisher_id: str) -> List[Task]:
276
+ r"""Get all tasks that are currently in-flight (SENT, RETURNED
277
+ or PROCESSING) published by the given publisher.
278
+
279
+ Args:
280
+ publisher_id (str): The ID of the publisher whose
281
+ in-flight tasks to retrieve.
282
+
283
+ Returns:
284
+ List[Task]: List of tasks that are currently in-flight.
285
+ """
286
+ async with self._condition:
287
+ in_flight_tasks = []
288
+ seen_task_ids = set() # Track seen IDs for duplicate detection
289
+
290
+ # Get tasks with SENT, RETURNED or PROCESSING
291
+ # status published by this publisher
292
+ for status in [
293
+ PacketStatus.SENT,
294
+ PacketStatus.PROCESSING,
295
+ PacketStatus.RETURNED,
296
+ ]:
297
+ for task_id in self._task_by_status[status]:
298
+ if task_id in self._task_dict:
299
+ packet = self._task_dict[task_id]
300
+ if packet.publisher_id == publisher_id:
301
+ # Defensive check: detect if task appears in
302
+ # multiple status sets (should never happen)
303
+ if task_id in seen_task_ids:
304
+ logger.warning(
305
+ f"Task {task_id} found in multiple "
306
+ f"status sets. This indicates a bug in "
307
+ f"status management."
308
+ )
309
+ continue
310
+ in_flight_tasks.append(packet.task)
311
+ seen_task_ids.add(task_id)
312
+
313
+ return in_flight_tasks
314
+
272
315
  async def get_task_by_id(self, task_id: str) -> Task:
273
316
  r"""Get a task from the channel by its ID."""
274
317
  async with self._condition: