camel-ai 0.2.25__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/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/models/anthropic_model.py +19 -55
- 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.25.dist-info → camel_ai-0.2.26.dist-info}/METADATA +2 -1
- {camel_ai-0.2.25.dist-info → camel_ai-0.2.26.dist-info}/RECORD +20 -17
- {camel_ai-0.2.25.dist-info → camel_ai-0.2.26.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.25.dist-info → camel_ai-0.2.26.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,226 @@
|
|
|
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
|
+
|
|
15
|
+
import ast
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from camel.extractors.base import BaseExtractorStrategy
|
|
19
|
+
from camel.logger import get_logger
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BoxedStrategy(BaseExtractorStrategy):
|
|
25
|
+
r"""Extracts content from \\boxed{} environments."""
|
|
26
|
+
|
|
27
|
+
async def extract(self, text: str) -> Optional[str]:
|
|
28
|
+
r"""Extract content from \\boxed{} environments.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
text (str): The input text to process.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Optional[str]: Content inside \\boxed{} if found, else None.
|
|
35
|
+
"""
|
|
36
|
+
# Find the start of the boxed content
|
|
37
|
+
boxed_pattern = "\\boxed{"
|
|
38
|
+
if boxed_pattern not in text:
|
|
39
|
+
logger.debug("No \\boxed{} content found in the response")
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
start_idx = text.find(boxed_pattern) + len(boxed_pattern)
|
|
43
|
+
if start_idx >= len(text):
|
|
44
|
+
logger.debug("Malformed \\boxed{} (no content after opening)")
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
# Use stack-based approach to handle nested braces
|
|
48
|
+
stack = 1 # Start with one opening brace
|
|
49
|
+
end_idx = start_idx
|
|
50
|
+
escape_mode = False
|
|
51
|
+
|
|
52
|
+
for i in range(start_idx, len(text)):
|
|
53
|
+
char = text[i]
|
|
54
|
+
|
|
55
|
+
# Handle escape sequences
|
|
56
|
+
if escape_mode:
|
|
57
|
+
escape_mode = False
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
if char == '\\':
|
|
61
|
+
escape_mode = True
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
if char == '{':
|
|
65
|
+
stack += 1
|
|
66
|
+
elif char == '}':
|
|
67
|
+
stack -= 1
|
|
68
|
+
|
|
69
|
+
if stack == 0: # Found the matching closing brace
|
|
70
|
+
end_idx = i
|
|
71
|
+
break
|
|
72
|
+
|
|
73
|
+
# Check if we found a complete boxed expression
|
|
74
|
+
if stack != 0:
|
|
75
|
+
logger.debug("Unbalanced braces in \\boxed{} content")
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
# Extract the content
|
|
79
|
+
content = text[start_idx:end_idx].strip()
|
|
80
|
+
logger.debug(f"Extracted boxed content: {content}")
|
|
81
|
+
return content
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class PythonListStrategy(BaseExtractorStrategy):
|
|
85
|
+
r"""Extracts and normalizes Python lists."""
|
|
86
|
+
|
|
87
|
+
async def extract(self, text: str) -> Optional[str]:
|
|
88
|
+
r"""Extract and normalize a Python list.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
text (str): The input text to process.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Optional[str]: Normalized list as a string if found, else None.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
text = text.strip()
|
|
98
|
+
if not (text.startswith('[') and text.endswith(']')):
|
|
99
|
+
logger.debug("Content is not a list format (missing brackets)")
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
# Fix any escaped quotes before parsing
|
|
104
|
+
fixed_content = text.replace('\\"', '"')
|
|
105
|
+
parsed = ast.literal_eval(fixed_content)
|
|
106
|
+
if isinstance(parsed, list):
|
|
107
|
+
# Sort the list for normalization
|
|
108
|
+
sorted_list = sorted(parsed, key=lambda x: str(x))
|
|
109
|
+
return repr(sorted_list)
|
|
110
|
+
else:
|
|
111
|
+
logger.debug(f"Content is not a list, got {type(parsed)}")
|
|
112
|
+
return None
|
|
113
|
+
except (SyntaxError, ValueError) as e:
|
|
114
|
+
logger.debug(f"Failed to parse as Python list: {e}")
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class PythonDictStrategy(BaseExtractorStrategy):
|
|
119
|
+
r"""Extracts and normalizes Python dictionaries."""
|
|
120
|
+
|
|
121
|
+
async def extract(self, text: str) -> Optional[str]:
|
|
122
|
+
r"""Extract and normalize a Python dictionary.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
text (str): The input text to process.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Optional[str]: Normalized dictionary as a string, else None.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
text = text.strip()
|
|
132
|
+
if not (text.startswith('{') and text.endswith('}')):
|
|
133
|
+
logger.debug("Content is not a dictionary format (missing braces)")
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
# Fix any escaped quotes before parsing
|
|
138
|
+
fixed_content = text.replace('\\"', '"')
|
|
139
|
+
parsed = ast.literal_eval(fixed_content)
|
|
140
|
+
if isinstance(parsed, dict):
|
|
141
|
+
# Sort the dictionary items for normalization
|
|
142
|
+
sorted_dict = dict(
|
|
143
|
+
sorted(parsed.items(), key=lambda x: str(x[0]))
|
|
144
|
+
)
|
|
145
|
+
return repr(sorted_dict)
|
|
146
|
+
else:
|
|
147
|
+
logger.debug(
|
|
148
|
+
f"Content is not a dictionary, got {type(parsed)}"
|
|
149
|
+
)
|
|
150
|
+
return None
|
|
151
|
+
except (SyntaxError, ValueError) as e:
|
|
152
|
+
logger.debug(f"Failed to parse as Python dictionary: {e}")
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class PythonSetStrategy(BaseExtractorStrategy):
|
|
157
|
+
r"""Extracts and normalizes Python sets."""
|
|
158
|
+
|
|
159
|
+
async def extract(self, text: str) -> Optional[str]:
|
|
160
|
+
r"""Extract and normalize a Python set.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
text (str): The input text to process.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Optional[str]: Normalized set as a string if found, else None.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
text = text.strip()
|
|
170
|
+
# Check for set syntax: {1, 2, 3} or set([1, 2, 3])
|
|
171
|
+
if not (
|
|
172
|
+
(text.startswith('{') and text.endswith('}'))
|
|
173
|
+
or (text.startswith('set(') and text.endswith(')'))
|
|
174
|
+
):
|
|
175
|
+
logger.debug("Content is not a set format")
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
# Fix any escaped quotes before parsing
|
|
180
|
+
fixed_content = text.replace('\\"', '"')
|
|
181
|
+
parsed = ast.literal_eval(fixed_content)
|
|
182
|
+
if isinstance(parsed, set):
|
|
183
|
+
# Sort the set elements for normalization
|
|
184
|
+
sorted_set = sorted(parsed, key=lambda x: str(x))
|
|
185
|
+
return repr(set(sorted_set))
|
|
186
|
+
else:
|
|
187
|
+
logger.debug(f"Content is not a set, got {type(parsed)}")
|
|
188
|
+
return None
|
|
189
|
+
except (SyntaxError, ValueError) as e:
|
|
190
|
+
logger.debug(f"Failed to parse as Python set: {e}")
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class PythonTupleStrategy(BaseExtractorStrategy):
|
|
195
|
+
r"""Extracts and normalizes Python tuples."""
|
|
196
|
+
|
|
197
|
+
async def extract(self, text: str) -> Optional[str]:
|
|
198
|
+
r"""Extract and normalize a Python tuple.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
text (str): The input text to process.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Optional[str]: Normalized tuple as a string if found, else None.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
text = text.strip()
|
|
208
|
+
# Check for tuple syntax: (1, 2, 3) or (1,)
|
|
209
|
+
if not (text.startswith('(') and text.endswith(')')):
|
|
210
|
+
logger.debug("Content is not a tuple format (missing parentheses)")
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
# Fix any escaped quotes before parsing
|
|
215
|
+
fixed_content = text.replace('\\"', '"')
|
|
216
|
+
parsed = ast.literal_eval(fixed_content)
|
|
217
|
+
if isinstance(parsed, tuple):
|
|
218
|
+
# Sort the tuple elements for normalization
|
|
219
|
+
sorted_tuple = tuple(sorted(parsed, key=lambda x: str(x)))
|
|
220
|
+
return repr(sorted_tuple)
|
|
221
|
+
else:
|
|
222
|
+
logger.debug(f"Content is not a tuple, got {type(parsed)}")
|
|
223
|
+
return None
|
|
224
|
+
except (SyntaxError, ValueError) as e:
|
|
225
|
+
logger.debug(f"Failed to parse as Python tuple: {e}")
|
|
226
|
+
return None
|
camel/models/anthropic_model.py
CHANGED
|
@@ -35,13 +35,13 @@ class AnthropicModel(BaseModelBackend):
|
|
|
35
35
|
model_type (Union[ModelType, str]): Model for which a backend is
|
|
36
36
|
created, one of CLAUDE_* series.
|
|
37
37
|
model_config_dict (Optional[Dict[str, Any]], optional): A dictionary
|
|
38
|
-
that will be fed into
|
|
38
|
+
that will be fed into `openai.ChatCompletion.create()`. If
|
|
39
39
|
:obj:`None`, :obj:`AnthropicConfig().as_dict()` will be used.
|
|
40
40
|
(default: :obj:`None`)
|
|
41
41
|
api_key (Optional[str], optional): The API key for authenticating with
|
|
42
42
|
the Anthropic service. (default: :obj:`None`)
|
|
43
43
|
url (Optional[str], optional): The url to the Anthropic service.
|
|
44
|
-
(default: :obj:`
|
|
44
|
+
(default: :obj:`https://api.anthropic.com/v1/`)
|
|
45
45
|
token_counter (Optional[BaseTokenCounter], optional): Token counter to
|
|
46
46
|
use for the model. If not provided, :obj:`AnthropicTokenCounter`
|
|
47
47
|
will be used. (default: :obj:`None`)
|
|
@@ -61,43 +61,24 @@ class AnthropicModel(BaseModelBackend):
|
|
|
61
61
|
url: Optional[str] = None,
|
|
62
62
|
token_counter: Optional[BaseTokenCounter] = None,
|
|
63
63
|
) -> None:
|
|
64
|
-
from
|
|
64
|
+
from openai import AsyncOpenAI, OpenAI
|
|
65
65
|
|
|
66
66
|
if model_config_dict is None:
|
|
67
67
|
model_config_dict = AnthropicConfig().as_dict()
|
|
68
68
|
api_key = api_key or os.environ.get("ANTHROPIC_API_KEY")
|
|
69
|
-
url =
|
|
69
|
+
url = (
|
|
70
|
+
url
|
|
71
|
+
or os.environ.get("ANTHROPIC_API_BASE_URL")
|
|
72
|
+
or "https://api.anthropic.com/v1/"
|
|
73
|
+
)
|
|
70
74
|
super().__init__(
|
|
71
75
|
model_type, model_config_dict, api_key, url, token_counter
|
|
72
76
|
)
|
|
73
|
-
self.client =
|
|
74
|
-
self.async_client = AsyncAnthropic(
|
|
75
|
-
api_key=self._api_key, base_url=self._url
|
|
76
|
-
)
|
|
77
|
+
self.client = OpenAI(base_url=self._url, api_key=self._api_key)
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
obj = ChatCompletion.construct(
|
|
81
|
-
id=None,
|
|
82
|
-
choices=[
|
|
83
|
-
dict(
|
|
84
|
-
index=0,
|
|
85
|
-
message={
|
|
86
|
-
"role": "assistant",
|
|
87
|
-
"content": next(
|
|
88
|
-
content.text
|
|
89
|
-
for content in response.content
|
|
90
|
-
if content.type == "text"
|
|
91
|
-
),
|
|
92
|
-
},
|
|
93
|
-
finish_reason=response.stop_reason,
|
|
94
|
-
)
|
|
95
|
-
],
|
|
96
|
-
created=None,
|
|
97
|
-
model=response.model,
|
|
98
|
-
object="chat.completion",
|
|
79
|
+
self.async_client = AsyncOpenAI(
|
|
80
|
+
api_key=self._api_key, base_url=self._url
|
|
99
81
|
)
|
|
100
|
-
return obj
|
|
101
82
|
|
|
102
83
|
@property
|
|
103
84
|
def token_counter(self) -> BaseTokenCounter:
|
|
@@ -126,22 +107,13 @@ class AnthropicModel(BaseModelBackend):
|
|
|
126
107
|
Returns:
|
|
127
108
|
ChatCompletion: Response in the OpenAI API format.
|
|
128
109
|
"""
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if messages[0]["role"] == "system":
|
|
132
|
-
sys_msg = str(messages.pop(0)["content"])
|
|
133
|
-
else:
|
|
134
|
-
sys_msg = NOT_GIVEN # type: ignore[assignment]
|
|
135
|
-
response = self.client.messages.create(
|
|
110
|
+
response = self.client.chat.completions.create(
|
|
136
111
|
model=self.model_type,
|
|
137
|
-
|
|
138
|
-
messages=messages, # type: ignore[arg-type]
|
|
112
|
+
messages=messages,
|
|
139
113
|
**self.model_config_dict,
|
|
114
|
+
tools=tools, # type: ignore[arg-type]
|
|
140
115
|
)
|
|
141
116
|
|
|
142
|
-
# format response to openai format
|
|
143
|
-
response = self._convert_response_from_anthropic_to_openai(response)
|
|
144
|
-
|
|
145
117
|
return response
|
|
146
118
|
|
|
147
119
|
async def _arun(
|
|
@@ -159,21 +131,14 @@ class AnthropicModel(BaseModelBackend):
|
|
|
159
131
|
Returns:
|
|
160
132
|
ChatCompletion: Response in the OpenAI API format.
|
|
161
133
|
"""
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if messages[0]["role"] == "system":
|
|
165
|
-
sys_msg = str(messages.pop(0)["content"])
|
|
166
|
-
else:
|
|
167
|
-
sys_msg = NOT_GIVEN # type: ignore[assignment]
|
|
168
|
-
response = await self.async_client.messages.create(
|
|
134
|
+
response = await self.async_client.chat.completions.create(
|
|
169
135
|
model=self.model_type,
|
|
170
|
-
|
|
171
|
-
messages=messages, # type: ignore[arg-type]
|
|
136
|
+
messages=messages,
|
|
172
137
|
**self.model_config_dict,
|
|
138
|
+
tools=tools, # type: ignore[arg-type]
|
|
173
139
|
)
|
|
174
140
|
|
|
175
|
-
|
|
176
|
-
return self._convert_response_from_anthropic_to_openai(response)
|
|
141
|
+
return response
|
|
177
142
|
|
|
178
143
|
def check_model_config(self):
|
|
179
144
|
r"""Check whether the model configuration is valid for anthropic
|
|
@@ -181,8 +146,7 @@ class AnthropicModel(BaseModelBackend):
|
|
|
181
146
|
|
|
182
147
|
Raises:
|
|
183
148
|
ValueError: If the model configuration dictionary contains any
|
|
184
|
-
unexpected arguments to
|
|
185
|
-
:obj:`model_path` or :obj:`server_url`.
|
|
149
|
+
unexpected arguments to Anthropic API.
|
|
186
150
|
"""
|
|
187
151
|
for param in self.model_config_dict:
|
|
188
152
|
if param not in ANTHROPIC_API_PARAMS:
|
camel/py.typed
ADDED
|
File without changes
|
|
@@ -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 List, Union
|
|
16
|
+
from typing import List, Optional, Union
|
|
17
17
|
|
|
18
18
|
from pydantic import BaseModel, ConfigDict, Field
|
|
19
19
|
|
|
@@ -45,6 +45,7 @@ class Relationship(BaseModel):
|
|
|
45
45
|
subj (Node): The subject/source node of the relationship.
|
|
46
46
|
obj (Node): The object/target node of the relationship.
|
|
47
47
|
type (str): The type of the relationship.
|
|
48
|
+
timestamp (str, optional): The timestamp of the relationship.
|
|
48
49
|
properties (dict): Additional properties associated with the
|
|
49
50
|
relationship.
|
|
50
51
|
"""
|
|
@@ -52,6 +53,7 @@ class Relationship(BaseModel):
|
|
|
52
53
|
subj: Node
|
|
53
54
|
obj: Node
|
|
54
55
|
type: str = "Relationship"
|
|
56
|
+
timestamp: Optional[str] = None
|
|
55
57
|
properties: dict = Field(default_factory=dict)
|
|
56
58
|
|
|
57
59
|
|
|
@@ -339,18 +339,24 @@ class Neo4jGraph(BaseGraphStorage):
|
|
|
339
339
|
]
|
|
340
340
|
)
|
|
341
341
|
|
|
342
|
-
def add_triplet(
|
|
343
|
-
|
|
342
|
+
def add_triplet(
|
|
343
|
+
self, subj: str, obj: str, rel: str, timestamp: Optional[str] = None
|
|
344
|
+
) -> None:
|
|
345
|
+
r"""Adds a relationship (triplet) between two entities
|
|
346
|
+
in the database with a timestamp.
|
|
344
347
|
|
|
345
348
|
Args:
|
|
346
349
|
subj (str): The identifier for the subject entity.
|
|
347
350
|
obj (str): The identifier for the object entity.
|
|
348
351
|
rel (str): The relationship between the subject and object.
|
|
352
|
+
timestamp (Optional[str]): The timestamp of the relationship.
|
|
353
|
+
Defaults to None.
|
|
349
354
|
"""
|
|
350
355
|
query = """
|
|
351
356
|
MERGE (n1:`%s` {id:$subj})
|
|
352
357
|
MERGE (n2:`%s` {id:$obj})
|
|
353
|
-
MERGE (n1)-[:`%s`]->(n2)
|
|
358
|
+
MERGE (n1)-[r:`%s`]->(n2)
|
|
359
|
+
SET r.timestamp = $timestamp
|
|
354
360
|
"""
|
|
355
361
|
|
|
356
362
|
prepared_statement = query % (
|
|
@@ -361,7 +367,10 @@ class Neo4jGraph(BaseGraphStorage):
|
|
|
361
367
|
|
|
362
368
|
# Execute the query within a database session
|
|
363
369
|
with self.driver.session(database=self.database) as session:
|
|
364
|
-
session.run(
|
|
370
|
+
session.run(
|
|
371
|
+
prepared_statement,
|
|
372
|
+
{"subj": subj, "obj": obj, "timestamp": timestamp},
|
|
373
|
+
)
|
|
365
374
|
|
|
366
375
|
def _delete_rel(self, subj: str, obj: str, rel: str) -> None:
|
|
367
376
|
r"""Deletes a specific relationship between two nodes in the Neo4j
|
|
@@ -721,3 +730,68 @@ class Neo4jGraph(BaseGraphStorage):
|
|
|
721
730
|
return result[0] if result else {}
|
|
722
731
|
except CypherSyntaxError as e:
|
|
723
732
|
raise ValueError(f"Generated Cypher Statement is not valid\n{e}")
|
|
733
|
+
|
|
734
|
+
def get_triplet(
|
|
735
|
+
self,
|
|
736
|
+
subj: Optional[str] = None,
|
|
737
|
+
obj: Optional[str] = None,
|
|
738
|
+
rel: Optional[str] = None,
|
|
739
|
+
) -> List[Dict[str, Any]]:
|
|
740
|
+
r"""
|
|
741
|
+
Query triplet information. If subj, obj, or rel is
|
|
742
|
+
not specified, returns all matching triplets.
|
|
743
|
+
|
|
744
|
+
Args:
|
|
745
|
+
subj (Optional[str]): The ID of the subject node.
|
|
746
|
+
If None, matches any subject node.
|
|
747
|
+
obj (Optional[str]): The ID of the object node.
|
|
748
|
+
If None, matches any object node.
|
|
749
|
+
rel (Optional[str]): The type of relationship.
|
|
750
|
+
If None, matches any relationship type.
|
|
751
|
+
|
|
752
|
+
Returns:
|
|
753
|
+
List[Dict[str, Any]]: A list of matching triplets,
|
|
754
|
+
each containing subj, obj, rel, and timestamp.
|
|
755
|
+
"""
|
|
756
|
+
import logging
|
|
757
|
+
|
|
758
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
759
|
+
logger = logging.getLogger(__name__)
|
|
760
|
+
|
|
761
|
+
# Construct the query
|
|
762
|
+
query = """
|
|
763
|
+
MATCH (n1:Entity)-[r]->(n2:Entity)
|
|
764
|
+
WHERE ($subj IS NULL OR n1.id = $subj)
|
|
765
|
+
AND ($obj IS NULL OR n2.id = $obj)
|
|
766
|
+
AND ($rel IS NULL OR type(r) = $rel)
|
|
767
|
+
RETURN n1.id AS subj, n2.id AS obj,
|
|
768
|
+
type(r) AS rel, r.timestamp AS timestamp
|
|
769
|
+
"""
|
|
770
|
+
|
|
771
|
+
# Construct the query parameters
|
|
772
|
+
params = {
|
|
773
|
+
"subj": subj
|
|
774
|
+
if subj is not None
|
|
775
|
+
else None, # If subj is None, match any subject node
|
|
776
|
+
"obj": obj
|
|
777
|
+
if obj is not None
|
|
778
|
+
else None, # If obj is None, match any object node
|
|
779
|
+
"rel": rel
|
|
780
|
+
if rel is not None
|
|
781
|
+
else None, # If rel is None, match any relationship type
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
logger.debug(f"Executing query: {query}")
|
|
785
|
+
logger.debug(f"Query parameters: {params}")
|
|
786
|
+
|
|
787
|
+
with self.driver.session(database=self.database) as session:
|
|
788
|
+
try:
|
|
789
|
+
result = session.run(query, params)
|
|
790
|
+
records = [record.data() for record in result]
|
|
791
|
+
logger.debug(
|
|
792
|
+
f"Query returned {len(records)} records: {records}"
|
|
793
|
+
)
|
|
794
|
+
return records
|
|
795
|
+
except Exception as e:
|
|
796
|
+
logger.error(f"Error executing query: {e}")
|
|
797
|
+
return []
|
camel/toolkits/__init__.py
CHANGED
|
@@ -58,6 +58,7 @@ from .mcp_toolkit import MCPToolkit
|
|
|
58
58
|
from .web_toolkit import WebToolkit
|
|
59
59
|
from .file_write_toolkit import FileWriteToolkit
|
|
60
60
|
from .terminal_toolkit import TerminalToolkit
|
|
61
|
+
from .pubmed_toolkit import PubMedToolkit
|
|
61
62
|
|
|
62
63
|
|
|
63
64
|
__all__ = [
|
|
@@ -104,4 +105,5 @@ __all__ = [
|
|
|
104
105
|
'WebToolkit',
|
|
105
106
|
'FileWriteToolkit',
|
|
106
107
|
'TerminalToolkit',
|
|
108
|
+
'PubMedToolkit',
|
|
107
109
|
]
|