From 8355b739c54aa4da9fc79352ec094832304b9ae9 Mon Sep 17 00:00:00 2001 From: Bilgin Ibryam Date: Wed, 7 May 2025 12:52:13 +0100 Subject: [PATCH 1/4] WIP experiment to reuse thrid-party tools as native tools Signed-off-by: Bilgin Ibryam --- dapr_agents/tool/__init__.py | 1 + dapr_agents/tool/third_party/__init__.py | 2 + dapr_agents/tool/third_party/crewai_tool.py | 124 +++++++++++ .../tool/third_party/langchain_tool.py | 158 ++++++++++++++ .../09-third-party-tool-call/README.md | 206 ++++++++++++++++++ .../agent_with_crewai_tool.py | 53 +++++ .../agent_with_langchain_tool.py | 59 +++++ .../09-third-party-tool-call/requirements.txt | 7 + .../09-third-party-tool-call/sample_data.txt | 14 ++ 9 files changed, 624 insertions(+) create mode 100644 dapr_agents/tool/third_party/__init__.py create mode 100644 dapr_agents/tool/third_party/crewai_tool.py create mode 100644 dapr_agents/tool/third_party/langchain_tool.py create mode 100644 quickstarts/09-third-party-tool-call/README.md create mode 100644 quickstarts/09-third-party-tool-call/agent_with_crewai_tool.py create mode 100644 quickstarts/09-third-party-tool-call/agent_with_langchain_tool.py create mode 100644 quickstarts/09-third-party-tool-call/requirements.txt create mode 100644 quickstarts/09-third-party-tool-call/sample_data.txt 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..d6604b95 --- /dev/null +++ b/dapr_agents/tool/third_party/__init__.py @@ -0,0 +1,2 @@ +from .langchain_tool import LangchainTool +from .crewai_tool import CrewAITool \ No newline at end of file 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..30b0c7d4 --- /dev/null +++ b/dapr_agents/tool/third_party/crewai_tool.py @@ -0,0 +1,124 @@ +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) \ No newline at end of file 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..19530b26 --- /dev/null +++ b/dapr_agents/tool/third_party/langchain_tool.py @@ -0,0 +1,158 @@ +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) \ No newline at end of file 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..8b28100f --- /dev/null +++ b/quickstarts/09-third-party-tool-call/agent_with_crewai_tool.py @@ -0,0 +1,53 @@ +#!/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..12936e60 --- /dev/null +++ b/quickstarts/09-third-party-tool-call/agent_with_langchain_tool.py @@ -0,0 +1,59 @@ +#!/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 LangchainTool, tool +from dapr_agents.tool.third_party import LangchainTool +from dapr_agents.tool import 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()) \ No newline at end of file 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..216f3ce4 --- /dev/null +++ b/quickstarts/09-third-party-tool-call/requirements.txt @@ -0,0 +1,7 @@ +dapr-agents>=0.5.0 +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 From d8fd8d1bbb05d493c27c5fc5c865b6cd8ba5f75a Mon Sep 17 00:00:00 2001 From: Bilgin Ibryam Date: Fri, 23 May 2025 21:55:22 +0100 Subject: [PATCH 2/4] Fix linting errors Signed-off-by: Bilgin Ibryam --- dapr_agents/tool/third_party/__init__.py | 2 +- dapr_agents/tool/third_party/crewai_tool.py | 45 +++++++------ .../tool/third_party/langchain_tool.py | 65 ++++++++++--------- .../agent_with_crewai_tool.py | 22 ++++--- .../agent_with_langchain_tool.py | 18 +++-- 5 files changed, 86 insertions(+), 66 deletions(-) diff --git a/dapr_agents/tool/third_party/__init__.py b/dapr_agents/tool/third_party/__init__.py index d6604b95..8fa5f05d 100644 --- a/dapr_agents/tool/third_party/__init__.py +++ b/dapr_agents/tool/third_party/__init__.py @@ -1,2 +1,2 @@ from .langchain_tool import LangchainTool -from .crewai_tool import CrewAITool \ No newline at end of file +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 index 30b0c7d4..6193c108 100644 --- a/dapr_agents/tool/third_party/crewai_tool.py +++ b/dapr_agents/tool/third_party/crewai_tool.py @@ -14,6 +14,7 @@ class CrewAIBaseTool: pass + class CrewAITool(AgentTool): """ Adapter for using CrewAI tools with dapr-agents. @@ -36,23 +37,23 @@ def __init__(self, tool: Any, **kwargs): # 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: @@ -62,16 +63,16 @@ def populate_name(cls, data: Any) -> Any: 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 """ @@ -87,25 +88,27 @@ def _run(self, *args: Any, **kwargs: Any) -> str: 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: + 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. """ @@ -113,12 +116,12 @@ def to_function_call(self, format_type: str = "openai", use_deprecated: bool = F 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 + 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) \ No newline at end of file + 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 index 19530b26..0be183f5 100644 --- a/dapr_agents/tool/third_party/langchain_tool.py +++ b/dapr_agents/tool/third_party/langchain_tool.py @@ -15,6 +15,7 @@ class LangchainBaseTool: pass + class LangchainTool(AgentTool): """ Adapter for using LangChain tools with dapr-agents. @@ -37,23 +38,23 @@ def __init__(self, tool: Any, **kwargs): # 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: @@ -63,37 +64,41 @@ def populate_name(cls, data: Any) -> Any: 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] + 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: @@ -105,7 +110,7 @@ def _run_with_query(self, query=None, **kwargs): return self.tool.run(query) elif callable(self.tool): return self.tool(query) - + # Fall back to kwargs pattern else: if hasattr(self.tool, "_run"): @@ -114,32 +119,34 @@ def _run_with_query(self, query=None, **kwargs): 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: + + 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. """ @@ -147,12 +154,12 @@ def to_function_call(self, format_type: str = "openai", use_deprecated: bool = F 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 + 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) \ No newline at end of file + return super().to_function_call(format_type, use_deprecated) 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 index 8b28100f..5a98d610 100644 --- a/quickstarts/09-third-party-tool-call/agent_with_crewai_tool.py +++ b/quickstarts/09-third-party-tool-call/agent_with_crewai_tool.py @@ -12,24 +12,25 @@ 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}" + return ( + f"Text Statistics:\n- Words: {words}\n- Lines: {lines}\n- Characters: {chars}" + ) -async def main(): +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" + 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 @@ -38,16 +39,19 @@ async def main(): 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] + 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." + 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 index 12936e60..3f1f23c6 100644 --- a/quickstarts/09-third-party-tool-call/agent_with_langchain_tool.py +++ b/quickstarts/09-third-party-tool-call/agent_with_langchain_tool.py @@ -13,17 +13,22 @@ 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}" + return ( + f"Text Statistics:\n- Words: {words}\n- Lines: {lines}\n- Characters: {chars}" + ) + async def main(): # Create LangChain tool - DuckDuckGo search @@ -33,9 +38,9 @@ async def main(): search_tool = LangchainTool( ddg_search_tool, name="WebSearch", - description="Search the web for current information on any topic" + description="Search the web for current information on any topic", ) - + # Set the args model for proper argument handling search_tool.args_model = SearchArgs @@ -44,16 +49,17 @@ async def main(): """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] + 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()) \ No newline at end of file + asyncio.run(main()) From 5a0208ceb1e539ada824e3f7e0239c76e8db954a Mon Sep 17 00:00:00 2001 From: Bilgin Ibryam Date: Fri, 23 May 2025 22:23:11 +0100 Subject: [PATCH 3/4] Fix flake8 lint issues and update tox config to exclude virtualenv and build dirs Signed-off-by: Bilgin Ibryam --- quickstarts/09-third-party-tool-call/agent_with_crewai_tool.py | 2 +- .../09-third-party-tool-call/agent_with_langchain_tool.py | 3 +-- tox.ini | 3 ++- 3 files changed, 4 insertions(+), 4 deletions(-) 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 index 5a98d610..6fd72c75 100644 --- a/quickstarts/09-third-party-tool-call/agent_with_crewai_tool.py +++ b/quickstarts/09-third-party-tool-call/agent_with_crewai_tool.py @@ -36,7 +36,7 @@ async def main(): # 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, + 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], 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 index 3f1f23c6..014ff6d0 100644 --- a/quickstarts/09-third-party-tool-call/agent_with_langchain_tool.py +++ b/quickstarts/09-third-party-tool-call/agent_with_langchain_tool.py @@ -6,9 +6,8 @@ from langchain_community.tools import DuckDuckGoSearchRun from dapr_agents.agent import Agent -from dapr_agents.tool import LangchainTool, tool -from dapr_agents.tool.third_party import LangchainTool from dapr_agents.tool import tool +from dapr_agents.tool.third_party import LangchainTool from pydantic import BaseModel, Field load_dotenv() 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 From 395c47ee9775d047d22c3f302a8f359080b02274 Mon Sep 17 00:00:00 2001 From: Bilgin Ibryam Date: Fri, 23 May 2025 22:31:40 +0100 Subject: [PATCH 4/4] Increase dapr-agents versions to latest Signed-off-by: Bilgin Ibryam --- quickstarts/09-third-party-tool-call/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quickstarts/09-third-party-tool-call/requirements.txt b/quickstarts/09-third-party-tool-call/requirements.txt index 216f3ce4..6604f853 100644 --- a/quickstarts/09-third-party-tool-call/requirements.txt +++ b/quickstarts/09-third-party-tool-call/requirements.txt @@ -1,4 +1,4 @@ -dapr-agents>=0.5.0 +dapr-agents>=0.5.1 python-dotenv langchain>=0.1.0 langchain-community>=0.0.10