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.
- camel/__init__.py +1 -1
- camel/agents/_types.py +6 -2
- camel/agents/_utils.py +38 -0
- camel/agents/chat_agent.py +4014 -410
- camel/agents/mcp_agent.py +30 -27
- camel/agents/repo_agent.py +2 -1
- camel/benchmarks/browsecomp.py +6 -6
- 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/configs/vllm_config.py +2 -0
- camel/data_collectors/alpaca_collector.py +15 -6
- camel/datagen/self_improving_cot.py +1 -1
- camel/datasets/base_generator.py +39 -10
- camel/environments/__init__.py +12 -0
- camel/environments/rlcards_env.py +860 -0
- 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 +4 -16
- camel/interpreters/docker_interpreter.py +3 -2
- camel/interpreters/e2b_interpreter.py +34 -1
- camel/interpreters/internal_python_interpreter.py +51 -2
- camel/interpreters/microsandbox_interpreter.py +395 -0
- camel/loaders/__init__.py +11 -2
- camel/loaders/base_loader.py +85 -0
- camel/loaders/chunkr_reader.py +9 -0
- camel/loaders/firecrawl_reader.py +4 -4
- camel/logger.py +1 -1
- camel/memories/agent_memories.py +84 -1
- camel/memories/base.py +34 -0
- camel/memories/blocks/chat_history_block.py +122 -4
- camel/memories/blocks/vectordb_block.py +8 -1
- camel/memories/context_creators/score_based.py +29 -237
- camel/memories/records.py +88 -8
- camel/messages/base.py +166 -40
- camel/messages/func_message.py +32 -5
- 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 +117 -18
- camel/models/aws_bedrock_model.py +2 -33
- camel/models/azure_openai_model.py +205 -91
- camel/models/base_audio_model.py +3 -1
- camel/models/base_model.py +189 -24
- camel/models/cohere_model.py +5 -17
- camel/models/cometapi_model.py +83 -0
- camel/models/crynux_model.py +1 -16
- camel/models/deepseek_model.py +6 -16
- camel/models/fish_audio_model.py +6 -0
- camel/models/gemini_model.py +71 -20
- camel/models/groq_model.py +1 -17
- camel/models/internlm_model.py +1 -16
- camel/models/litellm_model.py +49 -32
- 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/model_manager.py +24 -6
- camel/models/modelscope_model.py +1 -16
- camel/models/moonshot_model.py +185 -19
- 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 +171 -46
- camel/models/openai_model.py +205 -77
- 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/configs.py +11 -11
- camel/runtimes/daytona_runtime.py +15 -16
- camel/runtimes/docker_runtime.py +6 -6
- camel/runtimes/remote_http_runtime.py +5 -5
- camel/services/agent_openapi_server.py +380 -0
- camel/societies/__init__.py +2 -0
- camel/societies/role_playing.py +26 -28
- camel/societies/workforce/__init__.py +2 -0
- camel/societies/workforce/events.py +122 -0
- camel/societies/workforce/prompts.py +249 -38
- camel/societies/workforce/role_playing_worker.py +82 -20
- camel/societies/workforce/single_agent_worker.py +634 -34
- camel/societies/workforce/structured_output_handler.py +512 -0
- camel/societies/workforce/task_channel.py +169 -23
- camel/societies/workforce/utils.py +176 -9
- camel/societies/workforce/worker.py +77 -23
- camel/societies/workforce/workflow_memory_manager.py +772 -0
- camel/societies/workforce/workforce.py +3168 -478
- camel/societies/workforce/workforce_callback.py +74 -0
- camel/societies/workforce/workforce_logger.py +203 -175
- camel/societies/workforce/workforce_metrics.py +33 -0
- camel/storages/__init__.py +4 -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/__init__.py +6 -0
- camel/storages/vectordb_storages/chroma.py +731 -0
- camel/storages/vectordb_storages/oceanbase.py +13 -13
- camel/storages/vectordb_storages/pgvector.py +349 -0
- camel/storages/vectordb_storages/qdrant.py +3 -3
- camel/storages/vectordb_storages/surreal.py +365 -0
- camel/storages/vectordb_storages/tidb.py +8 -6
- camel/tasks/task.py +244 -27
- camel/toolkits/__init__.py +46 -8
- camel/toolkits/aci_toolkit.py +64 -19
- camel/toolkits/arxiv_toolkit.py +6 -6
- camel/toolkits/base.py +63 -5
- camel/toolkits/code_execution.py +28 -1
- camel/toolkits/context_summarizer_toolkit.py +684 -0
- camel/toolkits/craw4ai_toolkit.py +93 -0
- camel/toolkits/dappier_toolkit.py +10 -6
- camel/toolkits/dingtalk.py +1135 -0
- camel/toolkits/edgeone_pages_mcp_toolkit.py +49 -0
- camel/toolkits/excel_toolkit.py +901 -67
- camel/toolkits/file_toolkit.py +1402 -0
- camel/toolkits/function_tool.py +30 -6
- camel/toolkits/github_toolkit.py +107 -20
- camel/toolkits/gmail_toolkit.py +1839 -0
- camel/toolkits/google_calendar_toolkit.py +38 -4
- camel/toolkits/google_drive_mcp_toolkit.py +54 -0
- camel/toolkits/human_toolkit.py +34 -10
- camel/toolkits/hybrid_browser_toolkit/__init__.py +18 -0
- camel/toolkits/hybrid_browser_toolkit/config_loader.py +185 -0
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +246 -0
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +1973 -0
- camel/toolkits/hybrid_browser_toolkit/installer.py +203 -0
- camel/toolkits/hybrid_browser_toolkit/ts/package-lock.json +3749 -0
- camel/toolkits/hybrid_browser_toolkit/ts/package.json +32 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/browser-scripts.js +125 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +1815 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +233 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +590 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/index.ts +7 -0
- 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 +130 -0
- camel/toolkits/hybrid_browser_toolkit/ts/tsconfig.json +26 -0
- camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +319 -0
- camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +1032 -0
- camel/toolkits/hybrid_browser_toolkit_py/__init__.py +17 -0
- camel/toolkits/hybrid_browser_toolkit_py/actions.py +575 -0
- camel/toolkits/hybrid_browser_toolkit_py/agent.py +311 -0
- camel/toolkits/hybrid_browser_toolkit_py/browser_session.py +787 -0
- camel/toolkits/hybrid_browser_toolkit_py/config_loader.py +490 -0
- camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +2390 -0
- camel/toolkits/hybrid_browser_toolkit_py/snapshot.py +233 -0
- camel/toolkits/hybrid_browser_toolkit_py/stealth_script.js +0 -0
- camel/toolkits/hybrid_browser_toolkit_py/unified_analyzer.js +1043 -0
- camel/toolkits/image_generation_toolkit.py +390 -0
- camel/toolkits/jina_reranker_toolkit.py +3 -4
- camel/toolkits/klavis_toolkit.py +5 -1
- camel/toolkits/markitdown_toolkit.py +104 -0
- camel/toolkits/math_toolkit.py +64 -10
- camel/toolkits/mcp_toolkit.py +370 -45
- camel/toolkits/memory_toolkit.py +5 -1
- camel/toolkits/message_agent_toolkit.py +608 -0
- camel/toolkits/message_integration.py +724 -0
- camel/toolkits/minimax_mcp_toolkit.py +195 -0
- camel/toolkits/note_taking_toolkit.py +277 -0
- camel/toolkits/notion_mcp_toolkit.py +224 -0
- camel/toolkits/openbb_toolkit.py +5 -1
- camel/toolkits/origene_mcp_toolkit.py +56 -0
- camel/toolkits/playwright_mcp_toolkit.py +12 -31
- camel/toolkits/pptx_toolkit.py +25 -12
- camel/toolkits/resend_toolkit.py +168 -0
- camel/toolkits/screenshot_toolkit.py +213 -0
- camel/toolkits/search_toolkit.py +437 -142
- camel/toolkits/slack_toolkit.py +104 -50
- camel/toolkits/sympy_toolkit.py +1 -1
- camel/toolkits/task_planning_toolkit.py +3 -3
- 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/thinking_toolkit.py +1 -1
- camel/toolkits/vertex_ai_veo_toolkit.py +590 -0
- camel/toolkits/video_analysis_toolkit.py +106 -26
- camel/toolkits/video_download_toolkit.py +17 -14
- camel/toolkits/web_deploy_toolkit.py +1219 -0
- camel/toolkits/wechat_official_toolkit.py +483 -0
- camel/toolkits/zapier_toolkit.py +5 -1
- camel/types/__init__.py +2 -2
- camel/types/agents/tool_calling_record.py +4 -1
- camel/types/enums.py +316 -40
- camel/types/openai_types.py +2 -2
- camel/types/unified_model_type.py +31 -4
- 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/mcp_client.py +45 -1
- camel/utils/message_summarizer.py +148 -0
- camel/utils/token_counting.py +43 -20
- camel/utils/tool_result.py +44 -0
- {camel_ai-0.2.67.dist-info → camel_ai-0.2.80a2.dist-info}/METADATA +296 -85
- {camel_ai-0.2.67.dist-info → camel_ai-0.2.80a2.dist-info}/RECORD +219 -146
- camel/loaders/pandas_reader.py +0 -368
- camel/toolkits/dalle_toolkit.py +0 -175
- camel/toolkits/file_write_toolkit.py +0 -444
- camel/toolkits/openai_agent_toolkit.py +0 -135
- camel/toolkits/terminal_toolkit.py +0 -1037
- {camel_ai-0.2.67.dist-info → camel_ai-0.2.80a2.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.67.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,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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
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
|
|