camel-ai 0.2.76a2__py3-none-any.whl → 0.2.76a4__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/societies/workforce/workforce.py +51 -2
- camel/toolkits/mcp_toolkit.py +307 -333
- camel/utils/context_utils.py +52 -0
- {camel_ai-0.2.76a2.dist-info → camel_ai-0.2.76a4.dist-info}/METADATA +1 -1
- {camel_ai-0.2.76a2.dist-info → camel_ai-0.2.76a4.dist-info}/RECORD +8 -8
- {camel_ai-0.2.76a2.dist-info → camel_ai-0.2.76a4.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.76a2.dist-info → camel_ai-0.2.76a4.dist-info}/licenses/LICENSE +0 -0
camel/__init__.py
CHANGED
|
@@ -2075,8 +2075,40 @@ class Workforce(BaseNode):
|
|
|
2075
2075
|
TaskAssignResult: Assignment result containing task assignments
|
|
2076
2076
|
with their dependencies.
|
|
2077
2077
|
"""
|
|
2078
|
+
# Wait for workers to be ready before assignment with exponential
|
|
2079
|
+
# backoff
|
|
2080
|
+
worker_readiness_timeout = 2.0 # Maximum wait time in seconds
|
|
2081
|
+
worker_readiness_check_interval = 0.05 # Initial check interval
|
|
2082
|
+
start_time = time.time()
|
|
2083
|
+
check_interval = worker_readiness_check_interval
|
|
2084
|
+
backoff_multiplier = 1.5 # Exponential backoff factor
|
|
2085
|
+
max_interval = 0.5 # Cap the maximum interval
|
|
2086
|
+
|
|
2087
|
+
while (time.time() - start_time) < worker_readiness_timeout:
|
|
2088
|
+
valid_worker_ids = self._get_valid_worker_ids()
|
|
2089
|
+
if len(valid_worker_ids) > 0:
|
|
2090
|
+
elapsed = time.time() - start_time
|
|
2091
|
+
logger.debug(
|
|
2092
|
+
f"Workers ready after {elapsed:.3f}s: "
|
|
2093
|
+
f"{len(valid_worker_ids)} workers available"
|
|
2094
|
+
)
|
|
2095
|
+
break
|
|
2096
|
+
|
|
2097
|
+
await asyncio.sleep(check_interval)
|
|
2098
|
+
# Exponential backoff with cap
|
|
2099
|
+
check_interval = min(
|
|
2100
|
+
check_interval * backoff_multiplier, max_interval
|
|
2101
|
+
)
|
|
2102
|
+
else:
|
|
2103
|
+
# Timeout reached, log warning but continue
|
|
2104
|
+
logger.warning(
|
|
2105
|
+
f"Worker readiness timeout after "
|
|
2106
|
+
f"{worker_readiness_timeout}s, "
|
|
2107
|
+
f"proceeding with {len(self._children)} children"
|
|
2108
|
+
)
|
|
2109
|
+
valid_worker_ids = self._get_valid_worker_ids()
|
|
2110
|
+
|
|
2078
2111
|
self.coordinator_agent.reset()
|
|
2079
|
-
valid_worker_ids = self._get_valid_worker_ids()
|
|
2080
2112
|
|
|
2081
2113
|
logger.debug(
|
|
2082
2114
|
f"Sending batch assignment request to coordinator "
|
|
@@ -2110,7 +2142,24 @@ class Workforce(BaseNode):
|
|
|
2110
2142
|
invalid_assignments, tasks, valid_worker_ids
|
|
2111
2143
|
)
|
|
2112
2144
|
)
|
|
2113
|
-
|
|
2145
|
+
|
|
2146
|
+
# Combine assignments with deduplication, prioritizing retry results
|
|
2147
|
+
assignment_map = {a.task_id: a for a in valid_assignments}
|
|
2148
|
+
assignment_map.update(
|
|
2149
|
+
{a.task_id: a for a in retry_and_fallback_assignments}
|
|
2150
|
+
)
|
|
2151
|
+
all_assignments = list(assignment_map.values())
|
|
2152
|
+
|
|
2153
|
+
# Log any overwrites for debugging
|
|
2154
|
+
valid_task_ids = {a.task_id for a in valid_assignments}
|
|
2155
|
+
retry_task_ids = {a.task_id for a in retry_and_fallback_assignments}
|
|
2156
|
+
overlap_task_ids = valid_task_ids & retry_task_ids
|
|
2157
|
+
|
|
2158
|
+
if overlap_task_ids:
|
|
2159
|
+
logger.warning(
|
|
2160
|
+
f"Retry assignments overrode {len(overlap_task_ids)} "
|
|
2161
|
+
f"valid assignments for tasks: {sorted(overlap_task_ids)}"
|
|
2162
|
+
)
|
|
2114
2163
|
|
|
2115
2164
|
# Update Task.dependencies for all final assignments
|
|
2116
2165
|
self._update_task_dependencies_from_assignments(all_assignments, tasks)
|
camel/toolkits/mcp_toolkit.py
CHANGED
|
@@ -18,6 +18,8 @@ import warnings
|
|
|
18
18
|
from contextlib import AsyncExitStack
|
|
19
19
|
from typing import Any, Dict, List, Optional
|
|
20
20
|
|
|
21
|
+
from typing_extensions import TypeGuard
|
|
22
|
+
|
|
21
23
|
from camel.logger import get_logger
|
|
22
24
|
from camel.toolkits import BaseToolkit, FunctionTool
|
|
23
25
|
from camel.utils.commons import run_async
|
|
@@ -43,6 +45,187 @@ class MCPToolError(Exception):
|
|
|
43
45
|
pass
|
|
44
46
|
|
|
45
47
|
|
|
48
|
+
_EMPTY_SCHEMA = {
|
|
49
|
+
"additionalProperties": False,
|
|
50
|
+
"type": "object",
|
|
51
|
+
"properties": {},
|
|
52
|
+
"required": [],
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def ensure_strict_json_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
|
57
|
+
r"""Mutates the given JSON schema to ensure it conforms to the
|
|
58
|
+
`strict` standard that the OpenAI API expects.
|
|
59
|
+
"""
|
|
60
|
+
if schema == {}:
|
|
61
|
+
return _EMPTY_SCHEMA
|
|
62
|
+
return _ensure_strict_json_schema(schema, path=(), root=schema)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _ensure_strict_json_schema(
|
|
66
|
+
json_schema: object,
|
|
67
|
+
*,
|
|
68
|
+
path: tuple[str, ...],
|
|
69
|
+
root: dict[str, object],
|
|
70
|
+
) -> dict[str, Any]:
|
|
71
|
+
if not is_dict(json_schema):
|
|
72
|
+
raise TypeError(
|
|
73
|
+
f"Expected {json_schema} to be a dictionary; path={path}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
defs = json_schema.get("$defs")
|
|
77
|
+
if is_dict(defs):
|
|
78
|
+
for def_name, def_schema in defs.items():
|
|
79
|
+
_ensure_strict_json_schema(
|
|
80
|
+
def_schema, path=(*path, "$defs", def_name), root=root
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
definitions = json_schema.get("definitions")
|
|
84
|
+
if is_dict(definitions):
|
|
85
|
+
for definition_name, definition_schema in definitions.items():
|
|
86
|
+
_ensure_strict_json_schema(
|
|
87
|
+
definition_schema,
|
|
88
|
+
path=(*path, "definitions", definition_name),
|
|
89
|
+
root=root,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
typ = json_schema.get("type")
|
|
93
|
+
if typ == "object" and "additionalProperties" not in json_schema:
|
|
94
|
+
json_schema["additionalProperties"] = False
|
|
95
|
+
elif (
|
|
96
|
+
typ == "object"
|
|
97
|
+
and "additionalProperties" in json_schema
|
|
98
|
+
and json_schema["additionalProperties"]
|
|
99
|
+
):
|
|
100
|
+
raise ValueError(
|
|
101
|
+
"additionalProperties should not be set for object types. This "
|
|
102
|
+
"could be because you're using an older version of Pydantic, or "
|
|
103
|
+
"because you configured additional properties to be allowed. If "
|
|
104
|
+
"you really need this, update the function or output tool "
|
|
105
|
+
"to not use a strict schema."
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# object types
|
|
109
|
+
# { 'type': 'object', 'properties': { 'a': {...} } }
|
|
110
|
+
properties = json_schema.get("properties")
|
|
111
|
+
if is_dict(properties):
|
|
112
|
+
json_schema["required"] = list(properties.keys())
|
|
113
|
+
json_schema["properties"] = {
|
|
114
|
+
key: _ensure_strict_json_schema(
|
|
115
|
+
prop_schema, path=(*path, "properties", key), root=root
|
|
116
|
+
)
|
|
117
|
+
for key, prop_schema in properties.items()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# arrays
|
|
121
|
+
# { 'type': 'array', 'items': {...} }
|
|
122
|
+
items = json_schema.get("items")
|
|
123
|
+
if is_dict(items):
|
|
124
|
+
json_schema["items"] = _ensure_strict_json_schema(
|
|
125
|
+
items, path=(*path, "items"), root=root
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# unions
|
|
129
|
+
any_of = json_schema.get("anyOf")
|
|
130
|
+
if is_list(any_of):
|
|
131
|
+
json_schema["anyOf"] = [
|
|
132
|
+
_ensure_strict_json_schema(
|
|
133
|
+
variant, path=(*path, "anyOf", str(i)), root=root
|
|
134
|
+
)
|
|
135
|
+
for i, variant in enumerate(any_of)
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
# intersections
|
|
139
|
+
all_of = json_schema.get("allOf")
|
|
140
|
+
if is_list(all_of):
|
|
141
|
+
if len(all_of) == 1:
|
|
142
|
+
json_schema.update(
|
|
143
|
+
_ensure_strict_json_schema(
|
|
144
|
+
all_of[0], path=(*path, "allOf", "0"), root=root
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
json_schema.pop("allOf")
|
|
148
|
+
else:
|
|
149
|
+
json_schema["allOf"] = [
|
|
150
|
+
_ensure_strict_json_schema(
|
|
151
|
+
entry, path=(*path, "allOf", str(i)), root=root
|
|
152
|
+
)
|
|
153
|
+
for i, entry in enumerate(all_of)
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
# strip `None` defaults as there's no meaningful distinction here
|
|
157
|
+
# the schema will still be `nullable` and the model will default
|
|
158
|
+
# to using `None` anyway
|
|
159
|
+
if json_schema.get("default", None) is None:
|
|
160
|
+
json_schema.pop("default", None)
|
|
161
|
+
|
|
162
|
+
# we can't use `$ref`s if there are also other properties defined, e.g.
|
|
163
|
+
# `{"$ref": "...", "description": "my description"}`
|
|
164
|
+
#
|
|
165
|
+
# so we unravel the ref
|
|
166
|
+
# `{"type": "string", "description": "my description"}`
|
|
167
|
+
ref = json_schema.get("$ref")
|
|
168
|
+
if ref and has_more_than_n_keys(json_schema, 1):
|
|
169
|
+
assert isinstance(ref, str), f"Received non-string $ref - {ref}"
|
|
170
|
+
|
|
171
|
+
resolved = resolve_ref(root=root, ref=ref)
|
|
172
|
+
if not is_dict(resolved):
|
|
173
|
+
raise ValueError(
|
|
174
|
+
f"Expected `$ref: {ref}` to resolved to a dictionary but got "
|
|
175
|
+
f"{resolved}"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# properties from the json schema take priority
|
|
179
|
+
# over the ones on the `$ref`
|
|
180
|
+
json_schema.update({**resolved, **json_schema})
|
|
181
|
+
json_schema.pop("$ref")
|
|
182
|
+
# Since the schema expanded from `$ref` might not
|
|
183
|
+
# have `additionalProperties: false` applied
|
|
184
|
+
# we call `_ensure_strict_json_schema` again to fix the inlined
|
|
185
|
+
# schema and ensure it's valid
|
|
186
|
+
return _ensure_strict_json_schema(json_schema, path=path, root=root)
|
|
187
|
+
|
|
188
|
+
return json_schema
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def resolve_ref(*, root: dict[str, object], ref: str) -> object:
|
|
192
|
+
if not ref.startswith("#/"):
|
|
193
|
+
raise ValueError(
|
|
194
|
+
f"Unexpected $ref format {ref!r}; Does not start with #/"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
path = ref[2:].split("/")
|
|
198
|
+
resolved = root
|
|
199
|
+
for key in path:
|
|
200
|
+
value = resolved[key]
|
|
201
|
+
assert is_dict(value), (
|
|
202
|
+
f"encountered non-dictionary entry while resolving {ref} - "
|
|
203
|
+
f"{resolved}"
|
|
204
|
+
)
|
|
205
|
+
resolved = value
|
|
206
|
+
|
|
207
|
+
return resolved
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def is_dict(obj: object) -> TypeGuard[dict[str, object]]:
|
|
211
|
+
# just pretend that we know there are only `str` keys
|
|
212
|
+
# as that check is not worth the performance cost
|
|
213
|
+
return isinstance(obj, dict)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def is_list(obj: object) -> TypeGuard[list[object]]:
|
|
217
|
+
return isinstance(obj, list)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def has_more_than_n_keys(obj: dict[str, object], n: int) -> bool:
|
|
221
|
+
i = 0
|
|
222
|
+
for _ in obj.keys():
|
|
223
|
+
i += 1
|
|
224
|
+
if i > n:
|
|
225
|
+
return True
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
|
|
46
229
|
class MCPToolkit(BaseToolkit):
|
|
47
230
|
r"""MCPToolkit provides a unified interface for managing multiple
|
|
48
231
|
MCP server connections and their tools.
|
|
@@ -476,367 +659,149 @@ class MCPToolkit(BaseToolkit):
|
|
|
476
659
|
raise ValueError(error_msg) from e
|
|
477
660
|
|
|
478
661
|
def _ensure_strict_tool_schema(self, tool: FunctionTool) -> FunctionTool:
|
|
479
|
-
r"""Ensure a tool has a strict schema compatible with
|
|
480
|
-
requirements
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
662
|
+
r"""Ensure a tool has a strict schema compatible with
|
|
663
|
+
OpenAI's requirements.
|
|
664
|
+
|
|
665
|
+
Strategy:
|
|
666
|
+
- Ensure parameters exist with at least an empty properties object
|
|
667
|
+
(OpenAI requirement).
|
|
668
|
+
- Try converting parameters to strict using ensure_strict_json_schema.
|
|
669
|
+
- If conversion fails, mark function.strict = False and
|
|
670
|
+
keep best-effort parameters.
|
|
487
671
|
"""
|
|
488
672
|
try:
|
|
489
673
|
schema = tool.get_openai_tool_schema()
|
|
490
674
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
requirements.
|
|
495
|
-
"""
|
|
496
|
-
if isinstance(obj, dict):
|
|
497
|
-
# Check if this is the root object
|
|
498
|
-
if in_root and path == "":
|
|
499
|
-
# Root must be an object, not anyOf
|
|
500
|
-
if "anyOf" in obj and "type" not in obj:
|
|
501
|
-
raise ValueError(
|
|
502
|
-
"Root object must not be anyOf and must "
|
|
503
|
-
"be an object"
|
|
504
|
-
)
|
|
505
|
-
if obj.get("type") and obj["type"] != "object":
|
|
506
|
-
raise ValueError(
|
|
507
|
-
"Root object must have type 'object'"
|
|
508
|
-
)
|
|
509
|
-
|
|
510
|
-
# Handle object types
|
|
511
|
-
if obj.get("type") == "object":
|
|
512
|
-
# Ensure additionalProperties is false
|
|
513
|
-
obj["additionalProperties"] = False
|
|
514
|
-
|
|
515
|
-
# Process properties
|
|
516
|
-
if "properties" in obj:
|
|
517
|
-
props = obj["properties"]
|
|
518
|
-
# Only set required if it doesn't exist or needs
|
|
519
|
-
# updating
|
|
520
|
-
if "required" not in obj:
|
|
521
|
-
# If no required field exists, make all fields
|
|
522
|
-
# required
|
|
523
|
-
obj["required"] = list(props.keys())
|
|
524
|
-
else:
|
|
525
|
-
# Ensure required field only contains valid
|
|
526
|
-
# property names
|
|
527
|
-
existing_required = obj.get("required", [])
|
|
528
|
-
valid_required = [
|
|
529
|
-
req
|
|
530
|
-
for req in existing_required
|
|
531
|
-
if req in props
|
|
532
|
-
]
|
|
533
|
-
# Add any missing properties to required
|
|
534
|
-
for prop_name in props:
|
|
535
|
-
if prop_name not in valid_required:
|
|
536
|
-
valid_required.append(prop_name)
|
|
537
|
-
obj["required"] = valid_required
|
|
538
|
-
|
|
539
|
-
# Recursively process each property
|
|
540
|
-
for prop_name, prop_schema in props.items():
|
|
541
|
-
_validate_and_fix_schema(
|
|
542
|
-
prop_schema, f"{path}.{prop_name}", False
|
|
543
|
-
)
|
|
544
|
-
|
|
545
|
-
# Handle arrays
|
|
546
|
-
elif obj.get("type") == "array":
|
|
547
|
-
if "items" in obj:
|
|
548
|
-
_validate_and_fix_schema(
|
|
549
|
-
obj["items"], f"{path}.items", False
|
|
550
|
-
)
|
|
675
|
+
def _has_strict_mode_incompatible_features(json_schema):
|
|
676
|
+
r"""Check if schema has features incompatible
|
|
677
|
+
with OpenAI strict mode."""
|
|
551
678
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
for i, schema in enumerate(obj["anyOf"]):
|
|
556
|
-
_validate_and_fix_schema(
|
|
557
|
-
schema, f"{path}.anyOf[{i}]", False
|
|
558
|
-
)
|
|
559
|
-
|
|
560
|
-
# Handle string format validation
|
|
561
|
-
elif obj.get("type") == "string":
|
|
562
|
-
if "format" in obj:
|
|
563
|
-
allowed_formats = [
|
|
564
|
-
"date-time",
|
|
565
|
-
"time",
|
|
566
|
-
"date",
|
|
567
|
-
"duration",
|
|
568
|
-
"email",
|
|
569
|
-
"hostname",
|
|
570
|
-
"ipv4",
|
|
571
|
-
"ipv6",
|
|
572
|
-
"uuid",
|
|
573
|
-
]
|
|
574
|
-
if obj["format"] not in allowed_formats:
|
|
575
|
-
del obj["format"] # Remove unsupported format
|
|
576
|
-
|
|
577
|
-
# Handle number/integer validation
|
|
578
|
-
elif obj.get("type") in ["number", "integer"]:
|
|
579
|
-
# These properties are supported
|
|
580
|
-
supported_props = [
|
|
581
|
-
"multipleOf",
|
|
582
|
-
"maximum",
|
|
583
|
-
"exclusiveMaximum",
|
|
584
|
-
"minimum",
|
|
585
|
-
"exclusiveMinimum",
|
|
586
|
-
]
|
|
587
|
-
# Remove any unsupported properties
|
|
588
|
-
for key in list(obj.keys()):
|
|
589
|
-
if key not in [
|
|
590
|
-
*supported_props,
|
|
591
|
-
"type",
|
|
592
|
-
"description",
|
|
593
|
-
"default",
|
|
594
|
-
]:
|
|
595
|
-
del obj[key]
|
|
596
|
-
|
|
597
|
-
# Process nested structures
|
|
598
|
-
for key in ["allOf", "oneOf", "$defs", "definitions"]:
|
|
599
|
-
if key in obj:
|
|
600
|
-
if isinstance(obj[key], list):
|
|
601
|
-
for i, item in enumerate(obj[key]):
|
|
602
|
-
_validate_and_fix_schema(
|
|
603
|
-
item, f"{path}.{key}[{i}]", False
|
|
604
|
-
)
|
|
605
|
-
elif isinstance(obj[key], dict):
|
|
606
|
-
for def_name, def_schema in obj[key].items():
|
|
607
|
-
_validate_and_fix_schema(
|
|
608
|
-
def_schema,
|
|
609
|
-
f"{path}.{key}.{def_name}",
|
|
610
|
-
False,
|
|
611
|
-
)
|
|
612
|
-
|
|
613
|
-
elif isinstance(obj, list):
|
|
614
|
-
for i, item in enumerate(obj):
|
|
615
|
-
_validate_and_fix_schema(item, f"{path}[{i}]", False)
|
|
616
|
-
|
|
617
|
-
def _check_schema_limits(obj, counts=None):
|
|
618
|
-
r"""Check if schema exceeds OpenAI limits."""
|
|
619
|
-
if counts is None:
|
|
620
|
-
counts = {
|
|
621
|
-
"properties": 0,
|
|
622
|
-
"depth": 0,
|
|
623
|
-
"enums": 0,
|
|
624
|
-
"string_length": 0,
|
|
625
|
-
}
|
|
679
|
+
def _check_incompatible(obj, path=""):
|
|
680
|
+
if not isinstance(obj, dict):
|
|
681
|
+
return False
|
|
626
682
|
|
|
627
|
-
|
|
628
|
-
if isinstance(
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
683
|
+
# Check for allOf in array items (known to cause issues)
|
|
684
|
+
if "items" in obj and isinstance(obj["items"], dict):
|
|
685
|
+
items_schema = obj["items"]
|
|
686
|
+
if "allOf" in items_schema:
|
|
687
|
+
logger.debug(
|
|
688
|
+
f"Found allOf in array items at {path}"
|
|
632
689
|
)
|
|
690
|
+
return True
|
|
691
|
+
# Recursively check items schema
|
|
692
|
+
if _check_incompatible(items_schema, f"{path}.items"):
|
|
693
|
+
return True
|
|
694
|
+
|
|
695
|
+
# Check for other potentially problematic patterns
|
|
696
|
+
# anyOf/oneOf in certain contexts can also cause issues
|
|
697
|
+
if (
|
|
698
|
+
"anyOf" in obj and len(obj["anyOf"]) > 10
|
|
699
|
+
): # Large unions can be problematic
|
|
700
|
+
return True
|
|
701
|
+
|
|
702
|
+
# Recursively check nested objects
|
|
703
|
+
for key in [
|
|
704
|
+
"properties",
|
|
705
|
+
"additionalProperties",
|
|
706
|
+
"patternProperties",
|
|
707
|
+
]:
|
|
708
|
+
if key in obj and isinstance(obj[key], dict):
|
|
709
|
+
if key == "properties":
|
|
710
|
+
for prop_name, prop_schema in obj[key].items():
|
|
711
|
+
if isinstance(
|
|
712
|
+
prop_schema, dict
|
|
713
|
+
) and _check_incompatible(
|
|
714
|
+
prop_schema,
|
|
715
|
+
f"{path}.{key}.{prop_name}",
|
|
716
|
+
):
|
|
717
|
+
return True
|
|
718
|
+
elif _check_incompatible(
|
|
719
|
+
obj[key], f"{path}.{key}"
|
|
720
|
+
):
|
|
721
|
+
return True
|
|
633
722
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
counts["string_length"] += len(val)
|
|
645
|
-
|
|
646
|
-
# Count property names
|
|
647
|
-
if "properties" in o:
|
|
648
|
-
for name in o["properties"].keys():
|
|
649
|
-
counts["string_length"] += len(name)
|
|
650
|
-
|
|
651
|
-
# Process nested structures
|
|
652
|
-
for key in ["items", "allOf", "oneOf", "anyOf"]:
|
|
653
|
-
if key in o:
|
|
654
|
-
if isinstance(o[key], dict):
|
|
655
|
-
_count_properties(o[key], depth)
|
|
656
|
-
elif isinstance(o[key], list):
|
|
657
|
-
for item in o[key]:
|
|
658
|
-
_count_properties(item, depth)
|
|
659
|
-
|
|
660
|
-
_count_properties(obj)
|
|
661
|
-
|
|
662
|
-
# Check limits, reference: https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#objects-have-limitations-on-nesting-depth-and-size # noqa: E501
|
|
663
|
-
if counts["properties"] > 5000:
|
|
664
|
-
raise ValueError(
|
|
665
|
-
"Schema exceeds maximum of 5000 properties"
|
|
666
|
-
)
|
|
667
|
-
if counts["enums"] > 1000:
|
|
668
|
-
raise ValueError(
|
|
669
|
-
"Schema exceeds maximum of 1000 enum values"
|
|
670
|
-
)
|
|
671
|
-
if counts["string_length"] > 120000:
|
|
672
|
-
raise ValueError(
|
|
673
|
-
"Schema exceeds maximum total string length of 120000"
|
|
674
|
-
)
|
|
675
|
-
|
|
676
|
-
return True
|
|
723
|
+
# Check arrays and unions
|
|
724
|
+
for key in ["allOf", "anyOf", "oneOf"]:
|
|
725
|
+
if key in obj and isinstance(obj[key], list):
|
|
726
|
+
for i, item in enumerate(obj[key]):
|
|
727
|
+
if isinstance(
|
|
728
|
+
item, dict
|
|
729
|
+
) and _check_incompatible(
|
|
730
|
+
item, f"{path}.{key}[{i}]"
|
|
731
|
+
):
|
|
732
|
+
return True
|
|
677
733
|
|
|
678
|
-
|
|
679
|
-
def _has_strict_mode_issues(obj):
|
|
680
|
-
r"""Check for any issues that would prevent strict mode."""
|
|
681
|
-
issues = []
|
|
734
|
+
return False
|
|
682
735
|
|
|
683
|
-
|
|
684
|
-
if isinstance(o, dict):
|
|
685
|
-
# Check for additionalProperties: true
|
|
686
|
-
if o.get("additionalProperties") is True:
|
|
687
|
-
issues.append(
|
|
688
|
-
f"additionalProperties: true at {path}"
|
|
689
|
-
)
|
|
736
|
+
return _check_incompatible(json_schema)
|
|
690
737
|
|
|
691
|
-
|
|
692
|
-
unsupported = [
|
|
693
|
-
"not",
|
|
694
|
-
"dependentRequired",
|
|
695
|
-
"dependentSchemas",
|
|
696
|
-
"if",
|
|
697
|
-
"then",
|
|
698
|
-
"else",
|
|
699
|
-
"patternProperties",
|
|
700
|
-
]
|
|
701
|
-
for keyword in unsupported:
|
|
702
|
-
if keyword in o:
|
|
703
|
-
issues.append(
|
|
704
|
-
f"Unsupported keyword '{keyword}' "
|
|
705
|
-
f"at {path}"
|
|
706
|
-
)
|
|
707
|
-
|
|
708
|
-
# Recursively check
|
|
709
|
-
for key, value in o.items():
|
|
710
|
-
if isinstance(value, (dict, list)):
|
|
711
|
-
_check_issues(value, f"{path}.{key}")
|
|
712
|
-
|
|
713
|
-
elif isinstance(o, list):
|
|
714
|
-
for i, item in enumerate(o):
|
|
715
|
-
_check_issues(item, f"{path}[{i}]")
|
|
716
|
-
|
|
717
|
-
_check_issues(obj)
|
|
718
|
-
return issues
|
|
719
|
-
|
|
720
|
-
# Check if already strict and compliant
|
|
721
|
-
if schema.get("function", {}).get("strict") is True:
|
|
722
|
-
# Validate it's actually compliant
|
|
723
|
-
try:
|
|
724
|
-
params = schema["function"].get("parameters", {})
|
|
725
|
-
if params:
|
|
726
|
-
_validate_and_fix_schema(params)
|
|
727
|
-
_check_schema_limits(params)
|
|
728
|
-
return tool
|
|
729
|
-
except Exception:
|
|
730
|
-
# Not actually compliant, continue to fix it
|
|
731
|
-
pass
|
|
732
|
-
|
|
733
|
-
# Apply sanitization first to handle optional fields properly
|
|
738
|
+
# Apply sanitization if available
|
|
734
739
|
if "function" in schema:
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
740
|
+
try:
|
|
741
|
+
from camel.toolkits.function_tool import (
|
|
742
|
+
sanitize_and_enforce_required,
|
|
743
|
+
)
|
|
739
744
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
# aren't false These can't use strict mode
|
|
744
|
-
def _has_open_props(obj, path=""):
|
|
745
|
-
"""Check if any object has additionalProperties that
|
|
746
|
-
isn't false."""
|
|
747
|
-
if isinstance(obj, dict):
|
|
748
|
-
if (
|
|
749
|
-
obj.get("type") == "object"
|
|
750
|
-
and "additionalProperties" in obj
|
|
751
|
-
):
|
|
752
|
-
if obj["additionalProperties"] is not False:
|
|
753
|
-
return True
|
|
745
|
+
schema = sanitize_and_enforce_required(schema)
|
|
746
|
+
except ImportError:
|
|
747
|
+
logger.debug("sanitize_and_enforce_required not available")
|
|
754
748
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
if isinstance(value, dict):
|
|
765
|
-
if _has_open_props(value, f"{path}.{key}"):
|
|
766
|
-
return True
|
|
767
|
-
elif isinstance(value, list):
|
|
768
|
-
for i, item in enumerate(value):
|
|
769
|
-
if _has_open_props(
|
|
770
|
-
item,
|
|
771
|
-
f"{path}.{key}[{i}]",
|
|
772
|
-
):
|
|
773
|
-
return True
|
|
774
|
-
elif isinstance(value, dict) and key not in [
|
|
775
|
-
"description",
|
|
776
|
-
"type",
|
|
777
|
-
"enum",
|
|
778
|
-
]:
|
|
779
|
-
if _has_open_props(value, f"{path}.{key}"):
|
|
780
|
-
return True
|
|
781
|
-
return False
|
|
749
|
+
parameters = schema["function"].get("parameters", {})
|
|
750
|
+
if not parameters:
|
|
751
|
+
# Empty parameters - use minimal valid schema
|
|
752
|
+
parameters = {
|
|
753
|
+
"type": "object",
|
|
754
|
+
"properties": {},
|
|
755
|
+
"additionalProperties": False,
|
|
756
|
+
}
|
|
757
|
+
schema["function"]["parameters"] = parameters
|
|
782
758
|
|
|
783
|
-
#
|
|
784
|
-
if
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
f"Tool '{tool.get_function_name()}' has "
|
|
790
|
-
f"dynamic additionalProperties and cannot use "
|
|
791
|
-
f"strict mode"
|
|
792
|
-
)
|
|
793
|
-
return tool
|
|
759
|
+
# MCP spec doesn't require 'properties', but OpenAI spec does
|
|
760
|
+
if (
|
|
761
|
+
parameters.get("type") == "object"
|
|
762
|
+
and "properties" not in parameters
|
|
763
|
+
):
|
|
764
|
+
parameters["properties"] = {}
|
|
794
765
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
#
|
|
766
|
+
try:
|
|
767
|
+
# _check_schema_limits(parameters)
|
|
768
|
+
|
|
769
|
+
# Check for OpenAI strict mode incompatible features
|
|
770
|
+
if _has_strict_mode_incompatible_features(parameters):
|
|
771
|
+
raise ValueError(
|
|
772
|
+
"Schema contains features "
|
|
773
|
+
"incompatible with strict mode"
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
strict_params = ensure_strict_json_schema(parameters)
|
|
777
|
+
schema["function"]["parameters"] = strict_params
|
|
778
|
+
schema["function"]["strict"] = True
|
|
779
|
+
except Exception as e:
|
|
780
|
+
# Fallback to non-strict mode on any failure
|
|
799
781
|
schema["function"]["strict"] = False
|
|
800
|
-
tool.set_openai_tool_schema(schema)
|
|
801
782
|
logger.warning(
|
|
802
|
-
f"Tool '{tool.get_function_name()}'
|
|
803
|
-
f"
|
|
804
|
-
f"{'; '.join(issues[:3])}{'...' if len(issues) > 3 else ''}" # noqa: E501
|
|
783
|
+
f"Tool '{tool.get_function_name()}' "
|
|
784
|
+
f"cannot use strict mode: {e}"
|
|
805
785
|
)
|
|
806
|
-
return tool
|
|
807
|
-
|
|
808
|
-
# Enable strict mode
|
|
809
|
-
schema["function"]["strict"] = True
|
|
810
|
-
|
|
811
|
-
parameters = schema["function"].get("parameters", {})
|
|
812
|
-
if parameters:
|
|
813
|
-
# Validate and fix the parameters schema
|
|
814
|
-
_validate_and_fix_schema(parameters)
|
|
815
|
-
|
|
816
|
-
# Check schema limits
|
|
817
|
-
_check_schema_limits(parameters)
|
|
818
786
|
|
|
819
787
|
tool.set_openai_tool_schema(schema)
|
|
820
|
-
logger.debug(
|
|
821
|
-
f"Updated tool '{tool.get_function_name()}' to strict mode"
|
|
822
|
-
)
|
|
823
788
|
|
|
824
789
|
except Exception as e:
|
|
825
|
-
#
|
|
790
|
+
# Final fallback - ensure tool still works
|
|
826
791
|
try:
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
792
|
+
current_schema = tool.get_openai_tool_schema()
|
|
793
|
+
if "function" in current_schema:
|
|
794
|
+
current_schema["function"]["strict"] = False
|
|
795
|
+
tool.set_openai_tool_schema(current_schema)
|
|
830
796
|
logger.warning(
|
|
831
|
-
f"
|
|
832
|
-
f"
|
|
833
|
-
f"
|
|
797
|
+
f"Error processing schema for tool "
|
|
798
|
+
f"'{tool.get_function_name()}': {str(e)[:100]}. "
|
|
799
|
+
f"Using non-strict mode."
|
|
834
800
|
)
|
|
835
801
|
except Exception as inner_e:
|
|
836
|
-
# If even setting strict=False fails, log the error
|
|
837
802
|
logger.error(
|
|
838
|
-
f"Critical error processing "
|
|
839
|
-
f"
|
|
803
|
+
f"Critical error processing tool "
|
|
804
|
+
f"'{tool.get_function_name()}': {inner_e}. "
|
|
840
805
|
f"Tool may not function correctly."
|
|
841
806
|
)
|
|
842
807
|
|
|
@@ -879,6 +844,7 @@ class MCPToolkit(BaseToolkit):
|
|
|
879
844
|
)
|
|
880
845
|
|
|
881
846
|
all_tools = []
|
|
847
|
+
seen_names: set[str] = set()
|
|
882
848
|
for i, client in enumerate(self.clients):
|
|
883
849
|
try:
|
|
884
850
|
client_tools = client.get_tools()
|
|
@@ -887,6 +853,14 @@ class MCPToolkit(BaseToolkit):
|
|
|
887
853
|
strict_tools = []
|
|
888
854
|
for tool in client_tools:
|
|
889
855
|
strict_tool = self._ensure_strict_tool_schema(tool)
|
|
856
|
+
name = strict_tool.get_function_name()
|
|
857
|
+
if name in seen_names:
|
|
858
|
+
logger.warning(
|
|
859
|
+
f"Duplicate tool name detected and "
|
|
860
|
+
f"skipped: '{name}' from client {i+1}"
|
|
861
|
+
)
|
|
862
|
+
continue
|
|
863
|
+
seen_names.add(name)
|
|
890
864
|
strict_tools.append(strict_tool)
|
|
891
865
|
|
|
892
866
|
all_tools.extend(strict_tools)
|
camel/utils/context_utils.py
CHANGED
|
@@ -393,3 +393,55 @@ class ContextUtility:
|
|
|
393
393
|
str: The session ID.
|
|
394
394
|
"""
|
|
395
395
|
return self.session_id
|
|
396
|
+
|
|
397
|
+
def load_markdown_context_to_memory(
|
|
398
|
+
self, agent: "ChatAgent", filename: str
|
|
399
|
+
) -> str:
|
|
400
|
+
r"""Load context from a markdown file and append it to agent memory.
|
|
401
|
+
Preserves existing conversation history without wiping it.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
agent (ChatAgent): The agent to append context to.
|
|
405
|
+
filename (str): Name of the markdown file (without .md extension).
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
str: Status message indicating success or failure with details.
|
|
409
|
+
"""
|
|
410
|
+
try:
|
|
411
|
+
content = self.load_markdown_file(filename)
|
|
412
|
+
|
|
413
|
+
if not content.strip():
|
|
414
|
+
return f"Context file not found or empty: {filename}"
|
|
415
|
+
|
|
416
|
+
from camel.messages import BaseMessage
|
|
417
|
+
from camel.types import OpenAIBackendRole
|
|
418
|
+
|
|
419
|
+
prefix_prompt = (
|
|
420
|
+
"The following is the context from a previous "
|
|
421
|
+
"session or workflow. This information might help you "
|
|
422
|
+
"understand the background, choose which tools to use, "
|
|
423
|
+
"and plan your next steps."
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# TODO: change to system message once multi-system-message
|
|
427
|
+
# is supported
|
|
428
|
+
context_message = BaseMessage.make_assistant_message(
|
|
429
|
+
role_name="Assistant",
|
|
430
|
+
content=f"{prefix_prompt}\n\n{content}",
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
agent.update_memory(context_message, OpenAIBackendRole.USER)
|
|
434
|
+
|
|
435
|
+
char_count = len(content)
|
|
436
|
+
log_msg = (
|
|
437
|
+
f"Context appended to agent {agent.agent_id} "
|
|
438
|
+
f"({char_count} characters)"
|
|
439
|
+
)
|
|
440
|
+
logger.info(log_msg)
|
|
441
|
+
|
|
442
|
+
return log_msg
|
|
443
|
+
|
|
444
|
+
except Exception as e:
|
|
445
|
+
error_msg = f"Failed to load markdown context to memory: {e}"
|
|
446
|
+
logger.error(error_msg)
|
|
447
|
+
return error_msg
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
camel/__init__.py,sha256=
|
|
1
|
+
camel/__init__.py,sha256=VEPqtodQz0M5XRxGR-gpj2ZYNaBX-IYXD71NCkHVyn4,901
|
|
2
2
|
camel/generators.py,sha256=JRqj9_m1PF4qT6UtybzTQ-KBT9MJQt18OAAYvQ_fr2o,13844
|
|
3
3
|
camel/human.py,sha256=Xg8x1cS5KK4bQ1SDByiHZnzsRpvRP-KZViNvmu38xo4,5475
|
|
4
4
|
camel/logger.py,sha256=WgEwael_eT6D-lVAKHpKIpwXSTjvLbny5jbV1Ab8lnA,5760
|
|
@@ -285,7 +285,7 @@ camel/societies/workforce/structured_output_handler.py,sha256=xr8szFN86hg3jQ825a
|
|
|
285
285
|
camel/societies/workforce/task_channel.py,sha256=TXRwiqtmRPdelEmFCVN3jhd5XpgaSLwy9uHPtGecujA,11418
|
|
286
286
|
camel/societies/workforce/utils.py,sha256=THgNHSeZsNVnjTzQTur3qCJhi72MrDS8X2gPET174cI,8434
|
|
287
287
|
camel/societies/workforce/worker.py,sha256=MtUqYkTC9V-PIIRwSkKiB9w_YSu92iOpoha2rktEiQ0,6248
|
|
288
|
-
camel/societies/workforce/workforce.py,sha256=
|
|
288
|
+
camel/societies/workforce/workforce.py,sha256=m5m2d2aZdKlxek8xTDrPHqvur9rg5LGYcUecZrR0ym8,144628
|
|
289
289
|
camel/societies/workforce/workforce_logger.py,sha256=0YT__ys48Bgn0IICKIZBmSWhON-eA1KShebjCdn5ppE,24525
|
|
290
290
|
camel/storages/__init__.py,sha256=RwpEyvxpMbJzVDZJJygeBg4AzyYMkTjjkfB53hTuqGo,2141
|
|
291
291
|
camel/storages/graph_storages/__init__.py,sha256=G29BNn651C0WTOpjCl4QnVM-4B9tcNh8DdmsCiONH8Y,948
|
|
@@ -354,7 +354,7 @@ camel/toolkits/klavis_toolkit.py,sha256=ZKerhgz5e-AV-iv0ftf07HgWikknIHjB3EOQswfu
|
|
|
354
354
|
camel/toolkits/linkedin_toolkit.py,sha256=wn4eXwYYlVA7doTna7k7WYhUqTBF83W79S-UJs_IQr0,8065
|
|
355
355
|
camel/toolkits/markitdown_toolkit.py,sha256=lwN6qQY8TLZkNWOqzeKZG3Fku-HMpGFrdRwhtPaJSlw,3844
|
|
356
356
|
camel/toolkits/math_toolkit.py,sha256=SJbzT6akHRlmqo1QwCj1f7-6pEv0sNKJbcYvYAylHQw,5439
|
|
357
|
-
camel/toolkits/mcp_toolkit.py,sha256=
|
|
357
|
+
camel/toolkits/mcp_toolkit.py,sha256=Y6bwq_Wy1FeBi7uVjiR6SrN1GQeJJDqgDt6Hcl26gOw,37530
|
|
358
358
|
camel/toolkits/memory_toolkit.py,sha256=TeKYd5UMwgjVpuS2orb-ocFL13eUNKujvrFOruDCpm8,4436
|
|
359
359
|
camel/toolkits/meshy_toolkit.py,sha256=NbgdOBD3FYLtZf-AfonIv6-Q8-8DW129jsaP1PqI2rs,7126
|
|
360
360
|
camel/toolkits/message_agent_toolkit.py,sha256=yWvAaxoxAvDEtD7NH7IkkHIyfWIYK47WZhn5E_RaxKo,22661
|
|
@@ -459,7 +459,7 @@ camel/utils/__init__.py,sha256=qQeMHZJ8Bbgpm4tBu-LWc_P3iFjXBlVfALdKTiD_s8I,3305
|
|
|
459
459
|
camel/utils/async_func.py,sha256=KqoktGSWjZBuAMQ2CV0X6FRgHGlzCKLfeaWvp-f1Qz8,1568
|
|
460
460
|
camel/utils/commons.py,sha256=xEhN__xkM1AT0dvvlAHZiPkGvfpwpj9BhCAWD51qJQ0,37163
|
|
461
461
|
camel/utils/constants.py,sha256=cqnxmpUeOwrtsR-tRO4bbOc6ZP19TLj7avjm3FONMJs,1410
|
|
462
|
-
camel/utils/context_utils.py,sha256=
|
|
462
|
+
camel/utils/context_utils.py,sha256=oJ6mdN2q4NKoHV-KBtlGhCEiKGrOtdwwmZpYbLL-2Ek,15466
|
|
463
463
|
camel/utils/deduplication.py,sha256=UHikAtOW1TTDunf2t_wa2kFbmkrXWf7HfOKwLvwCxzo,8958
|
|
464
464
|
camel/utils/filename.py,sha256=HYNc1wbSCgNR1CN21cwHxdAhpnsf5ySJ6jUDfeqOK20,2532
|
|
465
465
|
camel/utils/langfuse.py,sha256=OowR6A790XG-b0UHiTYduYvS18PvSGFdmqki2Poogo0,8578
|
|
@@ -479,7 +479,7 @@ camel/verifiers/math_verifier.py,sha256=tA1D4S0sm8nsWISevxSN0hvSVtIUpqmJhzqfbuMo
|
|
|
479
479
|
camel/verifiers/models.py,sha256=GdxYPr7UxNrR1577yW4kyroRcLGfd-H1GXgv8potDWU,2471
|
|
480
480
|
camel/verifiers/physics_verifier.py,sha256=c1grrRddcrVN7szkxhv2QirwY9viIRSITWeWFF5HmLs,30187
|
|
481
481
|
camel/verifiers/python_verifier.py,sha256=ogTz77wODfEcDN4tMVtiSkRQyoiZbHPY2fKybn59lHw,20558
|
|
482
|
-
camel_ai-0.2.
|
|
483
|
-
camel_ai-0.2.
|
|
484
|
-
camel_ai-0.2.
|
|
485
|
-
camel_ai-0.2.
|
|
482
|
+
camel_ai-0.2.76a4.dist-info/METADATA,sha256=RpkQkSkR6m7JLdYutzU7R0qy2319g-mAOcWTZgmfgK4,54897
|
|
483
|
+
camel_ai-0.2.76a4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
484
|
+
camel_ai-0.2.76a4.dist-info/licenses/LICENSE,sha256=id0nB2my5kG0xXeimIu5zZrbHLS6EQvxvkKkzIHaT2k,11343
|
|
485
|
+
camel_ai-0.2.76a4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|