-
Notifications
You must be signed in to change notification settings - Fork 13.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
openai[minor]: implement langchain-openai package #15503
Changes from 18 commits
20694ac
48cbc10
57d2970
8c43bf3
134b092
e309378
edb0851
6f5eb50
d4fceed
24e1eac
ad6bce5
e6a8c25
26c395c
1aa1dcb
01bd344
9b1778b
f11d9ec
361d6e8
61edd7e
6c22e81
c70d6c0
7b1204f
26620d8
81b0bb6
25e7262
00efbf4
7f385f2
01ed344
759cf14
fcfc090
3254233
fe7425f
d27bf40
8e81c79
32e207a
0f06c8a
287eec7
970034f
caa03ab
8aaf50e
778fe39
92945d2
c389fbc
4b8ed72
5e49c5b
cee3f6a
0abb3e0
06b2f3c
0827b72
bf766b3
0f7b35f
e4a64fb
4a29380
fa73df4
fefec8f
8c8b0df
b4b6f71
fe97a5c
be6a46e
6277cdc
07fd7e9
5f05ad9
390bb77
fb8ddc7
412d059
1e79455
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,51 +1,15 @@ | ||
from typing import Literal, Optional, Type, TypedDict | ||
|
||
from langchain_core.pydantic_v1 import BaseModel | ||
from langchain_core.utils.json_schema import dereference_refs | ||
|
||
|
||
class FunctionDescription(TypedDict): | ||
"""Representation of a callable function to the OpenAI API.""" | ||
|
||
name: str | ||
"""The name of the function.""" | ||
description: str | ||
"""A description of the function.""" | ||
parameters: dict | ||
"""The parameters of the function.""" | ||
|
||
|
||
class ToolDescription(TypedDict): | ||
"""Representation of a callable function to the OpenAI API.""" | ||
|
||
type: Literal["function"] | ||
function: FunctionDescription | ||
|
||
|
||
def convert_pydantic_to_openai_function( | ||
model: Type[BaseModel], | ||
*, | ||
name: Optional[str] = None, | ||
description: Optional[str] = None, | ||
) -> FunctionDescription: | ||
"""Converts a Pydantic model to a function description for the OpenAI API.""" | ||
schema = dereference_refs(model.schema()) | ||
schema.pop("definitions", None) | ||
return { | ||
"name": name or schema["title"], | ||
"description": description or schema["description"], | ||
"parameters": schema, | ||
} | ||
|
||
|
||
def convert_pydantic_to_openai_tool( | ||
model: Type[BaseModel], | ||
*, | ||
name: Optional[str] = None, | ||
description: Optional[str] = None, | ||
) -> ToolDescription: | ||
"""Converts a Pydantic model to a function description for the OpenAI API.""" | ||
function = convert_pydantic_to_openai_function( | ||
model, name=name, description=description | ||
) | ||
return {"type": "function", "function": function} | ||
# these stubs are just for backwards compatibility | ||
|
||
from langchain_core.utils.function_calling import ( | ||
FunctionDescription, | ||
ToolDescription, | ||
convert_pydantic_to_openai_function, | ||
convert_pydantic_to_openai_tool, | ||
) | ||
|
||
__all__ = [ | ||
"FunctionDescription", | ||
"ToolDescription", | ||
"convert_pydantic_to_openai_function", | ||
"convert_pydantic_to_openai_tool", | ||
] |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. deleted integration tests that were duplicates of unit tests |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved this one from integration tests |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved from
and aliased |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,207 @@ | ||
"""Methods for creating function specs in the style of OpenAI Functions""" | ||
|
||
import inspect | ||
from typing import ( | ||
Any, | ||
Callable, | ||
Dict, | ||
List, | ||
Optional, | ||
Sequence, | ||
Tuple, | ||
Type, | ||
Union, | ||
cast, | ||
) | ||
|
||
from langchain.output_parsers.openai_functions import ( | ||
JsonOutputFunctionsParser, | ||
PydanticAttrOutputFunctionsParser, | ||
PydanticOutputFunctionsParser, | ||
) | ||
from langchain.utils.openai_functions import convert_pydantic_to_openai_function | ||
|
||
from langchain_core.language_models import BaseLanguageModel | ||
from langchain_core.output_parsers import ( | ||
BaseGenerationOutputParser, | ||
BaseLLMOutputParser, | ||
BaseOutputParser, | ||
) | ||
from langchain_core.prompts import BasePromptTemplate | ||
from langchain_core.pydantic_v1 import BaseModel | ||
from langchain_core.runnables import Runnable | ||
|
||
|
||
class FunctionDescription(TypedDict): | ||
"""Representation of a callable function to the OpenAI API.""" | ||
|
||
name: str | ||
"""The name of the function.""" | ||
description: str | ||
"""A description of the function.""" | ||
parameters: dict | ||
"""The parameters of the function.""" | ||
|
||
|
||
class ToolDescription(TypedDict): | ||
"""Representation of a callable function to the OpenAI API.""" | ||
|
||
type: Literal["function"] | ||
function: FunctionDescription | ||
|
||
|
||
def convert_pydantic_to_openai_function( | ||
model: Type[BaseModel], | ||
*, | ||
name: Optional[str] = None, | ||
description: Optional[str] = None, | ||
) -> FunctionDescription: | ||
"""Converts a Pydantic model to a function description for the OpenAI API.""" | ||
schema = dereference_refs(model.schema()) | ||
schema.pop("definitions", None) | ||
return { | ||
"name": name or schema["title"], | ||
"description": description or schema["description"], | ||
"parameters": schema, | ||
} | ||
|
||
|
||
def convert_pydantic_to_openai_tool( | ||
model: Type[BaseModel], | ||
*, | ||
name: Optional[str] = None, | ||
description: Optional[str] = None, | ||
) -> ToolDescription: | ||
"""Converts a Pydantic model to a function description for the OpenAI API.""" | ||
function = convert_pydantic_to_openai_function( | ||
model, name=name, description=description | ||
) | ||
return {"type": "function", "function": function} | ||
|
||
|
||
def _get_python_function_name(function: Callable) -> str: | ||
"""Get the name of a Python function.""" | ||
return function.__name__ | ||
|
||
|
||
def _parse_python_function_docstring(function: Callable) -> Tuple[str, dict]: | ||
"""Parse the function and argument descriptions from the docstring of a function. | ||
|
||
Assumes the function docstring follows Google Python style guide. | ||
""" | ||
docstring = inspect.getdoc(function) | ||
if docstring: | ||
docstring_blocks = docstring.split("\n\n") | ||
descriptors = [] | ||
args_block = None | ||
past_descriptors = False | ||
for block in docstring_blocks: | ||
if block.startswith("Args:"): | ||
args_block = block | ||
break | ||
elif block.startswith("Returns:") or block.startswith("Example:"): | ||
# Don't break in case Args come after | ||
past_descriptors = True | ||
elif not past_descriptors: | ||
descriptors.append(block) | ||
else: | ||
continue | ||
description = " ".join(descriptors) | ||
else: | ||
description = "" | ||
args_block = None | ||
arg_descriptions = {} | ||
if args_block: | ||
arg = None | ||
for line in args_block.split("\n")[1:]: | ||
if ":" in line: | ||
arg, desc = line.split(":", maxsplit=1) | ||
arg_descriptions[arg.strip()] = desc.strip() | ||
elif arg: | ||
arg_descriptions[arg.strip()] += " " + line.strip() | ||
return description, arg_descriptions | ||
|
||
|
||
def _get_python_function_arguments(function: Callable, arg_descriptions: dict) -> dict: | ||
"""Get JsonSchema describing a Python functions arguments. | ||
|
||
Assumes all function arguments are of primitive types (int, float, str, bool) or | ||
are subclasses of pydantic.BaseModel. | ||
""" | ||
properties = {} | ||
annotations = inspect.getfullargspec(function).annotations | ||
for arg, arg_type in annotations.items(): | ||
if arg == "return": | ||
continue | ||
if isinstance(arg_type, type) and issubclass(arg_type, BaseModel): | ||
# Mypy error: | ||
# "type" has no attribute "schema" | ||
properties[arg] = arg_type.schema() # type: ignore[attr-defined] | ||
elif arg_type.__name__ in PYTHON_TO_JSON_TYPES: | ||
properties[arg] = {"type": PYTHON_TO_JSON_TYPES[arg_type.__name__]} | ||
if arg in arg_descriptions: | ||
if arg not in properties: | ||
properties[arg] = {} | ||
properties[arg]["description"] = arg_descriptions[arg] | ||
return properties | ||
|
||
|
||
def _get_python_function_required_args(function: Callable) -> List[str]: | ||
"""Get the required arguments for a Python function.""" | ||
spec = inspect.getfullargspec(function) | ||
required = spec.args[: -len(spec.defaults)] if spec.defaults else spec.args | ||
required += [k for k in spec.kwonlyargs if k not in (spec.kwonlydefaults or {})] | ||
|
||
is_class = type(function) is type | ||
if is_class and required[0] == "self": | ||
required = required[1:] | ||
return required | ||
|
||
|
||
def convert_python_function_to_openai_function( | ||
function: Callable, | ||
) -> Dict[str, Any]: | ||
"""Convert a Python function to an OpenAI function-calling API compatible dict. | ||
|
||
Assumes the Python function has type hints and a docstring with a description. If | ||
the docstring has Google Python style argument descriptions, these will be | ||
included as well. | ||
""" | ||
description, arg_descriptions = _parse_python_function_docstring(function) | ||
return { | ||
"name": _get_python_function_name(function), | ||
"description": description, | ||
"parameters": { | ||
"type": "object", | ||
"properties": _get_python_function_arguments(function, arg_descriptions), | ||
"required": _get_python_function_required_args(function), | ||
}, | ||
} | ||
|
||
|
||
def convert_to_openai_function( | ||
function: Union[Dict[str, Any], Type[BaseModel], Callable], | ||
) -> Dict[str, Any]: | ||
"""Convert a raw function/class to an OpenAI function. | ||
|
||
Args: | ||
function: Either a dictionary, a pydantic.BaseModel class, or a Python function. | ||
If a dictionary is passed in, it is assumed to already be a valid OpenAI | ||
function. | ||
|
||
Returns: | ||
A dict version of the passed in function which is compatible with the | ||
OpenAI function-calling API. | ||
""" | ||
if isinstance(function, dict): | ||
return function | ||
elif isinstance(function, type) and issubclass(function, BaseModel): | ||
return cast(Dict, convert_pydantic_to_openai_function(function)) | ||
elif callable(function): | ||
return convert_python_function_to_openai_function(function) | ||
|
||
else: | ||
raise ValueError( | ||
f"Unsupported function type {type(function)}. Functions must be passed in" | ||
f" as Dict, pydantic.BaseModel, or Callable." | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved function calling stuff to core/utils/function_calling