2024-01-25 14:51:24 -05:00
from __future__ import annotations
2024-01-12 20:46:45 +01:00
import inspect
import json
2024-01-25 14:51:24 -05:00
import re
2024-01-12 20:46:45 +01:00
from copy import copy
2024-01-25 14:51:24 -05:00
from enum import Enum
from inspect import getdoc , isclass
from typing import TYPE_CHECKING , Any , Callable , List , Optional , Union , get_args , get_origin , get_type_hints
2024-01-12 20:46:45 +01:00
2024-01-16 13:10:48 +01:00
from docstring_parser import parse
2024-01-25 14:51:24 -05:00
from pydantic import BaseModel , Field , create_model
if TYPE_CHECKING :
from types import GenericAlias
else :
# python 3.8 compat
from typing import _GenericAlias as GenericAlias
2024-01-12 20:46:45 +01:00
class PydanticDataType ( Enum ):
"""
Defines the data types supported by the grammar_generator.
Attributes:
STRING (str): Represents a string data type.
BOOLEAN (str): Represents a boolean data type.
INTEGER (str): Represents an integer data type.
FLOAT (str): Represents a float data type.
OBJECT (str): Represents an object data type.
ARRAY (str): Represents an array data type.
ENUM (str): Represents an enum data type.
CUSTOM_CLASS (str): Represents a custom class data type.
"""
2024-01-16 13:10:48 +01:00
2024-01-12 20:46:45 +01:00
STRING = "string"
TRIPLE_QUOTED_STRING = "triple_quoted_string"
2024-01-16 13:10:48 +01:00
MARKDOWN_CODE_BLOCK = "markdown_code_block"
2024-01-12 20:46:45 +01:00
BOOLEAN = "boolean"
INTEGER = "integer"
FLOAT = "float"
OBJECT = "object"
ARRAY = "array"
ENUM = "enum"
ANY = "any"
NULL = "null"
CUSTOM_CLASS = "custom-class"
CUSTOM_DICT = "custom-dict"
SET = "set"
2024-01-25 14:51:24 -05:00
def map_pydantic_type_to_gbnf ( pydantic_type : type [ Any ]) -> str :
2024-01-12 20:46:45 +01:00
if isclass ( pydantic_type ) and issubclass ( pydantic_type , str ):
return PydanticDataType . STRING . value
elif isclass ( pydantic_type ) and issubclass ( pydantic_type , bool ):
return PydanticDataType . BOOLEAN . value
elif isclass ( pydantic_type ) and issubclass ( pydantic_type , int ):
return PydanticDataType . INTEGER . value
elif isclass ( pydantic_type ) and issubclass ( pydantic_type , float ):
return PydanticDataType . FLOAT . value
elif isclass ( pydantic_type ) and issubclass ( pydantic_type , Enum ):
return PydanticDataType . ENUM . value
elif isclass ( pydantic_type ) and issubclass ( pydantic_type , BaseModel ):
return format_model_and_field_name ( pydantic_type . __name__ )
2024-01-25 14:51:24 -05:00
elif get_origin ( pydantic_type ) is list :
2024-01-12 20:46:45 +01:00
element_type = get_args ( pydantic_type )[ 0 ]
return f " { map_pydantic_type_to_gbnf ( element_type ) } -list"
2024-01-25 14:51:24 -05:00
elif get_origin ( pydantic_type ) is set :
2024-01-12 20:46:45 +01:00
element_type = get_args ( pydantic_type )[ 0 ]
return f " { map_pydantic_type_to_gbnf ( element_type ) } -set"
2024-01-25 14:51:24 -05:00
elif get_origin ( pydantic_type ) is Union :
2024-01-12 20:46:45 +01:00
union_types = get_args ( pydantic_type )
union_rules = [ map_pydantic_type_to_gbnf ( ut ) for ut in union_types ]
return f "union- { '-or-' . join ( union_rules ) } "
2024-01-25 14:51:24 -05:00
elif get_origin ( pydantic_type ) is Optional :
2024-01-12 20:46:45 +01:00
element_type = get_args ( pydantic_type )[ 0 ]
return f "optional- { map_pydantic_type_to_gbnf ( element_type ) } "
elif isclass ( pydantic_type ):
return f " { PydanticDataType . CUSTOM_CLASS . value } - { format_model_and_field_name ( pydantic_type . __name__ ) } "
2024-01-25 14:51:24 -05:00
elif get_origin ( pydantic_type ) is dict :
2024-01-12 20:46:45 +01:00
key_type , value_type = get_args ( pydantic_type )
return f "custom-dict-key-type- { format_model_and_field_name ( map_pydantic_type_to_gbnf ( key_type )) } -value-type- { format_model_and_field_name ( map_pydantic_type_to_gbnf ( value_type )) } "
else :
return "unknown"
def format_model_and_field_name ( model_name : str ) -> str :
2024-01-16 13:10:48 +01:00
parts = re . findall ( "[A-Z][^A-Z]*" , model_name )
2024-01-12 20:46:45 +01:00
if not parts : # Check if the list is empty
return model_name . lower () . replace ( "_" , "-" )
2024-01-16 13:10:48 +01:00
return "-" . join ( part . lower () . replace ( "_" , "-" ) for part in parts )
2024-01-12 20:46:45 +01:00
def generate_list_rule ( element_type ):
"""
Generate a GBNF rule for a list of a given element type.
:param element_type: The type of the elements in the list (e.g., 'string').
:return: A string representing the GBNF rule for a list of the given type.
"""
rule_name = f " { map_pydantic_type_to_gbnf ( element_type ) } -list"
element_rule = map_pydantic_type_to_gbnf ( element_type )
2024-01-16 13:10:48 +01:00
list_rule = rf ' { rule_name } ::= "[" { element_rule } ("," { element_rule } )* "]"'
2024-01-12 20:46:45 +01:00
return list_rule
def get_members_structure ( cls , rule_name ):
if issubclass ( cls , Enum ):
# Handle Enum types
2024-01-16 13:10:48 +01:00
members = [ f '" \\ " { member . value } \\ ""' for name , member in cls . __members__ . items ()]
2024-01-12 20:46:45 +01:00
return f " { cls . __name__ . lower () } ::= " + " | " . join ( members )
if cls . __annotations__ and cls . __annotations__ != {}:
result = f ' { rule_name } ::= " {{ "'
# Modify this comprehension
2024-01-16 13:10:48 +01:00
members = [
f ' " \\ " { name } \\ "" ":" { map_pydantic_type_to_gbnf ( param_type ) } '
for name , param_type in cls . __annotations__ . items ()
if name != "self"
]
2024-01-12 20:46:45 +01:00
result += '"," ' . join ( members )
result += ' "}"'
2024-01-25 14:51:24 -05:00
return result
if rule_name == "custom-class-any" :
2024-01-16 13:10:48 +01:00
result = f " { rule_name } ::= "
result += "value"
2024-01-25 14:51:24 -05:00
return result
2024-01-12 20:46:45 +01:00
2024-01-25 14:51:24 -05:00
init_signature = inspect . signature ( cls . __init__ )
parameters = init_signature . parameters
result = f ' { rule_name } ::= " {{ "'
# Modify this comprehension too
members = [
f ' " \\ " { name } \\ "" ":" { map_pydantic_type_to_gbnf ( param . annotation ) } '
for name , param in parameters . items ()
if name != "self" and param . annotation != inspect . Parameter . empty
]
result += '", "' . join ( members )
result += ' "}"'
return result
2024-01-12 20:46:45 +01:00
def regex_to_gbnf ( regex_pattern : str ) -> str :
"""
Translate a basic regex pattern to a GBNF rule.
Note: This function handles only a subset of simple regex patterns.
"""
gbnf_rule = regex_pattern
# Translate common regex components to GBNF
2024-01-16 13:10:48 +01:00
gbnf_rule = gbnf_rule . replace ( " \\ d" , "[0-9]" )
gbnf_rule = gbnf_rule . replace ( " \\ s" , "[ \t\n ]" )
2024-01-12 20:46:45 +01:00
# Handle quantifiers and other regex syntax that is similar in GBNF
# (e.g., '*', '+', '?', character classes)
return gbnf_rule
def generate_gbnf_integer_rules ( max_digit = None , min_digit = None ):
"""
Generate GBNF Integer Rules
Generates GBNF (Generalized Backus-Naur Form) rules for integers based on the given maximum and minimum digits.
Parameters:
2024-01-16 13:10:48 +01:00
max_digit (int): The maximum number of digits for the integer. Default is None.
min_digit (int): The minimum number of digits for the integer. Default is None.
2024-01-12 20:46:45 +01:00
Returns:
2024-01-16 13:10:48 +01:00
integer_rule (str): The identifier for the integer rule generated.
additional_rules (list): A list of additional rules generated based on the given maximum and minimum digits.
2024-01-12 20:46:45 +01:00
"""
additional_rules = []
# Define the rule identifier based on max_digit and min_digit
integer_rule = "integer-part"
if max_digit is not None :
integer_rule += f "-max { max_digit } "
if min_digit is not None :
integer_rule += f "-min { min_digit } "
# Handling Integer Rules
if max_digit is not None or min_digit is not None :
# Start with an empty rule part
2024-01-16 13:10:48 +01:00
integer_rule_part = ""
2024-01-12 20:46:45 +01:00
# Add mandatory digits as per min_digit
if min_digit is not None :
2024-01-16 13:10:48 +01:00
integer_rule_part += "[0-9] " * min_digit
2024-01-12 20:46:45 +01:00
# Add optional digits up to max_digit
if max_digit is not None :
optional_digits = max_digit - ( min_digit if min_digit is not None else 0 )
2024-01-16 13:10:48 +01:00
integer_rule_part += "" . join ([ "[0-9]? " for _ in range ( optional_digits )])
2024-01-12 20:46:45 +01:00
# Trim the rule part and append it to additional rules
integer_rule_part = integer_rule_part . strip ()
if integer_rule_part :
2024-01-16 13:10:48 +01:00
additional_rules . append ( f " { integer_rule } ::= { integer_rule_part } " )
2024-01-12 20:46:45 +01:00
return integer_rule , additional_rules
def generate_gbnf_float_rules ( max_digit = None , min_digit = None , max_precision = None , min_precision = None ):
"""
Generate GBNF float rules based on the given constraints.
:param max_digit: Maximum number of digits in the integer part (default: None)
:param min_digit: Minimum number of digits in the integer part (default: None)
:param max_precision: Maximum number of digits in the fractional part (default: None)
:param min_precision: Minimum number of digits in the fractional part (default: None)
:return: A tuple containing the float rule and additional rules as a list
Example Usage:
max_digit = 3
min_digit = 1
max_precision = 2
min_precision = 1
generate_gbnf_float_rules(max_digit, min_digit, max_precision, min_precision)
Output:
('float-3-1-2-1', ['integer-part-max3-min1 ::= [0-9] [0-9] [0-9]?', 'fractional-part-max2-min1 ::= [0-9] [0-9]?', 'float-3-1-2-1 ::= integer-part-max3-min1 "." fractional-part-max2-min
*1'])
Note:
GBNF stands for Generalized Backus-Naur Form, which is a notation technique to specify the syntax of programming languages or other formal grammars.
"""
additional_rules = []
# Define the integer part rule
2024-01-16 13:10:48 +01:00
integer_part_rule = (
"integer-part" + ( f "-max { max_digit } " if max_digit is not None else "" ) + (
2024-01-12 20:46:45 +01:00
f "-min { min_digit } " if min_digit is not None else "" )
2024-01-16 13:10:48 +01:00
)
2024-01-12 20:46:45 +01:00
# Define the fractional part rule based on precision constraints
fractional_part_rule = "fractional-part"
2024-01-16 13:10:48 +01:00
fractional_rule_part = ""
2024-01-12 20:46:45 +01:00
if max_precision is not None or min_precision is not None :
fractional_part_rule += ( f "-max { max_precision } " if max_precision is not None else "" ) + (
2024-01-16 13:10:48 +01:00
f "-min { min_precision } " if min_precision is not None else ""
)
2024-01-12 20:46:45 +01:00
# Minimum number of digits
2024-01-16 13:10:48 +01:00
fractional_rule_part = "[0-9]" * ( min_precision if min_precision is not None else 1 )
2024-01-12 20:46:45 +01:00
# Optional additional digits
2024-01-16 13:10:48 +01:00
fractional_rule_part += "" . join (
[ " [0-9]?" ] * (( max_precision - (
min_precision if min_precision is not None else 1 )) if max_precision is not None else 0 )
)
additional_rules . append ( f " { fractional_part_rule } ::= { fractional_rule_part } " )
2024-01-12 20:46:45 +01:00
# Define the float rule
float_rule = f "float- { max_digit if max_digit is not None else 'X' } - { min_digit if min_digit is not None else 'X' } - { max_precision if max_precision is not None else 'X' } - { min_precision if min_precision is not None else 'X' } "
additional_rules . append ( f ' { float_rule } ::= { integer_part_rule } "." { fractional_part_rule } ' )
# Generating the integer part rule definition, if necessary
if max_digit is not None or min_digit is not None :
2024-01-16 13:10:48 +01:00
integer_rule_part = "[0-9]"
2024-01-12 20:46:45 +01:00
if min_digit is not None and min_digit > 1 :
2024-01-16 13:10:48 +01:00
integer_rule_part += " [0-9]" * ( min_digit - 1 )
2024-01-12 20:46:45 +01:00
if max_digit is not None :
2024-01-16 13:10:48 +01:00
integer_rule_part += "" . join ([ " [0-9]?" ] * ( max_digit - ( min_digit if min_digit is not None else 1 )))
additional_rules . append ( f " { integer_part_rule } ::= { integer_rule_part . strip () } " )
2024-01-12 20:46:45 +01:00
return float_rule , additional_rules
2024-01-16 13:10:48 +01:00
def generate_gbnf_rule_for_type (
model_name , field_name , field_type , is_optional , processed_models , created_rules , field_info = None
2024-01-25 14:51:24 -05:00
) -> tuple [ str , list [ str ]]:
2024-01-12 20:46:45 +01:00
"""
Generate GBNF rule for a given field type.
:param model_name: Name of the model.
:param field_name: Name of the field.
:param field_type: Type of the field.
:param is_optional: Whether the field is optional.
:param processed_models: List of processed models.
:param created_rules: List of created rules.
:param field_info: Additional information about the field (optional).
:return: Tuple containing the GBNF type and a list of additional rules.
2024-01-25 14:51:24 -05:00
:rtype: tuple[str, list]
2024-01-12 20:46:45 +01:00
"""
rules = []
field_name = format_model_and_field_name ( field_name )
gbnf_type = map_pydantic_type_to_gbnf ( field_type )
if isclass ( field_type ) and issubclass ( field_type , BaseModel ):
nested_model_name = format_model_and_field_name ( field_type . __name__ )
2024-01-16 13:10:48 +01:00
nested_model_rules , _ = generate_gbnf_grammar ( field_type , processed_models , created_rules )
2024-01-12 20:46:45 +01:00
rules . extend ( nested_model_rules )
gbnf_type , rules = nested_model_name , rules
elif isclass ( field_type ) and issubclass ( field_type , Enum ):
2024-01-16 13:10:48 +01:00
enum_values = [ f '" \\ " { e . value } \\ ""' for e in field_type ] # Adding escaped quotes
2024-01-12 20:46:45 +01:00
enum_rule = f " { model_name } - { field_name } ::= { ' | ' . join ( enum_values ) } "
rules . append ( enum_rule )
gbnf_type , rules = model_name + "-" + field_name , rules
2024-01-16 13:10:48 +01:00
elif get_origin ( field_type ) == list : # Array
2024-01-12 20:46:45 +01:00
element_type = get_args ( field_type )[ 0 ]
2024-01-16 13:10:48 +01:00
element_rule_name , additional_rules = generate_gbnf_rule_for_type (
model_name , f " { field_name } -element" , element_type , is_optional , processed_models , created_rules
)
2024-01-12 20:46:45 +01:00
rules . extend ( additional_rules )
array_rule = f """ { model_name } - { field_name } ::= "[" ws { element_rule_name } ("," ws { element_rule_name } )* "]" """
rules . append ( array_rule )
gbnf_type , rules = model_name + "-" + field_name , rules
elif get_origin ( field_type ) == set or field_type == set : # Array
element_type = get_args ( field_type )[ 0 ]
2024-01-16 13:10:48 +01:00
element_rule_name , additional_rules = generate_gbnf_rule_for_type (
model_name , f " { field_name } -element" , element_type , is_optional , processed_models , created_rules
)
2024-01-12 20:46:45 +01:00
rules . extend ( additional_rules )
array_rule = f """ { model_name } - { field_name } ::= "[" ws { element_rule_name } ("," ws { element_rule_name } )* "]" """
rules . append ( array_rule )
gbnf_type , rules = model_name + "-" + field_name , rules
elif gbnf_type . startswith ( "custom-class-" ):
2024-01-25 14:51:24 -05:00
rules . append ( get_members_structure ( field_type , gbnf_type ))
2024-01-12 20:46:45 +01:00
elif gbnf_type . startswith ( "custom-dict-" ):
key_type , value_type = get_args ( field_type )
2024-01-16 13:10:48 +01:00
additional_key_type , additional_key_rules = generate_gbnf_rule_for_type (
model_name , f " { field_name } -key-type" , key_type , is_optional , processed_models , created_rules
)
additional_value_type , additional_value_rules = generate_gbnf_rule_for_type (
model_name , f " { field_name } -value-type" , value_type , is_optional , processed_models , created_rules
)
gbnf_type = rf ' { gbnf_type } ::= " {{ " ( { additional_key_type } ": " { additional_value_type } ("," "\n" ws { additional_key_type } ":" { additional_value_type } )* )? " }} " '
2024-01-12 20:46:45 +01:00
rules . extend ( additional_key_rules )
rules . extend ( additional_value_rules )
elif gbnf_type . startswith ( "union-" ):
union_types = get_args ( field_type )
union_rules = []
for union_type in union_types :
2024-01-25 14:51:24 -05:00
if isinstance ( union_type , GenericAlias ):
2024-01-16 13:10:48 +01:00
union_gbnf_type , union_rules_list = generate_gbnf_rule_for_type (
model_name , field_name , union_type , False , processed_models , created_rules
)
2024-01-12 20:46:45 +01:00
union_rules . append ( union_gbnf_type )
rules . extend ( union_rules_list )
2024-01-25 14:51:24 -05:00
elif not issubclass ( union_type , type ( None )):
2024-01-16 13:10:48 +01:00
union_gbnf_type , union_rules_list = generate_gbnf_rule_for_type (
model_name , field_name , union_type , False , processed_models , created_rules
)
2024-01-12 20:46:45 +01:00
union_rules . append ( union_gbnf_type )
rules . extend ( union_rules_list )
# Defining the union grammar rule separately
if len ( union_rules ) == 1 :
union_grammar_rule = f " { model_name } - { field_name } -optional ::= { ' | ' . join ( union_rules ) } | null"
else :
union_grammar_rule = f " { model_name } - { field_name } -union ::= { ' | ' . join ( union_rules ) } "
rules . append ( union_grammar_rule )
if len ( union_rules ) == 1 :
gbnf_type = f " { model_name } - { field_name } -optional"
else :
gbnf_type = f " { model_name } - { field_name } -union"
elif isclass ( field_type ) and issubclass ( field_type , str ):
2024-01-16 13:10:48 +01:00
if field_info and hasattr ( field_info , "json_schema_extra" ) and field_info . json_schema_extra is not None :
triple_quoted_string = field_info . json_schema_extra . get ( "triple_quoted_string" , False )
markdown_string = field_info . json_schema_extra . get ( "markdown_code_block" , False )
2024-01-12 20:46:45 +01:00
gbnf_type = PydanticDataType . TRIPLE_QUOTED_STRING . value if triple_quoted_string else PydanticDataType . STRING . value
2024-01-16 13:10:48 +01:00
gbnf_type = PydanticDataType . MARKDOWN_CODE_BLOCK . value if markdown_string else gbnf_type
2024-01-12 20:46:45 +01:00
2024-01-16 13:10:48 +01:00
elif field_info and hasattr ( field_info , "pattern" ):
2024-01-12 20:46:45 +01:00
# Convert regex pattern to grammar rule
regex_pattern = field_info . regex . pattern
gbnf_type = f "pattern- { field_name } ::= { regex_to_gbnf ( regex_pattern ) } "
else :
gbnf_type = PydanticDataType . STRING . value
2024-01-16 13:10:48 +01:00
elif (
isclass ( field_type )
and issubclass ( field_type , float )
and field_info
and hasattr ( field_info , "json_schema_extra" )
and field_info . json_schema_extra is not None
):
2024-01-12 20:46:45 +01:00
# Retrieve precision attributes for floats
2024-01-16 13:10:48 +01:00
max_precision = (
field_info . json_schema_extra . get ( "max_precision" ) if field_info and hasattr ( field_info ,
"json_schema_extra" ) else None
)
min_precision = (
field_info . json_schema_extra . get ( "min_precision" ) if field_info and hasattr ( field_info ,
"json_schema_extra" ) else None
)
max_digits = field_info . json_schema_extra . get ( "max_digit" ) if field_info and hasattr ( field_info ,
"json_schema_extra" ) else None
min_digits = field_info . json_schema_extra . get ( "min_digit" ) if field_info and hasattr ( field_info ,
"json_schema_extra" ) else None
2024-01-12 20:46:45 +01:00
# Generate GBNF rule for float with given attributes
2024-01-16 13:10:48 +01:00
gbnf_type , rules = generate_gbnf_float_rules (
max_digit = max_digits , min_digit = min_digits , max_precision = max_precision , min_precision = min_precision
)
2024-01-12 20:46:45 +01:00
2024-01-16 13:10:48 +01:00
elif (
isclass ( field_type )
and issubclass ( field_type , int )
and field_info
and hasattr ( field_info , "json_schema_extra" )
and field_info . json_schema_extra is not None
):
2024-01-12 20:46:45 +01:00
# Retrieve digit attributes for integers
2024-01-16 13:10:48 +01:00
max_digits = field_info . json_schema_extra . get ( "max_digit" ) if field_info and hasattr ( field_info ,
"json_schema_extra" ) else None
min_digits = field_info . json_schema_extra . get ( "min_digit" ) if field_info and hasattr ( field_info ,
"json_schema_extra" ) else None
2024-01-12 20:46:45 +01:00
# Generate GBNF rule for integer with given attributes
gbnf_type , rules = generate_gbnf_integer_rules ( max_digit = max_digits , min_digit = min_digits )
else :
gbnf_type , rules = gbnf_type , []
2024-01-25 14:51:24 -05:00
return gbnf_type , rules
2024-01-12 20:46:45 +01:00
2024-01-25 14:51:24 -05:00
def generate_gbnf_grammar ( model : type [ BaseModel ], processed_models : set [ type [ BaseModel ]], created_rules : dict [ str , list [ str ]]) -> tuple [ list [ str ], bool ]:
2024-01-12 20:46:45 +01:00
"""
Generate GBnF Grammar
Generates a GBnF grammar for a given model.
:param model: A Pydantic model class to generate the grammar for. Must be a subclass of BaseModel.
:param processed_models: A set of already processed models to prevent infinite recursion.
:param created_rules: A dict containing already created rules to prevent duplicates.
:return: A list of GBnF grammar rules in string format. And two booleans indicating if an extra markdown or triple quoted string is in the grammar.
Example Usage:
```
model = MyModel
processed_models = set()
created_rules = dict()
gbnf_grammar = generate_gbnf_grammar(model, processed_models, created_rules)
```
"""
if model in processed_models :
2024-01-25 14:51:24 -05:00
return [], False
2024-01-12 20:46:45 +01:00
processed_models . add ( model )
model_name = format_model_and_field_name ( model . __name__ )
if not issubclass ( model , BaseModel ):
# For non-Pydantic classes, generate model_fields from __annotations__ or __init__
2024-01-16 13:10:48 +01:00
if hasattr ( model , "__annotations__" ) and model . __annotations__ :
2024-01-12 20:46:45 +01:00
model_fields = { name : ( typ , ... ) for name , typ in model . __annotations__ . items ()}
else :
init_signature = inspect . signature ( model . __init__ )
parameters = init_signature . parameters
2024-01-16 13:10:48 +01:00
model_fields = { name : ( param . annotation , param . default ) for name , param in parameters . items () if
name != "self" }
2024-01-12 20:46:45 +01:00
else :
# For Pydantic models, use model_fields and check for ellipsis (required fields)
model_fields = model . __annotations__
model_rule_parts = []
nested_rules = []
has_markdown_code_block = False
has_triple_quoted_string = False
look_for_markdown_code_block = False
look_for_triple_quoted_string = False
for field_name , field_info in model_fields . items ():
if not issubclass ( model , BaseModel ):
field_type , default_value = field_info
# Check if the field is optional (not required)
is_optional = ( default_value is not inspect . Parameter . empty ) and ( default_value is not Ellipsis )
else :
field_type = field_info
field_info = model . model_fields [ field_name ]
is_optional = field_info . is_required is False and get_origin ( field_type ) is Optional
2024-01-16 13:10:48 +01:00
rule_name , additional_rules = generate_gbnf_rule_for_type (
model_name , format_model_and_field_name ( field_name ), field_type , is_optional , processed_models ,
created_rules , field_info
)
look_for_markdown_code_block = True if rule_name == "markdown_code_block" else False
2024-01-12 20:46:45 +01:00
look_for_triple_quoted_string = True if rule_name == "triple_quoted_string" else False
if not look_for_markdown_code_block and not look_for_triple_quoted_string :
if rule_name not in created_rules :
created_rules [ rule_name ] = additional_rules
2024-01-16 13:10:48 +01:00
model_rule_parts . append ( f ' ws " \\ " { field_name } \\ "" ":" ws { rule_name } ' ) # Adding escaped quotes
2024-01-12 20:46:45 +01:00
nested_rules . extend ( additional_rules )
else :
2024-01-16 13:10:48 +01:00
has_triple_quoted_string = look_for_triple_quoted_string
has_markdown_code_block = look_for_markdown_code_block
2024-01-12 20:46:45 +01:00
fields_joined = r ' "," "\n" ' . join ( model_rule_parts )
2024-01-16 13:10:48 +01:00
model_rule = rf ' { model_name } ::= " {{ " "\n" { fields_joined } "\n" ws " }} "'
2024-01-12 20:46:45 +01:00
2024-01-16 13:10:48 +01:00
has_special_string = False
2024-01-12 20:46:45 +01:00
if has_triple_quoted_string :
2024-01-16 13:10:48 +01:00
model_rule += '" \\ n" ws "}"'
2024-01-12 20:46:45 +01:00
model_rule += '" \\ n" triple-quoted-string'
2024-01-16 13:10:48 +01:00
has_special_string = True
2024-01-12 20:46:45 +01:00
if has_markdown_code_block :
2024-01-16 13:10:48 +01:00
model_rule += '" \\ n" ws "}"'
2024-01-12 20:46:45 +01:00
model_rule += '" \\ n" markdown-code-block'
2024-01-16 13:10:48 +01:00
has_special_string = True
2024-01-12 20:46:45 +01:00
all_rules = [ model_rule ] + nested_rules
2024-01-16 13:10:48 +01:00
return all_rules , has_special_string
2024-01-12 20:46:45 +01:00
2024-01-16 13:10:48 +01:00
def generate_gbnf_grammar_from_pydantic_models (
2024-01-25 14:51:24 -05:00
models : list [ type [ BaseModel ]], outer_object_name : str | None = None , outer_object_content : str | None = None ,
2024-01-16 13:10:48 +01:00
list_of_outputs : bool = False
) -> str :
2024-01-12 20:46:45 +01:00
"""
Generate GBNF Grammar from Pydantic Models.
This method takes a list of Pydantic models and uses them to generate a GBNF grammar string. The generated grammar string can be used for parsing and validating data using the generated
* grammar.
2024-01-16 13:10:48 +01:00
Args:
2024-01-25 14:51:24 -05:00
models (list[type[BaseModel]]): A list of Pydantic models to generate the grammar from.
2024-01-16 13:10:48 +01:00
outer_object_name (str): Outer object name for the GBNF grammar. If None, no outer object will be generated. Eg. "function" for function calling.
outer_object_content (str): Content for the outer rule in the GBNF grammar. Eg. "function_parameters" or "params" for function calling.
list_of_outputs (str, optional): Allows a list of output objects
2024-01-12 20:46:45 +01:00
Returns:
2024-01-16 13:10:48 +01:00
str: The generated GBNF grammar string.
2024-01-12 20:46:45 +01:00
Examples:
models = [UserModel, PostModel]
grammar = generate_gbnf_grammar_from_pydantic(models)
print(grammar)
# Output:
# root ::= UserModel | PostModel
# ...
"""
2024-01-25 14:51:24 -05:00
processed_models : set [ type [ BaseModel ]] = set ()
2024-01-12 20:46:45 +01:00
all_rules = []
2024-01-25 14:51:24 -05:00
created_rules : dict [ str , list [ str ]] = {}
2024-01-12 20:46:45 +01:00
if outer_object_name is None :
for model in models :
2024-01-16 13:10:48 +01:00
model_rules , _ = generate_gbnf_grammar ( model , processed_models , created_rules )
2024-01-12 20:46:45 +01:00
all_rules . extend ( model_rules )
if list_of_outputs :
2024-01-16 13:10:48 +01:00
root_rule = r 'root ::= (" "| "\n") "[" ws grammar-models ("," ws grammar-models)* ws "]"' + " \n "
2024-01-12 20:46:45 +01:00
else :
2024-01-16 13:10:48 +01:00
root_rule = r 'root ::= (" "| "\n") grammar-models' + " \n "
2024-01-12 20:46:45 +01:00
root_rule += "grammar-models ::= " + " | " . join (
[ format_model_and_field_name ( model . __name__ ) for model in models ])
all_rules . insert ( 0 , root_rule )
return " \n " . join ( all_rules )
elif outer_object_name is not None :
if list_of_outputs :
2024-01-16 13:10:48 +01:00
root_rule = (
rf 'root ::= (" "| "\n") "[" ws { format_model_and_field_name ( outer_object_name ) } ("," ws { format_model_and_field_name ( outer_object_name ) } )* ws "]"'
+ " \n "
)
2024-01-12 20:46:45 +01:00
else :
root_rule = f "root ::= { format_model_and_field_name ( outer_object_name ) } \n "
2024-01-16 13:10:48 +01:00
model_rule = (
rf ' { format_model_and_field_name ( outer_object_name ) } ::= (" "| "\n") " {{ " ws "\" { outer_object_name } \"" ":" ws grammar-models'
)
2024-01-12 20:46:45 +01:00
fields_joined = " | " . join (
2024-01-16 13:10:48 +01:00
[ rf " { format_model_and_field_name ( model . __name__ ) } -grammar-model" for model in models ])
2024-01-12 20:46:45 +01:00
2024-01-16 13:10:48 +01:00
grammar_model_rules = f " \n grammar-models ::= { fields_joined } "
2024-01-12 20:46:45 +01:00
mod_rules = []
for model in models :
2024-01-16 13:10:48 +01:00
mod_rule = rf " { format_model_and_field_name ( model . __name__ ) } -grammar-model ::= "
mod_rule += (
rf '"\" { model . __name__ } \"" "," ws "\" { outer_object_content } \"" ":" ws { format_model_and_field_name ( model . __name__ ) } ' + " \n "
)
2024-01-12 20:46:45 +01:00
mod_rules . append ( mod_rule )
grammar_model_rules += " \n " + " \n " . join ( mod_rules )
2024-01-16 13:10:48 +01:00
2024-01-12 20:46:45 +01:00
for model in models :
2024-01-16 13:10:48 +01:00
model_rules , has_special_string = generate_gbnf_grammar ( model , processed_models ,
created_rules )
if not has_special_string :
model_rules [ 0 ] += r '"\n" ws "}"'
2024-01-12 20:46:45 +01:00
all_rules . extend ( model_rules )
all_rules . insert ( 0 , root_rule + model_rule + grammar_model_rules )
return " \n " . join ( all_rules )
def get_primitive_grammar ( grammar ):
"""
Returns the needed GBNF primitive grammar for a given GBNF grammar string.
Args:
2024-01-16 13:10:48 +01:00
grammar (str): The string containing the GBNF grammar.
2024-01-12 20:46:45 +01:00
Returns:
2024-01-16 13:10:48 +01:00
str: GBNF primitive grammar string.
2024-01-12 20:46:45 +01:00
"""
2024-01-25 14:51:24 -05:00
type_list : list [ type [ object ]] = []
2024-01-12 20:46:45 +01:00
if "string-list" in grammar :
type_list . append ( str )
if "boolean-list" in grammar :
type_list . append ( bool )
if "integer-list" in grammar :
type_list . append ( int )
if "float-list" in grammar :
type_list . append ( float )
additional_grammar = [ generate_list_rule ( t ) for t in type_list ]
primitive_grammar = r """
boolean ::= "true" | "false"
null ::= "null"
string ::= "\"" (
[^"\\] |
"\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F])
)* "\"" ws
ws ::= ([ \t\n] ws)?
float ::= ("-"? ([0-9] | [1-9] [0-9]*)) ("." [0-9]+)? ([eE] [-+]? [0-9]+)? ws
integer ::= [0-9]+"""
any_block = ""
if "custom-class-any" in grammar :
2024-01-16 13:10:48 +01:00
any_block = """
2024-01-12 20:46:45 +01:00
value ::= object | array | string | number | boolean | null
object ::=
"{" ws (
string ":" ws value
("," ws string ":" ws value)*
)? "}" ws
array ::=
"[" ws (
value
("," ws value)*
)? "]" ws
2024-01-16 13:10:48 +01:00
number ::= integer | float"""
2024-01-12 20:46:45 +01:00
markdown_code_block_grammar = ""
if "markdown-code-block" in grammar :
markdown_code_block_grammar = r '''
markdown-code-block ::= opening-triple-ticks markdown-code-block-content closing-triple-ticks
markdown-code-block-content ::= ( [^`] | "`" [^`] | "`" "`" [^`] )*
opening-triple-ticks ::= "```" "python" "\n" | "```" "c" "\n" | "```" "cpp" "\n" | "```" "txt" "\n" | "```" "text" "\n" | "```" "json" "\n" | "```" "javascript" "\n" | "```" "css" "\n" | "```" "html" "\n" | "```" "markdown" "\n"
closing-triple-ticks ::= "```" "\n"'''
if "triple-quoted-string" in grammar :
markdown_code_block_grammar = r """
triple-quoted-string ::= triple-quotes triple-quoted-string-content triple-quotes
triple-quoted-string-content ::= ( [^'] | "'" [^'] | "'" "'" [^'] )*
triple-quotes ::= "'''" """
2024-01-16 13:10:48 +01:00
return " \n " + " \n " . join ( additional_grammar ) + any_block + primitive_grammar + markdown_code_block_grammar
2024-01-12 20:46:45 +01:00
2024-01-16 13:10:48 +01:00
def generate_markdown_documentation (
2024-01-25 14:51:24 -05:00
pydantic_models : list [ type [ BaseModel ]], model_prefix = "Model" , fields_prefix = "Fields" ,
2024-01-16 13:10:48 +01:00
documentation_with_field_description = True
) -> str :
2024-01-12 20:46:45 +01:00
"""
2024-01-16 13:10:48 +01:00
Generate markdown documentation for a list of Pydantic models.
2024-01-12 20:46:45 +01:00
Args:
2024-01-25 14:51:24 -05:00
pydantic_models (list[type[BaseModel]]): list of Pydantic model classes.
2024-01-16 13:10:48 +01:00
model_prefix (str): Prefix for the model section.
fields_prefix (str): Prefix for the fields section.
documentation_with_field_description (bool): Include field descriptions in the documentation.
2024-01-12 20:46:45 +01:00
Returns:
2024-01-16 13:10:48 +01:00
str: Generated text documentation.
2024-01-12 20:46:45 +01:00
"""
documentation = ""
pyd_models = [( model , True ) for model in pydantic_models ]
for model , add_prefix in pyd_models :
if add_prefix :
2024-01-16 13:10:48 +01:00
documentation += f " { model_prefix } : { model . __name__ } \n "
2024-01-12 20:46:45 +01:00
else :
2024-01-16 13:10:48 +01:00
documentation += f "Model: { model . __name__ } \n "
2024-01-12 20:46:45 +01:00
# Handling multi-line model description with proper indentation
class_doc = getdoc ( model )
base_class_doc = getdoc ( BaseModel )
class_description = class_doc if class_doc and class_doc != base_class_doc else ""
if class_description != "" :
documentation += " Description: "
2024-01-16 13:10:48 +01:00
documentation += format_multiline_description ( class_description , 0 ) + " \n "
2024-01-12 20:46:45 +01:00
if add_prefix :
# Indenting the fields section
documentation += f " { fields_prefix } : \n "
else :
documentation += f " Fields: \n "
if isclass ( model ) and issubclass ( model , BaseModel ):
for name , field_type in model . __annotations__ . items ():
# if name == "markdown_code_block":
# continue
if get_origin ( field_type ) == list :
element_type = get_args ( field_type )[ 0 ]
if isclass ( element_type ) and issubclass ( element_type , BaseModel ):
pyd_models . append (( element_type , False ))
if get_origin ( field_type ) == Union :
element_types = get_args ( field_type )
for element_type in element_types :
if isclass ( element_type ) and issubclass ( element_type , BaseModel ):
pyd_models . append (( element_type , False ))
2024-01-16 13:10:48 +01:00
documentation += generate_field_markdown (
name , field_type , model , documentation_with_field_description = documentation_with_field_description
)
2024-01-12 20:46:45 +01:00
documentation += " \n "
2024-01-16 13:10:48 +01:00
if hasattr ( model , "Config" ) and hasattr ( model . Config ,
"json_schema_extra" ) and "example" in model . Config . json_schema_extra :
2024-01-12 20:46:45 +01:00
documentation += f " Expected Example Output for { format_model_and_field_name ( model . __name__ ) } : \n "
2024-01-16 13:10:48 +01:00
json_example = json . dumps ( model . Config . json_schema_extra [ "example" ])
2024-01-12 20:46:45 +01:00
documentation += format_multiline_description ( json_example , 2 ) + " \n "
return documentation
2024-01-16 13:10:48 +01:00
def generate_field_markdown (
2024-01-25 14:51:24 -05:00
field_name : str , field_type : type [ Any ], model : type [ BaseModel ], depth = 1 ,
2024-01-16 13:10:48 +01:00
documentation_with_field_description = True
) -> str :
"""
Generate markdown documentation for a Pydantic model field.
Args:
field_name (str): Name of the field.
2024-01-25 14:51:24 -05:00
field_type (type[Any]): Type of the field.
model (type[BaseModel]): Pydantic model class.
2024-01-16 13:10:48 +01:00
depth (int): Indentation depth in the documentation.
documentation_with_field_description (bool): Include field descriptions in the documentation.
Returns:
str: Generated text documentation for the field.
"""
indent = " " * depth
field_info = model . model_fields . get ( field_name )
field_description = field_info . description if field_info and field_info . description else ""
if get_origin ( field_type ) == list :
element_type = get_args ( field_type )[ 0 ]
field_text = f " { indent }{ field_name } ( { format_model_and_field_name ( field_type . __name__ ) } of { format_model_and_field_name ( element_type . __name__ ) } )"
if field_description != "" :
field_text += ": \n "
else :
field_text += " \n "
elif get_origin ( field_type ) == Union :
element_types = get_args ( field_type )
types = []
for element_type in element_types :
types . append ( format_model_and_field_name ( element_type . __name__ ))
field_text = f " { indent }{ field_name } ( { ' or ' . join ( types ) } )"
if field_description != "" :
field_text += ": \n "
else :
field_text += " \n "
else :
field_text = f " { indent }{ field_name } ( { format_model_and_field_name ( field_type . __name__ ) } )"
if field_description != "" :
field_text += ": \n "
else :
field_text += " \n "
if not documentation_with_field_description :
return field_text
if field_description != "" :
field_text += f " Description: " + field_description + " \n "
# Check for and include field-specific examples if available
if hasattr ( model , "Config" ) and hasattr ( model . Config ,
"json_schema_extra" ) and "example" in model . Config . json_schema_extra :
field_example = model . Config . json_schema_extra [ "example" ] . get ( field_name )
if field_example is not None :
example_text = f "' { field_example } '" if isinstance ( field_example , str ) else field_example
field_text += f " { indent } Example: { example_text } \n "
if isclass ( field_type ) and issubclass ( field_type , BaseModel ):
field_text += f " { indent } Details: \n "
for name , type_ in field_type . __annotations__ . items ():
field_text += generate_field_markdown ( name , type_ , field_type , depth + 2 )
return field_text
2024-01-25 14:51:24 -05:00
def format_json_example ( example : dict [ str , Any ], depth : int ) -> str :
2024-01-16 13:10:48 +01:00
"""
Format a JSON example into a readable string with indentation.
Args:
example (dict): JSON example to be formatted.
depth (int): Indentation depth.
Returns:
str: Formatted JSON example string.
"""
indent = " " * depth
formatted_example = "{ \n "
for key , value in example . items ():
value_text = f "' { value } '" if isinstance ( value , str ) else value
formatted_example += f " { indent }{ key } : { value_text } , \n "
formatted_example = formatted_example . rstrip ( ", \n " ) + " \n " + indent + "}"
return formatted_example
def generate_text_documentation (
2024-01-25 14:51:24 -05:00
pydantic_models : list [ type [ BaseModel ]], model_prefix = "Model" , fields_prefix = "Fields" ,
2024-01-16 13:10:48 +01:00
documentation_with_field_description = True
) -> str :
"""
Generate text documentation for a list of Pydantic models.
Args:
2024-01-25 14:51:24 -05:00
pydantic_models (list[type[BaseModel]]): List of Pydantic model classes.
2024-01-16 13:10:48 +01:00
model_prefix (str): Prefix for the model section.
fields_prefix (str): Prefix for the fields section.
documentation_with_field_description (bool): Include field descriptions in the documentation.
Returns:
str: Generated text documentation.
"""
documentation = ""
pyd_models = [( model , True ) for model in pydantic_models ]
for model , add_prefix in pyd_models :
if add_prefix :
documentation += f " { model_prefix } : { model . __name__ } \n "
else :
documentation += f "Model: { model . __name__ } \n "
# Handling multi-line model description with proper indentation
class_doc = getdoc ( model )
base_class_doc = getdoc ( BaseModel )
class_description = class_doc if class_doc and class_doc != base_class_doc else ""
if class_description != "" :
documentation += " Description: "
documentation += " \n " + format_multiline_description ( class_description , 2 ) + " \n "
if isclass ( model ) and issubclass ( model , BaseModel ):
documentation_fields = ""
for name , field_type in model . __annotations__ . items ():
# if name == "markdown_code_block":
# continue
if get_origin ( field_type ) == list :
element_type = get_args ( field_type )[ 0 ]
if isclass ( element_type ) and issubclass ( element_type , BaseModel ):
pyd_models . append (( element_type , False ))
if get_origin ( field_type ) == Union :
element_types = get_args ( field_type )
for element_type in element_types :
if isclass ( element_type ) and issubclass ( element_type , BaseModel ):
pyd_models . append (( element_type , False ))
documentation_fields += generate_field_text (
name , field_type , model , documentation_with_field_description = documentation_with_field_description
)
if documentation_fields != "" :
if add_prefix :
documentation += f " { fields_prefix } : \n { documentation_fields } "
else :
documentation += f " Fields: \n { documentation_fields } "
documentation += " \n "
if hasattr ( model , "Config" ) and hasattr ( model . Config ,
"json_schema_extra" ) and "example" in model . Config . json_schema_extra :
documentation += f " Expected Example Output for { format_model_and_field_name ( model . __name__ ) } : \n "
json_example = json . dumps ( model . Config . json_schema_extra [ "example" ])
documentation += format_multiline_description ( json_example , 2 ) + " \n "
return documentation
def generate_field_text (
2024-01-25 14:51:24 -05:00
field_name : str , field_type : type [ Any ], model : type [ BaseModel ], depth = 1 ,
2024-01-16 13:10:48 +01:00
documentation_with_field_description = True
) -> str :
2024-01-12 20:46:45 +01:00
"""
Generate text documentation for a Pydantic model field.
Args:
2024-01-16 13:10:48 +01:00
field_name (str): Name of the field.
2024-01-25 14:51:24 -05:00
field_type (type[Any]): Type of the field.
model (type[BaseModel]): Pydantic model class.
2024-01-16 13:10:48 +01:00
depth (int): Indentation depth in the documentation.
documentation_with_field_description (bool): Include field descriptions in the documentation.
2024-01-12 20:46:45 +01:00
Returns:
2024-01-16 13:10:48 +01:00
str: Generated text documentation for the field.
2024-01-12 20:46:45 +01:00
"""
2024-01-16 13:10:48 +01:00
indent = " " * depth
2024-01-12 20:46:45 +01:00
field_info = model . model_fields . get ( field_name )
field_description = field_info . description if field_info and field_info . description else ""
if get_origin ( field_type ) == list :
element_type = get_args ( field_type )[ 0 ]
field_text = f " { indent }{ field_name } ( { format_model_and_field_name ( field_type . __name__ ) } of { format_model_and_field_name ( element_type . __name__ ) } )"
if field_description != "" :
field_text += ": \n "
else :
field_text += " \n "
elif get_origin ( field_type ) == Union :
element_types = get_args ( field_type )
types = []
for element_type in element_types :
types . append ( format_model_and_field_name ( element_type . __name__ ))
field_text = f " { indent }{ field_name } ( { ' or ' . join ( types ) } )"
if field_description != "" :
field_text += ": \n "
else :
field_text += " \n "
else :
field_text = f " { indent }{ field_name } ( { format_model_and_field_name ( field_type . __name__ ) } )"
if field_description != "" :
field_text += ": \n "
else :
field_text += " \n "
if not documentation_with_field_description :
return field_text
if field_description != "" :
field_text += f " { indent } Description: " + field_description + " \n "
# Check for and include field-specific examples if available
2024-01-16 13:10:48 +01:00
if hasattr ( model , "Config" ) and hasattr ( model . Config ,
"json_schema_extra" ) and "example" in model . Config . json_schema_extra :
field_example = model . Config . json_schema_extra [ "example" ] . get ( field_name )
2024-01-12 20:46:45 +01:00
if field_example is not None :
example_text = f "' { field_example } '" if isinstance ( field_example , str ) else field_example
field_text += f " { indent } Example: { example_text } \n "
if isclass ( field_type ) and issubclass ( field_type , BaseModel ):
field_text += f " { indent } Details: \n "
for name , type_ in field_type . __annotations__ . items ():
field_text += generate_field_text ( name , type_ , field_type , depth + 2 )
return field_text
def format_multiline_description ( description : str , indent_level : int ) -> str :
"""
Format a multiline description with proper indentation.
Args:
2024-01-16 13:10:48 +01:00
description (str): Multiline description.
indent_level (int): Indentation level.
2024-01-12 20:46:45 +01:00
Returns:
2024-01-16 13:10:48 +01:00
str: Formatted multiline description.
2024-01-12 20:46:45 +01:00
"""
2024-01-16 13:10:48 +01:00
indent = " " * indent_level
return indent + description . replace ( " \n " , " \n " + indent )
2024-01-12 20:46:45 +01:00
2024-01-16 13:10:48 +01:00
def save_gbnf_grammar_and_documentation (
grammar , documentation , grammar_file_path = "./grammar.gbnf" , documentation_file_path = "./grammar_documentation.md"
):
2024-01-12 20:46:45 +01:00
"""
Save GBNF grammar and documentation to specified files.
Args:
2024-01-16 13:10:48 +01:00
grammar (str): GBNF grammar string.
documentation (str): Documentation string.
grammar_file_path (str): File path to save the GBNF grammar.
documentation_file_path (str): File path to save the documentation.
2024-01-12 20:46:45 +01:00
Returns:
2024-01-16 13:10:48 +01:00
None
2024-01-12 20:46:45 +01:00
"""
try :
2024-01-16 13:10:48 +01:00
with open ( grammar_file_path , "w" ) as file :
2024-01-12 20:46:45 +01:00
file . write ( grammar + get_primitive_grammar ( grammar ))
print ( f "Grammar successfully saved to { grammar_file_path } " )
except IOError as e :
print ( f "An error occurred while saving the grammar file: { e } " )
try :
2024-01-16 13:10:48 +01:00
with open ( documentation_file_path , "w" ) as file :
2024-01-12 20:46:45 +01:00
file . write ( documentation )
print ( f "Documentation successfully saved to { documentation_file_path } " )
except IOError as e :
print ( f "An error occurred while saving the documentation file: { e } " )
def remove_empty_lines ( string ):
"""
Remove empty lines from a string.
Args:
2024-01-16 13:10:48 +01:00
string (str): Input string.
2024-01-12 20:46:45 +01:00
Returns:
2024-01-16 13:10:48 +01:00
str: String with empty lines removed.
2024-01-12 20:46:45 +01:00
"""
lines = string . splitlines ()
non_empty_lines = [ line for line in lines if line . strip () != "" ]
string_no_empty_lines = " \n " . join ( non_empty_lines )
return string_no_empty_lines
2024-01-16 13:10:48 +01:00
def generate_and_save_gbnf_grammar_and_documentation (
pydantic_model_list ,
grammar_file_path = "./generated_grammar.gbnf" ,
documentation_file_path = "./generated_grammar_documentation.md" ,
2024-01-25 14:51:24 -05:00
outer_object_name : str | None = None ,
outer_object_content : str | None = None ,
2024-01-16 13:10:48 +01:00
model_prefix : str = "Output Model" ,
fields_prefix : str = "Output Fields" ,
list_of_outputs : bool = False ,
documentation_with_field_description = True ,
):
2024-01-12 20:46:45 +01:00
"""
Generate GBNF grammar and documentation, and save them to specified files.
Args:
2024-01-16 13:10:48 +01:00
pydantic_model_list: List of Pydantic model classes.
grammar_file_path (str): File path to save the generated GBNF grammar.
documentation_file_path (str): File path to save the generated documentation.
outer_object_name (str): Outer object name for the GBNF grammar. If None, no outer object will be generated. Eg. "function" for function calling.
outer_object_content (str): Content for the outer rule in the GBNF grammar. Eg. "function_parameters" or "params" for function calling.
model_prefix (str): Prefix for the model section in the documentation.
fields_prefix (str): Prefix for the fields section in the documentation.
list_of_outputs (bool): Whether the output is a list of items.
documentation_with_field_description (bool): Include field descriptions in the documentation.
2024-01-12 20:46:45 +01:00
Returns:
2024-01-16 13:10:48 +01:00
None
2024-01-12 20:46:45 +01:00
"""
2024-01-16 13:10:48 +01:00
documentation = generate_markdown_documentation (
pydantic_model_list , model_prefix , fields_prefix ,
documentation_with_field_description = documentation_with_field_description
)
grammar = generate_gbnf_grammar_from_pydantic_models ( pydantic_model_list , outer_object_name , outer_object_content ,
list_of_outputs )
2024-01-12 20:46:45 +01:00
grammar = remove_empty_lines ( grammar )
save_gbnf_grammar_and_documentation ( grammar , documentation , grammar_file_path , documentation_file_path )
2024-01-16 13:10:48 +01:00
def generate_gbnf_grammar_and_documentation (
pydantic_model_list ,
2024-01-25 14:51:24 -05:00
outer_object_name : str | None = None ,
outer_object_content : str | None = None ,
2024-01-16 13:10:48 +01:00
model_prefix : str = "Output Model" ,
fields_prefix : str = "Output Fields" ,
list_of_outputs : bool = False ,
documentation_with_field_description = True ,
):
2024-01-12 20:46:45 +01:00
"""
Generate GBNF grammar and documentation for a list of Pydantic models.
Args:
2024-01-16 13:10:48 +01:00
pydantic_model_list: List of Pydantic model classes.
outer_object_name (str): Outer object name for the GBNF grammar. If None, no outer object will be generated. Eg. "function" for function calling.
outer_object_content (str): Content for the outer rule in the GBNF grammar. Eg. "function_parameters" or "params" for function calling.
model_prefix (str): Prefix for the model section in the documentation.
fields_prefix (str): Prefix for the fields section in the documentation.
list_of_outputs (bool): Whether the output is a list of items.
documentation_with_field_description (bool): Include field descriptions in the documentation.
2024-01-12 20:46:45 +01:00
Returns:
2024-01-16 13:10:48 +01:00
tuple: GBNF grammar string, documentation string.
2024-01-12 20:46:45 +01:00
"""
2024-01-16 13:10:48 +01:00
documentation = generate_markdown_documentation (
copy ( pydantic_model_list ), model_prefix , fields_prefix ,
documentation_with_field_description = documentation_with_field_description
)
grammar = generate_gbnf_grammar_from_pydantic_models ( pydantic_model_list , outer_object_name , outer_object_content ,
list_of_outputs )
2024-01-12 20:46:45 +01:00
grammar = remove_empty_lines ( grammar + get_primitive_grammar ( grammar ))
return grammar , documentation
2024-01-16 13:10:48 +01:00
def generate_gbnf_grammar_and_documentation_from_dictionaries (
2024-01-25 14:51:24 -05:00
dictionaries : list [ dict [ str , Any ]],
outer_object_name : str | None = None ,
outer_object_content : str | None = None ,
2024-01-16 13:10:48 +01:00
model_prefix : str = "Output Model" ,
fields_prefix : str = "Output Fields" ,
list_of_outputs : bool = False ,
documentation_with_field_description = True ,
):
2024-01-12 20:46:45 +01:00
"""
Generate GBNF grammar and documentation from a list of dictionaries.
Args:
2024-01-25 14:51:24 -05:00
dictionaries (list[dict]): List of dictionaries representing Pydantic models.
2024-01-16 13:10:48 +01:00
outer_object_name (str): Outer object name for the GBNF grammar. If None, no outer object will be generated. Eg. "function" for function calling.
outer_object_content (str): Content for the outer rule in the GBNF grammar. Eg. "function_parameters" or "params" for function calling.
model_prefix (str): Prefix for the model section in the documentation.
fields_prefix (str): Prefix for the fields section in the documentation.
list_of_outputs (bool): Whether the output is a list of items.
documentation_with_field_description (bool): Include field descriptions in the documentation.
2024-01-12 20:46:45 +01:00
Returns:
2024-01-16 13:10:48 +01:00
tuple: GBNF grammar string, documentation string.
2024-01-12 20:46:45 +01:00
"""
pydantic_model_list = create_dynamic_models_from_dictionaries ( dictionaries )
2024-01-16 13:10:48 +01:00
documentation = generate_markdown_documentation (
copy ( pydantic_model_list ), model_prefix , fields_prefix ,
documentation_with_field_description = documentation_with_field_description
)
grammar = generate_gbnf_grammar_from_pydantic_models ( pydantic_model_list , outer_object_name , outer_object_content ,
list_of_outputs )
2024-01-12 20:46:45 +01:00
grammar = remove_empty_lines ( grammar + get_primitive_grammar ( grammar ))
return grammar , documentation
2024-01-25 14:51:24 -05:00
def create_dynamic_model_from_function ( func : Callable [ ... , Any ]):
2024-01-12 20:46:45 +01:00
"""
Creates a dynamic Pydantic model from a given function's type hints and adds the function as a 'run' method.
Args:
2024-01-16 13:10:48 +01:00
func (Callable): A function with type hints from which to create the model.
2024-01-12 20:46:45 +01:00
Returns:
2024-01-16 13:10:48 +01:00
A dynamic Pydantic model class with the provided function as a 'run' method.
2024-01-12 20:46:45 +01:00
"""
2024-01-16 13:10:48 +01:00
# Get the signature of the function
sig = inspect . signature ( func )
# Parse the docstring
2024-01-25 14:51:24 -05:00
assert func . __doc__ is not None
2024-01-16 13:10:48 +01:00
docstring = parse ( func . __doc__ )
2024-01-12 20:46:45 +01:00
dynamic_fields = {}
2024-01-16 13:10:48 +01:00
param_docs = []
for param in sig . parameters . values ():
# Exclude 'self' parameter
if param . name == "self" :
continue
2024-01-12 20:46:45 +01:00
2024-01-16 13:10:48 +01:00
# Assert that the parameter has a type annotation
if param . annotation == inspect . Parameter . empty :
raise TypeError ( f "Parameter ' { param . name } ' in function ' { func . __name__ } ' lacks a type annotation" )
# Find the parameter's description in the docstring
param_doc = next (( d for d in docstring . params if d . arg_name == param . name ), None )
# Assert that the parameter has a description
if not param_doc or not param_doc . description :
raise ValueError (
f "Parameter ' { param . name } ' in function ' { func . __name__ } ' lacks a description in the docstring" )
# Add parameter details to the schema
param_docs . append (( param . name , param_doc ))
if param . default == inspect . Parameter . empty :
default_value = ...
2024-01-12 20:46:45 +01:00
else :
2024-01-16 13:10:48 +01:00
default_value = param . default
dynamic_fields [ param . name ] = (
param . annotation if param . annotation != inspect . Parameter . empty else str , default_value )
2024-01-12 20:46:45 +01:00
# Creating the dynamic model
2024-01-25 14:51:24 -05:00
dynamic_model = create_model ( f " { func . __name__ } " , ** dynamic_fields ) # type: ignore[call-overload]
2024-01-12 20:46:45 +01:00
2024-01-25 14:51:24 -05:00
for name , param_doc in param_docs :
dynamic_model . model_fields [ name ] . description = param_doc . description
2024-01-16 13:10:48 +01:00
dynamic_model . __doc__ = docstring . short_description
2024-01-12 20:46:45 +01:00
def run_method_wrapper ( self ):
2024-01-16 13:10:48 +01:00
func_args = { name : getattr ( self , name ) for name , _ in dynamic_fields . items ()}
2024-01-12 20:46:45 +01:00
return func ( ** func_args )
# Adding the wrapped function as a 'run' method
2024-01-16 13:10:48 +01:00
setattr ( dynamic_model , "run" , run_method_wrapper )
return dynamic_model
2024-01-12 20:46:45 +01:00
2024-01-25 14:51:24 -05:00
def add_run_method_to_dynamic_model ( model : type [ BaseModel ], func : Callable [ ... , Any ]):
2024-01-12 20:46:45 +01:00
"""
Add a 'run' method to a dynamic Pydantic model, using the provided function.
Args:
2024-01-25 14:51:24 -05:00
model (type[BaseModel]): Dynamic Pydantic model class.
2024-01-16 13:10:48 +01:00
func (Callable): Function to be added as a 'run' method to the model.
2024-01-12 20:46:45 +01:00
Returns:
2024-01-25 14:51:24 -05:00
type[BaseModel]: Pydantic model class with the added 'run' method.
2024-01-12 20:46:45 +01:00
"""
def run_method_wrapper ( self ):
func_args = { name : getattr ( self , name ) for name in model . model_fields }
return func ( ** func_args )
# Adding the wrapped function as a 'run' method
2024-01-16 13:10:48 +01:00
setattr ( model , "run" , run_method_wrapper )
2024-01-12 20:46:45 +01:00
return model
2024-01-25 14:51:24 -05:00
def create_dynamic_models_from_dictionaries ( dictionaries : list [ dict [ str , Any ]]):
2024-01-12 20:46:45 +01:00
"""
Create a list of dynamic Pydantic model classes from a list of dictionaries.
Args:
2024-01-25 14:51:24 -05:00
dictionaries (list[dict]): List of dictionaries representing model structures.
2024-01-12 20:46:45 +01:00
Returns:
2024-01-25 14:51:24 -05:00
list[type[BaseModel]]: List of generated dynamic Pydantic model classes.
2024-01-12 20:46:45 +01:00
"""
dynamic_models = []
for func in dictionaries :
model_name = format_model_and_field_name ( func . get ( "name" , "" ))
2024-01-16 13:10:48 +01:00
dyn_model = convert_dictionary_to_pydantic_model ( func , model_name )
2024-01-12 20:46:45 +01:00
dynamic_models . append ( dyn_model )
return dynamic_models
def map_grammar_names_to_pydantic_model_class ( pydantic_model_list ):
output = {}
for model in pydantic_model_list :
output [ format_model_and_field_name ( model . __name__ )] = model
return output
from enum import Enum
def json_schema_to_python_types ( schema ):
type_map = {
2024-01-16 13:10:48 +01:00
"any" : Any ,
"string" : str ,
"number" : float ,
"integer" : int ,
"boolean" : bool ,
"array" : list ,
2024-01-12 20:46:45 +01:00
}
return type_map [ schema ]
def list_to_enum ( enum_name , values ):
return Enum ( enum_name , { value : value for value in values })
2024-01-25 14:51:24 -05:00
def convert_dictionary_to_pydantic_model ( dictionary : dict [ str , Any ], model_name : str = "CustomModel" ) -> type [ Any ]:
2024-01-12 20:46:45 +01:00
"""
Convert a dictionary to a Pydantic model class.
Args:
2024-01-16 13:10:48 +01:00
dictionary (dict): Dictionary representing the model structure.
model_name (str): Name of the generated Pydantic model.
2024-01-12 20:46:45 +01:00
Returns:
2024-01-25 14:51:24 -05:00
type[BaseModel]: Generated Pydantic model class.
2024-01-12 20:46:45 +01:00
"""
2024-01-25 14:51:24 -05:00
fields : dict [ str , Any ] = {}
2024-01-12 20:46:45 +01:00
if "properties" in dictionary :
for field_name , field_data in dictionary . get ( "properties" , {}) . items ():
2024-01-16 13:10:48 +01:00
if field_data == "object" :
submodel = convert_dictionary_to_pydantic_model ( dictionary , f " { model_name } _ { field_name } " )
2024-01-12 20:46:45 +01:00
fields [ field_name ] = ( submodel , ... )
else :
2024-01-16 13:10:48 +01:00
field_type = field_data . get ( "type" , "str" )
2024-01-12 20:46:45 +01:00
if field_data . get ( "enum" , []):
fields [ field_name ] = ( list_to_enum ( field_name , field_data . get ( "enum" , [])), ... )
2024-01-16 13:10:48 +01:00
elif field_type == "array" :
2024-01-12 20:46:45 +01:00
items = field_data . get ( "items" , {})
if items != {}:
array = { "properties" : items }
2024-01-16 13:10:48 +01:00
array_type = convert_dictionary_to_pydantic_model ( array , f " { model_name } _ { field_name } _items" )
2024-01-25 14:51:24 -05:00
fields [ field_name ] = ( List [ array_type ], ... ) # type: ignore[valid-type]
2024-01-12 20:46:45 +01:00
else :
fields [ field_name ] = ( list , ... )
2024-01-16 13:10:48 +01:00
elif field_type == "object" :
submodel = convert_dictionary_to_pydantic_model ( field_data , f " { model_name } _ { field_name } " )
2024-01-12 20:46:45 +01:00
fields [ field_name ] = ( submodel , ... )
2024-01-16 13:10:48 +01:00
elif field_type == "required" :
required = field_data . get ( "enum" , [])
for key , field in fields . items ():
if key not in required :
fields [ key ] = ( Optional [ fields [ key ][ 0 ]], ... )
2024-01-12 20:46:45 +01:00
else :
field_type = json_schema_to_python_types ( field_type )
fields [ field_name ] = ( field_type , ... )
if "function" in dictionary :
for field_name , field_data in dictionary . get ( "function" , {}) . items ():
if field_name == "name" :
model_name = field_data
elif field_name == "description" :
fields [ "__doc__" ] = field_data
elif field_name == "parameters" :
2024-01-16 13:10:48 +01:00
return convert_dictionary_to_pydantic_model ( field_data , f " { model_name } " )
2024-01-12 20:46:45 +01:00
if "parameters" in dictionary :
field_data = { "function" : dictionary }
2024-01-16 13:10:48 +01:00
return convert_dictionary_to_pydantic_model ( field_data , f " { model_name } " )
if "required" in dictionary :
required = dictionary . get ( "required" , [])
for key , field in fields . items ():
if key not in required :
fields [ key ] = ( Optional [ fields [ key ][ 0 ]], ... )
2024-01-12 20:46:45 +01:00
custom_model = create_model ( model_name , ** fields )
return custom_model