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.
- camel/__init__.py +1 -1
- camel/agents/_utils.py +38 -0
- camel/agents/chat_agent.py +2217 -519
- camel/agents/mcp_agent.py +30 -27
- camel/configs/__init__.py +15 -0
- camel/configs/aihubmix_config.py +88 -0
- camel/configs/amd_config.py +70 -0
- camel/configs/cometapi_config.py +104 -0
- camel/configs/minimax_config.py +93 -0
- camel/configs/nebius_config.py +103 -0
- camel/data_collectors/alpaca_collector.py +15 -6
- camel/datasets/base_generator.py +39 -10
- camel/environments/single_step.py +28 -3
- camel/environments/tic_tac_toe.py +1 -1
- camel/interpreters/__init__.py +2 -0
- camel/interpreters/docker/Dockerfile +3 -12
- camel/interpreters/e2b_interpreter.py +34 -1
- camel/interpreters/microsandbox_interpreter.py +395 -0
- camel/loaders/__init__.py +11 -2
- camel/loaders/chunkr_reader.py +9 -0
- camel/memories/agent_memories.py +48 -4
- camel/memories/base.py +26 -0
- camel/memories/blocks/chat_history_block.py +122 -4
- camel/memories/context_creators/score_based.py +25 -384
- camel/memories/records.py +88 -8
- camel/messages/base.py +153 -34
- camel/models/__init__.py +10 -0
- camel/models/aihubmix_model.py +83 -0
- camel/models/aiml_model.py +1 -16
- camel/models/amd_model.py +101 -0
- camel/models/anthropic_model.py +6 -19
- camel/models/aws_bedrock_model.py +2 -33
- camel/models/azure_openai_model.py +114 -89
- camel/models/base_audio_model.py +3 -1
- camel/models/base_model.py +32 -14
- camel/models/cohere_model.py +1 -16
- camel/models/cometapi_model.py +83 -0
- camel/models/crynux_model.py +1 -16
- camel/models/deepseek_model.py +1 -16
- camel/models/fish_audio_model.py +6 -0
- camel/models/gemini_model.py +36 -18
- camel/models/groq_model.py +1 -17
- camel/models/internlm_model.py +1 -16
- camel/models/litellm_model.py +1 -16
- camel/models/lmstudio_model.py +1 -17
- camel/models/minimax_model.py +83 -0
- camel/models/mistral_model.py +1 -16
- camel/models/model_factory.py +27 -1
- camel/models/modelscope_model.py +1 -16
- camel/models/moonshot_model.py +105 -24
- camel/models/nebius_model.py +83 -0
- camel/models/nemotron_model.py +0 -5
- camel/models/netmind_model.py +1 -16
- camel/models/novita_model.py +1 -16
- camel/models/nvidia_model.py +1 -16
- camel/models/ollama_model.py +4 -19
- camel/models/openai_compatible_model.py +62 -41
- camel/models/openai_model.py +62 -57
- camel/models/openrouter_model.py +1 -17
- camel/models/ppio_model.py +1 -16
- camel/models/qianfan_model.py +1 -16
- camel/models/qwen_model.py +1 -16
- camel/models/reka_model.py +1 -16
- camel/models/samba_model.py +34 -47
- camel/models/sglang_model.py +64 -31
- camel/models/siliconflow_model.py +1 -16
- camel/models/stub_model.py +0 -4
- camel/models/togetherai_model.py +1 -16
- camel/models/vllm_model.py +1 -16
- camel/models/volcano_model.py +0 -17
- camel/models/watsonx_model.py +1 -16
- camel/models/yi_model.py +1 -16
- camel/models/zhipuai_model.py +60 -16
- camel/parsers/__init__.py +18 -0
- camel/parsers/mcp_tool_call_parser.py +176 -0
- camel/retrievers/auto_retriever.py +1 -0
- camel/runtimes/daytona_runtime.py +11 -12
- camel/societies/__init__.py +2 -0
- camel/societies/workforce/__init__.py +2 -0
- camel/societies/workforce/events.py +122 -0
- camel/societies/workforce/prompts.py +146 -66
- camel/societies/workforce/role_playing_worker.py +15 -11
- camel/societies/workforce/single_agent_worker.py +302 -65
- camel/societies/workforce/structured_output_handler.py +30 -18
- camel/societies/workforce/task_channel.py +163 -27
- camel/societies/workforce/utils.py +107 -13
- camel/societies/workforce/workflow_memory_manager.py +772 -0
- camel/societies/workforce/workforce.py +1949 -579
- camel/societies/workforce/workforce_callback.py +74 -0
- camel/societies/workforce/workforce_logger.py +168 -145
- camel/societies/workforce/workforce_metrics.py +33 -0
- camel/storages/key_value_storages/json.py +15 -2
- camel/storages/key_value_storages/mem0_cloud.py +48 -47
- camel/storages/object_storages/google_cloud.py +1 -1
- camel/storages/vectordb_storages/oceanbase.py +13 -13
- camel/storages/vectordb_storages/qdrant.py +3 -3
- camel/storages/vectordb_storages/tidb.py +8 -6
- camel/tasks/task.py +4 -3
- camel/toolkits/__init__.py +20 -7
- camel/toolkits/aci_toolkit.py +45 -0
- camel/toolkits/base.py +6 -4
- camel/toolkits/code_execution.py +28 -1
- camel/toolkits/context_summarizer_toolkit.py +684 -0
- camel/toolkits/dappier_toolkit.py +5 -1
- camel/toolkits/dingtalk.py +1135 -0
- camel/toolkits/edgeone_pages_mcp_toolkit.py +11 -31
- camel/toolkits/excel_toolkit.py +1 -1
- camel/toolkits/{file_write_toolkit.py → file_toolkit.py} +430 -36
- camel/toolkits/function_tool.py +13 -3
- camel/toolkits/github_toolkit.py +104 -17
- camel/toolkits/gmail_toolkit.py +1839 -0
- camel/toolkits/google_calendar_toolkit.py +38 -4
- camel/toolkits/google_drive_mcp_toolkit.py +12 -31
- camel/toolkits/hybrid_browser_toolkit/config_loader.py +15 -0
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +77 -8
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +884 -88
- camel/toolkits/hybrid_browser_toolkit/installer.py +203 -0
- camel/toolkits/hybrid_browser_toolkit/ts/package-lock.json +5 -612
- camel/toolkits/hybrid_browser_toolkit/ts/package.json +0 -1
- camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +959 -89
- camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +9 -2
- camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +281 -213
- camel/toolkits/hybrid_browser_toolkit/ts/src/parent-child-filter.ts +226 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/snapshot-parser.ts +219 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/som-screenshot-injected.ts +543 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +23 -3
- camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +72 -7
- camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +582 -132
- camel/toolkits/hybrid_browser_toolkit_py/actions.py +158 -0
- camel/toolkits/hybrid_browser_toolkit_py/browser_session.py +55 -8
- camel/toolkits/hybrid_browser_toolkit_py/config_loader.py +43 -0
- camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +321 -8
- camel/toolkits/hybrid_browser_toolkit_py/snapshot.py +10 -4
- camel/toolkits/hybrid_browser_toolkit_py/unified_analyzer.js +45 -4
- camel/toolkits/{openai_image_toolkit.py → image_generation_toolkit.py} +151 -53
- camel/toolkits/klavis_toolkit.py +5 -1
- camel/toolkits/markitdown_toolkit.py +27 -1
- camel/toolkits/math_toolkit.py +64 -10
- camel/toolkits/mcp_toolkit.py +366 -71
- camel/toolkits/memory_toolkit.py +5 -1
- camel/toolkits/message_integration.py +18 -13
- camel/toolkits/minimax_mcp_toolkit.py +195 -0
- camel/toolkits/note_taking_toolkit.py +19 -10
- camel/toolkits/notion_mcp_toolkit.py +16 -26
- camel/toolkits/openbb_toolkit.py +5 -1
- camel/toolkits/origene_mcp_toolkit.py +8 -49
- camel/toolkits/playwright_mcp_toolkit.py +12 -31
- camel/toolkits/resend_toolkit.py +168 -0
- camel/toolkits/search_toolkit.py +264 -91
- camel/toolkits/slack_toolkit.py +64 -10
- camel/toolkits/terminal_toolkit/__init__.py +18 -0
- camel/toolkits/terminal_toolkit/terminal_toolkit.py +957 -0
- camel/toolkits/terminal_toolkit/utils.py +532 -0
- camel/toolkits/vertex_ai_veo_toolkit.py +590 -0
- camel/toolkits/video_analysis_toolkit.py +17 -11
- camel/toolkits/wechat_official_toolkit.py +483 -0
- camel/toolkits/zapier_toolkit.py +5 -1
- camel/types/__init__.py +2 -2
- camel/types/enums.py +274 -7
- camel/types/openai_types.py +2 -2
- camel/types/unified_model_type.py +15 -0
- camel/utils/commons.py +36 -5
- camel/utils/constants.py +3 -0
- camel/utils/context_utils.py +1003 -0
- camel/utils/mcp.py +138 -4
- camel/utils/token_counting.py +43 -20
- {camel_ai-0.2.73a4.dist-info → camel_ai-0.2.80a2.dist-info}/METADATA +223 -83
- {camel_ai-0.2.73a4.dist-info → camel_ai-0.2.80a2.dist-info}/RECORD +170 -141
- camel/loaders/pandas_reader.py +0 -368
- camel/toolkits/openai_agent_toolkit.py +0 -135
- camel/toolkits/terminal_toolkit.py +0 -1550
- {camel_ai-0.2.73a4.dist-info → camel_ai-0.2.80a2.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.73a4.dist-info → camel_ai-0.2.80a2.dist-info}/licenses/LICENSE +0 -0
camel/toolkits/mcp_toolkit.py
CHANGED
|
@@ -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
|
|
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
|
-
#
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
449
|
-
requirements.
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
470
|
-
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
camel/toolkits/memory_toolkit.py
CHANGED
|
@@ -43,7 +43,11 @@ class MemoryToolkit(BaseToolkit):
|
|
|
43
43
|
(default: :obj:`None`)
|
|
44
44
|
"""
|
|
45
45
|
|
|
46
|
-
def __init__(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
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
|