composio-llamaindex 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of composio-llamaindex might be problematic. Click here for more details.
- composio-llamaindex-0.0.1/PKG-INFO +32 -0
- composio-llamaindex-0.0.1/README.md +13 -0
- composio-llamaindex-0.0.1/composio-llamaindex +136 -0
- composio-llamaindex-0.0.1/composio_llamaindex.egg-info/PKG-INFO +32 -0
- composio-llamaindex-0.0.1/composio_llamaindex.egg-info/SOURCES.txt +15 -0
- composio-llamaindex-0.0.1/composio_llamaindex.egg-info/dependency_links.txt +1 -0
- composio-llamaindex-0.0.1/composio_llamaindex.egg-info/requires.txt +6 -0
- composio-llamaindex-0.0.1/composio_llamaindex.egg-info/top_level.txt +1 -0
- composio-llamaindex-0.0.1/lib/__init__.py +3 -0
- composio-llamaindex-0.0.1/lib/composio_tool_spec.py +86 -0
- composio-llamaindex-0.0.1/lib/composio_toolset.py +43 -0
- composio-llamaindex-0.0.1/lib/openapi_tool_spec.py +99 -0
- composio-llamaindex-0.0.1/lib/storage.py +15 -0
- composio-llamaindex-0.0.1/lib/test_composio_tool_spec.py +64 -0
- composio-llamaindex-0.0.1/pyproject.toml +15 -0
- composio-llamaindex-0.0.1/setup.cfg +4 -0
- composio-llamaindex-0.0.1/setup.py +29 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: composio-llamaindex
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Provides integrations skill with 50+ services in LlamaIndex
|
|
5
|
+
Home-page: https://github.com/SamparkAI/llama-composio
|
|
6
|
+
Author: Karan
|
|
7
|
+
Author-email: karan@composio.dev
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.7
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: requests
|
|
14
|
+
Requires-Dist: jsonschema
|
|
15
|
+
Requires-Dist: argparse
|
|
16
|
+
Requires-Dist: llama_index
|
|
17
|
+
Requires-Dist: nest_asyncio
|
|
18
|
+
Requires-Dist: beaupy===3.7.2
|
|
19
|
+
|
|
20
|
+
Composio <> Llama Indxex
|
|
21
|
+
Use Composio to get an array of tools with your llamaindex agent.
|
|
22
|
+
|
|
23
|
+
# Usage
|
|
24
|
+
Inside your llamaindex codebase:
|
|
25
|
+
```
|
|
26
|
+
from composio_llama import ComposioToolset
|
|
27
|
+
toolset = ComposioToolset(<token>)
|
|
28
|
+
agent = OpenAIAgent.from_tools(toolset.to_tool_list(), llm=llm, verbose=True)
|
|
29
|
+
...
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Composio <> Llama Indxex
|
|
2
|
+
Use Composio to get an array of tools with your llamaindex agent.
|
|
3
|
+
|
|
4
|
+
# Usage
|
|
5
|
+
Inside your llamaindex codebase:
|
|
6
|
+
```
|
|
7
|
+
from composio_llama import ComposioToolset
|
|
8
|
+
toolset = ComposioToolset(<token>)
|
|
9
|
+
agent = OpenAIAgent.from_tools(toolset.to_tool_list(), llm=llm, verbose=True)
|
|
10
|
+
...
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import webbrowser
|
|
5
|
+
import requests
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from lib.storage import save_user_id, get_user_id
|
|
9
|
+
import time
|
|
10
|
+
from beaupy.spinners import Spinner, BARS
|
|
11
|
+
|
|
12
|
+
COMPOSIO_TOKEN = 'ghp_1J2g3h4i5j6k7l8m9n0o33'
|
|
13
|
+
BASE_URL = "https://hermes-production-6901.up.railway.app/api"
|
|
14
|
+
|
|
15
|
+
def auth_tool(args):
|
|
16
|
+
user_id = get_user_id()
|
|
17
|
+
if user_id is None:
|
|
18
|
+
print("Error: No authenticated user found. Please authenticate first.")
|
|
19
|
+
return
|
|
20
|
+
print(f"Authenticating tool: {args.tool_name} for User ID: {user_id}")
|
|
21
|
+
url = f"{BASE_URL}/user/auth"
|
|
22
|
+
payload = json.dumps({
|
|
23
|
+
"endUserID": user_id,
|
|
24
|
+
"provider": {
|
|
25
|
+
"name": args.tool_name,
|
|
26
|
+
"scope": [
|
|
27
|
+
"user.email",
|
|
28
|
+
"user.profile"
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
"redirectURIClient": "https://composio.dev/"
|
|
32
|
+
})
|
|
33
|
+
headers = {
|
|
34
|
+
'X_COMPOSIO_TOKEN': COMPOSIO_TOKEN,
|
|
35
|
+
'Content-Type': 'application/json'
|
|
36
|
+
}
|
|
37
|
+
response = requests.request("POST", url, headers=headers, data=payload)
|
|
38
|
+
if response.status_code == 200:
|
|
39
|
+
auth_url = response.json().get('providerAuthURL')
|
|
40
|
+
connReqID = response.json().get('connectionReqId')
|
|
41
|
+
print(f"Auth URL: {auth_url}")
|
|
42
|
+
print(f"Connection Request ID: {connReqID}")
|
|
43
|
+
webbrowser.open(auth_url)
|
|
44
|
+
else:
|
|
45
|
+
print("Failed to authenticate tool.")
|
|
46
|
+
spinner = Spinner(BARS, f"Authenticating you...")
|
|
47
|
+
spinner.start()
|
|
48
|
+
|
|
49
|
+
while True: # Loop forever
|
|
50
|
+
status_check_url = f"{BASE_URL}/auth/{connReqID}/status"
|
|
51
|
+
status_payload = json.dumps({})
|
|
52
|
+
status_headers = {
|
|
53
|
+
'X_COMPOSIO_TOKEN': COMPOSIO_TOKEN,
|
|
54
|
+
'Content-Type': 'application/json',
|
|
55
|
+
}
|
|
56
|
+
response = requests.request("GET", status_check_url, headers=status_headers, data=status_payload)
|
|
57
|
+
status_data = response.json()
|
|
58
|
+
status = status_data.get('status')
|
|
59
|
+
if status == 'PENDING' or status == 'STARTED':
|
|
60
|
+
time.sleep(1) # Wait for 5 seconds before retrying
|
|
61
|
+
else:
|
|
62
|
+
print(f"Authentication status: {status_data.get('status')}")
|
|
63
|
+
break
|
|
64
|
+
|
|
65
|
+
spinner.stop()
|
|
66
|
+
|
|
67
|
+
def list_tools(args):
|
|
68
|
+
user_id = get_user_id()
|
|
69
|
+
if user_id is None:
|
|
70
|
+
print("Error: No authenticated user found. Please authenticate first.")
|
|
71
|
+
return
|
|
72
|
+
print(f"Fetching tools for User ID: {user_id}...")
|
|
73
|
+
url = f"{BASE_URL}/all_tools"
|
|
74
|
+
headers = {
|
|
75
|
+
'X_COMPOSIO_TOKEN': COMPOSIO_TOKEN,
|
|
76
|
+
'X_ENDUSER_ID': user_id
|
|
77
|
+
}
|
|
78
|
+
response = requests.post(url, headers=headers, json={
|
|
79
|
+
"skip": 0,
|
|
80
|
+
"limit": 1000,
|
|
81
|
+
"actions": True,
|
|
82
|
+
"triggers": False
|
|
83
|
+
})
|
|
84
|
+
if response.status_code == 200:
|
|
85
|
+
tools_data = response.json()
|
|
86
|
+
for tool in tools_data.get('tools', []):
|
|
87
|
+
print(f"Name: {tool['Name']}, Description: {tool['Description']}")
|
|
88
|
+
if args.auth and tool['Authentication']['isAuthenticated'] == "False":
|
|
89
|
+
print(f"Tool {tool['Name']} requires authentication.")
|
|
90
|
+
else:
|
|
91
|
+
print("Failed to fetch tools.")
|
|
92
|
+
|
|
93
|
+
def auth(args):
|
|
94
|
+
if not args.email:
|
|
95
|
+
print("Error: Email ID is required for authentication.")
|
|
96
|
+
return
|
|
97
|
+
print(f"Authenticating {args.email}...")
|
|
98
|
+
url = f"{BASE_URL}/user/create/{args.email}"
|
|
99
|
+
headers = {
|
|
100
|
+
'X_COMPOSIO_TOKEN': COMPOSIO_TOKEN
|
|
101
|
+
}
|
|
102
|
+
response = requests.post(url, headers=headers)
|
|
103
|
+
if response.status_code == 200:
|
|
104
|
+
user_id = response.json().get('userId')
|
|
105
|
+
print(f"Authenticated successfully. User ID: {user_id}")
|
|
106
|
+
save_user_id(user_id)
|
|
107
|
+
else:
|
|
108
|
+
print("Authentication failed.")
|
|
109
|
+
|
|
110
|
+
def main():
|
|
111
|
+
parser = argparse.ArgumentParser(description='Composio CLI')
|
|
112
|
+
subparsers = parser.add_subparsers(help='commands')
|
|
113
|
+
|
|
114
|
+
# Auth command
|
|
115
|
+
auth_parser = subparsers.add_parser('auth', help='Authenticate user')
|
|
116
|
+
auth_parser.add_argument('email', type=str, help='User email ID for authentication')
|
|
117
|
+
auth_parser.set_defaults(func=auth)
|
|
118
|
+
|
|
119
|
+
# List tools command
|
|
120
|
+
list_tools_parser = subparsers.add_parser('list_tools', help='List tools')
|
|
121
|
+
list_tools_parser.add_argument('--auth', action='store_true', help='List only authenticated tools')
|
|
122
|
+
list_tools_parser.set_defaults(func=list_tools)
|
|
123
|
+
|
|
124
|
+
# Auth tool command
|
|
125
|
+
auth_tool_parser = subparsers.add_parser('auth_tool', help='Authenticate a tool')
|
|
126
|
+
auth_tool_parser.add_argument('tool_name', type=str, help='Name of the tool to authenticate')
|
|
127
|
+
auth_tool_parser.set_defaults(func=auth_tool)
|
|
128
|
+
|
|
129
|
+
args = parser.parse_args()
|
|
130
|
+
if hasattr(args, 'func'):
|
|
131
|
+
args.func(args)
|
|
132
|
+
else:
|
|
133
|
+
parser.print_help()
|
|
134
|
+
|
|
135
|
+
if __name__ == '__main__':
|
|
136
|
+
main()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: composio-llamaindex
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Provides integrations skill with 50+ services in LlamaIndex
|
|
5
|
+
Home-page: https://github.com/SamparkAI/llama-composio
|
|
6
|
+
Author: Karan
|
|
7
|
+
Author-email: karan@composio.dev
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.7
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: requests
|
|
14
|
+
Requires-Dist: jsonschema
|
|
15
|
+
Requires-Dist: argparse
|
|
16
|
+
Requires-Dist: llama_index
|
|
17
|
+
Requires-Dist: nest_asyncio
|
|
18
|
+
Requires-Dist: beaupy===3.7.2
|
|
19
|
+
|
|
20
|
+
Composio <> Llama Indxex
|
|
21
|
+
Use Composio to get an array of tools with your llamaindex agent.
|
|
22
|
+
|
|
23
|
+
# Usage
|
|
24
|
+
Inside your llamaindex codebase:
|
|
25
|
+
```
|
|
26
|
+
from composio_llama import ComposioToolset
|
|
27
|
+
toolset = ComposioToolset(<token>)
|
|
28
|
+
agent = OpenAIAgent.from_tools(toolset.to_tool_list(), llm=llm, verbose=True)
|
|
29
|
+
...
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
composio-llamaindex
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.py
|
|
5
|
+
composio_llamaindex.egg-info/PKG-INFO
|
|
6
|
+
composio_llamaindex.egg-info/SOURCES.txt
|
|
7
|
+
composio_llamaindex.egg-info/dependency_links.txt
|
|
8
|
+
composio_llamaindex.egg-info/requires.txt
|
|
9
|
+
composio_llamaindex.egg-info/top_level.txt
|
|
10
|
+
lib/__init__.py
|
|
11
|
+
lib/composio_tool_spec.py
|
|
12
|
+
lib/composio_toolset.py
|
|
13
|
+
lib/openapi_tool_spec.py
|
|
14
|
+
lib/storage.py
|
|
15
|
+
lib/test_composio_tool_spec.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
lib
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Dict, Any, List, Union, Type
|
|
3
|
+
import logging
|
|
4
|
+
import requests
|
|
5
|
+
from inspect import Parameter, Signature
|
|
6
|
+
import types
|
|
7
|
+
|
|
8
|
+
from llama_index.core.tools.tool_spec.base import BaseToolSpec
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
BASE_URL = "https://hermes-production-6901.up.railway.app/api"
|
|
13
|
+
|
|
14
|
+
def map_composio_type_to_python(type_spec) -> Type:
|
|
15
|
+
if isinstance(type_spec, dict):
|
|
16
|
+
type_str = type_spec.get('type')
|
|
17
|
+
if type_str == 'string':
|
|
18
|
+
return str
|
|
19
|
+
elif type_str == 'number':
|
|
20
|
+
return float if '.' in str(type_spec.get('example', '')) else int
|
|
21
|
+
elif type_str == 'boolean':
|
|
22
|
+
return bool
|
|
23
|
+
# elif type_str == 'object':
|
|
24
|
+
# properties = type_spec.get('properties', {})
|
|
25
|
+
# required = type_spec.get('required', [])
|
|
26
|
+
# return Dict[str, Union[*tuple(map_composio_type_to_python(prop) for prop in properties.values()), Any]] if properties else Dict[str, Any]
|
|
27
|
+
# elif type_str == 'array':
|
|
28
|
+
# items_spec = type_spec.get('items', {})
|
|
29
|
+
# return List[map_composio_type_to_python(items_spec)] if items_spec else List[Any]
|
|
30
|
+
# Add more mappings as necessary
|
|
31
|
+
# Fallback or default type
|
|
32
|
+
return Any # Using Any for unspecified or complex types
|
|
33
|
+
|
|
34
|
+
class ComposioToolSpec(BaseToolSpec):
|
|
35
|
+
"""Generic tool spec based on composio_tool.json schema."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, tool_schema: str, composio_token: str, user_id: str) -> None:
|
|
38
|
+
"""Initialize with composio tool schema."""
|
|
39
|
+
self.tool_schema = json.loads(tool_schema)
|
|
40
|
+
self.spec_functions = self._generate_spec_functions()
|
|
41
|
+
|
|
42
|
+
def _generate_spec_functions(self) -> List[str]:
|
|
43
|
+
"""Generate spec functions based on the tools actions."""
|
|
44
|
+
spec_functions = []
|
|
45
|
+
for tool in self.tool_schema.get("tools", []):
|
|
46
|
+
for action in tool.get("Actions", []):
|
|
47
|
+
function_name = tool["Name"] + "_" + action["Id"]
|
|
48
|
+
spec_functions.append(function_name)
|
|
49
|
+
input_params = action["Signature"]["Input"]["properties"]
|
|
50
|
+
print("input_params:", input_params)
|
|
51
|
+
setattr(self, function_name, self._create_function(tool["Name"], action["Id"], function_name, action["Description"], input_params, action["Signature"]["Input"].get("required", [])))
|
|
52
|
+
return spec_functions
|
|
53
|
+
|
|
54
|
+
def _create_function(self, tool_name: str, action_id: str, function_name: str, description: str, input_params: Dict[str, Any], required_params: List[str] = []):
|
|
55
|
+
"""Create a function for the given action with typed arguments."""
|
|
56
|
+
|
|
57
|
+
# Function template that uses **kwargs to accept any arguments and performs an actual API call.
|
|
58
|
+
def template_function(**kwargs) -> Dict[str, Any]:
|
|
59
|
+
missing_params = [param for param in input_params if param not in kwargs and param in required_params]
|
|
60
|
+
if missing_params:
|
|
61
|
+
return {"error": f"Missing required params: {missing_params}"}
|
|
62
|
+
params = {param: kwargs[param] for param in input_params if param in kwargs}
|
|
63
|
+
logger.info(f"Executing action {action_id} with {params}")
|
|
64
|
+
|
|
65
|
+
request_body = json.dumps(params)
|
|
66
|
+
response = requests.post(f"{BASE_URL}/{tool_name}/{action_id}", data=request_body, headers={'Content-Type': 'application/json'})
|
|
67
|
+
return response.json()
|
|
68
|
+
|
|
69
|
+
parameters = [
|
|
70
|
+
Parameter(
|
|
71
|
+
name=param,
|
|
72
|
+
kind=Parameter.POSITIONAL_OR_KEYWORD,
|
|
73
|
+
annotation=map_composio_type_to_python(input_params[param]),
|
|
74
|
+
# default=Parameter.empty if param in required_params else Parameter.default
|
|
75
|
+
)
|
|
76
|
+
for param in input_params
|
|
77
|
+
]
|
|
78
|
+
new_sig = Signature(parameters, return_annotation=Dict[str, Any])
|
|
79
|
+
|
|
80
|
+
func = types.FunctionType(template_function.__code__, globals(), name=function_name, closure=template_function.__closure__)
|
|
81
|
+
|
|
82
|
+
# Assign the new signature to the function.
|
|
83
|
+
func.__signature__ = new_sig
|
|
84
|
+
func.__doc__ = description
|
|
85
|
+
|
|
86
|
+
return func
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import requests
|
|
3
|
+
from .composio_tool_spec import ComposioToolSpec
|
|
4
|
+
from llama_index.core.tools.tool_spec.base import BaseToolSpec
|
|
5
|
+
from .storage import get_user_id
|
|
6
|
+
from .composio_tool_spec import BASE_URL
|
|
7
|
+
|
|
8
|
+
COMPOSIO_TOKEN = "ghp_1J2g3h4i5j6k7l8m9n0o33"
|
|
9
|
+
|
|
10
|
+
class ComposioToolset(ComposioToolSpec):
|
|
11
|
+
def __init__(self):
|
|
12
|
+
self.user_id = get_user_id()
|
|
13
|
+
print(f"User ID: {self.user_id}")
|
|
14
|
+
self.authenticated_tools = self.get_authenticated_tools(self.user_id)
|
|
15
|
+
tools_schema = json.dumps({"tools": self.authenticated_tools}) # Combine all tools into a single schema
|
|
16
|
+
super().__init__(tools_schema, COMPOSIO_TOKEN, self.user_id)
|
|
17
|
+
|
|
18
|
+
def get_authenticated_tools(self, user_id: str):
|
|
19
|
+
print("Fetching tools...")
|
|
20
|
+
if user_id is None:
|
|
21
|
+
raise Exception("No authenticated user found. Please authenticate first.")
|
|
22
|
+
|
|
23
|
+
url = f"{BASE_URL}/all_tools"
|
|
24
|
+
headers = {
|
|
25
|
+
'X_COMPOSIO_TOKEN': COMPOSIO_TOKEN,
|
|
26
|
+
'X_ENDUSER_ID': user_id
|
|
27
|
+
}
|
|
28
|
+
response = requests.post(url, headers=headers, json={
|
|
29
|
+
"skip": 0,
|
|
30
|
+
"limit": 1000,
|
|
31
|
+
"actions": True,
|
|
32
|
+
"triggers": False
|
|
33
|
+
})
|
|
34
|
+
if response.status_code == 200:
|
|
35
|
+
tools_data = response.json()
|
|
36
|
+
authenticated_tools = [
|
|
37
|
+
tool for tool in tools_data.get('tools', [])
|
|
38
|
+
# if tool['Authentication']['isAuthenticated'] == "True"
|
|
39
|
+
]
|
|
40
|
+
print("Authenticated tools:", authenticated_tools)
|
|
41
|
+
return authenticated_tools
|
|
42
|
+
else:
|
|
43
|
+
raise Exception("Failed to fetch tools.")
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import yaml
|
|
3
|
+
import requests
|
|
4
|
+
from inspect import Parameter, Signature
|
|
5
|
+
from typing import List, Dict, Any
|
|
6
|
+
import types
|
|
7
|
+
import logging
|
|
8
|
+
import nest_asyncio
|
|
9
|
+
|
|
10
|
+
from llama_index.core.tools.tool_spec.base import BaseToolSpec
|
|
11
|
+
|
|
12
|
+
nest_asyncio.apply()
|
|
13
|
+
|
|
14
|
+
os.environ["OPENAI_API_KEY"] = "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"
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
def map_openapi_type_to_python(type_spec):
|
|
19
|
+
if isinstance(type_spec, dict):
|
|
20
|
+
type_str = type_spec.get('type')
|
|
21
|
+
if type_str == 'string':
|
|
22
|
+
return str
|
|
23
|
+
# Add more mappings as necessary
|
|
24
|
+
# Fallback or default type
|
|
25
|
+
return str # or any other default type
|
|
26
|
+
|
|
27
|
+
class APIToolSpec(BaseToolSpec):
|
|
28
|
+
"""Generic API tool spec based on OpenAPI schema."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, api_schema: str) -> None:
|
|
31
|
+
"""Initialize with OpenAPI schema."""
|
|
32
|
+
self.api_schema = yaml.safe_load(api_schema)
|
|
33
|
+
self.spec_functions = self._generate_spec_functions()
|
|
34
|
+
|
|
35
|
+
def _generate_spec_functions(self) -> List[str]:
|
|
36
|
+
"""Generate spec functions based on the OpenAPI paths."""
|
|
37
|
+
spec_functions = []
|
|
38
|
+
for path, methods in self.api_schema.get("paths", {}).items():
|
|
39
|
+
for method, details in methods.items():
|
|
40
|
+
print("details:", details)
|
|
41
|
+
function_name = self._generate_function_name(path, method)
|
|
42
|
+
spec_functions.append(function_name)
|
|
43
|
+
request_body_params = self._extract_request_body_params(details)
|
|
44
|
+
setattr(self, function_name, self._create_function(path, method, details.get("summary", ""), request_body_params))
|
|
45
|
+
return spec_functions
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def _generate_function_name(path: str, method: str) -> str:
|
|
49
|
+
"""Generate a function name based on the path and method."""
|
|
50
|
+
clean_path = path.strip("/").replace("/", "_")
|
|
51
|
+
return f"{method}_{clean_path}"
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def _extract_request_body_params(details: Dict[str, Any]) -> Dict[str, Any]:
|
|
55
|
+
"""Extract request body parameters from the OpenAPI details."""
|
|
56
|
+
try:
|
|
57
|
+
return details['requestBody']['content']['application/json']['schema']['properties']
|
|
58
|
+
except KeyError:
|
|
59
|
+
return {}
|
|
60
|
+
|
|
61
|
+
def _create_function(self, path: str, method: str, description: str, request_body_params: Dict[str, Any]):
|
|
62
|
+
"""Create a function for the given path and method with typed arguments."""
|
|
63
|
+
|
|
64
|
+
# Function template that uses **kwargs to accept any arguments and performs an actual API call.
|
|
65
|
+
def template_function(**kwargs) -> Dict[str, Any]:
|
|
66
|
+
missing_params = [param for param in request_body_params if param not in kwargs]
|
|
67
|
+
if missing_params:
|
|
68
|
+
return {"error": f"Missing required params: {missing_params}"}
|
|
69
|
+
params = {param: kwargs[param] for param in request_body_params}
|
|
70
|
+
logger.info(f"Executing {method.upper()} request to {path} with {params}")
|
|
71
|
+
|
|
72
|
+
# Actual API call
|
|
73
|
+
full_url = f"{self.api_schema['servers'][0]['url']}{path}"
|
|
74
|
+
headers = {'Content-Type': 'application/json'}
|
|
75
|
+
if method.lower() == 'post':
|
|
76
|
+
response = requests.post(full_url, json=params, headers=headers)
|
|
77
|
+
elif method.lower() == 'get':
|
|
78
|
+
response = requests.get(full_url, params=params, headers=headers)
|
|
79
|
+
else:
|
|
80
|
+
return {"error": f"Method {method.upper()} not supported"}
|
|
81
|
+
return response.json()
|
|
82
|
+
|
|
83
|
+
# Create parameters with types for the new function's signature.
|
|
84
|
+
parameters = [
|
|
85
|
+
Parameter(name=param, kind=Parameter.POSITIONAL_OR_KEYWORD, annotation=map_openapi_type_to_python(type_))
|
|
86
|
+
for param, type_ in request_body_params.items()
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
# Create a new signature from the parameters including the return annotation.
|
|
90
|
+
new_sig = Signature(parameters, return_annotation=Dict[str, Any])
|
|
91
|
+
|
|
92
|
+
# Use types.FunctionType to create a new function with the desired signature.
|
|
93
|
+
func = types.FunctionType(template_function.__code__, globals(), "function", closure=template_function.__closure__)
|
|
94
|
+
|
|
95
|
+
# Assign the new signature to the function.
|
|
96
|
+
func.__signature__ = new_sig
|
|
97
|
+
func.__doc__ = description
|
|
98
|
+
|
|
99
|
+
return func
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def save_user_id(user_id):
|
|
6
|
+
user_data = {'user_id': user_id}
|
|
7
|
+
with open('user_data.json', 'w') as outfile:
|
|
8
|
+
json.dump(user_data, outfile)
|
|
9
|
+
|
|
10
|
+
def get_user_id():
|
|
11
|
+
if os.path.exists('user_data.json'):
|
|
12
|
+
with open('user_data.json', 'r') as infile:
|
|
13
|
+
user_data = json.load(infile)
|
|
14
|
+
return user_data.get('user_id')
|
|
15
|
+
return None
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from .composio_tool_spec import ComposioToolSpec
|
|
3
|
+
from inspect import signature
|
|
4
|
+
from llama_index.agent.openai import OpenAIAgent
|
|
5
|
+
from llama_index.llms.openai import OpenAI
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestComposioToolSpec(unittest.TestCase):
|
|
9
|
+
def setUp(self):
|
|
10
|
+
with open("lib/data/composio_tool.json", "r") as file:
|
|
11
|
+
self.tool_spec_json = file.read()
|
|
12
|
+
self.composio_tool_spec = ComposioToolSpec(self.tool_spec_json, "composio_token", "user_id")
|
|
13
|
+
|
|
14
|
+
def test_spec_functions_generation(self):
|
|
15
|
+
"""Test if spec functions are generated correctly based on the tool schema."""
|
|
16
|
+
expected_functions = [
|
|
17
|
+
"Clickup_CreateFolderView",
|
|
18
|
+
"Slack_SendMessage",
|
|
19
|
+
"Slack_UpdateMessage",
|
|
20
|
+
"Slack_DeleteMessage",
|
|
21
|
+
"Github_CreateIssue"
|
|
22
|
+
]
|
|
23
|
+
self.assertEqual(sorted(self.composio_tool_spec.spec_functions), sorted(expected_functions))
|
|
24
|
+
|
|
25
|
+
def test_function_signature(self):
|
|
26
|
+
"""Test if a specific function has the correct signature based on the tool schema."""
|
|
27
|
+
send_message_func = getattr(self.composio_tool_spec, "Slack_SendMessage", None)
|
|
28
|
+
self.assertIsNotNone(send_message_func)
|
|
29
|
+
sign = send_message_func.__signature__
|
|
30
|
+
expected_params = ['channel', 'text']
|
|
31
|
+
actual_params = list(sign.parameters.keys())
|
|
32
|
+
self.assertEqual(expected_params, actual_params)
|
|
33
|
+
sig = signature(send_message_func)
|
|
34
|
+
expected_sig = "(channel: str, text: str) -> Dict[str, Any]"
|
|
35
|
+
self.assertEqual(str(sig), expected_sig)
|
|
36
|
+
description = send_message_func.__doc__
|
|
37
|
+
expected_desc = "Send a message to a channel or user"
|
|
38
|
+
self.assertEqual(description, expected_desc)
|
|
39
|
+
|
|
40
|
+
def test_missing_required_params(self):
|
|
41
|
+
"""Test if the function correctly handles missing required parameters."""
|
|
42
|
+
send_message_func = getattr(self.composio_tool_spec, "Slack_SendMessage", None)
|
|
43
|
+
self.assertIsNotNone(send_message_func)
|
|
44
|
+
response = send_message_func(text="Hello, World!")
|
|
45
|
+
self.assertIn("error", response)
|
|
46
|
+
self.assertIn("Missing required params", response["error"])
|
|
47
|
+
|
|
48
|
+
def test_nested_request_signature(self):
|
|
49
|
+
"""Test if the function correctly handles nested request body parameters."""
|
|
50
|
+
create_issue_func = getattr(self.composio_tool_spec, "Clickup_CreateFolderView", None)
|
|
51
|
+
self.assertIsNotNone(create_issue_func)
|
|
52
|
+
sig = signature(create_issue_func)
|
|
53
|
+
print("sig:", sig)
|
|
54
|
+
|
|
55
|
+
def test_agent(self):
|
|
56
|
+
"""Test the agent vy spec."""
|
|
57
|
+
llm = OpenAI(model="gpt-4-turbo-preview")
|
|
58
|
+
agent = OpenAIAgent.from_tools(self.composio_tool_spec.to_tool_list(), llm=llm, verbose=True)
|
|
59
|
+
agent.chat("Hello, World!")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == '__main__':
|
|
64
|
+
unittest.main()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
dynamic = ["classifiers", "version", "readme", "authors", "requires-python", "description"]
|
|
3
|
+
dependencies = [
|
|
4
|
+
"requests",
|
|
5
|
+
"jsonschema",
|
|
6
|
+
"argparse",
|
|
7
|
+
"llama_index",
|
|
8
|
+
"nest_asyncio",
|
|
9
|
+
"beaupy===3.7.2"
|
|
10
|
+
]
|
|
11
|
+
name = "composio-llamaindex"
|
|
12
|
+
|
|
13
|
+
[build-system]
|
|
14
|
+
requires = [ "setuptools>=42", "wheel"]
|
|
15
|
+
build-backend = "setuptools.build_meta"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from setuptools import setup
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
def get_current_dir():
|
|
5
|
+
return os.path.dirname(os.path.realpath(__file__))
|
|
6
|
+
|
|
7
|
+
def resolve_paths(*paths):
|
|
8
|
+
return os.path.join(*paths)
|
|
9
|
+
|
|
10
|
+
readme_path = resolve_paths(get_current_dir(), 'README.md')
|
|
11
|
+
|
|
12
|
+
setup(
|
|
13
|
+
name = 'composio-llamaindex',
|
|
14
|
+
version = '0.0.1',
|
|
15
|
+
author = 'Karan',
|
|
16
|
+
author_email = 'karan@composio.dev',
|
|
17
|
+
description = 'Provides integrations skill with 50+ services in LlamaIndex',
|
|
18
|
+
long_description = open(readme_path).read(),
|
|
19
|
+
long_description_content_type = 'text/markdown',
|
|
20
|
+
url = 'https://github.com/SamparkAI/llama-composio',
|
|
21
|
+
classifiers = [
|
|
22
|
+
'Programming Language :: Python :: 3',
|
|
23
|
+
'License :: OSI Approved :: Apache Software License',
|
|
24
|
+
'Operating System :: OS Independent'
|
|
25
|
+
],
|
|
26
|
+
python_requires = '>=3.7',
|
|
27
|
+
include_package_data = True,
|
|
28
|
+
scripts = ['composio-llamaindex'],
|
|
29
|
+
)
|