diff --git a/dapr_agents/tool/__init__.py b/dapr_agents/tool/__init__.py index 9d4899bd..59a64e8f 100644 --- a/dapr_agents/tool/__init__.py +++ b/dapr_agents/tool/__init__.py @@ -1,2 +1,3 @@ from .base import AgentTool, tool from .executor import AgentToolExecutor +from .third_party import LangchainTool, CrewAITool diff --git a/dapr_agents/tool/third_party/__init__.py b/dapr_agents/tool/third_party/__init__.py new file mode 100644 index 00000000..8fa5f05d --- /dev/null +++ b/dapr_agents/tool/third_party/__init__.py @@ -0,0 +1,2 @@ +from .langchain_tool import LangchainTool +from .crewai_tool import CrewAITool diff --git a/dapr_agents/tool/third_party/crewai_tool.py b/dapr_agents/tool/third_party/crewai_tool.py new file mode 100644 index 00000000..6193c108 --- /dev/null +++ b/dapr_agents/tool/third_party/crewai_tool.py @@ -0,0 +1,127 @@ +from typing import Any, Dict, Optional + +from pydantic import BaseModel, model_validator, Field + +from dapr_agents.tool import AgentTool +from dapr_agents.tool.utils.function_calling import to_function_call_definition +from dapr_agents.types import ToolError + +# Try to import CrewAI BaseTool to support proper type checking +try: + from crewai.tools import BaseTool as CrewAIBaseTool +except ImportError: + # Define a placeholder for static type checking + class CrewAIBaseTool: + pass + + +class CrewAITool(AgentTool): + """ + Adapter for using CrewAI tools with dapr-agents. + + This class wraps a CrewAI tool and makes it compatible with the dapr-agents + framework, preserving the original tool's name, description, and schema. + """ + + tool: Any = Field(default=None) + """The wrapped CrewAI tool.""" + + def __init__(self, tool: Any, **kwargs): + """ + Initialize the CrewAITool with a CrewAI tool. + + Args: + tool: The CrewAI tool to wrap + **kwargs: Optional overrides for name, description, etc. + """ + # Extract metadata from CrewAI tool + name = kwargs.get("name", "") + description = kwargs.get("description", "") + + # If name/description not provided via kwargs, extract from tool + if not name: + # Get name from the tool and format it (CrewAI tools often have spaces) + raw_name = getattr(tool, "name", tool.__class__.__name__) + name = raw_name.replace(" ", "_").title() + + if not description: + # Get description from the tool + description = getattr(tool, "description", tool.__doc__ or "") + + # Initialize the AgentTool with the CrewAI tool's metadata + super().__init__(name=name, description=description) + + # Set the tool after parent initialization + self.tool = tool + + @model_validator(mode="before") + @classmethod + def populate_name(cls, data: Any) -> Any: + # Override name validation to properly format CrewAI tool name + return data + + def _run(self, *args: Any, **kwargs: Any) -> str: + """ + Execute the wrapped CrewAI tool. + + Attempts to call the tool's run method or _execute method, depending on what's available. + + Args: + *args: Positional arguments to pass to the tool + **kwargs: Keyword arguments to pass to the tool + + Returns: + str: The result of the tool execution + + Raises: + ToolError: If the tool execution fails + """ + try: + # Try different calling patterns based on CrewAI tool implementation + if hasattr(self.tool, "run"): + return self.tool.run(*args, **kwargs) + elif hasattr(self.tool, "_execute"): + return self.tool._execute(*args, **kwargs) + elif callable(self.tool): + return self.tool(*args, **kwargs) + else: + raise ToolError(f"Cannot execute CrewAI tool: {self.tool}") + except Exception as e: + raise ToolError(f"Error executing CrewAI tool: {str(e)}") + + def model_post_init(self, __context: Any) -> None: + """Initialize args_model from the CrewAI tool schema if available.""" + super().model_post_init(__context) + + # Try to use the CrewAI tool's schema if available + if hasattr(self.tool, "args_schema"): + self.args_model = self.tool.args_schema + + def to_function_call( + self, format_type: str = "openai", use_deprecated: bool = False + ) -> Dict: + """ + Converts the tool to a function call definition based on its schema. + + If the CrewAI tool has an args_schema, use it directly. + + Args: + format_type (str): The format type (e.g., 'openai'). + use_deprecated (bool): Whether to use deprecated format. + + Returns: + Dict: The function call representation. + """ + # Use to_function_call_definition from function_calling utility + if hasattr(self.tool, "args_schema") and self.tool.args_schema: + # For CrewAI tools, we have their schema model directly + return to_function_call_definition( + self.name, + self.description, + self.args_model, + format_type, + use_deprecated, + ) + else: + # Fallback to the regular AgentTool implementation + return super().to_function_call(format_type, use_deprecated) diff --git a/dapr_agents/tool/third_party/langchain_tool.py b/dapr_agents/tool/third_party/langchain_tool.py new file mode 100644 index 00000000..0be183f5 --- /dev/null +++ b/dapr_agents/tool/third_party/langchain_tool.py @@ -0,0 +1,165 @@ +import inspect +from typing import Any, Dict, Optional + +from pydantic import BaseModel, model_validator, Field + +from dapr_agents.tool import AgentTool +from dapr_agents.tool.utils.function_calling import to_function_call_definition +from dapr_agents.types import ToolError + +# Try to import LangChain BaseTool to support proper type checking +try: + from langchain_core.tools import BaseTool as LangchainBaseTool +except ImportError: + # Define a placeholder for static type checking + class LangchainBaseTool: + pass + + +class LangchainTool(AgentTool): + """ + Adapter for using LangChain tools with dapr-agents. + + This class wraps a LangChain tool and makes it compatible with the dapr-agents + framework, preserving the original tool's name, description, and schema. + """ + + tool: Any = Field(default=None) + """The wrapped LangChain tool.""" + + def __init__(self, tool: Any, **kwargs): + """ + Initialize the LangchainTool with a LangChain tool. + + Args: + tool: The LangChain tool to wrap + **kwargs: Optional overrides for name, description, etc. + """ + # Extract metadata from LangChain tool + name = kwargs.get("name", "") + description = kwargs.get("description", "") + + # If name/description not provided via kwargs, extract from tool + if not name: + # Get name from the tool + raw_name = getattr(tool, "name", tool.__class__.__name__) + name = raw_name.replace(" ", "_").title() + + if not description: + # Get description from the tool + description = getattr(tool, "description", tool.__doc__ or "") + + # Initialize the AgentTool with the LangChain tool's metadata + super().__init__(name=name, description=description) + + # Set the tool after parent initialization + self.tool = tool + + @model_validator(mode="before") + @classmethod + def populate_name(cls, data: Any) -> Any: + # Override name validation to properly format LangChain tool name + return data + + def _run(self, *args: Any, **kwargs: Any) -> str: + """ + Execute the wrapped LangChain tool. + + Attempts to call the tool's _run method or run method, depending on what's available. + + Args: + *args: Positional arguments to pass to the tool + **kwargs: Keyword arguments to pass to the tool + + Returns: + str: The result of the tool execution + + Raises: + ToolError: If the tool execution fails + """ + try: + # Handle common issue where args/kwargs are passed differently + # If 'args' is in kwargs, extract and use as the query + if ( + "args" in kwargs + and isinstance(kwargs["args"], list) + and len(kwargs["args"]) > 0 + ): + query = kwargs["args"][0] + return self._run_with_query(query) + + # If args has content, use the first arg + elif args and len(args) > 0: + query = args[0] + return self._run_with_query(query) + + # Otherwise, just pass through the kwargs + else: + return self._run_with_query(**kwargs) + except Exception as e: + raise ToolError(f"Error executing LangChain tool: {str(e)}") + + def _run_with_query(self, query=None, **kwargs): + """Helper method to run the tool with different calling patterns.""" + try: + # First check for single argument query-based pattern + if query is not None: + if hasattr(self.tool, "_run"): + return self.tool._run(query) + elif hasattr(self.tool, "run"): + return self.tool.run(query) + elif callable(self.tool): + return self.tool(query) + + # Fall back to kwargs pattern + else: + if hasattr(self.tool, "_run"): + return self.tool._run(**kwargs) + elif hasattr(self.tool, "run"): + return self.tool.run(**kwargs) + elif callable(self.tool): + return self.tool(**kwargs) + + # If we get here, couldn't find a way to execute + raise ToolError(f"Cannot execute LangChain tool: {self.tool}") + except Exception as e: + raise ToolError(f"Error executing LangChain tool: {str(e)}") + + def model_post_init(self, __context: Any) -> None: + """Initialize args_model from the LangChain tool schema if available.""" + super().model_post_init(__context) + + # Try to use the LangChain tool's schema if available + if hasattr(self.tool, "args_schema"): + self.args_model = self.tool.args_schema + elif hasattr(self.tool, "schema"): + self.args_model = self.tool.schema + + def to_function_call( + self, format_type: str = "openai", use_deprecated: bool = False + ) -> Dict: + """ + Converts the tool to a function call definition based on its schema. + + If the LangChain tool has an args_schema, use it directly. + + Args: + format_type (str): The format type (e.g., 'openai'). + use_deprecated (bool): Whether to use deprecated format. + + Returns: + Dict: The function call representation. + """ + # Use to_function_call_definition from function_calling utility + if hasattr(self.tool, "args_schema") and self.tool.args_schema: + # For LangChain tools, we have their schema model directly + return to_function_call_definition( + self.name, + self.description, + self.args_model, + format_type, + use_deprecated, + ) + else: + # Fallback to the regular AgentTool implementation + return super().to_function_call(format_type, use_deprecated) diff --git a/quickstarts/09-third-party-tool-call/README.md b/quickstarts/09-third-party-tool-call/README.md new file mode 100644 index 00000000..c859a611 --- /dev/null +++ b/quickstarts/09-third-party-tool-call/README.md @@ -0,0 +1,206 @@ +# Third-Party Tools Quickstart + +This quickstart demonstrates how to integrate third-party tools from other agentic frameworks like LangChain and CrewAI with dapr-agents. You'll learn how to import and use existing tools from these ecosystems to work with Dapr Agents. + +## Prerequisites + +- Python 3.10 (recommended) +- pip package manager +- OpenAI API key +- For LangChain example: langchain, langchain-community +- For CrewAI example: crewai, crewai-tools + +## Environment Setup + +```bash +# Create a virtual environment +python3.10 -m venv .venv + +# Activate the virtual environment +# On Windows: +.venv\Scripts\activate +# On macOS/Linux: +source .venv/bin/activate + +# Install dependencies +pip install -r requirements.txt +``` + +## Configuration + +Create a `.env` file in the project root: + +```env +OPENAI_API_KEY=your_api_key_here +``` + +Replace `your_api_key_here` with your actual OpenAI API key. + +## Examples + +### LangChain Tools Integration + +This example shows how to integrate LangChain tools with dapr-agents: + +1. In `agent_with_langchain_tool.py`, we integrate DuckDuckGo search with a custom word counter tool: + +```python +from dotenv import load_dotenv +import asyncio +from langchain_community.tools import DuckDuckGoSearchRun +from dapr_agents.agent import Agent +from dapr_agents.tool import LangchainTool, tool +from pydantic import BaseModel, Field + +load_dotenv() + +# Create a search args model +class SearchArgs(BaseModel): + query: str = Field(..., description="The search query to look up information for") + +@tool +def count_words(text: str) -> str: + """Count the number of words, lines, and characters in the text.""" + words = len(text.split()) + lines = len(text.splitlines()) + chars = len(text) + return f"Text Statistics:\n- Words: {words}\n- Lines: {lines}\n- Characters: {chars}" + +async def main(): + # Create LangChain tool - DuckDuckGo search + ddg_search_tool = DuckDuckGoSearchRun() + + # Wrap LangChain tool with dapr's LangchainTool adapter + search_tool = LangchainTool( + ddg_search_tool, + name="WebSearch", + description="Search the web for current information on any topic" + ) + + # Set the args model for proper argument handling + search_tool.args_model = SearchArgs + + # Create a dapr agent with both tools + agent = Agent( + """You are a helpful assistant that can search the web and analyze text. + Use the WebSearch tool to find information about topics, + then use the CountWords tool to analyze the text statistics of the results.""", + tools=[search_tool, count_words] + ) + + # Run the agent with a query + query = "What is Dapr and then count the words in your search results." + print(f"User: {query}") + + # Properly await the run method + result = await agent.run(query) + print(f"Agent: {result}") + +if __name__ == "__main__": + asyncio.run(main()) +``` + +2. Run the LangChain tools agent: + +```bash +python agent_with_langchain_tool.py +``` + +### CrewAI Tools Integration + +This example shows how to integrate CrewAI tools: + +1. In `agent_with_crewai_tool.py`, we integrate a file reading tool with the same word counter: + +```python +import asyncio +from pathlib import Path +from dotenv import load_dotenv +from crewai_tools import FileReadTool +from dapr_agents.agent import Agent +from dapr_agents.tool import CrewAITool, tool + +load_dotenv() + +@tool +def count_words(text: str) -> str: + """Count the number of words, lines, and characters in the text.""" + words = len(text.split()) + lines = len(text.splitlines()) + chars = len(text) + return f"Text Statistics:\n- Words: {words}\n- Lines: {lines}\n- Characters: {chars}" + +async def main(): + # Create CrewAI FileReadTool + file_read_tool = FileReadTool(file_path="sample_data.txt") + + # Wrap with CrewAITool adapter + file_tool = CrewAITool( + file_read_tool, + name="ReadFile", + description="Reads text from the sample file" + ) + + # Create a dapr agent with both tools - CrewAI wrapped tool and native tool + agent = Agent( + """You are a helpful assistant that can read files and analyze text. + The ReadFile tool is already configured to read from a specific sample file, + so you can just use it without arguments. + After getting the content, you can use the CountWords tool to analyze the text statistics.""", + tools=[file_tool, count_words] + ) + + # Run the agent with a query about the file + query = "Please read the sample file and tell me its word count and other statistics." + print(f"User: {query}") + + # Properly await the run method + result = await agent.run(query) + print(f"Agent: {result}") + +if __name__ == "__main__": + asyncio.run(main()) +``` + +2. Run the CrewAI tools agent: + +```bash +python agent_with_crewai_tool.py +``` + +## Key Concepts + +### Tool Adaptation +- Third-party tools need to be wrapped with adapter classes to work with dapr-agents +- `LangchainTool` adapter converts LangChain tools to the dapr-agents format +- `CrewAITool` adapter converts CrewAI tools to the dapr-agents format +- You can customize names and descriptions when adapting tools + +### Agent Setup +- The `Agent` class works the same way as with native dapr-agents tools +- Adapted tools are provided in the tools list alongside native tools +- You can mix and match native dapr-agents tools with adapted third-party tools + +### Execution Flow +1. The agent receives a user query +2. The LLM determines which tool(s) to use based on the query +3. The adapter converts the call format for the third-party tool +4. The third-party tool executes and returns results +5. The adapter converts the results back to the dapr-agents format +6. The final answer is provided to the user + +## Popular Tool Options + +You can use many different tools from these frameworks with the appropriate adapter. Check the [LangChain tools](https://python.langchain.com/docs/integrations/tools/) and [CrewAI tools](https://github.com/crewAIInc/crewAI-tools/) for more options. + +## Troubleshooting + +1. **OpenAI API Key**: Ensure your key is correctly set in the `.env` file +2. **Tool Dependencies**: Verify that you've installed all required packages for the specific tools +3. **Tool Execution Errors**: Check tool implementations for exceptions +4. **Adaptation Issues**: Make sure you're using the correct adapter for each tool type +5. **File Paths**: For file-based tools like CrewAI's FileReadTool, ensure the files exist at the specified path + +## Next Steps + +After completing this quickstart, explore how to combine native dapr-agents tools with third-party tools in more complex workflows. \ No newline at end of file diff --git a/quickstarts/09-third-party-tool-call/agent_with_crewai_tool.py b/quickstarts/09-third-party-tool-call/agent_with_crewai_tool.py new file mode 100644 index 00000000..6fd72c75 --- /dev/null +++ b/quickstarts/09-third-party-tool-call/agent_with_crewai_tool.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 + +import os +import asyncio +from pathlib import Path +from dotenv import load_dotenv + +from crewai_tools import FileReadTool + +from dapr_agents.agent import Agent +from dapr_agents.tool import CrewAITool, tool + +load_dotenv() + + +@tool +def count_words(text: str) -> str: + """Count the number of words, lines, and characters in the text.""" + words = len(text.split()) + lines = len(text.splitlines()) + chars = len(text) + return ( + f"Text Statistics:\n- Words: {words}\n- Lines: {lines}\n- Characters: {chars}" + ) + + +async def main(): + # Create CrewAI FileReadTool + file_read_tool = FileReadTool(file_path="sample_data.txt") + + # Wrap with CrewAITool adapter + file_tool = CrewAITool( + file_read_tool, name="ReadFile", description="Reads text from the sample file" + ) + + # Create a dapr agent with both tools - CrewAI wrapped tool and native tool + agent = Agent( + """You are a helpful assistant that can read files and analyze text. + The ReadFile tool is already configured to read from a specific sample file, + so you can just use it without arguments. + After getting the content, you can use the CountWords tool to analyze the text statistics.""", + tools=[file_tool, count_words], + ) + + # Run the agent with a query about the file + query = ( + "Please read the sample file and tell me its word count and other statistics." + ) + print(f"User: {query}") + + # Properly await the run method + result = await agent.run(query) + print(f"Agent: {result}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/quickstarts/09-third-party-tool-call/agent_with_langchain_tool.py b/quickstarts/09-third-party-tool-call/agent_with_langchain_tool.py new file mode 100644 index 00000000..014ff6d0 --- /dev/null +++ b/quickstarts/09-third-party-tool-call/agent_with_langchain_tool.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +import os +import asyncio +from dotenv import load_dotenv + +from langchain_community.tools import DuckDuckGoSearchRun + +from dapr_agents.agent import Agent +from dapr_agents.tool import tool +from dapr_agents.tool.third_party import LangchainTool +from pydantic import BaseModel, Field + +load_dotenv() + + +# Create a search args model +class SearchArgs(BaseModel): + query: str = Field(..., description="The search query to look up information for") + + +@tool +def count_words(text: str) -> str: + """Count the number of words, lines, and characters in the text.""" + words = len(text.split()) + lines = len(text.splitlines()) + chars = len(text) + return ( + f"Text Statistics:\n- Words: {words}\n- Lines: {lines}\n- Characters: {chars}" + ) + + +async def main(): + # Create LangChain tool - DuckDuckGo search + ddg_search_tool = DuckDuckGoSearchRun() + + # Wrap LangChain tool with dapr's LangchainTool adapter + search_tool = LangchainTool( + ddg_search_tool, + name="WebSearch", + description="Search the web for current information on any topic", + ) + + # Set the args model for proper argument handling + search_tool.args_model = SearchArgs + + # Create a dapr agent with both tools + agent = Agent( + """You are a helpful assistant that can search the web and analyze text. + Use the WebSearch tool to find information about topics, + then use the CountWords tool to analyze the text statistics of the results.""", + tools=[search_tool, count_words], + ) + + # Run the agent with a query + query = "What is Dapr and then count the words in your search results." + print(f"User: {query}") + + # Properly await the run method + result = await agent.run(query) + print(f"Agent: {result}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/quickstarts/09-third-party-tool-call/requirements.txt b/quickstarts/09-third-party-tool-call/requirements.txt new file mode 100644 index 00000000..6604f853 --- /dev/null +++ b/quickstarts/09-third-party-tool-call/requirements.txt @@ -0,0 +1,7 @@ +dapr-agents>=0.5.1 +python-dotenv +langchain>=0.1.0 +langchain-community>=0.0.10 +duckduckgo-search>=3.8.0 +crewai>=0.28.0 +crewai-tools>=0.1.0 \ No newline at end of file diff --git a/quickstarts/09-third-party-tool-call/sample_data.txt b/quickstarts/09-third-party-tool-call/sample_data.txt new file mode 100644 index 00000000..f9a652b0 --- /dev/null +++ b/quickstarts/09-third-party-tool-call/sample_data.txt @@ -0,0 +1,14 @@ +## Key Information + +- Dapr is a portable, event-driven runtime for building distributed applications +- It helps developers build resilient, microservice applications +- Dapr integrates with any language or framework + +## Example Use Cases + +1. Building microservices +2. Creating event-driven applications +3. Connecting services across different platforms +4. Managing application state consistently + +For more information, visit https://dapr.io \ No newline at end of file diff --git a/tox.ini b/tox.ini index d42cf1ba..db425fca 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,8 @@ basepython = python3 usedevelop = False deps = flake8 commands = - flake8 . --ignore=E501,F401,W503,E203 + flake8 . --ignore=E501,F401,W503,E203 --exclude=.git,__pycache__,.venv,build,dist,.tox + [testenv:ruff] basepython = python3