camel-ai 0.2.73a4__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 (173) hide show
  1. camel/__init__.py +1 -1
  2. camel/agents/_utils.py +38 -0
  3. camel/agents/chat_agent.py +2217 -519
  4. camel/agents/mcp_agent.py +30 -27
  5. camel/configs/__init__.py +15 -0
  6. camel/configs/aihubmix_config.py +88 -0
  7. camel/configs/amd_config.py +70 -0
  8. camel/configs/cometapi_config.py +104 -0
  9. camel/configs/minimax_config.py +93 -0
  10. camel/configs/nebius_config.py +103 -0
  11. camel/data_collectors/alpaca_collector.py +15 -6
  12. camel/datasets/base_generator.py +39 -10
  13. camel/environments/single_step.py +28 -3
  14. camel/environments/tic_tac_toe.py +1 -1
  15. camel/interpreters/__init__.py +2 -0
  16. camel/interpreters/docker/Dockerfile +3 -12
  17. camel/interpreters/e2b_interpreter.py +34 -1
  18. camel/interpreters/microsandbox_interpreter.py +395 -0
  19. camel/loaders/__init__.py +11 -2
  20. camel/loaders/chunkr_reader.py +9 -0
  21. camel/memories/agent_memories.py +48 -4
  22. camel/memories/base.py +26 -0
  23. camel/memories/blocks/chat_history_block.py +122 -4
  24. camel/memories/context_creators/score_based.py +25 -384
  25. camel/memories/records.py +88 -8
  26. camel/messages/base.py +153 -34
  27. camel/models/__init__.py +10 -0
  28. camel/models/aihubmix_model.py +83 -0
  29. camel/models/aiml_model.py +1 -16
  30. camel/models/amd_model.py +101 -0
  31. camel/models/anthropic_model.py +6 -19
  32. camel/models/aws_bedrock_model.py +2 -33
  33. camel/models/azure_openai_model.py +114 -89
  34. camel/models/base_audio_model.py +3 -1
  35. camel/models/base_model.py +32 -14
  36. camel/models/cohere_model.py +1 -16
  37. camel/models/cometapi_model.py +83 -0
  38. camel/models/crynux_model.py +1 -16
  39. camel/models/deepseek_model.py +1 -16
  40. camel/models/fish_audio_model.py +6 -0
  41. camel/models/gemini_model.py +36 -18
  42. camel/models/groq_model.py +1 -17
  43. camel/models/internlm_model.py +1 -16
  44. camel/models/litellm_model.py +1 -16
  45. camel/models/lmstudio_model.py +1 -17
  46. camel/models/minimax_model.py +83 -0
  47. camel/models/mistral_model.py +1 -16
  48. camel/models/model_factory.py +27 -1
  49. camel/models/modelscope_model.py +1 -16
  50. camel/models/moonshot_model.py +105 -24
  51. camel/models/nebius_model.py +83 -0
  52. camel/models/nemotron_model.py +0 -5
  53. camel/models/netmind_model.py +1 -16
  54. camel/models/novita_model.py +1 -16
  55. camel/models/nvidia_model.py +1 -16
  56. camel/models/ollama_model.py +4 -19
  57. camel/models/openai_compatible_model.py +62 -41
  58. camel/models/openai_model.py +62 -57
  59. camel/models/openrouter_model.py +1 -17
  60. camel/models/ppio_model.py +1 -16
  61. camel/models/qianfan_model.py +1 -16
  62. camel/models/qwen_model.py +1 -16
  63. camel/models/reka_model.py +1 -16
  64. camel/models/samba_model.py +34 -47
  65. camel/models/sglang_model.py +64 -31
  66. camel/models/siliconflow_model.py +1 -16
  67. camel/models/stub_model.py +0 -4
  68. camel/models/togetherai_model.py +1 -16
  69. camel/models/vllm_model.py +1 -16
  70. camel/models/volcano_model.py +0 -17
  71. camel/models/watsonx_model.py +1 -16
  72. camel/models/yi_model.py +1 -16
  73. camel/models/zhipuai_model.py +60 -16
  74. camel/parsers/__init__.py +18 -0
  75. camel/parsers/mcp_tool_call_parser.py +176 -0
  76. camel/retrievers/auto_retriever.py +1 -0
  77. camel/runtimes/daytona_runtime.py +11 -12
  78. camel/societies/__init__.py +2 -0
  79. camel/societies/workforce/__init__.py +2 -0
  80. camel/societies/workforce/events.py +122 -0
  81. camel/societies/workforce/prompts.py +146 -66
  82. camel/societies/workforce/role_playing_worker.py +15 -11
  83. camel/societies/workforce/single_agent_worker.py +302 -65
  84. camel/societies/workforce/structured_output_handler.py +30 -18
  85. camel/societies/workforce/task_channel.py +163 -27
  86. camel/societies/workforce/utils.py +107 -13
  87. camel/societies/workforce/workflow_memory_manager.py +772 -0
  88. camel/societies/workforce/workforce.py +1949 -579
  89. camel/societies/workforce/workforce_callback.py +74 -0
  90. camel/societies/workforce/workforce_logger.py +168 -145
  91. camel/societies/workforce/workforce_metrics.py +33 -0
  92. camel/storages/key_value_storages/json.py +15 -2
  93. camel/storages/key_value_storages/mem0_cloud.py +48 -47
  94. camel/storages/object_storages/google_cloud.py +1 -1
  95. camel/storages/vectordb_storages/oceanbase.py +13 -13
  96. camel/storages/vectordb_storages/qdrant.py +3 -3
  97. camel/storages/vectordb_storages/tidb.py +8 -6
  98. camel/tasks/task.py +4 -3
  99. camel/toolkits/__init__.py +20 -7
  100. camel/toolkits/aci_toolkit.py +45 -0
  101. camel/toolkits/base.py +6 -4
  102. camel/toolkits/code_execution.py +28 -1
  103. camel/toolkits/context_summarizer_toolkit.py +684 -0
  104. camel/toolkits/dappier_toolkit.py +5 -1
  105. camel/toolkits/dingtalk.py +1135 -0
  106. camel/toolkits/edgeone_pages_mcp_toolkit.py +11 -31
  107. camel/toolkits/excel_toolkit.py +1 -1
  108. camel/toolkits/{file_write_toolkit.py → file_toolkit.py} +430 -36
  109. camel/toolkits/function_tool.py +13 -3
  110. camel/toolkits/github_toolkit.py +104 -17
  111. camel/toolkits/gmail_toolkit.py +1839 -0
  112. camel/toolkits/google_calendar_toolkit.py +38 -4
  113. camel/toolkits/google_drive_mcp_toolkit.py +12 -31
  114. camel/toolkits/hybrid_browser_toolkit/config_loader.py +15 -0
  115. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +77 -8
  116. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +884 -88
  117. camel/toolkits/hybrid_browser_toolkit/installer.py +203 -0
  118. camel/toolkits/hybrid_browser_toolkit/ts/package-lock.json +5 -612
  119. camel/toolkits/hybrid_browser_toolkit/ts/package.json +0 -1
  120. camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +959 -89
  121. camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +9 -2
  122. camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +281 -213
  123. camel/toolkits/hybrid_browser_toolkit/ts/src/parent-child-filter.ts +226 -0
  124. camel/toolkits/hybrid_browser_toolkit/ts/src/snapshot-parser.ts +219 -0
  125. camel/toolkits/hybrid_browser_toolkit/ts/src/som-screenshot-injected.ts +543 -0
  126. camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +23 -3
  127. camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +72 -7
  128. camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +582 -132
  129. camel/toolkits/hybrid_browser_toolkit_py/actions.py +158 -0
  130. camel/toolkits/hybrid_browser_toolkit_py/browser_session.py +55 -8
  131. camel/toolkits/hybrid_browser_toolkit_py/config_loader.py +43 -0
  132. camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +321 -8
  133. camel/toolkits/hybrid_browser_toolkit_py/snapshot.py +10 -4
  134. camel/toolkits/hybrid_browser_toolkit_py/unified_analyzer.js +45 -4
  135. camel/toolkits/{openai_image_toolkit.py → image_generation_toolkit.py} +151 -53
  136. camel/toolkits/klavis_toolkit.py +5 -1
  137. camel/toolkits/markitdown_toolkit.py +27 -1
  138. camel/toolkits/math_toolkit.py +64 -10
  139. camel/toolkits/mcp_toolkit.py +366 -71
  140. camel/toolkits/memory_toolkit.py +5 -1
  141. camel/toolkits/message_integration.py +18 -13
  142. camel/toolkits/minimax_mcp_toolkit.py +195 -0
  143. camel/toolkits/note_taking_toolkit.py +19 -10
  144. camel/toolkits/notion_mcp_toolkit.py +16 -26
  145. camel/toolkits/openbb_toolkit.py +5 -1
  146. camel/toolkits/origene_mcp_toolkit.py +8 -49
  147. camel/toolkits/playwright_mcp_toolkit.py +12 -31
  148. camel/toolkits/resend_toolkit.py +168 -0
  149. camel/toolkits/search_toolkit.py +264 -91
  150. camel/toolkits/slack_toolkit.py +64 -10
  151. camel/toolkits/terminal_toolkit/__init__.py +18 -0
  152. camel/toolkits/terminal_toolkit/terminal_toolkit.py +957 -0
  153. camel/toolkits/terminal_toolkit/utils.py +532 -0
  154. camel/toolkits/vertex_ai_veo_toolkit.py +590 -0
  155. camel/toolkits/video_analysis_toolkit.py +17 -11
  156. camel/toolkits/wechat_official_toolkit.py +483 -0
  157. camel/toolkits/zapier_toolkit.py +5 -1
  158. camel/types/__init__.py +2 -2
  159. camel/types/enums.py +274 -7
  160. camel/types/openai_types.py +2 -2
  161. camel/types/unified_model_type.py +15 -0
  162. camel/utils/commons.py +36 -5
  163. camel/utils/constants.py +3 -0
  164. camel/utils/context_utils.py +1003 -0
  165. camel/utils/mcp.py +138 -4
  166. camel/utils/token_counting.py +43 -20
  167. {camel_ai-0.2.73a4.dist-info → camel_ai-0.2.80a2.dist-info}/METADATA +223 -83
  168. {camel_ai-0.2.73a4.dist-info → camel_ai-0.2.80a2.dist-info}/RECORD +170 -141
  169. camel/loaders/pandas_reader.py +0 -368
  170. camel/toolkits/openai_agent_toolkit.py +0 -135
  171. camel/toolkits/terminal_toolkit.py +0 -1550
  172. {camel_ai-0.2.73a4.dist-info → camel_ai-0.2.80a2.dist-info}/WHEEL +0 -0
  173. {camel_ai-0.2.73a4.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,80 +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 any object in the schema has additionalProperties: true
461
- def _has_additional_properties_true(obj):
462
- r"""Recursively check if any object has additionalProperties: true""" # noqa: E501
463
- if isinstance(obj, dict):
464
- if obj.get("additionalProperties") is True:
465
- return True
466
- for value in obj.values():
467
- if _has_additional_properties_true(value):
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
+ )
468
691
  return True
469
- elif isinstance(obj, list):
470
- for item in obj:
471
- if _has_additional_properties_true(item):
692
+ # Recursively check items schema
693
+ if _check_incompatible(items_schema, f"{path}.items"):
472
694
  return True
473
- return False
474
695
 
475
- # FIRST: Check if the schema contains additionalProperties: true
476
- # This must be checked before strict mode validation
477
- if _has_additional_properties_true(schema):
478
- # Force strict mode to False and log warning
479
- if "function" in schema:
480
- schema["function"]["strict"] = False
481
- tool.set_openai_tool_schema(schema)
482
- logger.warning(
483
- f"Tool '{tool.get_function_name()}' contains "
484
- f"additionalProperties: true which is incompatible with "
485
- f"OpenAI strict mode. Setting strict=False for this tool."
486
- )
487
- return tool
488
-
489
- # SECOND: Check if the tool already has strict mode enabled
490
- # Only do this if there are no additionalProperties conflicts
491
- if schema.get("function", {}).get("strict") is True:
492
- return tool
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
493
702
 
494
- # Update the schema to be strict
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
495
740
  if "function" in schema:
496
- schema["function"]["strict"] = True
497
-
498
- # Ensure parameters have proper strict mode configuration
499
- parameters = schema["function"].get("parameters", {})
500
- if parameters:
501
- # Ensure additionalProperties is false
502
- parameters["additionalProperties"] = False
503
-
504
- # Process properties to handle optional fields
505
- properties = parameters.get("properties", {})
506
- parameters["required"] = list(properties.keys())
507
-
508
- # Apply the sanitization function from function_tool
741
+ try:
509
742
  from camel.toolkits.function_tool import (
510
743
  sanitize_and_enforce_required,
511
744
  )
512
745
 
513
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
+ )
514
787
 
515
788
  tool.set_openai_tool_schema(schema)
516
- logger.debug(
517
- f"Updated tool '{tool.get_function_name()}' to strict mode"
518
- )
519
789
 
520
790
  except Exception as e:
521
- 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
+ )
522
808
 
523
809
  return tool
524
810
 
@@ -559,6 +845,7 @@ class MCPToolkit(BaseToolkit):
559
845
  )
560
846
 
561
847
  all_tools = []
848
+ seen_names: set[str] = set()
562
849
  for i, client in enumerate(self.clients):
563
850
  try:
564
851
  client_tools = client.get_tools()
@@ -567,6 +854,14 @@ class MCPToolkit(BaseToolkit):
567
854
  strict_tools = []
568
855
  for tool in client_tools:
569
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)
570
865
  strict_tools.append(strict_tool)
571
866
 
572
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
 
@@ -148,12 +148,10 @@ class ToolkitMessageIntegration:
148
148
  """
149
149
  return FunctionTool(self.send_message_to_user)
150
150
 
151
- def register_toolkits(
152
- self, toolkit: BaseToolkit, tool_names: Optional[List[str]] = None
153
- ) -> BaseToolkit:
154
- r"""Add messaging capabilities to toolkit methods.
151
+ def register_toolkits(self, toolkit: BaseToolkit) -> BaseToolkit:
152
+ r"""Add messaging capabilities to all toolkit methods.
155
153
 
156
- This method modifies a toolkit so that specified tools can send
154
+ This method modifies a toolkit so that all its tools can send
157
155
  status messages to users while executing their primary function.
158
156
  The tools will accept optional messaging parameters:
159
157
  - message_title: Title of the status message
@@ -162,20 +160,18 @@ class ToolkitMessageIntegration:
162
160
 
163
161
  Args:
164
162
  toolkit: The toolkit to add messaging capabilities to
165
- tool_names: List of specific tool names to modify.
166
- If None, messaging is added to all tools.
167
163
 
168
164
  Returns:
169
- The toolkit with messaging capabilities added
165
+ The same toolkit instance with messaging capabilities added to
166
+ all methods.
170
167
  """
171
168
  original_tools = toolkit.get_tools()
172
169
  enhanced_methods = {}
173
170
  for tool in original_tools:
174
171
  method_name = tool.func.__name__
175
- if tool_names is None or method_name in tool_names:
176
- enhanced_func = self._add_messaging_to_tool(tool.func)
177
- enhanced_methods[method_name] = enhanced_func
178
- setattr(toolkit, method_name, enhanced_func)
172
+ enhanced_func = self._add_messaging_to_tool(tool.func)
173
+ enhanced_methods[method_name] = enhanced_func
174
+ setattr(toolkit, method_name, enhanced_func)
179
175
  original_get_tools_method = toolkit.get_tools
180
176
 
181
177
  def enhanced_get_tools() -> List[FunctionTool]:
@@ -201,7 +197,7 @@ class ToolkitMessageIntegration:
201
197
  def enhanced_clone_for_new_session(new_session_id=None):
202
198
  cloned_toolkit = original_clone_method(new_session_id)
203
199
  return message_integration_instance.register_toolkits(
204
- cloned_toolkit, tool_names
200
+ cloned_toolkit
205
201
  )
206
202
 
207
203
  toolkit.clone_for_new_session = enhanced_clone_for_new_session
@@ -300,6 +296,12 @@ class ToolkitMessageIntegration:
300
296
  This internal method modifies the function signature and docstring
301
297
  to include optional messaging parameters that trigger status updates.
302
298
  """
299
+ if getattr(func, "__message_integration_enhanced__", False):
300
+ logger.debug(
301
+ f"Function {func.__name__} already enhanced, skipping"
302
+ )
303
+ return func
304
+
303
305
  # Get the original signature
304
306
  original_sig = inspect.signature(func)
305
307
 
@@ -464,6 +466,9 @@ class ToolkitMessageIntegration:
464
466
  # Apply the new signature to the wrapper
465
467
  wrapper.__signature__ = new_sig # type: ignore[attr-defined]
466
468
 
469
+ # Mark this function as enhanced by message integration
470
+ wrapper.__message_integration_enhanced__ = True # type: ignore[attr-defined]
471
+
467
472
  # Create a hybrid approach:
468
473
  # store toolkit instance info but preserve calling behavior
469
474
  # We'll use a property-like