8000 WIP experiment to reuse third-party tools as native tools by bibryam · Pull Request #117 · dapr/dapr-agents · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

WIP experiment to reuse third-party tools as native tools #117

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dapr_agents/tool/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from .base import AgentTool, tool
from .executor import AgentToolExecutor
from .third_party import LangchainTool, CrewAITool
2 changes: 2 additions & 0 deletions dapr_agents/tool/third_party/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .langchain_tool import LangchainTool
from .crewai_tool import CrewAITool
124 changes: 124 additions & 0 deletions dapr_agents/tool/third_party/crewai_tool.py
Original file line number Diff line number Diff line change
@@ -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)
158 changes: 158 additions & 0 deletions dapr_agents/tool/third_party/langchain_tool.py
Original file line number Diff line number Diff line change
@@ -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)
Loading
Loading
0