mirror of
https://github.com/MervinPraison/PraisonAI-Tools.git
synced 2026-11-09 03:39:37 -10:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
090b910800 | ||
|
|
f2d1d3695d | ||
|
|
6a560069da |
No files matched your search
@@ -660,6 +660,29 @@ agent.start("Check the weather in London and send a Slack message about it")
|
||||
|
||||
---
|
||||
|
||||
## Composio Integration
|
||||
|
||||
Load 250+ managed app integrations (GitHub, Slack, Gmail, Jira, etc.) as agent-callable tools:
|
||||
|
||||
```bash
|
||||
pip install praisonai-tools[composio]
|
||||
export COMPOSIO_API_KEY=your_api_key
|
||||
```
|
||||
|
||||
```python
|
||||
from praisonaiagents import Agent
|
||||
from praisonai_tools import composio_tools, composio_list_apps
|
||||
|
||||
print(composio_list_apps()[:5]) # ["github", "slack", ...]
|
||||
|
||||
agent = Agent(name="dev", tools=composio_tools(apps=["github"]))
|
||||
agent.start("Star the praisonai/PraisonAI repository")
|
||||
```
|
||||
|
||||
For explicit action execution, use `ComposioTool` or `composio_execute()`.
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions! To add a new tool:
|
||||
|
||||
@@ -144,4 +144,10 @@ __all__ = [
|
||||
"AgentFolioTool",
|
||||
"check_behavioral_trust",
|
||||
"verify_task_delegation_safety",
|
||||
# Composio
|
||||
"ComposioTool",
|
||||
"ComposioTools",
|
||||
"composio_execute",
|
||||
"composio_tools",
|
||||
"composio_list_apps",
|
||||
]
|
||||
@@ -442,7 +442,10 @@ def __getattr__(name):
|
||||
"daytona_list_workspaces": "daytona_tool",
|
||||
# Composio
|
||||
"ComposioTool": "composio_tool",
|
||||
"ComposioTools": "composio_tool",
|
||||
"composio_execute": "composio_tool",
|
||||
"composio_tools": "composio_tool",
|
||||
"composio_list_apps": "composio_tool",
|
||||
# JSON
|
||||
"JSONTool": "json_tool",
|
||||
"read_json": "json_tool",
|
||||
|
||||
@@ -1,72 +1,215 @@
|
||||
"""Composio Tool for PraisonAI Agents.
|
||||
"""Composio integration tools for PraisonAI Agents.
|
||||
|
||||
Execute actions via Composio integrations.
|
||||
Composio provides 250+ managed app integrations exposed as agent-callable tools.
|
||||
|
||||
Requires: pip install praisonai-tools[composio]
|
||||
Environment: COMPOSIO_API_KEY
|
||||
|
||||
Usage:
|
||||
from praisonai_tools import ComposioTool
|
||||
|
||||
composio = ComposioTool()
|
||||
result = composio.execute("GITHUB_CREATE_ISSUE", {"repo": "owner/repo", "title": "Bug"})
|
||||
from praisonaiagents import Agent
|
||||
from praisonai_tools import composio_tools
|
||||
|
||||
Environment Variables:
|
||||
COMPOSIO_API_KEY: Composio API key
|
||||
agent = Agent(name="dev", tools=composio_tools(apps=["github"]))
|
||||
agent.start("Star the praisonai/PraisonAI repository")
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
import os
|
||||
from importlib import util
|
||||
from typing import Any, Callable, Dict, List, Optional, Union
|
||||
|
||||
from praisonai_tools.tools.base import BaseTool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _check_composio_available(api_key: Optional[str] = None) -> tuple[bool, Optional[str]]:
|
||||
"""Return (is_available, error_message).
|
||||
|
||||
``api_key`` takes precedence over the ``COMPOSIO_API_KEY`` environment
|
||||
variable so callers who pass a key explicitly are not forced to also set
|
||||
the env var.
|
||||
"""
|
||||
if util.find_spec("composio") is None:
|
||||
return False, "composio package is not installed. Install with: pip install praisonai-tools[composio]"
|
||||
|
||||
if not api_key and not os.environ.get("COMPOSIO_API_KEY"):
|
||||
return False, (
|
||||
"COMPOSIO_API_KEY environment variable is not set. "
|
||||
"Please set it to use Composio tools."
|
||||
)
|
||||
|
||||
return True, None
|
||||
|
||||
|
||||
class ComposioTools:
|
||||
"""Composio managed-integration tools for Agent(tools=[...])."""
|
||||
|
||||
def __init__(self, api_key: Optional[str] = None):
|
||||
self.api_key = api_key or os.environ.get("COMPOSIO_API_KEY")
|
||||
self._client = None
|
||||
|
||||
def _get_client(self):
|
||||
if self._client is None:
|
||||
try:
|
||||
from composio import Composio
|
||||
|
||||
self._client = Composio(api_key=self.api_key)
|
||||
except ImportError:
|
||||
from composio import ComposioToolSet
|
||||
|
||||
self._client = ComposioToolSet(api_key=self.api_key)
|
||||
return self._client
|
||||
|
||||
def get_tools(
|
||||
self,
|
||||
apps: Optional[List[str]] = None,
|
||||
actions: Optional[List[str]] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
user_id: Optional[str] = None,
|
||||
) -> List[Callable]:
|
||||
"""Fetch Composio tools as agent-callable Python functions."""
|
||||
is_available, error = _check_composio_available(self.api_key)
|
||||
if not is_available:
|
||||
logger.error(error)
|
||||
return []
|
||||
|
||||
try:
|
||||
client = self._get_client()
|
||||
|
||||
if hasattr(client, "tools") and hasattr(client.tools, "get"):
|
||||
# Composio v3 SDK: `apps` -> `toolkits`, `user_id` required.
|
||||
# The API key is an auth credential, not an entity identifier,
|
||||
# so never use it as a user_id fallback.
|
||||
kwargs: Dict[str, Any] = {"user_id": user_id or "default"}
|
||||
if apps:
|
||||
kwargs["toolkits"] = apps
|
||||
if actions:
|
||||
kwargs["tools"] = actions
|
||||
if tags:
|
||||
kwargs["tags"] = tags
|
||||
tools = client.tools.get(**kwargs)
|
||||
else:
|
||||
# Legacy ComposioToolSet SDK.
|
||||
toolset_kwargs: Dict[str, Any] = {}
|
||||
if apps:
|
||||
toolset_kwargs["apps"] = apps
|
||||
if actions:
|
||||
toolset_kwargs["actions"] = actions
|
||||
if tags:
|
||||
toolset_kwargs["tags"] = tags
|
||||
tools = client.get_tools(**toolset_kwargs)
|
||||
|
||||
return list(tools) if tools else []
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Composio get_tools error: {e}")
|
||||
return []
|
||||
|
||||
def list_apps(self) -> List[str]:
|
||||
"""List available Composio app slugs."""
|
||||
is_available, error = _check_composio_available(self.api_key)
|
||||
if not is_available:
|
||||
logger.error(error)
|
||||
return []
|
||||
|
||||
try:
|
||||
client = self._get_client()
|
||||
|
||||
if hasattr(client, "apps") and hasattr(client.apps, "get"):
|
||||
apps = client.apps.get()
|
||||
elif hasattr(client, "get_apps"):
|
||||
apps = client.get_apps()
|
||||
else:
|
||||
return []
|
||||
|
||||
result = []
|
||||
for app in apps or []:
|
||||
if isinstance(app, str):
|
||||
slug = app
|
||||
elif isinstance(app, dict):
|
||||
slug = app.get("key") or app.get("name") or app.get("slug")
|
||||
else:
|
||||
slug = (
|
||||
getattr(app, "key", None)
|
||||
or getattr(app, "name", None)
|
||||
or getattr(app, "slug", None)
|
||||
)
|
||||
if slug:
|
||||
result.append(slug)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Composio list_apps error: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def composio_tools(
|
||||
apps: Optional[List[str]] = None,
|
||||
actions: Optional[List[str]] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
user_id: Optional[str] = None,
|
||||
api_key: Optional[str] = None,
|
||||
) -> List[Callable]:
|
||||
"""Load Composio tools as agent-callable Python functions."""
|
||||
return ComposioTools(api_key=api_key).get_tools(
|
||||
apps=apps, actions=actions, tags=tags, user_id=user_id
|
||||
)
|
||||
|
||||
|
||||
def composio_list_apps(api_key: Optional[str] = None) -> List[str]:
|
||||
"""List available Composio app slugs."""
|
||||
return ComposioTools(api_key=api_key).list_apps()
|
||||
|
||||
|
||||
class ComposioTool(BaseTool):
|
||||
"""Tool for Composio integrations."""
|
||||
|
||||
"""Single-tool wrapper for explicit Composio action execution."""
|
||||
|
||||
name = "composio"
|
||||
description = "Execute actions via Composio integrations."
|
||||
|
||||
|
||||
def __init__(self, api_key: Optional[str] = None):
|
||||
self.api_key = api_key or os.getenv("COMPOSIO_API_KEY")
|
||||
self._toolset = None
|
||||
super().__init__()
|
||||
|
||||
|
||||
@property
|
||||
def toolset(self):
|
||||
if self._toolset is None:
|
||||
try:
|
||||
from composio import ComposioToolSet
|
||||
except ImportError:
|
||||
raise ImportError("composio-core not installed. Install with: pip install composio-core")
|
||||
raise ImportError(
|
||||
"composio not installed. Install with: pip install praisonai-tools[composio]"
|
||||
)
|
||||
self._toolset = ComposioToolSet(api_key=self.api_key)
|
||||
return self._toolset
|
||||
|
||||
|
||||
def run(
|
||||
self,
|
||||
action: str = "execute",
|
||||
action_name: Optional[str] = None,
|
||||
params: Optional[Dict] = None,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
) -> Union[str, Dict[str, Any], List[Dict[str, Any]]]:
|
||||
if action == "execute":
|
||||
return self.execute(action_name=action_name, params=params or kwargs)
|
||||
elif action == "list_actions":
|
||||
if action == "list_actions":
|
||||
return self.list_actions(app=kwargs.get("app"))
|
||||
return {"error": f"Unknown action: {action}"}
|
||||
|
||||
|
||||
def execute(self, action_name: str, params: Dict = None) -> Dict[str, Any]:
|
||||
"""Execute a Composio action."""
|
||||
if not action_name:
|
||||
return {"error": "action_name is required"}
|
||||
|
||||
|
||||
try:
|
||||
result = self.toolset.execute_action(action_name, params or {})
|
||||
return {"result": result}
|
||||
except Exception as e:
|
||||
logger.error(f"Composio execute error: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def list_actions(self, app: str = None) -> List[Dict[str, Any]]:
|
||||
"""List available actions."""
|
||||
try:
|
||||
@@ -81,5 +224,5 @@ class ComposioTool(BaseTool):
|
||||
|
||||
|
||||
def composio_execute(action_name: str, params: Dict = None) -> Dict[str, Any]:
|
||||
"""Execute Composio action."""
|
||||
"""Execute a Composio action."""
|
||||
return ComposioTool().execute(action_name=action_name, params=params)
|
||||
@@ -61,6 +61,9 @@ video-motion = [
|
||||
"playwright>=1.40",
|
||||
"imageio-ffmpeg>=0.5",
|
||||
]
|
||||
composio = [
|
||||
"composio>=0.5.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://docs.praison.ai"
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Unit tests for Composio tools."""
|
||||
|
||||
import os
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from praisonai_tools.tools.composio_tool import (
|
||||
ComposioTool,
|
||||
ComposioTools,
|
||||
_check_composio_available,
|
||||
composio_execute,
|
||||
composio_list_apps,
|
||||
composio_tools,
|
||||
)
|
||||
|
||||
|
||||
class TestCheckComposioAvailable:
|
||||
def test_missing_package(self):
|
||||
with patch("praisonai_tools.tools.composio_tool.util.find_spec", return_value=None):
|
||||
ok, msg = _check_composio_available()
|
||||
assert ok is False
|
||||
assert "not installed" in msg
|
||||
|
||||
def test_missing_api_key(self):
|
||||
with patch("praisonai_tools.tools.composio_tool.util.find_spec", return_value=MagicMock()):
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
ok, msg = _check_composio_available()
|
||||
assert ok is False
|
||||
assert "COMPOSIO_API_KEY" in msg
|
||||
|
||||
def test_explicit_api_key_overrides_env(self):
|
||||
with patch("praisonai_tools.tools.composio_tool.util.find_spec", return_value=MagicMock()):
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
ok, msg = _check_composio_available(api_key="explicit-key")
|
||||
assert ok is True
|
||||
assert msg is None
|
||||
|
||||
def test_available(self):
|
||||
with patch("praisonai_tools.tools.composio_tool.util.find_spec", return_value=MagicMock()):
|
||||
with patch.dict(os.environ, {"COMPOSIO_API_KEY": "test-key"}, clear=True):
|
||||
ok, msg = _check_composio_available()
|
||||
assert ok is True
|
||||
assert msg is None
|
||||
|
||||
|
||||
class TestComposioTools:
|
||||
def test_get_tools_unavailable_returns_empty(self):
|
||||
with patch(
|
||||
"praisonai_tools.tools.composio_tool._check_composio_available",
|
||||
return_value=(False, "missing"),
|
||||
):
|
||||
assert ComposioTools().get_tools(apps=["github"]) == []
|
||||
|
||||
def test_get_tools_new_sdk(self):
|
||||
mock_tool = MagicMock()
|
||||
mock_client = MagicMock()
|
||||
mock_client.tools.get.return_value = [mock_tool]
|
||||
|
||||
with patch(
|
||||
"praisonai_tools.tools.composio_tool._check_composio_available",
|
||||
return_value=(True, None),
|
||||
):
|
||||
tools = ComposioTools(api_key="k")
|
||||
with patch.object(tools, "_get_client", return_value=mock_client):
|
||||
result = tools.get_tools(apps=["github"])
|
||||
|
||||
assert result == [mock_tool]
|
||||
mock_client.tools.get.assert_called_once_with(user_id="default", toolkits=["github"])
|
||||
|
||||
def test_get_tools_new_sdk_explicit_user_id(self):
|
||||
mock_tool = MagicMock()
|
||||
mock_client = MagicMock()
|
||||
mock_client.tools.get.return_value = [mock_tool]
|
||||
|
||||
with patch(
|
||||
"praisonai_tools.tools.composio_tool._check_composio_available",
|
||||
return_value=(True, None),
|
||||
):
|
||||
tools = ComposioTools(api_key="k")
|
||||
with patch.object(tools, "_get_client", return_value=mock_client):
|
||||
result = tools.get_tools(apps=["github"], user_id="user-123")
|
||||
|
||||
assert result == [mock_tool]
|
||||
mock_client.tools.get.assert_called_once_with(user_id="user-123", toolkits=["github"])
|
||||
|
||||
def test_list_apps(self):
|
||||
app = MagicMock(key="github")
|
||||
mock_client = MagicMock()
|
||||
mock_client.apps.get.return_value = [app]
|
||||
|
||||
with patch(
|
||||
"praisonai_tools.tools.composio_tool._check_composio_available",
|
||||
return_value=(True, None),
|
||||
):
|
||||
tools = ComposioTools(api_key="k")
|
||||
with patch.object(tools, "_get_client", return_value=mock_client):
|
||||
assert tools.list_apps() == ["github"]
|
||||
|
||||
|
||||
class TestStandaloneFunctions:
|
||||
def test_composio_tools_delegates(self):
|
||||
with patch.object(ComposioTools, "get_tools", return_value=["t1"]) as mock_get:
|
||||
assert composio_tools(apps=["slack"]) == ["t1"]
|
||||
mock_get.assert_called_once_with(apps=["slack"], actions=None, tags=None, user_id=None)
|
||||
|
||||
def test_composio_list_apps_delegates(self):
|
||||
with patch.object(ComposioTools, "list_apps", return_value=["github"]) as mock_list:
|
||||
assert composio_list_apps() == ["github"]
|
||||
mock_list.assert_called_once_with()
|
||||
|
||||
|
||||
class TestComposioTool:
|
||||
def test_execute_requires_action_name(self):
|
||||
assert ComposioTool().execute(action_name="", params={}) == {"error": "action_name is required"}
|
||||
|
||||
def test_composio_execute_delegates(self):
|
||||
with patch.object(ComposioTool, "execute", return_value={"result": "ok"}) as mock_exec:
|
||||
assert composio_execute("GITHUB_STAR", {"repo": "x"}) == {"result": "ok"}
|
||||
mock_exec.assert_called_once_with(action_name="GITHUB_STAR", params={"repo": "x"})
|
||||
Reference in new issue
Block a user