diff --git a/.secrets.baseline b/.secrets.baseline index 33538bfada..cc084e5da0 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -143,7 +143,7 @@ "filename": "autogen/oai/openai_utils.py", "hashed_secret": "aa5bc2e0df7182f74186f26d6e9063b9d57603ec", "is_verified": false, - "line_number": 353, + "line_number": 378, "is_secret": false }, { @@ -151,7 +151,7 @@ "filename": "autogen/oai/openai_utils.py", "hashed_secret": "cbb43d092552e9af4b21efc76bc8c49c071c1d81", "is_verified": false, - "line_number": 354, + "line_number": 379, "is_secret": false }, { @@ -159,7 +159,7 @@ "filename": "autogen/oai/openai_utils.py", "hashed_secret": "79d8b9da0f827f788759bdbe5b9254a02c74d877", "is_verified": false, - "line_number": 577, + "line_number": 602, "is_secret": false } ], @@ -1035,7 +1035,7 @@ "filename": "test/oai/test_utils.py", "hashed_secret": "f72c85879027f6160ce36e1c5074ef8207bfe105", "is_verified": false, - "line_number": 30, + "line_number": 31, "is_secret": false }, { @@ -1043,7 +1043,7 @@ "filename": "test/oai/test_utils.py", "hashed_secret": "4c88039c5079180dacb0e29d715055d95b2b7589", "is_verified": false, - "line_number": 39, + "line_number": 40, "is_secret": false }, { @@ -1051,7 +1051,7 @@ "filename": "test/oai/test_utils.py", "hashed_secret": "7460e665be1988cc62f1caf9d47716b07d55858c", "is_verified": false, - "line_number": 69, + "line_number": 70, "is_secret": false }, { @@ -1059,7 +1059,7 @@ "filename": "test/oai/test_utils.py", "hashed_secret": "b5c2827eb65bf13b87130e7e3c424ba9ff07cd67", "is_verified": false, - "line_number": 76, + "line_number": 77, "is_secret": false }, { @@ -1067,7 +1067,7 @@ "filename": "test/oai/test_utils.py", "hashed_secret": "178c7a21b087dfafc826a21b61aff284c71fd258", "is_verified": false, - "line_number": 202, + "line_number": 203, "is_secret": false }, { @@ -1075,7 +1075,7 @@ "filename": "test/oai/test_utils.py", "hashed_secret": "aa5c90e1b80bb987f562ac30eaa1a71c832892f5", "is_verified": false, - "line_number": 203, + "line_number": 204, "is_secret": false }, { @@ -1083,7 +1083,7 @@ "filename": "test/oai/test_utils.py", "hashed_secret": "4489f55309f29853a4075cbbdf1f18b584809726", "is_verified": false, - "line_number": 205, + "line_number": 206, "is_secret": false }, { @@ -1091,7 +1091,7 @@ "filename": "test/oai/test_utils.py", "hashed_secret": "95cfb33d5e102631e226e7ff9da4b17d6ba5f3e4", "is_verified": false, - "line_number": 217, + "line_number": 218, "is_secret": false }, { @@ -1099,7 +1099,7 @@ "filename": "test/oai/test_utils.py", "hashed_secret": "7943297a6a2188abe697bd1e0189fdd1274818be", "is_verified": false, - "line_number": 219, + "line_number": 220, "is_secret": false }, { @@ -1107,7 +1107,7 @@ "filename": "test/oai/test_utils.py", "hashed_secret": "8cc86c45479a8e0bbb1ddea57d3e195b611241f2", "is_verified": false, - "line_number": 239, + "line_number": 240, "is_secret": false }, { @@ -1115,7 +1115,7 @@ "filename": "test/oai/test_utils.py", "hashed_secret": "eda6571eea7bd0ac4553ac9d745631f1f2bec7a4", "is_verified": false, - "line_number": 241, + "line_number": 242, "is_secret": false }, { @@ -1123,7 +1123,7 @@ "filename": "test/oai/test_utils.py", "hashed_secret": "0ad02c88ffd9754bfbfc24ade0bf8bc48d76b232", "is_verified": false, - "line_number": 250, + "line_number": 251, "is_secret": false }, { @@ -1131,7 +1131,7 @@ "filename": "test/oai/test_utils.py", "hashed_secret": "11841233da3f9f37c5fa14e8b482dde913db6edf", "is_verified": false, - "line_number": 258, + "line_number": 259, "is_secret": false }, { @@ -1139,7 +1139,7 @@ "filename": "test/oai/test_utils.py", "hashed_secret": "11cac88cbfa53881646b024097f531c4f234151b", "is_verified": false, - "line_number": 436, + "line_number": 475, "is_secret": false }, { @@ -1147,7 +1147,7 @@ "filename": "test/oai/test_utils.py", "hashed_secret": "8e8324e8ea2ec13efb774680c6e3850625e575e6", "is_verified": false, - "line_number": 436, + "line_number": 475, "is_secret": false }, { @@ -1155,7 +1155,7 @@ "filename": "test/oai/test_utils.py", "hashed_secret": "8e2fa04ab430ff4817e87e3294f33727fc78ed6c", "is_verified": false, - "line_number": 439, + "line_number": 478, "is_secret": false } ], @@ -1187,16 +1187,6 @@ "is_secret": false } ], - "test/tools/experimental/crawl4ai/test_crawl4ai.py": [ - { - "type": "Secret Keyword", - "filename": "test/tools/experimental/crawl4ai/test_crawl4ai.py", - "hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", - "is_verified": false, - "line_number": 51, - "is_secret": false - } - ], "test/website/test_process_notebooks.py": [ { "type": "Base64 High Entropy String", @@ -1606,5 +1596,5 @@ } ] }, - "generated_at": "2025-02-20T10:09:56Z" + "generated_at": "2025-02-20T13:06:27Z" } diff --git a/autogen/interop/__init__.py b/autogen/interop/__init__.py index b4ac8d7566..178f0bccb8 100644 --- a/autogen/interop/__init__.py +++ b/autogen/interop/__init__.py @@ -5,7 +5,8 @@ from .crewai import CrewAIInteroperability from .interoperability import Interoperability from .interoperable import Interoperable -from .langchain import LangChainInteroperability +from .langchain import LangChainChatModelFactory, LangChainInteroperability +from .litellm import LiteLLmConfigFactory from .pydantic_ai import PydanticAIInteroperability from .registry import register_interoperable_class @@ -13,7 +14,9 @@ "CrewAIInteroperability", "Interoperability", "Interoperable", + "LangChainChatModelFactory", "LangChainInteroperability", + "LiteLLmConfigFactory", "PydanticAIInteroperability", "register_interoperable_class", ] diff --git a/autogen/interop/langchain/__init__.py b/autogen/interop/langchain/__init__.py index 6af1cf3a37..b858b04777 100644 --- a/autogen/interop/langchain/__init__.py +++ b/autogen/interop/langchain/__init__.py @@ -2,6 +2,7 @@ # # SPDX-License-Identifier: Apache-2.0 -from .langchain import LangChainInteroperability +from .langchain_chat_model_factory import LangChainChatModelFactory +from .langchain_tool import LangChainInteroperability -__all__ = ["LangChainInteroperability"] +__all__ = ["LangChainChatModelFactory", "LangChainInteroperability"] diff --git a/autogen/tools/experimental/browser_use/langchain_factory.py b/autogen/interop/langchain/langchain_chat_model_factory.py similarity index 73% rename from autogen/tools/experimental/browser_use/langchain_factory.py rename to autogen/interop/langchain/langchain_chat_model_factory.py index fe1fdeb1f5..a696b3d56c 100644 --- a/autogen/tools/experimental/browser_use/langchain_factory.py +++ b/autogen/interop/langchain/langchain_chat_model_factory.py @@ -3,10 +3,11 @@ # SPDX-License-Identifier: Apache-2.0 from abc import ABC, abstractmethod -from copy import deepcopy -from typing import Any, Callable +from typing import Any, Callable, TypeVar -from ....import_utils import optional_import_block, require_optional_import +from ...doc_utils import export_module +from ...import_utils import optional_import_block, require_optional_import +from ...oai import get_first_llm_config with optional_import_block(): from langchain_anthropic import ChatAnthropic @@ -16,7 +17,9 @@ from langchain_openai import AzureChatOpenAI, ChatOpenAI -__all__ = ["LangchainFactory"] +__all__ = ["LangChainChatModelFactory"] + +T = TypeVar("T", bound="LangChainChatModelFactory") @require_optional_import( @@ -24,38 +27,27 @@ "browser-use", except_for=["__init__", "register_factory"], ) -class LangchainFactory(ABC): - _factories: set["LangchainFactory"] = set() +@export_module("autogen.interop") +class LangChainChatModelFactory(ABC): + _factories: set["LangChainChatModelFactory"] = set() @classmethod def create_base_chat_model(cls, llm_config: dict[str, Any]) -> "BaseChatModel": # type: ignore [no-any-unimported] - first_llm_config = cls.get_first_llm_config(llm_config) - for factory in LangchainFactory._factories: + first_llm_config = get_first_llm_config(llm_config) + for factory in LangChainChatModelFactory._factories: if factory.accepts(first_llm_config): return factory.create(first_llm_config) raise ValueError("Could not find a factory for the given config.") @classmethod - def register_factory(cls) -> Callable[[type["LangchainFactory"]], type["LangchainFactory"]]: - def decorator(factory: type["LangchainFactory"]) -> type["LangchainFactory"]: + def register_factory(cls) -> Callable[[type[T]], type[T]]: + def decorator(factory: type[T]) -> type[T]: cls._factories.add(factory()) return factory return decorator - @classmethod - def get_first_llm_config(cls, llm_config: dict[str, Any]) -> dict[str, Any]: - llm_config = deepcopy(llm_config) - if "config_list" not in llm_config: - if "model" in llm_config: - return llm_config - raise ValueError("llm_config must be a valid config dictionary.") - - if len(llm_config["config_list"]) == 0: - raise ValueError("Config list must contain at least one config.") - return llm_config["config_list"][0] # type: ignore [no-any-return] - @classmethod def prepare_config(cls, first_llm_config: dict[str, Any]) -> dict[str, Any]: for pop_keys in ["api_type", "response_format"]: @@ -76,8 +68,8 @@ def accepts(cls, first_llm_config: dict[str, Any]) -> bool: return first_llm_config.get("api_type", "openai") == cls.get_api_type() # type: ignore [no-any-return] -@LangchainFactory.register_factory() -class ChatOpenAIFactory(LangchainFactory): +@LangChainChatModelFactory.register_factory() +class ChatOpenAIFactory(LangChainChatModelFactory): @classmethod def create(cls, first_llm_config: dict[str, Any]) -> "ChatOpenAI": # type: ignore [no-any-unimported] first_llm_config = cls.prepare_config(first_llm_config) @@ -89,7 +81,7 @@ def get_api_type(cls) -> str: return "openai" -@LangchainFactory.register_factory() +@LangChainChatModelFactory.register_factory() class DeepSeekFactory(ChatOpenAIFactory): @classmethod def create(cls, first_llm_config: dict[str, Any]) -> "ChatOpenAI": # type: ignore [no-any-unimported] @@ -102,8 +94,8 @@ def get_api_type(cls) -> str: return "deepseek" -@LangchainFactory.register_factory() -class ChatAnthropicFactory(LangchainFactory): +@LangChainChatModelFactory.register_factory() +class ChatAnthropicFactory(LangChainChatModelFactory): @classmethod def create(cls, first_llm_config: dict[str, Any]) -> "ChatAnthropic": # type: ignore [no-any-unimported] first_llm_config = cls.prepare_config(first_llm_config) @@ -115,8 +107,8 @@ def get_api_type(cls) -> str: return "anthropic" -@LangchainFactory.register_factory() -class ChatGoogleGenerativeAIFactory(LangchainFactory): +@LangChainChatModelFactory.register_factory() +class ChatGoogleGenerativeAIFactory(LangChainChatModelFactory): @classmethod def create(cls, first_llm_config: dict[str, Any]) -> "ChatGoogleGenerativeAI": # type: ignore [no-any-unimported] first_llm_config = cls.prepare_config(first_llm_config) @@ -128,8 +120,8 @@ def get_api_type(cls) -> str: return "google" -@LangchainFactory.register_factory() -class AzureChatOpenAIFactory(LangchainFactory): +@LangChainChatModelFactory.register_factory() +class AzureChatOpenAIFactory(LangChainChatModelFactory): @classmethod def create(cls, first_llm_config: dict[str, Any]) -> "AzureChatOpenAI": # type: ignore [no-any-unimported] first_llm_config = cls.prepare_config(first_llm_config) @@ -145,8 +137,8 @@ def get_api_type(cls) -> str: return "azure" -@LangchainFactory.register_factory() -class ChatOllamaFactory(LangchainFactory): +@LangChainChatModelFactory.register_factory() +class ChatOllamaFactory(LangChainChatModelFactory): @classmethod def create(cls, first_llm_config: dict[str, Any]) -> "ChatOllama": # type: ignore [no-any-unimported] first_llm_config = cls.prepare_config(first_llm_config) diff --git a/autogen/interop/langchain/langchain.py b/autogen/interop/langchain/langchain_tool.py similarity index 100% rename from autogen/interop/langchain/langchain.py rename to autogen/interop/langchain/langchain_tool.py diff --git a/autogen/interop/litellm/__init__.py b/autogen/interop/litellm/__init__.py new file mode 100644 index 0000000000..5151695815 --- /dev/null +++ b/autogen/interop/litellm/__init__.py @@ -0,0 +1,7 @@ +# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors +# +# SPDX-License-Identifier: Apache-2.0 + +from .litellm_config_factory import LiteLLmConfigFactory + +__all__ = ["LiteLLmConfigFactory"] diff --git a/autogen/interop/litellm/litellm_config_factory.py b/autogen/interop/litellm/litellm_config_factory.py new file mode 100644 index 0000000000..d691763892 --- /dev/null +++ b/autogen/interop/litellm/litellm_config_factory.py @@ -0,0 +1,112 @@ +# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors +# +# SPDX-License-Identifier: Apache-2.0 + +import os +from abc import ABC, abstractmethod +from typing import Any, Callable, TypeVar + +from ...doc_utils import export_module +from ...oai import get_first_llm_config + +__all__ = ["LiteLLmConfigFactory"] + +T = TypeVar("T", bound="LiteLLmConfigFactory") + + +@export_module("autogen.interop") +class LiteLLmConfigFactory(ABC): + _factories: set["LiteLLmConfigFactory"] = set() + + @classmethod + def create_lite_llm_config(cls, llm_config: dict[str, Any]) -> dict[str, Any]: + first_llm_config = get_first_llm_config(llm_config) + for factory in LiteLLmConfigFactory._factories: + if factory.accepts(first_llm_config): + return factory.create(first_llm_config) + + raise ValueError("Could not find a factory for the given config.") + + @classmethod + def register_factory(cls) -> Callable[[type[T]], type[T]]: + def decorator(factory: type[T]) -> type[T]: + cls._factories.add(factory()) + return factory + + return decorator + + @classmethod + def create(cls, first_llm_config: dict[str, Any]) -> dict[str, Any]: + model = first_llm_config.pop("model") + api_type = first_llm_config.pop("api_type", "openai") + + first_llm_config["provider"] = f"{api_type}/{model}" + return first_llm_config + + @classmethod + @abstractmethod + def get_api_type(cls) -> str: ... + + @classmethod + def accepts(cls, first_llm_config: dict[str, Any]) -> bool: + return first_llm_config.get("api_type", "openai") == cls.get_api_type() # type: ignore [no-any-return] + + +@LiteLLmConfigFactory.register_factory() +class DefaultLiteLLmConfigFactory(LiteLLmConfigFactory): + @classmethod + def get_api_type(cls) -> str: + raise NotImplementedError("DefaultLiteLLmConfigFactory does not have an API type.") + + @classmethod + def accepts(cls, first_llm_config: dict[str, Any]) -> bool: + non_base_api_types = ["google", "ollama"] + return first_llm_config.get("api_type", "openai") not in non_base_api_types + + @classmethod + def create(cls, first_llm_config: dict[str, Any]) -> dict[str, Any]: + api_type = first_llm_config.get("api_type", "openai") + if api_type != "openai" and "api_key" not in first_llm_config: + raise ValueError("API key is required.") + first_llm_config["api_token"] = first_llm_config.pop("api_key", os.getenv("OPENAI_API_KEY")) + + first_llm_config = super().create(first_llm_config) + + return first_llm_config + + +@LiteLLmConfigFactory.register_factory() +class GoogleLiteLLmConfigFactory(LiteLLmConfigFactory): + @classmethod + def get_api_type(cls) -> str: + return "google" + + @classmethod + def create(cls, first_llm_config: dict[str, Any]) -> dict[str, Any]: + # api type must be changed before calling super().create + # litellm uses gemini as the api type for google + first_llm_config["api_type"] = "gemini" + first_llm_config["api_token"] = first_llm_config.pop("api_key") + first_llm_config = super().create(first_llm_config) + + return first_llm_config + + @classmethod + def accepts(cls, first_llm_config: dict[str, Any]) -> bool: + api_type: str = first_llm_config.get("api_type", "") + return api_type == cls.get_api_type() or api_type == "gemini" + + +@LiteLLmConfigFactory.register_factory() +class OllamaLiteLLmConfigFactory(LiteLLmConfigFactory): + @classmethod + def get_api_type(cls) -> str: + return "ollama" + + @classmethod + def create(cls, first_llm_config: dict[str, Any]) -> dict[str, Any]: + first_llm_config = super().create(first_llm_config) + if "client_host" in first_llm_config: + first_llm_config["api_base"] = first_llm_config.pop("client_host") + + return first_llm_config diff --git a/autogen/oai/__init__.py b/autogen/oai/__init__.py index bdce79bf49..c8510db8e1 100644 --- a/autogen/oai/__init__.py +++ b/autogen/oai/__init__.py @@ -15,6 +15,7 @@ config_list_openai_aoai, filter_config, get_config_list, + get_first_llm_config, ) __all__ = [ @@ -30,4 +31,5 @@ "config_list_openai_aoai", "filter_config", "get_config_list", + "get_first_llm_config", ] diff --git a/autogen/oai/openai_utils.py b/autogen/oai/openai_utils.py index b391a5d081..eaff4616be 100644 --- a/autogen/oai/openai_utils.py +++ b/autogen/oai/openai_utils.py @@ -12,6 +12,7 @@ import re import tempfile import time +from copy import deepcopy from pathlib import Path from typing import Any, Optional, Union @@ -188,6 +189,30 @@ def get_config_list( return config_list +@export_module("autogen") +def get_first_llm_config(llm_config: dict[str, Any]) -> dict[str, Any]: + """Get the first LLM config from the given LLM config. + + Args: + llm_config (dict): The LLM config. + + Returns: + dict: The first LLM config. + + Raises: + ValueError: If the LLM config is invalid. + """ + llm_config = deepcopy(llm_config) + if "config_list" not in llm_config: + if "model" in llm_config: + return llm_config + raise ValueError("llm_config must be a valid config dictionary.") + + if len(llm_config["config_list"]) == 0: + raise ValueError("Config list must contain at least one config.") + return llm_config["config_list"][0] # type: ignore [no-any-return] + + @export_module("autogen") def config_list_openai_aoai( key_file_path: Optional[str] = ".", diff --git a/autogen/tools/experimental/browser_use/browser_use.py b/autogen/tools/experimental/browser_use/browser_use.py index 17cae57f6b..49d4d8d345 100644 --- a/autogen/tools/experimental/browser_use/browser_use.py +++ b/autogen/tools/experimental/browser_use/browser_use.py @@ -15,7 +15,7 @@ from browser_use import Agent, Controller from browser_use.browser.browser import Browser, BrowserConfig - from .langchain_factory import LangchainFactory + from ....interop.langchain.langchain_chat_model_factory import LangChainChatModelFactory __all__ = ["BrowserUseResult", "BrowserUseTool"] @@ -93,7 +93,7 @@ async def browser_use( # type: ignore[no-any-unimported] browser: Annotated[Browser, Depends(on(browser))], agent_kwargs: Annotated[dict[str, Any], Depends(on(agent_kwargs))], ) -> BrowserUseResult: - llm = LangchainFactory.create_base_chat_model(llm_config) + llm = LangChainChatModelFactory.create_base_chat_model(llm_config) max_steps = agent_kwargs.pop("max_steps", 100) diff --git a/autogen/tools/experimental/crawl4ai/crawl4ai.py b/autogen/tools/experimental/crawl4ai/crawl4ai.py index e300449a59..bccde3f283 100644 --- a/autogen/tools/experimental/crawl4ai/crawl4ai.py +++ b/autogen/tools/experimental/crawl4ai/crawl4ai.py @@ -2,13 +2,13 @@ # # SPDX-License-Identifier: Apache-2.0 -import os from typing import Annotated, Any, Optional from pydantic import BaseModel from ....doc_utils import export_module from ....import_utils import optional_import_block, require_optional_import +from ....interop import LiteLLmConfigFactory from ... import Tool from ...dependency_injection import Depends, on @@ -117,35 +117,6 @@ def _validate_llm_strategy_kwargs(llm_strategy_kwargs: Optional[dict[str, Any]], if check_parameters_error_msg: raise ValueError(check_parameters_error_msg) - # crawl4ai uses LiteLLM under the hood. - @staticmethod - def _get_lite_llm_config(llm_config: dict[str, Any]) -> dict[str, Any]: - if "config_list" not in llm_config: - if "model" in llm_config: - model = llm_config["model"] - api_type = "openai" - lite_llm_config = {"api_token": os.getenv("OPENAI_API_KEY")} - else: - raise ValueError("llm_config must be a valid config dictionary.") - else: - try: - lite_llm_config = llm_config["config_list"][0].copy() - api_key = lite_llm_config.pop("api_key", None) - if api_key: - lite_llm_config["api_token"] = api_key - model = lite_llm_config.pop("model") - api_type = lite_llm_config.pop("api_type", "openai") # type: ignore[assignment] - # litellm uses "gemini" instead of "google" for the api_type - api_type = api_type if api_type != "google" else "gemini" - if api_type == "ollama" and "client_host" in lite_llm_config: - lite_llm_config["api_base"] = lite_llm_config.pop("client_host") - - except (KeyError, TypeError): - raise ValueError("llm_config must be a valid config dictionary.") - - lite_llm_config["provider"] = f"{api_type}/{model}" - return lite_llm_config - @staticmethod def _get_crawl_config( # type: ignore[no-any-unimported] llm_config: dict[str, Any], @@ -153,7 +124,7 @@ def _get_crawl_config( # type: ignore[no-any-unimported] llm_strategy_kwargs: Optional[dict[str, Any]] = None, extraction_model: Optional[type[BaseModel]] = None, ) -> "CrawlerRunConfig": - lite_llm_config = Crawl4AITool._get_lite_llm_config(llm_config) + lite_llm_config = LiteLLmConfigFactory.create_lite_llm_config(llm_config) if llm_strategy_kwargs is None: llm_strategy_kwargs = {} diff --git a/test/interop/litellm/__init__.py b/test/interop/litellm/__init__.py new file mode 100644 index 0000000000..1cce1a249d --- /dev/null +++ b/test/interop/litellm/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors +# +# SPDX-License-Identifier: Apache-2.0 diff --git a/test/interop/litellm/test_litellm_config_factory.py b/test/interop/litellm/test_litellm_config_factory.py new file mode 100644 index 0000000000..9f14b40c74 --- /dev/null +++ b/test/interop/litellm/test_litellm_config_factory.py @@ -0,0 +1,66 @@ +# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors +# +# SPDX-License-Identifier: Apache-2.0 + + +from typing import Any + +import pytest + +from autogen.interop import LiteLLmConfigFactory + + +class TestLiteLLmConfigFactory: + def test_number_of_factories(self) -> None: + assert len(LiteLLmConfigFactory._factories) == 3 + + @pytest.mark.parametrize( + ("config_list", "expected"), + [ + ( + [{"api_type": "openai", "model": "gpt-4o-mini", "api_key": ""}], + {"api_token": "", "provider": "openai/gpt-4o-mini"}, + ), + ( + [ + {"api_type": "deepseek", "model": "deepseek-model", "api_key": "", "base_url": "test-url"}, + ], + {"base_url": "test-url", "api_token": "", "provider": "deepseek/deepseek-model"}, + ), + ( + [ + { + "api_type": "azure", + "model": "gpt-4o-mini", + "api_key": "", + "base_url": "test", + "api_version": "test", + }, + ], + {"base_url": "test", "api_version": "test", "api_token": "", "provider": "azure/gpt-4o-mini"}, + ), + ( + [ + {"api_type": "google", "model": "gemini", "api_key": ""}, + ], + {"api_token": "", "provider": "gemini/gemini"}, + ), + ( + [ + {"api_type": "anthropic", "model": "sonnet", "api_key": ""}, + ], + {"api_token": "", "provider": "anthropic/sonnet"}, + ), + ( + [{"api_type": "ollama", "model": "mistral:7b"}], + {"provider": "ollama/mistral:7b"}, + ), + ( + [{"api_type": "ollama", "model": "mistral:7b", "client_host": "http://127.0.0.1:11434"}], + {"api_base": "http://127.0.0.1:11434", "provider": "ollama/mistral:7b"}, + ), + ], + ) + def test_get_provider_and_api_key(self, config_list: list[dict[str, Any]], expected: dict[str, Any]) -> None: + lite_llm_config = LiteLLmConfigFactory.create_lite_llm_config({"config_list": config_list}) + assert lite_llm_config == expected diff --git a/test/oai/test_utils.py b/test/oai/test_utils.py index 7aa8871d58..2e039acf63 100755 --- a/test/oai/test_utils.py +++ b/test/oai/test_utils.py @@ -20,6 +20,7 @@ from autogen.oai.openai_utils import ( DEFAULT_AZURE_API_VERSION, filter_config, + get_first_llm_config, is_valid_api_key, ) @@ -398,6 +399,44 @@ def test_get_config_list(): assert len(config_list_with_empty_key) == 2, "The config_list should exclude configurations with empty api_keys." +@pytest.mark.parametrize( + ("llm_config", "expected"), + [ + ( + {"model": "gpt-4o-mini", "api_key": ""}, + {"model": "gpt-4o-mini", "api_key": ""}, + ), + ( + {"config_list": [{"model": "gpt-4o-mini", "api_key": ""}]}, + {"model": "gpt-4o-mini", "api_key": ""}, + ), + ( + { + "config_list": [ + {"model": "gpt-4o-mini", "api_key": ""}, + {"model": "gpt-4o", "api_key": ""}, + ] + }, + {"model": "gpt-4o-mini", "api_key": ""}, + ), + ], +) +def test_get_first_llm_config(llm_config: dict[str, Any], expected: dict[str, Any]) -> None: + assert get_first_llm_config(llm_config) == expected + + +@pytest.mark.parametrize( + ("llm_config", "error_message"), + [ + ({}, "llm_config must be a valid config dictionary."), + ({"config_list": []}, "Config list must contain at least one config."), + ], +) +def test_get_first_llm_config_incorrect_config(llm_config: dict[str, Any], error_message: str) -> None: + with pytest.raises(ValueError, match=error_message): + get_first_llm_config(llm_config) + + def test_tags(): config_list = json.loads(JSON_SAMPLE) diff --git a/test/tools/experimental/browser_use/test_langchain_factory.py b/test/tools/experimental/browser_use/test_langchain_factory.py index b6972613ae..76bd55c92b 100644 --- a/test/tools/experimental/browser_use/test_langchain_factory.py +++ b/test/tools/experimental/browser_use/test_langchain_factory.py @@ -11,7 +11,7 @@ with optional_import_block(): from langchain_openai import AzureChatOpenAI, ChatOpenAI - from autogen.tools.experimental.browser_use.langchain_factory import ChatOpenAIFactory, LangchainFactory + from autogen.interop.langchain.langchain_chat_model_factory import ChatOpenAIFactory, LangChainChatModelFactory @skip_on_missing_imports( @@ -22,43 +22,7 @@ class TestLangchainFactory: test_api_key = "test" # pragma: allowlist secret def test_number_of_factories(self) -> None: - assert len(LangchainFactory._factories) == 6 - - @pytest.mark.parametrize( - ("llm_config", "expected"), - [ - ( - {"model": "gpt-4o-mini", "api_key": test_api_key}, - {"model": "gpt-4o-mini", "api_key": test_api_key}, - ), - ( - {"config_list": [{"model": "gpt-4o-mini", "api_key": test_api_key}]}, - {"model": "gpt-4o-mini", "api_key": test_api_key}, - ), - ( - { - "config_list": [ - {"model": "gpt-4o-mini", "api_key": test_api_key}, - {"model": "gpt-4o", "api_key": test_api_key}, - ] - }, - {"model": "gpt-4o-mini", "api_key": test_api_key}, - ), - ], - ) - def test_get_first_llm_config(self, llm_config: dict[str, Any], expected: dict[str, Any]) -> None: - assert LangchainFactory.get_first_llm_config(llm_config) == expected - - @pytest.mark.parametrize( - ("llm_config", "error_message"), - [ - ({}, "llm_config must be a valid config dictionary."), - ({"config_list": []}, "Config list must contain at least one config."), - ], - ) - def test_get_first_llm_config_incorrect_config(self, llm_config: dict[str, Any], error_message: str) -> None: - with pytest.raises(ValueError, match=error_message): - LangchainFactory.get_first_llm_config(llm_config) + assert len(LangChainChatModelFactory._factories) == 6 @pytest.mark.parametrize( ("config_list", "llm_class_name", "base_url"), @@ -127,7 +91,7 @@ def test_create_base_chat_model( # type: ignore[no-any-unimported] llm_class_name: str, base_url: Optional[str], ) -> None: - llm = LangchainFactory.create_base_chat_model(llm_config={"config_list": config_list}) + llm = LangChainChatModelFactory.create_base_chat_model(llm_config={"config_list": config_list}) assert llm.__class__.__name__ == llm_class_name if llm_class_name == "AzureChatOpenAI": assert isinstance(llm, AzureChatOpenAI) @@ -160,7 +124,7 @@ def test_create_base_chat_model_raises_if_mandatory_key_missing( self, config_list: list[dict[str, str]], error_msg: str ) -> None: with pytest.raises(ValueError, match=error_msg): - LangchainFactory.create_base_chat_model(llm_config={"config_list": config_list}) + LangChainChatModelFactory.create_base_chat_model(llm_config={"config_list": config_list}) @skip_on_missing_imports( diff --git a/test/tools/experimental/crawl4ai/test_crawl4ai.py b/test/tools/experimental/crawl4ai/test_crawl4ai.py index e7890833ad..9c21d8df9b 100644 --- a/test/tools/experimental/crawl4ai/test_crawl4ai.py +++ b/test/tools/experimental/crawl4ai/test_crawl4ai.py @@ -44,54 +44,6 @@ async def test_without_llm(self) -> None: result = await tool_without_llm(url="https://docs.ag2.ai/docs/Home") assert isinstance(result, str) - @pytest.mark.parametrize( - "config_list", - [ - [ - {"api_type": "openai", "model": "gpt-4o-mini", "api_key": "test"}, - ], - [ - {"api_type": "deepseek", "model": "deepseek-model", "api_key": "test", "base_url": "test"}, - ], - [ - { - "api_type": "azure", - "model": "gpt-4o-mini", - "api_key": "test", - "base_url": "test", - "api_version": "test", - }, - ], - [ - {"api_type": "google", "model": "gemini", "api_key": "test"}, - ], - [ - {"api_type": "anthropic", "model": "sonnet", "api_key": "test"}, - ], - [{"api_type": "ollama", "model": "mistral:7b"}], - [{"api_type": "ollama", "model": "mistral:7b", "client_host": "http://127.0.0.1:11434"}], - ], - ) - def test_get_provider_and_api_key(self, config_list: list[dict[str, Any]]) -> None: - lite_llm_config = Crawl4AITool._get_lite_llm_config({"config_list": config_list}) - - api_type = config_list[0]["api_type"] - model = config_list[0]["model"] - api_type = api_type if api_type != "google" else "gemini" - provider = f"{api_type}/{model}" - - if api_type == "ollama": - if "client_host" in config_list[0]: - assert lite_llm_config == {"provider": provider, "api_base": config_list[0]["client_host"]} - else: - assert lite_llm_config == {"provider": provider} - else: - assert all(key in lite_llm_config for key in ["provider", "api_token"]) - assert lite_llm_config["provider"] == provider - - if api_type == "deepseek" or api_type == "azure": - assert "base_url" in lite_llm_config - @pytest.mark.parametrize( "use_extraction_model", [ @@ -99,7 +51,11 @@ def test_get_provider_and_api_key(self, config_list: list[dict[str, Any]]) -> No True, ], ) - def test_get_crawl_config(self, mock_credentials: Credentials, use_extraction_model: bool) -> None: + def test_get_crawl_config( + self, mock_credentials: Credentials, use_extraction_model: bool, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + class Product(BaseModel): name: str price: str