diff --git a/autogen/agentchat/contrib/swarm_agent.py b/autogen/agentchat/contrib/swarm_agent.py index c525c53db3..6ad536ae07 100644 --- a/autogen/agentchat/contrib/swarm_agent.py +++ b/autogen/agentchat/contrib/swarm_agent.py @@ -27,6 +27,8 @@ # e.g. def my_function(context_variables: Dict[str, Any], my_other_parameters: Any) -> Any: __CONTEXT_VARIABLES_PARAM_NAME__ = "context_variables" +__TOOL_EXECUTOR_NAME__ = "Tool_Execution" + class AfterWorkOption(Enum): TERMINATE = "TERMINATE" @@ -36,6 +38,14 @@ class AfterWorkOption(Enum): @dataclass class AFTER_WORK: + """Handles the next step in the conversation when an agent doesn't suggest a tool call or a handoff + + Args: + agent: The agent to hand off to or the after work option. Can be a SwarmAgent, a string name of a SwarmAgent, an AfterWorkOption, or a Callable. + The Callable signature is: + def my_after_work_func(last_speaker: SwarmAgent, messages: List[Dict[str, Any]], groupchat: GroupChat) -> Union[AfterWorkOption, SwarmAgent, str]: + """ + agent: Union[AfterWorkOption, "SwarmAgent", str, Callable] def __post_init__(self): @@ -45,8 +55,20 @@ def __post_init__(self): @dataclass class ON_CONDITION: + """Defines a condition for transitioning to another agent or nested chats + + Args: + target: The agent to hand off to or the nested chat configuration. Can be a SwarmAgent or a Dict. + If a Dict, it should follow the convention of the nested chat configuration, with the exception of a carryover configuration which is unique to Swarms. + Swarm Nested chat documentation: https://ag2ai.github.io/ag2/docs/topics/swarm#registering-handoffs-to-a-nested-chat + condition: The condition for transitioning to the target agent, evaluated by the LLM to determine whether to call the underlying function/tool which does the transition. + available: Optional condition to determine if this ON_CONDITION is available. Can be a Callable or a string. + If a string, it will look up the value of the context variable with that name, which should be a bool. + """ + target: Union["SwarmAgent", Dict[str, Any]] = None condition: str = "" + available: Optional[Union[Callable, str]] = None def __post_init__(self): # Ensure valid types @@ -58,9 +80,21 @@ def __post_init__(self): # Ensure they have a condition assert isinstance(self.condition, str) and self.condition.strip(), "'condition' must be a non-empty string" + if self.available is not None: + assert isinstance(self.available, (Callable, str)), "'available' must be a callable or a string" + @dataclass class UPDATE_SYSTEM_MESSAGE: + """Update the agent's system message before they reply + + Args: + update_function: The string or function to update the agent's system message. Can be a string or a Callable. + If a string, it will be used as a template and substitute the context variables. + If a Callable, it should have the signature: + def my_update_function(agent: ConversableAgent, messages: List[Dict[str, Any]]) -> str + """ + update_function: Union[Callable, str] def __post_init__(self): @@ -107,9 +141,9 @@ def initiate_swarm_chat( - REVERT_TO_USER : Revert to the user agent if a user agent is provided. If not provided, terminate the conversation. - STAY : Stay with the last speaker. - Callable: A custom function that takes the current agent, messages, groupchat, and context_variables as arguments and returns the next agent. The function should return None to terminate. + Callable: A custom function that takes the current agent, messages, and groupchat as arguments and returns an AfterWorkOption or a SwarmAgent (by reference or string name). ```python - def custom_afterwork_func(last_speaker: SwarmAgent, messages: List[Dict[str, Any]], groupchat: GroupChat, context_variables: Optional[Dict[str, Any]]) -> Optional[SwarmAgent]: + def custom_afterwork_func(last_speaker: SwarmAgent, messages: List[Dict[str, Any]], groupchat: GroupChat) -> Union[AfterWorkOption, SwarmAgent, str]: ``` Returns: ChatResult: Conversations chat history. @@ -129,7 +163,7 @@ def custom_afterwork_func(last_speaker: SwarmAgent, messages: List[Dict[str, Any messages = [{"role": "user", "content": messages}] tool_execution = SwarmAgent( - name="Tool_Execution", + name=__TOOL_EXECUTOR_NAME__, system_message="Tool Execution", ) tool_execution._set_to_tool_execution() @@ -138,11 +172,6 @@ def custom_afterwork_func(last_speaker: SwarmAgent, messages: List[Dict[str, Any for agent in agents: tool_execution._function_map.update(agent._function_map) - # Point all SwarmAgent's context variables to this function's context_variables - # providing a single (shared) context across all SwarmAgents in the swarm - for agent in agents + [tool_execution]: - agent._context_variables = context_variables - INIT_AGENT_USED = False def swarm_transition(last_speaker: SwarmAgent, groupchat: GroupChat): @@ -191,34 +220,40 @@ def determine_next_agent(last_speaker: SwarmAgent, groupchat: GroupChat): if (user_agent and last_speaker == user_agent) or groupchat.messages[-1]["role"] == "tool": return last_swarm_speaker - # No agent selected via hand-offs (tool calls) - # Assume the work is Done - # override if agent-level after_work is defined, else use the global after_work - tmp_after_work = last_swarm_speaker.after_work if last_swarm_speaker.after_work is not None else after_work - if isinstance(tmp_after_work, AFTER_WORK): - tmp_after_work = tmp_after_work.agent - - if isinstance(tmp_after_work, SwarmAgent): - return tmp_after_work - elif isinstance(tmp_after_work, AfterWorkOption): - if tmp_after_work == AfterWorkOption.TERMINATE or ( - user_agent is None and tmp_after_work == AfterWorkOption.REVERT_TO_USER - ): + # Resolve after_work condition (agent-level overrides global) + after_work_condition = ( + last_swarm_speaker.after_work if last_swarm_speaker.after_work is not None else after_work + ) + if isinstance(after_work_condition, AFTER_WORK): + after_work_condition = after_work_condition.agent + + # Evaluate callable after_work + if isinstance(after_work_condition, Callable): + after_work_condition = after_work_condition(last_speaker, groupchat.messages, groupchat) + + if isinstance(after_work_condition, str): # Agent name in a string + if after_work_condition in swarm_agent_names: + return groupchat.agent_by_name(name=after_work_condition) + else: + raise ValueError(f"Invalid agent name in after_work: {after_work_condition}") + elif isinstance(after_work_condition, SwarmAgent): + return after_work_condition + elif isinstance(after_work_condition, AfterWorkOption): + if after_work_condition == AfterWorkOption.TERMINATE: return None - elif tmp_after_work == AfterWorkOption.REVERT_TO_USER: - return user_agent - elif tmp_after_work == AfterWorkOption.STAY: + elif after_work_condition == AfterWorkOption.REVERT_TO_USER: + return None if user_agent is None else user_agent + elif after_work_condition == AfterWorkOption.STAY: return last_speaker - elif isinstance(tmp_after_work, Callable): - return tmp_after_work(last_speaker, groupchat.messages, groupchat, context_variables) else: - raise ValueError("Invalid After Work condition") + raise ValueError("Invalid After Work condition or return value from callable") def create_nested_chats(agent: SwarmAgent, nested_chat_agents: List[SwarmAgent]): """Create nested chat agents and register nested chats""" for i, nested_chat_handoff in enumerate(agent._nested_chat_handoffs): nested_chats: Dict[str, Any] = nested_chat_handoff["nested_chats"] condition = nested_chat_handoff["condition"] + available = nested_chat_handoff["available"] # Create a nested chat agent specifically for this nested chat nested_chat_agent = SwarmAgent(name=f"nested_chat_{agent.name}_{i + 1}") @@ -239,7 +274,7 @@ def create_nested_chats(agent: SwarmAgent, nested_chat_agents: List[SwarmAgent]) nested_chat_agents.append(nested_chat_agent) # Nested chat is triggered through an agent transfer to this nested chat agent - agent.register_hand_off(ON_CONDITION(nested_chat_agent, condition)) + agent.register_hand_off(ON_CONDITION(nested_chat_agent, condition, available)) nested_chat_agents = [] for agent in agents: @@ -249,6 +284,10 @@ def create_nested_chats(agent: SwarmAgent, nested_chat_agents: List[SwarmAgent]) for agent in agents + nested_chat_agents: tool_execution._function_map.update(agent._function_map) + # Add conditional functions to the tool_execution agent + for func_name, (func, on_condition) in agent._conditional_functions.items(): + tool_execution._function_map[func_name] = func + swarm_agent_names = [agent.name for agent in agents + nested_chat_agents] # If there's only one message and there's no identified swarm agent @@ -270,6 +309,11 @@ def create_nested_chats(agent: SwarmAgent, nested_chat_agents: List[SwarmAgent]) manager = GroupChatManager(groupchat) clear_history = True + # Point all SwarmAgent's context variables to this function's context_variables + # providing a single (shared) context across all SwarmAgents in the swarm + for agent in agents + [tool_execution] + [manager]: + agent._context_variables = context_variables + if len(messages) > 1: last_agent, last_message = manager.resume(messages=messages) clear_history = False @@ -337,6 +381,7 @@ class SwarmAgent(ConversableAgent): Additional args: functions (List[Callable]): A list of functions to register with the agent. + update_agent_state_before_reply (List[Callable]): A list of functions, including UPDATE_SYSTEM_MESSAGEs, called to update the agent before it replies. """ def __init__( @@ -387,6 +432,13 @@ def __init__( self.register_update_agent_state_before_reply(update_agent_state_before_reply) + # Store conditional functions (and their ON_CONDITION instances) to add/remove later when transitioning to this agent + self._conditional_functions = {} + + # Register the hook to update agent state (except tool executor) + if name != __TOOL_EXECUTOR_NAME__: + self.register_hook("update_agent_state", self._update_conditional_functions) + def register_update_agent_state_before_reply(self, functions: Optional[Union[List[Callable], Callable]]): """ Register functions that will be called when the agent is selected and before it speaks. @@ -490,16 +542,50 @@ def transfer_to_agent() -> "SwarmAgent": return transfer_to_agent transfer_func = make_transfer_function(transit) - self.add_single_function(transfer_func, f"transfer_to_{transit.target.name}", transit.condition) + + # Store function to add/remove later based on it being 'available' + # Function names are made unique and allow multiple ON_CONDITIONS to the same agent + base_func_name = f"transfer_{self.name}_to_{transit.target.name}" + func_name = base_func_name + count = 2 + while func_name in self._conditional_functions: + func_name = f"{base_func_name}_{count}" + count += 1 + + # Store function to add/remove later based on it being 'available' + self._conditional_functions[func_name] = (transfer_func, transit) elif isinstance(transit.target, Dict): # Transition to a nested chat # We will store them here and establish them in the initiate_swarm_chat - self._nested_chat_handoffs.append({"nested_chats": transit.target, "condition": transit.condition}) + self._nested_chat_handoffs.append( + {"nested_chats": transit.target, "condition": transit.condition, "available": transit.available} + ) else: raise ValueError("Invalid hand off condition, must be either ON_CONDITION or AFTER_WORK") + @staticmethod + def _update_conditional_functions(agent: Agent, messages: Optional[List[Dict]] = None) -> None: + """Updates the agent's functions based on the ON_CONDITION's available condition.""" + for func_name, (func, on_condition) in agent._conditional_functions.items(): + is_available = True + + if on_condition.available is not None: + if isinstance(on_condition.available, Callable): + is_available = on_condition.available(agent, next(iter(agent.chat_messages.values()))) + elif isinstance(on_condition.available, str): + is_available = agent.get_context(on_condition.available) or False + + if is_available: + if func_name not in agent._function_map: + agent.add_single_function(func, func_name, on_condition.condition) + else: + # Remove function using the stored name + if func_name in agent._function_map: + agent.update_tool_signature(func_name, is_remove=True) + del agent._function_map[func_name] + def generate_swarm_tool_reply( self, messages: Optional[List[Dict]] = None, @@ -621,6 +707,7 @@ def process_nested_chat_carryover( recipient: ConversableAgent, messages: List[Dict[str, Any]], sender: ConversableAgent, + config: Any, trim_n_messages: int = 0, ) -> None: """Process carryover messages for a nested chat (typically for the first chat of a swarm) @@ -666,7 +753,12 @@ def concat_carryover(chat_message: str, carryover_message: Union[str, List[Dict[ carryover_summary_method = carryover_config["summary_method"] carryover_summary_args = carryover_config.get("summary_args") or {} - chat_message = chat.get("message", "") + chat_message = "" + message = chat.get("message") + + # If the message is a callable, run it and get the result + if message: + chat_message = message(recipient, messages, sender, config) if callable(message) else message # deep copy and trim the latest messages content_messages = copy.deepcopy(messages) @@ -725,15 +817,24 @@ def _summary_from_nested_chats( Returns: Tuple[bool, str]: A tuple where the first element indicates the completion of the chat, and the second element contains the summary of the last chat if any chats were initiated. """ - # Carryover configuration allowed on the first chat in the queue only, trim the last two messages specifically for swarm nested chat carryover as these are the messages for the transition to the nested chat agent + restore_chat_queue_message = False if len(chat_queue) > 0 and "carryover_config" in chat_queue[0]: - SwarmAgent.process_nested_chat_carryover(chat_queue[0], recipient, messages, sender, 2) + if "message" in chat_queue[0]: + # As we're updating the message in the nested chat queue, we need to restore it after finishing this nested chat. + restore_chat_queue_message = True + original_chat_queue_message = chat_queue[0]["message"] + SwarmAgent.process_nested_chat_carryover(chat_queue[0], recipient, messages, sender, config, 2) chat_to_run = ConversableAgent._get_chats_to_run(chat_queue, recipient, messages, sender, config) if not chat_to_run: return True, None res = sender.initiate_chats(chat_to_run) + + # We need to restore the chat queue message if it has been modified so that it will be the original message for subsequent uses + if restore_chat_queue_message: + chat_queue[0]["message"] = original_chat_queue_message + return True, res[-1].summary diff --git a/autogen/interop/crewai/crewai.py b/autogen/interop/crewai/crewai.py index c391b22888..97abda9e77 100644 --- a/autogen/interop/crewai/crewai.py +++ b/autogen/interop/crewai/crewai.py @@ -18,9 +18,36 @@ def _sanitize_name(s: str) -> str: class CrewAIInteroperability(Interoperable): - def convert_tool(self, tool: Any) -> Tool: + """ + A class implementing the `Interoperable` protocol for converting CrewAI tools + to a general `Tool` format. + + This class takes a `CrewAITool` and converts it into a standard `Tool` object. + """ + + def convert_tool(self, tool: Any, **kwargs: Any) -> Tool: + """ + Converts a given CrewAI tool into a general `Tool` format. + + This method ensures that the provided tool is a valid `CrewAITool`, sanitizes + the tool's name, processes its description, and prepares a function to interact + with the tool's arguments. It then returns a standardized `Tool` object. + + Args: + tool (Any): The tool to convert, expected to be an instance of `CrewAITool`. + **kwargs (Any): Additional arguments, which are not supported by this method. + + Returns: + Tool: A standardized `Tool` object converted from the CrewAI tool. + + Raises: + ValueError: If the provided tool is not an instance of `CrewAITool`, or if + any additional arguments are passed. + """ if not isinstance(tool, CrewAITool): raise ValueError(f"Expected an instance of `crewai.tools.BaseTool`, got {type(tool)}") + if kwargs: + raise ValueError(f"The CrewAIInteroperability does not support any additional arguments, got {kwargs}") # needed for type checking crewai_tool: CrewAITool = tool # type: ignore[no-any-unimported] diff --git a/autogen/interop/interoperability.py b/autogen/interop/interoperability.py index 2c9ed1ffbf..27df3cd9c7 100644 --- a/autogen/interop/interoperability.py +++ b/autogen/interop/interoperability.py @@ -11,28 +11,83 @@ class Interoperability: + """ + A class to handle interoperability between different tool types. + + This class allows the conversion of tools to various interoperability classes and provides functionality + for retrieving and registering interoperability classes. + """ + _interoperability_classes: Dict[str, Type[Interoperable]] = get_all_interoperability_classes() def __init__(self) -> None: + """ + Initializes an instance of the Interoperability class. + + This constructor does not perform any specific actions as the class is primarily used for its class + methods to manage interoperability classes. + """ pass - def convert_tool(self, *, tool: Any, type: str) -> Tool: + def convert_tool(self, *, tool: Any, type: str, **kwargs: Any) -> Tool: + """ + Converts a given tool to an instance of a specified interoperability type. + + Args: + tool (Any): The tool object to be converted. + type (str): The type of interoperability to convert the tool to. + **kwargs (Any): Additional arguments to be passed during conversion. + + Returns: + Tool: The converted tool. + + Raises: + ValueError: If the interoperability class for the provided type is not found. + """ interop_cls = self.get_interoperability_class(type) interop = interop_cls() - return interop.convert_tool(tool) + return interop.convert_tool(tool, **kwargs) @classmethod def get_interoperability_class(cls, type: str) -> Type[Interoperable]: + """ + Retrieves the interoperability class corresponding to the specified type. + + Args: + type (str): The type of the interoperability class to retrieve. + + Returns: + Type[Interoperable]: The interoperability class type. + + Raises: + ValueError: If no interoperability class is found for the provided type. + """ if type not in cls._interoperability_classes: raise ValueError(f"Interoperability class {type} not found") return cls._interoperability_classes[type] @classmethod def supported_types(cls) -> List[str]: + """ + Returns a sorted list of all supported interoperability types. + + Returns: + List[str]: A sorted list of strings representing the supported interoperability types. + """ return sorted(cls._interoperability_classes.keys()) @classmethod def register_interoperability_class(cls, name: str, interoperability_class: Type[Interoperable]) -> None: + """ + Registers a new interoperability class with the given name. + + Args: + name (str): The name to associate with the interoperability class. + interoperability_class (Type[Interoperable]): The class implementing the Interoperable protocol. + + Raises: + ValueError: If the provided class does not implement the Interoperable protocol. + """ if not issubclass(interoperability_class, Interoperable): raise ValueError( f"Expected a class implementing `Interoperable` protocol, got {type(interoperability_class)}" diff --git a/autogen/interop/interoperable.py b/autogen/interop/interoperable.py index dfe0f82500..75aefaaf25 100644 --- a/autogen/interop/interoperable.py +++ b/autogen/interop/interoperable.py @@ -11,4 +11,24 @@ @runtime_checkable class Interoperable(Protocol): - def convert_tool(self, tool: Any) -> Tool: ... + """ + A Protocol defining the interoperability interface for tool conversion. + + This protocol ensures that any class implementing it provides the method + `convert_tool` to convert a given tool into a desired format or type. + """ + + def convert_tool(self, tool: Any, **kwargs: Any) -> Tool: + """ + Converts a given tool to a desired format or type. + + This method should be implemented by any class adhering to the `Interoperable` protocol. + + Args: + tool (Any): The tool object to be converted. + **kwargs (Any): Additional parameters to pass during the conversion process. + + Returns: + Tool: The converted tool in the desired format or type. + """ + ... diff --git a/autogen/interop/langchain/langchain.py b/autogen/interop/langchain/langchain.py index b3f4713c63..925e00431a 100644 --- a/autogen/interop/langchain/langchain.py +++ b/autogen/interop/langchain/langchain.py @@ -13,9 +13,38 @@ class LangchainInteroperability(Interoperable): - def convert_tool(self, tool: Any) -> Tool: + """ + A class implementing the `Interoperable` protocol for converting Langchain tools + into a general `Tool` format. + + This class takes a `LangchainTool` and converts it into a standard `Tool` object, + ensuring compatibility between Langchain tools and other systems that expect + the `Tool` format. + """ + + def convert_tool(self, tool: Any, **kwargs: Any) -> Tool: + """ + Converts a given Langchain tool into a general `Tool` format. + + This method verifies that the provided tool is a valid `LangchainTool`, + processes the tool's input and description, and returns a standardized + `Tool` object. + + Args: + tool (Any): The tool to convert, expected to be an instance of `LangchainTool`. + **kwargs (Any): Additional arguments, which are not supported by this method. + + Returns: + Tool: A standardized `Tool` object converted from the Langchain tool. + + Raises: + ValueError: If the provided tool is not an instance of `LangchainTool`, or if + any additional arguments are passed. + """ if not isinstance(tool, LangchainTool): raise ValueError(f"Expected an instance of `langchain_core.tools.BaseTool`, got {type(tool)}") + if kwargs: + raise ValueError(f"The LangchainInteroperability does not support any additional arguments, got {kwargs}") # needed for type checking langchain_tool: LangchainTool = tool # type: ignore[no-any-unimported] diff --git a/autogen/interop/pydantic_ai/pydantic_ai.py b/autogen/interop/pydantic_ai/pydantic_ai.py index 27a146704c..b170ccf501 100644 --- a/autogen/interop/pydantic_ai/pydantic_ai.py +++ b/autogen/interop/pydantic_ai/pydantic_ai.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 +import warnings from functools import wraps from inspect import signature from typing import Any, Callable, Optional @@ -17,11 +18,38 @@ class PydanticAIInteroperability(Interoperable): + """ + A class implementing the `Interoperable` protocol for converting Pydantic AI tools + into a general `Tool` format. + + This class takes a `PydanticAITool` and converts it into a standard `Tool` object, + ensuring compatibility between Pydantic AI tools and other systems that expect + the `Tool` format. It also provides a mechanism for injecting context parameters + into the tool's function. + """ + @staticmethod def inject_params( # type: ignore[no-any-unimported] ctx: Optional[RunContext[Any]], tool: PydanticAITool, ) -> Callable[..., Any]: + """ + Wraps the tool's function to inject context parameters and handle retries. + + This method ensures that context parameters are properly passed to the tool + when invoked and that retries are managed according to the tool's settings. + + Args: + ctx (Optional[RunContext[Any]]): The run context, which may include dependencies + and retry information. + tool (PydanticAITool): The Pydantic AI tool whose function is to be wrapped. + + Returns: + Callable[..., Any]: A wrapped function that includes context injection and retry handling. + + Raises: + ValueError: If the tool fails after the maximum number of retries. + """ max_retries = tool.max_retries if tool.max_retries is not None else 1 f = tool.function @@ -54,14 +82,42 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: return wrapper - def convert_tool(self, tool: Any, deps: Any = None) -> AG2PydanticAITool: + def convert_tool(self, tool: Any, deps: Any = None, **kwargs: Any) -> AG2PydanticAITool: + """ + Converts a given Pydantic AI tool into a general `Tool` format. + + This method verifies that the provided tool is a valid `PydanticAITool`, + handles context dependencies if necessary, and returns a standardized `Tool` object. + + Args: + tool (Any): The tool to convert, expected to be an instance of `PydanticAITool`. + deps (Any, optional): The dependencies to inject into the context, required if + the tool takes a context. Defaults to None. + **kwargs (Any): Additional arguments that are not used in this method. + + Returns: + AG2PydanticAITool: A standardized `Tool` object converted from the Pydantic AI tool. + + Raises: + ValueError: If the provided tool is not an instance of `PydanticAITool`, or if + dependencies are missing for tools that require a context. + UserWarning: If the `deps` argument is provided for a tool that does not take a context. + """ if not isinstance(tool, PydanticAITool): raise ValueError(f"Expected an instance of `pydantic_ai.tools.Tool`, got {type(tool)}") # needed for type checking pydantic_ai_tool: PydanticAITool = tool # type: ignore[no-any-unimported] - if deps is not None: + if tool.takes_ctx and deps is None: + raise ValueError("If the tool takes a context, the `deps` argument must be provided") + if not tool.takes_ctx and deps is not None: + warnings.warn( + "The `deps` argument is provided but will be ignored because the tool does not take a context.", + UserWarning, + ) + + if tool.takes_ctx: ctx = RunContext( deps=deps, retry=0, diff --git a/autogen/tools/pydantic_ai_tool.py b/autogen/tools/pydantic_ai_tool.py index 5c999eba3a..a106cd4b70 100644 --- a/autogen/tools/pydantic_ai_tool.py +++ b/autogen/tools/pydantic_ai_tool.py @@ -12,9 +12,33 @@ class PydanticAITool(Tool): + """ + A class representing a Pydantic AI Tool that extends the general Tool functionality + with additional functionality specific to Pydantic AI tools. + + This class inherits from the Tool class and adds functionality for registering + tools with a ConversableAgent, along with providing additional schema information + specific to Pydantic AI tools, such as parameters and function signatures. + + Attributes: + parameters_json_schema (Dict[str, Any]): A schema describing the parameters + that the tool's function expects. + """ + def __init__( self, name: str, description: str, func: Callable[..., Any], parameters_json_schema: Dict[str, Any] ) -> None: + """ + Initializes a PydanticAITool object with the provided name, description, + function, and parameter schema. + + Args: + name (str): The name of the tool. + description (str): A description of what the tool does. + func (Callable[..., Any]): The function that is executed when the tool is called. + parameters_json_schema (Dict[str, Any]): A schema describing the parameters + that the function accepts. + """ super().__init__(name, description, func) self._func_schema = { "type": "function", @@ -26,4 +50,13 @@ def __init__( } def register_for_llm(self, agent: ConversableAgent) -> None: + """ + Registers the tool with the ConversableAgent for use with a language model (LLM). + + This method updates the agent's tool signature to include the function schema, + allowing the agent to invoke the tool correctly during interactions with the LLM. + + Args: + agent (ConversableAgent): The agent with which the tool will be registered. + """ agent.update_tool_signature(self._func_schema, is_remove=False) diff --git a/autogen/tools/tool.py b/autogen/tools/tool.py index b01367235c..c0e615f37c 100644 --- a/autogen/tools/tool.py +++ b/autogen/tools/tool.py @@ -16,6 +16,18 @@ class Tool: + """ + A class representing a Tool that can be used by an agent for various tasks. + + This class encapsulates a tool with a name, description, and an executable function. + The tool can be registered with a ConversableAgent for use either with an LLM or for direct execution. + + Attributes: + name (str): The name of the tool. + description (str): A brief description of the tool's purpose or function. + func (Callable[..., Any]): The function to be executed when the tool is called. + """ + def __init__(self, name: str, description: str, func: Callable[..., Any]) -> None: """Create a new Tool object. @@ -41,7 +53,25 @@ def func(self) -> Callable[..., Any]: return self._func def register_for_llm(self, agent: ConversableAgent) -> None: + """ + Registers the tool for use with a ConversableAgent's language model (LLM). + + This method registers the tool so that it can be invoked by the agent during + interactions with the language model. + + Args: + agent (ConversableAgent): The agent to which the tool will be registered. + """ agent.register_for_llm(name=self._name, description=self._description)(self._func) def register_for_execution(self, agent: ConversableAgent) -> None: + """ + Registers the tool for direct execution by a ConversableAgent. + + This method registers the tool so that it can be executed by the agent, + typically outside of the context of an LLM interaction. + + Args: + agent (ConversableAgent): The agent to which the tool will be registered. + """ agent.register_for_execution(name=self._name)(self._func) diff --git a/notebook/tools_crewai_tools_integration.ipynb b/notebook/tools_crewai_tools_integration.ipynb deleted file mode 100644 index 07bdd0e341..0000000000 --- a/notebook/tools_crewai_tools_integration.ipynb +++ /dev/null @@ -1,190 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Integrating CrewAI Tools with the AG2 Framework\n", - "\n", - "In this tutorial, we demonstrate how to integrate [CrewAI Tools](https://github.com/crewAIInc/crewAI-tools/tree/main) into the AG2 framework. This process enables smooth interoperability between the two systems, allowing developers to leverage CrewAI's powerful tools within AG2's flexible agent-based architecture. By the end of this guide, you will understand how to configure agents, convert CrewAI tools for use in AG2, and validate the integration with a practical example.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Installation\n", - "Install the required packages for integrating CrewAI tools into the AG2 framework.\n", - "This ensures all dependencies for both frameworks are installed.\n", - "\n", - "```bash\n", - "pip install ag2[interop-crewai]\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Imports\n", - "\n", - "Import necessary modules and tools.\n", - "- `os` is used to access environment variables.\n", - "- `Path` helps in handling file paths.\n", - "- `TemporaryDirectory` is used for creating a temporary workspace.\n", - "- `FileWriterTool` and `ScrapeWebsiteTool` are the CrewAI tools we will integrate.\n", - "- `AssistantAgent` and `UserProxyAgent` are core AG2 classes.\n", - "- `CrewAIInteroperability` facilitates the interoperability between AG2 and CrewAI." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "from pathlib import Path\n", - "from tempfile import TemporaryDirectory\n", - "\n", - "from crewai_tools import FileWriterTool, ScrapeWebsiteTool\n", - "\n", - "from autogen import AssistantAgent, UserProxyAgent\n", - "from autogen.interop.crewai import CrewAIInteroperability" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Agent Configuration\n", - "\n", - "Configure the agents for the interaction.\n", - "- `config_list` defines the LLM configurations, including the model and API key.\n", - "- `UserProxyAgent` simulates user inputs without requiring actual human interaction (set to `NEVER`).\n", - "- `AssistantAgent` represents the AI agent, configured with the LLM settings." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "config_list = [{\"model\": \"gpt-4o-mini\", \"api_key\": os.environ[\"OPENAI_API_KEY\"]}]\n", - "user_proxy = UserProxyAgent(\n", - " name=\"User\",\n", - " human_input_mode=\"NEVER\",\n", - ")\n", - "\n", - "chatbot = AssistantAgent(\n", - " name=\"chatbot\",\n", - " llm_config={\"config_list\": config_list},\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Tool Integration\n", - "\n", - "Initialize and register the CrewAI tool with AG2.\n", - "- `crewai_tool` is an instance of the `FileWriterTool` from CrewAI.\n", - "- `CrewAIInteroperability` converts the CrewAI tool to make it usable in AG2.\n", - "- `register_for_execution` and `register_for_llm` allow the tool to work with the UserProxyAgent and AssistantAgent." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "crewai_tool = FileWriterTool()\n", - "crewai_interop = CrewAIInteroperability()\n", - "ag2_tool = crewai_interop.convert_tool(crewai_tool)\n", - "\n", - "ag2_tool.register_for_execution(user_proxy)\n", - "ag2_tool.register_for_llm(chatbot)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## File creation\n", - "\n", - "Demonstrate the integration by writing to a file using the converted CrewAI tool.\n", - "- A temporary directory is created to simulate a file operation environment.\n", - "- The `message` instructs the chatbot to use the tool to write a specific string into a file.\n", - "- `user_proxy.initiate_chat` starts the interaction, with the chatbot processing the request and using the tool.\n", - "- Finally, the output file is verified to ensure the integration works correctly." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "with TemporaryDirectory() as tmpdirname:\n", - " filename = \"tool_result.txt\"\n", - " message = f\"\"\"Write 'Easy Migration :)' into {filename}.\n", - "Use {tmpdirname} dir.\n", - "\"\"\"\n", - "\n", - " user_proxy.initiate_chat(recipient=chatbot, message=message, max_turns=2)\n", - "\n", - " assert Path(tmpdirname, filename).read_text() == \"Easy Migration :)\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "crewai_scrape_tool = ScrapeWebsiteTool()\n", - "ag2_tool = crewai_interop.convert_tool(crewai_scrape_tool)\n", - "\n", - "ag2_tool.register_for_execution(user_proxy)\n", - "ag2_tool.register_for_llm(chatbot)\n", - "\n", - "message = \"Scape the website https://ag2.ai/\"\n", - "\n", - "chat_result = user_proxy.initiate_chat(recipient=chatbot, message=message, max_turns=2)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(chat_result.summary)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.16" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/notebook/tools_interoperability.ipynb b/notebook/tools_interoperability.ipynb new file mode 100644 index 0000000000..00469be0d4 --- /dev/null +++ b/notebook/tools_interoperability.ipynb @@ -0,0 +1,418 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Cross-Framework LLM Tool Integration with AG2\n", + "\n", + "In this tutorial, we demonstrate how to integrate LLM tools from various frameworks—including [LangChain Tools](https://python.langchain.com/v0.1/docs/modules/tools), [CrewAI Tools](https://github.com/crewAIInc/crewAI-tools/tree/main), and [PydanticAI Tools](https://ai.pydantic.dev/tools/) into the AG2 framework. This process enables smooth interoperability between these systems, allowing developers to leverage the unique capabilities of each toolset within AG2's flexible agent-based architecture. By the end of this guide, you will understand how to configure agents, adapt these tools for use in AG2, and validate the integration through practical examples." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## LangChain Tools Integration\n", + "\n", + "LangChain is a popular framework that offers a wide range of tools to work with LLMs. LangChain has already implemented a variety of tools that can be easily integrated into AG2. You can explore the available tools in the [LangChain Community Tools](https://github.com/langchain-ai/langchain/tree/master/libs/community/langchain_community/tools) folder. These tools, such as those for querying APIs, web scraping, and text generation, can be quickly incorporated into AG2, providing powerful functionality for your agents.\n", + "\n", + "### Installation\n", + "To integrate LangChain tools into the AG2 framework, install the required dependencies:\n", + "\n", + "```bash\n", + "pip install ag2[interop-langchain]\n", + "```\n", + "\n", + "Additionally, this notebook uses LangChain's [Wikipedia Tool](https://api.python.langchain.com/en/latest/tools/langchain_community.tools.wikipedia.tool.WikipediaQueryRun.html), which requires the `wikipedia` package. Install it with:\n", + "\n", + "```bash\n", + "pip install wikipedia\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Imports\n", + "\n", + "Import necessary modules and tools.\n", + "- `WikipediaQueryRun` and `WikipediaAPIWrapper`: Tools for querying Wikipedia.\n", + "- `AssistantAgent` and `UserProxyAgent`: Agents that facilitate communication in the AG2 framework.\n", + "- `Interoperability`: This module acts as a bridge, making it easier to integrate LangChain tools with AG2’s architecture." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from langchain_community.tools import WikipediaQueryRun\n", + "from langchain_community.utilities import WikipediaAPIWrapper\n", + "\n", + "from autogen import AssistantAgent, UserProxyAgent\n", + "from autogen.interop import Interoperability" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Agent Configuration\n", + "\n", + "Configure the agents for the interaction.\n", + "- `config_list` defines the LLM configurations, including the model and API key.\n", + "- `UserProxyAgent` simulates user inputs without requiring actual human interaction (set to `NEVER`).\n", + "- `AssistantAgent` represents the AI agent, configured with the LLM settings." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "config_list = [{\"model\": \"gpt-4o\", \"api_key\": os.environ[\"OPENAI_API_KEY\"]}]\n", + "user_proxy = UserProxyAgent(\n", + " name=\"User\",\n", + " human_input_mode=\"NEVER\",\n", + ")\n", + "\n", + "chatbot = AssistantAgent(\n", + " name=\"chatbot\",\n", + " llm_config={\"config_list\": config_list},\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Tool Integration\n", + "\n", + "- Initialize and register the LangChain tool with AG2.\n", + "- `WikipediaAPIWrapper`: Configured to fetch the top 1 result from Wikipedia with a maximum of 1000 characters per document.\n", + "- `WikipediaQueryRun`: A LangChain tool that executes Wikipedia queries.\n", + "- `LangchainInteroperability`: Converts the LangChain tool into a format compatible with the AG2 framework.\n", + "- `ag2_tool.register_for_execution(user_proxy)`: Registers the tool for use by the user_proxy agent.\n", + "- `ag2_tool.register_for_llm(chatbot)`: Registers the tool for integration with the chatbot agent.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "api_wrapper = WikipediaAPIWrapper(top_k_results=1, doc_content_chars_max=1000)\n", + "langchain_tool = WikipediaQueryRun(api_wrapper=api_wrapper)\n", + "\n", + "interop = Interoperability()\n", + "ag2_tool = interop.convert_tool(tool=langchain_tool, type=\"langchain\")\n", + "\n", + "ag2_tool.register_for_execution(user_proxy)\n", + "ag2_tool.register_for_llm(chatbot)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "message = \"Tell me about the history of the United States\"\n", + "user_proxy.initiate_chat(recipient=chatbot, message=message, max_turns=2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## CrewAI Tools Integration\n", + "\n", + "CrewAI provides a variety of powerful tools designed for tasks such as web scraping, search, code interpretation, and more. These tools are easy to integrate into the AG2 framework, allowing you to enhance your agents with advanced capabilities. You can explore the full list of available tools in the [CrewAI Tools](https://github.com/crewAIInc/crewAI-tools/tree/main) repository.\n", + "\n", + "### Installation\n", + "Install the required packages for integrating CrewAI tools into the AG2 framework.\n", + "This ensures all dependencies for both frameworks are installed.\n", + "\n", + "```bash\n", + "pip install ag2[interop-crewai]\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Imports\n", + "\n", + "Import necessary modules and tools.\n", + "- `ScrapeWebsiteTool` are the CrewAI tools for web scraping\n", + "- `AssistantAgent` and `UserProxyAgent` are core AG2 classes.\n", + "- `Interoperability`: This module acts as a bridge, making it easier to integrate CrewAI tools with AG2’s architecture." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from crewai_tools import ScrapeWebsiteTool\n", + "\n", + "from autogen import AssistantAgent, UserProxyAgent\n", + "from autogen.interop import Interoperability" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Agent Configuration\n", + "\n", + "Configure the agents for the interaction.\n", + "- `config_list` defines the LLM configurations, including the model and API key.\n", + "- `UserProxyAgent` simulates user inputs without requiring actual human interaction (set to `NEVER`).\n", + "- `AssistantAgent` represents the AI agent, configured with the LLM settings." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "config_list = [{\"model\": \"gpt-4o\", \"api_key\": os.environ[\"OPENAI_API_KEY\"]}]\n", + "user_proxy = UserProxyAgent(\n", + " name=\"User\",\n", + " human_input_mode=\"NEVER\",\n", + ")\n", + "\n", + "chatbot = AssistantAgent(\n", + " name=\"chatbot\",\n", + " llm_config={\"config_list\": config_list},\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Tool Integration\n", + "\n", + "Initialize and register the CrewAI tool with AG2.\n", + "- `crewai_tool` is an instance of the `ScrapeWebsiteTool` from CrewAI.\n", + "- `Interoperability` converts the CrewAI tool to make it usable in AG2.\n", + "- `register_for_execution` and `register_for_llm` allow the tool to work with the UserProxyAgent and AssistantAgent." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "interop = Interoperability()\n", + "crewai_tool = ScrapeWebsiteTool()\n", + "ag2_tool = interop.convert_tool(tool=crewai_tool, type=\"crewai\")\n", + "\n", + "ag2_tool.register_for_execution(user_proxy)\n", + "ag2_tool.register_for_llm(chatbot)\n", + "\n", + "message = \"Scrape the website https://ag2.ai/\"\n", + "\n", + "chat_result = user_proxy.initiate_chat(recipient=chatbot, message=message, max_turns=2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(chat_result.summary)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## PydanticAI Tools Integration\n", + "\n", + "[PydanticAI](https://ai.pydantic.dev/) is a newer framework that offers powerful capabilities for working with LLMs. Although it currently does not have a repository with pre-built tools, it provides features like **dependency injection**, allowing you to inject a \"Context\" into a tool for better execution without relying on LLMs. This context can be used for passing parameters or managing state during the execution of a tool. While the framework is still growing, you can integrate its tools into AG2 to enhance agent capabilities, especially for tasks that involve structured data and context-driven logic.\n", + "\n", + "### Installation\n", + "To integrate LangChain tools into the AG2 framework, install the required dependencies:\n", + "\n", + "```bash\n", + "pip install ag2[interop-pydantic-ai]\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Imports\n", + "\n", + "Import necessary modules and tools.\n", + "- `BaseModel`: Used to define data structures for tool inputs and outputs.\n", + "- `RunContext`: Provides context during the execution of tools.\n", + "- `PydanticAITool`: Represents a tool in the PydanticAI framework.\n", + "- `AssistantAgent` and `UserProxyAgent`: Agents that facilitate communication in the AG2 framework.\n", + "- `Interoperability`: This module acts as a bridge, making it easier to integrate PydanticAI tools with AG2’s architecture." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from typing import Optional\n", + "\n", + "from pydantic import BaseModel\n", + "from pydantic_ai import RunContext\n", + "from pydantic_ai.tools import Tool as PydanticAITool\n", + "\n", + "from autogen import AssistantAgent, UserProxyAgent\n", + "from autogen.interop import Interoperability" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Agent Configuration\n", + "\n", + "Configure the agents for the interaction.\n", + "- `config_list` defines the LLM configurations, including the model and API key.\n", + "- `UserProxyAgent` simulates user inputs without requiring actual human interaction (set to `NEVER`).\n", + "- `AssistantAgent` represents the AI agent, configured with the LLM settings." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "config_list = [{\"model\": \"gpt-4o\", \"api_key\": os.environ[\"OPENAI_API_KEY\"]}]\n", + "user_proxy = UserProxyAgent(\n", + " name=\"User\",\n", + " human_input_mode=\"NEVER\",\n", + ")\n", + "\n", + "chatbot = AssistantAgent(\n", + " name=\"chatbot\",\n", + " llm_config={\"config_list\": config_list},\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Tool Integration\n", + "\n", + "Integrate the PydanticAI tool with AG2.\n", + "\n", + "- Define a `Player` model using `BaseModel` to structure the input data.\n", + "- Use `RunContext` to securely inject dependencies (like the `Player` instance) into the tool function without exposing them to the LLM.\n", + "- Implement `get_player` to define the tool's functionality, accessing `ctx.deps` for injected data.\n", + "- Convert the tool to an AG2-compatible format with `Interoperability` and register it for execution and LLM communication.\n", + "- Convert the PydanticAI tool into an AG2-compatible format using `convert_tool`.\n", + "- Register the tool for both execution and communication with the LLM by associating it with the `user_proxy` and `chatbot`." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "class Player(BaseModel):\n", + " name: str\n", + " age: int\n", + "\n", + "\n", + "def get_player(ctx: RunContext[Player], additional_info: Optional[str] = None) -> str: # type: ignore[valid-type]\n", + " \"\"\"Get the player's name.\n", + "\n", + " Args:\n", + " additional_info: Additional information which can be used.\n", + " \"\"\"\n", + " return f\"Name: {ctx.deps.name}, Age: {ctx.deps.age}, Additional info: {additional_info}\" # type: ignore[attr-defined]\n", + "\n", + "\n", + "interop = Interoperability()\n", + "pydantic_ai_tool = PydanticAITool(get_player, takes_ctx=True)\n", + "\n", + "# player will be injected as a dependency\n", + "player = Player(name=\"Luka\", age=25)\n", + "ag2_tool = interop.convert_tool(tool=pydantic_ai_tool, type=\"pydanticai\", deps=player)\n", + "\n", + "ag2_tool.register_for_execution(user_proxy)\n", + "ag2_tool.register_for_llm(chatbot)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Initiate a conversation between the `UserProxyAgent` and the `AssistantAgent`.\n", + "\n", + "- Use the `initiate_chat` method to send a message from the `user_proxy` to the `chatbot`.\n", + "- In this example, the user requests the chatbot to retrieve player information, providing \"goal keeper\" as additional context.\n", + "- The `Player` instance is securely injected into the tool using `RunContext`, ensuring the chatbot can retrieve and use this data during the interaction." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "user_proxy.initiate_chat(\n", + " recipient=chatbot, message=\"Get player, for additional information use 'goal keeper'\", max_turns=3\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.16" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebook/tools_langchain_tools_integration.ipynb b/notebook/tools_langchain_tools_integration.ipynb deleted file mode 100644 index 48a1d0986a..0000000000 --- a/notebook/tools_langchain_tools_integration.ipynb +++ /dev/null @@ -1,168 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Integrating LangChain Tools with the AG2 Framework\n", - "\n", - "In this tutorial, we demonstrate how to integrate [LangChain Tools](https://python.langchain.com/v0.1/docs/modules/tools) into the AG2 framework. This process enables smooth interoperability between the two systems, allowing developers to leverage LangChain's powerful tools within AG2's flexible agent-based architecture. By the end of this guide, you will understand how to configure agents, convert LangChain tools for use in AG2, and validate the integration with a practical example.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Installation\n", - "To integrate LangChain tools into the AG2 framework, install the required dependencies:\n", - "\n", - "```bash\n", - "pip install ag2[interop-langchain]\n", - "```\n", - "\n", - "Additionally, this notebook uses LangChain's [Wikipedia Tool](https://api.python.langchain.com/en/latest/tools/langchain_community.tools.wikipedia.tool.WikipediaQueryRun.html), which requires the `wikipedia` package. Install it with:\n", - "\n", - "```bash\n", - "pip install wikipedia\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Imports\n", - "\n", - "Import necessary modules and tools.\n", - "- `os`: For accessing environment variables.\n", - "- `WikipediaQueryRun` and `WikipediaAPIWrapper`: Tools for querying Wikipedia.\n", - "- `AssistantAgent` and `UserProxyAgent`: Agents that facilitate communication in the AG2 framework.\n", - "- `LangchainInteroperability`: A bridge for integrating LangChain tools with the AG2 framework." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "\n", - "from langchain_community.tools import WikipediaQueryRun\n", - "from langchain_community.utilities import WikipediaAPIWrapper\n", - "\n", - "from autogen import AssistantAgent, UserProxyAgent\n", - "from autogen.interop.langchain import LangchainInteroperability" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Agent Configuration\n", - "\n", - "Configure the agents for the interaction.\n", - "- `config_list` defines the LLM configurations, including the model and API key.\n", - "- `UserProxyAgent` simulates user inputs without requiring actual human interaction (set to `NEVER`).\n", - "- `AssistantAgent` represents the AI agent, configured with the LLM settings." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "config_list = [{\"model\": \"gpt-4o\", \"api_key\": os.environ[\"OPENAI_API_KEY\"]}]\n", - "user_proxy = UserProxyAgent(\n", - " name=\"User\",\n", - " human_input_mode=\"NEVER\",\n", - ")\n", - "\n", - "chatbot = AssistantAgent(\n", - " name=\"chatbot\",\n", - " llm_config={\"config_list\": config_list},\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Tool Integration\n", - "\n", - "- Initialize and register the LangChain tool with AG2.\n", - "- `WikipediaAPIWrapper`: Configured to fetch the top 1 result from Wikipedia with a maximum of 1000 characters per document.\n", - "- `WikipediaQueryRun`: A LangChain tool that executes Wikipedia queries.\n", - "- `LangchainInteroperability`: Converts the LangChain tool into a format compatible with the AG2 framework.\n", - "- `ag2_tool.register_for_execution(user_proxy)`: Registers the tool for use by the user_proxy agent.\n", - "- `ag2_tool.register_for_llm(chatbot)`: Registers the tool for integration with the chatbot agent.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "api_wrapper = WikipediaAPIWrapper(top_k_results=1, doc_content_chars_max=1000)\n", - "langchain_tool = WikipediaQueryRun(api_wrapper=api_wrapper)\n", - "\n", - "langchain_interop = LangchainInteroperability()\n", - "ag2_tool = langchain_interop.convert_tool(langchain_tool)\n", - "\n", - "ag2_tool.register_for_execution(user_proxy)\n", - "ag2_tool.register_for_llm(chatbot)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Wikipedia Browsing\n", - "\n", - "- `user_proxy` queries the `chatbot`, which uses a Wikipedia tool to retrieve information.\n", - "- The `chatbot` identifies the query's intent and fetches a summary from Wikipedia.\n", - "- Tool execution returns a concise response from the relevant Wikipedia page." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "message = \"Tell me about the history of the United States\"\n", - "user_proxy.initiate_chat(recipient=chatbot, message=message, max_turns=2)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.16" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/notebook/tools_pydantic_ai_tools_integration.ipynb b/notebook/tools_pydantic_ai_tools_integration.ipynb deleted file mode 100644 index edf2170135..0000000000 --- a/notebook/tools_pydantic_ai_tools_integration.ipynb +++ /dev/null @@ -1,183 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Integrating PydanticAI Tools with the AG2 Framework\n", - "\n", - "In this tutorial, we demonstrate how to integrate [PydanticAI Tools](https://ai.pydantic.dev/tools/) into the AG2 framework. This process enables smooth interoperability between the two systems, allowing developers to leverage PydanticAI's powerful tools within AG2's flexible agent-based architecture. By the end of this guide, you will understand how to configure agents, convert PydanticAI tools for use in AG2, and validate the integration with a practical example.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Installation\n", - "To integrate LangChain tools into the AG2 framework, install the required dependencies:\n", - "\n", - "```bash\n", - "pip install ag2[interop-pydantic-ai]\n", - "```\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Imports\n", - "\n", - "Import necessary modules and tools.\n", - "- `BaseModel`: Used to define data structures for tool inputs and outputs.\n", - "- `RunContext`: Provides context during the execution of tools.\n", - "- `PydanticAITool`: Represents a tool in the PydanticAI framework.\n", - "- `AssistantAgent` and `UserProxyAgent`: Agents that facilitate communication in the AG2 framework.\n", - "- `PydanticAIInteroperability`: A bridge for integrating PydanticAI tools with the AG2 framework." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "from typing import Optional\n", - "\n", - "from pydantic import BaseModel\n", - "from pydantic_ai import RunContext\n", - "from pydantic_ai.tools import Tool as PydanticAITool\n", - "\n", - "from autogen import AssistantAgent, UserProxyAgent\n", - "from autogen.interop.pydantic_ai import PydanticAIInteroperability" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Agent Configuration\n", - "\n", - "Configure the agents for the interaction.\n", - "- `config_list` defines the LLM configurations, including the model and API key.\n", - "- `UserProxyAgent` simulates user inputs without requiring actual human interaction (set to `NEVER`).\n", - "- `AssistantAgent` represents the AI agent, configured with the LLM settings." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "config_list = [{\"model\": \"gpt-4o\", \"api_key\": os.environ[\"OPENAI_API_KEY\"]}]\n", - "user_proxy = UserProxyAgent(\n", - " name=\"User\",\n", - " human_input_mode=\"NEVER\",\n", - ")\n", - "\n", - "chatbot = AssistantAgent(\n", - " name=\"chatbot\",\n", - " llm_config={\"config_list\": config_list},\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Tool Integration\n", - "\n", - "Integrate the PydanticAI tool with AG2.\n", - "\n", - "- Define a `Player` model using `BaseModel` to structure the input data.\n", - "- Use `RunContext` to securely inject dependencies (like the `Player` instance) into the tool function without exposing them to the LLM.\n", - "- Implement `get_player` to define the tool's functionality, accessing `ctx.deps` for injected data.\n", - "- Convert the tool to an AG2-compatible format with `PydanticAIInteroperability` and register it for execution and LLM communication.\n", - "- Convert the PydanticAI tool into an AG2-compatible format using `convert_tool`.\n", - "- Register the tool for both execution and communication with the LLM by associating it with the `user_proxy` and `chatbot`." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "class Player(BaseModel):\n", - " name: str\n", - " age: int\n", - "\n", - "\n", - "def get_player(ctx: RunContext[Player], additional_info: Optional[str] = None) -> str: # type: ignore[valid-type]\n", - " \"\"\"Get the player's name.\n", - "\n", - " Args:\n", - " additional_info: Additional information which can be used.\n", - " \"\"\"\n", - " return f\"Name: {ctx.deps.name}, Age: {ctx.deps.age}, Additional info: {additional_info}\" # type: ignore[attr-defined]\n", - "\n", - "\n", - "pydantic_ai_interop = PydanticAIInteroperability()\n", - "pydantic_ai_tool = PydanticAITool(get_player, takes_ctx=True)\n", - "\n", - "# player will be injected as a dependency\n", - "player = Player(name=\"Luka\", age=25)\n", - "ag2_tool = pydantic_ai_interop.convert_tool(tool=pydantic_ai_tool, deps=player)\n", - "\n", - "ag2_tool.register_for_execution(user_proxy)\n", - "ag2_tool.register_for_llm(chatbot)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Initiate a conversation between the `UserProxyAgent` and the `AssistantAgent`.\n", - "\n", - "- Use the `initiate_chat` method to send a message from the `user_proxy` to the `chatbot`.\n", - "- In this example, the user requests the chatbot to retrieve player information, providing \"goal keeper\" as additional context.\n", - "- The `Player` instance is securely injected into the tool using `RunContext`, ensuring the chatbot can retrieve and use this data during the interaction." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "user_proxy.initiate_chat(\n", - " recipient=chatbot, message=\"Get player, for additional information use 'goal keeper'\", max_turns=3\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.16" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/test/agentchat/contrib/test_swarm.py b/test/agentchat/contrib/test_swarm.py index ae2f3cf9b9..55fae1826d 100644 --- a/test/agentchat/contrib/test_swarm.py +++ b/test/agentchat/contrib/test_swarm.py @@ -1,7 +1,7 @@ # Copyright (c) 2023 - 2024, Owners of https://github.com/ag2ai # # SPDX-License-Identifier: Apache-2.0 -from typing import Any, Dict, List +from typing import Any, Dict, List, Union from unittest.mock import MagicMock, patch import pytest @@ -17,6 +17,7 @@ initiate_swarm_chat, ) from autogen.agentchat.conversable_agent import ConversableAgent +from autogen.agentchat.groupchat import GroupChat from autogen.agentchat.user_proxy_agent import UserProxyAgent TEST_MESSAGES = [{"role": "user", "content": "Initial message"}] @@ -218,7 +219,7 @@ def mock_generate_oai_reply(*args, **kwargs): # 4. Test Callable # Transfer to agent2 - def test_callable(last_speaker, messages, groupchat, context_variables): + def test_callable(last_speaker, messages, groupchat): return agent2 agent1.after_work = AFTER_WORK(test_callable) @@ -257,7 +258,7 @@ def mock_generate_oai_reply_tool(*args, **kwargs): return True, { "role": "assistant", "name": "agent1", - "tool_calls": [{"type": "function", "function": {"name": "transfer_to_agent2"}}], + "tool_calls": [{"type": "function", "function": {"name": "transfer_agent1_to_agent2"}}], } # Mock LLM responses @@ -655,5 +656,129 @@ def hello_world(context_variables: dict) -> SwarmResult: ) +def test_after_work_callable(): + """Test Callable in an AFTER_WORK handoff""" + + testing_llm_config = { + "config_list": [ + { + "model": "gpt-4o", + "api_key": "SAMPLE_API_KEY", + } + ] + } + + agent1 = SwarmAgent("agent1", llm_config=testing_llm_config) + agent2 = SwarmAgent("agent2", llm_config=testing_llm_config) + agent3 = SwarmAgent("agent3", llm_config=testing_llm_config) + + def return_agent( + last_speaker: SwarmAgent, messages: List[Dict[str, Any]], groupchat: GroupChat + ) -> Union[AfterWorkOption, SwarmAgent, str]: + return agent2 + + def return_agent_str( + last_speaker: SwarmAgent, messages: List[Dict[str, Any]], groupchat: GroupChat + ) -> Union[AfterWorkOption, SwarmAgent, str]: + return "agent3" + + def return_after_work_option( + last_speaker: SwarmAgent, messages: List[Dict[str, Any]], groupchat: GroupChat + ) -> Union[AfterWorkOption, SwarmAgent, str]: + return AfterWorkOption.TERMINATE + + agent1.register_hand_off( + hand_to=[ + AFTER_WORK(agent=return_agent), + ] + ) + + agent2.register_hand_off( + hand_to=[ + AFTER_WORK(agent=return_agent_str), + ] + ) + + agent3.register_hand_off( + hand_to=[ + AFTER_WORK(agent=return_after_work_option), + ] + ) + + # Fake generate_oai_reply + def mock_generate_oai_reply(*args, **kwargs): + return True, "This is a mock response from the agent." + + # Mock LLM responses + agent1.register_reply([ConversableAgent, None], mock_generate_oai_reply) + agent2.register_reply([ConversableAgent, None], mock_generate_oai_reply) + agent3.register_reply([ConversableAgent, None], mock_generate_oai_reply) + + chat_result, context_vars, last_speaker = initiate_swarm_chat( + initial_agent=agent1, + messages=TEST_MESSAGES, + agents=[agent1, agent2, agent3], + max_rounds=5, + ) + + # Confirm transitions and it terminated with 4 messages + assert chat_result.chat_history[1]["name"] == "agent1" + assert chat_result.chat_history[2]["name"] == "agent2" + assert chat_result.chat_history[3]["name"] == "agent3" + assert len(chat_result.chat_history) == 4 + + +def test_on_condition_unique_function_names(): + """Test that ON_CONDITION in handoffs generate unique function names""" + + testing_llm_config = { + "config_list": [ + { + "model": "gpt-4o", + "api_key": "SAMPLE_API_KEY", + } + ] + } + + agent1 = SwarmAgent("agent1", llm_config=testing_llm_config) + agent2 = SwarmAgent("agent2", llm_config=testing_llm_config) + + agent1.register_hand_off( + hand_to=[ + ON_CONDITION(target=agent2, condition="always take me to agent 2"), + ON_CONDITION(target=agent2, condition="sometimes take me there"), + ON_CONDITION(target=agent2, condition="always take me there"), + ] + ) + + # Fake generate_oai_reply + def mock_generate_oai_reply(*args, **kwargs): + return True, "This is a mock response from the agent." + + # Fake generate_oai_reply + def mock_generate_oai_reply_tool(*args, **kwargs): + return True, { + "role": "assistant", + "name": "agent1", + "tool_calls": [{"type": "function", "function": {"name": "transfer_agent1_to_agent2"}}], + } + + # Mock LLM responses + agent1.register_reply([ConversableAgent, None], mock_generate_oai_reply_tool) + agent2.register_reply([ConversableAgent, None], mock_generate_oai_reply) + + chat_result, context_vars, last_speaker = initiate_swarm_chat( + initial_agent=agent1, + messages=TEST_MESSAGES, + agents=[agent1, agent2], + max_rounds=5, + ) + + # Check that agent1 has 3 functions and they have unique names + assert "transfer_agent1_to_agent2" in agent1._function_map + assert "transfer_agent1_to_agent2_2" in agent1._function_map + assert "transfer_agent1_to_agent2_3" in agent1._function_map + + if __name__ == "__main__": pytest.main([__file__]) diff --git a/test/interop/pydantic_ai/test_pydantic_ai.py b/test/interop/pydantic_ai/test_pydantic_ai.py index fc7b7b463f..6b5dae9590 100644 --- a/test/interop/pydantic_ai/test_pydantic_ai.py +++ b/test/interop/pydantic_ai/test_pydantic_ai.py @@ -163,9 +163,13 @@ def get_player(ctx: RunContext[Player], additional_info: Optional[str] = None) - return f"Name: {ctx.deps.name}, Age: {ctx.deps.age}, Additional info: {additional_info}" # type: ignore[attr-defined] self.pydantic_ai_interop = PydanticAIInteroperability() - pydantic_ai_tool = PydanticAITool(get_player, takes_ctx=True) + self.pydantic_ai_tool = PydanticAITool(get_player, takes_ctx=True) player = Player(name="Luka", age=25) - self.tool = self.pydantic_ai_interop.convert_tool(tool=pydantic_ai_tool, deps=player) + self.tool = self.pydantic_ai_interop.convert_tool(tool=self.pydantic_ai_tool, deps=player) + + def test_convert_tool_raises_error_if_take_ctx_is_true_and_deps_is_none(self) -> None: + with pytest.raises(ValueError, match="If the tool takes a context, the `deps` argument must be provided"): + self.pydantic_ai_interop.convert_tool(tool=self.pydantic_ai_tool, deps=None) def test_expected_tools(self) -> None: config_list = [{"model": "gpt-4o", "api_key": "abc"}] diff --git a/test/interop/test_interoperability.py b/test/interop/test_interoperability.py index 8dba1c763c..df7789dd13 100644 --- a/test/interop/test_interoperability.py +++ b/test/interop/test_interoperability.py @@ -33,7 +33,7 @@ def test_register_interoperability_class(self) -> None: try: class MyInteroperability: - def convert_tool(self, tool: Any) -> Tool: + def convert_tool(self, tool: Any, **kwargs: Any) -> Tool: return Tool(name="test", description="test description", func=tool) Interoperability.register_interoperability_class("my_interop", MyInteroperability) diff --git a/website/docs/topics/swarm.ipynb b/website/docs/topics/swarm.ipynb index 41ec9c7022..45cd9411d6 100644 --- a/website/docs/topics/swarm.ipynb +++ b/website/docs/topics/swarm.ipynb @@ -137,28 +137,58 @@ "See the documentation on [registering a nested chat](https://ag2ai.github.io/ag2/docs/reference/agentchat/conversable_agent#register_nested_chats) for further information on the parameters `reply_func_from_nested_chats`, `use_async`, and `config`.\n", "\n", "Once a nested chat is complete, the resulting output from the last chat in the nested chats will be returned as the agent that triggered the nested chat's response.\n", - "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ "### AFTER_WORK\n", "\n", - "When the last active agent's response doesn't suggest a tool call or handoff, the chat will terminate by default. However, you can register an `AFTER_WORK` handoff to define a fallback agent if you don't want the chat to end at this agent. At the swarm chat level, you also pass in an `AFTER_WORK` handoff to define the fallback mechanism for the entire chat.\n", - "If this parameter is set for the agent and the chat, we will prioritize the agent's setting. There should only be one `AFTER_WORK`. If multiple `AFTER_WORK` handoffs are passed, only the last one will be used.\n", + "When the active agent's response doesn't suggest a tool call or handoff, the chat will terminate by default. However, you can register an `AFTER_WORK` handoff to control what to do next. You can register these `AFTER_WORK` handoffs at the agent level and also the swarm level (through the `after_work` parameter on `initiate_swarm_chat`). The agent level takes precedence over the swarm level.\n", + "\n", + "The AFTER_WORK takes a single parameter and this can be an agent, an agent's name, an `AfterWorkOption`, or a callable function.\n", "\n", - "Besides fallback to an agent, we provide 3 `AfterWorkOption`: \n", + "The `AfterWorkOption` options are:\n", "- `TERMINATE`: Terminate the chat \n", "- `STAY`: Stay at the current agent \n", - "- `REVERT_TO_USER`: Revert to the user agent. Only if a user agent is passed in when initializing. (See below for more details) \n", + "- `REVERT_TO_USER`: Revert to the user agent. Only if a user agent is passed in when initializing. (See below for more details)\n", "\n", - "```python\n", - "agent_1 = SwarmAgent(...)\n", + "The callable function signature is:\n", + "`def my_after_work_func(last_speaker: SwarmAgent, messages: List[Dict[str, Any]], groupchat: GroupChat) -> Union[AfterWorkOption, SwarmAgent, str]:`\n", "\n", - "# Register the handoff\n", + "Note: there should only be one `AFTER_WORK`, if your requirement is more complex, use the callable function parameter.\n", + "\n", + "Here are examples of registering AFTER_WORKS\n", + "\n", + "```python\n", + "# Register the handoff to an agent\n", "agent_1.handoff(hand_to=[\n", " ON_CONDITION(...), \n", " ON_CONDITION(...),\n", - " AFTER_WORK(agent_4) # Fallback to agent_4 if no handoff is suggested\n", + " AFTER_WORK(agent_4) # Fallback to agent_4 if no ON_CONDITION handoff is suggested\n", "])\n", "\n", + "# Register the handoff to an AfterWorkOption\n", "agent_2.handoff(hand_to=[AFTER_WORK(AfterWorkOption.TERMINATE)]) # Terminate the chat if no handoff is suggested\n", + "\n", + "def my_after_work_func(last_speaker: SwarmAgent, messages: List[Dict[str, Any]], groupchat: GroupChat) -> Union[AfterWorkOption, SwarmAgent, str]:\n", + " if last_speaker.get_context(\"agent_1_done\"):\n", + " return agent_2\n", + " else:\n", + " return AfterWorkOption.TERMINATE\n", + "\n", + "# Register the handoff to a function that will return an agent or AfterWorkOption\n", + "agent_3.handoff(hand_to=[AFTER_WORK(my_after_work_func)])\n", + "\n", + "# Register the swarm level AFTER_WORK that becomes the default for agents that don't have one specified\n", + "chat_history, context_variables, last_active_agent = initiate_swarm_chat(\n", + " ...\n", + " after_work=AfterWorkOption.TERMINATE # Or an agent or Callable\n", + ")\n", + "\n", + "```\n", "```" ] },