stores 0.1.7.dev6__py3-none-any.whl → 0.1.8.dev2__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/format.py CHANGED
@@ -61,7 +61,7 @@ def get_type_repr(typ: Type | GenericAlias) -> list[str]:
61
61
  return [type_mappings[typ.__name__]]
62
62
 
63
63
 
64
- def get_type_schema(typ: Type | GenericAlias):
64
+ def get_type_schema(typ: Type | GenericAlias, provider: ProviderFormat):
65
65
  origin = get_origin(typ)
66
66
  args = get_args(typ)
67
67
 
@@ -77,24 +77,27 @@ def get_type_schema(typ: Type | GenericAlias):
77
77
  schema["enum"] = [v.value for v in typ]
78
78
  elif isinstance(typ, type) and typ.__class__.__name__ == "_TypedDictMeta":
79
79
  hints = get_type_hints(typ)
80
- schema["properties"] = {k: get_type_schema(v) for k, v in hints.items()}
81
- schema["additionalProperties"] = False
80
+ schema["properties"] = {
81
+ k: get_type_schema(v, provider) for k, v in hints.items()
82
+ }
83
+ if provider != ProviderFormat.GOOGLE_GEMINI:
84
+ schema["additionalProperties"] = False
82
85
  schema["required"] = list(hints.keys())
83
86
  elif origin in (list, List) or typ is dict:
84
87
  if args:
85
- schema["items"] = get_type_schema(args[0])
88
+ schema["items"] = get_type_schema(args[0], provider)
86
89
  else:
87
90
  raise TypeError("Insufficient argument type information")
88
91
  elif origin in (dict, Dict) or typ is dict:
89
92
  raise TypeError("Insufficient argument type information")
90
93
  elif origin in (tuple, Tuple) or typ is tuple:
91
94
  if args:
92
- schema["items"] = get_type_schema(args[0])
95
+ schema["items"] = get_type_schema(args[0], provider)
93
96
  else:
94
97
  raise TypeError("Insufficient argument type information")
95
98
  elif origin is Union or origin is T.UnionType:
96
99
  for arg in args:
97
- subschema = get_type_schema(arg)
100
+ subschema = get_type_schema(arg, provider)
98
101
  del subschema["type"]
99
102
  schema = {
100
103
  **schema,
@@ -103,14 +106,17 @@ def get_type_schema(typ: Type | GenericAlias):
103
106
 
104
107
  # Un-nest single member type lists since Gemini does not accept list of types
105
108
  # Optional for OpenAI or Anthropic
106
- if schema["type"] and len(schema["type"]) == 1:
107
- schema["type"] = schema["type"][0]
109
+ if schema["type"]:
110
+ if len(schema["type"]) == 1:
111
+ schema["type"] = schema["type"][0]
112
+ elif len(schema["type"]) > 1 and provider == ProviderFormat.GOOGLE_GEMINI:
113
+ schema["type"] = schema["type"][0]
108
114
 
109
115
  return schema
110
116
 
111
117
 
112
118
  def get_param_schema(param: inspect.Parameter, provider: ProviderFormat):
113
- param_schema = get_type_schema(param.annotation)
119
+ param_schema = get_type_schema(param.annotation, provider)
114
120
 
115
121
  if param_schema["type"] is None:
116
122
  raise TypeError(f"Unsupported type: {param.annotation.__name__}")
@@ -9,7 +9,6 @@ from typing import (
9
9
  Callable,
10
10
  List,
11
11
  Literal,
12
- Optional,
13
12
  Tuple,
14
13
  Union,
15
14
  get_args,
@@ -98,7 +97,7 @@ def _handle_non_string_literal(annotation: type):
98
97
  return list[new_annotation], {"item": literal_map}
99
98
  if origin is Union or origin is UnionType:
100
99
  union_literal_maps = {}
101
- argtype_args = [a for a in get_args(annotation) if a != NoneType]
100
+ argtype_args = [a for a in get_args(annotation)]
102
101
  new_union, literal_map = _handle_non_string_literal(argtype_args[0])
103
102
  union_literal_maps[new_union.__name__] = literal_map
104
103
  for child_argtype in argtype_args[1:]:
@@ -198,12 +197,12 @@ def wrap_tool(tool: Callable):
198
197
  # Process args with default values: make sure type includes None
199
198
  new_annotation = argtype
200
199
  if new_annotation is Parameter.empty:
201
- new_annotation = Optional[type(new_arg.default)]
200
+ new_annotation = type(new_arg.default) | None
202
201
  origin = get_origin(new_annotation)
203
202
  if origin not in [Union, UnionType] or NoneType not in get_args(
204
203
  new_annotation
205
204
  ):
206
- new_annotation = Optional[new_annotation]
205
+ new_annotation = new_annotation | None
207
206
  new_arg = new_arg.replace(
208
207
  default=None,
209
208
  kind=Parameter.POSITIONAL_OR_KEYWORD,
@@ -402,13 +401,17 @@ class BaseIndex:
402
401
  # Handle sync
403
402
  yield tool_fn(**kwargs)
404
403
 
405
- def parse_and_execute(self, msg: str):
404
+ def parse_and_execute(self, msg: str, collect_results=False):
406
405
  toolcall = llm_parse_json(msg, keys=["toolname", "kwargs"])
407
- return self.execute(toolcall.get("toolname"), toolcall.get("kwargs"))
406
+ return self.execute(
407
+ toolcall.get("toolname"), toolcall.get("kwargs"), collect_results
408
+ )
408
409
 
409
- async def async_parse_and_execute(self, msg: str):
410
+ async def aparse_and_execute(self, msg: str, collect_results=False):
410
411
  toolcall = llm_parse_json(msg, keys=["toolname", "kwargs"])
411
- return await self.aexecute(toolcall.get("toolname"), toolcall.get("kwargs"))
412
+ return await self.aexecute(
413
+ toolcall.get("toolname"), toolcall.get("kwargs"), collect_results
414
+ )
412
415
 
413
416
  def stream_parse_and_execute(self, msg: str):
414
417
  toolcall = llm_parse_json(msg, keys=["toolname", "kwargs"])
@@ -416,7 +419,7 @@ class BaseIndex:
416
419
 
417
420
  async def astream_parse_and_execute(self, msg: str):
418
421
  toolcall = llm_parse_json(msg, keys=["toolname", "kwargs"])
419
- async for value in self.astream_parse_and_execute(
422
+ async for value in self.astream_execute(
420
423
  toolcall.get("toolname"), toolcall.get("kwargs")
421
424
  ):
422
425
  yield value
@@ -8,7 +8,7 @@ from pathlib import Path
8
8
  from typing import Optional
9
9
 
10
10
  import requests
11
- from git import Repo
11
+ from git import GitCommandError, Repo
12
12
 
13
13
  from stores.constants import VENV_NAME
14
14
  from stores.indexes.base_index import BaseIndex
@@ -44,6 +44,8 @@ def lookup_index(index_id: str, index_version: str | None = None):
44
44
  )
45
45
  if response.ok:
46
46
  return response.json()
47
+ else:
48
+ raise ValueError(f"Index {index_id} not found in database")
47
49
 
48
50
 
49
51
  class RemoteIndex(BaseIndex):
@@ -88,7 +90,10 @@ class RemoteIndex(BaseIndex):
88
90
  if not repo_url:
89
91
  # Otherwise, assume index references a GitHub repo
90
92
  repo_url = f"https://github.com/{index_id}.git"
91
- repo = Repo.clone_from(repo_url, self.index_folder)
93
+ try:
94
+ repo = Repo.clone_from(repo_url, self.index_folder)
95
+ except GitCommandError as e:
96
+ raise ValueError(f"Index {index_id} not found") from e
92
97
  if commit_like:
93
98
  repo.git.checkout(commit_like)
94
99
 
@@ -327,8 +327,7 @@ def parse_tool_signature(
327
327
  if signature_dict.get("isasyncgenfunction"):
328
328
 
329
329
  async def func_handler(*args, **kwargs):
330
- # TODO: Make this truly async
331
- async for value in run_remote_tool(
330
+ async for value in run_remote_tool_async(
332
331
  tool_id=signature_dict["tool_id"],
333
332
  index_folder=index_folder,
334
333
  args=args,
@@ -338,6 +337,7 @@ def parse_tool_signature(
338
337
  stream=True,
339
338
  ):
340
339
  yield value
340
+
341
341
  elif signature_dict.get("isgeneratorfunction"):
342
342
 
343
343
  def func_handler(*args, **kwargs):
@@ -347,7 +347,7 @@ def parse_tool_signature(
347
347
  def run():
348
348
  async def runner():
349
349
  try:
350
- async for item in run_remote_tool(
350
+ async for item in run_remote_tool_async(
351
351
  tool_id=signature_dict["tool_id"],
352
352
  index_folder=index_folder,
353
353
  args=args,
@@ -381,26 +381,63 @@ def parse_tool_signature(
381
381
  elif signature_dict.get("iscoroutinefunction"):
382
382
 
383
383
  async def func_handler(*args, **kwargs):
384
- # TODO: Make this truly async
385
- return run_remote_tool(
384
+ result = []
385
+ async for item in run_remote_tool_async(
386
386
  tool_id=signature_dict["tool_id"],
387
387
  index_folder=index_folder,
388
388
  args=args,
389
389
  kwargs=kwargs,
390
390
  venv=venv,
391
391
  env_var=env_var,
392
- )
392
+ stream=True,
393
+ ):
394
+ result.append(item)
395
+ return result[-1] if result else None
393
396
  else:
394
397
 
395
- def func_handler(*args, **kwargs):
396
- return run_remote_tool(
398
+ async def func_handler_async_fallback(*args, **kwargs):
399
+ result = []
400
+ async for item in run_remote_tool_async(
397
401
  tool_id=signature_dict["tool_id"],
398
402
  index_folder=index_folder,
399
403
  args=args,
400
404
  kwargs=kwargs,
401
405
  venv=venv,
402
406
  env_var=env_var,
403
- )
407
+ stream=True,
408
+ ):
409
+ result.append(item)
410
+ return result[-1] if result else None
411
+
412
+ def func_handler(*args, **kwargs):
413
+ coro = func_handler_async_fallback(*args, **kwargs)
414
+ try:
415
+ # Check if we're in an async context
416
+ asyncio.get_running_loop()
417
+ in_async = True
418
+ except RuntimeError:
419
+ in_async = False
420
+
421
+ if not in_async:
422
+ # Safe to run directly
423
+ return asyncio.run(coro)
424
+
425
+ q = queue.Queue()
426
+
427
+ def runner():
428
+ try:
429
+ result = asyncio.run(coro)
430
+ q.put(result)
431
+ except Exception as e:
432
+ q.put(e)
433
+
434
+ t = threading.Thread(target=runner)
435
+ t.start()
436
+ result = q.get()
437
+ t.join()
438
+ if isinstance(result, Exception):
439
+ raise result
440
+ return result
404
441
 
405
442
  # Reconstruct signature from list of args
406
443
  params = []
@@ -426,92 +463,7 @@ def parse_tool_signature(
426
463
  return func
427
464
 
428
465
 
429
- # TODO: Sanitize tool_id, args, and kwargs
430
- def run_remote_tool(
431
- tool_id: str,
432
- index_folder: os.PathLike,
433
- args: list | None = None,
434
- kwargs: dict | None = None,
435
- venv: str = VENV_NAME,
436
- env_var: dict | None = None,
437
- stream: bool = False,
438
- ):
439
- args = args or []
440
- kwargs = kwargs or {}
441
- env_var = env_var or {}
442
-
443
- module_name = ".".join(tool_id.split(".")[:-1])
444
- tool_name = tool_id.split(".")[-1]
445
- payload = json.dumps(
446
- {
447
- "args": args,
448
- "kwargs": kwargs,
449
- }
450
- ).encode("utf-8")
451
-
452
- # We use sockets to pass function output
453
- listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
454
- listener.bind(("localhost", 0))
455
- listener.listen(1)
456
- _, port = listener.getsockname()
457
-
458
- result_data = {}
459
-
460
- def handle_connection_sync():
461
- conn, _ = listener.accept()
462
- with conn:
463
- buffer = ""
464
- while True:
465
- chunk = conn.recv(4096).decode("utf-8")
466
- if not chunk:
467
- break
468
- buffer += chunk
469
- while "\n" in buffer:
470
- line, buffer = buffer.split("\n", 1)
471
- if not line.strip():
472
- continue
473
- msg = json.loads(line)
474
- if msg.get("ok") and "stream" in msg:
475
- result_data.setdefault("stream", []).append(msg["stream"])
476
- elif msg.get("ok") and "result" in msg:
477
- result_data["result"] = msg["result"]
478
- elif "error" in msg:
479
- result_data["error"] = msg["error"]
480
- elif msg.get("done"):
481
- return
482
-
483
- async def handle_connection_async():
484
- loop = asyncio.get_running_loop()
485
- conn, _ = await loop.sock_accept(listener)
486
- conn.setblocking(False)
487
- buffer = ""
488
- try:
489
- while True:
490
- chunk = await loop.sock_recv(conn, 4096)
491
- if not chunk:
492
- break
493
- buffer += chunk.decode("utf-8")
494
- while "\n" in buffer:
495
- line, buffer = buffer.split("\n", 1)
496
- if not line.strip():
497
- continue
498
- msg = json.loads(line)
499
- if msg.get("ok") and "stream" in msg:
500
- yield msg["stream"]
501
- elif msg.get("ok") and "result" in msg:
502
- yield msg["result"]
503
- elif "error" in msg:
504
- raise RuntimeError(f"Subprocess error:\n{msg['error']}")
505
- elif msg.get("done"):
506
- return
507
- finally:
508
- conn.close()
509
-
510
- if not stream:
511
- thread = threading.Thread(target=lambda: handle_connection_sync())
512
- thread.start()
513
-
514
- runner = f"""
466
+ tool_runner = """
515
467
  import asyncio, inspect, json, socket, sys, traceback
516
468
  sys.path.insert(0, "{index_folder}")
517
469
 
@@ -560,26 +512,98 @@ finally:
560
512
  pass
561
513
  """
562
514
 
563
- proc = subprocess.Popen(
564
- [get_python_command(Path(index_folder) / venv), "-c", runner],
565
- stdin=subprocess.PIPE,
566
- stdout=subprocess.DEVNULL,
567
- stderr=subprocess.PIPE,
515
+
516
+ # TODO: Sanitize tool_id, args, and kwargs
517
+ async def run_remote_tool_async(
518
+ tool_id: str,
519
+ index_folder: os.PathLike,
520
+ args: list | None = None,
521
+ kwargs: dict | None = None,
522
+ venv: str = VENV_NAME,
523
+ env_var: dict | None = None,
524
+ stream: bool = True,
525
+ ):
526
+ args = args or []
527
+ kwargs = kwargs or {}
528
+ env_var = env_var or {}
529
+
530
+ module_name = ".".join(tool_id.split(".")[:-1])
531
+ tool_name = tool_id.split(".")[-1]
532
+ payload = json.dumps({"args": args, "kwargs": kwargs}).encode("utf-8")
533
+
534
+ listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
535
+ listener.bind(("localhost", 0))
536
+ listener.listen(1)
537
+ listener.setblocking(False)
538
+ _, port = listener.getsockname()
539
+
540
+ loop = asyncio.get_running_loop()
541
+ conn_task = loop.create_task(loop.sock_accept(listener))
542
+
543
+ runner = tool_runner.format(
544
+ index_folder=index_folder,
545
+ port=port,
546
+ module_name=module_name,
547
+ tool_name=tool_name,
548
+ )
549
+
550
+ proc = await asyncio.create_subprocess_exec(
551
+ get_python_command(Path(index_folder) / venv),
552
+ "-c",
553
+ runner,
554
+ stdin=asyncio.subprocess.PIPE,
555
+ stdout=asyncio.subprocess.DEVNULL,
556
+ stderr=asyncio.subprocess.PIPE,
568
557
  env=env_var or None,
569
558
  )
570
- proc.stdin.write(payload)
571
- proc.stdin.close()
572
-
573
- if not stream:
574
- thread.join()
575
-
576
- if "error" in result_data:
577
- raise RuntimeError(f"Subprocess failed with error:\n{result_data['error']}")
578
- elif "result" in result_data:
579
- return result_data["result"]
580
- elif "stream" in result_data:
581
- return result_data["stream"]
582
- else:
583
- raise RuntimeError("Subprocess completed without returning data.")
584
- else:
585
- return handle_connection_async()
559
+
560
+ try:
561
+ proc.stdin.write(payload)
562
+ await proc.stdin.drain()
563
+ proc.stdin.close()
564
+
565
+ conn, _ = await conn_task
566
+ conn.setblocking(False)
567
+
568
+ buffer = ""
569
+ result = None
570
+ while True:
571
+ chunk = await loop.sock_recv(conn, 4096)
572
+ if not chunk:
573
+ break
574
+ buffer += chunk.decode("utf-8")
575
+ while "\n" in buffer:
576
+ line, buffer = buffer.split("\n", 1)
577
+ if not line.strip():
578
+ continue
579
+ msg = json.loads(line)
580
+
581
+ if msg.get("ok") and "stream" in msg:
582
+ if stream:
583
+ yield msg["stream"]
584
+ else:
585
+ result = msg["stream"]
586
+ elif msg.get("ok") and "result" in msg:
587
+ result = msg["result"]
588
+ elif "error" in msg:
589
+ raise RuntimeError(f"Subprocess error:\n{msg['error']}")
590
+ elif "done" in msg and result is not None:
591
+ yield result
592
+ return
593
+
594
+ except asyncio.CancelledError:
595
+ proc.kill()
596
+ await proc.wait()
597
+ raise
598
+ finally:
599
+ try:
600
+ conn.close()
601
+ except Exception:
602
+ pass
603
+ try:
604
+ listener.close()
605
+ except Exception:
606
+ pass
607
+ if proc.returncode is None:
608
+ proc.kill()
609
+ await proc.wait()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stores
3
- Version: 0.1.7.dev6
3
+ Version: 0.1.8.dev2
4
4
  Summary: Repository of Python functions and tools for LLMs
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.10
@@ -1,15 +1,15 @@
1
1
  stores/__init__.py,sha256=KYpKkNrMLx6ssVUbxHnn9wFBq5F5KnaFchcimIfDf9g,186
2
2
  stores/constants.py,sha256=7WqFmoGCtmUKHA5WHxOJvvK7g-yYu_KGoqnuVFADNao,57
3
- stores/format.py,sha256=LduYBVDiUDB1J1HDyu9jHrRG1V97pw6C5g76OirpJJY,7792
3
+ stores/format.py,sha256=L6DaRklE0CpUzMoVTckliaP-M5UrF6gW8nEG_m4y21M,8089
4
4
  stores/parse.py,sha256=HYPNPzQod2vpu1Cln7yQ8aVkZT1Mw2IN0sZ2A1DIaqE,4967
5
5
  stores/utils.py,sha256=GPWT6lCoGobwP3PlEOHyJfKyd0dobamjyErcR7lgm7M,242
6
6
  stores/indexes/__init__.py,sha256=s-RNqml8uGREQhxwSdDoxcbcxeD8soB9BcL5dBKsQfI,215
7
- stores/indexes/base_index.py,sha256=YrEwETZ5eXj3rXK5qxOllRXqFifQoteYdzPAasbvEyg,15536
7
+ stores/indexes/base_index.py,sha256=oaLN_Tk6PdoTSlRdGsd2wTvBVQ4S3ylV8rbIVSSNkEQ,15608
8
8
  stores/indexes/index.py,sha256=Cub5mtnYGipHfPR8BexJYRSKfuJmcGPp0B3ou2bGNqs,2901
9
9
  stores/indexes/local_index.py,sha256=Gg9LkEo1L0_NZZYPItsF_-1y6nFP369C3QlPvPyvPx8,3708
10
- stores/indexes/remote_index.py,sha256=-GV4l2c7GL6_bcVOQnSK5rzYru237bwC5L6eiO-QFzM,3438
11
- stores/indexes/venv_utils.py,sha256=k664ptVhPfs4AHCpB6CiE-pWHGcqi982YjsZDDt64Ys,18577
12
- stores-0.1.7.dev6.dist-info/METADATA,sha256=Gg4CaLZDS0isd_R0Vhpn7IGCIQQ51Og5rBuBANnV7tk,3081
13
- stores-0.1.7.dev6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
14
- stores-0.1.7.dev6.dist-info/licenses/LICENSE,sha256=VTidYE7_Dam0Dwyq095EhhDIqi47g03oVpLAHQgKws0,1066
15
- stores-0.1.7.dev6.dist-info/RECORD,,
10
+ stores/indexes/remote_index.py,sha256=kbyfjqeOgH5IgC0gYfoq79loFM1AeOCCYcsKimudZAg,3666
11
+ stores/indexes/venv_utils.py,sha256=oN0LMQxeZQvVlkqsu3vrshaYaSmSDE-VOFSmMKc8QeI,18822
12
+ stores-0.1.8.dev2.dist-info/METADATA,sha256=xsNHI0bP0mF7StNBmRniznrzUFFsiPMptKqSmO8pWko,3081
13
+ stores-0.1.8.dev2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
14
+ stores-0.1.8.dev2.dist-info/licenses/LICENSE,sha256=VTidYE7_Dam0Dwyq095EhhDIqi47g03oVpLAHQgKws0,1066
15
+ stores-0.1.8.dev2.dist-info/RECORD,,