CoolFace
Apppublic

chwellofficial/nt360Slides

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
schema_utils.py443 linesDownload Raw Back to utils
1from copy import deepcopy2from typing import Any, List3 4from openai import NOT_GIVEN5 6from utils.dict_utils import (7    get_dict_paths_with_key,8    get_dict_at_path,9    has_more_than_n_keys,10)11 12supported_string_formats = [13    "date-time",14    "time",15    "date",16    "duration",17    "email",18    "hostname",19    "ipv4",20    "ipv6",21    "uuid",22]23 24 25def remove_fields_from_schema(schema: dict, fields_to_remove: List[str]):26    schema = deepcopy(schema)27    properties_paths = get_dict_paths_with_key(schema, "properties")28    for path in properties_paths:29        parent_obj = get_dict_at_path(schema, path)30        if "properties" in parent_obj and isinstance(parent_obj["properties"], dict):31            for field in fields_to_remove:32                if field in parent_obj["properties"]:33                    del parent_obj["properties"][field]34 35    required_paths = get_dict_paths_with_key(schema, "required")36    for path in required_paths:37        parent_obj = get_dict_at_path(schema, path)38        if "required" in parent_obj and isinstance(parent_obj["required"], list):39            parent_obj["required"] = [40                field41                for field in parent_obj["required"]42                if field not in fields_to_remove43            ]44 45    return schema46 47 48def add_field_in_schema(schema: dict, field: dict, required: bool = False) -> dict:49 50    if not isinstance(field, dict) or len(field) != 1:51        raise ValueError(52            "`field` must be a dict with exactly one entry: {name: schema_dict}"53        )54 55    field_name, field_schema = next(iter(field.items()))56    if not isinstance(field_name, str):57        raise TypeError("Field name must be a string")58    if not isinstance(field_schema, dict):59        raise TypeError("Field schema must be a dictionary")60 61    updated_schema: dict = deepcopy(schema)62 63    root_properties = updated_schema.get("properties")64    if not isinstance(root_properties, dict):65        updated_schema["properties"] = {}66        root_properties = updated_schema["properties"]67 68    root_properties[field_name] = field_schema69 70    # Update root-level required based on the flag71    existing_required = updated_schema.get("required")72    if not isinstance(existing_required, list):73        existing_required = []74 75    if required:76        if field_name not in existing_required:77            existing_required.append(field_name)78    else:79        if field_name in existing_required:80            existing_required = [name for name in existing_required if name != field_name]81 82    if existing_required:83        updated_schema["required"] = existing_required84    else:85        updated_schema.pop("required", None)86 87    return updated_schema88 89 90# From OpenAI91def ensure_strict_json_schema(92    json_schema: object,93    *,94    path: tuple[str, ...],95    root: dict[str, object],96) -> dict[str, Any]:97    """Mutates the given JSON schema to ensure it conforms to the `strict` standard98    that the API expects.99    """100    if not isinstance(json_schema, dict):101        raise TypeError(f"Expected {json_schema} to be a dictionary; path={path}")102 103    defs = json_schema.get("$defs")104    if isinstance(defs, dict):105        for def_name, def_schema in defs.items():106            ensure_strict_json_schema(107                def_schema, path=(*path, "$defs", def_name), root=root108            )109 110    definitions = json_schema.get("definitions")111    if isinstance(definitions, dict):112        for definition_name, definition_schema in definitions.items():113            ensure_strict_json_schema(114                definition_schema,115                path=(*path, "definitions", definition_name),116                root=root,117            )118 119    typ = json_schema.get("type")120    if typ == "object" and "additionalProperties" not in json_schema:121        json_schema["additionalProperties"] = False122 123    # object types124    # { 'type': 'object', 'properties': { 'a':  {...} } }125    properties = json_schema.get("properties")126    if isinstance(properties, dict):127        json_schema["required"] = [prop for prop in properties.keys()]128        json_schema["properties"] = {129            key: ensure_strict_json_schema(130                prop_schema, path=(*path, "properties", key), root=root131            )132            for key, prop_schema in properties.items()133        }134 135    # arrays136    # { 'type': 'array', 'items': {...} }137    # OpenAI requires array schemas to have "items". Zod tuples may emit prefixItems only.138    items = json_schema.get("items")139    if isinstance(items, dict):140        json_schema["items"] = ensure_strict_json_schema(141            items, path=(*path, "items"), root=root142        )143    elif typ == "array":144        prefix_items = json_schema.get("prefixItems")145        if (146            isinstance(prefix_items, list)147            and len(prefix_items) > 0148            and isinstance(prefix_items[0], dict)149        ):150            json_schema["items"] = ensure_strict_json_schema(151                prefix_items[0], path=(*path, "items"), root=root152            )153            json_schema.pop("prefixItems", None)154        else:155            json_schema["items"] = {"type": "string"}156 157    # unions158    any_of = json_schema.get("anyOf")159    if isinstance(any_of, list):160        json_schema["anyOf"] = [161            ensure_strict_json_schema(variant, path=(*path, "anyOf", str(i)), root=root)162            for i, variant in enumerate(any_of)163        ]164 165    # intersections166    all_of = json_schema.get("allOf")167    if isinstance(all_of, list):168        if len(all_of) == 1:169            json_schema.update(170                ensure_strict_json_schema(171                    all_of[0], path=(*path, "allOf", "0"), root=root172                )173            )174            json_schema.pop("allOf")175        else:176            json_schema["allOf"] = [177                ensure_strict_json_schema(178                    entry, path=(*path, "allOf", str(i)), root=root179                )180                for i, entry in enumerate(all_of)181            ]182 183    # string184    if typ == "string":185        if "format" in json_schema:186            if json_schema["format"] not in supported_string_formats:187                del json_schema["format"]188 189    # strip `None` defaults as there's no meaningful distinction here190    # the schema will still be `nullable` and the model will default191    # to using `None` anyway192    if json_schema.get("default", NOT_GIVEN) is None:193        json_schema.pop("default")194 195    # we can't use `$ref`s if there are also other properties defined, e.g.196    # `{"$ref": "...", "description": "my description"}`197    #198    # so we unravel the ref199    # `{"type": "string", "description": "my description"}`200    ref = json_schema.get("$ref")201    if ref and has_more_than_n_keys(json_schema, 1):202        assert isinstance(ref, str), f"Received non-string $ref - {ref}"203 204        resolved = resolve_ref(root=root, ref=ref)205        if not isinstance(resolved, dict):206            raise ValueError(207                f"Expected `$ref: {ref}` to resolved to a dictionary but got {resolved}"208            )209 210        # properties from the json schema take priority over the ones on the `$ref`211        json_schema.update({**resolved, **json_schema})212        json_schema.pop("$ref")213        # Since the schema expanded from `$ref` might not have `additionalProperties: false` applied,214        # we call `_ensure_strict_json_schema` again to fix the inlined schema and ensure it's valid.215        return ensure_strict_json_schema(json_schema, path=path, root=root)216 217    return json_schema218 219 220def resolve_ref(*, root: dict[str, object], ref: str) -> object:221    if not ref.startswith("#/"):222        raise ValueError(f"Unexpected $ref format {ref!r}; Does not start with #/")223 224    path = ref[2:].split("/")225    resolved = root226    for key in path:227        value = resolved[key]228        assert isinstance(229            value, dict230        ), f"encountered non-dictionary entry while resolving {ref} - {resolved}"231        resolved = value232 233    return resolved234 235 236# Flattens a JSON schema by inlining all $ref references and removing $defs/definitions237def flatten_json_schema(schema: dict) -> dict:238    root_schema = deepcopy(schema)239 240    def _flatten(node: Any) -> Any:241        if isinstance(node, dict):242            # If node is a pure $ref (or combined with extra fields), inline it243            if "$ref" in node:244                ref_value = node["$ref"]245                assert isinstance(246                    ref_value, str247                ), f"Received non-string $ref - {ref_value}"248                resolved = resolve_ref(root=root_schema, ref=ref_value)249                assert isinstance(250                    resolved, dict251                ), f"Expected `$ref: {ref_value}` to resolve to a dictionary but got {type(resolved)}"252                # Merge: referenced first, then overlay current (excluding $ref)253                merged: dict[str, Any] = deepcopy(resolved)254                for key, value in node.items():255                    if key == "$ref":256                        continue257                    merged[key] = value258                return _flatten(merged)259 260            flattened: dict[str, Any] = {}261            for key, value in node.items():262                # Drop defs/definitions in output263                if key in ("$defs", "definitions"):264                    continue265                if key == "properties" and isinstance(value, dict):266                    flattened[key] = {267                        prop_key: _flatten(prop_val)268                        for prop_key, prop_val in value.items()269                    }270                elif key in ("items", "contains", "additionalProperties", "not"):271                    if isinstance(value, dict):272                        flattened[key] = _flatten(value)273                    elif isinstance(value, list):274                        flattened[key] = [_flatten(v) for v in value]275                    else:276                        flattened[key] = value277                elif key in ("allOf", "anyOf", "oneOf", "prefixItems") and isinstance(278                    value, list279                ):280                    flattened[key] = [_flatten(v) for v in value]281                else:282                    flattened[key] = (283                        _flatten(value) if isinstance(value, (dict, list)) else value284                    )285            return flattened286        if isinstance(node, list):287            return [_flatten(v) for v in node]288        return node289 290    result = _flatten(schema)291    # Ensure top-level cleanup just in case292    if isinstance(result, dict):293        result.pop("$defs", None)294        result.pop("definitions", None)295    return result296 297 298def ensure_array_schemas_have_items(schema: dict) -> dict[str, Any]:299    """300    Recursively ensure every JSON schema node with type="array" has an "items" key.301    Codex Responses API requires array schemas to specify items. Mutates a deep copy.302    """303    result = deepcopy(schema)304 305    def _is_array_schema_type(type_value: Any) -> bool:306        if type_value == "array":307            return True308        if isinstance(type_value, list):309            return "array" in type_value310        return False311 312    def _ensure(node: Any) -> Any:313        if isinstance(node, dict):314            if _is_array_schema_type(node.get("type")) and "items" not in node:315                node["items"] = {"type": "string"}316            for key, value in list(node.items()):317                node[key] = _ensure(value)318        elif isinstance(node, list):319            for idx, value in enumerate(node):320                node[idx] = _ensure(value)321        return node322 323    return _ensure(result)324 325 326def remove_titles_from_schema(schema: dict) -> dict[str, Any]:327 328    def _strip_titles(node: Any) -> Any:329        if isinstance(node, dict):330            rebuilt: dict[str, Any] = {}331            for key, value in node.items():332                # Preserve properties named "title" under the JSON Schema "properties" mapping333                if key == "properties" and isinstance(value, dict):334                    rebuilt[key] = {335                        prop_name: _strip_titles(prop_schema)336                        for prop_name, prop_schema in value.items()337                    }338                    continue339 340                # Remove schema metadata field "title" elsewhere341                if key == "title":342                    continue343 344                rebuilt[key] = _strip_titles(value)345            return rebuilt346        if isinstance(node, list):347            return [_strip_titles(item) for item in node]348        return node349 350    return _strip_titles(deepcopy(schema))351 352 353# ? Not used354def generate_constraint_sentences(schema: dict) -> str:355    """356    Generate human-readable constraint sentences from a JSON schema.357 358    Args:359        schema: JSON schema dictionary360 361    Returns:362        String containing constraint sentences separated by newlines363    """364    constraints = []365 366    def extract_constraints_recursive(obj, prefix=""):367        if isinstance(obj, dict):368            if "properties" in obj:369                properties = obj["properties"]370                for prop_name, prop_def in properties.items():371                    current_path = f"{prefix}.{prop_name}" if prefix else prop_name372 373                    if isinstance(prop_def, dict):374                        prop_type = prop_def.get("type")375 376                        # Handle string constraints377                        if prop_type == "string":378                            min_length = prop_def.get("minLength")379                            max_length = prop_def.get("maxLength")380 381                            if min_length is not None and max_length is not None:382                                constraints.append(383                                    f"    - {current_path} should be less than {max_length} characters and greater than {min_length} characters"384                                )385                            elif max_length is not None:386                                constraints.append(387                                    f"    - {current_path} should be less than {max_length} characters"388                                )389                            elif min_length is not None:390                                constraints.append(391                                    f"    - {current_path} should be greater than {min_length} characters"392                                )393 394                        # Handle array constraints395                        elif prop_type == "array":396                            min_items = prop_def.get("minItems")397                            max_items = prop_def.get("maxItems")398 399                            if min_items is not None and max_items is not None:400                                constraints.append(401                                    f"    - {current_path} should have more than {min_items} items and less than {max_items} items"402                                )403                            elif max_items is not None:404                                constraints.append(405                                    f"    - {current_path} should have less than {max_items} items"406                                )407                            elif min_items is not None:408                                constraints.append(409                                    f"    - {current_path} should have more than {min_items} items"410                                )411 412                        # Recurse into nested objects413                        if prop_type == "object" or "properties" in prop_def:414                            extract_constraints_recursive(prop_def, current_path)415 416                        # Handle array items if they have properties417                        if prop_type == "array" and "items" in prop_def:418                            items_def = prop_def["items"]419                            if isinstance(items_def, dict) and (420                                "properties" in items_def421                                or items_def.get("type") == "object"422                            ):423                                extract_constraints_recursive(424                                    items_def, f"{current_path}[*]"425                                )426 427            # Also recurse into other nested structures428            for key, value in obj.items():429                if key not in [430                    "properties",431                    "type",432                    "minLength",433                    "maxLength",434                    "minItems",435                    "maxItems",436                ] and isinstance(value, dict):437                    extract_constraints_recursive(value, prefix)438 439    # Start extraction from the root schema440    extract_constraints_recursive(schema)441 442    return "\n".join(constraints)443