toolslm 0.2.0__tar.gz → 0.2.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: toolslm
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Tools to make language models a bit easier to use
5
5
  Home-page: https://github.com/AnswerDotAI/toolslm
6
6
  Author: Jeremy Howard
@@ -1,7 +1,7 @@
1
1
  [DEFAULT]
2
2
  repo = toolslm
3
3
  lib_name = toolslm
4
- version = 0.2.0
4
+ version = 0.2.2
5
5
  min_python = 3.9
6
6
  license = apache2
7
7
  black_formatting = False
@@ -35,7 +35,7 @@ clean_ids = True
35
35
  clear_all = False
36
36
  conda_user = fastai
37
37
  console_scripts = folder2ctx=toolslm.xml:folder2ctx_cli
38
- cell_number = True
38
+ cell_number = False
39
39
  skip_procs =
40
40
  update_pyproject = True
41
41
 
@@ -0,0 +1 @@
1
+ __version__ = "0.2.2"
@@ -3,26 +3,26 @@
3
3
  # %% auto 0
4
4
  __all__ = ['clean_md', 'read_md', 'html2md', 'read_html', 'get_llmstxt', 'split_url', 'find_docs', 'read_docs']
5
5
 
6
- # %% ../03_download.ipynb 2
6
+ # %% ../03_download.ipynb
7
7
  from fastcore.utils import *
8
8
  from httpx import get
9
9
  from fastcore.meta import delegates
10
10
  from urllib.parse import urlparse, urljoin
11
11
 
12
- # %% ../03_download.ipynb 4
12
+ # %% ../03_download.ipynb
13
13
  def clean_md(text, rm_comments=True, rm_details=True):
14
14
  "Remove comments and `<details>` sections from `text`"
15
15
  if rm_comments: text = re.sub(r'\n?<!--.*?-->\n?', '', text, flags=re.DOTALL)
16
16
  if rm_details: text = re.sub(r'\n?<details>.*?</details>\n?', '', text, flags=re.DOTALL)
17
17
  return text
18
18
 
19
- # %% ../03_download.ipynb 5
19
+ # %% ../03_download.ipynb
20
20
  @delegates(get)
21
21
  def read_md(url, rm_comments=True, rm_details=True, **kwargs):
22
22
  "Read text from `url` and clean with `clean_docs`"
23
23
  return clean_md(get(url, **kwargs).text, rm_comments=rm_comments, rm_details=rm_details)
24
24
 
25
- # %% ../03_download.ipynb 7
25
+ # %% ../03_download.ipynb
26
26
  def html2md(s:str, ignore_links=True):
27
27
  "Convert `s` from HTML to markdown"
28
28
  import html2text
@@ -32,7 +32,7 @@ def html2md(s:str, ignore_links=True):
32
32
  o.ignore_images = True
33
33
  return o.handle(s)
34
34
 
35
- # %% ../03_download.ipynb 8
35
+ # %% ../03_download.ipynb
36
36
  def read_html(url, # URL to read
37
37
  sel=None, # Read only outerHTML of CSS selector `sel`
38
38
  rm_comments=True, # Removes HTML comments
@@ -54,7 +54,7 @@ def read_html(url, # URL to read
54
54
  if wrap_tag: return '\n'.join([f"\n<{wrap_tag}>\n{o}</{wrap_tag}>\n" for o in mds])
55
55
  else: return'\n'.join(mds)
56
56
 
57
- # %% ../03_download.ipynb 13
57
+ # %% ../03_download.ipynb
58
58
  def get_llmstxt(url, optional=False, n_workers=None):
59
59
  "Get llms.txt file from and expand it with `llms_txt.create_ctx()`"
60
60
  if not url.endswith('llms.txt'): return None
@@ -63,7 +63,7 @@ def get_llmstxt(url, optional=False, n_workers=None):
63
63
  if resp.status_code!=200: return None
64
64
  return llms_txt.create_ctx(resp.text, optional=optional, n_workers=n_workers)
65
65
 
66
- # %% ../03_download.ipynb 15
66
+ # %% ../03_download.ipynb
67
67
  def split_url(url):
68
68
  "Split `url` into base, path, and file name, normalising name to '/' if empty"
69
69
  parsed = urlparse(url.strip('/'))
@@ -73,13 +73,13 @@ def split_url(url):
73
73
  if not path and not fname: path='/'
74
74
  return base,path,fname
75
75
 
76
- # %% ../03_download.ipynb 17
76
+ # %% ../03_download.ipynb
77
77
  def _tryget(url):
78
78
  "Return response from `url` if `status_code!=404`, otherwise `None`"
79
79
  res = get(url)
80
80
  return None if res.status_code==404 else url
81
81
 
82
- # %% ../03_download.ipynb 18
82
+ # %% ../03_download.ipynb
83
83
  def find_docs(url):
84
84
  "If available, return LLM-friendly llms.txt context or markdown file location from `url`"
85
85
  base,path,fname = split_url(url)
@@ -99,7 +99,7 @@ def find_docs(url):
99
99
  if parsed_url.path == '/' or not parsed_url.path: return None
100
100
  return find_docs(urljoin(url, '..'))
101
101
 
102
- # %% ../03_download.ipynb 23
102
+ # %% ../03_download.ipynb
103
103
  def read_docs(url, optional=False, n_workers=None, rm_comments=True, rm_details=True):
104
104
  "If available, return LLM-friendly llms.txt context or markdown file response for `url`"
105
105
  url = find_docs(url)
@@ -3,7 +3,7 @@
3
3
  # %% auto 0
4
4
  __all__ = ['empty', 'custom_types', 'get_schema', 'PathArg', 'python', 'mk_ns', 'call_func', 'call_func_async']
5
5
 
6
- # %% ../01_funccall.ipynb 2
6
+ # %% ../01_funccall.ipynb
7
7
  import inspect
8
8
  from collections import abc
9
9
  from fastcore.utils import *
@@ -11,10 +11,10 @@ from fastcore.docments import docments
11
11
  from typing import get_origin, get_args, Dict, List, Optional, Tuple, Union
12
12
  from types import UnionType
13
13
 
14
- # %% ../01_funccall.ipynb 3
14
+ # %% ../01_funccall.ipynb
15
15
  empty = inspect.Parameter.empty
16
16
 
17
- # %% ../01_funccall.ipynb 11
17
+ # %% ../01_funccall.ipynb
18
18
  def _types(t:type)->tuple[str,Optional[str]]:
19
19
  "Tuple of json schema type name and (if appropriate) array item name."
20
20
  if t is empty: raise TypeError('Missing type')
@@ -31,7 +31,7 @@ def _types(t:type)->tuple[str,Optional[str]]:
31
31
  # if t is the type itself like int, use the __name__ representation as the key
32
32
  else: return tmap.get(t.__name__, "object"), None
33
33
 
34
- # %% ../01_funccall.ipynb 18
34
+ # %% ../01_funccall.ipynb
35
35
  def _param(name, info):
36
36
  "json schema parameter given `name` and `info` from docments full dict."
37
37
  paramt,itemt = _types(info.anno)
@@ -40,19 +40,20 @@ def _param(name, info):
40
40
  if info.default is not empty: pschema["default"] = info.default
41
41
  return pschema
42
42
 
43
- # %% ../01_funccall.ipynb 21
43
+ # %% ../01_funccall.ipynb
44
44
  custom_types = {Path}
45
45
 
46
46
  def _handle_type(t, defs):
47
47
  "Handle a single type, creating nested schemas if necessary"
48
48
  if t is NoneType: return {'type': 'null'}
49
49
  if t in custom_types: return {'type':'string', 'format':t.__name__}
50
+ if t in (dict, list, tuple, set): return {'type': _types(t)[0]}
50
51
  if isinstance(t, type) and not issubclass(t, (int, float, str, bool)) or inspect.isfunction(t):
51
52
  defs[t.__name__] = _get_nested_schema(t)
52
53
  return {'$ref': f'#/$defs/{t.__name__}'}
53
54
  return {'type': _types(t)[0]}
54
55
 
55
- # %% ../01_funccall.ipynb 23
56
+ # %% ../01_funccall.ipynb
56
57
  def _is_container(t):
57
58
  "Check if type is a container (list, dict, tuple, set, Union)"
58
59
  origin = get_origin(t)
@@ -62,7 +63,7 @@ def _is_parameterized(t):
62
63
  "Check if type has arguments (e.g. list[int] vs list, dict[str, int] vs dict)"
63
64
  return _is_container(t) and (get_args(t) != ())
64
65
 
65
- # %% ../01_funccall.ipynb 29
66
+ # %% ../01_funccall.ipynb
66
67
  def _handle_container(origin, args, defs):
67
68
  "Handle container types like dict, list, tuple, set, and Union"
68
69
  if origin is Union or origin is UnionType:
@@ -83,7 +84,7 @@ def _handle_container(origin, args, defs):
83
84
  return schema
84
85
  return None
85
86
 
86
- # %% ../01_funccall.ipynb 30
87
+ # %% ../01_funccall.ipynb
87
88
  def _process_property(name, obj, props, req, defs):
88
89
  "Process a single property of the schema"
89
90
  p = _param(name, obj)
@@ -91,12 +92,12 @@ def _process_property(name, obj, props, req, defs):
91
92
  if obj.default is empty: req[name] = True
92
93
 
93
94
  if _is_container(obj.anno) and _is_parameterized(obj.anno):
94
- p.update(_handle_container(get_origin(obj.anno), get_args(obj.anno), defs))
95
+ p.update(_handle_container(get_origin(obj.anno), get_args(obj.anno), defs))
95
96
  else:
96
97
  # Non-container type or container without arguments
97
98
  p.update(_handle_type(obj.anno, defs))
98
99
 
99
- # %% ../01_funccall.ipynb 31
100
+ # %% ../01_funccall.ipynb
100
101
  def _get_nested_schema(obj):
101
102
  "Generate nested JSON schema for a class or function"
102
103
  d = docments(obj, full=True)
@@ -111,9 +112,10 @@ def _get_nested_schema(obj):
111
112
  if defs: schema['$defs'] = defs
112
113
  return schema
113
114
 
114
- # %% ../01_funccall.ipynb 35
115
- def get_schema(f:callable, pname='input_schema')->dict:
115
+ # %% ../01_funccall.ipynb
116
+ def get_schema(f:Union[callable,dict], pname='input_schema')->dict:
116
117
  "Generate JSON schema for a class, function, or method"
118
+ if isinstance(f, dict): return f
117
119
  schema = _get_nested_schema(f)
118
120
  desc = f.__doc__
119
121
  assert desc, "Docstring missing!"
@@ -122,16 +124,16 @@ def get_schema(f:callable, pname='input_schema')->dict:
122
124
  if ret.anno is not empty: desc += f'\n\nReturns:\n- type: {_types(ret.anno)[0]}'
123
125
  return {"name": f.__name__, "description": desc, pname: schema}
124
126
 
125
- # %% ../01_funccall.ipynb 46
127
+ # %% ../01_funccall.ipynb
126
128
  def PathArg(
127
129
  path: str # A filesystem path
128
130
  ): return Path(path)
129
131
 
130
- # %% ../01_funccall.ipynb 66
132
+ # %% ../01_funccall.ipynb
131
133
  import ast, time, signal, traceback
132
134
  from fastcore.utils import *
133
135
 
134
- # %% ../01_funccall.ipynb 67
136
+ # %% ../01_funccall.ipynb
135
137
  def _copy_loc(new, orig):
136
138
  "Copy location information from original node to new node and all children."
137
139
  new = ast.copy_location(new, orig)
@@ -140,7 +142,7 @@ def _copy_loc(new, orig):
140
142
  elif isinstance(o, list): setattr(new, field, [_copy_loc(value, orig) for value in o])
141
143
  return new
142
144
 
143
- # %% ../01_funccall.ipynb 69
145
+ # %% ../01_funccall.ipynb
144
146
  def _run(code:str, glb:dict=None, loc:dict=None):
145
147
  "Run `code`, returning final expression (similar to IPython)"
146
148
  tree = ast.parse(code)
@@ -163,14 +165,14 @@ def _run(code:str, glb:dict=None, loc:dict=None):
163
165
  if _result is not None: return _result
164
166
  return stdout_buffer.getvalue().strip()
165
167
 
166
- # %% ../01_funccall.ipynb 74
167
- def python(code:str, # Code to execute
168
- glb:Optional[dict]=None, # Globals namespace
169
- loc:Optional[dict]=None, # Locals namespace
170
- timeout:int=3600 # Maximum run time in seconds before a `TimeoutError` is raised
171
- ): # Result of last node, if it's an expression, or `None` otherwise
172
- """Executes python `code` with `timeout` and returning final expression (similar to IPython).
173
- Raised exceptions are returned as a string, with a stack trace."""
168
+ # %% ../01_funccall.ipynb
169
+ def python(
170
+ code:str, # Code to execute
171
+ glb:Optional[dict]=None, # Globals namespace
172
+ loc:Optional[dict]=None, # Locals namespace
173
+ timeout:int=3600 # Maximum run time in seconds
174
+ ):
175
+ "Executes python `code` with `timeout` and returning final expression (similar to IPython)."
174
176
  def handler(*args): raise TimeoutError()
175
177
  if glb is None: glb = inspect.currentframe().f_back.f_globals
176
178
  if loc is None: loc=glb
@@ -180,7 +182,7 @@ def python(code:str, # Code to execute
180
182
  except Exception as e: return traceback.format_exc()
181
183
  finally: signal.alarm(0)
182
184
 
183
- # %% ../01_funccall.ipynb 85
185
+ # %% ../01_funccall.ipynb
184
186
  def mk_ns(*funcs_or_objs):
185
187
  merged = {}
186
188
  for o in funcs_or_objs:
@@ -189,16 +191,25 @@ def mk_ns(*funcs_or_objs):
189
191
  if callable(o) and hasattr(o, '__name__'): merged |= {o.__name__: o}
190
192
  return merged
191
193
 
192
- # %% ../01_funccall.ipynb 94
193
- def call_func(fc_name, fc_inputs, ns):
194
+ # %% ../01_funccall.ipynb
195
+ def call_func(fc_name, fc_inputs, ns, raise_on_err=True):
194
196
  "Call the function `fc_name` with the given `fc_inputs` using namespace `ns`."
195
197
  if not isinstance(ns, abc.Mapping): ns = mk_ns(*ns)
196
198
  func = ns[fc_name]
197
- return func(**fc_inputs)
198
-
199
- # %% ../01_funccall.ipynb 101
200
- async def call_func_async(fc_name, fc_inputs, ns):
199
+ # Clean up bad param names
200
+ inps = {re.sub(r'\W', '', k):v for k,v in fc_inputs.items()}
201
+ try: return func(**fc_inputs)
202
+ except Exception as e:
203
+ if raise_on_err: raise e from None
204
+ else: return traceback.format_exc()
205
+
206
+ # %% ../01_funccall.ipynb
207
+ async def call_func_async(fc_name, fc_inputs, ns, raise_on_err=True):
201
208
  "Awaits the function `fc_name` with the given `fc_inputs` using namespace `ns`."
202
- if not isinstance(ns, abc.Mapping): ns = mk_ns(*ns)
203
- func = ns[fc_name]
204
- return await func(**fc_inputs)
209
+ res = call_func(fc_name, fc_inputs, ns, raise_on_err=raise_on_err)
210
+ if inspect.iscoroutine(res):
211
+ try: res = await res
212
+ except Exception as e:
213
+ if raise_on_err: raise e from None
214
+ else: return traceback.format_exc()
215
+ return res
@@ -3,18 +3,18 @@
3
3
  # %% auto 0
4
4
  __all__ = ['get_shell']
5
5
 
6
- # %% ../02_shell.ipynb 2
6
+ # %% ../02_shell.ipynb
7
7
  import ast, time, signal, traceback
8
8
  from fastcore.utils import *
9
9
 
10
- # %% ../02_shell.ipynb 4
10
+ # %% ../02_shell.ipynb
11
11
  from IPython.terminal.interactiveshell import TerminalInteractiveShell
12
12
  from IPython.utils.capture import capture_output
13
13
 
14
- # %% ../02_shell.ipynb 7
14
+ # %% ../02_shell.ipynb
15
15
  TerminalInteractiveShell.orig_run = TerminalInteractiveShell.run_cell
16
16
 
17
- # %% ../02_shell.ipynb 8
17
+ # %% ../02_shell.ipynb
18
18
  @patch
19
19
  def run_cell(self:TerminalInteractiveShell, cell, timeout=None):
20
20
  "Wrapper for original `run_cell` which adds timeout and output capture"
@@ -31,7 +31,7 @@ def run_cell(self:TerminalInteractiveShell, cell, timeout=None):
31
31
  finally:
32
32
  if timeout: signal.alarm(0)
33
33
 
34
- # %% ../02_shell.ipynb 9
34
+ # %% ../02_shell.ipynb
35
35
  def get_shell()->TerminalInteractiveShell:
36
36
  "Get a `TerminalInteractiveShell` with minimal functionality"
37
37
  sh = TerminalInteractiveShell()
@@ -3,7 +3,7 @@
3
3
  # %% auto 0
4
4
  __all__ = ['doctype', 'json_to_xml', 'mk_doctype', 'mk_doc', 'docs_xml', 'files2ctx', 'folder2ctx', 'folder2ctx_cli']
5
5
 
6
- # %% ../00_xml.ipynb 3
6
+ # %% ../00_xml.ipynb
7
7
  import hashlib,xml.etree.ElementTree as ET
8
8
  from collections import namedtuple
9
9
 
@@ -15,7 +15,7 @@ from fastcore.script import call_parse
15
15
  try: from IPython import display
16
16
  except: display=None
17
17
 
18
- # %% ../00_xml.ipynb 4
18
+ # %% ../00_xml.ipynb
19
19
  def json_to_xml(d:dict, # JSON dictionary to convert
20
20
  rnm:str # Root name
21
21
  )->str:
@@ -31,10 +31,10 @@ def json_to_xml(d:dict, # JSON dictionary to convert
31
31
  ET.indent(root)
32
32
  return ET.tostring(root, encoding='unicode')
33
33
 
34
- # %% ../00_xml.ipynb 9
34
+ # %% ../00_xml.ipynb
35
35
  doctype = namedtuple('doctype', ['src', 'content'])
36
36
 
37
- # %% ../00_xml.ipynb 11
37
+ # %% ../00_xml.ipynb
38
38
  def _add_nls(s):
39
39
  "Add newlines to start and end of `s` if missing"
40
40
  if not s: return s
@@ -42,7 +42,7 @@ def _add_nls(s):
42
42
  if s[-1]!='\n': s = s+'\n'
43
43
  return s
44
44
 
45
- # %% ../00_xml.ipynb 16
45
+ # %% ../00_xml.ipynb
46
46
  def mk_doctype(content:str, # The document content
47
47
  src:Optional[str]=None # URL, filename, etc; defaults to `md5(content)` if not provided
48
48
  ) -> namedtuple:
@@ -50,7 +50,7 @@ def mk_doctype(content:str, # The document content
50
50
  if src is None: src = hashlib.md5(content.encode()).hexdigest()[:8]
51
51
  return doctype(_add_nls(str(src).strip()), _add_nls(content.strip()))
52
52
 
53
- # %% ../00_xml.ipynb 19
53
+ # %% ../00_xml.ipynb
54
54
  def mk_doc(index:int, # The document index
55
55
  content:str, # The document content
56
56
  src:Optional[str]=None, # URL, filename, etc; defaults to `md5(content)` if not provided
@@ -62,7 +62,7 @@ def mk_doc(index:int, # The document index
62
62
  src = Src(NotStr(dt.src))
63
63
  return Document(src, content, index=index, **kwargs)
64
64
 
65
- # %% ../00_xml.ipynb 22
65
+ # %% ../00_xml.ipynb
66
66
  def docs_xml(docs:list[str], # The content of each document
67
67
  srcs:Optional[list]=None, # URLs, filenames, etc; each one defaults to `md5(content)` if not provided
68
68
  prefix:bool=True, # Include Anthropic's suggested prose intro?
@@ -75,7 +75,7 @@ def docs_xml(docs:list[str], # The content of each document
75
75
  docs = (mk_doc(i+1, d, s, **kw) for i,(d,s,kw) in enumerate(zip(docs,srcs,details)))
76
76
  return pre + to_xml(Documents(docs))
77
77
 
78
- # %% ../00_xml.ipynb 29
78
+ # %% ../00_xml.ipynb
79
79
  def files2ctx(
80
80
  fnames:list[Union[str,Path]], # List of file names to add to context
81
81
  prefix:bool=True # Include Anthropic's suggested prose intro?
@@ -84,7 +84,7 @@ def files2ctx(
84
84
  contents = [o.read_text() for o in fnames]
85
85
  return docs_xml(contents, fnames, prefix=prefix)
86
86
 
87
- # %% ../00_xml.ipynb 32
87
+ # %% ../00_xml.ipynb
88
88
  @delegates(globtastic)
89
89
  def folder2ctx(
90
90
  folder:Union[str,Path], # Folder name containing files to add to context
@@ -94,7 +94,7 @@ def folder2ctx(
94
94
  fnames = globtastic(folder, **kwargs)
95
95
  return files2ctx(fnames, prefix=prefix)
96
96
 
97
- # %% ../00_xml.ipynb 34
97
+ # %% ../00_xml.ipynb
98
98
  @call_parse
99
99
  @delegates(folder2ctx)
100
100
  def folder2ctx_cli(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: toolslm
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Tools to make language models a bit easier to use
5
5
  Home-page: https://github.com/AnswerDotAI/toolslm
6
6
  Author: Jeremy Howard
@@ -1 +0,0 @@
1
- __version__ = "0.2.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes