agentcrew-ai 0.8.2__py3-none-any.whl → 0.8.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. AgentCrew/__init__.py +1 -1
  2. AgentCrew/modules/agents/local_agent.py +11 -0
  3. AgentCrew/modules/browser_automation/element_extractor.py +4 -3
  4. AgentCrew/modules/browser_automation/js/draw_element_boxes.js +200 -0
  5. AgentCrew/modules/browser_automation/js/extract_clickable_elements.js +57 -23
  6. AgentCrew/modules/browser_automation/js/extract_elements_by_text.js +21 -19
  7. AgentCrew/modules/browser_automation/js/extract_input_elements.js +22 -23
  8. AgentCrew/modules/browser_automation/js/filter_hidden_elements.js +104 -0
  9. AgentCrew/modules/browser_automation/js/remove_element_boxes.js +29 -0
  10. AgentCrew/modules/browser_automation/js_loader.py +385 -92
  11. AgentCrew/modules/browser_automation/service.py +118 -347
  12. AgentCrew/modules/browser_automation/tool.py +28 -29
  13. AgentCrew/modules/chat/message/conversation.py +9 -8
  14. AgentCrew/modules/console/input_handler.py +2 -0
  15. AgentCrew/modules/console/ui_effects.py +3 -4
  16. AgentCrew/modules/custom_llm/service.py +25 -3
  17. AgentCrew/modules/file_editing/tool.py +9 -11
  18. AgentCrew/modules/google/native_service.py +13 -0
  19. AgentCrew/modules/llm/constants.py +38 -1
  20. AgentCrew/modules/llm/model_registry.py +9 -0
  21. AgentCrew/modules/llm/types.py +12 -1
  22. AgentCrew/modules/memory/base_service.py +2 -2
  23. AgentCrew/modules/memory/chroma_service.py +80 -138
  24. AgentCrew/modules/memory/tool.py +15 -15
  25. AgentCrew/modules/openai/response_service.py +19 -11
  26. AgentCrew/modules/openai/service.py +15 -0
  27. AgentCrew/modules/prompts/constants.py +27 -14
  28. {agentcrew_ai-0.8.2.dist-info → agentcrew_ai-0.8.3.dist-info}/METADATA +2 -2
  29. {agentcrew_ai-0.8.2.dist-info → agentcrew_ai-0.8.3.dist-info}/RECORD +33 -30
  30. {agentcrew_ai-0.8.2.dist-info → agentcrew_ai-0.8.3.dist-info}/WHEEL +0 -0
  31. {agentcrew_ai-0.8.2.dist-info → agentcrew_ai-0.8.3.dist-info}/entry_points.txt +0 -0
  32. {agentcrew_ai-0.8.2.dist-info → agentcrew_ai-0.8.3.dist-info}/licenses/LICENSE +0 -0
  33. {agentcrew_ai-0.8.2.dist-info → agentcrew_ai-0.8.3.dist-info}/top_level.txt +0 -0
@@ -6,14 +6,367 @@ for use with Chrome DevTools Protocol.
6
6
  """
7
7
 
8
8
  from pathlib import Path
9
- from typing import Dict
9
+ from typing import Dict, Any, Optional
10
+ import time
11
+ from loguru import logger
12
+
13
+
14
+ class JavaScriptExecutor:
15
+ """Handles JavaScript code execution and result parsing for browser automation."""
16
+
17
+ @staticmethod
18
+ def execute_and_parse_result(chrome_interface: Any, js_code: str) -> Dict[str, Any]:
19
+ """
20
+ Execute JavaScript code and parse the result.
21
+
22
+ Args:
23
+ chrome_interface: Chrome DevTools Protocol interface
24
+ js_code: JavaScript code to execute
25
+
26
+ Returns:
27
+ Parsed result dictionary
28
+ """
29
+ try:
30
+ result = (None, [])
31
+ retried = 0
32
+ while result[0] is None and retried < 10:
33
+ result = chrome_interface.Runtime.evaluate(
34
+ expression=js_code,
35
+ returnByValue=True,
36
+ awaitPromise=True,
37
+ timeout=60000,
38
+ )
39
+ retried += 1
40
+ time.sleep(0.4)
41
+
42
+ if isinstance(result, tuple) and len(result) >= 2:
43
+ if isinstance(result[1], dict):
44
+ return (
45
+ result[1].get("result", {}).get("result", {}).get("value", {})
46
+ )
47
+ elif isinstance(result[1], list) and len(result[1]) > 0:
48
+ return (
49
+ result[1][0]
50
+ .get("result", {})
51
+ .get("result", {})
52
+ .get("value", {})
53
+ )
54
+ else:
55
+ return {
56
+ "success": False,
57
+ "error": "Invalid response format from JavaScript execution",
58
+ }
59
+ else:
60
+ return {
61
+ "success": False,
62
+ "error": "No response from JavaScript execution",
63
+ }
64
+
65
+ except Exception as e:
66
+ logger.error(f"JavaScript execution error: {e}")
67
+ return {"success": False, "error": f"JavaScript execution error: {str(e)}"}
68
+
69
+ @staticmethod
70
+ def get_current_url(chrome_interface: Any) -> str:
71
+ """
72
+ Get the current page URL.
73
+
74
+ Args:
75
+ chrome_interface: Chrome DevTools Protocol interface
76
+
77
+ Returns:
78
+ Current URL or "Unknown" if retrieval fails
79
+ """
80
+ try:
81
+ runtime_result = chrome_interface.Runtime.evaluate(
82
+ expression="window.location.href"
83
+ )
84
+
85
+ if isinstance(runtime_result, tuple) and len(runtime_result) >= 2:
86
+ if isinstance(runtime_result[1], dict):
87
+ current_url = (
88
+ runtime_result[1]
89
+ .get("result", {})
90
+ .get("result", {})
91
+ .get("value", "Unknown")
92
+ )
93
+ elif isinstance(runtime_result[1], list) and len(runtime_result[1]) > 0:
94
+ current_url = (
95
+ runtime_result[1][0]
96
+ .get("result", {})
97
+ .get("result", {})
98
+ .get("value", "Unknown")
99
+ )
100
+ else:
101
+ current_url = "Unknown"
102
+ else:
103
+ current_url = "Unknown"
104
+
105
+ return current_url
106
+
107
+ except Exception as e:
108
+ logger.warning(f"Could not get current URL: {e}")
109
+ return "Unknown"
110
+
111
+ @staticmethod
112
+ def focus_and_clear_element(chrome_interface: Any, xpath: str) -> Dict[str, Any]:
113
+ """
114
+ Focus an element and clear its content.
115
+
116
+ Args:
117
+ chrome_interface: Chrome DevTools Protocol interface
118
+ xpath: XPath selector for the element
119
+
120
+ Returns:
121
+ Result dictionary with success status
122
+ """
123
+ js_code = js_loader.get_focus_and_clear_element_js(xpath)
124
+ return JavaScriptExecutor.execute_and_parse_result(chrome_interface, js_code)
125
+
126
+ @staticmethod
127
+ def draw_element_boxes(
128
+ chrome_interface: Any, uuid_xpath_dict: Dict[str, str]
129
+ ) -> Dict[str, Any]:
130
+ """
131
+ Draw colored rectangle boxes with UUID labels over elements.
132
+
133
+ Args:
134
+ uuid_xpath_dict: Dictionary mapping UUIDs to XPath selectors
135
+
136
+ Returns:
137
+ Dict containing the result of the drawing operation
138
+ """
139
+ try:
140
+ js_code = js_loader.get_draw_element_boxes_js(uuid_xpath_dict)
141
+ eval_result = JavaScriptExecutor.execute_and_parse_result(
142
+ chrome_interface, js_code
143
+ )
144
+
145
+ if not eval_result:
146
+ return {
147
+ "success": False,
148
+ "error": "No result from drawing element boxes",
149
+ }
150
+
151
+ return eval_result
152
+
153
+ except Exception as e:
154
+ logger.error(f"Draw element boxes error: {e}")
155
+ return {"success": False, "error": f"Draw element boxes error: {str(e)}"}
156
+
157
+ @staticmethod
158
+ def remove_element_boxes(chrome_interface: Any) -> Dict[str, Any]:
159
+ """
160
+ Remove the overlay container with element boxes.
161
+
162
+ Returns:
163
+ Dict containing the result of the removal operation
164
+ """
165
+ try:
166
+ js_code = js_loader.get_remove_element_boxes_js()
167
+ eval_result = JavaScriptExecutor.execute_and_parse_result(
168
+ chrome_interface, js_code
169
+ )
170
+
171
+ if not eval_result:
172
+ return {
173
+ "success": False,
174
+ "error": "No result from removing element boxes",
175
+ }
176
+
177
+ return eval_result
178
+
179
+ except Exception as e:
180
+ logger.error(f"Remove element boxes error: {e}")
181
+ return {"success": False, "error": f"Remove element boxes error: {str(e)}"}
182
+
183
+ @staticmethod
184
+ def trigger_input_events(
185
+ chrome_interface: Any, xpath: str, value: str
186
+ ) -> Dict[str, Any]:
187
+ """
188
+ Trigger input and change events on an element.
189
+
190
+ Args:
191
+ chrome_interface: Chrome DevTools Protocol interface
192
+ xpath: XPath selector for the element
193
+ value: Value to set
194
+
195
+ Returns:
196
+ Result dictionary with success status
197
+ """
198
+ js_code = js_loader.get_trigger_input_events_js(xpath, value)
199
+ return JavaScriptExecutor.execute_and_parse_result(chrome_interface, js_code)
200
+
201
+ @staticmethod
202
+ def simulate_typing(chrome_interface: Any, text: str) -> Dict[str, Any]:
203
+ """
204
+ Simulate keyboard typing character by character.
205
+
206
+ Args:
207
+ chrome_interface: Chrome DevTools Protocol interface
208
+ text: Text to type
209
+
210
+ Returns:
211
+ Result dictionary with success status and characters typed
212
+ """
213
+ try:
214
+ for char in text:
215
+ time.sleep(0.05)
216
+
217
+ if char == "\n":
218
+ chrome_interface.Input.dispatchKeyEvent(
219
+ **{
220
+ "type": "rawKeyDown",
221
+ "windowsVirtualKeyCode": 13,
222
+ "unmodifiedText": "\r",
223
+ "text": "\r",
224
+ }
225
+ )
226
+ chrome_interface.Input.dispatchKeyEvent(
227
+ **{
228
+ "type": "char",
229
+ "windowsVirtualKeyCode": 13,
230
+ "unmodifiedText": "\r",
231
+ "text": "\r",
232
+ }
233
+ )
234
+ chrome_interface.Input.dispatchKeyEvent(
235
+ **{
236
+ "type": "keyUp",
237
+ "windowsVirtualKeyCode": 13,
238
+ "unmodifiedText": "\r",
239
+ "text": "\r",
240
+ }
241
+ )
242
+ elif char == "\t":
243
+ chrome_interface.Input.dispatchKeyEvent(type="char", text="\t")
244
+ else:
245
+ chrome_interface.Input.dispatchKeyEvent(type="char", text=char)
246
+
247
+ return {
248
+ "success": True,
249
+ "message": f"Successfully typed {len(text)} characters",
250
+ "characters_typed": len(text),
251
+ }
252
+
253
+ except Exception as e:
254
+ logger.error(f"Error during typing simulation: {e}")
255
+ return {"success": False, "error": f"Typing simulation failed: {str(e)}"}
256
+
257
+ @staticmethod
258
+ def dispatch_key_event(
259
+ chrome_interface: Any, key: str, modifiers: Optional[list] = None
260
+ ) -> Dict[str, Any]:
261
+ """
262
+ Dispatch key events using CDP.
263
+
264
+ Args:
265
+ chrome_interface: Chrome DevTools Protocol interface
266
+ key: Key to dispatch (e.g., 'Enter', 'Up', 'Down')
267
+ modifiers: Optional list of modifiers ('ctrl', 'alt', 'shift')
268
+
269
+ Returns:
270
+ Result dictionary with success status
271
+ """
272
+ if modifiers is None:
273
+ modifiers = []
274
+
275
+ try:
276
+ key_name = key.lower().strip()
277
+ key_code = key_codes.get(key_name)
278
+
279
+ if key_code is None:
280
+ return {
281
+ "success": False,
282
+ "error": f"Unknown key '{key}'. Supported keys: {', '.join(sorted(key_codes.keys()))}",
283
+ "key": key,
284
+ "modifiers": modifiers,
285
+ }
286
+
287
+ modifier_flags = 0
288
+ if modifiers:
289
+ modifier_names = [m.strip().lower() for m in modifiers]
290
+ for mod in modifier_names:
291
+ if mod in ["alt"]:
292
+ modifier_flags |= 1
293
+ elif mod in ["ctrl", "control"]:
294
+ modifier_flags |= 2
295
+ elif mod in ["meta", "cmd", "command"]:
296
+ modifier_flags |= 4
297
+ elif mod in ["shift"]:
298
+ modifier_flags |= 8
299
+
300
+ chrome_interface.Input.dispatchKeyEvent(
301
+ type="rawKeyDown",
302
+ windowsVirtualKeyCode=key_code,
303
+ modifiers=modifier_flags,
304
+ )
305
+
306
+ printable_keys = {"space", "spacebar", "enter", "return", "tab"}
307
+ if key_name in printable_keys:
308
+ if key_name in ["space", "spacebar"]:
309
+ char_text = " "
310
+ elif key_name in ["enter", "return"]:
311
+ char_text = "\r"
312
+ elif key_name == "tab":
313
+ char_text = "\t"
314
+ else:
315
+ char_text = ""
316
+
317
+ if char_text:
318
+ chrome_interface.Input.dispatchKeyEvent(
319
+ type="char",
320
+ windowsVirtualKeyCode=key_code,
321
+ text=char_text,
322
+ unmodifiedText=char_text,
323
+ modifiers=modifier_flags,
324
+ )
325
+
326
+ chrome_interface.Input.dispatchKeyEvent(
327
+ type="keyUp", windowsVirtualKeyCode=key_code, modifiers=modifier_flags
328
+ )
329
+
330
+ time.sleep(0.1)
331
+
332
+ return {
333
+ "success": True,
334
+ "message": f"Successfully dispatched key '{key}' with modifiers '{modifiers}'",
335
+ "key": key,
336
+ "key_code": key_code,
337
+ "modifiers": modifiers,
338
+ "modifier_flags": modifier_flags,
339
+ }
340
+
341
+ except Exception as e:
342
+ logger.error(f"Key dispatch error: {e}")
343
+ return {
344
+ "success": False,
345
+ "error": f"Key dispatch error: {str(e)}",
346
+ "key": key,
347
+ "modifiers": modifiers,
348
+ }
349
+
350
+ @staticmethod
351
+ def filter_hidden_elements(chrome_interface: Any) -> Dict[str, Any]:
352
+ """
353
+ Filter hidden elements from HTML using computed styles.
354
+ Does not modify the actual page, returns filtered HTML string.
355
+
356
+ Args:
357
+ chrome_interface: Chrome DevTools Protocol interface
358
+
359
+ Returns:
360
+ Result dictionary with filtered HTML string
361
+ """
362
+ js_code = js_loader.get_filter_hidden_elements_js()
363
+ return JavaScriptExecutor.execute_and_parse_result(chrome_interface, js_code)
10
364
 
11
365
 
12
366
  class JavaScriptLoader:
13
367
  """Loads and processes JavaScript files for browser automation."""
14
368
 
15
369
  def __init__(self):
16
- """Initialize the JavaScript loader with the js directory path."""
17
370
  self.js_dir = Path(__file__).parent / "js"
18
371
  self._js_cache: Dict[str, str] = {}
19
372
 
@@ -30,11 +383,9 @@ class JavaScriptLoader:
30
383
  Raises:
31
384
  FileNotFoundError: If the JavaScript file doesn't exist
32
385
  """
33
- # Ensure .js extension
34
386
  if not filename.endswith(".js"):
35
387
  filename += ".js"
36
388
 
37
- # Check cache first
38
389
  if filename in self._js_cache:
39
390
  return self._js_cache[filename]
40
391
 
@@ -45,95 +396,46 @@ class JavaScriptLoader:
45
396
  with open(file_path, "r", encoding="utf-8") as f:
46
397
  js_code = f.read()
47
398
 
48
- # Cache the loaded code
49
399
  self._js_cache[filename] = js_code
50
400
  return js_code
51
401
 
52
402
  def get_extract_clickable_elements_js(self) -> str:
53
- """Get JavaScript code for extracting clickable elements."""
54
403
  return self.load_js_file("extract_clickable_elements.js")
55
404
 
56
405
  def get_extract_input_elements_js(self) -> str:
57
- """Get JavaScript code for extracting input elements."""
58
406
  return self.load_js_file("extract_input_elements.js")
59
407
 
60
408
  def get_extract_scrollable_elements_js(self) -> str:
61
- """Get JavaScript code for extracting scrollable elements."""
62
409
  return self.load_js_file("extract_scrollable_elements.js")
63
410
 
64
411
  def get_extract_elements_by_text_js(self, text: str) -> str:
65
- """
66
- Get JavaScript code for extracting elements by text.
67
-
68
- Args:
69
- text: Text to search for in elements
70
-
71
- Returns:
72
- JavaScript code with text parameter injected
73
- """
74
- # Load the base function
75
412
  js_code = self.load_js_file("extract_elements_by_text.js")
76
-
77
- # Escape the text for JavaScript
78
413
  escaped_text = text.replace("'", "\\'").replace("\\", "\\\\")
79
-
80
- # Wrap with IIFE and inject the text parameter
81
414
  wrapper = f"""
82
415
  (() => {{
83
416
  const text = `{escaped_text}`;
84
417
  return extractElementsByText(text);
85
418
  }})();
86
419
  """
87
-
88
420
  return js_code + "\n" + wrapper
89
421
 
90
422
  def get_click_element_js(self, xpath: str) -> str:
91
- """
92
- Get JavaScript code for clicking an element.
93
-
94
- Args:
95
- xpath: XPath selector for the element to click
96
-
97
- Returns:
98
- JavaScript code with xpath parameter injected
99
- """
100
423
  js_code = self.load_js_file("click_element.js")
101
-
102
- # Escape the xpath for JavaScript
103
424
  escaped_xpath = xpath.replace("`", "\\`").replace("\\", "\\\\")
104
-
105
- # Wrap with IIFE and inject the xpath parameter
106
425
  wrapper = f"""
107
426
  (() => {{
108
427
  const xpath = `{escaped_xpath}`;
109
428
  return clickElement(xpath);
110
429
  }})();
111
430
  """
112
-
113
431
  return js_code + "\n" + wrapper
114
432
 
115
433
  def get_scroll_page_js(
116
434
  self, direction: str, distance: int, xpath: str = "", element_uuid: str = ""
117
435
  ) -> str:
118
- """
119
- Get JavaScript code for scrolling the page or element.
120
-
121
- Args:
122
- direction: Direction to scroll ('up', 'down', 'left', 'right')
123
- distance: Distance to scroll in pixels
124
- xpath: Optional XPath of specific element to scroll
125
- element_uuid: Optional UUID of the element for identification
126
-
127
- Returns:
128
- JavaScript code with parameters injected
129
- """
130
436
  js_code = self.load_js_file("scroll_page.js")
131
-
132
- # Escape parameters for JavaScript
133
437
  escaped_xpath = xpath.replace("`", "\\`").replace("\\", "\\\\")
134
438
  escaped_uuid = element_uuid.replace("`", "\\`").replace("\\", "\\\\")
135
-
136
- # Wrap with IIFE and inject parameters
137
439
  wrapper = f"""
138
440
  (() => {{
139
441
  const direction = '{direction}';
@@ -143,50 +445,22 @@ class JavaScriptLoader:
143
445
  return scrollPage(direction, distance, xpath, elementUuid);
144
446
  }})();
145
447
  """
146
-
147
448
  return js_code + "\n" + wrapper
148
449
 
149
450
  def get_focus_and_clear_element_js(self, xpath: str) -> str:
150
- """
151
- Get JavaScript code for focusing and clearing an element.
152
-
153
- Args:
154
- xpath: XPath selector for the element
155
-
156
- Returns:
157
- JavaScript code with xpath parameter injected
158
- """
159
451
  js_code = self.load_js_file("focus_and_clear_element.js")
160
-
161
- # Escape the xpath for JavaScript
162
452
  escaped_xpath = xpath.replace("`", "\\`").replace("\\", "\\\\")
163
-
164
- # Wrap with IIFE and inject the xpath parameter
165
453
  wrapper = f"""
166
454
  (() => {{
167
455
  const xpath = `{escaped_xpath}`;
168
456
  return focusAndClearElement(xpath);
169
457
  }})();
170
458
  """
171
-
172
459
  return js_code + "\n" + wrapper
173
460
 
174
461
  def get_trigger_input_events_js(self, xpath: str, value: str) -> str:
175
- """
176
- Get JavaScript code for triggering input events.
177
-
178
- Args:
179
- xpath: XPath selector for the element
180
-
181
- Returns:
182
- JavaScript code with xpath parameter injected
183
- """
184
462
  js_code = self.load_js_file("trigger_input_events.js")
185
-
186
- # Escape the xpath for JavaScript
187
463
  escaped_xpath = xpath.replace("`", "\\`").replace("\\", "\\\\")
188
-
189
- # Wrap with IIFE and inject the xpath parameter
190
464
  wrapper = f"""
191
465
  (() => {{
192
466
  const xpath = `{escaped_xpath}`;
@@ -194,37 +468,60 @@ class JavaScriptLoader:
194
468
  return triggerInputEvents(xpath, value);
195
469
  }})();
196
470
  """
471
+ return js_code + "\n" + wrapper
472
+
473
+ def get_draw_element_boxes_js(self, uuid_xpath_dict: Dict[str, str]) -> str:
474
+ import json
475
+
476
+ js_code = self.load_js_file("draw_element_boxes.js")
477
+ json_str = json.dumps(uuid_xpath_dict)
478
+ wrapper = f"""
479
+ (() => {{
480
+ const uuidXpathMap = {json_str};
481
+ return drawElementBoxes(uuidXpathMap);
482
+ }})();
483
+ """
484
+ return js_code + "\n" + wrapper
197
485
 
486
+ def get_remove_element_boxes_js(self) -> str:
487
+ js_code = self.load_js_file("remove_element_boxes.js")
488
+ wrapper = """
489
+ (() => {
490
+ return removeElementBoxes();
491
+ })();
492
+ """
493
+ return js_code + "\n" + wrapper
494
+
495
+ def get_filter_hidden_elements_js(self) -> str:
496
+ js_code = self.load_js_file("filter_hidden_elements.js")
497
+ wrapper = """
498
+ (() => {
499
+ return filterHiddenElements();
500
+ })();
501
+ """
198
502
  return js_code + "\n" + wrapper
199
503
 
200
504
  def clear_cache(self):
201
- """Clear the JavaScript file cache."""
202
505
  self._js_cache.clear()
203
506
 
204
507
 
205
- # Global instance for convenience
206
508
  js_loader = JavaScriptLoader()
207
509
 
208
- # Key code mapping for common keys
209
510
  key_codes = {
210
- # Arrow Keys
211
511
  "up": 38,
212
512
  "down": 40,
213
513
  "left": 37,
214
514
  "right": 39,
215
- # Navigation Keys
216
515
  "home": 36,
217
516
  "end": 35,
218
517
  "pageup": 33,
219
518
  "pagedown": 34,
220
- # Control Keys
221
519
  "enter": 13,
222
520
  "escape": 27,
223
521
  "tab": 9,
224
522
  "backspace": 8,
225
523
  "delete": 46,
226
524
  "space": 32,
227
- # Function Keys
228
525
  "f1": 112,
229
526
  "f2": 113,
230
527
  "f3": 114,
@@ -237,7 +534,6 @@ key_codes = {
237
534
  "f10": 121,
238
535
  "f11": 122,
239
536
  "f12": 123,
240
- # Numpad
241
537
  "numpad0": 96,
242
538
  "numpad1": 97,
243
539
  "numpad2": 98,
@@ -248,18 +544,15 @@ key_codes = {
248
544
  "numpad7": 103,
249
545
  "numpad8": 104,
250
546
  "numpad9": 105,
251
- # Media Keys
252
547
  "volumeup": 175,
253
548
  "volume_up": 175,
254
549
  "volumedown": 174,
255
550
  "volume_down": 174,
256
551
  "volumemute": 173,
257
552
  "volume_mute": 173,
258
- # Lock Keys
259
553
  "capslock": 20,
260
554
  "numlock": 144,
261
555
  "scrolllock": 145,
262
- # Modifier Keys (for key events, not just modifiers)
263
556
  "shift": 16,
264
557
  "ctrl": 17,
265
558
  "control": 17,