gordonchan's picture
Upload 41 files
ca56e6a verified
from typing import Any, Dict, List, Optional
from openai.types.chat.completion_create_params import Function
from pydantic import BaseModel
from api.utils.compat import model_dump
def convert_data_type(param_type: str) -> str:
""" convert data_type to typescript data type """
return "number" if param_type in {"integer", "float"} else param_type
def get_param_type(param: Dict[str, Any]) -> str:
""" get param_type of parameter """
param_type = "any"
if "type" in param:
raw_param_type = param["type"]
param_type = (
" | ".join(raw_param_type)
if type(raw_param_type) is list
else raw_param_type
)
elif "oneOf" in param:
one_of_types = [
convert_data_type(item["type"])
for item in param["oneOf"]
if "type" in item
]
one_of_types = list(set(one_of_types))
param_type = " | ".join(one_of_types)
return convert_data_type(param_type)
def get_format_param(param: Dict[str, Any]) -> Optional[str]:
""" Get "format" from param. There are cases where format is not directly in param but in oneOf """
if "format" in param:
return param["format"]
if "oneOf" in param:
formats = [item["format"] for item in param["oneOf"] if "format" in item]
if formats:
return " or ".join(formats)
return None
def get_param_info(param: Dict[str, Any]) -> Optional[str]:
""" get additional information about parameter such as: format, default value, min, max, ... """
param_type = param.get("type", "any")
info_list = []
if "description" in param:
desc = param["description"]
if not desc.endswith("."):
desc += "."
info_list.append(desc)
if "default" in param:
default_value = param["default"]
if param_type == "string":
default_value = f'"{default_value}"' # if string --> add ""
info_list.append(f"Default={default_value}.")
format_param = get_format_param(param)
if format_param is not None:
info_list.append(f"Format={format_param}")
info_list.extend(
f"{field_name}={str(param[field])}"
for field, field_name in [
("maximum", "Maximum"),
("minimum", "Minimum"),
("maxLength", "Maximum length"),
("minLength", "Minimum length"),
]
if field in param
)
if info_list:
result = "// " + " ".join(info_list)
return result.replace("\n", " ")
return None
def append_new_param_info(info_list: List[str], param_declaration: str, comment_info: Optional[str], depth: int):
""" Append a new parameter with comment to the info_list """
offset = "".join([" " for _ in range(depth)]) if depth >= 1 else ""
if comment_info is not None:
# if depth == 0: # format: //comment\nparam: type
info_list.append(f"{offset}{comment_info}")
info_list.append(f"{offset}{param_declaration}")
def get_enum_option_str(enum_options: List) -> str:
"""get enum option separated by: "|"
Args:
enum_options (List): list of options
Returns:
_type_: concatenation of options separated by "|"
"""
# if each option is string --> add quote
return " | ".join([f'"{v}"' if type(v) is str else str(v) for v in enum_options])
def get_array_typescript(param_name: Optional[str], param_dic: dict, depth: int = 0) -> str:
"""recursive implementation for generating type script of array
Args:
param_name (Optional[str]): name of param, optional
param_dic (dict): param_dic
depth (int, optional): nested level. Defaults to 0.
Returns:
_type_: typescript of array
"""
offset = "".join([" " for _ in range(depth)]) if depth >= 1 else ""
items_info = param_dic.get("items", {})
if len(items_info) == 0:
return f"{offset}{param_name}: []" if param_name is not None else "[]"
array_type = get_param_type(items_info)
if array_type == "object":
info_lines = []
child_lines = get_parameter_typescript(
items_info.get("properties", {}), items_info.get("required", []), depth + 1
)
# if comment_info is not None:
# info_lines.append(f"{offset}{comment_info}")
if param_name is not None:
info_lines.append(f"{offset}{param_name}" + ": {")
else:
info_lines.append(f"{offset}" + "{")
info_lines.extend(child_lines)
info_lines.append(f"{offset}" + "}[]")
return "\n".join(info_lines)
elif array_type == "array":
item_info = get_array_typescript(None, items_info, depth + 1)
if param_name is None:
return f"{item_info}[]"
return f"{offset}{param_name}: {item_info.strip()}[]"
else:
if "enum" not in items_info:
return (
f"{array_type}[]"
if param_name is None
else f"{offset}{param_name}: {array_type}[],"
)
item_type = get_enum_option_str(items_info["enum"])
if param_name is None:
return f"({item_type})[]"
else:
return f"{offset}{param_name}: ({item_type})[]"
def get_parameter_typescript(properties, required_params, depth=0) -> List[str]:
"""Recursion, returning the information about parameters including data type, description and other information
These kinds of information will be put into the prompt
Args:
properties (_type_): properties in parameters
required_params (_type_): List of required parameters
depth (int, optional): the depth of params (nested level). Defaults to 0.
Returns:
_type_: list of lines containing information about all parameters
"""
tp_lines = []
for param_name, param in properties.items():
# Sometimes properties have "required" field as a list of string.
# Even though it is supposed to be not under properties. So we skip it
if not isinstance(param, dict):
continue
# Param Description
comment_info = get_param_info(param)
# Param Name declaration
param_declaration = f"{param_name}"
if isinstance(required_params, list) and param_name not in required_params:
param_declaration += "?"
param_type = get_param_type(param)
offset = ""
if depth >= 1:
offset = "".join([" " for _ in range(depth)])
if param_type == "object": # param_type is object
child_lines = get_parameter_typescript(param.get("properties", {}), param.get("required", []), depth + 1)
if comment_info is not None:
tp_lines.append(f"{offset}{comment_info}")
param_declaration += ": {"
tp_lines.append(f"{offset}{param_declaration}")
tp_lines.extend(child_lines)
tp_lines.append(f"{offset}" + "},")
elif param_type == "array": # param_type is an array
item_info = param.get("items", {})
if "type" not in item_info: # don't know type of array
param_declaration += ": [],"
append_new_param_info(tp_lines, param_declaration, comment_info, depth)
else:
array_declaration = get_array_typescript(param_declaration, param, depth)
if not array_declaration.endswith(","):
array_declaration += ","
if comment_info is not None:
tp_lines.append(f"{offset}{comment_info}")
tp_lines.append(array_declaration)
else:
if "enum" in param:
param_type = " | ".join([f'"{v}"' for v in param["enum"]])
param_declaration += f": {param_type},"
append_new_param_info(tp_lines, param_declaration, comment_info, depth)
return tp_lines
def generate_schema_from_functions(functions: List[Function], namespace="functions") -> str:
"""
Convert functions schema to a schema that language models can understand.
"""
schema = "// Supported function definitions that should be called when necessary.\n"
schema += f"namespace {namespace} {{\n\n"
for function in functions:
# Convert a Function object to dict, if necessary
if isinstance(function, BaseModel):
function = model_dump(function)
function_name = function.get("name", None)
if function_name is None:
continue
description = function.get("description", "")
schema += f"// {description}\n"
schema += f"type {function_name}"
parameters = function.get("parameters", None)
if parameters is not None and parameters.get("properties") is not None:
schema += " = (_: {\n"
required_params = parameters.get("required", [])
tp_lines = get_parameter_typescript(parameters.get("properties"), required_params, 0)
schema += "\n".join(tp_lines)
schema += "\n}) => any;\n\n"
else:
# Doesn't have any parameters
schema += " = () => any;\n\n"
schema += f"}} // namespace {namespace}"
return schema
def generate_schema_from_openapi(specification: Dict[str, Any], description: str, namespace: str) -> str:
"""
Convert OpenAPI specification object to a schema that language models can understand.
Input:
specification: can be obtained by json. loads of any OpanAPI json spec, or yaml.safe_load for yaml OpenAPI specs
Example output:
// General Description
namespace functions {
// Simple GET endpoint
type getEndpoint = (_: {
// This is a string parameter
param_string: string,
param_integer: number,
param_boolean?: boolean,
param_enum: "value1" | "value2" | "value3",
}) => any;
} // namespace functions
"""
description_clean = description.replace("\n", "")
schema = f"// {description_clean}\n"
schema += f"namespace {namespace} {{\n\n"
for path_name, paths in specification.get("paths", {}).items():
for method_name, method_info in paths.items():
operationId = method_info.get("operationId", None)
if operationId is None:
continue
description = method_info.get("description", method_info.get("summary", ""))
schema += f"// {description}\n"
schema += f"type {operationId}"
if ("requestBody" in method_info) or (method_info.get("parameters") is not None):
schema += f" = (_: {{\n"
# Body
if "requestBody" in method_info:
try:
body_schema = (
method_info.get("requestBody", {})
.get("content", {})
.get("application/json", {})
.get("schema", {})
)
except AttributeError:
body_schema = {}
for param_name, param in body_schema.get("properties", {}).items():
# Param Description
description = param.get("description")
if description is not None:
schema += f"// {description}\n"
# Param Name
schema += f"{param_name}"
if (
(not param.get("required", False))
or (param.get("nullable", False))
or (param_name in body_schema.get("required", []))
):
schema += "?"
# Param Type
param_type = param.get("type", "any")
if param_type == "integer":
param_type = "number"
if "enum" in param:
param_type = " | ".join([f'"{v}"' for v in param["enum"]])
schema += f": {param_type},\n"
# URL
for param in method_info.get("parameters", []):
# Param Description
if description := param.get("description"):
schema += f"// {description}\n"
# Param Name
schema += f"{param['name']}"
if (not param.get("required", False)) or (param.get("nullable", False)):
schema += "?"
if param.get("schema") is None:
continue
# Param Type
param_type = param["schema"].get("type", "any")
if param_type == "integer":
param_type = "number"
if "enum" in param["schema"]:
param_type = " | ".join([f'"{v}"' for v in param["schema"]["enum"]])
schema += f": {param_type},\n"
schema += f"}}) => any;\n\n"
else:
# Doesn't have any parameters
schema += f" = () => any;\n\n"
schema += f"}} // namespace {namespace}"
return schema
if __name__ == "__main__":
functions = [
{
"name": "get_current_weather",
"description": "Get the current weather in a given location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA",
},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["location"],
},
}
]
print(generate_schema_from_functions(functions))