diff --git a/camel/embeddings/vlm_embedding.py b/camel/embeddings/vlm_embedding.py index 7fe5f23827..88807d5aac 100644 --- a/camel/embeddings/vlm_embedding.py +++ b/camel/embeddings/vlm_embedding.py @@ -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: diff --git a/camel/interpreters/internal_python_interpreter.py b/camel/interpreters/internal_python_interpreter.py index c75556abe3..6849f316b2 100644 --- a/camel/interpreters/internal_python_interpreter.py +++ b/camel/interpreters/internal_python_interpreter.py @@ -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) diff --git a/camel/models/anthropic_model.py b/camel/models/anthropic_model.py index 72ba855942..3ea4baf090 100644 --- a/camel/models/anthropic_model.py +++ b/camel/models/anthropic_model.py @@ -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 @@ -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", @@ -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", ""), @@ -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") @@ -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") diff --git a/camel/models/mistral_model.py b/camel/models/mistral_model.py index e8a5ecc866..f73d975687 100644 --- a/camel/models/mistral_model.py +++ b/camel/models/mistral_model.py @@ -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] ) ) diff --git a/camel/retrievers/hybrid_retrival.py b/camel/retrievers/hybrid_retrival.py index b3765c81e7..dd558a39b4 100644 --- a/camel/retrievers/hybrid_retrival.py +++ b/camel/retrievers/hybrid_retrival.py @@ -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 @@ -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. diff --git a/test/models/test_anthropic_model.py b/test/models/test_anthropic_model.py index e3da4bcdce..82648656c3 100644 --- a/test/models/test_anthropic_model.py +++ b/test/models/test_anthropic_model.py @@ -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, @@ -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() @@ -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"}' @@ -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 )