camel-ai 0.2.67__py3-none-any.whl → 0.2.80a2__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.
Files changed (224) hide show
  1. camel/__init__.py +1 -1
  2. camel/agents/_types.py +6 -2
  3. camel/agents/_utils.py +38 -0
  4. camel/agents/chat_agent.py +4014 -410
  5. camel/agents/mcp_agent.py +30 -27
  6. camel/agents/repo_agent.py +2 -1
  7. camel/benchmarks/browsecomp.py +6 -6
  8. camel/configs/__init__.py +15 -0
  9. camel/configs/aihubmix_config.py +88 -0
  10. camel/configs/amd_config.py +70 -0
  11. camel/configs/cometapi_config.py +104 -0
  12. camel/configs/minimax_config.py +93 -0
  13. camel/configs/nebius_config.py +103 -0
  14. camel/configs/vllm_config.py +2 -0
  15. camel/data_collectors/alpaca_collector.py +15 -6
  16. camel/datagen/self_improving_cot.py +1 -1
  17. camel/datasets/base_generator.py +39 -10
  18. camel/environments/__init__.py +12 -0
  19. camel/environments/rlcards_env.py +860 -0
  20. camel/environments/single_step.py +28 -3
  21. camel/environments/tic_tac_toe.py +1 -1
  22. camel/interpreters/__init__.py +2 -0
  23. camel/interpreters/docker/Dockerfile +4 -16
  24. camel/interpreters/docker_interpreter.py +3 -2
  25. camel/interpreters/e2b_interpreter.py +34 -1
  26. camel/interpreters/internal_python_interpreter.py +51 -2
  27. camel/interpreters/microsandbox_interpreter.py +395 -0
  28. camel/loaders/__init__.py +11 -2
  29. camel/loaders/base_loader.py +85 -0
  30. camel/loaders/chunkr_reader.py +9 -0
  31. camel/loaders/firecrawl_reader.py +4 -4
  32. camel/logger.py +1 -1
  33. camel/memories/agent_memories.py +84 -1
  34. camel/memories/base.py +34 -0
  35. camel/memories/blocks/chat_history_block.py +122 -4
  36. camel/memories/blocks/vectordb_block.py +8 -1
  37. camel/memories/context_creators/score_based.py +29 -237
  38. camel/memories/records.py +88 -8
  39. camel/messages/base.py +166 -40
  40. camel/messages/func_message.py +32 -5
  41. camel/models/__init__.py +10 -0
  42. camel/models/aihubmix_model.py +83 -0
  43. camel/models/aiml_model.py +1 -16
  44. camel/models/amd_model.py +101 -0
  45. camel/models/anthropic_model.py +117 -18
  46. camel/models/aws_bedrock_model.py +2 -33
  47. camel/models/azure_openai_model.py +205 -91
  48. camel/models/base_audio_model.py +3 -1
  49. camel/models/base_model.py +189 -24
  50. camel/models/cohere_model.py +5 -17
  51. camel/models/cometapi_model.py +83 -0
  52. camel/models/crynux_model.py +1 -16
  53. camel/models/deepseek_model.py +6 -16
  54. camel/models/fish_audio_model.py +6 -0
  55. camel/models/gemini_model.py +71 -20
  56. camel/models/groq_model.py +1 -17
  57. camel/models/internlm_model.py +1 -16
  58. camel/models/litellm_model.py +49 -32
  59. camel/models/lmstudio_model.py +1 -17
  60. camel/models/minimax_model.py +83 -0
  61. camel/models/mistral_model.py +1 -16
  62. camel/models/model_factory.py +27 -1
  63. camel/models/model_manager.py +24 -6
  64. camel/models/modelscope_model.py +1 -16
  65. camel/models/moonshot_model.py +185 -19
  66. camel/models/nebius_model.py +83 -0
  67. camel/models/nemotron_model.py +0 -5
  68. camel/models/netmind_model.py +1 -16
  69. camel/models/novita_model.py +1 -16
  70. camel/models/nvidia_model.py +1 -16
  71. camel/models/ollama_model.py +4 -19
  72. camel/models/openai_compatible_model.py +171 -46
  73. camel/models/openai_model.py +205 -77
  74. camel/models/openrouter_model.py +1 -17
  75. camel/models/ppio_model.py +1 -16
  76. camel/models/qianfan_model.py +1 -16
  77. camel/models/qwen_model.py +1 -16
  78. camel/models/reka_model.py +1 -16
  79. camel/models/samba_model.py +34 -47
  80. camel/models/sglang_model.py +64 -31
  81. camel/models/siliconflow_model.py +1 -16
  82. camel/models/stub_model.py +0 -4
  83. camel/models/togetherai_model.py +1 -16
  84. camel/models/vllm_model.py +1 -16
  85. camel/models/volcano_model.py +0 -17
  86. camel/models/watsonx_model.py +1 -16
  87. camel/models/yi_model.py +1 -16
  88. camel/models/zhipuai_model.py +60 -16
  89. camel/parsers/__init__.py +18 -0
  90. camel/parsers/mcp_tool_call_parser.py +176 -0
  91. camel/retrievers/auto_retriever.py +1 -0
  92. camel/runtimes/configs.py +11 -11
  93. camel/runtimes/daytona_runtime.py +15 -16
  94. camel/runtimes/docker_runtime.py +6 -6
  95. camel/runtimes/remote_http_runtime.py +5 -5
  96. camel/services/agent_openapi_server.py +380 -0
  97. camel/societies/__init__.py +2 -0
  98. camel/societies/role_playing.py +26 -28
  99. camel/societies/workforce/__init__.py +2 -0
  100. camel/societies/workforce/events.py +122 -0
  101. camel/societies/workforce/prompts.py +249 -38
  102. camel/societies/workforce/role_playing_worker.py +82 -20
  103. camel/societies/workforce/single_agent_worker.py +634 -34
  104. camel/societies/workforce/structured_output_handler.py +512 -0
  105. camel/societies/workforce/task_channel.py +169 -23
  106. camel/societies/workforce/utils.py +176 -9
  107. camel/societies/workforce/worker.py +77 -23
  108. camel/societies/workforce/workflow_memory_manager.py +772 -0
  109. camel/societies/workforce/workforce.py +3168 -478
  110. camel/societies/workforce/workforce_callback.py +74 -0
  111. camel/societies/workforce/workforce_logger.py +203 -175
  112. camel/societies/workforce/workforce_metrics.py +33 -0
  113. camel/storages/__init__.py +4 -0
  114. camel/storages/key_value_storages/json.py +15 -2
  115. camel/storages/key_value_storages/mem0_cloud.py +48 -47
  116. camel/storages/object_storages/google_cloud.py +1 -1
  117. camel/storages/vectordb_storages/__init__.py +6 -0
  118. camel/storages/vectordb_storages/chroma.py +731 -0
  119. camel/storages/vectordb_storages/oceanbase.py +13 -13
  120. camel/storages/vectordb_storages/pgvector.py +349 -0
  121. camel/storages/vectordb_storages/qdrant.py +3 -3
  122. camel/storages/vectordb_storages/surreal.py +365 -0
  123. camel/storages/vectordb_storages/tidb.py +8 -6
  124. camel/tasks/task.py +244 -27
  125. camel/toolkits/__init__.py +46 -8
  126. camel/toolkits/aci_toolkit.py +64 -19
  127. camel/toolkits/arxiv_toolkit.py +6 -6
  128. camel/toolkits/base.py +63 -5
  129. camel/toolkits/code_execution.py +28 -1
  130. camel/toolkits/context_summarizer_toolkit.py +684 -0
  131. camel/toolkits/craw4ai_toolkit.py +93 -0
  132. camel/toolkits/dappier_toolkit.py +10 -6
  133. camel/toolkits/dingtalk.py +1135 -0
  134. camel/toolkits/edgeone_pages_mcp_toolkit.py +49 -0
  135. camel/toolkits/excel_toolkit.py +901 -67
  136. camel/toolkits/file_toolkit.py +1402 -0
  137. camel/toolkits/function_tool.py +30 -6
  138. camel/toolkits/github_toolkit.py +107 -20
  139. camel/toolkits/gmail_toolkit.py +1839 -0
  140. camel/toolkits/google_calendar_toolkit.py +38 -4
  141. camel/toolkits/google_drive_mcp_toolkit.py +54 -0
  142. camel/toolkits/human_toolkit.py +34 -10
  143. camel/toolkits/hybrid_browser_toolkit/__init__.py +18 -0
  144. camel/toolkits/hybrid_browser_toolkit/config_loader.py +185 -0
  145. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +246 -0
  146. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +1973 -0
  147. camel/toolkits/hybrid_browser_toolkit/installer.py +203 -0
  148. camel/toolkits/hybrid_browser_toolkit/ts/package-lock.json +3749 -0
  149. camel/toolkits/hybrid_browser_toolkit/ts/package.json +32 -0
  150. camel/toolkits/hybrid_browser_toolkit/ts/src/browser-scripts.js +125 -0
  151. camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +1815 -0
  152. camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +233 -0
  153. camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +590 -0
  154. camel/toolkits/hybrid_browser_toolkit/ts/src/index.ts +7 -0
  155. camel/toolkits/hybrid_browser_toolkit/ts/src/parent-child-filter.ts +226 -0
  156. camel/toolkits/hybrid_browser_toolkit/ts/src/snapshot-parser.ts +219 -0
  157. camel/toolkits/hybrid_browser_toolkit/ts/src/som-screenshot-injected.ts +543 -0
  158. camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +130 -0
  159. camel/toolkits/hybrid_browser_toolkit/ts/tsconfig.json +26 -0
  160. camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +319 -0
  161. camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +1032 -0
  162. camel/toolkits/hybrid_browser_toolkit_py/__init__.py +17 -0
  163. camel/toolkits/hybrid_browser_toolkit_py/actions.py +575 -0
  164. camel/toolkits/hybrid_browser_toolkit_py/agent.py +311 -0
  165. camel/toolkits/hybrid_browser_toolkit_py/browser_session.py +787 -0
  166. camel/toolkits/hybrid_browser_toolkit_py/config_loader.py +490 -0
  167. camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +2390 -0
  168. camel/toolkits/hybrid_browser_toolkit_py/snapshot.py +233 -0
  169. camel/toolkits/hybrid_browser_toolkit_py/stealth_script.js +0 -0
  170. camel/toolkits/hybrid_browser_toolkit_py/unified_analyzer.js +1043 -0
  171. camel/toolkits/image_generation_toolkit.py +390 -0
  172. camel/toolkits/jina_reranker_toolkit.py +3 -4
  173. camel/toolkits/klavis_toolkit.py +5 -1
  174. camel/toolkits/markitdown_toolkit.py +104 -0
  175. camel/toolkits/math_toolkit.py +64 -10
  176. camel/toolkits/mcp_toolkit.py +370 -45
  177. camel/toolkits/memory_toolkit.py +5 -1
  178. camel/toolkits/message_agent_toolkit.py +608 -0
  179. camel/toolkits/message_integration.py +724 -0
  180. camel/toolkits/minimax_mcp_toolkit.py +195 -0
  181. camel/toolkits/note_taking_toolkit.py +277 -0
  182. camel/toolkits/notion_mcp_toolkit.py +224 -0
  183. camel/toolkits/openbb_toolkit.py +5 -1
  184. camel/toolkits/origene_mcp_toolkit.py +56 -0
  185. camel/toolkits/playwright_mcp_toolkit.py +12 -31
  186. camel/toolkits/pptx_toolkit.py +25 -12
  187. camel/toolkits/resend_toolkit.py +168 -0
  188. camel/toolkits/screenshot_toolkit.py +213 -0
  189. camel/toolkits/search_toolkit.py +437 -142
  190. camel/toolkits/slack_toolkit.py +104 -50
  191. camel/toolkits/sympy_toolkit.py +1 -1
  192. camel/toolkits/task_planning_toolkit.py +3 -3
  193. camel/toolkits/terminal_toolkit/__init__.py +18 -0
  194. camel/toolkits/terminal_toolkit/terminal_toolkit.py +957 -0
  195. camel/toolkits/terminal_toolkit/utils.py +532 -0
  196. camel/toolkits/thinking_toolkit.py +1 -1
  197. camel/toolkits/vertex_ai_veo_toolkit.py +590 -0
  198. camel/toolkits/video_analysis_toolkit.py +106 -26
  199. camel/toolkits/video_download_toolkit.py +17 -14
  200. camel/toolkits/web_deploy_toolkit.py +1219 -0
  201. camel/toolkits/wechat_official_toolkit.py +483 -0
  202. camel/toolkits/zapier_toolkit.py +5 -1
  203. camel/types/__init__.py +2 -2
  204. camel/types/agents/tool_calling_record.py +4 -1
  205. camel/types/enums.py +316 -40
  206. camel/types/openai_types.py +2 -2
  207. camel/types/unified_model_type.py +31 -4
  208. camel/utils/commons.py +36 -5
  209. camel/utils/constants.py +3 -0
  210. camel/utils/context_utils.py +1003 -0
  211. camel/utils/mcp.py +138 -4
  212. camel/utils/mcp_client.py +45 -1
  213. camel/utils/message_summarizer.py +148 -0
  214. camel/utils/token_counting.py +43 -20
  215. camel/utils/tool_result.py +44 -0
  216. {camel_ai-0.2.67.dist-info → camel_ai-0.2.80a2.dist-info}/METADATA +296 -85
  217. {camel_ai-0.2.67.dist-info → camel_ai-0.2.80a2.dist-info}/RECORD +219 -146
  218. camel/loaders/pandas_reader.py +0 -368
  219. camel/toolkits/dalle_toolkit.py +0 -175
  220. camel/toolkits/file_write_toolkit.py +0 -444
  221. camel/toolkits/openai_agent_toolkit.py +0 -135
  222. camel/toolkits/terminal_toolkit.py +0 -1037
  223. {camel_ai-0.2.67.dist-info → camel_ai-0.2.80a2.dist-info}/WHEEL +0 -0
  224. {camel_ai-0.2.67.dist-info → camel_ai-0.2.80a2.dist-info}/licenses/LICENSE +0 -0
@@ -14,16 +14,25 @@
14
14
 
15
15
  import json
16
16
  import os
17
+ import warnings
17
18
  from contextlib import AsyncExitStack
18
19
  from typing import Any, Dict, List, Optional
19
20
 
21
+ from typing_extensions import TypeGuard
22
+
20
23
  from camel.logger import get_logger
21
- from camel.toolkits import BaseToolkit, FunctionTool
24
+ from camel.toolkits.base import BaseToolkit
25
+ from camel.toolkits.function_tool import FunctionTool
22
26
  from camel.utils.commons import run_async
23
27
  from camel.utils.mcp_client import MCPClient, create_mcp_client
24
28
 
25
29
  logger = get_logger(__name__)
26
30
 
31
+ # Suppress parameter description warnings for MCP tools
32
+ warnings.filterwarnings(
33
+ "ignore", message="Parameter description is missing", category=UserWarning
34
+ )
35
+
27
36
 
28
37
  class MCPConnectionError(Exception):
29
38
  r"""Raised when MCP connection fails."""
@@ -37,6 +46,187 @@ class MCPToolError(Exception):
37
46
  pass
38
47
 
39
48
 
49
+ _EMPTY_SCHEMA = {
50
+ "additionalProperties": False,
51
+ "type": "object",
52
+ "properties": {},
53
+ "required": [],
54
+ }
55
+
56
+
57
+ def ensure_strict_json_schema(schema: dict[str, Any]) -> dict[str, Any]:
58
+ r"""Mutates the given JSON schema to ensure it conforms to the
59
+ `strict` standard that the OpenAI API expects.
60
+ """
61
+ if schema == {}:
62
+ return _EMPTY_SCHEMA
63
+ return _ensure_strict_json_schema(schema, path=(), root=schema)
64
+
65
+
66
+ def _ensure_strict_json_schema(
67
+ json_schema: object,
68
+ *,
69
+ path: tuple[str, ...],
70
+ root: dict[str, object],
71
+ ) -> dict[str, Any]:
72
+ if not is_dict(json_schema):
73
+ raise TypeError(
74
+ f"Expected {json_schema} to be a dictionary; path={path}"
75
+ )
76
+
77
+ defs = json_schema.get("$defs")
78
+ if is_dict(defs):
79
+ for def_name, def_schema in defs.items():
80
+ _ensure_strict_json_schema(
81
+ def_schema, path=(*path, "$defs", def_name), root=root
82
+ )
83
+
84
+ definitions = json_schema.get("definitions")
85
+ if is_dict(definitions):
86
+ for definition_name, definition_schema in definitions.items():
87
+ _ensure_strict_json_schema(
88
+ definition_schema,
89
+ path=(*path, "definitions", definition_name),
90
+ root=root,
91
+ )
92
+
93
+ typ = json_schema.get("type")
94
+ if typ == "object" and "additionalProperties" not in json_schema:
95
+ json_schema["additionalProperties"] = False
96
+ elif (
97
+ typ == "object"
98
+ and "additionalProperties" in json_schema
99
+ and json_schema["additionalProperties"]
100
+ ):
101
+ raise ValueError(
102
+ "additionalProperties should not be set for object types. This "
103
+ "could be because you're using an older version of Pydantic, or "
104
+ "because you configured additional properties to be allowed. If "
105
+ "you really need this, update the function or output tool "
106
+ "to not use a strict schema."
107
+ )
108
+
109
+ # object types
110
+ # { 'type': 'object', 'properties': { 'a': {...} } }
111
+ properties = json_schema.get("properties")
112
+ if is_dict(properties):
113
+ json_schema["required"] = list(properties.keys())
114
+ json_schema["properties"] = {
115
+ key: _ensure_strict_json_schema(
116
+ prop_schema, path=(*path, "properties", key), root=root
117
+ )
118
+ for key, prop_schema in properties.items()
119
+ }
120
+
121
+ # arrays
122
+ # { 'type': 'array', 'items': {...} }
123
+ items = json_schema.get("items")
124
+ if is_dict(items):
125
+ json_schema["items"] = _ensure_strict_json_schema(
126
+ items, path=(*path, "items"), root=root
127
+ )
128
+
129
+ # unions
130
+ any_of = json_schema.get("anyOf")
131
+ if is_list(any_of):
132
+ json_schema["anyOf"] = [
133
+ _ensure_strict_json_schema(
134
+ variant, path=(*path, "anyOf", str(i)), root=root
135
+ )
136
+ for i, variant in enumerate(any_of)
137
+ ]
138
+
139
+ # intersections
140
+ all_of = json_schema.get("allOf")
141
+ if is_list(all_of):
142
+ if len(all_of) == 1:
143
+ json_schema.update(
144
+ _ensure_strict_json_schema(
145
+ all_of[0], path=(*path, "allOf", "0"), root=root
146
+ )
147
+ )
148
+ json_schema.pop("allOf")
149
+ else:
150
+ json_schema["allOf"] = [
151
+ _ensure_strict_json_schema(
152
+ entry, path=(*path, "allOf", str(i)), root=root
153
+ )
154
+ for i, entry in enumerate(all_of)
155
+ ]
156
+
157
+ # strip `None` defaults as there's no meaningful distinction here
158
+ # the schema will still be `nullable` and the model will default
159
+ # to using `None` anyway
160
+ if json_schema.get("default", None) is None:
161
+ json_schema.pop("default", None)
162
+
163
+ # we can't use `$ref`s if there are also other properties defined, e.g.
164
+ # `{"$ref": "...", "description": "my description"}`
165
+ #
166
+ # so we unravel the ref
167
+ # `{"type": "string", "description": "my description"}`
168
+ ref = json_schema.get("$ref")
169
+ if ref and has_more_than_n_keys(json_schema, 1):
170
+ assert isinstance(ref, str), f"Received non-string $ref - {ref}"
171
+
172
+ resolved = resolve_ref(root=root, ref=ref)
173
+ if not is_dict(resolved):
174
+ raise ValueError(
175
+ f"Expected `$ref: {ref}` to resolved to a dictionary but got "
176
+ f"{resolved}"
177
+ )
178
+
179
+ # properties from the json schema take priority
180
+ # over the ones on the `$ref`
181
+ json_schema.update({**resolved, **json_schema})
182
+ json_schema.pop("$ref")
183
+ # Since the schema expanded from `$ref` might not
184
+ # have `additionalProperties: false` applied
185
+ # we call `_ensure_strict_json_schema` again to fix the inlined
186
+ # schema and ensure it's valid
187
+ return _ensure_strict_json_schema(json_schema, path=path, root=root)
188
+
189
+ return json_schema
190
+
191
+
192
+ def resolve_ref(*, root: dict[str, object], ref: str) -> object:
193
+ if not ref.startswith("#/"):
194
+ raise ValueError(
195
+ f"Unexpected $ref format {ref!r}; Does not start with #/"
196
+ )
197
+
198
+ path = ref[2:].split("/")
199
+ resolved = root
200
+ for key in path:
201
+ value = resolved[key]
202
+ assert is_dict(value), (
203
+ f"encountered non-dictionary entry while resolving {ref} - "
204
+ f"{resolved}"
205
+ )
206
+ resolved = value
207
+
208
+ return resolved
209
+
210
+
211
+ def is_dict(obj: object) -> TypeGuard[dict[str, object]]:
212
+ # just pretend that we know there are only `str` keys
213
+ # as that check is not worth the performance cost
214
+ return isinstance(obj, dict)
215
+
216
+
217
+ def is_list(obj: object) -> TypeGuard[list[object]]:
218
+ return isinstance(obj, list)
219
+
220
+
221
+ def has_more_than_n_keys(obj: dict[str, object], n: int) -> bool:
222
+ i = 0
223
+ for _ in obj.keys():
224
+ i += 1
225
+ if i > n:
226
+ return True
227
+ return False
228
+
229
+
40
230
  class MCPToolkit(BaseToolkit):
41
231
  r"""MCPToolkit provides a unified interface for managing multiple
42
232
  MCP server connections and their tools.
@@ -214,26 +404,34 @@ class MCPToolkit(BaseToolkit):
214
404
  self._exit_stack = AsyncExitStack()
215
405
 
216
406
  try:
217
- # Connect to all clients using AsyncExitStack
218
- for i, client in enumerate(self.clients):
219
- try:
220
- # Use MCPClient directly as async context manager
221
- await self._exit_stack.enter_async_context(client)
222
- msg = f"Connected to client {i+1}/{len(self.clients)}"
223
- logger.debug(msg)
224
- except Exception as e:
225
- logger.error(f"Failed to connect to client {i+1}: {e}")
226
- # AsyncExitStack will handle cleanup of already connected
227
- await self._exit_stack.aclose()
228
- self._exit_stack = None
229
- error_msg = f"Failed to connect to client {i+1}: {e}"
230
- raise MCPConnectionError(error_msg) from e
407
+ # Apply timeout to the entire connection process
408
+ import asyncio
409
+
410
+ timeout_seconds = self.timeout or 30.0
411
+ await asyncio.wait_for(
412
+ self._connect_all_clients(), timeout=timeout_seconds
413
+ )
231
414
 
232
415
  self._is_connected = True
233
416
  msg = f"Successfully connected to {len(self.clients)} MCP servers"
234
417
  logger.info(msg)
235
418
  return self
236
419
 
420
+ except (asyncio.TimeoutError, asyncio.CancelledError):
421
+ self._is_connected = False
422
+ if self._exit_stack:
423
+ await self._exit_stack.aclose()
424
+ self._exit_stack = None
425
+
426
+ timeout_seconds = self.timeout or 30.0
427
+ error_msg = (
428
+ f"Connection timeout after {timeout_seconds}s. "
429
+ f"One or more MCP servers are not responding. "
430
+ f"Please check if the servers are running and accessible."
431
+ )
432
+ logger.error(error_msg)
433
+ raise MCPConnectionError(error_msg)
434
+
237
435
  except Exception:
238
436
  self._is_connected = False
239
437
  if self._exit_stack:
@@ -241,6 +439,23 @@ class MCPToolkit(BaseToolkit):
241
439
  self._exit_stack = None
242
440
  raise
243
441
 
442
+ async def _connect_all_clients(self):
443
+ r"""Connect to all clients sequentially."""
444
+ # Connect to all clients using AsyncExitStack
445
+ for i, client in enumerate(self.clients):
446
+ try:
447
+ # Use MCPClient directly as async context manager
448
+ await self._exit_stack.enter_async_context(client)
449
+ msg = f"Connected to client {i+1}/{len(self.clients)}"
450
+ logger.debug(msg)
451
+ except Exception as e:
452
+ logger.error(f"Failed to connect to client {i+1}: {e}")
453
+ # AsyncExitStack will cleanup already connected clients
454
+ await self._exit_stack.aclose()
455
+ self._exit_stack = None
456
+ error_msg = f"Failed to connect to client {i+1}: {e}"
457
+ raise MCPConnectionError(error_msg) from e
458
+
244
459
  async def disconnect(self):
245
460
  r"""Disconnect from all MCP servers."""
246
461
  if not self._is_connected:
@@ -445,50 +660,151 @@ class MCPToolkit(BaseToolkit):
445
660
  raise ValueError(error_msg) from e
446
661
 
447
662
  def _ensure_strict_tool_schema(self, tool: FunctionTool) -> FunctionTool:
448
- r"""Ensure a tool has a strict schema compatible with OpenAI's
449
- requirements.
450
-
451
- Args:
452
- tool (FunctionTool): The tool to check and update if necessary.
453
-
454
- Returns:
455
- FunctionTool: The tool with a strict schema.
663
+ r"""Ensure a tool has a strict schema compatible with
664
+ OpenAI's requirements.
665
+
666
+ Strategy:
667
+ - Ensure parameters exist with at least an empty properties object
668
+ (OpenAI requirement).
669
+ - Try converting parameters to strict using ensure_strict_json_schema.
670
+ - If conversion fails, mark function.strict = False and
671
+ keep best-effort parameters.
456
672
  """
457
673
  try:
458
674
  schema = tool.get_openai_tool_schema()
459
675
 
460
- # Check if the tool already has strict mode enabled
461
- if schema.get("function", {}).get("strict") is True:
462
- return tool
463
-
464
- # Update the schema to be strict
676
+ def _has_strict_mode_incompatible_features(json_schema):
677
+ r"""Check if schema has features incompatible
678
+ with OpenAI strict mode."""
679
+
680
+ def _check_incompatible(obj, path=""):
681
+ if not isinstance(obj, dict):
682
+ return False
683
+
684
+ # Check for allOf in array items (known to cause issues)
685
+ if "items" in obj and isinstance(obj["items"], dict):
686
+ items_schema = obj["items"]
687
+ if "allOf" in items_schema:
688
+ logger.debug(
689
+ f"Found allOf in array items at {path}"
690
+ )
691
+ return True
692
+ # Recursively check items schema
693
+ if _check_incompatible(items_schema, f"{path}.items"):
694
+ return True
695
+
696
+ # Check for other potentially problematic patterns
697
+ # anyOf/oneOf in certain contexts can also cause issues
698
+ if (
699
+ "anyOf" in obj and len(obj["anyOf"]) > 10
700
+ ): # Large unions can be problematic
701
+ return True
702
+
703
+ # Recursively check nested objects
704
+ for key in [
705
+ "properties",
706
+ "additionalProperties",
707
+ "patternProperties",
708
+ ]:
709
+ if key in obj and isinstance(obj[key], dict):
710
+ if key == "properties":
711
+ for prop_name, prop_schema in obj[key].items():
712
+ if isinstance(
713
+ prop_schema, dict
714
+ ) and _check_incompatible(
715
+ prop_schema,
716
+ f"{path}.{key}.{prop_name}",
717
+ ):
718
+ return True
719
+ elif _check_incompatible(
720
+ obj[key], f"{path}.{key}"
721
+ ):
722
+ return True
723
+
724
+ # Check arrays and unions
725
+ for key in ["allOf", "anyOf", "oneOf"]:
726
+ if key in obj and isinstance(obj[key], list):
727
+ for i, item in enumerate(obj[key]):
728
+ if isinstance(
729
+ item, dict
730
+ ) and _check_incompatible(
731
+ item, f"{path}.{key}[{i}]"
732
+ ):
733
+ return True
734
+
735
+ return False
736
+
737
+ return _check_incompatible(json_schema)
738
+
739
+ # Apply sanitization if available
465
740
  if "function" in schema:
466
- schema["function"]["strict"] = True
467
-
468
- # Ensure parameters have proper strict mode configuration
469
- parameters = schema["function"].get("parameters", {})
470
- if parameters:
471
- # Ensure additionalProperties is false
472
- parameters["additionalProperties"] = False
473
-
474
- # Process properties to handle optional fields
475
- properties = parameters.get("properties", {})
476
- parameters["required"] = list(properties.keys())
477
-
478
- # Apply the sanitization function from function_tool
741
+ try:
479
742
  from camel.toolkits.function_tool import (
480
743
  sanitize_and_enforce_required,
481
744
  )
482
745
 
483
746
  schema = sanitize_and_enforce_required(schema)
747
+ except ImportError:
748
+ logger.debug("sanitize_and_enforce_required not available")
749
+
750
+ parameters = schema["function"].get("parameters", {})
751
+ if not parameters:
752
+ # Empty parameters - use minimal valid schema
753
+ parameters = {
754
+ "type": "object",
755
+ "properties": {},
756
+ "additionalProperties": False,
757
+ }
758
+ schema["function"]["parameters"] = parameters
759
+
760
+ # MCP spec doesn't require 'properties', but OpenAI spec does
761
+ if (
762
+ parameters.get("type") == "object"
763
+ and "properties" not in parameters
764
+ ):
765
+ parameters["properties"] = {}
766
+
767
+ try:
768
+ # _check_schema_limits(parameters)
769
+
770
+ # Check for OpenAI strict mode incompatible features
771
+ if _has_strict_mode_incompatible_features(parameters):
772
+ raise ValueError(
773
+ "Schema contains features "
774
+ "incompatible with strict mode"
775
+ )
776
+
777
+ strict_params = ensure_strict_json_schema(parameters)
778
+ schema["function"]["parameters"] = strict_params
779
+ schema["function"]["strict"] = True
780
+ except Exception as e:
781
+ # Fallback to non-strict mode on any failure
782
+ schema["function"]["strict"] = False
783
+ logger.warning(
784
+ f"Tool '{tool.get_function_name()}' "
785
+ f"cannot use strict mode: {e}"
786
+ )
484
787
 
485
788
  tool.set_openai_tool_schema(schema)
486
- logger.debug(
487
- f"Updated tool '{tool.get_function_name()}' to strict mode"
488
- )
489
789
 
490
790
  except Exception as e:
491
- logger.warning(f"Failed to ensure strict schema for tool: {e}")
791
+ # Final fallback - ensure tool still works
792
+ try:
793
+ current_schema = tool.get_openai_tool_schema()
794
+ if "function" in current_schema:
795
+ current_schema["function"]["strict"] = False
796
+ tool.set_openai_tool_schema(current_schema)
797
+ logger.warning(
798
+ f"Error processing schema for tool "
799
+ f"'{tool.get_function_name()}': {str(e)[:100]}. "
800
+ f"Using non-strict mode."
801
+ )
802
+ except Exception as inner_e:
803
+ logger.error(
804
+ f"Critical error processing tool "
805
+ f"'{tool.get_function_name()}': {inner_e}. "
806
+ f"Tool may not function correctly."
807
+ )
492
808
 
493
809
  return tool
494
810
 
@@ -529,6 +845,7 @@ class MCPToolkit(BaseToolkit):
529
845
  )
530
846
 
531
847
  all_tools = []
848
+ seen_names: set[str] = set()
532
849
  for i, client in enumerate(self.clients):
533
850
  try:
534
851
  client_tools = client.get_tools()
@@ -537,6 +854,14 @@ class MCPToolkit(BaseToolkit):
537
854
  strict_tools = []
538
855
  for tool in client_tools:
539
856
  strict_tool = self._ensure_strict_tool_schema(tool)
857
+ name = strict_tool.get_function_name()
858
+ if name in seen_names:
859
+ logger.warning(
860
+ f"Duplicate tool name detected and "
861
+ f"skipped: '{name}' from client {i+1}"
862
+ )
863
+ continue
864
+ seen_names.add(name)
540
865
  strict_tools.append(strict_tool)
541
866
 
542
867
  all_tools.extend(strict_tools)
@@ -43,7 +43,11 @@ class MemoryToolkit(BaseToolkit):
43
43
  (default: :obj:`None`)
44
44
  """
45
45
 
46
- def __init__(self, agent: 'ChatAgent', timeout: Optional[float] = None):
46
+ def __init__(
47
+ self,
48
+ agent: 'ChatAgent',
49
+ timeout: Optional[float] = None,
50
+ ):
47
51
  super().__init__(timeout=timeout)
48
52
  self.agent = agent
49
53