camel-ai 0.2.24__py3-none-any.whl → 0.2.26__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 +1 -1
- camel/agents/chat_agent.py +4 -4
- camel/agents/knowledge_graph_agent.py +15 -3
- camel/configs/anthropic_config.py +0 -1
- camel/configs/sglang_config.py +7 -5
- camel/datasets/base.py +219 -17
- camel/environments/base.py +16 -8
- camel/extractors/__init__.py +2 -2
- camel/extractors/base.py +86 -64
- camel/extractors/python_strategies.py +226 -0
- camel/interpreters/subprocess_interpreter.py +187 -46
- camel/models/anthropic_model.py +19 -55
- camel/models/sglang_model.py +35 -5
- camel/py.typed +0 -0
- camel/storages/graph_storages/graph_element.py +3 -1
- camel/storages/graph_storages/neo4j_graph.py +78 -4
- camel/toolkits/__init__.py +2 -0
- camel/toolkits/pubmed_toolkit.py +346 -0
- camel/toolkits/terminal_toolkit.py +2 -2
- {camel_ai-0.2.24.dist-info → camel_ai-0.2.26.dist-info}/METADATA +2 -1
- {camel_ai-0.2.24.dist-info → camel_ai-0.2.26.dist-info}/RECORD +23 -20
- {camel_ai-0.2.24.dist-info → camel_ai-0.2.26.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.24.dist-info → camel_ai-0.2.26.dist-info}/licenses/LICENSE +0 -0
camel/__init__.py
CHANGED
camel/agents/chat_agent.py
CHANGED
|
@@ -699,12 +699,12 @@ class ChatAgent(BaseAgent):
|
|
|
699
699
|
if not response and self.model_backend.num_models > 1:
|
|
700
700
|
raise ModelProcessingError(
|
|
701
701
|
"Unable to process messages: none of the provided models "
|
|
702
|
-
"run
|
|
702
|
+
"run successfully."
|
|
703
703
|
)
|
|
704
704
|
elif not response:
|
|
705
705
|
raise ModelProcessingError(
|
|
706
706
|
f"Unable to process messages: the only provided model "
|
|
707
|
-
f"did not run
|
|
707
|
+
f"did not run successfully. Error: {error_info}"
|
|
708
708
|
)
|
|
709
709
|
|
|
710
710
|
logger.info(
|
|
@@ -744,12 +744,12 @@ class ChatAgent(BaseAgent):
|
|
|
744
744
|
if not response and self.model_backend.num_models > 1:
|
|
745
745
|
raise ModelProcessingError(
|
|
746
746
|
"Unable to process messages: none of the provided models "
|
|
747
|
-
"run
|
|
747
|
+
"run successfully."
|
|
748
748
|
)
|
|
749
749
|
elif not response:
|
|
750
750
|
raise ModelProcessingError(
|
|
751
751
|
f"Unable to process messages: the only provided model "
|
|
752
|
-
f"did not run
|
|
752
|
+
f"did not run successfully. Error: {error_info}"
|
|
753
753
|
)
|
|
754
754
|
|
|
755
755
|
logger.info(
|
|
@@ -226,7 +226,8 @@ class KnowledgeGraphAgent(ChatAgent):
|
|
|
226
226
|
node_pattern = r"Node\(id='(.*?)', type='(.*?)'\)"
|
|
227
227
|
rel_pattern = (
|
|
228
228
|
r"Relationship\(subj=Node\(id='(.*?)', type='(.*?)'\), "
|
|
229
|
-
r"obj=Node\(id='(.*?)', type='(.*?)'\),
|
|
229
|
+
r"obj=Node\(id='(.*?)', type='(.*?)'\), "
|
|
230
|
+
r"type='(.*?)'(?:, timestamp='(.*?)')?\)"
|
|
230
231
|
)
|
|
231
232
|
|
|
232
233
|
nodes = {}
|
|
@@ -243,13 +244,24 @@ class KnowledgeGraphAgent(ChatAgent):
|
|
|
243
244
|
|
|
244
245
|
# Extract relationships
|
|
245
246
|
for match in re.finditer(rel_pattern, input_string):
|
|
246
|
-
|
|
247
|
+
groups = match.groups()
|
|
248
|
+
if len(groups) == 6:
|
|
249
|
+
subj_id, subj_type, obj_id, obj_type, rel_type, timestamp = (
|
|
250
|
+
groups
|
|
251
|
+
)
|
|
252
|
+
else:
|
|
253
|
+
subj_id, subj_type, obj_id, obj_type, rel_type = groups
|
|
254
|
+
timestamp = None
|
|
247
255
|
properties = {'source': 'agent_created'}
|
|
248
256
|
if subj_id in nodes and obj_id in nodes:
|
|
249
257
|
subj = nodes[subj_id]
|
|
250
258
|
obj = nodes[obj_id]
|
|
251
259
|
relationship = Relationship(
|
|
252
|
-
subj=subj,
|
|
260
|
+
subj=subj,
|
|
261
|
+
obj=obj,
|
|
262
|
+
type=rel_type,
|
|
263
|
+
timestamp=timestamp,
|
|
264
|
+
properties=properties,
|
|
253
265
|
)
|
|
254
266
|
if self._validate_relationship(relationship):
|
|
255
267
|
relationships.append(relationship)
|
|
@@ -70,7 +70,6 @@ class AnthropicConfig(BaseConfig):
|
|
|
70
70
|
stop_sequences: ClassVar[Union[List[str], NotGiven]] = []
|
|
71
71
|
temperature: float = 1
|
|
72
72
|
top_p: Union[float, NotGiven] = 0.7
|
|
73
|
-
top_k: Union[int, NotGiven] = 5
|
|
74
73
|
stream: bool = False
|
|
75
74
|
metadata: Union[dict, NotGiven] = NotGiven()
|
|
76
75
|
thinking: Union[dict, NotGiven] = NotGiven()
|
camel/configs/sglang_config.py
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
-
from typing import Sequence, Union
|
|
16
|
+
from typing import Any, Dict, List, Optional, Sequence, Union
|
|
17
17
|
|
|
18
18
|
from camel.configs.base_config import BaseConfig
|
|
19
19
|
from camel.types import NOT_GIVEN, NotGiven
|
|
@@ -56,10 +56,11 @@ class SGLangConfig(BaseConfig):
|
|
|
56
56
|
in the chat completion. The total length of input tokens and
|
|
57
57
|
generated tokens is limited by the model's context length.
|
|
58
58
|
(default: :obj:`None`)
|
|
59
|
-
tools (list[
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
tools (list[Dict[str, Any]], optional): A list of tool definitions
|
|
60
|
+
that the model can dynamically invoke. Each tool should be
|
|
61
|
+
defined as a dictionary following OpenAI's function calling
|
|
62
|
+
specification format. For more details, refer to the OpenAI
|
|
63
|
+
documentation.
|
|
63
64
|
"""
|
|
64
65
|
|
|
65
66
|
stop: Union[str, Sequence[str], NotGiven] = NOT_GIVEN
|
|
@@ -70,6 +71,7 @@ class SGLangConfig(BaseConfig):
|
|
|
70
71
|
presence_penalty: float = 0.0
|
|
71
72
|
stream: bool = False
|
|
72
73
|
max_tokens: Union[int, NotGiven] = NOT_GIVEN
|
|
74
|
+
tools: Optional[Union[List[Dict[str, Any]]]] = None
|
|
73
75
|
|
|
74
76
|
|
|
75
77
|
SGLANG_API_PARAMS = {param for param in SGLangConfig.model_fields.keys()}
|
camel/datasets/base.py
CHANGED
|
@@ -12,14 +12,17 @@
|
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
14
14
|
|
|
15
|
+
import json
|
|
15
16
|
import os
|
|
16
17
|
import random
|
|
18
|
+
from pathlib import Path
|
|
17
19
|
from typing import (
|
|
18
20
|
Any,
|
|
19
21
|
Callable,
|
|
20
22
|
Dict,
|
|
21
23
|
List,
|
|
22
24
|
Optional,
|
|
25
|
+
Sized,
|
|
23
26
|
TypeVar,
|
|
24
27
|
Union,
|
|
25
28
|
)
|
|
@@ -326,42 +329,241 @@ class SeedDataset(BaseDataset):
|
|
|
326
329
|
r"""A dataset containing validated seed examples for data generation.
|
|
327
330
|
Ensures that all items adhere to the DataPoint schema.
|
|
328
331
|
|
|
329
|
-
This class
|
|
330
|
-
|
|
332
|
+
This class can initialize from Hugging Face Datasets,
|
|
333
|
+
PyTorch Datasets, JSON file paths, or lists of dictionaries,
|
|
334
|
+
converting them into a consistent internal format.
|
|
331
335
|
"""
|
|
332
336
|
|
|
333
337
|
def __init__(
|
|
334
338
|
self,
|
|
335
|
-
data: List[Dict[str,
|
|
339
|
+
data: Union[HFDataset, Dataset, Path, List[Dict[str, Any]]],
|
|
336
340
|
cache_dir: Optional[str] = None,
|
|
341
|
+
seed: Optional[int] = None,
|
|
337
342
|
min_samples: int = 1,
|
|
343
|
+
strict: bool = False,
|
|
338
344
|
**kwargs,
|
|
339
345
|
):
|
|
340
|
-
r"""Initialize the seed dataset.
|
|
346
|
+
r"""Initialize the seed dataset and validate integrity.
|
|
341
347
|
|
|
342
348
|
Args:
|
|
343
|
-
data (List[Dict[str,
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
(
|
|
349
|
+
data (Union[HFDataset, Dataset, Path, List[Dict[str, Any]]]):
|
|
350
|
+
Input data, which can be:
|
|
351
|
+
- A Hugging Face Dataset (HFDataset)
|
|
352
|
+
- A PyTorch Dataset (torch.utils.data.Dataset)
|
|
353
|
+
- A Path object representing the path to a JSON file
|
|
354
|
+
- A list of dictionaries with DataPoint-compatible fields
|
|
355
|
+
seed (Optional[int]): Seed for reproducibility.
|
|
356
|
+
(default: :obj:`1`)
|
|
347
357
|
min_samples (int): Minimum number of samples required.
|
|
348
358
|
(default: :obj:`1`)
|
|
359
|
+
strict (bool): Whether to raise an error on invalid datapoints
|
|
360
|
+
(True) or skip/filter them (False). (default: False)
|
|
349
361
|
**kwargs: Additional dataset parameters.
|
|
350
362
|
|
|
351
363
|
Raises:
|
|
352
|
-
|
|
353
|
-
|
|
364
|
+
TypeError: If the data type is not supported.
|
|
365
|
+
ValueError: If dataset size is less than min_samples or
|
|
366
|
+
if sample validation fails.
|
|
367
|
+
FileNotFoundError: If the JSON file path doesn't exist.
|
|
368
|
+
json.JSONDecodeError: If the JSON file is invalid.
|
|
354
369
|
"""
|
|
355
|
-
|
|
370
|
+
# Initialize BaseDataset with empty data, we'll populate it ourselves
|
|
371
|
+
super().__init__(data=[], cache_dir=cache_dir, **kwargs)
|
|
372
|
+
|
|
373
|
+
self._rng = random.Random(seed)
|
|
374
|
+
self._strict = strict
|
|
375
|
+
|
|
376
|
+
# Type checking and conversion into list of dicts to have a
|
|
377
|
+
# consistent internal format. Since Seed Dataset should be
|
|
378
|
+
# small, we can load it entirely into memory
|
|
379
|
+
|
|
380
|
+
self.data: List[DataPoint] = self._init_data(data)
|
|
381
|
+
self._length = len(self.data)
|
|
382
|
+
|
|
383
|
+
if self._length < min_samples:
|
|
356
384
|
raise ValueError(
|
|
357
|
-
|
|
385
|
+
"The dataset does not contain enough samples. "
|
|
386
|
+
f"Need {max(0, min_samples)}, got {self._length}"
|
|
358
387
|
)
|
|
359
388
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
389
|
+
def _init_data(
|
|
390
|
+
self, data: Union[HFDataset, Dataset, Path, List[Dict[str, Any]]]
|
|
391
|
+
) -> List[DataPoint]:
|
|
392
|
+
if isinstance(data, HFDataset):
|
|
393
|
+
raw_data = self._init_from_hf_dataset(data)
|
|
394
|
+
elif isinstance(data, Dataset):
|
|
395
|
+
raw_data = self._init_from_pytorch_dataset(data)
|
|
396
|
+
elif isinstance(data, Path):
|
|
397
|
+
raw_data = self._init_from_json_path(data)
|
|
398
|
+
elif isinstance(data, list):
|
|
399
|
+
raw_data = self._init_from_list(data)
|
|
400
|
+
else:
|
|
401
|
+
raise TypeError("Unsupported data type")
|
|
402
|
+
|
|
403
|
+
def create_datapoint(
|
|
404
|
+
item: Dict[str, Any], idx: int
|
|
405
|
+
) -> Optional[DataPoint]:
|
|
406
|
+
# Add type checks for required fields to make mypy happy
|
|
407
|
+
question = item.get('question')
|
|
408
|
+
if not isinstance(question, str):
|
|
409
|
+
if self._strict:
|
|
410
|
+
raise ValueError(
|
|
411
|
+
f"Sample at index {idx} has invalid 'question': "
|
|
412
|
+
f"expected str, got {type(question)}"
|
|
413
|
+
)
|
|
414
|
+
else:
|
|
415
|
+
logger.warning(
|
|
416
|
+
f"Skipping sample at index {idx}: invalid 'question'"
|
|
417
|
+
)
|
|
418
|
+
return None
|
|
419
|
+
|
|
420
|
+
rationale = item.get('rationale')
|
|
421
|
+
if not isinstance(rationale, str):
|
|
422
|
+
if self._strict:
|
|
423
|
+
raise ValueError(
|
|
424
|
+
f"Sample at index {idx} has invalid 'rationale': "
|
|
425
|
+
f"expected str, got {type(rationale)}"
|
|
426
|
+
)
|
|
427
|
+
else:
|
|
428
|
+
logger.warning(
|
|
429
|
+
f"Skipping sample at index {idx}: invalid 'rationale'"
|
|
430
|
+
)
|
|
431
|
+
return None
|
|
432
|
+
|
|
433
|
+
final_answer = item.get('final_answer')
|
|
434
|
+
if not isinstance(final_answer, str):
|
|
435
|
+
if self._strict:
|
|
436
|
+
raise ValueError(
|
|
437
|
+
f"Sample at index {idx} has invalid 'final_answer': "
|
|
438
|
+
f"expected str, got {type(final_answer)}"
|
|
439
|
+
)
|
|
440
|
+
else:
|
|
441
|
+
logger.warning(
|
|
442
|
+
f"Skipping sample at index {idx}: "
|
|
443
|
+
"invalid 'final_answer'"
|
|
444
|
+
)
|
|
445
|
+
return None
|
|
446
|
+
|
|
447
|
+
try:
|
|
448
|
+
return DataPoint(
|
|
449
|
+
question=question,
|
|
450
|
+
rationale=rationale,
|
|
451
|
+
final_answer=final_answer,
|
|
452
|
+
metadata=item.get('metadata'),
|
|
453
|
+
difficulty=item.get('difficulty'),
|
|
454
|
+
)
|
|
455
|
+
except ValidationError as e:
|
|
456
|
+
if self._strict:
|
|
457
|
+
raise ValueError(
|
|
458
|
+
f"Sample at index {idx} validation error: {e}"
|
|
459
|
+
)
|
|
460
|
+
else:
|
|
461
|
+
logger.warning(
|
|
462
|
+
f"Skipping invalid sample at index {idx} "
|
|
463
|
+
f"due to validation error: {e}"
|
|
464
|
+
)
|
|
465
|
+
return None
|
|
466
|
+
|
|
467
|
+
unfiltered_data = [
|
|
468
|
+
create_datapoint(item, i) for i, item in enumerate(raw_data)
|
|
469
|
+
]
|
|
470
|
+
return [dp for dp in unfiltered_data if dp is not None]
|
|
471
|
+
|
|
472
|
+
def __len__(self) -> int:
|
|
473
|
+
r"""Return the size of the dataset."""
|
|
474
|
+
return self._length
|
|
475
|
+
|
|
476
|
+
def __getitem__(self, idx: int) -> DataPoint:
|
|
477
|
+
r"""Get an item from the dataset.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
idx (int): Index of the item to get.
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
DataPoint: DataPoint from the dataset with the given index.
|
|
484
|
+
|
|
485
|
+
Raises:
|
|
486
|
+
IndexError: If idx is out of bounds.
|
|
487
|
+
"""
|
|
488
|
+
if idx < 0 or idx >= self._length:
|
|
489
|
+
raise IndexError(
|
|
490
|
+
f"Index {idx} out of bounds for dataset of size {self._length}"
|
|
491
|
+
)
|
|
492
|
+
return self.data[idx]
|
|
493
|
+
|
|
494
|
+
def sample(self) -> DataPoint:
|
|
495
|
+
r"""Sample a random datapoint from the dataset.
|
|
496
|
+
|
|
497
|
+
Returns:
|
|
498
|
+
DataPoint: A randomly sampled DataPoint.
|
|
499
|
+
|
|
500
|
+
Raises:
|
|
501
|
+
RuntimeError: If the dataset is empty.
|
|
502
|
+
"""
|
|
503
|
+
if self._length == 0:
|
|
504
|
+
raise RuntimeError("Dataset is empty, cannot sample.")
|
|
505
|
+
idx = self._rng.randint(0, self._length - 1)
|
|
506
|
+
return self[idx]
|
|
507
|
+
|
|
508
|
+
@property
|
|
509
|
+
def metadata(self) -> Dict[str, Any]:
|
|
510
|
+
r"""Get dataset metadata."""
|
|
511
|
+
return self._metadata.copy()
|
|
512
|
+
|
|
513
|
+
def _init_from_hf_dataset(self, data: HFDataset) -> List[Dict[str, Any]]:
|
|
514
|
+
return [dict(item) for item in data]
|
|
515
|
+
|
|
516
|
+
def _init_from_pytorch_dataset(
|
|
517
|
+
self, data: Dataset
|
|
518
|
+
) -> List[Dict[str, Any]]:
|
|
519
|
+
if not isinstance(data, Sized):
|
|
520
|
+
raise TypeError(
|
|
521
|
+
f"{type(data).__name__} does not implement `__len__()`."
|
|
522
|
+
)
|
|
523
|
+
raw_data = []
|
|
524
|
+
|
|
525
|
+
for i in range(len(data)):
|
|
526
|
+
item = data[i]
|
|
527
|
+
if not isinstance(item, dict):
|
|
528
|
+
raise TypeError(
|
|
529
|
+
f"Item at index {i} is not a dict: "
|
|
530
|
+
f"got {type(item).__name__}"
|
|
531
|
+
)
|
|
532
|
+
raw_data.append(dict(item))
|
|
533
|
+
return raw_data
|
|
534
|
+
|
|
535
|
+
def _init_from_json_path(self, data: Path) -> List[Dict[str, Any]]:
|
|
536
|
+
if not data.exists():
|
|
537
|
+
raise FileNotFoundError(f"JSON file not found: {data}")
|
|
538
|
+
try:
|
|
539
|
+
logger.debug(f"Loading JSON from {data}")
|
|
540
|
+
with data.open('r', encoding='utf-8') as f:
|
|
541
|
+
loaded_data = json.load(f)
|
|
542
|
+
logger.info(
|
|
543
|
+
f"Successfully loaded {len(loaded_data)} items from {data}"
|
|
544
|
+
)
|
|
545
|
+
except json.JSONDecodeError as e:
|
|
546
|
+
raise ValueError(f"Invalid JSON in file {data}: {e}")
|
|
547
|
+
if not isinstance(loaded_data, list):
|
|
548
|
+
raise ValueError("JSON file must contain a list of dictionaries")
|
|
549
|
+
for i, item in enumerate(loaded_data):
|
|
550
|
+
if not isinstance(item, dict):
|
|
551
|
+
raise ValueError(
|
|
552
|
+
f"Expected a dictionary at index {i}, "
|
|
553
|
+
f"got {type(item).__name__}"
|
|
554
|
+
)
|
|
555
|
+
return loaded_data
|
|
556
|
+
|
|
557
|
+
def _init_from_list(
|
|
558
|
+
self, data: List[Dict[str, Any]]
|
|
559
|
+
) -> List[Dict[str, Any]]:
|
|
560
|
+
for i, item in enumerate(data):
|
|
561
|
+
if not isinstance(item, dict):
|
|
562
|
+
raise ValueError(
|
|
563
|
+
f"Expected a dictionary at index {i}, "
|
|
564
|
+
f"got {type(item).__name__}"
|
|
565
|
+
)
|
|
566
|
+
return data
|
|
365
567
|
|
|
366
568
|
|
|
367
569
|
class SyntheticDataset(BaseDataset):
|
camel/environments/base.py
CHANGED
|
@@ -151,20 +151,26 @@ class BaseEnvironment(ABC):
|
|
|
151
151
|
r"""Initialize the environment.
|
|
152
152
|
|
|
153
153
|
Args:
|
|
154
|
-
dataset: Dataset to sample questions from.
|
|
155
|
-
verifier: Verifier to check responses.
|
|
156
|
-
extractor: Extractor to process LLM responses.
|
|
157
|
-
max_steps: Maximum steps per episode.
|
|
158
|
-
|
|
159
|
-
|
|
154
|
+
dataset (BaseDataset): Dataset to sample questions from.
|
|
155
|
+
verifier (BaseVerifier): Verifier to check responses.
|
|
156
|
+
extractor (BaseExtractor): Extractor to process LLM responses.
|
|
157
|
+
max_steps (Optional[int]): Maximum steps per episode. (default:
|
|
158
|
+
:obj:`None`)
|
|
159
|
+
teacher_agent (Optional[ChatAgent]): Optional agent for reward
|
|
160
|
+
shaping and hints. (default: :obj:`None`)
|
|
161
|
+
curriculum_config (Optional[Dict[str, Any]]): Configuration for
|
|
162
|
+
curriculum learning including:
|
|
160
163
|
- difficulty_levels: List of available difficulty levels
|
|
161
164
|
- promotion_threshold: Score needed to advance
|
|
162
165
|
- demotion_threshold: Score triggering level decrease
|
|
163
166
|
- min_questions_per_level: Questions before promotion
|
|
164
|
-
|
|
167
|
+
(default: :obj:`None`)
|
|
168
|
+
practice_env_config (Optional[Dict[str, Any]]): Configuration for
|
|
169
|
+
practice environments:
|
|
165
170
|
- max_practice_envs: Maximum concurrent environments
|
|
166
171
|
- difficulty_range: Allowed difficulty variation
|
|
167
172
|
- focus_areas: Specific skills to practice
|
|
173
|
+
(default: :obj:`None`)
|
|
168
174
|
**kwargs: Additional environment parameters.
|
|
169
175
|
"""
|
|
170
176
|
self.dataset = dataset
|
|
@@ -289,7 +295,9 @@ class BaseEnvironment(ABC):
|
|
|
289
295
|
# extract verifiable part from llm response
|
|
290
296
|
extraction_result = await self.extractor.extract(action.llm_response)
|
|
291
297
|
|
|
292
|
-
#
|
|
298
|
+
# Ensure extraction_result is a string
|
|
299
|
+
if extraction_result is None:
|
|
300
|
+
extraction_result = ""
|
|
293
301
|
|
|
294
302
|
# verify the extracted
|
|
295
303
|
verification_result = await self.verifier.verify(
|
camel/extractors/__init__.py
CHANGED
|
@@ -11,6 +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
|
-
from .base import BaseExtractor
|
|
14
|
+
from .base import BaseExtractor, BaseExtractorStrategy
|
|
15
15
|
|
|
16
|
-
__all__ = ["BaseExtractor"]
|
|
16
|
+
__all__ = ["BaseExtractor", "BaseExtractorStrategy"]
|