Team Ai
Apppublic

KBaba7/llama.cpp

sourceHugging Faceapache-2.0updated 2y agoView on Hugging Face
0likes
json_schema_to_grammar.py812 linesDownload Raw Back to examples
1#!/usr/bin/env python32from __future__ import annotations3 4import argparse5import itertools6import json7import re8import sys9from typing import Any, List, Optional, Set, Tuple, Union10 11def _build_repetition(item_rule, min_items, max_items, separator_rule=None):12 13    if min_items == 0 and max_items == 1:14        return f'{item_rule}?'15 16    if not separator_rule:17        if min_items == 1 and max_items is None:18            return f'{item_rule}+'19        elif min_items == 0 and max_items is None:20            return f'{item_rule}*'21        else:22            return f'{item_rule}{{{min_items},{max_items if max_items is not None else ""}}}'23 24    result = item_rule + ' ' + _build_repetition(f'({separator_rule} {item_rule})', min_items - 1 if min_items > 0 else 0, max_items - 1 if max_items is not None else None)25    return f'({result})?' if min_items == 0 else result26 27def _generate_min_max_int(min_value: Optional[int], max_value: Optional[int], out: list, decimals_left: int = 16, top_level: bool = True):28    has_min = min_value != None29    has_max = max_value != None30 31    def digit_range(from_char: str, to_char: str):32        out.append("[")33        if from_char == to_char:34            out.append(from_char)35        else:36            out.append(from_char)37            out.append("-")38            out.append(to_char)39        out.append("]")40 41    def more_digits(min_digits: int, max_digits: int):42        out.append("[0-9]")43        if min_digits == max_digits and min_digits == 1:44            return45        out.append("{")46        out.append(str(min_digits))47        if max_digits != min_digits:48            out.append(",")49            if max_digits != sys.maxsize:50                out.append(str(max_digits))51        out.append("}")52 53    def uniform_range(from_str: str, to_str: str):54        i = 055        while i < len(from_str) and from_str[i] == to_str[i]:56            i += 157        if i > 0:58            out.append("\"")59            out.append(from_str[:i])60            out.append("\"")61        if i < len(from_str):62            if i > 0:63                out.append(" ")64            sub_len = len(from_str) - i - 165            if sub_len > 0:66                from_sub = from_str[i+1:]67                to_sub = to_str[i+1:]68                sub_zeros = "0" * sub_len69                sub_nines = "9" * sub_len70 71                to_reached = False72                out.append("(")73                if from_sub == sub_zeros:74                    digit_range(from_str[i], chr(ord(to_str[i]) - 1))75                    out.append(" ")76                    more_digits(sub_len, sub_len)77                else:78                    out.append("[")79                    out.append(from_str[i])80                    out.append("] ")81                    out.append("(")82                    uniform_range(from_sub, sub_nines)83                    out.append(")")84                    if ord(from_str[i]) < ord(to_str[i]) - 1:85                        out.append(" | ")86                        if to_sub == sub_nines:87                            digit_range(chr(ord(from_str[i]) + 1), to_str[i])88                            to_reached = True89                        else:90                            digit_range(chr(ord(from_str[i]) + 1), chr(ord(to_str[i]) - 1))91                        out.append(" ")92                        more_digits(sub_len, sub_len)93                if not to_reached:94                    out.append(" | ")95                    digit_range(to_str[i], to_str[i])96                    out.append(" ")97                    uniform_range(sub_zeros, to_sub)98                out.append(")")99            else:100                out.append("[")101                out.append(from_str[i])102                out.append("-")103                out.append(to_str[i])104                out.append("]")105 106    if has_min and has_max:107        if min_value < 0 and max_value < 0:108            out.append("\"-\" (")109            _generate_min_max_int(-max_value, -min_value, out, decimals_left, top_level=True)110            out.append(")")111            return112 113        if min_value < 0:114            out.append("\"-\" (")115            _generate_min_max_int(0, -min_value, out, decimals_left, top_level=True)116            out.append(") | ")117            min_value = 0118 119        min_s = str(min_value)120        max_s = str(max_value)121        min_digits = len(min_s)122        max_digits = len(max_s)123 124        for digits in range(min_digits, max_digits):125            uniform_range(min_s, "9" * digits)126            min_s = "1" + "0" * digits127            out.append(" | ")128        uniform_range(min_s, max_s)129        return130 131    less_decimals = max(decimals_left - 1, 1)132 133    if has_min:134        if min_value < 0:135            out.append("\"-\" (")136            _generate_min_max_int(None, -min_value, out, decimals_left, top_level=False)137            out.append(") | [0] | [1-9] ")138            more_digits(0, decimals_left - 1)139        elif min_value == 0:140            if top_level:141                out.append("[0] | [1-9] ")142                more_digits(0, less_decimals)143            else:144                more_digits(1, decimals_left)145        elif min_value <= 9:146            c = str(min_value)147            range_start = '1' if top_level else '0'148            if c > range_start:149                digit_range(range_start, chr(ord(c) - 1))150                out.append(" ")151                more_digits(1, less_decimals)152                out.append(" | ")153            digit_range(c, "9")154            out.append(" ")155            more_digits(0, less_decimals)156        else:157            min_s = str(min_value)158            length = len(min_s)159            c = min_s[0]160 161            if c > "1":162                digit_range("1" if top_level else "0", chr(ord(c) - 1))163                out.append(" ")164                more_digits(length, less_decimals)165                out.append(" | ")166            digit_range(c, c)167            out.append(" (")168            _generate_min_max_int(int(min_s[1:]), None, out, less_decimals, top_level=False)169            out.append(")")170            if c < "9":171                out.append(" | ")172                digit_range(chr(ord(c) + 1), "9")173                out.append(" ")174                more_digits(length - 1, less_decimals)175        return176 177    if has_max:178        if max_value >= 0:179            if top_level:180                out.append("\"-\" [1-9] ")181                more_digits(0, less_decimals)182                out.append(" | ")183            _generate_min_max_int(0, max_value, out, decimals_left, top_level=True)184        else:185            out.append("\"-\" (")186            _generate_min_max_int(-max_value, None, out, decimals_left, top_level=False)187            out.append(")")188        return189 190    raise RuntimeError("At least one of min_value or max_value must be set")191 192class BuiltinRule:193    def __init__(self, content: str, deps: list | None = None):194        self.content = content195        self.deps = deps or []196 197# Constraining spaces to prevent model "running away".198SPACE_RULE = '| " " | "\\n" [ \\t]{0,20}'199 200PRIMITIVE_RULES = {201    'boolean'      : BuiltinRule('("true" | "false") space', []),202    'decimal-part' : BuiltinRule('[0-9]{1,16}', []),203    'integral-part': BuiltinRule('[0] | [1-9] [0-9]{0,15}', []),204    'number'       : BuiltinRule('("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space', ['integral-part', 'decimal-part']),205    'integer'      : BuiltinRule('("-"? integral-part) space', ['integral-part']),206    'value'        : BuiltinRule('object | array | string | number | boolean | null', ['object', 'array', 'string', 'number', 'boolean', 'null']),207    'object'       : BuiltinRule('"{" space ( string ":" space value ("," space string ":" space value)* )? "}" space', ['string', 'value']),208    'array'        : BuiltinRule('"[" space ( value ("," space value)* )? "]" space', ['value']),209    'uuid'         : BuiltinRule(r'"\"" [0-9a-fA-F]{8} "-" [0-9a-fA-F]{4} "-" [0-9a-fA-F]{4} "-" [0-9a-fA-F]{4} "-" [0-9a-fA-F]{12} "\"" space', []),210    'char'         : BuiltinRule(r'[^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})', []),211    'string'       : BuiltinRule(r'"\"" char* "\"" space', ['char']),212    'null'         : BuiltinRule('"null" space', []),213}214 215# TODO: support "uri", "email" string formats216STRING_FORMAT_RULES = {217    'date'            : BuiltinRule('[0-9]{4} "-" ( "0" [1-9] | "1" [0-2] ) "-" ( \"0\" [1-9] | [1-2] [0-9] | "3" [0-1] )', []),218    'time'            : BuiltinRule('([01] [0-9] | "2" [0-3]) ":" [0-5] [0-9] ":" [0-5] [0-9] ( "." [0-9]{3} )? ( "Z" | ( "+" | "-" ) ( [01] [0-9] | "2" [0-3] ) ":" [0-5] [0-9] )', []),219    'date-time'       : BuiltinRule('date "T" time', ['date', 'time']),220    'date-string'     : BuiltinRule('"\\"" date "\\"" space', ['date']),221    'time-string'     : BuiltinRule('"\\"" time "\\"" space', ['time']),222    'date-time-string': BuiltinRule('"\\"" date-time "\\"" space', ['date-time']),223}224 225DOTALL = '[\\U00000000-\\U0010FFFF]'226DOT = '[^\\x0A\\x0D]'227 228RESERVED_NAMES = set(["root", "dot", *PRIMITIVE_RULES.keys(), *STRING_FORMAT_RULES.keys()])229 230INVALID_RULE_CHARS_RE = re.compile(r'[^a-zA-Z0-9-]+')231GRAMMAR_LITERAL_ESCAPE_RE = re.compile(r'[\r\n"]')232GRAMMAR_RANGE_LITERAL_ESCAPE_RE = re.compile(r'[\r\n"\]\-\\]')233GRAMMAR_LITERAL_ESCAPES = {'\r': '\\r', '\n': '\\n', '"': '\\"', '-': '\\-', ']': '\\]'}234 235NON_LITERAL_SET = set('|.()[]{}*+?')236ESCAPED_IN_REGEXPS_BUT_NOT_IN_LITERALS = set('^$.[]()|{}*+?')237 238 239class SchemaConverter:240    def __init__(self, *, prop_order, allow_fetch, dotall, raw_pattern):241        self._prop_order = prop_order242        self._allow_fetch = allow_fetch243        self._dotall = dotall244        self._raw_pattern = raw_pattern245        self._rules = {246            'space': SPACE_RULE,247        }248        self._refs = {}249        self._refs_being_resolved = set()250 251    def _format_literal(self, literal):252        escaped = GRAMMAR_LITERAL_ESCAPE_RE.sub(253            lambda m: GRAMMAR_LITERAL_ESCAPES.get(m.group(0)) or m.group(0), literal254        )255        return f'"{escaped}"'256 257    def not_literal(self, literal: str, dotall: bool = True, maybe_escaped_underscores = False) -> str:258        '''259            not_literal('a') -> '[^a]'260            not_literal('abc') -> '([^a] | "a" ([^b] | "b" ([^c])?)?)?'261        '''262        assert len(literal) > 0, 'Empty literal not supported'263        def recurse(i: int):264            c = literal[i]265            if maybe_escaped_underscores and c == '_':266                yield f'[^{c}\\\\]'267                yield ' | '268                yield f'"\\\\"? "{c}"'269            else:270                yield f'[^{c}]'271            if i < len(literal) - 1:272                yield ' | '273                yield self._format_literal(c)274                yield ' ('275                yield from recurse(i + 1)276                yield ')?'277 278        return ''.join(('(', *recurse(0), ')'))279 280    def _not_strings(self, strings):281        class TrieNode:282            def __init__(self):283                self.children = {}284                self.is_end_of_string = False285 286            def insert(self, string):287                node = self288                for c in string:289                    node = node.children.setdefault(c, TrieNode())290                node.is_end_of_string = True291 292        trie = TrieNode()293        for s in strings:294            trie.insert(s)295 296        char_rule = self._add_primitive('char', PRIMITIVE_RULES['char'])297        out = ['["] ( ']298 299        def visit(node):300            rejects = []301            first = True302            for c in sorted(node.children.keys()):303                child = node.children[c]304                rejects.append(c)305                if first:306                    first = False307                else:308                    out.append(' | ')309                out.append(f'[{c}]')310                if child.children:311                    out.append(f' (')312                    visit(child)313                    out.append(')')314                elif child.is_end_of_string:315                    out.append(f' {char_rule}+')316            if node.children:317                if not first:318                    out.append(' | ')319                out.append(f'[^"{"".join(rejects)}] {char_rule}*')320        visit(trie)321 322        out.append(f' ){"" if trie.is_end_of_string else "?"} ["] space')323        return ''.join(out)324 325    def _add_rule(self, name, rule):326        esc_name = INVALID_RULE_CHARS_RE.sub('-', name)327        if esc_name not in self._rules or self._rules[esc_name] == rule:328            key = esc_name329        else:330            i = 0331            while f'{esc_name}{i}' in self._rules and self._rules[f'{esc_name}{i}'] != rule:332                i += 1333            key = f'{esc_name}{i}'334        self._rules[key] = rule335        return key336 337    def resolve_refs(self, schema: dict, url: str):338        '''339            Resolves all $ref fields in the given schema, fetching any remote schemas,340            replacing $ref with absolute reference URL and populating self._refs with the341            respective referenced (sub)schema dictionaries.342        '''343        def visit(n: dict):344            if isinstance(n, list):345                return [visit(x) for x in n]346            elif isinstance(n, dict):347                ref = n.get('$ref')348                if ref is not None and ref not in self._refs:349                    if ref.startswith('https://'):350                        assert self._allow_fetch, 'Fetching remote schemas is not allowed (use --allow-fetch for force)'351                        import requests352 353                        frag_split = ref.split('#')354                        base_url = frag_split[0]355 356                        target = self._refs.get(base_url)357                        if target is None:358                            target = self.resolve_refs(requests.get(ref).json(), base_url)359                            self._refs[base_url] = target360 361                        if len(frag_split) == 1 or frag_split[-1] == '':362                            return target363                    elif ref.startswith('#/'):364                        target = schema365                        ref = f'{url}{ref}'366                        n['$ref'] = ref367                    else:368                        raise ValueError(f'Unsupported ref {ref}')369 370                    for sel in ref.split('#')[-1].split('/')[1:]:371                        assert target is not None and sel in target, f'Error resolving ref {ref}: {sel} not in {target}'372                        target = target[sel]373 374                    self._refs[ref] = target375                else:376                    for v in n.values():377                        visit(v)378 379            return n380        return visit(schema)381 382    def _generate_union_rule(self, name, alt_schemas):383        return ' | '.join((384            self.visit(alt_schema, f'{name}{"-" if name else "alternative-"}{i}')385            for i, alt_schema in enumerate(alt_schemas)386        ))387 388    def _visit_pattern(self, pattern, name):389        '''390            Transforms a regular expression pattern into a GBNF rule.391 392            Input: https://json-schema.org/understanding-json-schema/reference/regular_expressions393            Output: https://github.com/ggerganov/llama.cpp/blob/master/grammars/README.md394 395            Unsupported features: negative/positive lookaheads, greedy/non-greedy modifiers.396 397            Mostly a 1:1 translation, except for {x} / {x,} / {x,y} quantifiers for which398            we define sub-rules to keep the output lean.399        '''400 401        assert pattern.startswith('^') and pattern.endswith('$'), 'Pattern must start with "^" and end with "$"'402        pattern = pattern[1:-1]403        sub_rule_ids = {}404 405        i = 0406        length = len(pattern)407 408        def to_rule(s: tuple[str, bool]) -> str:409            (txt, is_literal) = s410            return "\"" + txt + "\"" if is_literal else txt411 412        def transform() -> tuple[str, bool]:413            '''414                Parse a unit at index i (advancing it), and return its string representation + whether it's a literal.415            '''416            nonlocal i417            nonlocal pattern418            nonlocal sub_rule_ids419 420            start = i421            # For each component of this sequence, store its string representation and whether it's a literal.422            # We only need a flat structure here to apply repetition operators to the last item, and423            # to merge literals at the and (we're parsing grouped ( sequences ) recursively and don't treat '|' specially424            # (GBNF's syntax is luckily very close to regular expressions!)425            seq: list[tuple[str, bool]] = []426 427            def get_dot():428                if self._dotall:429                    rule = DOTALL430                else:431                    # Accept any character... except \n and \r line break chars (\x0A and \xOD)432                    rule = DOT433                return self._add_rule(f'dot', rule)434 435            def join_seq():436                nonlocal seq437                ret = []438                for is_literal, g in itertools.groupby(seq, lambda x: x[1]):439                    if is_literal:440                        ret.append((''.join(x[0] for x in g), True))441                    else:442                        ret.extend(g)443                if len(ret) == 1:444                    return ret[0]445                return (' '.join(to_rule(x) for x in seq), False)446 447            while i < length:448                c = pattern[i]449                if c == '.':450                    seq.append((get_dot(), False))451                    i += 1452                elif c == '(':453                    i += 1454                    if i < length:455                        assert pattern[i] != '?', f'Unsupported pattern syntax "{pattern[i]}" at index {i} of /{pattern}/'456                    seq.append((f'({to_rule(transform())})', False))457                elif c == ')':458                    i += 1459                    assert start > 0 and pattern[start-1] == '(', f'Unbalanced parentheses; start = {start}, i = {i}, pattern = {pattern}'460                    return join_seq()461                elif c == '[':462                    square_brackets = c463                    i += 1464                    while i < length and pattern[i] != ']':465                        if pattern[i] == '\\':466                            square_brackets += pattern[i:i+2]467                            i += 2468                        else:469                            square_brackets += pattern[i]470                            i += 1471                    assert i < length, f'Unbalanced square brackets; start = {start}, i = {i}, pattern = {pattern}'472                    square_brackets += ']'473                    i += 1474                    seq.append((square_brackets, False))475                elif c == '|':476                    seq.append(('|', False))477                    i += 1478                elif c in ('*', '+', '?'):479                    seq[-1] = (to_rule(seq[-1]) + c, False)480                    i += 1481                elif c == '{':482                    curly_brackets = c483                    i += 1484                    while i < length and pattern[i] != '}':485                        curly_brackets += pattern[i]486                        i += 1487                    assert i < length, f'Unbalanced curly brackets; start = {start}, i = {i}, pattern = {pattern}'488                    curly_brackets += '}'489                    i += 1490                    nums = [s.strip() for s in curly_brackets[1:-1].split(',')]491                    min_times = 0492                    max_times = None493                    try:494                        if len(nums) == 1:495                            min_times = int(nums[0])496                            max_times = min_times497                        else:498                            assert len(nums) == 2499                            min_times = int(nums[0]) if nums[0] else 0500                            max_times = int(nums[1]) if nums[1] else None501                    except ValueError:502                        raise ValueError(f'Invalid quantifier {curly_brackets} in /{pattern}/')503 504                    (sub, sub_is_literal) = seq[-1]505 506                    if not sub_is_literal:507                        id = sub_rule_ids.get(sub)508                        if id is None:509                            id = self._add_rule(f'{name}-{len(sub_rule_ids) + 1}', sub)510                            sub_rule_ids[sub] = id511                        sub = id512 513                    seq[-1] = (_build_repetition(f'"{sub}"' if sub_is_literal else sub, min_times, max_times), False)514                else:515                    literal = ''516                    while i < length:517                        if pattern[i] == '\\' and i < length - 1:518                            next = pattern[i + 1]519                            if next in ESCAPED_IN_REGEXPS_BUT_NOT_IN_LITERALS:520                                i += 1521                                literal += pattern[i]522                                i += 1523                            else:524                                literal += pattern[i:i+2]525                                i += 2526                        elif pattern[i] == '"' and not self._raw_pattern:527                            literal += '\\"'528                            i += 1529                        elif pattern[i] not in NON_LITERAL_SET and \530                                (i == length - 1 or literal == '' or pattern[i+1] == '.' or pattern[i+1] not in NON_LITERAL_SET):531                            literal += pattern[i]532                            i += 1533                        else:534                            break535                    if literal:536                        seq.append((literal, True))537 538            return join_seq()539 540        return self._add_rule(541            name,542            to_rule(transform()) if self._raw_pattern \543                else "\"\\\"\" (" + to_rule(transform()) + ") \"\\\"\" space")544 545 546    def _resolve_ref(self, ref):547        ref_name = ref.split('/')[-1]548        if ref_name not in self._rules and ref not in self._refs_being_resolved:549            self._refs_being_resolved.add(ref)550            resolved = self._refs[ref]551            ref_name = self.visit(resolved, ref_name)552            self._refs_being_resolved.remove(ref)553        return ref_name554 555    def _generate_constant_rule(self, value):556        return self._format_literal(json.dumps(value))557 558    def visit(self, schema, name):559        schema_type = schema.get('type')560        schema_format = schema.get('format')561        rule_name = name + '-' if name in RESERVED_NAMES else name or 'root'562 563        if (ref := schema.get('$ref')) is not None:564            return self._add_rule(rule_name, self._resolve_ref(ref))565 566        elif 'oneOf' in schema or 'anyOf' in schema:567            return self._add_rule(rule_name, self._generate_union_rule(name, schema.get('oneOf') or schema['anyOf']))568 569        elif isinstance(schema_type, list):570            return self._add_rule(rule_name, self._generate_union_rule(name, [{**schema, 'type': t} for t in schema_type]))571 572        elif 'const' in schema:573            return self._add_rule(rule_name, self._generate_constant_rule(schema['const']) + ' space')574 575        elif 'enum' in schema:576            rule = '(' + ' | '.join((self._generate_constant_rule(v) for v in schema['enum'])) + ') space'577            return self._add_rule(rule_name, rule)578 579        elif schema_type in (None, 'object') and \580             ('properties' in schema or \581              ('additionalProperties' in schema and schema['additionalProperties'] is not True)):582            required = set(schema.get('required', []))583            properties = list(schema.get('properties', {}).items())584            return self._add_rule(rule_name, self._build_object_rule(properties, required, name, schema.get('additionalProperties')))585 586        elif schema_type in (None, 'object') and 'allOf' in schema:587            required = set()588            properties = []589            hybrid_name = name590            def add_component(comp_schema, is_required):591                if (ref := comp_schema.get('$ref')) is not None:592                    comp_schema = self._refs[ref]593 594                if 'properties' in comp_schema:595                    for prop_name, prop_schema in comp_schema['properties'].items():596                        properties.append((prop_name, prop_schema))597                        if is_required:598                            required.add(prop_name)599 600            for t in schema['allOf']:601                if 'anyOf' in t:602                    for tt in t['anyOf']:603                        add_component(tt, is_required=False)604                else:605                    add_component(t, is_required=True)606 607            return self._add_rule(rule_name, self._build_object_rule(properties, required, hybrid_name, additional_properties=None))608 609        elif schema_type in (None, 'array') and ('items' in schema or 'prefixItems' in schema):610            items = schema.get('items') or schema['prefixItems']611            if isinstance(items, list):612                return self._add_rule(613                    rule_name,614                    '"[" space ' +615                    ' "," space '.join(616                        self.visit(item, f'{name}{"-" if name else ""}tuple-{i}')617                        for i, item in enumerate(items)) +618                    ' "]" space')619            else:620                item_rule_name = self.visit(items, f'{name}{"-" if name else ""}item')621                min_items = schema.get("minItems", 0)622                max_items = schema.get("maxItems")623                return self._add_rule(rule_name, '"[" space ' + _build_repetition(item_rule_name, min_items, max_items, separator_rule='"," space') + ' "]" space')624 625        elif schema_type in (None, 'string') and 'pattern' in schema:626            return self._visit_pattern(schema['pattern'], rule_name)627 628        elif schema_type in (None, 'string') and re.match(r'^uuid[1-5]?$', schema_format or ''):629            return self._add_primitive(630                'root' if rule_name == 'root' else schema_format,631                PRIMITIVE_RULES['uuid']632            )633 634        elif schema_type in (None, 'string') and f'{schema_format}-string' in STRING_FORMAT_RULES:635            prim_name = f'{schema_format}-string'636            return self._add_rule(rule_name, self._add_primitive(prim_name, STRING_FORMAT_RULES[prim_name]))637 638        elif schema_type == 'string' and ('minLength' in schema or 'maxLength' in schema):639            char_rule = self._add_primitive('char', PRIMITIVE_RULES['char'])640            min_len = schema.get('minLength', 0)641            max_len = schema.get('maxLength')642 643            return self._add_rule(rule_name, r'"\"" ' + _build_repetition(char_rule, min_len, max_len) + r' "\"" space')644 645        elif schema_type in (None, 'integer') and \646                ('minimum' in schema or 'exclusiveMinimum' in schema or 'maximum' in schema or 'exclusiveMaximum' in schema):647            min_value = None648            max_value = None649            if 'minimum' in schema:650                min_value = schema['minimum']651            elif 'exclusiveMinimum' in schema:652                min_value = schema['exclusiveMinimum'] + 1653            if 'maximum' in schema:654                max_value = schema['maximum']655            elif 'exclusiveMaximum' in schema:656                max_value = schema['exclusiveMaximum'] - 1657 658            out = ["("]659            _generate_min_max_int(min_value, max_value, out)660            out.append(") space")661            return self._add_rule(rule_name, ''.join(out))662 663        elif (schema_type == 'object') or (len(schema) == 0):664            return self._add_rule(rule_name, self._add_primitive('object', PRIMITIVE_RULES['object']))665 666        else:667            assert schema_type in PRIMITIVE_RULES, f'Unrecognized schema: {schema}'668            # TODO: support minimum, maximum, exclusiveMinimum, exclusiveMaximum at least for zero669            return self._add_primitive('root' if rule_name == 'root' else schema_type, PRIMITIVE_RULES[schema_type])670 671    def _add_primitive(self, name: str, rule: BuiltinRule):672        n = self._add_rule(name, rule.content)673 674        for dep in rule.deps:675            dep_rule = PRIMITIVE_RULES.get(dep) or STRING_FORMAT_RULES.get(dep)676            assert dep_rule, f'Rule {dep} not known'677            if dep not in self._rules:678                self._add_primitive(dep, dep_rule)679        return n680 681    def _build_object_rule(self, properties: List[Tuple[str, Any]], required: Set[str], name: str, additional_properties: Optional[Union[bool, Any]]):682        prop_order = self._prop_order683        # sort by position in prop_order (if specified) then by original order684        sorted_props = [kv[0] for _, kv in sorted(enumerate(properties), key=lambda ikv: (prop_order.get(ikv[1][0], len(prop_order)), ikv[0]))]685 686        prop_kv_rule_names = {}687        for prop_name, prop_schema in properties:688            prop_rule_name = self.visit(prop_schema, f'{name}{"-" if name else ""}{prop_name}')689            prop_kv_rule_names[prop_name] = self._add_rule(690                f'{name}{"-" if name else ""}{prop_name}-kv',691                fr'{self._format_literal(json.dumps(prop_name))} space ":" space {prop_rule_name}'692            )693        required_props = [k for k in sorted_props if k in required]694        optional_props = [k for k in sorted_props if k not in required]695 696        if additional_properties is not None and additional_properties != False:697            sub_name = f'{name}{"-" if name else ""}additional'698            value_rule = self.visit(additional_properties, f'{sub_name}-value') if isinstance(additional_properties, dict) else \699                self._add_primitive('value', PRIMITIVE_RULES['value'])700            key_rule = self._add_primitive('string', PRIMITIVE_RULES['string']) if not sorted_props \701                else self._add_rule(f'{sub_name}-k', self._not_strings(sorted_props))702 703            prop_kv_rule_names["*"] = self._add_rule(704                f'{sub_name}-kv',705                f'{key_rule} ":" space {value_rule}'706            )707            optional_props.append("*")708 709        rule = '"{" space '710        rule += ' "," space '.join(prop_kv_rule_names[k] for k in required_props)711 712        if optional_props:713            rule += ' ('714            if required_props:715                rule += ' "," space ( '716 717            def get_recursive_refs(ks, first_is_optional):718                [k, *rest] = ks719                kv_rule_name = prop_kv_rule_names[k]720                comma_ref = f'( "," space {kv_rule_name} )'721                if first_is_optional:722                    res = comma_ref + ('*' if k == '*' else '?')723                else:724                    res = kv_rule_name + (' ' + comma_ref + "*" if k == '*' else '')725                if len(rest) > 0:726                    res += ' ' + self._add_rule(727                        f'{name}{"-" if name else ""}{k}-rest',728                        get_recursive_refs(rest, first_is_optional=True)729                    )730                return res731 732            rule += ' | '.join(733                get_recursive_refs(optional_props[i:], first_is_optional=False)734                for i in range(len(optional_props))735            )736            if required_props:737                rule += ' )'738            rule += ' )?'739 740        rule += ' "}" space'741 742        return rule743 744    def format_grammar(self):745        return '\n'.join(746            f'{name} ::= {rule}'747            for name, rule in sorted(self._rules.items(), key=lambda kv: kv[0])748        )749 750 751def main(args_in = None):752    parser = argparse.ArgumentParser(753        description='''754            Generates a grammar (suitable for use in ./llama-cli) that produces JSON conforming to a755            given JSON schema. Only a subset of JSON schema features are supported; more may be756            added in the future.757        ''',758    )759    parser.add_argument(760        '--prop-order',761        default=[],762        type=lambda s: s.split(','),763        help='''764            comma-separated property names defining the order of precedence for object properties;765            properties not specified here are given lower precedence than those that are, and766            are kept in their original order from the schema. Required properties are always767            given precedence over optional properties.768        '''769    )770    parser.add_argument(771        '--allow-fetch',772        action='store_true',773        default=False,774        help='Whether to allow fetching referenced schemas over HTTPS')775    parser.add_argument(776        '--dotall',777        action='store_true',778        default=False,779        help='Whether to treat dot (".") as matching all chars including line breaks in regular expression patterns')780    parser.add_argument(781        '--raw-pattern',782        action='store_true',783        default=False,784        help='Treats string patterns as raw patterns w/o quotes (or quote escapes)')785 786    parser.add_argument('schema', help='file containing JSON schema ("-" for stdin)')787    args = parser.parse_args(args_in)788 789    if args.schema.startswith('https://'):790        url = args.schema791        import requests792        schema = requests.get(url).json()793    elif args.schema == '-':794        url = 'stdin'795        schema = json.load(sys.stdin)796    else:797        url = f'file://{args.schema}'798        with open(args.schema) as f:799            schema = json.load(f)800    converter = SchemaConverter(801        prop_order={name: idx for idx, name in enumerate(args.prop_order)},802        allow_fetch=args.allow_fetch,803        dotall=args.dotall,804        raw_pattern=args.raw_pattern)805    schema = converter.resolve_refs(schema, url)806    converter.visit(schema, '')807    print(converter.format_grammar())808 809 810if __name__ == '__main__':811    main()812