Skip to content

Commit

Permalink
Merge branch 'master' into kaavee/download
Browse files Browse the repository at this point in the history
  • Loading branch information
kaavee315 committed Jun 8, 2024
2 parents 8d62402 + 59e9b1b commit facbf5a
Show file tree
Hide file tree
Showing 50 changed files with 514 additions and 253 deletions.
2 changes: 1 addition & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ ignore-patterns=
ignore=

[MESSAGES CONTROL]
disable=C0114,C0115,C0116,C0301,C0103,W0603,R1715,W0621,R0903,W0237,W0511,W0622,R0913,R0902,W0221,C0302,R0801,C0411,C0412
disable=C0114,C0115,C0116,C0301,C0103,W0603,R1715,W0621,R0903,W0237,W0511,W0622,R0913,R0902,W0221,C0302,R0801,C0411,C0412,W0719,W0718,R0914,R0916,R0912,R0911,W0102

# To discuss
# W0621: redefined-outer-name
Expand Down
54 changes: 28 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<p align="center">
<picture width="200">
<source media="(prefers-color-scheme: dark)" width="172" srcset="https://mintlify.s3-us-west-1.amazonaws.com/composio-27/logo/dark.svg">
<img alt="Composio" width="172" src="https://mintlify.s3-us-west-1.amazonaws.com/composio-27/logo/light.svg"/>
</picture>
<a href="https://composio.dev//#gh-dark-mode-only">
<img src="./docs/imgs/composio_white_font.svg" width="318px" alt="Composio logo" />
</a>
<a href="https://composio.dev//#gh-light-mode-only">
<img src="./docs/imgs/composio_black_font.svg" width="318px" alt="Composio Logo" />
</a>
</p>
<p align="center">
<a href="https://github.com/composiodev/composio/actions/workflows/common.yml">
Expand All @@ -17,9 +19,9 @@

</p>

<h2 align="center">
<h2 align="center"><i>
Production Ready Toolset for AI Agents
</h2>
</i></h2>

<h4 align="center">Equip your agent with high-quality tools & integrations without worrying about authentication, accuracy, and reliability in a single line of code!
</h4>
Expand All @@ -29,7 +31,7 @@
</p>

<p>
<a href="https://app.composio.dev">Dashboard</a> <b>|</b>
<a href="https://app.composio.dev">Try on Dashboard</a> <b>|</b>
<a href="https://www.composio.dev">Homepage</a> <b>|</b>
<!-- <a href="https://docs.composio.dev/guides/examples">Examples</a> |
<a href="https://docs.composio.dev/chat-with-docs">Chat with Docs</a> | -->
Expand Down Expand Up @@ -59,10 +61,10 @@

- [📋 Table of contents](#-table-of-contents)
- [🤔 Why Composio?](#-why-composio)
- [🔥 Key Features](#-key-features)
- [🚀 Getting Started](#-getting-started)
- [1. Installation](#1-installation)
- [2. Testing Composio in Action](#2-testing-composio-in-action)
- [🔥 Key Features](#-key-features)
- [💡 Examples](#-examples)
- [Competitor Researcher](#competitor-researcher)
- [Todolist to Calendar](#todolist-to-calendar)
Expand All @@ -78,7 +80,24 @@
We believe AI Based Agents/Workflows are the future.
Composio is the best toolset to integrate AI Agents to best Agentic Tools and use them to accomplish tasks.

<img alt="Illustration" src="./docs/imgs/banner.png" style="border-radius: 5px"/>
<img alt="Illustration" src="./docs/imgs/banner.gif" style="border-radius: 5px"/>

## 🔥 Key Features

- **100+ Tools**: Support for a range of different categories

- **Softwares**: Do anything on GitHub, Notion, Linear, Gmail, Slack, Hubspot, Salesforce, & 90 more.
- **OS**: Click anywhere, Type anything, Copy to Clipboard, & more.
- **Browser**: Smart Search, Take a screenshot, MultiOn, Download, Upload, & more.
- **Search**: Google Search, Perplexity Search, Tavily, Exa & more.
- **SWE**: Ngrok, Database, Redis, Vercel, Git, etc.
- **RAG**: Agentic RAG for any type of data on the fly!

- **Frameworks**: Use tools with agent frameworks like **OpenAI, Claude, LlamaIndex, Langchain, CrewAI, Autogen, Gemini, Julep, Lyzr**, and more in a single line of code.
- **Managed Authorisation**: Supports six different auth protocols. _Access Token, Refresh token, OAuth, API Keys, JWT, and more_ abstracted out so you can focus on the building agents.
- **Accuracy**: Get _upto 40% better agentic accuracy_ in your tool calls due to better tool designs.
- **Embeddable**: Whitelabel in the backend of your applications managing Auth & Integrations for all your users & agents and maintain a consistent experience.
- **Pluggable**: Designed to be extended with additional Tools, Frameworks and Authorisation Protocols very easily.

## 🚀 Getting Started

Expand Down Expand Up @@ -156,23 +175,6 @@ response_after_tool_calls = composio_tool_set.wait_and_handle_assistant_tool_cal
print(response_after_tool_calls)
```

## 🔥 Key Features

- **100+ Tools**: Support for a range of different categories

- **Softwares**: Do anything on GitHub, Notion, Linear, Gmail, Slack, Hubspot, Salesforce, & 90 more.
- **OS**: Click anywhere, Type anything, Copy to Clipboard, & more.
- **Browser**: Smart Search, Take a screenshot, MultiOn, Download, Upload, & more.
- **Search**: Google Search, Perplexity Search, Tavily, Exa & more.
- **SWE**: Ngrok, Database, Redis, Vercel, Git, etc.
- **RAG**: Agentic RAG for any type of data on the fly!

- **Frameworks**: Use tools with agent frameworks like **OpenAI, Claude, LlamaIndex, Langchain, CrewAI, Autogen, Gemini, Julep, Lyzr**, and more in a single line of code.
- **Managed Authorisation**: Supports six different auth protocols. _Access Token, Refresh token, OAuth, API Keys, JWT, and more_ abstracted out so you can focus on the building agents.
- **Accuracy**: Get _upto 40% better agentic accuracy_ in your tool calls due to better tool designs.
- **Embeddable**: Whitelabel in the backend of your applications managing Auth & Integrations for all your users & agents and maintain a consistent experience.
- **Pluggable**: Designed to be extended with additional Tools, Frameworks and Authorisation Protocols very easily.

## 💡 Examples

### [Competitor Researcher](https://docs.composio.dev/guides/examples/CompetitorResearcher)
Expand Down
1 change: 1 addition & 0 deletions composio/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from composio.cli.triggers import _triggers
from composio.cli.whoami import _whoami
from composio.core.cls.did_you_mean import DYMGroup
from composio.cli.utils import HelpfulCmdBase


class HelpDYMGroup(DYMGroup):
Expand Down
1 change: 0 additions & 1 deletion composio/cli/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,6 @@ def _update(context: Context, beta: bool = False) -> None:
key=lambda x: x.appKey,
)
if not beta:
c = []

def filter_non_beta_items(items):
filtered_items = []
Expand Down
1 change: 1 addition & 0 deletions composio/cli/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .helpfulcmd import HelpfulCmdBase
93 changes: 80 additions & 13 deletions composio/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
import time
import typing as t
import warnings
from datetime import datetime

import base64
import requests

from datetime import datetime
from pydantic import BaseModel, ConfigDict

from composio.client.endpoints import Endpoint, v1
Expand Down Expand Up @@ -521,17 +522,26 @@ class ActiveTriggers(Collection[ActiveTriggerModel]):
def get( # type: ignore
self,
trigger_ids: t.Optional[t.List[str]] = None,
connected_account_ids: t.Optional[t.List[str]] = None,
integration_ids: t.Optional[t.List[str]] = None,
trigger_names: t.Optional[t.List[str]] = None,
) -> t.List[ActiveTriggerModel]:
"""List active triggers."""
trigger_ids = trigger_ids or []
connected_account_ids = connected_account_ids or []
integration_ids = integration_ids or []
trigger_names = trigger_names or []
queries = {}
if len(trigger_ids) > 0:
queries["triggerIds"] = ",".join(trigger_ids)
if len(connected_account_ids) > 0:
queries["connectedAccountIds"] = ",".join(connected_account_ids)
if len(integration_ids) > 0:
queries["integrationIds"] = ",".join(integration_ids)
if len(trigger_names) > 0:
queries["triggerNames"] = ",".join(trigger_names)
return self._raise_if_empty(
super().get(
queries=(
{"triggerIds": ",".join(trigger_ids)}
if len(trigger_ids) > 0
else {}
)
)
super().get(queries=queries)
)


Expand All @@ -542,6 +552,8 @@ class ActionParameterPropertyModel(BaseModel):
description: t.Optional[str] = None
title: t.Optional[str] = None
type: t.Optional[str] = None
oneOf: t.Optional[t.List["ActionParameterPropertyModel"]] = None
file_readable: t.Optional[bool] = False


class ActionParametersModel(BaseModel):
Expand Down Expand Up @@ -744,13 +756,32 @@ def execute(
action=action,
request_data=params,
)
actionsResp = self.client.actions.get(actions=[action])
if len(actionsResp) == 0:
raise ComposioClientError(f"Action {action} not found")
action_model = actionsResp[0]
action_req_schema = action_model.parameters.properties
modified_params = {}
for param, value in params.items():
file_readable = action_req_schema[param].file_readable or False
if file_readable and isinstance(value, str) and os.path.isfile(value):
with open(value, 'rb') as file:
file_content = file.read()
try:
file_content.decode('utf-8') # Try decoding as UTF-8 to check if it's normal text
modified_params[param] = file_content.decode('utf-8')
except UnicodeDecodeError:
# If decoding fails, treat as binary and encode in base64
modified_params[param] = base64.b64encode(file_content).decode('utf-8')
else:
modified_params[param] = value
if action.no_auth:
return self._raise_if_required(
self.client.http.post(
url=str(self.endpoint / action.action / "execute"),
json={
"appName": action.app,
"input": params,
"input": modified_params,
"entityId": entity_id,
},
)
Expand All @@ -767,7 +798,7 @@ def execute(
url=str(self.endpoint / action.action / "execute"),
json={
"connectedAccountId": connected_account,
"input": params,
"input": modified_params,
"entityId": entity_id,
},
)
Expand Down Expand Up @@ -975,13 +1006,13 @@ def execute(

def get_connection(
self,
app: t.Optional[str] = None,
app: t.Optional[t.Union[str, App]] = None,
connected_account_id: t.Optional[str] = None,
) -> ConnectedAccountModel:
"""
Get connected account for an action.
:param action: Action type enum
:param app: App name
:param connected_account_id: Connected account ID to use as filter
:return: Connected account object
:raises: If no connected account found for given entity ID
Expand Down Expand Up @@ -1013,6 +1044,42 @@ def get_connection(
)
return latest_account

def get_connections(self) -> t.List[ConnectedAccountModel]:
"""
Get all connections for an entity.
"""
return self.client.connected_accounts.get(entity_ids=[self.id], active=True)

def enable_trigger(self, app: t.Union[str, App], trigger_name: str, config: t.Dict[str, t.Any]) -> t.Dict:
"""
Enable a trigger for an entity.
:param app: App name
:param trigger_name: Trigger name
:param config: Trigger config
"""
connected_account = self.get_connection(app=app)
return self.client.triggers.enable(
name=trigger_name,
connected_account_id=connected_account.id,
config=config,
)

def disable_trigger(self, trigger_id: str) -> t.Dict:
"""
Disable a trigger for an entity.
:param trigger_id: Trigger ID
"""
return self.client.triggers.disable(id=trigger_id)

def get_active_triggers(self) -> t.List[ActiveTriggerModel]:
"""
Get all active triggers for an entity.
"""
connected_accounts = self.get_connections()
return self.client.active_triggers.get(connected_account_ids=[connected_account.id for connected_account in connected_accounts])

def initiate_connection(
self,
app_name: t.Union[str, App],
Expand Down
3 changes: 3 additions & 0 deletions composio/client/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@
class Tag(tuple, Enum):
"""App tags."""

# pylint: disable=function-redefined,invalid-overridden-method
@property
def name(self) -> str:
"""Returns trigger name."""
return self.value[0]

# pylint: enable=function-redefined,invalid-overridden-method

IMPORTANT = ("default", "important")
ASANA_JOBS = ("asana", "Jobs")
ASANA_TEAM_MEMBERSHIPS = ("asana", "Team memberships")
Expand Down
2 changes: 0 additions & 2 deletions composio/client/local_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,9 @@ def get_list_of_action_schemas(self, apps=[], actions=[], tags=[]):
action_obj = tool_obj.get_actions_dict()[action.value[1]]
all_action_objs.append(action_obj)

# all_action_objs = list(set(all_action_objs))
all_action_schemas = [
action_obj.get_action_schema() for action_obj in all_action_objs
]
# all_action_schemas = list(set(all_action_schemas))

all_action_schemas = list(
{
Expand Down
44 changes: 35 additions & 9 deletions composio/core/local/action.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import hashlib
import json
from abc import ABC, abstractmethod
from typing import List

import inflection
import jsonref
import os
import base64

from abc import ABC, abstractmethod
from typing import List
from pydantic import BaseModel


Expand Down Expand Up @@ -82,6 +84,16 @@ def get_tool_merged_action_name(self) -> str:
return f"{self._tool_name}_{inflection.underscore(self.action_name)}"

def get_action_schema(self):
request_schema_json = self.request_schema.model_json_schema(by_alias=False)
modified_properties = request_schema_json.get('properties', {})
for param, details in modified_properties.items():
if details.get('file_readable', False):
details['oneOf'] = [
{'type': details.get('type'), 'description': details.get('description', '')},
{'type': 'string', 'format': 'file-path', 'description': f"File path to {details.get('description', '')}"}
]
del details['type'] # Remove original type to avoid conflict in oneOf
request_schema_json['properties'] = modified_properties
action_schema = {
"appKey": self._tool_name,
"appName": self._tool_name,
Expand All @@ -92,14 +104,11 @@ def get_action_schema(self):
"tags": self.tags, # type: ignore
"enabled": True,
"description": self.__class__.__doc__ if self.__class__.__doc__ else self.action_name, # type: ignore
"parameters": jsonref.loads(
json.dumps(self.request_schema.model_json_schema(by_alias=False))
),
"parameters": jsonref.loads(json.dumps(request_schema_json)),
"response": jsonref.loads(
json.dumps(self.response_schema.model_json_schema())
),
}

return action_schema

def execute_action(self, request_data: dict, metadata: dict):
Expand All @@ -108,16 +117,33 @@ def execute_action(self, request_data: dict, metadata: dict):
# print(f"Executing {self.__class__.__name__} on Tool: {self.tool_name} with request data {request_data} and meta data {metadata}")
try:
request_schema = self.request_schema # type: ignore
req = request_schema.model_validate_json(json_data=json.dumps(request_data))
modified_request_data = {}

for param, value in request_data.items():
annotations = request_schema.model_fields[param].json_schema_extra
file_readable = annotations is not None and annotations.get('file_readable', False)
if file_readable and isinstance(value, str) and os.path.isfile(value):
with open(value, 'rb') as file:
file_content = file.read()
try:
file_content.decode('utf-8') # Try decoding as UTF-8 to check if it's normal text
modified_request_data[param] = file_content.decode('utf-8')
except UnicodeDecodeError:
# If decoding fails, treat as binary and encode in base64
modified_request_data[param] = base64.b64encode(file_content).decode('utf-8')
else:
modified_request_data[param] = value

req = request_schema.model_validate_json(json_data=json.dumps(modified_request_data))
return self.execute(req, metadata) # type: ignore
except json.JSONDecodeError as e:
# logger.error(f"Error executing {action.__name__} on Tool: {tool_name}: {e}\n{traceback.format_exc()}")
return {
"status": "failure",
"details": f"Could not parse response with error: {e}. Please contact the tool developer.",
}
except Exception as e:
# logger.error(f"Error executing {action.__name__} on Tool: {tool_name}: {e}\n{traceback.format_exc()}")
except Exception as e:
return {
"status": "failure",
"details": "Error executing action with error: " + str(e),
Expand Down
Loading

0 comments on commit facbf5a

Please sign in to comment.