Skip to content
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

Merged
merged 66 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
20694ac
init
efriis Jan 2, 2024
48cbc10
add openai
efriis Jan 2, 2024
57d2970
add numpy
efriis Jan 2, 2024
8c43bf3
Merge branch 'master' into erick/partner-openai
efriis Jan 2, 2024
134b092
embeddings
efriis Jan 2, 2024
e309378
add back transformers option
efriis Jan 2, 2024
edb0851
chat model
efriis Jan 2, 2024
6f5eb50
downgrade mypy again
efriis Jan 2, 2024
d4fceed
x
efriis Jan 2, 2024
24e1eac
unit tests
efriis Jan 2, 2024
ad6bce5
remove duplicate test
efriis Jan 2, 2024
e6a8c25
more duplicate test
efriis Jan 2, 2024
26c395c
conversion
efriis Jan 3, 2024
1aa1dcb
Merge branch 'master' into erick/partner-openai
efriis Jan 3, 2024
01bd344
x
efriis Jan 3, 2024
9b1778b
Merge branch 'master' into erick/partner-openai
efriis Jan 3, 2024
f11d9ec
lint
efriis Jan 3, 2024
361d6e8
x
efriis Jan 3, 2024
61edd7e
lint core
efriis Jan 3, 2024
6c22e81
x
efriis Jan 3, 2024
c70d6c0
fix import test
efriis Jan 3, 2024
7b1204f
Merge branch 'master' into erick/partner-openai
efriis Jan 3, 2024
26620d8
chatopenai start
efriis Jan 3, 2024
81b0bb6
cookbook ChatOpenAI
efriis Jan 3, 2024
25e7262
docs ChatOpenAI
efriis Jan 3, 2024
00efbf4
docs ChatOpenAI
efriis Jan 3, 2024
7f385f2
docs ChatPromptTemplate
efriis Jan 3, 2024
01ed344
docs ChatPromptTemplate
efriis Jan 3, 2024
759cf14
docs ChatOpenAI
efriis Jan 3, 2024
fcfc090
docs OpenAI
efriis Jan 3, 2024
3254233
docs OpenAI
efriis Jan 3, 2024
fe7425f
docs OpenAIEmbeddings
efriis Jan 3, 2024
d27bf40
integration tests
efriis Jan 4, 2024
8e81c79
docs in separate pr
efriis Jan 4, 2024
32e207a
rm _utils
efriis Jan 4, 2024
0f06c8a
Merge branch 'master' into erick/partner-openai
efriis Jan 4, 2024
287eec7
ser
efriis Jan 4, 2024
970034f
reorg
efriis Jan 4, 2024
caa03ab
init
efriis Jan 4, 2024
8aaf50e
import test
efriis Jan 4, 2024
778fe39
tests
efriis Jan 4, 2024
92945d2
integration tests
efriis Jan 4, 2024
c389fbc
lint
efriis Jan 4, 2024
4b8ed72
mypy
efriis Jan 4, 2024
5e49c5b
fix
efriis Jan 4, 2024
cee3f6a
no more chat unit
efriis Jan 5, 2024
0abb3e0
Merge branch 'master' into erick/partner-openai
efriis Jan 5, 2024
06b2f3c
deps
efriis Jan 5, 2024
0827b72
deps
efriis Jan 5, 2024
bf766b3
deprecations
efriis Jan 5, 2024
0f7b35f
format
efriis Jan 5, 2024
e4a64fb
debug
efriis Jan 5, 2024
4a29380
rm print
efriis Jan 5, 2024
fa73df4
deps
efriis Jan 5, 2024
fefec8f
tests
efriis Jan 5, 2024
8c8b0df
t
efriis Jan 5, 2024
b4b6f71
-
efriis Jan 5, 2024
fe97a5c
simplify
efriis Jan 5, 2024
be6a46e
oops
efriis Jan 5, 2024
6277cdc
protect
efriis Jan 5, 2024
07fd7e9
delete
efriis Jan 5, 2024
5f05ad9
x
efriis Jan 5, 2024
390bb77
x
efriis Jan 5, 2024
fb8ddc7
warn
efriis Jan 5, 2024
412d059
secrets
efriis Jan 5, 2024
1e79455
Merge branch 'master' into erick/partner-openai
efriis Jan 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
66 changes: 15 additions & 51 deletions libs/community/langchain_community/utils/openai_functions.py
Copy link
Member Author

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

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",
]
31 changes: 0 additions & 31 deletions libs/community/tests/integration_tests/llms/test_openai.py
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deleted integration tests that were duplicates of unit tests

Original file line number Diff line number Diff line change
Expand Up @@ -22,37 +22,6 @@ def test_openai_call() -> None:
assert isinstance(output, str)


def test_openai_model_param() -> None:
llm = OpenAI(model="foo")
assert llm.model_name == "foo"
llm = OpenAI(model_name="foo")
assert llm.model_name == "foo"


def test_openai_extra_kwargs() -> None:
"""Test extra kwargs to openai."""
# Check that foo is saved in extra_kwargs.
llm = OpenAI(foo=3, max_tokens=10)
assert llm.max_tokens == 10
assert llm.model_kwargs == {"foo": 3}

# Test that if extra_kwargs are provided, they are added to it.
llm = OpenAI(foo=3, model_kwargs={"bar": 2})
assert llm.model_kwargs == {"foo": 3, "bar": 2}

# Test that if provided twice it errors
with pytest.raises(ValueError):
OpenAI(foo=3, model_kwargs={"foo": 2})

# Test that if explicit param is specified in kwargs it errors
with pytest.raises(ValueError):
OpenAI(model_kwargs={"temperature": 0.2})

# Test that "model" cannot be specified in kwargs
with pytest.raises(ValueError):
OpenAI(model_kwargs={"model": "gpt-3.5-turbo-instruct"})


def test_openai_llm_output_contains_model_name() -> None:
"""Test llm_output contains model_name."""
llm = OpenAI(max_tokens=10)
Expand Down
4 changes: 4 additions & 0 deletions libs/community/tests/unit_tests/llms/test_openai.py
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this one from integration tests

Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ def test_openai_invalid_model_kwargs() -> None:
with pytest.raises(ValueError):
OpenAI(model_kwargs={"model_name": "foo"})

# Test that "model" cannot be specified in kwargs
with pytest.raises(ValueError):
OpenAI(model_kwargs={"model": "gpt-3.5-turbo-instruct"})


@pytest.mark.requires("openai")
def test_openai_incorrect_field() -> None:
Expand Down
207 changes: 207 additions & 0 deletions libs/core/langchain_core/utils/function_calling.py
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved from

  • community/utils/openai_functions
  • langchain/chains/openai_functions

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."
)