1212import inspect
1313import docstring_parser
1414from loguru import logger
15- from typing import Optional , Dict , List , Union , Tuple , Callable
15+ import typing
16+ from typing import Optional , Dict , List , Union , Tuple , Callable , get_args , get_origin
1617from copy import deepcopy
1718
1819from litellm import completion , completion_cost , token_counter
4546
4647
4748def toolnames2funcs (tools ):
49+ """
50+ Convert a list of tool names to a dictionary of functions.
51+
52+ Args:
53+ tools: List of tools, each with a name.
54+
55+ Returns:
56+ Dictionary of function names to functions.
57+
58+ Raises:
59+ Exception: If a function is not found.
60+ """
4861 fmap = {}
4962 for tool in tools :
5063 name = tool ["function" ]["name" ]
@@ -54,15 +67,165 @@ def toolnames2funcs(tools):
5467 fmap [name ] = func
5568 return fmap
5669
70+
5771def get_func_by_name (name ):
58- # Walk up the call stack
72+ """
73+ Get a function by name.
74+
75+ Args:
76+ name: Name of the function.
77+
78+ Returns:
79+ Function if found, None otherwise.
80+
81+ Raises:
82+ Exception: If a function is not found.
83+ """
5984 for frame_info in inspect .stack ():
6085 frame = frame_info .frame
6186 func = frame .f_locals .get (name ) or frame .f_globals .get (name )
6287 if callable (func ):
6388 return func
6489 return None # Not found
6590
91+ def ptype2schema (py_type ):
92+ """
93+ Convert a Python type to a JSON schema.
94+
95+ Args:
96+ py_type: Python type to convert.
97+
98+ Returns:
99+ JSON schema for the given Python type.
100+
101+ Raises:
102+ ValueError: If the type is not supported.
103+ """
104+ # Handle bare None
105+ if py_type is type (None ):
106+ return {"type" : "null" }
107+
108+ origin = get_origin (py_type )
109+ args = get_args (py_type )
110+
111+ if origin is None :
112+ # Base types
113+ if py_type is str :
114+ return {"type" : "string" }
115+ elif py_type is int :
116+ return {"type" : "integer" }
117+ elif py_type is float :
118+ return {"type" : "number" }
119+ elif py_type is bool :
120+ return {"type" : "boolean" }
121+ elif py_type is type (None ):
122+ return {"type" : "null" }
123+ else :
124+ return {"type" : "string" } # Fallback
125+
126+ elif origin is list or origin is typing .List :
127+ item_type = ptype2schema (args [0 ]) if args else {"type" : "string" }
128+ return {"type" : "array" , "items" : item_type }
129+
130+ elif origin is dict or origin is typing .Dict :
131+ key_type , val_type = args if args else (str , str )
132+ # JSON Schema requires string keys
133+ if key_type != str :
134+ raise ValueError ("JSON object keys must be strings" )
135+ return {"type" : "object" , "additionalProperties" : ptype2schema (val_type )}
136+
137+ elif origin is typing .Union :
138+ # Flatten nested Union
139+ flat_args = []
140+ for arg in args :
141+ if get_origin (arg ) is typing .Union :
142+ flat_args .extend (get_args (arg ))
143+ else :
144+ flat_args .append (arg )
145+
146+ schemas = [ptype2schema (a ) for a in flat_args ]
147+ return {"anyOf" : schemas }
148+
149+ elif origin is typing .Literal :
150+ return {"enum" : list (args )}
151+
152+ else :
153+ return {"type" : "string" } # fallback for unsupported/unknown
154+
155+ def function2schema (func , include_return_type = True ):
156+ """
157+ Convert a function to a JSON schema.
158+
159+ Args:
160+ func: Function to convert.
161+ include_return_type: Whether to include the return type in the schema.
162+
163+ Returns:
164+ JSON schema for the given function.
165+
166+ Raises:
167+ ValueError: If the function docstring is empty.
168+ """
169+ doc = docstring_parser .parse (func .__doc__ )
170+ desc = doc .short_description + "\n \n " + doc .long_description if doc .long_description else doc .short_description
171+ if not desc :
172+ raise ValueError ("Function docstring is empty" )
173+ argdescs = {arg .arg_name : arg .description for arg in doc .params }
174+ argtypes = {}
175+ for arg in doc .params :
176+ argtype = arg .type_name
177+ # if the argtype is not specified, skip, we will use the argument type
178+ if argtype is None :
179+ continue
180+ # if the argtype starts with a brace, we assume it is already specified as a JSON schema
181+ if argtype .startswith ("{" ):
182+ argtypes [arg .arg_name ] = json .loads (argtype )
183+ else :
184+ # otherwise, we assume it is a python type
185+ argtypes [arg .arg_name ] = ptype2schema (argtype )
186+ retdesc = doc .returns .description if doc .returns else ""
187+ if not retdesc :
188+ raise ValueError ("Function return type is not specified in docstring" )
189+ retschema = ptype2schema (func .__annotations__ .get ("return" , None ))
190+ desc = desc + "\n \n " + "The function returns: " + str (retdesc )
191+ if include_return_type :
192+ desc = desc + "\n \n " + "The return type is: " + str (retschema )
193+ sig = inspect .signature (func )
194+ parameters = sig .parameters
195+
196+ props = {}
197+ required = []
198+
199+ for name , param in parameters .items ():
200+ if name == 'self' :
201+ continue
202+
203+ if name in argtypes :
204+ schema = argtypes [name ]
205+ else :
206+ # Use the type annotation if available, otherwise default to string
207+ ptype = param .annotation if param .annotation != inspect .Parameter .empty else str
208+ schema = ptype2schema (ptype )
209+ schema ["description" ] = argdescs .get (name , "" )
210+
211+ if param .default != inspect .Parameter .empty :
212+ schema ["default" ] = param .default
213+ else :
214+ required .append (name )
215+
216+ props [name ] = schema
217+
218+ return {
219+ "name" : func .__name__ ,
220+ "description" : desc ,
221+ "parameters" : {
222+ "type" : "object" ,
223+ "properties" : props ,
224+ "required" : required
225+ }
226+ }
227+
228+
66229class LLMS :
67230 """
68231 Class that represents a preconfigured set of large language modelservices.
@@ -345,38 +508,20 @@ def make_tooling(functions: Union[Callable, List[Callable]]) -> List[Dict]:
345508 be useful to the LLM. The same goes for the description of each of the arguments for
346509 the function.
347510
348- With the ReST docustring format, use :param argname: description followed by
349- a new line, then the type of the argument specified via :type argname: type.
350- Specify the description of the return value via :returns: description followed
351- by a new line, then the type of the return value specified via :rtype: type.
352- The description should have one summary line, followed by a blank line then
353- followed by the detailed description text.
354-
355- IMPORTANT: the standard python type names are not supported, instead use the json
356- schema types: string, number, integer, boolean, array, object, null. object can be
357- used to specify a dictionary type.
358- If you need to specify complex nested types, avoid using this method and instead
359- create the tooling descriptions directly, using a nested json schema e.g.
360- to specify a list of dictionaries with the key 'name' of type string and 'age' of type integer:
361- ```
362- {
363- "type": "array",
364- "items": {
365- "type": "object",
366- "properties": {
367- "name": {"type": "string"},
368- "age": {"type": "integer"}
369- },
370- "required": ["name", "age"]
371- }
372- "description": "A list of people with their name and age"
373- }
374- ```
511+ The type of all arguments and of the function return value should get specified using
512+ standard Python type annotations. These types will get converted to json schema types.
513+
514+ Each argument and the return value must be documented in the docstring.
515+
516+ If the type of a parameter is specified in the docstring, that type will get used
517+ instead of the type annotation specified in the function signature.
518+ If the type of a parameter is specified in the docstring as a json schema type
519+ starting and ending with a brace, that schema is directly used.
520+
375521 See https://platform.openai.com/docs/guides/function-calling
376522
377523 Args:
378- functions: a function or list of functions. The function(s) documentation strings are parsed
379- to create the tooling descriptions.
524+ functions: a function or list of functions.
380525
381526 Returns:
382527 A list of tool dictionaries, each dictionary describing a tool.
@@ -385,48 +530,10 @@ def make_tooling(functions: Union[Callable, List[Callable]]) -> List[Dict]:
385530 functions = [functions ]
386531 tools = []
387532 for func in functions :
388- if not callable (func ):
389- raise ValueError (f"Error: { func } is not callable" )
390- doc = docstring_parser .parse (func .__doc__ )
391- argspec = inspect .getfullargspec (func )
392- nrequired = len (argspec .args ) - len (argspec .defaults ) if argspec .defaults else len (argspec .args )
393- # for each parameter get the type as specified in the docstring, if not specified, get the
394- # name of the type from the argspec annotation information, if not specified there, assume string
395- argtypes = []
396- for idx , aname in enumerate (argspec .args ):
397- if idx < len (doc .params ):
398- argtypes .append (doc .params [idx ].type_name )
399- # it seems proper python types are not supported?
400- # elif argspec.annotations.get(aname):
401- # argtypes.append(argspec.annotations[aname].__name__)
402- else :
403- argtypes .append ("string" )
404- argdescs = []
405- for idx , aname in enumerate (argspec .args ):
406- if idx < len (doc .params ):
407- argdescs .append (doc .params [idx ].description )
408- else :
409- raise ValueError (f"Error: Missing description for parameter { aname } in doc of function { func .__name__ } " )
410- desc = doc .short_description + "\n \n " + doc .long_description
411- tools .append ({
412- "type" : "function" ,
413- "function" : {
414- "name" : func .__name__ ,
415- "description" : desc ,
416- "parameters" : {
417- "type" : "object" ,
418- "properties" : {
419- doc .params [i ].arg_name : {
420- "type" : argtypes [i ],
421- "description" : argdescs [i ],
422- } for i in range (len (argspec .args ))
423- },
424- "required" : [param .arg_name for param in doc .params [:nrequired ]],
425- },
426- },
427- })
533+ tools .append (function2schema (func ))
428534 return tools
429535
536+
430537 def supports_response_format (self , llmalias : str ) -> bool :
431538 """
432539 Check if the model supports the response format parameters. This usually just indicates support
0 commit comments