tooluniverse 0.1.0__py3-none-any.whl

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 tooluniverse might be problematic. Click here for more details.

@@ -0,0 +1,48 @@
1
+ [
2
+ {
3
+ "name": "Finish",
4
+ "description": "Indicate the end of multi-step reasoning.",
5
+ "parameter": {
6
+ "type": "object",
7
+ "properties": null
8
+ }
9
+ },
10
+ {
11
+ "name": "Tool_RAG",
12
+ "description": "Retrieve related tools from the toolbox based on the provided description",
13
+ "parameter": {
14
+ "type": "object",
15
+ "properties": {
16
+ "description": {
17
+ "type": "string",
18
+ "description": "The description of the tool capability required.",
19
+ "required": true
20
+ },
21
+ "limit": {
22
+ "type": "integer",
23
+ "description": "The number of tools to retrieve",
24
+ "required": true
25
+ }
26
+ }
27
+ },
28
+ "required": [
29
+ "description",
30
+ "limit"
31
+ ]
32
+ },
33
+ {
34
+ "name": "CallAgent",
35
+ "description": "Give a solution plan to the agent and let it solve the problem. Solution plan should reflect a distinct method, approach, or viewpoint to solve the given question. Call these function multiple times, and each solution plan should start with different aspects of the question, for example, genes, phenotypes, diseases, or drugs, etc. The CallAgent will achieve the task based on the plan, so only give the plan instead of unverified information.",
36
+ "parameter": {
37
+ "type": "object",
38
+ "properties":
39
+ {
40
+ "solution": {
41
+ "type": "string",
42
+ "description": "A feasible and concise solution plan that address the question.",
43
+ "required": true
44
+ }
45
+ }
46
+ }
47
+ }
48
+ ]
@@ -0,0 +1,216 @@
1
+ from .utils import read_json_list, evaluate_function_call, extract_function_call_json
2
+ import copy
3
+ import json
4
+ import random
5
+ import string
6
+ from .graphql_tool import OpentargetTool, OpentargetGeneticsTool, OpentargetToolDrugNameMatch
7
+ from .openfda_tool import FDADrugLabelTool, FDADrugLabelSearchTool, FDADrugLabelSearchIDTool, FDADrugLabelGetDrugGenericNameTool
8
+ from .restful_tool import MonarchTool, MonarchDiseasesForMultiplePhenoTool
9
+
10
+ import os
11
+
12
+ # Determine the directory where the current file is located
13
+ current_dir = os.path.dirname(os.path.abspath(__file__))
14
+
15
+ default_tool_files = {
16
+ 'opentarget': os.path.join(current_dir, 'data', 'opentarget_tools.json'),
17
+ 'fda_drug_label': os.path.join(current_dir, 'data', 'fda_drug_labeling_tools.json'),
18
+ 'special_tools': os.path.join(current_dir, 'data', 'special_tools.json'),
19
+ 'monarch': os.path.join(current_dir, 'data', 'monarch_tools.json')
20
+ }
21
+
22
+ tool_type_mappings = {
23
+ 'OpenTarget': OpentargetTool,
24
+ 'OpenTargetGenetics': OpentargetGeneticsTool,
25
+ 'FDADrugLabel': FDADrugLabelTool,
26
+ 'FDADrugLabelSearchTool': FDADrugLabelSearchTool,
27
+ 'Monarch': MonarchTool,
28
+ 'MonarchDiseasesForMultiplePheno': MonarchDiseasesForMultiplePhenoTool,
29
+ 'FDADrugLabelSearchIDTool': FDADrugLabelSearchIDTool,
30
+ 'FDADrugLabelGetDrugGenericNameTool': FDADrugLabelGetDrugGenericNameTool,
31
+ 'OpentargetToolDrugNameMatch': OpentargetToolDrugNameMatch,
32
+ }
33
+
34
+
35
+ class ToolUniverse:
36
+ def __init__(self, tool_files=default_tool_files):
37
+ # Initialize any necessary attributes here
38
+ self.all_tools = []
39
+ self.all_tool_dict = {}
40
+ self.tool_category_dicts = {}
41
+ if tool_files is None:
42
+ tool_files = default_tool_files
43
+ self.tool_files = tool_files
44
+ self.callable_functions = {}
45
+
46
+ def load_tools(self, tool_type=None):
47
+ print(f"Number of tools before load tools: {len(self.all_tools)}")
48
+ if tool_type is None:
49
+ for each in self.tool_files:
50
+ loaded_tool_list = read_json_list(self.tool_files[each])
51
+ self.all_tools += loaded_tool_list
52
+ self.tool_category_dicts[each] = loaded_tool_list
53
+ else:
54
+ for each in tool_type:
55
+ loaded_tool_list = read_json_list(self.tool_files[each])
56
+ self.all_tools += loaded_tool_list
57
+ self.tool_category_dicts[each] = loaded_tool_list
58
+ # Deduplication of tools
59
+ tool_name_list = []
60
+ dedup_all_tools = []
61
+ for each in self.all_tools:
62
+ if each['name'] not in tool_name_list:
63
+ tool_name_list.append(each['name'])
64
+ dedup_all_tools.append(each)
65
+ self.all_tools = dedup_all_tools
66
+ self.refresh_tool_name_desc()
67
+
68
+ print(f"Number of tools after load tools: {len(self.all_tools)}")
69
+
70
+ def return_all_loaded_tools(self):
71
+ return copy.deepcopy(self.all_tools)
72
+
73
+ def refresh_tool_name_desc(self, enable_full_desc=False):
74
+ tool_name_list = []
75
+ tool_desc_list = []
76
+ for tool in self.all_tools:
77
+ tool_name_list.append(tool['name'])
78
+ if enable_full_desc:
79
+ tool_desc_list.append(json.dumps(tool))
80
+ else:
81
+ tool_desc_list.append(tool['name']+': '+tool['description'])
82
+ self.all_tool_dict[tool['name']] = tool
83
+ return tool_name_list, tool_desc_list
84
+
85
+ def prepare_one_tool_prompt(self, tool):
86
+ valid_keys = ['name', 'description', 'parameter', 'required']
87
+ tool = copy.deepcopy(tool)
88
+ for key in list(tool.keys()):
89
+ if key not in valid_keys:
90
+ del tool[key]
91
+ return tool
92
+
93
+ def prepare_tool_prompts(self, tool_list):
94
+ copied_list = []
95
+ for tool in tool_list:
96
+ copied_list.append(self.prepare_one_tool_prompt(tool))
97
+ return copied_list
98
+
99
+ def remove_keys(self, tool_list, invalid_keys):
100
+ copied_list = copy.deepcopy(tool_list)
101
+ for tool in copied_list:
102
+ # Create a list of keys to avoid modifying the dictionary during iteration
103
+ for key in list(tool.keys()):
104
+ if key in invalid_keys:
105
+ del tool[key]
106
+ return copied_list
107
+
108
+ def prepare_tool_examples(self, tool_list):
109
+ valid_keys = ['name', 'description',
110
+ 'parameter', 'required', 'query_schema', 'fields', 'label', 'type']
111
+ copied_list = copy.deepcopy(tool_list)
112
+ for tool in copied_list:
113
+ # Create a list of keys to avoid modifying the dictionary during iteration
114
+ for key in list(tool.keys()):
115
+ if key not in valid_keys:
116
+ del tool[key]
117
+ return copied_list
118
+
119
+ def get_tool_by_name(self, tool_names):
120
+ picked_tool_list = []
121
+ for each_name in tool_names:
122
+ if each_name in self.all_tool_dict:
123
+ picked_tool_list.append(self.all_tool_dict[each_name])
124
+ else:
125
+ print(f"Tool name {each_name} not found in the loaded tools.")
126
+ return picked_tool_list
127
+
128
+ def get_one_tool_by_one_name(self, tool_name):
129
+ if tool_name in self.all_tool_dict:
130
+ return self.all_tool_dict[tool_name]
131
+ else:
132
+ print(f"Tool name {tool_name} not found in the loaded tools.")
133
+ return None
134
+
135
+ def get_tool_type_by_name(self, tool_name):
136
+ return self.all_tool_dict[tool_name]['type']
137
+
138
+ def tool_to_str(self, tool_list):
139
+ return '\n\n'.join(json.dumps(obj, indent=4) for obj in tool_list)
140
+
141
+ def extract_function_call_json(self, lst, return_message=False, verbose=True):
142
+ return extract_function_call_json(lst, return_message=return_message, verbose=verbose)
143
+
144
+ def call_id_gen(self):
145
+ return "".join(random.choices(string.ascii_letters + string.digits, k=9))
146
+
147
+ def run(self, fcall_str, return_message=False, verbose=True):
148
+ if return_message:
149
+ function_call_json, message = self.extract_function_call_json(
150
+ fcall_str, return_message=return_message, verbose=verbose)
151
+ else:
152
+ function_call_json = self.extract_function_call_json(
153
+ fcall_str, return_message=return_message, verbose=verbose)
154
+ if function_call_json is not None:
155
+ if isinstance(function_call_json, list):
156
+ # return the function call+result message with call id.
157
+ call_results = []
158
+ for i in range(len(function_call_json)):
159
+ call_result = self.run_one_function(function_call_json[i])
160
+ call_id = self.call_id_gen()
161
+ function_call_json[i]["call_id"] = call_id
162
+ call_results.append({"role": "tool", "content": json.dumps(
163
+ {"content": call_result, "call_id": call_id})})
164
+ revised_messages = [{"role": "assistant", "content": message,
165
+ "tool_calls": json.dumps(function_call_json)}]+call_results
166
+ return revised_messages
167
+ else:
168
+ return self.run_one_function(function_call_json)
169
+ else:
170
+ print("\033[91mNot a function call\033[0m")
171
+ return None
172
+
173
+ def run_one_function(self, function_call_json):
174
+ check_status, check_message = self.check_function_call(function_call_json)
175
+ if check_status is False:
176
+ return "Invalid function call: "+check_message# + " You must correct your invalid function call!"
177
+ function_name = function_call_json["name"]
178
+ arguments = function_call_json["arguments"]
179
+ if function_name in self.callable_functions:
180
+ return self.callable_functions[function_name].run(arguments)
181
+ else:
182
+ if function_name in self.all_tool_dict:
183
+ print(
184
+ "\033[92mInitiating callable_function from loaded tool dicts.\033[0m")
185
+ tool = self.init_tool(
186
+ self.all_tool_dict[function_name], add_to_cache=True)
187
+ return tool.run(arguments)
188
+
189
+ def init_tool(self, tool=None, tool_name=None, add_to_cache=True):
190
+ if tool_name is not None:
191
+ new_tool = tool_type_mappings[tool_name]()
192
+ else:
193
+ tool_type = tool['type']
194
+ tool_name = tool['name']
195
+ if 'OpentargetToolDrugNameMatch' == tool_type:
196
+ if 'FDADrugLabelGetDrugGenericNameTool' not in self.callable_functions:
197
+ self.callable_functions['FDADrugLabelGetDrugGenericNameTool'] = tool_type_mappings['FDADrugLabelGetDrugGenericNameTool']()
198
+ new_tool = tool_type_mappings[tool_type](tool_config=tool, drug_generic_tool=self.callable_functions['FDADrugLabelGetDrugGenericNameTool'])
199
+ else:
200
+ new_tool = tool_type_mappings[tool_type](tool_config=tool)
201
+ if add_to_cache:
202
+ self.callable_functions[tool_name] = new_tool
203
+ return new_tool
204
+
205
+ def check_function_call(self, fcall_str, function_config=None):
206
+ function_call_json = self.extract_function_call_json(fcall_str)
207
+ print("loaded function call json", function_call_json)
208
+ if function_call_json is not None:
209
+ if function_config is not None:
210
+ return evaluate_function_call(function_config, function_call_json)
211
+ function_name = function_call_json['name']
212
+ if not function_name in self.all_tool_dict:
213
+ return False, f"Function name {function_name} not found in loaded tools."
214
+ return evaluate_function_call(self.all_tool_dict[function_name], function_call_json)
215
+ else:
216
+ return False, "\033[91mInvalid JSON string of function call\033[0m"
@@ -0,0 +1,122 @@
1
+ from graphql import build_schema
2
+ from graphql.language import parse
3
+ from graphql.validation import validate
4
+ from .base_tool import BaseTool
5
+ import requests
6
+ import copy
7
+ import json
8
+
9
+
10
+ def validate_query(query_str, schema_str):
11
+ try:
12
+ # Build the GraphQL schema object from the provided schema string
13
+ schema = build_schema(schema_str)
14
+
15
+ # Parse the query string into an AST (Abstract Syntax Tree)
16
+ query_ast = parse(query_str)
17
+
18
+ # Validate the query AST against the schema
19
+ validation_errors = validate(schema, query_ast)
20
+
21
+ if not validation_errors:
22
+ return True
23
+ else:
24
+ # Collect and return the validation errors
25
+ error_messages = '\n'.join(str(error)
26
+ for error in validation_errors)
27
+ return f"Query validation errors:\n{error_messages}"
28
+ except Exception as e:
29
+ return f"An error occurred during validation: {str(e)}"
30
+
31
+ def remove_none_and_empty_values(json_obj):
32
+ """Remove all key-value pairs where the value is None or an empty list"""
33
+ if isinstance(json_obj, dict):
34
+ return {k: remove_none_and_empty_values(v) for k, v in json_obj.items() if v is not None and v != []}
35
+ elif isinstance(json_obj, list):
36
+ return [remove_none_and_empty_values(item) for item in json_obj if item is not None and item != []]
37
+ else:
38
+ return json_obj
39
+
40
+ def execute_query(endpoint_url, query, variables=None):
41
+ response = requests.post(
42
+ endpoint_url, json={'query': query, 'variables': variables})
43
+ try:
44
+ result = response.json()
45
+ # result = json.dumps(result, ensure_ascii=False)
46
+ result = remove_none_and_empty_values(result)
47
+ # Check if the response contains errors
48
+ if 'errors' in result:
49
+ print("Invalid Query: ", result['errors'])
50
+ return None
51
+ # Check if the data field is empty
52
+ elif not result.get('data') or all(not v for v in result['data'].values()):
53
+ print("No data returned")
54
+ return None
55
+ else:
56
+ return result
57
+ except requests.exceptions.JSONDecodeError as e:
58
+ print("JSONDecodeError: Could not decode the response as JSON")
59
+ return None
60
+
61
+
62
+ class GraphQLTool(BaseTool):
63
+ def __init__(self, tool_config, endpoint_url):
64
+ super().__init__(tool_config)
65
+ self.endpoint_url = endpoint_url
66
+ self.query_schema = tool_config['query_schema']
67
+ self.parameters = tool_config['parameter']['properties']
68
+ self.default_size = 5
69
+
70
+ def run(self, arguments):
71
+ arguments = copy.deepcopy(arguments)
72
+ if 'size' in self.parameters and 'size' not in arguments:
73
+ arguments['size'] = 5
74
+ return execute_query(endpoint_url=self.endpoint_url, query=self.query_schema, variables=arguments)
75
+
76
+
77
+ class OpentargetTool(GraphQLTool):
78
+ def __init__(self, tool_config):
79
+ endpoint_url = 'https://api.platform.opentargets.org/api/v4/graphql'
80
+ super().__init__(tool_config, endpoint_url)
81
+
82
+ def run(self, arguments):
83
+ for each_arg, arg_value in arguments.items(): # opentarget api cannot handle '-' in the arguments
84
+ if isinstance(arg_value, str):
85
+ if '-' in arg_value:
86
+ arguments[each_arg] = arg_value.replace('-', ' ')
87
+ return super().run(arguments)
88
+
89
+
90
+ class OpentargetToolDrugNameMatch(GraphQLTool):
91
+ def __init__(self, tool_config, drug_generic_tool=None):
92
+ endpoint_url = 'https://api.platform.opentargets.org/api/v4/graphql'
93
+ self.drug_generic_tool = drug_generic_tool
94
+ self.possible_drug_name_args = ['drugName']
95
+ super().__init__(tool_config, endpoint_url)
96
+
97
+ def run(self, arguments):
98
+ arguments = copy.deepcopy(arguments)
99
+ results = execute_query(endpoint_url=self.endpoint_url, query=self.query_schema, variables=arguments)
100
+ if results is None:
101
+ print("No results found for the drug brand name. Trying with the generic name.")
102
+ name_arguments = {}
103
+ for each_args in self.possible_drug_name_args:
104
+ if each_args in arguments:
105
+ name_arguments['drug_name'] = arguments[each_args]
106
+ break
107
+ if len(name_arguments)==0:
108
+ print("No drug name found in the arguments.")
109
+ return None
110
+ drug_name_results = self.drug_generic_tool.run(name_arguments)
111
+ if 'openfda.generic_name' in drug_name_results:
112
+ arguments[each_args] = drug_name_results['openfda.generic_name']
113
+ print("Found generic name. Trying with the generic name: ", arguments[each_args])
114
+ results = execute_query(endpoint_url=self.endpoint_url, query=self.query_schema, variables=arguments)
115
+ return results
116
+
117
+
118
+
119
+ class OpentargetGeneticsTool(GraphQLTool):
120
+ def __init__(self, tool_config):
121
+ endpoint_url = 'https://api.genetics.opentargets.org/graphql'
122
+ super().__init__(tool_config, endpoint_url)