stores 0.0.0__py3-none-any.whl → 0.1.1__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.
- stores/__init__.py +9 -0
- stores/constants.py +2 -0
- stores/format.py +214 -0
- stores/indexes/__init__.py +11 -0
- stores/indexes/base_index.py +283 -0
- stores/indexes/index.py +56 -0
- stores/indexes/local_index.py +84 -0
- stores/indexes/remote_index.py +76 -0
- stores/indexes/venv_utils.py +376 -0
- stores/parse.py +144 -0
- stores/utils.py +8 -0
- stores-0.1.1.dist-info/METADATA +85 -0
- stores-0.1.1.dist-info/RECORD +15 -0
- {stores-0.0.0.dist-info → stores-0.1.1.dist-info}/WHEEL +1 -2
- stores-0.1.1.dist-info/licenses/LICENSE +21 -0
- stores-0.0.0.dist-info/METADATA +0 -19
- stores-0.0.0.dist-info/RECORD +0 -4
- stores-0.0.0.dist-info/top_level.txt +0 -1
@@ -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,85 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: stores
|
3
|
+
Version: 0.1.1
|
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
|
+
```
|