stores 0.0.0__py3-none-any.whl → 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.
@@ -0,0 +1,76 @@
1
+ import json
2
+ import logging
3
+ import venv
4
+ from pathlib import Path
5
+
6
+ import requests
7
+ from git import Repo
8
+
9
+ from stores.constants import VENV_NAME
10
+ from stores.indexes.base_index import BaseIndex
11
+ from stores.indexes.venv_utils import init_venv_tools, install_venv_deps
12
+
13
+ logging.basicConfig()
14
+ logger = logging.getLogger("stores.indexes.remote_index")
15
+ logger.setLevel(logging.INFO)
16
+
17
+ # TODO: CACHE_DIR might resolve differently
18
+ CACHE_DIR = Path(".tools")
19
+ INDEX_LOOKUP_URL = (
20
+ "https://mnryl5tkkol3yitc3w2rupqbae0ovnej.lambda-url.us-east-1.on.aws/"
21
+ )
22
+
23
+
24
+ def lookup_index(index_id: str, index_version: str | None = None):
25
+ response = requests.post(
26
+ INDEX_LOOKUP_URL,
27
+ headers={
28
+ "content-type": "application/json",
29
+ },
30
+ data=json.dumps(
31
+ {
32
+ "index_id": index_id,
33
+ "index_version": index_version,
34
+ }
35
+ ),
36
+ )
37
+ if response.ok:
38
+ return response.json()
39
+
40
+
41
+ class RemoteIndex(BaseIndex):
42
+ def __init__(self, index_id: str, env_var: dict | None = None):
43
+ self.index_id = index_id
44
+ self.index_folder = CACHE_DIR / self.index_id
45
+ self.env_var = env_var or {}
46
+ if not self.index_folder.exists():
47
+ commit_like = None
48
+ if ":" in index_id:
49
+ index_id, commit_like = index_id.split(":")
50
+ # Lookup Stores DB
51
+ repo_url = None
52
+ try:
53
+ index_metadata = lookup_index(index_id, commit_like)
54
+ if index_metadata:
55
+ repo_url = index_metadata["clone_url"]
56
+ commit_like = index_metadata["commit"]
57
+ except Exception:
58
+ logger.warning(
59
+ f"Could not find {index_id} in stores, assuming index references a GitHub repo..."
60
+ )
61
+ pass
62
+ if not repo_url:
63
+ # Otherwise, assume index references a GitHub repo
64
+ repo_url = f"https://github.com/{index_id}.git"
65
+ repo = Repo.clone_from(repo_url, self.index_folder)
66
+ if commit_like:
67
+ repo.git.checkout(commit_like)
68
+
69
+ # Create venv and install deps
70
+ self.venv = self.index_folder / VENV_NAME
71
+ if not self.venv.exists():
72
+ venv.create(self.venv, symlinks=True, with_pip=True, upgrade_deps=True)
73
+ install_venv_deps(self.index_folder)
74
+ # Initialize tools
75
+ tools = init_venv_tools(self.index_folder, self.env_var)
76
+ super().__init__(tools)
@@ -0,0 +1,376 @@
1
+ import hashlib
2
+ import inspect
3
+ import json
4
+ import logging
5
+ import os
6
+ import pickle
7
+ import socket
8
+ import subprocess
9
+ import sys
10
+ import threading
11
+ from enum import Enum
12
+ from pathlib import Path
13
+ from typing import Dict, Literal, Tuple, TypedDict, Union
14
+
15
+ from makefun import create_function
16
+
17
+ from stores.constants import TOOLS_CONFIG_FILENAME, VENV_NAME
18
+
19
+ if sys.version_info >= (3, 11):
20
+ import tomllib
21
+ else:
22
+ import tomli as tomllib
23
+
24
+ logging.basicConfig()
25
+ logger = logging.getLogger("stores.indexes.venv_utils")
26
+ logger.setLevel(logging.INFO)
27
+
28
+ HASH_FILE = ".deps_hash"
29
+
30
+
31
+ SUPPORTED_DEP_CONFIGS = {
32
+ "pyproject.toml": f"{VENV_NAME}/bin/pip install .",
33
+ "setup.py": f"{VENV_NAME}/bin/pip install .",
34
+ "requirements.txt": f"{VENV_NAME}/bin/pip install -r requirements.txt",
35
+ }
36
+
37
+
38
+ def has_installed(config_path: os.PathLike):
39
+ """
40
+ Read hash file to check if dependencies have been installed
41
+ """
42
+ with open(config_path, "rb") as f:
43
+ config_hash = hashlib.sha256(f.read()).hexdigest()
44
+ hash_path = config_path.parent / HASH_FILE
45
+ if hash_path.exists():
46
+ with open(hash_path) as f:
47
+ return config_hash == f.read().strip()
48
+ else:
49
+ return False
50
+
51
+
52
+ def write_hash(config_path: os.PathLike):
53
+ """
54
+ Write hash file once dependencies have been installed
55
+ """
56
+ with open(config_path, "rb") as f:
57
+ config_hash = hashlib.sha256(f.read()).hexdigest()
58
+ hash_path = config_path.parent / HASH_FILE
59
+ with open(hash_path, "w") as f:
60
+ f.write(config_hash)
61
+
62
+
63
+ def install_venv_deps(index_folder: os.PathLike):
64
+ index_folder = Path(index_folder)
65
+
66
+ for config_file, install_cmd in SUPPORTED_DEP_CONFIGS.items():
67
+ config_path = index_folder / config_file
68
+ if config_path.exists():
69
+ # Check if already installed
70
+ if has_installed(config_path):
71
+ return "Already installed"
72
+ subprocess.check_call(
73
+ install_cmd.split(),
74
+ cwd=index_folder,
75
+ )
76
+ write_hash(config_path)
77
+ message = f"Installed with {index_folder}/{install_cmd}"
78
+ logger.info(message)
79
+ return message
80
+
81
+
82
+ def init_venv_tools(index_folder: os.PathLike, env_var: dict | None = None):
83
+ index_folder = Path(index_folder)
84
+ env_var = env_var or {}
85
+
86
+ index_manifest = index_folder / TOOLS_CONFIG_FILENAME
87
+ with open(index_manifest, "rb") as file:
88
+ manifest = tomllib.load(file)["index"]
89
+
90
+ tools = []
91
+ for tool_id in manifest.get("tools", []):
92
+ tool_sig = get_tool_signature(
93
+ tool_id=tool_id,
94
+ index_folder=index_folder,
95
+ venv=VENV_NAME,
96
+ )
97
+ tool = parse_tool_signature(
98
+ signature_dict=tool_sig,
99
+ index_folder=index_folder,
100
+ venv=VENV_NAME,
101
+ env_var=env_var,
102
+ )
103
+ tools.append(tool)
104
+ return tools
105
+
106
+
107
+ # TODO: Sanitize tool_id, args, and kwargs
108
+ def get_tool_signature(tool_id: str, index_folder: os.PathLike, venv: str = VENV_NAME):
109
+ module_name = ".".join(tool_id.split(".")[:-1])
110
+ tool_name = tool_id.split(".")[-1]
111
+
112
+ runner = f"""
113
+ import pickle, sys, traceback, inspect, enum
114
+ from typing import Any, Dict, List, Literal, Tuple, Union, get_args, get_origin, get_type_hints
115
+ import types as T
116
+
117
+
118
+ def extract_type_info(typ):
119
+ origin = get_origin(typ)
120
+ args = list(get_args(typ))
121
+ if origin is Literal:
122
+ return {{"type": "Literal", "values": args}}
123
+ elif inspect.isclass(typ) and issubclass(typ, enum.Enum):
124
+ return {{
125
+ "type": "Enum",
126
+ "type_name": typ.__name__,
127
+ "values": {{v.name: v.value for v in typ}},
128
+ }}
129
+ elif isinstance(typ, type) and typ.__class__.__name__ == "_TypedDictMeta":
130
+ hints = get_type_hints(typ)
131
+ return {{
132
+ "type": "TypedDict",
133
+ "type_name": typ.__name__,
134
+ "fields": {{k: extract_type_info(v) for k, v in hints.items()}}
135
+ }}
136
+ elif origin in (list, List) or typ is list:
137
+ return {{
138
+ "type": "List",
139
+ "item_type": extract_type_info(args[0]) if args else {{"type": Any}}
140
+ }}
141
+ elif origin in (dict, Dict) or typ is dict:
142
+ return {{
143
+ "type": "Dict",
144
+ "key_type": extract_type_info(args[0]) if args else {{"type": Any}},
145
+ "value_type": extract_type_info(args[1]) if len(args) > 1 else {{"type": Any}}
146
+ }}
147
+ elif origin in (tuple, Tuple) or typ is tuple:
148
+ return {{
149
+ "type": "Tuple",
150
+ "item_types": [extract_type_info(arg) for arg in args] if args else [{{"type": Any}}]
151
+ }}
152
+ elif origin is Union or origin is T.UnionType:
153
+ return {{
154
+ "type": "Union",
155
+ "options": [extract_type_info(arg) for arg in args]
156
+ }}
157
+ else:
158
+ return {{"type": typ}}
159
+
160
+ try:
161
+ from {module_name} import {tool_name}
162
+ sig = inspect.signature({tool_name})
163
+ hints = get_type_hints({tool_name})
164
+ params = {{}}
165
+ for name, param in sig.parameters.items():
166
+ hint = hints.get(name, param.annotation)
167
+ param_info = extract_type_info(hint)
168
+ param_info["kind"] = param.kind
169
+ param_info["default"] = param.default
170
+ params[name] = param_info
171
+ return_type = hints.get('return', sig.return_annotation)
172
+ return_info = extract_type_info(return_type)
173
+
174
+ pickle.dump(
175
+ {{
176
+ "ok": True,
177
+ "result": {{
178
+ "tool_id": "{tool_id}",
179
+ "params": params,
180
+ "return": return_info,
181
+ "is_async": inspect.iscoroutinefunction({tool_name}),
182
+ "doc": inspect.getdoc({tool_name}),
183
+ }},
184
+ }},
185
+ sys.stdout.buffer,
186
+ )
187
+ except Exception as e:
188
+ err = traceback.format_exc()
189
+ pickle.dump({{"ok": False, "error": err}}, sys.stdout.buffer)
190
+ """
191
+ result = subprocess.run(
192
+ [f"{venv}/bin/python", "-c", runner],
193
+ capture_output=True,
194
+ cwd=index_folder,
195
+ )
196
+ try:
197
+ response = pickle.loads(result.stdout)
198
+ except ModuleNotFoundError as e:
199
+ raise RuntimeError(
200
+ f"Error loading tool {tool_id}:\nThe tool most likely has a parameter of a custom type that cannot be exported"
201
+ ) from e
202
+ if response.get("ok"):
203
+ return response["result"]
204
+ else:
205
+ raise RuntimeError(f"Error loading tool {tool_id}:\n{response['error']}")
206
+
207
+
208
+ def parse_param_type(param_info: dict):
209
+ param_type = param_info["type"]
210
+ if not isinstance(param_type, str):
211
+ return param_type
212
+ if param_type == "Literal":
213
+ return Literal.__getitem__(tuple(param_info["values"]))
214
+ elif param_type == "Enum":
215
+ return Enum(param_info["type_name"], param_info["values"])
216
+ elif param_type == "TypedDict":
217
+ properties = {}
218
+ for k, v in param_info["fields"].items():
219
+ properties[k] = parse_param_type(v)
220
+ return TypedDict(param_info["type_name"], properties)
221
+ elif param_type == "List":
222
+ return list[parse_param_type(param_info["item_type"])]
223
+ elif param_type == "Dict":
224
+ return Dict[
225
+ parse_param_type(param_info["key_type"]),
226
+ parse_param_type(param_info["value_type"]),
227
+ ]
228
+ elif param_type == "Tuple":
229
+ return Tuple.__getitem__(
230
+ tuple([parse_param_type(i) for i in param_info["item_types"]])
231
+ )
232
+ elif param_type == "Union":
233
+ return Union.__getitem__(
234
+ tuple([parse_param_type(i) for i in param_info["options"]])
235
+ )
236
+ else:
237
+ raise TypeError(f"Invalid param type {param_type} in param info {param_info}")
238
+
239
+
240
+ def parse_tool_signature(
241
+ signature_dict: dict,
242
+ index_folder: os.PathLike,
243
+ venv: str = VENV_NAME,
244
+ env_var: dict | None = None,
245
+ ):
246
+ """
247
+ Create a wrapper function that replicates the remote tool
248
+ given its signature
249
+ """
250
+ env_var = env_var or {}
251
+
252
+ def func_handler(*args, **kwargs):
253
+ return run_remote_tool(
254
+ tool_id=signature_dict["tool_id"],
255
+ index_folder=index_folder,
256
+ args=args,
257
+ kwargs=kwargs,
258
+ venv=venv,
259
+ env_var=env_var,
260
+ )
261
+
262
+ async def async_func_handler(*args, **kwargs):
263
+ return run_remote_tool(
264
+ tool_id=signature_dict["tool_id"],
265
+ index_folder=index_folder,
266
+ args=args,
267
+ kwargs=kwargs,
268
+ venv=venv,
269
+ env_var=env_var,
270
+ )
271
+
272
+ # Reconstruct signature from list of args
273
+ params = []
274
+ for param_name, param_info in signature_dict["params"].items():
275
+ params.append(
276
+ inspect.Parameter(
277
+ name=param_name,
278
+ kind=param_info["kind"],
279
+ default=param_info["default"],
280
+ annotation=parse_param_type(param_info),
281
+ )
282
+ )
283
+ # Reconstruct return type
284
+ return_type = parse_param_type(signature_dict["return"])
285
+ signature = inspect.Signature(params, return_annotation=return_type)
286
+ func = create_function(
287
+ signature,
288
+ async_func_handler if signature_dict.get("is_async") else func_handler,
289
+ qualname=signature_dict["tool_id"],
290
+ doc=signature_dict.get("doc"),
291
+ )
292
+ func.__name__ = signature_dict["tool_id"]
293
+ return func
294
+
295
+
296
+ # TODO: Sanitize tool_id, args, and kwargs
297
+ def run_remote_tool(
298
+ tool_id: str,
299
+ index_folder: os.PathLike,
300
+ args: list | None = None,
301
+ kwargs: dict | None = None,
302
+ venv: str = VENV_NAME,
303
+ env_var: dict | None = None,
304
+ ):
305
+ args = args or []
306
+ kwargs = kwargs or {}
307
+ env_var = env_var or {}
308
+
309
+ module_name = ".".join(tool_id.split(".")[:-1])
310
+ tool_name = tool_id.split(".")[-1]
311
+ payload = json.dumps(
312
+ {
313
+ "args": args,
314
+ "kwargs": kwargs,
315
+ }
316
+ ).encode("utf-8")
317
+
318
+ # We use sockets to pass function output
319
+ listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
320
+ listener.bind(("localhost", 0))
321
+ listener.listen(1)
322
+ _, port = listener.getsockname()
323
+
324
+ def handle_connection():
325
+ conn, _ = listener.accept()
326
+ with conn:
327
+ data = b""
328
+ while True:
329
+ chunk = conn.recv(4096)
330
+ if not chunk:
331
+ break
332
+ data += chunk
333
+ listener.close()
334
+ return data
335
+
336
+ result_data = {}
337
+ t = threading.Thread(
338
+ target=lambda: result_data.setdefault("data", handle_connection())
339
+ )
340
+ t.start()
341
+
342
+ runner = f"""
343
+ import asyncio, inspect, json, socket, sys, traceback
344
+ sys.path.insert(0, "{index_folder}")
345
+ try:
346
+ from {module_name} import {tool_name}
347
+ params = json.load(sys.stdin)
348
+ args = params.get("args", [])
349
+ kwargs = params.get("kwargs", {{}})
350
+ if inspect.iscoroutinefunction({tool_name}):
351
+ loop = asyncio.new_event_loop()
352
+ asyncio.set_event_loop(loop)
353
+ result = loop.run_until_complete({tool_name}(*args, **kwargs))
354
+ else:
355
+ result = {tool_name}(*args, **kwargs)
356
+ response = json.dumps({{"ok": True, "result": result}})
357
+ except Exception as e:
358
+ err = traceback.format_exc()
359
+ response = json.dumps({{"ok": False, "error": err}})
360
+ sock = socket.create_connection(("localhost", {port}))
361
+ sock.sendall(response.encode("utf-8"))
362
+ sock.close()
363
+ """
364
+ subprocess.run(
365
+ [f"{index_folder}/{venv}/bin/python", "-c", runner],
366
+ input=payload,
367
+ capture_output=True,
368
+ env=env_var,
369
+ )
370
+
371
+ t.join()
372
+ response = json.loads(result_data["data"].decode("utf-8"))
373
+ if response.get("ok"):
374
+ return response["result"]
375
+ else:
376
+ raise RuntimeError(f"Subprocess failed with error:\n{response['error']}")
stores/parse.py ADDED
@@ -0,0 +1,144 @@
1
+ import json
2
+ import logging
3
+ import re
4
+ from itertools import combinations
5
+ from typing import Any
6
+
7
+ import dirtyjson
8
+ from dirtyjson.attributed_containers import AttributedDict, AttributedList
9
+ from fuzzywuzzy import process
10
+
11
+ logging.basicConfig()
12
+ logger = logging.getLogger("stores.parse")
13
+ logger.setLevel(logging.INFO)
14
+
15
+
16
+ def find_json(rgx: str, text: str):
17
+ match = re.search(rgx, text)
18
+ if match is None:
19
+ return text
20
+ else:
21
+ return match.groupdict().get("json")
22
+
23
+
24
+ def convert_attributed_container(
25
+ container: Any | AttributedDict | AttributedList | float | int,
26
+ ):
27
+ if isinstance(container, AttributedList):
28
+ return [convert_attributed_container(i) for i in container]
29
+ elif isinstance(container, AttributedDict):
30
+ dict_container = {**container}
31
+ for k, v in dict_container.items():
32
+ dict_container[k] = convert_attributed_container(v)
33
+ return dict_container
34
+ else:
35
+ return container
36
+
37
+
38
+ def llm_parse_json(text: str, keys: list[str] = None, autoescape=True):
39
+ """Read LLM output and extract JSON data from it."""
40
+
41
+ keys = keys or []
42
+
43
+ # First check for ```json
44
+ code_snippet_pattern = r"```json(?P<json>(.|\s|\n)*?)```"
45
+ code_snippet_result = find_json(code_snippet_pattern, text)
46
+ # Then try to find the longer match between [.*?] and {.*?}
47
+ array_pattern = re.compile("(?P<json>\\[.*\\])", re.DOTALL)
48
+ array_result = find_json(array_pattern, text)
49
+ dict_pattern = re.compile("(?P<json>{.*})", re.DOTALL)
50
+ dict_result = find_json(dict_pattern, text)
51
+
52
+ if array_result and dict_result and len(dict_result) > len(array_result):
53
+ results = [
54
+ code_snippet_result,
55
+ dict_result,
56
+ array_result,
57
+ ]
58
+ else:
59
+ results = [
60
+ code_snippet_result,
61
+ array_result,
62
+ dict_result,
63
+ ]
64
+
65
+ # Try each result in order
66
+ result_json = None
67
+ for result in results:
68
+ if result is not None:
69
+ try:
70
+ result_json = dirtyjson.loads(result)
71
+ break
72
+ except dirtyjson.error.Error as e:
73
+ if autoescape and e.msg.startswith("Expecting ',' delimiter"):
74
+ # Possibly due to non-escaped quotes
75
+ corrected_json_str = escape_quotes(result, keys)
76
+ if corrected_json_str:
77
+ result_json = dirtyjson.loads(corrected_json_str)
78
+ break
79
+
80
+ try:
81
+ result = (
82
+ result.replace("None", "null")
83
+ .replace("True", "true")
84
+ .replace("False", "false")
85
+ )
86
+ result_json = dirtyjson.loads(result)
87
+ break
88
+ except dirtyjson.error.Error:
89
+ continue
90
+
91
+ if result_json:
92
+ result_json = fuzzy_match_keys(result_json, keys)
93
+ return convert_attributed_container(result_json)
94
+
95
+ error_message = f"Failed to parse JSON from text {text}"
96
+ raise ValueError(error_message)
97
+
98
+
99
+ # Brute force escape chars
100
+ def escape_quotes(json_str: str, keys: list[str] = None):
101
+ keys = keys or []
102
+ quote_pos = [i for i, c in enumerate(json_str) if c in "\"'"]
103
+ # At minimum there should be 2*len(keys) quotes, any quotes
104
+ # more than this is a candidate for escape
105
+ # In addition, as long as there is an escaped quote, we need
106
+ # at least two none-escaped quotes
107
+ # TODO: Stricter conditions
108
+ max_escapes = len(quote_pos) - 2 * len(keys) - 2
109
+ candidate_json_str = None
110
+ for n in range(1, max_escapes + 1):
111
+ candidates = list(combinations(quote_pos, n))
112
+ for candidate in candidates:
113
+ new_json_str = ""
114
+ for start, end in zip(
115
+ [0, *candidate], [*candidate, len(json_str)], strict=True
116
+ ):
117
+ new_json_str += json_str[start:end] + "\\"
118
+ new_json_str = new_json_str[:-1]
119
+ try:
120
+ parsed = llm_parse_json(new_json_str, keys, autoescape=False)
121
+ if all(key in parsed for key in keys):
122
+ new_candidate = json.dumps(parsed)
123
+ if candidate_json_str is None:
124
+ candidate_json_str = new_candidate
125
+ # Get the largest valid JSON
126
+ elif len(new_candidate) > len(candidate_json_str):
127
+ candidate_json_str = new_candidate
128
+ except Exception:
129
+ pass
130
+ return candidate_json_str
131
+
132
+
133
+ def fuzzy_match_keys(json_dict: dict, gold_keys: list[str] = None, min_score=80):
134
+ if not gold_keys:
135
+ return json_dict
136
+ keys = list(json_dict.keys())
137
+ for key in keys:
138
+ closest_key, score = process.extractOne(key, gold_keys)
139
+ if score == 100:
140
+ continue
141
+ elif score >= min_score:
142
+ json_dict[closest_key] = json_dict[key]
143
+ del json_dict[key]
144
+ return json_dict
stores/utils.py ADDED
@@ -0,0 +1,8 @@
1
+ from collections import Counter
2
+
3
+
4
+ def check_duplicates(input_list: list):
5
+ counts = Counter(input_list)
6
+ duplicates = [i for i in counts if counts[i] > 1]
7
+ if duplicates:
8
+ raise ValueError(f"Found duplicate(s): {duplicates}")
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: stores
3
+ Version: 0.1.0
4
+ Summary: Repository of Python functions and tools for LLMs
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: dirtyjson>=1.0.8
8
+ Requires-Dist: dotenv>=0.9.9
9
+ Requires-Dist: fuzzywuzzy>=0.18.0
10
+ Requires-Dist: gitpython>=3.1.44
11
+ Requires-Dist: makefun>=1.15.6
12
+ Requires-Dist: python-levenshtein>=0.27.1
13
+ Requires-Dist: requests>=2.32.3
14
+ Requires-Dist: tomli>=1.1.0; python_version < '3.11'
15
+ Provides-Extra: anthropic
16
+ Requires-Dist: anthropic>=0.49.0; extra == 'anthropic'
17
+ Provides-Extra: google
18
+ Requires-Dist: google-genai>=1.7.0; extra == 'google'
19
+ Provides-Extra: langchain
20
+ Requires-Dist: langchain-google-genai>=2.1.0; extra == 'langchain'
21
+ Provides-Extra: langgraph
22
+ Requires-Dist: langchain-core>=0.3.45; extra == 'langgraph'
23
+ Requires-Dist: langchain-google-genai>=2.1.0; extra == 'langgraph'
24
+ Requires-Dist: langgraph>=0.3.16; extra == 'langgraph'
25
+ Provides-Extra: litellm
26
+ Requires-Dist: litellm>=1.63.11; extra == 'litellm'
27
+ Provides-Extra: llamaindex
28
+ Requires-Dist: llama-index-llms-google-genai>=0.1.4; extra == 'llamaindex'
29
+ Requires-Dist: llama-index>=0.12.25; extra == 'llamaindex'
30
+ Provides-Extra: openai
31
+ Requires-Dist: openai>=1.66.5; extra == 'openai'
32
+ Provides-Extra: openai-agent
33
+ Requires-Dist: openai-agents>=0.0.7; extra == 'openai-agent'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # stores
37
+
38
+ Repository of Python functions and tools for LLMs
39
+
40
+ ## Why we built Stores
41
+
42
+ Just as tool use is often cited as a key development in human civilization, we believe that tool use represents a major transition in AI development.
43
+
44
+ **The aim of Stores is to make it super simple to build LLM Agents that use tools.**
45
+
46
+ There are two main elements:
47
+ 1. A public repository of [tools](https://stores-tools.vercel.app) that anyone can contribute to
48
+ 2. This Python library that handles tool installation and formatting
49
+
50
+ For more details, check out the [documentation](https://stores-tools.vercel.app/docs).
51
+
52
+ ## Design principles
53
+
54
+ - **Open-source**: Each set of tools in the Stores collection is a public git repository. In the event the Stores database is no longer operational, the library and tools will still work as long as the git repositories exist.
55
+ - **Isolation**: Tools are isolated in their own virtual environments. This makes it trivial to manage tools with conflicting dependencies and reduces unnecessary access to sensitive environment variables.
56
+ - **Framework compatibility**: In order to pass information about tools, LLM providers often require different formats that can make it cumbersome to switch between providers. Stores makes it easy to output the required formats across providers.
57
+
58
+ ## Usage
59
+
60
+ ```sh
61
+ pip install stores
62
+ ```
63
+
64
+ Or if you are using `uv`:
65
+
66
+ ```sh
67
+ uv add stores
68
+ ```
69
+
70
+ Then load one of the available indexes and use it with your favorite LLM package.
71
+
72
+ ```python {6, 11}
73
+ import anthropic
74
+ import stores
75
+
76
+ client = anthropic.Anthropic()
77
+
78
+ index = stores.Index(["silanthro/hackernews"])
79
+
80
+ response = client.messages.create(
81
+ model=model,
82
+ messages=messages,
83
+ tools=index.format_tools("anthropic"),
84
+ )
85
+ ```