Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions camel/embeddings/vlm_embedding.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,11 @@ def embed_list(
if not objs:
raise ValueError("Input objs list is empty.")

image_processor_kwargs: Optional[dict] = kwargs.get(
'image_processor_kwargs', {}
image_processor_kwargs: dict = (
kwargs.get('image_processor_kwargs', {}) or {}
)
tokenizer_kwargs: Optional[dict] = kwargs.get('tokenizer_kwargs', {})
model_kwargs: Optional[dict] = kwargs.get('model_kwargs', {})
tokenizer_kwargs: dict = kwargs.get('tokenizer_kwargs', {}) or {}
model_kwargs: dict = kwargs.get('model_kwargs', {}) or {}

result_list = []
for obj in objs:
Expand Down
2 changes: 1 addition & 1 deletion camel/interpreters/internal_python_interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ def _execute_call(self, call: ast.Call) -> Any:
keyword.arg: self._execute_ast(keyword.value)
for keyword in call.keywords
}
return callable_func(*args, **kwargs)
return callable_func(*args, **kwargs) # type: ignore[arg-type]

def _execute_subscript(self, subscript: ast.Subscript):
index = self._execute_ast(subscript.slice)
Expand Down
123 changes: 25 additions & 98 deletions camel/models/anthropic_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,6 @@
update_langfuse_trace,
)

ANTHROPIC_SUPPORTED_STRING_FORMATS = {
"date",
"date-time",
"time",
}
ANTHROPIC_UNSUPPORTED_SCHEMA_CONSTRAINTS = {
"minimum": "Must be greater than or equal to {value}.",
"maximum": "Must be less than or equal to {value}.",
"exclusiveMinimum": "Must be greater than {value}.",
"exclusiveMaximum": "Must be less than {value}.",
"multipleOf": "Must be a multiple of {value}.",
"minLength": "Must be at least {value} characters long.",
"maxLength": "Must be at most {value} characters long.",
"minItems": "Must contain at least {value} items.",
"maxItems": "Must contain at most {value} items.",
"minProperties": "Must contain at least {value} properties.",
"maxProperties": "Must contain at most {value} properties.",
}

if os.environ.get("LANGFUSE_ENABLED", "False").lower() == "true":
try:
from langfuse.decorators import observe
Expand Down Expand Up @@ -355,76 +336,13 @@ def _convert_openai_to_anthropic_messages(

return system_message, anthropic_messages # type: ignore[return-value]

@staticmethod
def _append_constraint_descriptions(
schema: Dict[str, Any], descriptions: List[str]
) -> None:
r"""Append constraint descriptions to a schema node."""
if not descriptions:
return

description = schema.get("description")
suffix = " ".join(descriptions)
if isinstance(description, str) and description:
schema["description"] = f"{description} {suffix}"
else:
schema["description"] = suffix

@staticmethod
def _normalize_schema_for_anthropic(schema: Any) -> None:
r"""Normalize JSON Schema to Anthropic's supported subset.

This mirrors the documented SDK behavior at a minimal level by:
1. Removing unsupported validation constraints
2. Moving those constraints into descriptions
3. Enforcing ``additionalProperties: false`` for objects
4. Filtering unsupported string ``format`` values
"""
if isinstance(schema, dict):
descriptions = []

for (
key,
template,
) in ANTHROPIC_UNSUPPORTED_SCHEMA_CONSTRAINTS.items():
if key in schema:
value = schema.pop(key)
descriptions.append(template.format(value=value))

if schema.get("type") == "string":
string_format = schema.get("format")
if (
isinstance(string_format, str)
and string_format not in ANTHROPIC_SUPPORTED_STRING_FORMATS
):
schema.pop("format", None)

if (
schema.get("type") == "object"
and "additionalProperties" not in schema
):
schema["additionalProperties"] = False

if schema.get("uniqueItems") is True:
schema.pop("uniqueItems", None)
descriptions.append("Items must be unique.")

AnthropicModel._append_constraint_descriptions(
schema, descriptions
)

for value in list(schema.values()):
AnthropicModel._normalize_schema_for_anthropic(value)
elif isinstance(schema, list):
for item in schema:
AnthropicModel._normalize_schema_for_anthropic(item)

def _build_output_config(
self, response_format: Type[BaseModel]
) -> Dict[str, Any]:
r"""Build Anthropic output_config.format from a Pydantic model."""
schema = copy.deepcopy(response_format.model_json_schema())
self._normalize_schema_for_anthropic(schema)
from anthropic import transform_schema

schema = transform_schema(response_format.model_json_schema())
return {
"format": {
"type": "json_schema",
Expand Down Expand Up @@ -750,9 +668,7 @@ def _convert_openai_tools_to_anthropic(
for tool in tools:
if "function" in tool:
func = tool["function"]
input_schema = copy.deepcopy(func.get("parameters", {}))
if func.get("strict") is True:
self._normalize_schema_for_anthropic(input_schema)
input_schema = func.get("parameters", {})
anthropic_tool = {
"name": func.get("name", ""),
"description": func.get("description", ""),
Expand Down Expand Up @@ -864,19 +780,27 @@ def _run(
if extra_headers is not None:
request_params["extra_headers"] = extra_headers

# Convert tools first so we know whether tools are present
anthropic_tools = self._convert_openai_tools_to_anthropic(tools)

extra_body = copy.deepcopy(
self.model_config_dict.get("extra_body") or {}
)
if response_format is not None:
extra_body.pop("output_config", None)
request_params["output_config"] = self._build_output_config(
response_format
)
# Only use output_config when there are no tools.
# When tools are present the model must be free to emit
# tool_use blocks first; output_config constrains ALL text
# output to the JSON schema which can prevent tool calling.
# The upper-layer ChatAgent._format_response_if_needed will
# parse the final text into the requested format instead.
if not anthropic_tools:
request_params["output_config"] = self._build_output_config(
response_format
)
if extra_body:
request_params["extra_body"] = extra_body

# Convert tools
anthropic_tools = self._convert_openai_tools_to_anthropic(tools)
if anthropic_tools:
request_params["tools"] = anthropic_tools
tool_choice = self.model_config_dict.get("tool_choice")
Expand Down Expand Up @@ -1003,19 +927,22 @@ async def _arun(
if extra_headers is not None:
request_params["extra_headers"] = extra_headers

# Convert tools first so we know whether tools are present
anthropic_tools = self._convert_openai_tools_to_anthropic(tools)

extra_body = copy.deepcopy(
self.model_config_dict.get("extra_body") or {}
)
if response_format is not None:
extra_body.pop("output_config", None)
request_params["output_config"] = self._build_output_config(
response_format
)
# Only use output_config when there are no tools (see _run).
if not anthropic_tools:
request_params["output_config"] = self._build_output_config(
response_format
)
if extra_body:
request_params["extra_body"] = extra_body

# Convert tools
anthropic_tools = self._convert_openai_tools_to_anthropic(tools)
if anthropic_tools:
request_params["tools"] = anthropic_tools
tool_choice = self.model_config_dict.get("tool_choice")
Expand Down
6 changes: 3 additions & 3 deletions camel/models/mistral_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,15 +202,15 @@ def _to_mistral_chatmessage(
mistral_tool_calls = []
for tool_call in tool_calls_list:
function_call = FunctionCall(
name=tool_call["function"].get("name"), # type: ignore[attr-defined]
arguments=tool_call["function"].get("arguments"), # type: ignore[attr-defined]
name=tool_call["function"].get("name"), # type: ignore[index]
arguments=tool_call["function"].get("arguments"), # type: ignore[index]
)
# Preserve the original tool call id to keep tool result
# ordering valid across turns.
mistral_tool_calls.append(
ToolCall(
function=function_call,
id=tool_call.get("id"), # type: ignore[attr-defined]
id=tool_call.get("id"), # type: ignore[union-attr]
)
)

Expand Down
7 changes: 2 additions & 5 deletions camel/retrievers/hybrid_retrival.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2023-2026 @ CAMEL-AI.org. All Rights Reserved. =========
from typing import Any, Collection, Dict, List, Optional, Sequence, Union
from typing import Any, Dict, List, Optional, Union

from camel.embeddings import BaseEmbedding
from camel.retrievers import BaseRetriever, BM25Retriever, VectorRetriever
Expand Down Expand Up @@ -161,10 +161,7 @@ def query(
vector_retriever_similarity_threshold: float = 0.5,
bm25_retriever_top_k: int = 50,
return_detailed_info: bool = False,
) -> Union[
dict[str, Sequence[Collection[str]]],
dict[str, Sequence[Union[str, float]]],
]:
) -> Dict[str, Any]:
r"""Executes a hybrid retrieval query using both vector and BM25
retrievers.

Expand Down
74 changes: 3 additions & 71 deletions test/models/test_anthropic_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,7 @@ def test_convert_stream_chunk_message_stop():
assert result.choices[0].finish_reason == "stop"


def test_build_output_config_enforces_additional_properties_false():
def test_build_output_config():
"""Test output_config generation for structured outputs."""
model = AnthropicModel(
ModelType.CLAUDE_HAIKU_4_5,
Expand All @@ -621,71 +621,9 @@ def test_build_output_config_enforces_additional_properties_false():
assert output_config["format"]["type"] == "json_schema"
schema = output_config["format"]["schema"]
assert schema["type"] == "object"
assert schema["additionalProperties"] is False
assert "city" in schema["properties"]


def test_convert_openai_tools_to_anthropic_normalizes_strict_schema():
"""Test strict tool schemas are normalized for Anthropic."""
model = AnthropicModel(
ModelType.CLAUDE_HAIKU_4_5,
api_key="dummy_api_key",
)

tools = [
{
"type": "function",
"function": {
"name": "plan_trip",
"description": "Plan a trip",
"strict": True,
"parameters": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"minLength": 5,
},
"tags": {
"type": "array",
"items": {"type": "string"},
"uniqueItems": True,
},
"preferences": {
"type": "object",
"properties": {
"season": {"type": "string"},
},
},
},
"required": ["email"],
},
},
}
]

anthropic_tools = model._convert_openai_tools_to_anthropic(tools)
input_schema = anthropic_tools[0]["input_schema"]

assert input_schema["additionalProperties"] is False
assert (
input_schema["properties"]["preferences"]["additionalProperties"]
is False
)
assert "format" not in input_schema["properties"]["email"]
assert "minLength" not in input_schema["properties"]["email"]
assert (
"Must be at least 5 characters long."
in input_schema["properties"]["email"]["description"]
)
assert "uniqueItems" not in input_schema["properties"]["tags"]
assert (
"Items must be unique."
in input_schema["properties"]["tags"]["description"]
)


def test_run_passes_output_config_tool_choice_and_extra_fields():
"""Test Anthropic request payload for structured outputs with tools."""
mock_client = MagicMock()
Expand Down Expand Up @@ -749,12 +687,6 @@ def test_run_passes_output_config_tool_choice_and_extra_fields():
assert request_kwargs["extra_body"]["existing"] is True
assert "output_config" not in request_kwargs["extra_body"]
assert request_kwargs["output_config"]["format"]["type"] == "json_schema"
assert (
request_kwargs["output_config"]["format"]["schema"][
"additionalProperties"
]
is False
)
assert request_kwargs["tools"][0]["strict"] is True
assert result.choices[0].message.content == '{"city":"Kyoto"}'

Expand Down Expand Up @@ -824,7 +756,7 @@ async def test_arun_passes_output_config_tool_choice_and_extra_fields():
assert request_kwargs["output_config"]["format"]["type"] == "json_schema"
assert (
request_kwargs["tools"][0]["input_schema"]["properties"]["location"][
"description"
"minLength"
]
== "Must be at least 3 characters long."
== 3
)
Loading