camel-ai 0.2.23a0__py3-none-any.whl → 0.2.24__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of camel-ai might be problematic. Click here for more details.

@@ -0,0 +1,251 @@
1
+ # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software
9
+ # distributed under the License is distributed on an "AS IS" BASIS,
10
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ # See the License for the specific language governing permissions and
12
+ # limitations under the License.
13
+ # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
14
+ import inspect
15
+ from contextlib import AsyncExitStack, asynccontextmanager
16
+ from typing import (
17
+ TYPE_CHECKING,
18
+ Any,
19
+ Callable,
20
+ Dict,
21
+ List,
22
+ Optional,
23
+ Set,
24
+ Union,
25
+ )
26
+ from urllib.parse import urlparse
27
+
28
+ if TYPE_CHECKING:
29
+ from mcp import ListToolsResult, Tool
30
+
31
+ from camel.toolkits import BaseToolkit, FunctionTool
32
+
33
+
34
+ class MCPToolkit(BaseToolkit):
35
+ r"""MCPToolkit provides an abstraction layer to interact with external
36
+ tools using the Model Context Protocol (MCP). It supports two modes of
37
+ connection:
38
+
39
+ 1. stdio mode: Connects via standard input/output streams for local
40
+ command-line interactions.
41
+
42
+ 2. SSE mode (HTTP Server-Sent Events): Connects via HTTP for persistent,
43
+ event-based interactions.
44
+
45
+ Attributes:
46
+ command_or_url (str): URL for SSE mode or command executable for stdio
47
+ mode. (default: :obj:`'None'`)
48
+ args (List[str]): List of command-line arguments if stdio mode is used.
49
+ (default: :obj:`'None'`)
50
+ env (Dict[str, str]): Environment variables for the stdio mode command.
51
+ (default: :obj:`'None'`)
52
+ timeout (Optional[float]): Connection timeout. (default: :obj:`'None'`)
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ command_or_url: str,
58
+ args: Optional[List[str]] = None,
59
+ env: Optional[Dict[str, str]] = None,
60
+ timeout: Optional[float] = None,
61
+ ):
62
+ from mcp import Tool
63
+ from mcp.client.session import ClientSession
64
+
65
+ super().__init__(timeout=timeout)
66
+
67
+ self.command_or_url = command_or_url
68
+ self.args = args or []
69
+ self.env = env or {}
70
+
71
+ self._mcp_tools: List[Tool] = []
72
+ self._session: Optional['ClientSession'] = None
73
+ self._exit_stack = AsyncExitStack()
74
+ self._is_connected = False
75
+
76
+ @asynccontextmanager
77
+ async def connection(self):
78
+ r"""Async context manager for establishing and managing the connection
79
+ with the MCP server. Automatically selects SSE or stdio mode based
80
+ on the provided `command_or_url`.
81
+
82
+ Yields:
83
+ MCPToolkit: Instance with active connection ready for tool
84
+ interaction.
85
+ """
86
+ from mcp.client.session import ClientSession
87
+ from mcp.client.sse import sse_client
88
+ from mcp.client.stdio import StdioServerParameters, stdio_client
89
+
90
+ try:
91
+ if urlparse(self.command_or_url).scheme in ("http", "https"):
92
+ (
93
+ read_stream,
94
+ write_stream,
95
+ ) = await self._exit_stack.enter_async_context(
96
+ sse_client(self.command_or_url)
97
+ )
98
+ else:
99
+ server_parameters = StdioServerParameters(
100
+ command=self.command_or_url, args=self.args, env=self.env
101
+ )
102
+ (
103
+ read_stream,
104
+ write_stream,
105
+ ) = await self._exit_stack.enter_async_context(
106
+ stdio_client(server_parameters)
107
+ )
108
+
109
+ self._session = await self._exit_stack.enter_async_context(
110
+ ClientSession(read_stream, write_stream)
111
+ )
112
+ await self._session.initialize()
113
+ list_tools_result = await self.list_mcp_tools()
114
+ self._mcp_tools = list_tools_result.tools
115
+ self._is_connected = True
116
+ yield self
117
+
118
+ finally:
119
+ self._is_connected = False
120
+ await self._exit_stack.aclose()
121
+ self._session = None
122
+
123
+ async def list_mcp_tools(self) -> Union[str, "ListToolsResult"]:
124
+ r"""Retrieves the list of available tools from the connected MCP
125
+ server.
126
+
127
+ Returns:
128
+ ListToolsResult: Result containing available MCP tools.
129
+ """
130
+ if not self._session:
131
+ return "MCP Client is not connected. Call `connection()` first."
132
+ try:
133
+ return await self._session.list_tools()
134
+ except Exception as e:
135
+ return f"Failed to list MCP tools: {e!s}"
136
+
137
+ def generate_function_from_mcp_tool(self, mcp_tool: "Tool") -> Callable:
138
+ r"""Dynamically generates a Python callable function corresponding to
139
+ a given MCP tool.
140
+
141
+ Args:
142
+ mcp_tool (Tool): The MCP tool definition received from the MCP
143
+ server.
144
+
145
+ Returns:
146
+ Callable: A dynamically created async Python function that wraps
147
+ the MCP tool.
148
+ """
149
+ func_name = mcp_tool.name
150
+ func_desc = mcp_tool.description or "No description provided."
151
+ parameters_schema = mcp_tool.inputSchema.get("properties", {})
152
+ required_params = mcp_tool.inputSchema.get("required", [])
153
+
154
+ type_map = {
155
+ "string": str,
156
+ "integer": int,
157
+ "number": float,
158
+ "boolean": bool,
159
+ "array": list,
160
+ "object": dict,
161
+ }
162
+ annotations = {} # used to type hints
163
+ defaults: Dict[str, Any] = {} # store default values
164
+
165
+ func_params = []
166
+ for param_name, param_schema in parameters_schema.items():
167
+ param_type = param_schema.get("type", "Any")
168
+ param_type = type_map.get(param_type, Any)
169
+
170
+ annotations[param_name] = param_type
171
+ if param_name not in required_params:
172
+ defaults[param_name] = None
173
+
174
+ func_params.append(param_name)
175
+
176
+ async def dynamic_function(**kwargs):
177
+ r"""Auto-generated function for MCP Tool interaction.
178
+
179
+ Args:
180
+ kwargs: Keyword arguments corresponding to MCP tool parameters.
181
+
182
+ Returns:
183
+ str: The textual result returned by the MCP tool.
184
+ """
185
+ from mcp.types import CallToolResult
186
+
187
+ missing_params: Set[str] = set(required_params) - set(
188
+ kwargs.keys()
189
+ )
190
+ if missing_params:
191
+ raise ValueError(
192
+ f"Missing required parameters: {missing_params}"
193
+ )
194
+
195
+ result: CallToolResult = await self._session.call_tool(
196
+ func_name, kwargs
197
+ )
198
+
199
+ if not result.content:
200
+ return "No data available for this request."
201
+
202
+ # Handle different content types
203
+ content = result.content[0]
204
+ if content.type == "text":
205
+ return content.text
206
+ elif content.type == "image":
207
+ # Return image URL or data URI if available
208
+ if hasattr(content, "url") and content.url:
209
+ return f"Image available at: {content.url}"
210
+ return "Image content received (data URI not shown)"
211
+ elif content.type == "embedded_resource":
212
+ # Return resource information if available
213
+ if hasattr(content, "name") and content.name:
214
+ return f"Embedded resource: {content.name}"
215
+ return "Embedded resource received"
216
+ else:
217
+ msg = f"Received content of type '{content.type}'"
218
+ return f"{msg} which is not fully supported yet."
219
+
220
+ dynamic_function.__name__ = func_name
221
+ dynamic_function.__doc__ = func_desc
222
+ dynamic_function.__annotations__ = annotations
223
+
224
+ sig = inspect.Signature(
225
+ parameters=[
226
+ inspect.Parameter(
227
+ name=param,
228
+ kind=inspect.Parameter.KEYWORD_ONLY,
229
+ default=defaults.get(param, inspect.Parameter.empty),
230
+ annotation=annotations[param],
231
+ )
232
+ for param in func_params
233
+ ]
234
+ )
235
+ dynamic_function.__signature__ = sig # type: ignore[attr-defined]
236
+
237
+ return dynamic_function
238
+
239
+ def get_tools(self) -> List[FunctionTool]:
240
+ r"""Returns a list of FunctionTool objects representing the
241
+ functions in the toolkit. Each function is dynamically generated
242
+ based on the MCP tool definitions received from the server.
243
+
244
+ Returns:
245
+ List[FunctionTool]: A list of FunctionTool objects
246
+ representing the functions in the toolkit.
247
+ """
248
+ return [
249
+ FunctionTool(self.generate_function_from_mcp_tool(mcp_tool))
250
+ for mcp_tool in self._mcp_tools
251
+ ]
@@ -0,0 +1,376 @@
1
+ var MultimodalWebSurfer = MultimodalWebSurfer || (function() {
2
+ let nextLabel = 10;
3
+
4
+ let roleMapping = {
5
+ "a": "link",
6
+ "area": "link",
7
+ "button": "button",
8
+ "input, type=button": "button",
9
+ "input, type=checkbox": "checkbox",
10
+ "input, type=email": "textbox",
11
+ "input, type=number": "spinbutton",
12
+ "input, type=radio": "radio",
13
+ "input, type=range": "slider",
14
+ "input, type=reset": "button",
15
+ "input, type=search": "searchbox",
16
+ "input, type=submit": "button",
17
+ "input, type=tel": "textbox",
18
+ "input, type=text": "textbox",
19
+ "input, type=url": "textbox",
20
+ "search": "search",
21
+ "select": "combobox",
22
+ "option": "option",
23
+ "textarea": "textbox"
24
+ };
25
+
26
+ let getCursor = function(elm) {
27
+ return window.getComputedStyle(elm)["cursor"];
28
+ };
29
+
30
+ let getInteractiveElements = function() {
31
+
32
+ let results = []
33
+ let roles = ["scrollbar", "searchbox", "slider", "spinbutton", "switch", "tab", "treeitem", "button", "checkbox", "gridcell", "link", "menuitem", "menuitemcheckbox", "menuitemradio", "option", "progressbar", "radio", "textbox", "combobox", "menu", "tree", "treegrid", "grid", "listbox", "radiogroup", "widget"];
34
+ let inertCursors = ["auto", "default", "none", "text", "vertical-text", "not-allowed", "no-drop"];
35
+
36
+ // Get the main interactive elements
37
+ let nodeList = document.querySelectorAll("input, select, textarea, button, [href], [onclick], [contenteditable], [tabindex]:not([tabindex='-1'])");
38
+ for (let i=0; i<nodeList.length; i++) { // Copy to something mutable
39
+ results.push(nodeList[i]);
40
+ }
41
+
42
+ // Anything not already included that has a suitable role
43
+ nodeList = document.querySelectorAll("[role]");
44
+ for (let i=0; i<nodeList.length; i++) { // Copy to something mutable
45
+ if (results.indexOf(nodeList[i]) == -1) {
46
+ let role = nodeList[i].getAttribute("role");
47
+ if (roles.indexOf(role) > -1) {
48
+ results.push(nodeList[i]);
49
+ }
50
+ }
51
+ }
52
+
53
+ // Any element that changes the cursor to something implying interactivity
54
+ nodeList = document.querySelectorAll("*");
55
+ for (let i=0; i<nodeList.length; i++) {
56
+ let node = nodeList[i];
57
+
58
+ // Cursor is default, or does not suggest interactivity
59
+ let cursor = getCursor(node);
60
+ if (inertCursors.indexOf(cursor) >= 0) {
61
+ continue;
62
+ }
63
+
64
+ // Move up to the first instance of this cursor change
65
+ parent = node.parentNode;
66
+ while (parent && getCursor(parent) == cursor) {
67
+ node = parent;
68
+ parent = node.parentNode;
69
+ }
70
+
71
+ // Add the node if it is new
72
+ if (results.indexOf(node) == -1) {
73
+ results.push(node);
74
+ }
75
+ }
76
+
77
+ return results;
78
+ };
79
+
80
+ let labelElements = function(elements) {
81
+ for (let i=0; i<elements.length; i++) {
82
+ if (!elements[i].hasAttribute("__elementId")) {
83
+ elements[i].setAttribute("__elementId", "" + (nextLabel++));
84
+ }
85
+ }
86
+ };
87
+
88
+ let isTopmost = function(element, x, y) {
89
+ let hit = document.elementFromPoint(x, y);
90
+
91
+ // Hack to handle elements outside the viewport
92
+ if (hit === null) {
93
+ return true;
94
+ }
95
+
96
+ while (hit) {
97
+ if (hit == element) return true;
98
+ hit = hit.parentNode;
99
+ }
100
+ return false;
101
+ };
102
+
103
+ let getFocusedElementId = function() {
104
+ let elm = document.activeElement;
105
+ while (elm) {
106
+ if (elm.hasAttribute && elm.hasAttribute("__elementId")) {
107
+ return elm.getAttribute("__elementId");
108
+ }
109
+ elm = elm.parentNode;
110
+ }
111
+ return null;
112
+ };
113
+
114
+ let trimmedInnerText = function(element) {
115
+ if (!element) {
116
+ return "";
117
+ }
118
+ let text = element.innerText;
119
+ if (!text) {
120
+ return "";
121
+ }
122
+ return text.trim();
123
+ };
124
+
125
+ let getApproximateAriaName = function(element) {
126
+ // Check for aria labels
127
+ if (element.hasAttribute("aria-labelledby")) {
128
+ let buffer = "";
129
+ let ids = element.getAttribute("aria-labelledby").split(" ");
130
+ for (let i=0; i<ids.length; i++) {
131
+ let label = document.getElementById(ids[i]);
132
+ if (label) {
133
+ buffer = buffer + " " + trimmedInnerText(label);
134
+ }
135
+ }
136
+ return buffer.trim();
137
+ }
138
+
139
+ if (element.hasAttribute("aria-label")) {
140
+ return element.getAttribute("aria-label");
141
+ }
142
+
143
+ // Check for labels
144
+ if (element.hasAttribute("id")) {
145
+ let label_id = element.getAttribute("id");
146
+ let label = "";
147
+ let labels = document.querySelectorAll("label[for='" + label_id + "']");
148
+ for (let j=0; j<labels.length; j++) {
149
+ label += labels[j].innerText + " ";
150
+ }
151
+ label = label.trim();
152
+ if (label != "") {
153
+ return label;
154
+ }
155
+ }
156
+
157
+ if (element.parentElement && element.parentElement.tagName == "LABEL") {
158
+ return element.parentElement.innerText;
159
+ }
160
+
161
+ // Check for alt text or titles
162
+ if (element.hasAttribute("alt")) {
163
+ return element.getAttribute("alt")
164
+ }
165
+
166
+ if (element.hasAttribute("title")) {
167
+ return element.getAttribute("title")
168
+ }
169
+
170
+ return trimmedInnerText(element);
171
+ };
172
+
173
+ let getApproximateAriaRole = function(element) {
174
+ let tag = element.tagName.toLowerCase();
175
+ if (tag == "input" && element.hasAttribute("type")) {
176
+ tag = tag + ", type=" + element.getAttribute("type");
177
+ }
178
+
179
+ if (element.hasAttribute("role")) {
180
+ return [element.getAttribute("role"), tag];
181
+ }
182
+ else if (tag in roleMapping) {
183
+ return [roleMapping[tag], tag];
184
+ }
185
+ else {
186
+ return ["", tag];
187
+ }
188
+ };
189
+
190
+ let getInteractiveRects = function() {
191
+ labelElements(getInteractiveElements());
192
+ let elements = document.querySelectorAll("[__elementId]");
193
+ let results = {};
194
+ for (let i=0; i<elements.length; i++) {
195
+ let key = elements[i].getAttribute("__elementId");
196
+ let rects = elements[i].getClientRects();
197
+ let ariaRole = getApproximateAriaRole(elements[i]);
198
+ let ariaName = getApproximateAriaName(elements[i]);
199
+ let vScrollable = elements[i].scrollHeight - elements[i].clientHeight >= 1;
200
+
201
+ let record = {
202
+ "tag_name": ariaRole[1],
203
+ "role": ariaRole[0],
204
+ "aria-name": ariaName,
205
+ "v-scrollable": vScrollable,
206
+ "rects": []
207
+ };
208
+
209
+ for (const rect of rects) {
210
+ let x = rect.left + rect.width/2;
211
+ let y = rect.top + rect.height/2;
212
+ if (isTopmost(elements[i], x, y)) {
213
+ record["rects"].push(JSON.parse(JSON.stringify(rect)));
214
+ }
215
+ }
216
+
217
+ if (record["rects"].length > 0) {
218
+ results[key] = record;
219
+ }
220
+ }
221
+ return results;
222
+ };
223
+
224
+ let getVisualViewport = function() {
225
+ let vv = window.visualViewport;
226
+ let de = document.documentElement;
227
+ return {
228
+ "height": vv ? vv.height : 0,
229
+ "width": vv ? vv.width : 0,
230
+ "offsetLeft": vv ? vv.offsetLeft : 0,
231
+ "offsetTop": vv ? vv.offsetTop : 0,
232
+ "pageLeft": vv ? vv.pageLeft : 0,
233
+ "pageTop": vv ? vv.pageTop : 0,
234
+ "scale": vv ? vv.scale : 0,
235
+ "clientWidth": de ? de.clientWidth : 0,
236
+ "clientHeight": de ? de.clientHeight : 0,
237
+ "scrollWidth": de ? de.scrollWidth : 0,
238
+ "scrollHeight": de ? de.scrollHeight : 0
239
+ };
240
+ };
241
+
242
+ let _getMetaTags = function() {
243
+ let meta = document.querySelectorAll("meta");
244
+ let results = {};
245
+ for (let i = 0; i<meta.length; i++) {
246
+ let key = null;
247
+ if (meta[i].hasAttribute("name")) {
248
+ key = meta[i].getAttribute("name");
249
+ }
250
+ else if (meta[i].hasAttribute("property")) {
251
+ key = meta[i].getAttribute("property");
252
+ }
253
+ else {
254
+ continue;
255
+ }
256
+ if (meta[i].hasAttribute("content")) {
257
+ results[key] = meta[i].getAttribute("content");
258
+ }
259
+ }
260
+ return results;
261
+ };
262
+
263
+ let _getJsonLd = function() {
264
+ let jsonld = [];
265
+ let scripts = document.querySelectorAll('script[type="application/ld+json"]');
266
+ for (let i=0; i<scripts.length; i++) {
267
+ jsonld.push(scripts[i].innerHTML.trim());
268
+ }
269
+ return jsonld;
270
+ };
271
+
272
+ // From: https://www.stevefenton.co.uk/blog/2022/12/parse-microdata-with-javascript/
273
+ let _getMicrodata = function() {
274
+ function sanitize(input) {
275
+ return input.replace(/\s/gi, ' ').trim();
276
+ }
277
+
278
+ function addValue(information, name, value) {
279
+ if (information[name]) {
280
+ if (typeof information[name] === 'array') {
281
+ information[name].push(value);
282
+ } else {
283
+ const arr = [];
284
+ arr.push(information[name]);
285
+ arr.push(value);
286
+ information[name] = arr;
287
+ }
288
+ } else {
289
+ information[name] = value;
290
+ }
291
+ }
292
+
293
+ function traverseItem(item, information) {
294
+ const children = item.children;
295
+
296
+ for (let i = 0; i < children.length; i++) {
297
+ const child = children[i];
298
+
299
+ if (child.hasAttribute('itemscope')) {
300
+ if (child.hasAttribute('itemprop')) {
301
+ const itemProp = child.getAttribute('itemprop');
302
+ const itemType = child.getAttribute('itemtype');
303
+
304
+ const childInfo = {
305
+ itemType: itemType
306
+ };
307
+
308
+ traverseItem(child, childInfo);
309
+
310
+ itemProp.split(' ').forEach(propName => {
311
+ addValue(information, propName, childInfo);
312
+ });
313
+ }
314
+
315
+ } else if (child.hasAttribute('itemprop')) {
316
+ const itemProp = child.getAttribute('itemprop');
317
+ itemProp.split(' ').forEach(propName => {
318
+ if (propName === 'url') {
319
+ addValue(information, propName, child.href);
320
+ } else {
321
+ addValue(information, propName, sanitize(child.getAttribute("content") || child.content || child.textContent || child.src || ""));
322
+ }
323
+ });
324
+ traverseItem(child, information);
325
+ } else {
326
+ traverseItem(child, information);
327
+ }
328
+ }
329
+ }
330
+
331
+ const microdata = [];
332
+
333
+ document.querySelectorAll("[itemscope]").forEach(function(elem, i) {
334
+ const itemType = elem.getAttribute('itemtype');
335
+ const information = {
336
+ itemType: itemType
337
+ };
338
+ traverseItem(elem, information);
339
+ microdata.push(information);
340
+ });
341
+
342
+ return microdata;
343
+ };
344
+
345
+ let getPageMetadata = function() {
346
+ let jsonld = _getJsonLd();
347
+ let metaTags = _getMetaTags();
348
+ let microdata = _getMicrodata();
349
+ let results = {}
350
+ if (jsonld.length > 0) {
351
+ try {
352
+ results["jsonld"] = JSON.parse(jsonld);
353
+ }
354
+ catch (e) {
355
+ results["jsonld"] = jsonld;
356
+ }
357
+ }
358
+ if (microdata.length > 0) {
359
+ results["microdata"] = microdata;
360
+ }
361
+ for (let key in metaTags) {
362
+ if (metaTags.hasOwnProperty(key)) {
363
+ results["meta_tags"] = metaTags;
364
+ break;
365
+ }
366
+ }
367
+ return results;
368
+ };
369
+
370
+ return {
371
+ getInteractiveRects: getInteractiveRects,
372
+ getVisualViewport: getVisualViewport,
373
+ getFocusedElementId: getFocusedElementId,
374
+ getPageMetadata: getPageMetadata,
375
+ };
376
+ })();