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.
- camel/__init__.py +1 -1
- camel/agents/chat_agent.py +16 -2
- camel/configs/anthropic_config.py +45 -11
- camel/datagen/self_improving_cot.py +2 -2
- camel/datagen/self_instruct/self_instruct.py +46 -2
- camel/models/__init__.py +2 -0
- camel/models/anthropic_model.py +5 -1
- camel/models/base_audio_model.py +92 -0
- camel/models/fish_audio_model.py +18 -8
- camel/models/model_manager.py +9 -0
- camel/models/openai_audio_models.py +80 -1
- camel/societies/role_playing.py +119 -0
- camel/toolkits/__init__.py +17 -1
- camel/toolkits/audio_analysis_toolkit.py +238 -0
- camel/toolkits/excel_toolkit.py +172 -0
- camel/toolkits/file_write_toolkit.py +371 -0
- camel/toolkits/image_analysis_toolkit.py +202 -0
- camel/toolkits/mcp_toolkit.py +251 -0
- camel/toolkits/page_script.js +376 -0
- camel/toolkits/terminal_toolkit.py +421 -0
- camel/toolkits/video_analysis_toolkit.py +407 -0
- camel/toolkits/{video_toolkit.py → video_download_toolkit.py} +19 -25
- camel/toolkits/web_toolkit.py +1306 -0
- camel/types/enums.py +3 -0
- {camel_ai-0.2.23a0.dist-info → camel_ai-0.2.24.dist-info}/METADATA +241 -106
- {camel_ai-0.2.23a0.dist-info → camel_ai-0.2.24.dist-info}/RECORD +57 -47
- {camel_ai-0.2.23a0.dist-info → camel_ai-0.2.24.dist-info}/WHEEL +1 -1
- {camel_ai-0.2.23a0.dist-info → camel_ai-0.2.24.dist-info/licenses}/LICENSE +0 -0
|
@@ -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
|
+
})();
|