camel-ai 0.2.72a8__py3-none-any.whl → 0.2.73__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 +140 -345
- camel/memories/agent_memories.py +18 -17
- camel/societies/__init__.py +2 -0
- camel/societies/workforce/prompts.py +36 -10
- camel/societies/workforce/single_agent_worker.py +7 -5
- camel/societies/workforce/workforce.py +6 -4
- camel/storages/key_value_storages/mem0_cloud.py +48 -47
- camel/storages/vectordb_storages/__init__.py +1 -0
- camel/storages/vectordb_storages/surreal.py +100 -150
- camel/toolkits/__init__.py +6 -1
- camel/toolkits/base.py +60 -2
- camel/toolkits/excel_toolkit.py +153 -64
- camel/toolkits/file_write_toolkit.py +67 -0
- camel/toolkits/hybrid_browser_toolkit/config_loader.py +136 -413
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +131 -1966
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +1177 -0
- camel/toolkits/hybrid_browser_toolkit/ts/package-lock.json +4356 -0
- camel/toolkits/hybrid_browser_toolkit/ts/package.json +33 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/browser-scripts.js +125 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +945 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +226 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +522 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/index.ts +7 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +110 -0
- camel/toolkits/hybrid_browser_toolkit/ts/tsconfig.json +26 -0
- camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +254 -0
- camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +582 -0
- camel/toolkits/hybrid_browser_toolkit_py/__init__.py +17 -0
- camel/toolkits/hybrid_browser_toolkit_py/config_loader.py +447 -0
- camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +2077 -0
- camel/toolkits/mcp_toolkit.py +341 -46
- camel/toolkits/message_integration.py +719 -0
- camel/toolkits/note_taking_toolkit.py +18 -29
- camel/toolkits/notion_mcp_toolkit.py +234 -0
- camel/toolkits/screenshot_toolkit.py +116 -31
- camel/toolkits/search_toolkit.py +20 -2
- camel/toolkits/slack_toolkit.py +43 -48
- camel/toolkits/terminal_toolkit.py +288 -46
- camel/toolkits/video_analysis_toolkit.py +13 -13
- camel/toolkits/video_download_toolkit.py +11 -11
- camel/toolkits/web_deploy_toolkit.py +207 -12
- camel/types/enums.py +6 -0
- {camel_ai-0.2.72a8.dist-info → camel_ai-0.2.73.dist-info}/METADATA +49 -9
- {camel_ai-0.2.72a8.dist-info → camel_ai-0.2.73.dist-info}/RECORD +53 -36
- /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/actions.py +0 -0
- /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/agent.py +0 -0
- /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/browser_session.py +0 -0
- /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/snapshot.py +0 -0
- /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/stealth_script.js +0 -0
- /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/unified_analyzer.js +0 -0
- {camel_ai-0.2.72a8.dist-info → camel_ai-0.2.73.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.72a8.dist-info → camel_ai-0.2.73.dist-info}/licenses/LICENSE +0 -0
camel/toolkits/mcp_toolkit.py
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
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
|
|
|
@@ -24,6 +25,11 @@ from camel.utils.mcp_client import MCPClient, create_mcp_client
|
|
|
24
25
|
|
|
25
26
|
logger = get_logger(__name__)
|
|
26
27
|
|
|
28
|
+
# Suppress parameter description warnings for MCP tools
|
|
29
|
+
warnings.filterwarnings(
|
|
30
|
+
"ignore", message="Parameter description is missing", category=UserWarning
|
|
31
|
+
)
|
|
32
|
+
|
|
27
33
|
|
|
28
34
|
class MCPConnectionError(Exception):
|
|
29
35
|
r"""Raised when MCP connection fails."""
|
|
@@ -446,7 +452,7 @@ class MCPToolkit(BaseToolkit):
|
|
|
446
452
|
|
|
447
453
|
def _ensure_strict_tool_schema(self, tool: FunctionTool) -> FunctionTool:
|
|
448
454
|
r"""Ensure a tool has a strict schema compatible with OpenAI's
|
|
449
|
-
requirements.
|
|
455
|
+
requirements according to the structured outputs specification.
|
|
450
456
|
|
|
451
457
|
Args:
|
|
452
458
|
tool (FunctionTool): The tool to check and update if necessary.
|
|
@@ -457,60 +463,333 @@ class MCPToolkit(BaseToolkit):
|
|
|
457
463
|
try:
|
|
458
464
|
schema = tool.get_openai_tool_schema()
|
|
459
465
|
|
|
460
|
-
#
|
|
461
|
-
def
|
|
462
|
-
r"""Recursively
|
|
466
|
+
# Helper functions for validation and transformation
|
|
467
|
+
def _validate_and_fix_schema(obj, path="", in_root=True):
|
|
468
|
+
r"""Recursively validate and fix schema to meet strict
|
|
469
|
+
requirements.
|
|
470
|
+
"""
|
|
463
471
|
if isinstance(obj, dict):
|
|
464
|
-
if
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
if
|
|
468
|
-
|
|
472
|
+
# Check if this is the root object
|
|
473
|
+
if in_root and path == "":
|
|
474
|
+
# Root must be an object, not anyOf
|
|
475
|
+
if "anyOf" in obj and "type" not in obj:
|
|
476
|
+
raise ValueError(
|
|
477
|
+
"Root object must not be anyOf and must "
|
|
478
|
+
"be an object"
|
|
479
|
+
)
|
|
480
|
+
if obj.get("type") and obj["type"] != "object":
|
|
481
|
+
raise ValueError(
|
|
482
|
+
"Root object must have type 'object'"
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
# Handle object types
|
|
486
|
+
if obj.get("type") == "object":
|
|
487
|
+
# Ensure additionalProperties is false
|
|
488
|
+
obj["additionalProperties"] = False
|
|
489
|
+
|
|
490
|
+
# Process properties
|
|
491
|
+
if "properties" in obj:
|
|
492
|
+
props = obj["properties"]
|
|
493
|
+
# Only set required if it doesn't exist or needs
|
|
494
|
+
# updating
|
|
495
|
+
if "required" not in obj:
|
|
496
|
+
# If no required field exists, make all fields
|
|
497
|
+
# required
|
|
498
|
+
obj["required"] = list(props.keys())
|
|
499
|
+
else:
|
|
500
|
+
# Ensure required field only contains valid
|
|
501
|
+
# property names
|
|
502
|
+
existing_required = obj.get("required", [])
|
|
503
|
+
valid_required = [
|
|
504
|
+
req
|
|
505
|
+
for req in existing_required
|
|
506
|
+
if req in props
|
|
507
|
+
]
|
|
508
|
+
# Add any missing properties to required
|
|
509
|
+
for prop_name in props:
|
|
510
|
+
if prop_name not in valid_required:
|
|
511
|
+
valid_required.append(prop_name)
|
|
512
|
+
obj["required"] = valid_required
|
|
513
|
+
|
|
514
|
+
# Recursively process each property
|
|
515
|
+
for prop_name, prop_schema in props.items():
|
|
516
|
+
_validate_and_fix_schema(
|
|
517
|
+
prop_schema, f"{path}.{prop_name}", False
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
# Handle arrays
|
|
521
|
+
elif obj.get("type") == "array":
|
|
522
|
+
if "items" in obj:
|
|
523
|
+
_validate_and_fix_schema(
|
|
524
|
+
obj["items"], f"{path}.items", False
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
# Handle anyOf
|
|
528
|
+
elif "anyOf" in obj:
|
|
529
|
+
# Validate anyOf schemas
|
|
530
|
+
for i, schema in enumerate(obj["anyOf"]):
|
|
531
|
+
_validate_and_fix_schema(
|
|
532
|
+
schema, f"{path}.anyOf[{i}]", False
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
# Handle string format validation
|
|
536
|
+
elif obj.get("type") == "string":
|
|
537
|
+
if "format" in obj:
|
|
538
|
+
allowed_formats = [
|
|
539
|
+
"date-time",
|
|
540
|
+
"time",
|
|
541
|
+
"date",
|
|
542
|
+
"duration",
|
|
543
|
+
"email",
|
|
544
|
+
"hostname",
|
|
545
|
+
"ipv4",
|
|
546
|
+
"ipv6",
|
|
547
|
+
"uuid",
|
|
548
|
+
]
|
|
549
|
+
if obj["format"] not in allowed_formats:
|
|
550
|
+
del obj["format"] # Remove unsupported format
|
|
551
|
+
|
|
552
|
+
# Handle number/integer validation
|
|
553
|
+
elif obj.get("type") in ["number", "integer"]:
|
|
554
|
+
# These properties are supported
|
|
555
|
+
supported_props = [
|
|
556
|
+
"multipleOf",
|
|
557
|
+
"maximum",
|
|
558
|
+
"exclusiveMaximum",
|
|
559
|
+
"minimum",
|
|
560
|
+
"exclusiveMinimum",
|
|
561
|
+
]
|
|
562
|
+
# Remove any unsupported properties
|
|
563
|
+
for key in list(obj.keys()):
|
|
564
|
+
if key not in [
|
|
565
|
+
*supported_props,
|
|
566
|
+
"type",
|
|
567
|
+
"description",
|
|
568
|
+
"default",
|
|
569
|
+
]:
|
|
570
|
+
del obj[key]
|
|
571
|
+
|
|
572
|
+
# Process nested structures
|
|
573
|
+
for key in ["allOf", "oneOf", "$defs", "definitions"]:
|
|
574
|
+
if key in obj:
|
|
575
|
+
if isinstance(obj[key], list):
|
|
576
|
+
for i, item in enumerate(obj[key]):
|
|
577
|
+
_validate_and_fix_schema(
|
|
578
|
+
item, f"{path}.{key}[{i}]", False
|
|
579
|
+
)
|
|
580
|
+
elif isinstance(obj[key], dict):
|
|
581
|
+
for def_name, def_schema in obj[key].items():
|
|
582
|
+
_validate_and_fix_schema(
|
|
583
|
+
def_schema,
|
|
584
|
+
f"{path}.{key}.{def_name}",
|
|
585
|
+
False,
|
|
586
|
+
)
|
|
587
|
+
|
|
469
588
|
elif isinstance(obj, list):
|
|
470
|
-
for item in obj:
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
589
|
+
for i, item in enumerate(obj):
|
|
590
|
+
_validate_and_fix_schema(item, f"{path}[{i}]", False)
|
|
591
|
+
|
|
592
|
+
def _check_schema_limits(obj, counts=None):
|
|
593
|
+
r"""Check if schema exceeds OpenAI limits."""
|
|
594
|
+
if counts is None:
|
|
595
|
+
counts = {
|
|
596
|
+
"properties": 0,
|
|
597
|
+
"depth": 0,
|
|
598
|
+
"enums": 0,
|
|
599
|
+
"string_length": 0,
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
def _count_properties(o, depth=0):
|
|
603
|
+
if isinstance(o, dict):
|
|
604
|
+
if depth > 5:
|
|
605
|
+
raise ValueError(
|
|
606
|
+
"Schema exceeds maximum nesting depth of 5"
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
if o.get("type") == "object" and "properties" in o:
|
|
610
|
+
counts["properties"] += len(o["properties"])
|
|
611
|
+
for prop in o["properties"].values():
|
|
612
|
+
_count_properties(prop, depth + 1)
|
|
613
|
+
|
|
614
|
+
if "enum" in o:
|
|
615
|
+
counts["enums"] += len(o["enum"])
|
|
616
|
+
if isinstance(o["enum"], list):
|
|
617
|
+
for val in o["enum"]:
|
|
618
|
+
if isinstance(val, str):
|
|
619
|
+
counts["string_length"] += len(val)
|
|
620
|
+
|
|
621
|
+
# Count property names
|
|
622
|
+
if "properties" in o:
|
|
623
|
+
for name in o["properties"].keys():
|
|
624
|
+
counts["string_length"] += len(name)
|
|
625
|
+
|
|
626
|
+
# Process nested structures
|
|
627
|
+
for key in ["items", "allOf", "oneOf", "anyOf"]:
|
|
628
|
+
if key in o:
|
|
629
|
+
if isinstance(o[key], dict):
|
|
630
|
+
_count_properties(o[key], depth)
|
|
631
|
+
elif isinstance(o[key], list):
|
|
632
|
+
for item in o[key]:
|
|
633
|
+
_count_properties(item, depth)
|
|
634
|
+
|
|
635
|
+
_count_properties(obj)
|
|
636
|
+
|
|
637
|
+
# Check limits, reference: https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#objects-have-limitations-on-nesting-depth-and-size # noqa: E501
|
|
638
|
+
if counts["properties"] > 5000:
|
|
639
|
+
raise ValueError(
|
|
640
|
+
"Schema exceeds maximum of 5000 properties"
|
|
641
|
+
)
|
|
642
|
+
if counts["enums"] > 1000:
|
|
643
|
+
raise ValueError(
|
|
644
|
+
"Schema exceeds maximum of 1000 enum values"
|
|
645
|
+
)
|
|
646
|
+
if counts["string_length"] > 120000:
|
|
647
|
+
raise ValueError(
|
|
648
|
+
"Schema exceeds maximum total string length of 120000"
|
|
649
|
+
)
|
|
488
650
|
|
|
489
|
-
|
|
490
|
-
|
|
651
|
+
return True
|
|
652
|
+
|
|
653
|
+
# Check if schema has any issues that prevent strict mode
|
|
654
|
+
def _has_strict_mode_issues(obj):
|
|
655
|
+
r"""Check for any issues that would prevent strict mode."""
|
|
656
|
+
issues = []
|
|
657
|
+
|
|
658
|
+
def _check_issues(o, path=""):
|
|
659
|
+
if isinstance(o, dict):
|
|
660
|
+
# Check for additionalProperties: true
|
|
661
|
+
if o.get("additionalProperties") is True:
|
|
662
|
+
issues.append(
|
|
663
|
+
f"additionalProperties: true at {path}"
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
# Check for unsupported keywords
|
|
667
|
+
unsupported = [
|
|
668
|
+
"not",
|
|
669
|
+
"dependentRequired",
|
|
670
|
+
"dependentSchemas",
|
|
671
|
+
"if",
|
|
672
|
+
"then",
|
|
673
|
+
"else",
|
|
674
|
+
"patternProperties",
|
|
675
|
+
]
|
|
676
|
+
for keyword in unsupported:
|
|
677
|
+
if keyword in o:
|
|
678
|
+
issues.append(
|
|
679
|
+
f"Unsupported keyword '{keyword}' "
|
|
680
|
+
f"at {path}"
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
# Recursively check
|
|
684
|
+
for key, value in o.items():
|
|
685
|
+
if isinstance(value, (dict, list)):
|
|
686
|
+
_check_issues(value, f"{path}.{key}")
|
|
687
|
+
|
|
688
|
+
elif isinstance(o, list):
|
|
689
|
+
for i, item in enumerate(o):
|
|
690
|
+
_check_issues(item, f"{path}[{i}]")
|
|
691
|
+
|
|
692
|
+
_check_issues(obj)
|
|
693
|
+
return issues
|
|
694
|
+
|
|
695
|
+
# Check if already strict and compliant
|
|
491
696
|
if schema.get("function", {}).get("strict") is True:
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
697
|
+
# Validate it's actually compliant
|
|
698
|
+
try:
|
|
699
|
+
params = schema["function"].get("parameters", {})
|
|
700
|
+
if params:
|
|
701
|
+
_validate_and_fix_schema(params)
|
|
702
|
+
_check_schema_limits(params)
|
|
703
|
+
return tool
|
|
704
|
+
except Exception:
|
|
705
|
+
# Not actually compliant, continue to fix it
|
|
706
|
+
pass
|
|
707
|
+
|
|
708
|
+
# Apply sanitization first to handle optional fields properly
|
|
495
709
|
if "function" in schema:
|
|
710
|
+
# Apply the sanitization function first
|
|
711
|
+
from camel.toolkits.function_tool import (
|
|
712
|
+
sanitize_and_enforce_required,
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
schema = sanitize_and_enforce_required(schema)
|
|
716
|
+
|
|
717
|
+
# Special handling for schemas with additionalProperties that
|
|
718
|
+
# aren't false These can't use strict mode
|
|
719
|
+
def _has_open_props(obj, path=""):
|
|
720
|
+
"""Check if any object has additionalProperties that
|
|
721
|
+
isn't false."""
|
|
722
|
+
if isinstance(obj, dict):
|
|
723
|
+
if (
|
|
724
|
+
obj.get("type") == "object"
|
|
725
|
+
and "additionalProperties" in obj
|
|
726
|
+
):
|
|
727
|
+
if obj["additionalProperties"] is not False:
|
|
728
|
+
return True
|
|
729
|
+
|
|
730
|
+
# Recurse through the schema
|
|
731
|
+
for key, value in obj.items():
|
|
732
|
+
if key in [
|
|
733
|
+
"properties",
|
|
734
|
+
"items",
|
|
735
|
+
"allOf",
|
|
736
|
+
"oneOf",
|
|
737
|
+
"anyOf",
|
|
738
|
+
]:
|
|
739
|
+
if isinstance(value, dict):
|
|
740
|
+
if _has_open_props(value, f"{path}.{key}"):
|
|
741
|
+
return True
|
|
742
|
+
elif isinstance(value, list):
|
|
743
|
+
for i, item in enumerate(value):
|
|
744
|
+
if _has_open_props(
|
|
745
|
+
item,
|
|
746
|
+
f"{path}.{key}[{i}]",
|
|
747
|
+
):
|
|
748
|
+
return True
|
|
749
|
+
elif isinstance(value, dict) and key not in [
|
|
750
|
+
"description",
|
|
751
|
+
"type",
|
|
752
|
+
"enum",
|
|
753
|
+
]:
|
|
754
|
+
if _has_open_props(value, f"{path}.{key}"):
|
|
755
|
+
return True
|
|
756
|
+
return False
|
|
757
|
+
|
|
758
|
+
# Check if schema has dynamic additionalProperties
|
|
759
|
+
if _has_open_props(schema["function"].get("parameters", {})):
|
|
760
|
+
# Can't use strict mode with dynamic additionalProperties
|
|
761
|
+
schema["function"]["strict"] = False
|
|
762
|
+
tool.set_openai_tool_schema(schema)
|
|
763
|
+
logger.warning(
|
|
764
|
+
f"Tool '{tool.get_function_name()}' has "
|
|
765
|
+
f"dynamic additionalProperties and cannot use "
|
|
766
|
+
f"strict mode"
|
|
767
|
+
)
|
|
768
|
+
return tool
|
|
769
|
+
|
|
770
|
+
# Now check for blocking issues after sanitization
|
|
771
|
+
issues = _has_strict_mode_issues(schema)
|
|
772
|
+
if issues:
|
|
773
|
+
# Can't use strict mode
|
|
774
|
+
schema["function"]["strict"] = False
|
|
775
|
+
tool.set_openai_tool_schema(schema)
|
|
776
|
+
logger.warning(
|
|
777
|
+
f"Tool '{tool.get_function_name()}' has "
|
|
778
|
+
f"issues preventing strict mode: "
|
|
779
|
+
f"{'; '.join(issues[:3])}{'...' if len(issues) > 3 else ''}" # noqa: E501
|
|
780
|
+
)
|
|
781
|
+
return tool
|
|
782
|
+
|
|
783
|
+
# Enable strict mode
|
|
496
784
|
schema["function"]["strict"] = True
|
|
497
785
|
|
|
498
|
-
# Ensure parameters have proper strict mode configuration
|
|
499
786
|
parameters = schema["function"].get("parameters", {})
|
|
500
787
|
if parameters:
|
|
501
|
-
#
|
|
502
|
-
parameters
|
|
503
|
-
|
|
504
|
-
# Process properties to handle optional fields
|
|
505
|
-
properties = parameters.get("properties", {})
|
|
506
|
-
parameters["required"] = list(properties.keys())
|
|
507
|
-
|
|
508
|
-
# Apply the sanitization function from function_tool
|
|
509
|
-
from camel.toolkits.function_tool import (
|
|
510
|
-
sanitize_and_enforce_required,
|
|
511
|
-
)
|
|
788
|
+
# Validate and fix the parameters schema
|
|
789
|
+
_validate_and_fix_schema(parameters)
|
|
512
790
|
|
|
513
|
-
|
|
791
|
+
# Check schema limits
|
|
792
|
+
_check_schema_limits(parameters)
|
|
514
793
|
|
|
515
794
|
tool.set_openai_tool_schema(schema)
|
|
516
795
|
logger.debug(
|
|
@@ -518,7 +797,23 @@ class MCPToolkit(BaseToolkit):
|
|
|
518
797
|
)
|
|
519
798
|
|
|
520
799
|
except Exception as e:
|
|
521
|
-
|
|
800
|
+
# If we can't make it strict, disable strict mode
|
|
801
|
+
try:
|
|
802
|
+
if "function" in schema:
|
|
803
|
+
schema["function"]["strict"] = False
|
|
804
|
+
tool.set_openai_tool_schema(schema)
|
|
805
|
+
logger.warning(
|
|
806
|
+
f"Failed to ensure strict schema for "
|
|
807
|
+
f"tool '{tool.get_function_name()}': {str(e)[:100]}. "
|
|
808
|
+
f"Setting strict=False."
|
|
809
|
+
)
|
|
810
|
+
except Exception as inner_e:
|
|
811
|
+
# If even setting strict=False fails, log the error
|
|
812
|
+
logger.error(
|
|
813
|
+
f"Critical error processing "
|
|
814
|
+
f"tool '{tool.get_function_name()}': {inner_e}. "
|
|
815
|
+
f"Tool may not function correctly."
|
|
816
|
+
)
|
|
522
817
|
|
|
523
818
|
return tool
|
|
524
819
|
|