1111import re
1212import sys
1313from collections .abc import Iterable
14- from typing import Literal , NotRequired , TypedDict , cast
14+ from typing import Any , Literal , NotRequired , TypedDict , cast
1515
1616from litellm import ChatCompletionToolParam , ChatCompletionToolParamFunctionChunk
1717
@@ -41,6 +41,11 @@ class TextPart(TypedDict):
4141TASK_TRACKER_TOOL_NAME = "task_tracker"
4242
4343# Inspired by: https://docs.together.ai/docs/llama-3-function-calling#function-calling-w-llama-31-70b
44+ MISSING_DESCRIPTION_PLACEHOLDER = "No description provided"
45+ SCHEMA_INDENT_STEP = 2
46+ SCHEMA_UNION_KEYS = ("anyOf" , "oneOf" , "allOf" )
47+
48+
4449system_message_suffix_TEMPLATE = """
4550You have access to the following functions:
4651
@@ -487,6 +492,155 @@ def convert_tool_call_to_string(tool_call: dict) -> str:
487492 return ret
488493
489494
495+ def _summarize_schema_type (schema : object | None ) -> str :
496+ """
497+ Capture array, union, enum, and nested type info.
498+ """
499+ if not isinstance (schema , dict ):
500+ return "unknown" if schema is None else str (schema )
501+
502+ for key in SCHEMA_UNION_KEYS :
503+ if key in schema :
504+ return " or " .join (_summarize_schema_type (option ) for option in schema [key ])
505+
506+ schema_type = schema .get ("type" )
507+ if isinstance (schema_type , list ):
508+ return " or " .join (str (t ) for t in schema_type )
509+ if schema_type == "array" :
510+ items = schema .get ("items" )
511+ if isinstance (items , list ):
512+ item_types = ", " .join (_summarize_schema_type (item ) for item in items )
513+ return f"array[{ item_types } ]"
514+ if isinstance (items , dict ):
515+ return f"array[{ _summarize_schema_type (items )} ]"
516+ return "array"
517+ if schema_type :
518+ return str (schema_type )
519+ if "enum" in schema :
520+ return "enum"
521+ return "unknown"
522+
523+
524+ def _indent (indent : int ) -> str :
525+ return " " * indent
526+
527+
528+ def _nested_indent (indent : int , levels : int = 1 ) -> int :
529+ return indent + SCHEMA_INDENT_STEP * levels
530+
531+
532+ def _get_description (schema : dict [str , object ] | None ) -> str :
533+ """
534+ Extract description from schema, or return placeholder if missing.
535+ """
536+ if not isinstance (schema , dict ):
537+ return MISSING_DESCRIPTION_PLACEHOLDER
538+ description = schema .get ("description" )
539+ if isinstance (description , str ) and description .strip ():
540+ return description
541+ return MISSING_DESCRIPTION_PLACEHOLDER
542+
543+
544+ def _format_union_details (schema : dict [str , object ], indent : int ) -> list [str ] | None :
545+ for key in SCHEMA_UNION_KEYS :
546+ options = schema .get (key )
547+ if not isinstance (options , list ):
548+ continue
549+ lines = [f"{ _indent (indent )} { key } options:" ]
550+ for option in options :
551+ option_type = _summarize_schema_type (option )
552+ option_line = f"{ _indent (_nested_indent (indent ))} - { option_type } "
553+ option_line += (
554+ f": { _get_description (option if isinstance (option , dict ) else None )} "
555+ )
556+ lines .append (option_line )
557+ lines .extend (_format_schema_detail (option , _nested_indent (indent , 2 )))
558+ return lines
559+ return None
560+
561+
562+ def _format_array_details (schema : dict [str , object ], indent : int ) -> list [str ]:
563+ lines = [f"{ _indent (indent )} Array items:" ]
564+ items = schema .get ("items" )
565+ if isinstance (items , list ):
566+ for index , item_schema in enumerate (items ):
567+ item_type = _summarize_schema_type (item_schema )
568+ lines .append (
569+ f"{ _indent (_nested_indent (indent ))} - index { index } : { item_type } "
570+ )
571+ lines .extend (_format_schema_detail (item_schema , _nested_indent (indent , 2 )))
572+ elif isinstance (items , dict ):
573+ lines .append (
574+ f"{ _indent (_nested_indent (indent ))} Type: { _summarize_schema_type (items )} "
575+ )
576+ lines .extend (_format_schema_detail (items , _nested_indent (indent , 2 )))
577+ else :
578+ lines .append (f"{ _indent (_nested_indent (indent ))} Type: unknown" )
579+ return lines
580+
581+
582+ def _format_additional_properties (
583+ additional_props : object | None , indent : int
584+ ) -> list [str ]:
585+ if isinstance (additional_props , dict ):
586+ line = (
587+ f"{ _indent (indent )} Additional properties allowed: "
588+ f"{ _summarize_schema_type (additional_props )} "
589+ )
590+ lines = [line ]
591+ lines .extend (_format_schema_detail (additional_props , _nested_indent (indent )))
592+ return lines
593+ if additional_props is True :
594+ return [f"{ _indent (indent )} Additional properties allowed." ]
595+ if additional_props is False :
596+ return [f"{ _indent (indent )} Additional properties not allowed." ]
597+ return []
598+
599+
600+ def _format_object_details (schema : dict [str , Any ], indent : int ) -> list [str ]:
601+ lines : list [str ] = []
602+ properties = schema .get ("properties" , {})
603+ required = set (schema .get ("required" , []))
604+ if isinstance (properties , dict ) and properties :
605+ lines .append (f"{ _indent (indent )} Object properties:" )
606+ for name , prop in properties .items ():
607+ prop_type = _summarize_schema_type (prop )
608+ required_flag = "required" if name in required else "optional"
609+ prop_desc = _get_description (prop if isinstance (prop , dict ) else None )
610+ lines .append (
611+ f"{ _indent (_nested_indent (indent ))} - { name } ({ prop_type } ,"
612+ f" { required_flag } ): { prop_desc } "
613+ )
614+ lines .extend (_format_schema_detail (prop , _nested_indent (indent , 2 )))
615+ lines .extend (
616+ _format_additional_properties (schema .get ("additionalProperties" ), indent )
617+ )
618+ return lines
619+
620+
621+ def _format_schema_detail (schema : object | None , indent : int = 4 ) -> list [str ]:
622+ """Recursively describe arrays, objects, unions, and additional properties."""
623+ if not isinstance (schema , dict ):
624+ return []
625+
626+ union_lines = _format_union_details (schema , indent )
627+ if union_lines is not None :
628+ return union_lines
629+
630+ schema_type = schema .get ("type" )
631+ if isinstance (schema_type , list ):
632+ allowed_types = ", " .join (str (t ) for t in schema_type )
633+ return [f"{ _indent (indent )} Allowed types: { allowed_types } " ]
634+
635+ if schema_type == "array" :
636+ return _format_array_details (schema , indent )
637+
638+ if schema_type == "object" :
639+ return _format_object_details (schema , indent )
640+
641+ return []
642+
643+
490644def convert_tools_to_description (tools : list [ChatCompletionToolParam ]) -> str :
491645 ret = ""
492646 for i , tool in enumerate (tools ):
@@ -504,15 +658,14 @@ def convert_tools_to_description(tools: list[ChatCompletionToolParam]) -> str:
504658 required_params = set (fn ["parameters" ].get ("required" , []))
505659
506660 for j , (param_name , param_info ) in enumerate (properties .items ()):
507- # Indicate required/optional in parentheses with type
508661 is_required = param_name in required_params
509662 param_status = "required" if is_required else "optional"
510- param_type = param_info . get ( "type" , "string" )
663+ param_type = _summarize_schema_type ( param_info )
511664
512- # Get parameter description
513- desc = param_info .get ("description" , "No description provided" )
665+ desc = _get_description (
666+ param_info if isinstance (param_info , dict ) else None
667+ )
514668
515- # Handle enum values if present
516669 if "enum" in param_info :
517670 enum_values = ", " .join (f"`{ v } `" for v in param_info ["enum" ])
518671 desc += f"\n Allowed values: [{ enum_values } ]"
@@ -521,34 +674,10 @@ def convert_tools_to_description(tools: list[ChatCompletionToolParam]) -> str:
521674 f" ({ j + 1 } ) { param_name } ({ param_type } , { param_status } ): { desc } \n "
522675 )
523676
524- # Handle nested structure for array/object types
525- if param_type == "array" and "items" in param_info :
526- items = param_info ["items" ]
527- if items .get ("type" ) == "object" and "properties" in items :
528- ret += " task_list array item structure:\n "
529- item_properties = items ["properties" ]
530- item_required = set (items .get ("required" , []))
531- for k , (item_param_name , item_param_info ) in enumerate (
532- item_properties .items ()
533- ):
534- item_is_required = item_param_name in item_required
535- item_status = "required" if item_is_required else "optional"
536- item_type = item_param_info .get ("type" , "string" )
537- item_desc = item_param_info .get (
538- "description" , "No description provided"
539- )
540-
541- # Handle enum values for nested items
542- if "enum" in item_param_info :
543- item_enum_values = ", " .join (
544- f"`{ v } `" for v in item_param_info ["enum" ]
545- )
546- item_desc += f" Allowed values: [{ item_enum_values } ]"
677+ detail_lines = _format_schema_detail (param_info , indent = 6 )
678+ if detail_lines :
679+ ret += "\n " .join (detail_lines ) + "\n "
547680
548- ret += (
549- f" - { item_param_name } ({ item_type } , "
550- f"{ item_status } ): { item_desc } \n "
551- )
552681 else :
553682 ret += "No parameters are required for this function.\n "
554683
0 commit comments