mirror of
https://github.com/hwchase17/langchain.git
synced 2025-06-20 05:43:55 +00:00
openai[minor]: implement langchain-openai package (#15503)
Todo - [x] copy over integration tests - [x] update docs with new instructions in #15513 - [x] add linear ticket to bump core -> community, community->langchain, and core->openai deps - [ ] (optional): add `pip install langchain-openai` command to each notebook using it - [x] Update docstrings to not need `openai` install - [x] Add serialization - [x] deprecate old models Contributor steps: - [x] Add secret names to manual integrations workflow in .github/workflows/_integration_test.yml - [x] Add secrets to release workflow (for pre-release testing) in .github/workflows/_release.yml Maintainer steps (Contributors should not do these): - [x] set up pypi and test pypi projects - [x] add credential secrets to Github Actions - [ ] add package to conda-forge Functional changes to existing classes: - now relies on openai client v1 (1.6.1) via concrete dep in langchain-openai package Codebase organization - some function calling stuff moved to `langchain_core.utils.function_calling` in order to be used in both community and langchain-openai
This commit is contained in:
parent
a7d023aaf0
commit
ebc75c5ca7
1
.github/workflows/_integration_test.yml
vendored
1
.github/workflows/_integration_test.yml
vendored
@ -44,6 +44,7 @@ jobs:
|
|||||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
|
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
|
||||||
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
|
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
|
||||||
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
run: |
|
run: |
|
||||||
make integration_tests
|
make integration_tests
|
||||||
|
|
||||||
|
1
.github/workflows/_release.yml
vendored
1
.github/workflows/_release.yml
vendored
@ -156,6 +156,7 @@ jobs:
|
|||||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
|
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
|
||||||
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
|
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
|
||||||
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
run: make integration_tests
|
run: make integration_tests
|
||||||
working-directory: ${{ inputs.working-directory }}
|
working-directory: ${{ inputs.working-directory }}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import os
|
|||||||
import warnings
|
import warnings
|
||||||
from typing import Any, Callable, Dict, List, Union
|
from typing import Any, Callable, Dict, List, Union
|
||||||
|
|
||||||
|
from langchain_core._api.deprecation import deprecated
|
||||||
from langchain_core.outputs import ChatResult
|
from langchain_core.outputs import ChatResult
|
||||||
from langchain_core.pydantic_v1 import BaseModel, Field, root_validator
|
from langchain_core.pydantic_v1 import BaseModel, Field, root_validator
|
||||||
from langchain_core.utils import get_from_dict_or_env
|
from langchain_core.utils import get_from_dict_or_env
|
||||||
@ -16,6 +17,9 @@ from langchain_community.utils.openai import is_openai_v1
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated(
|
||||||
|
since="0.1.0", removal="0.2.0", alternative="langchain_openai.AzureChatOpenAI"
|
||||||
|
)
|
||||||
class AzureChatOpenAI(ChatOpenAI):
|
class AzureChatOpenAI(ChatOpenAI):
|
||||||
"""`Azure OpenAI` Chat Completion API.
|
"""`Azure OpenAI` Chat Completion API.
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ from typing import (
|
|||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from langchain_core._api.deprecation import deprecated
|
||||||
from langchain_core.callbacks import (
|
from langchain_core.callbacks import (
|
||||||
AsyncCallbackManagerForLLMRun,
|
AsyncCallbackManagerForLLMRun,
|
||||||
CallbackManagerForLLMRun,
|
CallbackManagerForLLMRun,
|
||||||
@ -143,6 +144,7 @@ def _convert_delta_to_message_chunk(
|
|||||||
return default_class(content=content)
|
return default_class(content=content)
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated(since="0.1.0", removal="0.2.0", alternative="langchain_openai.ChatOpenAI")
|
||||||
class ChatOpenAI(BaseChatModel):
|
class ChatOpenAI(BaseChatModel):
|
||||||
"""`OpenAI` Chat large language models API.
|
"""`OpenAI` Chat large language models API.
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import os
|
|||||||
import warnings
|
import warnings
|
||||||
from typing import Callable, Dict, Optional, Union
|
from typing import Callable, Dict, Optional, Union
|
||||||
|
|
||||||
|
from langchain_core._api.deprecation import deprecated
|
||||||
from langchain_core.pydantic_v1 import Field, root_validator
|
from langchain_core.pydantic_v1 import Field, root_validator
|
||||||
from langchain_core.utils import get_from_dict_or_env
|
from langchain_core.utils import get_from_dict_or_env
|
||||||
|
|
||||||
@ -12,6 +13,9 @@ from langchain_community.embeddings.openai import OpenAIEmbeddings
|
|||||||
from langchain_community.utils.openai import is_openai_v1
|
from langchain_community.utils.openai import is_openai_v1
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated(
|
||||||
|
since="0.1.0", removal="0.2.0", alternative="langchain_openai.AzureOpenAIEmbeddings"
|
||||||
|
)
|
||||||
class AzureOpenAIEmbeddings(OpenAIEmbeddings):
|
class AzureOpenAIEmbeddings(OpenAIEmbeddings):
|
||||||
"""`Azure OpenAI` Embeddings API."""
|
"""`Azure OpenAI` Embeddings API."""
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ from typing import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from langchain_core._api.deprecation import deprecated
|
||||||
from langchain_core.embeddings import Embeddings
|
from langchain_core.embeddings import Embeddings
|
||||||
from langchain_core.pydantic_v1 import BaseModel, Extra, Field, root_validator
|
from langchain_core.pydantic_v1 import BaseModel, Extra, Field, root_validator
|
||||||
from langchain_core.utils import get_from_dict_or_env, get_pydantic_field_names
|
from langchain_core.utils import get_from_dict_or_env, get_pydantic_field_names
|
||||||
@ -137,6 +138,11 @@ async def async_embed_with_retry(embeddings: OpenAIEmbeddings, **kwargs: Any) ->
|
|||||||
return await _async_embed_with_retry(**kwargs)
|
return await _async_embed_with_retry(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated(
|
||||||
|
since="0.1.0",
|
||||||
|
removal="0.2.0",
|
||||||
|
alternative="langchain_openai.OpenAIEmbeddings",
|
||||||
|
)
|
||||||
class OpenAIEmbeddings(BaseModel, Embeddings):
|
class OpenAIEmbeddings(BaseModel, Embeddings):
|
||||||
"""OpenAI embedding models.
|
"""OpenAI embedding models.
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ from typing import (
|
|||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from langchain_core._api.deprecation import deprecated
|
||||||
from langchain_core.callbacks import (
|
from langchain_core.callbacks import (
|
||||||
AsyncCallbackManagerForLLMRun,
|
AsyncCallbackManagerForLLMRun,
|
||||||
CallbackManagerForLLMRun,
|
CallbackManagerForLLMRun,
|
||||||
@ -724,6 +725,7 @@ class BaseOpenAI(BaseLLM):
|
|||||||
return self.max_context_size - num_tokens
|
return self.max_context_size - num_tokens
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated(since="0.1.0", removal="0.2.0", alternative="langchain_openai.OpenAI")
|
||||||
class OpenAI(BaseOpenAI):
|
class OpenAI(BaseOpenAI):
|
||||||
"""OpenAI large language models.
|
"""OpenAI large language models.
|
||||||
|
|
||||||
@ -750,6 +752,7 @@ class OpenAI(BaseOpenAI):
|
|||||||
return {**{"model": self.model_name}, **super()._invocation_params}
|
return {**{"model": self.model_name}, **super()._invocation_params}
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated(since="0.1.0", removal="0.2.0", alternative="langchain_openai.AzureOpenAI")
|
||||||
class AzureOpenAI(BaseOpenAI):
|
class AzureOpenAI(BaseOpenAI):
|
||||||
"""Azure-specific OpenAI large language models.
|
"""Azure-specific OpenAI large language models.
|
||||||
|
|
||||||
@ -953,6 +956,7 @@ class AzureOpenAI(BaseOpenAI):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated(since="0.1.0", removal="0.2.0", alternative="langchain_openai.ChatOpenAI")
|
||||||
class OpenAIChat(BaseLLM):
|
class OpenAIChat(BaseLLM):
|
||||||
"""OpenAI Chat large language models.
|
"""OpenAI Chat large language models.
|
||||||
|
|
||||||
|
@ -1,51 +1,15 @@
|
|||||||
from typing import Literal, Optional, Type, TypedDict
|
# these stubs are just for backwards compatibility
|
||||||
|
|
||||||
from langchain_core.pydantic_v1 import BaseModel
|
from langchain_core.utils.function_calling import (
|
||||||
from langchain_core.utils.json_schema import dereference_refs
|
FunctionDescription,
|
||||||
|
ToolDescription,
|
||||||
|
convert_pydantic_to_openai_function,
|
||||||
|
convert_pydantic_to_openai_tool,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
class FunctionDescription(TypedDict):
|
"FunctionDescription",
|
||||||
"""Representation of a callable function to the OpenAI API."""
|
"ToolDescription",
|
||||||
|
"convert_pydantic_to_openai_function",
|
||||||
name: str
|
"convert_pydantic_to_openai_tool",
|
||||||
"""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}
|
|
||||||
|
34
libs/community/poetry.lock
generated
34
libs/community/poetry.lock
generated
@ -1,4 +1,4 @@
|
|||||||
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
|
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aenum"
|
name = "aenum"
|
||||||
@ -1173,7 +1173,6 @@ files = [
|
|||||||
{file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18a64814ae7bce73925131381603fff0116e2df25230dfc80d6d690aa6e20b37"},
|
{file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18a64814ae7bce73925131381603fff0116e2df25230dfc80d6d690aa6e20b37"},
|
||||||
{file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c81f22b4f572f8a2110b0b741bb64e5a6427e0a198b2cdc1fbaf85f352a3aa"},
|
{file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c81f22b4f572f8a2110b0b741bb64e5a6427e0a198b2cdc1fbaf85f352a3aa"},
|
||||||
{file = "contourpy-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53cc3a40635abedbec7f1bde60f8c189c49e84ac180c665f2cd7c162cc454baa"},
|
{file = "contourpy-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53cc3a40635abedbec7f1bde60f8c189c49e84ac180c665f2cd7c162cc454baa"},
|
||||||
{file = "contourpy-1.1.0-cp310-cp310-win32.whl", hash = "sha256:9b2dd2ca3ac561aceef4c7c13ba654aaa404cf885b187427760d7f7d4c57cff8"},
|
|
||||||
{file = "contourpy-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:1f795597073b09d631782e7245016a4323cf1cf0b4e06eef7ea6627e06a37ff2"},
|
{file = "contourpy-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:1f795597073b09d631782e7245016a4323cf1cf0b4e06eef7ea6627e06a37ff2"},
|
||||||
{file = "contourpy-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0b7b04ed0961647691cfe5d82115dd072af7ce8846d31a5fac6c142dcce8b882"},
|
{file = "contourpy-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0b7b04ed0961647691cfe5d82115dd072af7ce8846d31a5fac6c142dcce8b882"},
|
||||||
{file = "contourpy-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27bc79200c742f9746d7dd51a734ee326a292d77e7d94c8af6e08d1e6c15d545"},
|
{file = "contourpy-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27bc79200c742f9746d7dd51a734ee326a292d77e7d94c8af6e08d1e6c15d545"},
|
||||||
@ -1182,7 +1181,6 @@ files = [
|
|||||||
{file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5cec36c5090e75a9ac9dbd0ff4a8cf7cecd60f1b6dc23a374c7d980a1cd710e"},
|
{file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5cec36c5090e75a9ac9dbd0ff4a8cf7cecd60f1b6dc23a374c7d980a1cd710e"},
|
||||||
{file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f0cbd657e9bde94cd0e33aa7df94fb73c1ab7799378d3b3f902eb8eb2e04a3a"},
|
{file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f0cbd657e9bde94cd0e33aa7df94fb73c1ab7799378d3b3f902eb8eb2e04a3a"},
|
||||||
{file = "contourpy-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:181cbace49874f4358e2929aaf7ba84006acb76694102e88dd15af861996c16e"},
|
{file = "contourpy-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:181cbace49874f4358e2929aaf7ba84006acb76694102e88dd15af861996c16e"},
|
||||||
{file = "contourpy-1.1.0-cp311-cp311-win32.whl", hash = "sha256:edb989d31065b1acef3828a3688f88b2abb799a7db891c9e282df5ec7e46221b"},
|
|
||||||
{file = "contourpy-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb3b7d9e6243bfa1efb93ccfe64ec610d85cfe5aec2c25f97fbbd2e58b531256"},
|
{file = "contourpy-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb3b7d9e6243bfa1efb93ccfe64ec610d85cfe5aec2c25f97fbbd2e58b531256"},
|
||||||
{file = "contourpy-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bcb41692aa09aeb19c7c213411854402f29f6613845ad2453d30bf421fe68fed"},
|
{file = "contourpy-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bcb41692aa09aeb19c7c213411854402f29f6613845ad2453d30bf421fe68fed"},
|
||||||
{file = "contourpy-1.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5d123a5bc63cd34c27ff9c7ac1cd978909e9c71da12e05be0231c608048bb2ae"},
|
{file = "contourpy-1.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5d123a5bc63cd34c27ff9c7ac1cd978909e9c71da12e05be0231c608048bb2ae"},
|
||||||
@ -1191,7 +1189,6 @@ files = [
|
|||||||
{file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:317267d915490d1e84577924bd61ba71bf8681a30e0d6c545f577363157e5e94"},
|
{file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:317267d915490d1e84577924bd61ba71bf8681a30e0d6c545f577363157e5e94"},
|
||||||
{file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d551f3a442655f3dcc1285723f9acd646ca5858834efeab4598d706206b09c9f"},
|
{file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d551f3a442655f3dcc1285723f9acd646ca5858834efeab4598d706206b09c9f"},
|
||||||
{file = "contourpy-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7a117ce7df5a938fe035cad481b0189049e8d92433b4b33aa7fc609344aafa1"},
|
{file = "contourpy-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7a117ce7df5a938fe035cad481b0189049e8d92433b4b33aa7fc609344aafa1"},
|
||||||
{file = "contourpy-1.1.0-cp38-cp38-win32.whl", hash = "sha256:108dfb5b3e731046a96c60bdc46a1a0ebee0760418951abecbe0fc07b5b93b27"},
|
|
||||||
{file = "contourpy-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4f26b25b4f86087e7d75e63212756c38546e70f2a92d2be44f80114826e1cd4"},
|
{file = "contourpy-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4f26b25b4f86087e7d75e63212756c38546e70f2a92d2be44f80114826e1cd4"},
|
||||||
{file = "contourpy-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc00bb4225d57bff7ebb634646c0ee2a1298402ec10a5fe7af79df9a51c1bfd9"},
|
{file = "contourpy-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc00bb4225d57bff7ebb634646c0ee2a1298402ec10a5fe7af79df9a51c1bfd9"},
|
||||||
{file = "contourpy-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:189ceb1525eb0655ab8487a9a9c41f42a73ba52d6789754788d1883fb06b2d8a"},
|
{file = "contourpy-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:189ceb1525eb0655ab8487a9a9c41f42a73ba52d6789754788d1883fb06b2d8a"},
|
||||||
@ -1200,7 +1197,6 @@ files = [
|
|||||||
{file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:143dde50520a9f90e4a2703f367cf8ec96a73042b72e68fcd184e1279962eb6f"},
|
{file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:143dde50520a9f90e4a2703f367cf8ec96a73042b72e68fcd184e1279962eb6f"},
|
||||||
{file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e94bef2580e25b5fdb183bf98a2faa2adc5b638736b2c0a4da98691da641316a"},
|
{file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e94bef2580e25b5fdb183bf98a2faa2adc5b638736b2c0a4da98691da641316a"},
|
||||||
{file = "contourpy-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ed614aea8462735e7d70141374bd7650afd1c3f3cb0c2dbbcbe44e14331bf002"},
|
{file = "contourpy-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ed614aea8462735e7d70141374bd7650afd1c3f3cb0c2dbbcbe44e14331bf002"},
|
||||||
{file = "contourpy-1.1.0-cp39-cp39-win32.whl", hash = "sha256:71551f9520f008b2950bef5f16b0e3587506ef4f23c734b71ffb7b89f8721999"},
|
|
||||||
{file = "contourpy-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:438ba416d02f82b692e371858143970ed2eb6337d9cdbbede0d8ad9f3d7dd17d"},
|
{file = "contourpy-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:438ba416d02f82b692e371858143970ed2eb6337d9cdbbede0d8ad9f3d7dd17d"},
|
||||||
{file = "contourpy-1.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a698c6a7a432789e587168573a864a7ea374c6be8d4f31f9d87c001d5a843493"},
|
{file = "contourpy-1.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a698c6a7a432789e587168573a864a7ea374c6be8d4f31f9d87c001d5a843493"},
|
||||||
{file = "contourpy-1.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397b0ac8a12880412da3551a8cb5a187d3298a72802b45a3bd1805e204ad8439"},
|
{file = "contourpy-1.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397b0ac8a12880412da3551a8cb5a187d3298a72802b45a3bd1805e204ad8439"},
|
||||||
@ -3885,7 +3881,7 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "langchain-core"
|
name = "langchain-core"
|
||||||
version = "0.1.5"
|
version = "0.1.6"
|
||||||
description = "Building applications with LLMs through composability"
|
description = "Building applications with LLMs through composability"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8.1,<4.0"
|
python-versions = ">=3.8.1,<4.0"
|
||||||
@ -4132,16 +4128,6 @@ files = [
|
|||||||
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
|
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
|
||||||
{file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
|
{file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
|
||||||
{file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
|
{file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
|
||||||
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
|
|
||||||
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
|
|
||||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
|
|
||||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
|
|
||||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
|
|
||||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
|
|
||||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
|
|
||||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
|
|
||||||
{file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
|
|
||||||
{file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
|
|
||||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
|
{file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
|
||||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
|
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
|
||||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
|
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
|
||||||
@ -6701,7 +6687,6 @@ files = [
|
|||||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
|
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
|
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
|
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
|
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
|
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
|
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
|
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
|
||||||
@ -6709,15 +6694,8 @@ files = [
|
|||||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
|
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
|
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
|
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
|
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
|
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
|
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
|
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
|
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
|
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
|
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
|
|
||||||
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
|
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
|
||||||
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
|
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
|
||||||
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
|
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
|
||||||
@ -6734,7 +6712,6 @@ files = [
|
|||||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
|
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
|
||||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
|
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
|
||||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
|
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
|
||||||
{file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
|
|
||||||
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
|
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
|
||||||
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
|
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
|
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
|
||||||
@ -6742,7 +6719,6 @@ files = [
|
|||||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
|
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
|
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
|
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
|
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
|
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
|
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
|
||||||
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
|
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
|
||||||
@ -7714,9 +7690,7 @@ python-versions = ">=3.7"
|
|||||||
files = [
|
files = [
|
||||||
{file = "SQLAlchemy-2.0.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:638c2c0b6b4661a4fd264f6fb804eccd392745c5887f9317feb64bb7cb03b3ea"},
|
{file = "SQLAlchemy-2.0.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:638c2c0b6b4661a4fd264f6fb804eccd392745c5887f9317feb64bb7cb03b3ea"},
|
||||||
{file = "SQLAlchemy-2.0.23-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3b5036aa326dc2df50cba3c958e29b291a80f604b1afa4c8ce73e78e1c9f01d"},
|
{file = "SQLAlchemy-2.0.23-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3b5036aa326dc2df50cba3c958e29b291a80f604b1afa4c8ce73e78e1c9f01d"},
|
||||||
{file = "SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:787af80107fb691934a01889ca8f82a44adedbf5ef3d6ad7d0f0b9ac557e0c34"},
|
|
||||||
{file = "SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c14eba45983d2f48f7546bb32b47937ee2cafae353646295f0e99f35b14286ab"},
|
{file = "SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c14eba45983d2f48f7546bb32b47937ee2cafae353646295f0e99f35b14286ab"},
|
||||||
{file = "SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0666031df46b9badba9bed00092a1ffa3aa063a5e68fa244acd9f08070e936d3"},
|
|
||||||
{file = "SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:89a01238fcb9a8af118eaad3ffcc5dedaacbd429dc6fdc43fe430d3a941ff965"},
|
{file = "SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:89a01238fcb9a8af118eaad3ffcc5dedaacbd429dc6fdc43fe430d3a941ff965"},
|
||||||
{file = "SQLAlchemy-2.0.23-cp310-cp310-win32.whl", hash = "sha256:cabafc7837b6cec61c0e1e5c6d14ef250b675fa9c3060ed8a7e38653bd732ff8"},
|
{file = "SQLAlchemy-2.0.23-cp310-cp310-win32.whl", hash = "sha256:cabafc7837b6cec61c0e1e5c6d14ef250b675fa9c3060ed8a7e38653bd732ff8"},
|
||||||
{file = "SQLAlchemy-2.0.23-cp310-cp310-win_amd64.whl", hash = "sha256:87a3d6b53c39cd173990de2f5f4b83431d534a74f0e2f88bd16eabb5667e65c6"},
|
{file = "SQLAlchemy-2.0.23-cp310-cp310-win_amd64.whl", hash = "sha256:87a3d6b53c39cd173990de2f5f4b83431d534a74f0e2f88bd16eabb5667e65c6"},
|
||||||
@ -7753,9 +7727,7 @@ files = [
|
|||||||
{file = "SQLAlchemy-2.0.23-cp38-cp38-win_amd64.whl", hash = "sha256:964971b52daab357d2c0875825e36584d58f536e920f2968df8d581054eada4b"},
|
{file = "SQLAlchemy-2.0.23-cp38-cp38-win_amd64.whl", hash = "sha256:964971b52daab357d2c0875825e36584d58f536e920f2968df8d581054eada4b"},
|
||||||
{file = "SQLAlchemy-2.0.23-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:616fe7bcff0a05098f64b4478b78ec2dfa03225c23734d83d6c169eb41a93e55"},
|
{file = "SQLAlchemy-2.0.23-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:616fe7bcff0a05098f64b4478b78ec2dfa03225c23734d83d6c169eb41a93e55"},
|
||||||
{file = "SQLAlchemy-2.0.23-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0e680527245895aba86afbd5bef6c316831c02aa988d1aad83c47ffe92655e74"},
|
{file = "SQLAlchemy-2.0.23-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0e680527245895aba86afbd5bef6c316831c02aa988d1aad83c47ffe92655e74"},
|
||||||
{file = "SQLAlchemy-2.0.23-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9585b646ffb048c0250acc7dad92536591ffe35dba624bb8fd9b471e25212a35"},
|
|
||||||
{file = "SQLAlchemy-2.0.23-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4895a63e2c271ffc7a81ea424b94060f7b3b03b4ea0cd58ab5bb676ed02f4221"},
|
{file = "SQLAlchemy-2.0.23-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4895a63e2c271ffc7a81ea424b94060f7b3b03b4ea0cd58ab5bb676ed02f4221"},
|
||||||
{file = "SQLAlchemy-2.0.23-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cc1d21576f958c42d9aec68eba5c1a7d715e5fc07825a629015fe8e3b0657fb0"},
|
|
||||||
{file = "SQLAlchemy-2.0.23-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:967c0b71156f793e6662dd839da54f884631755275ed71f1539c95bbada9aaab"},
|
{file = "SQLAlchemy-2.0.23-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:967c0b71156f793e6662dd839da54f884631755275ed71f1539c95bbada9aaab"},
|
||||||
{file = "SQLAlchemy-2.0.23-cp39-cp39-win32.whl", hash = "sha256:0a8c6aa506893e25a04233bc721c6b6cf844bafd7250535abb56cb6cc1368884"},
|
{file = "SQLAlchemy-2.0.23-cp39-cp39-win32.whl", hash = "sha256:0a8c6aa506893e25a04233bc721c6b6cf844bafd7250535abb56cb6cc1368884"},
|
||||||
{file = "SQLAlchemy-2.0.23-cp39-cp39-win_amd64.whl", hash = "sha256:f3420d00d2cb42432c1d0e44540ae83185ccbbc67a6054dcc8ab5387add6620b"},
|
{file = "SQLAlchemy-2.0.23-cp39-cp39-win_amd64.whl", hash = "sha256:f3420d00d2cb42432c1d0e44540ae83185ccbbc67a6054dcc8ab5387add6620b"},
|
||||||
@ -9172,4 +9144,4 @@ extended-testing = ["aiosqlite", "aleph-alpha-client", "anthropic", "arxiv", "as
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = ">=3.8.1,<4.0"
|
python-versions = ">=3.8.1,<4.0"
|
||||||
content-hash = "bccc7bda518d01eb91a86397c0b22b83db9d57ee45c2bca4e46fc8b22ddb6a17"
|
content-hash = "211766fff312525865b6b28225f61b70b18a0fcda9a3212ea8de7ef4f327c51a"
|
||||||
|
@ -141,7 +141,7 @@ wrapt = "^1.15.0"
|
|||||||
openai = "^1"
|
openai = "^1"
|
||||||
python-dotenv = "^1.0.0"
|
python-dotenv = "^1.0.0"
|
||||||
cassio = "^0.1.0"
|
cassio = "^0.1.0"
|
||||||
tiktoken = "^0.3.2"
|
tiktoken = ">=0.3.2,<0.6.0"
|
||||||
anthropic = "^0.3.11"
|
anthropic = "^0.3.11"
|
||||||
langchain-core = { path = "../core", develop = true }
|
langchain-core = { path = "../core", develop = true }
|
||||||
fireworks-ai = "^0.9.0"
|
fireworks-ai = "^0.9.0"
|
||||||
|
@ -22,37 +22,6 @@ def test_openai_call() -> None:
|
|||||||
assert isinstance(output, str)
|
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:
|
def test_openai_llm_output_contains_model_name() -> None:
|
||||||
"""Test llm_output contains model_name."""
|
"""Test llm_output contains model_name."""
|
||||||
llm = OpenAI(max_tokens=10)
|
llm = OpenAI(max_tokens=10)
|
||||||
|
@ -34,6 +34,10 @@ def test_openai_invalid_model_kwargs() -> None:
|
|||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
OpenAI(model_kwargs={"model_name": "foo"})
|
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")
|
@pytest.mark.requires("openai")
|
||||||
def test_openai_incorrect_field() -> None:
|
def test_openai_incorrect_field() -> None:
|
||||||
|
@ -90,9 +90,9 @@ SERIALIZABLE_MAPPING = {
|
|||||||
"MessagesPlaceholder",
|
"MessagesPlaceholder",
|
||||||
),
|
),
|
||||||
("langchain", "llms", "openai", "OpenAI"): (
|
("langchain", "llms", "openai", "OpenAI"): (
|
||||||
"langchain",
|
"langchain_openai",
|
||||||
"llms",
|
"llms",
|
||||||
"openai",
|
"base",
|
||||||
"OpenAI",
|
"OpenAI",
|
||||||
),
|
),
|
||||||
("langchain", "prompts", "chat", "ChatPromptTemplate"): (
|
("langchain", "prompts", "chat", "ChatPromptTemplate"): (
|
||||||
@ -203,9 +203,9 @@ SERIALIZABLE_MAPPING = {
|
|||||||
"StrOutputParser",
|
"StrOutputParser",
|
||||||
),
|
),
|
||||||
("langchain", "chat_models", "openai", "ChatOpenAI"): (
|
("langchain", "chat_models", "openai", "ChatOpenAI"): (
|
||||||
"langchain",
|
"langchain_openai",
|
||||||
"chat_models",
|
"chat_models",
|
||||||
"openai",
|
"base",
|
||||||
"ChatOpenAI",
|
"ChatOpenAI",
|
||||||
),
|
),
|
||||||
("langchain", "output_parsers", "list", "CommaSeparatedListOutputParser"): (
|
("langchain", "output_parsers", "list", "CommaSeparatedListOutputParser"): (
|
||||||
@ -221,9 +221,9 @@ SERIALIZABLE_MAPPING = {
|
|||||||
"RunnableParallel",
|
"RunnableParallel",
|
||||||
),
|
),
|
||||||
("langchain", "chat_models", "azure_openai", "AzureChatOpenAI"): (
|
("langchain", "chat_models", "azure_openai", "AzureChatOpenAI"): (
|
||||||
"langchain",
|
"langchain_openai",
|
||||||
"chat_models",
|
"chat_models",
|
||||||
"azure_openai",
|
"azure",
|
||||||
"AzureChatOpenAI",
|
"AzureChatOpenAI",
|
||||||
),
|
),
|
||||||
("langchain", "chat_models", "bedrock", "BedrockChat"): (
|
("langchain", "chat_models", "bedrock", "BedrockChat"): (
|
||||||
@ -323,9 +323,9 @@ SERIALIZABLE_MAPPING = {
|
|||||||
"GooglePalm",
|
"GooglePalm",
|
||||||
),
|
),
|
||||||
("langchain", "llms", "openai", "AzureOpenAI"): (
|
("langchain", "llms", "openai", "AzureOpenAI"): (
|
||||||
"langchain",
|
"langchain_openai",
|
||||||
"llms",
|
"llms",
|
||||||
"openai",
|
"azure",
|
||||||
"AzureOpenAI",
|
"AzureOpenAI",
|
||||||
),
|
),
|
||||||
("langchain", "llms", "replicate", "Replicate"): (
|
("langchain", "llms", "replicate", "Replicate"): (
|
||||||
|
202
libs/core/langchain_core/utils/function_calling.py
Normal file
202
libs/core/langchain_core/utils/function_calling.py
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
"""Methods for creating function specs in the style of OpenAI Functions"""
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Callable,
|
||||||
|
Dict,
|
||||||
|
List,
|
||||||
|
Literal,
|
||||||
|
Optional,
|
||||||
|
Tuple,
|
||||||
|
Type,
|
||||||
|
Union,
|
||||||
|
cast,
|
||||||
|
)
|
||||||
|
|
||||||
|
from typing_extensions import TypedDict
|
||||||
|
|
||||||
|
from langchain_core.pydantic_v1 import BaseModel
|
||||||
|
from langchain_core.utils.json_schema import dereference_refs
|
||||||
|
|
||||||
|
PYTHON_TO_JSON_TYPES = {
|
||||||
|
"str": "string",
|
||||||
|
"int": "number",
|
||||||
|
"float": "number",
|
||||||
|
"bool": "boolean",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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."
|
||||||
|
)
|
@ -1,16 +1,12 @@
|
|||||||
"""Methods for creating chains that use OpenAI function-calling APIs."""
|
"""Methods for creating chains that use OpenAI function-calling APIs."""
|
||||||
import inspect
|
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
Callable,
|
Callable,
|
||||||
Dict,
|
Dict,
|
||||||
List,
|
|
||||||
Optional,
|
Optional,
|
||||||
Sequence,
|
Sequence,
|
||||||
Tuple,
|
|
||||||
Type,
|
Type,
|
||||||
Union,
|
Union,
|
||||||
cast,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from langchain_core.output_parsers import (
|
from langchain_core.output_parsers import (
|
||||||
@ -21,6 +17,10 @@ from langchain_core.output_parsers import (
|
|||||||
from langchain_core.prompts import BasePromptTemplate
|
from langchain_core.prompts import BasePromptTemplate
|
||||||
from langchain_core.pydantic_v1 import BaseModel
|
from langchain_core.pydantic_v1 import BaseModel
|
||||||
from langchain_core.runnables import Runnable
|
from langchain_core.runnables import Runnable
|
||||||
|
from langchain_core.utils.function_calling import (
|
||||||
|
PYTHON_TO_JSON_TYPES,
|
||||||
|
convert_to_openai_function,
|
||||||
|
)
|
||||||
|
|
||||||
from langchain.base_language import BaseLanguageModel
|
from langchain.base_language import BaseLanguageModel
|
||||||
from langchain.chains import LLMChain
|
from langchain.chains import LLMChain
|
||||||
@ -29,142 +29,6 @@ from langchain.output_parsers.openai_functions import (
|
|||||||
PydanticAttrOutputFunctionsParser,
|
PydanticAttrOutputFunctionsParser,
|
||||||
PydanticOutputFunctionsParser,
|
PydanticOutputFunctionsParser,
|
||||||
)
|
)
|
||||||
from langchain.utils.openai_functions import convert_pydantic_to_openai_function
|
|
||||||
|
|
||||||
PYTHON_TO_JSON_TYPES = {
|
|
||||||
"str": "string",
|
|
||||||
"int": "number",
|
|
||||||
"float": "number",
|
|
||||||
"bool": "boolean",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_openai_output_parser(
|
def get_openai_output_parser(
|
||||||
@ -557,3 +421,14 @@ def create_structured_output_chain(
|
|||||||
output_parser=output_parser,
|
output_parser=output_parser,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"create_openai_fn_chain",
|
||||||
|
"create_openai_fn_runnable",
|
||||||
|
"create_structured_output_chain",
|
||||||
|
"create_structured_output_runnable",
|
||||||
|
"get_openai_output_parser",
|
||||||
|
"PYTHON_TO_JSON_TYPES",
|
||||||
|
"convert_to_openai_function",
|
||||||
|
]
|
||||||
|
182
libs/langchain/poetry.lock
generated
182
libs/langchain/poetry.lock
generated
@ -1,4 +1,4 @@
|
|||||||
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
|
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiodns"
|
name = "aiodns"
|
||||||
@ -2358,7 +2358,7 @@ files = [
|
|||||||
{file = "greenlet-3.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b72b802496cccbd9b31acea72b6f87e7771ccfd7f7927437d592e5c92ed703c"},
|
{file = "greenlet-3.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b72b802496cccbd9b31acea72b6f87e7771ccfd7f7927437d592e5c92ed703c"},
|
||||||
{file = "greenlet-3.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:527cd90ba3d8d7ae7dceb06fda619895768a46a1b4e423bdb24c1969823b8362"},
|
{file = "greenlet-3.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:527cd90ba3d8d7ae7dceb06fda619895768a46a1b4e423bdb24c1969823b8362"},
|
||||||
{file = "greenlet-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:37f60b3a42d8b5499be910d1267b24355c495064f271cfe74bf28b17b099133c"},
|
{file = "greenlet-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:37f60b3a42d8b5499be910d1267b24355c495064f271cfe74bf28b17b099133c"},
|
||||||
{file = "greenlet-3.0.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1482fba7fbed96ea7842b5a7fc11d61727e8be75a077e603e8ab49d24e234383"},
|
{file = "greenlet-3.0.0-cp311-universal2-macosx_10_9_universal2.whl", hash = "sha256:c3692ecf3fe754c8c0f2c95ff19626584459eab110eaab66413b1e7425cd84e9"},
|
||||||
{file = "greenlet-3.0.0-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:be557119bf467d37a8099d91fbf11b2de5eb1fd5fc5b91598407574848dc910f"},
|
{file = "greenlet-3.0.0-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:be557119bf467d37a8099d91fbf11b2de5eb1fd5fc5b91598407574848dc910f"},
|
||||||
{file = "greenlet-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73b2f1922a39d5d59cc0e597987300df3396b148a9bd10b76a058a2f2772fc04"},
|
{file = "greenlet-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73b2f1922a39d5d59cc0e597987300df3396b148a9bd10b76a058a2f2772fc04"},
|
||||||
{file = "greenlet-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1e22c22f7826096ad503e9bb681b05b8c1f5a8138469b255eb91f26a76634f2"},
|
{file = "greenlet-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1e22c22f7826096ad503e9bb681b05b8c1f5a8138469b255eb91f26a76634f2"},
|
||||||
@ -2368,6 +2368,7 @@ files = [
|
|||||||
{file = "greenlet-3.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:952256c2bc5b4ee8df8dfc54fc4de330970bf5d79253c863fb5e6761f00dda35"},
|
{file = "greenlet-3.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:952256c2bc5b4ee8df8dfc54fc4de330970bf5d79253c863fb5e6761f00dda35"},
|
||||||
{file = "greenlet-3.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:269d06fa0f9624455ce08ae0179430eea61085e3cf6457f05982b37fd2cefe17"},
|
{file = "greenlet-3.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:269d06fa0f9624455ce08ae0179430eea61085e3cf6457f05982b37fd2cefe17"},
|
||||||
{file = "greenlet-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9adbd8ecf097e34ada8efde9b6fec4dd2a903b1e98037adf72d12993a1c80b51"},
|
{file = "greenlet-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9adbd8ecf097e34ada8efde9b6fec4dd2a903b1e98037adf72d12993a1c80b51"},
|
||||||
|
{file = "greenlet-3.0.0-cp312-universal2-macosx_10_9_universal2.whl", hash = "sha256:553d6fb2324e7f4f0899e5ad2c427a4579ed4873f42124beba763f16032959af"},
|
||||||
{file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6b5ce7f40f0e2f8b88c28e6691ca6806814157ff05e794cdd161be928550f4c"},
|
{file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6b5ce7f40f0e2f8b88c28e6691ca6806814157ff05e794cdd161be928550f4c"},
|
||||||
{file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecf94aa539e97a8411b5ea52fc6ccd8371be9550c4041011a091eb8b3ca1d810"},
|
{file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecf94aa539e97a8411b5ea52fc6ccd8371be9550c4041011a091eb8b3ca1d810"},
|
||||||
{file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80dcd3c938cbcac986c5c92779db8e8ce51a89a849c135172c88ecbdc8c056b7"},
|
{file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80dcd3c938cbcac986c5c92779db8e8ce51a89a849c135172c88ecbdc8c056b7"},
|
||||||
@ -3475,7 +3476,7 @@ url = "../community"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "langchain-core"
|
name = "langchain-core"
|
||||||
version = "0.1.5"
|
version = "0.1.6"
|
||||||
description = "Building applications with LLMs through composability"
|
description = "Building applications with LLMs through composability"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8.1,<4.0"
|
python-versions = ">=3.8.1,<4.0"
|
||||||
@ -3499,6 +3500,25 @@ extended-testing = ["jinja2 (>=3,<4)"]
|
|||||||
type = "directory"
|
type = "directory"
|
||||||
url = "../core"
|
url = "../core"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "langchain-openai"
|
||||||
|
version = "0.0.1"
|
||||||
|
description = "An integration package connecting OpenAI and LangChain"
|
||||||
|
optional = true
|
||||||
|
python-versions = ">=3.8.1,<4.0"
|
||||||
|
files = []
|
||||||
|
develop = false
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
langchain-core = ">=0.0.12"
|
||||||
|
numpy = "^1"
|
||||||
|
openai = "^1.6.1"
|
||||||
|
tiktoken = "^0.5.2"
|
||||||
|
|
||||||
|
[package.source]
|
||||||
|
type = "directory"
|
||||||
|
url = "../partners/openai"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "langsmith"
|
name = "langsmith"
|
||||||
version = "0.0.77"
|
version = "0.0.77"
|
||||||
@ -3727,16 +3747,6 @@ files = [
|
|||||||
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
|
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
|
||||||
{file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
|
{file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
|
||||||
{file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
|
{file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
|
||||||
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
|
|
||||||
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
|
|
||||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
|
|
||||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
|
|
||||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
|
|
||||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
|
|
||||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
|
|
||||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
|
|
||||||
{file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
|
|
||||||
{file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
|
|
||||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
|
{file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
|
||||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
|
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
|
||||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
|
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
|
||||||
@ -4622,22 +4632,23 @@ sympy = "*"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openai"
|
name = "openai"
|
||||||
version = "1.2.4"
|
version = "1.6.1"
|
||||||
description = "The official Python library for the openai API"
|
description = "The official Python library for the openai API"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7.1"
|
python-versions = ">=3.7.1"
|
||||||
files = [
|
files = [
|
||||||
{file = "openai-1.2.4-py3-none-any.whl", hash = "sha256:53927a2ca276eec0a0efdc1ae829f74a51f49b7d3e14cc6f820aeafb0abfd802"},
|
{file = "openai-1.6.1-py3-none-any.whl", hash = "sha256:bc9f774838d67ac29fb24cdeb2d58faf57de8b311085dcd1348f7aa02a96c7ee"},
|
||||||
{file = "openai-1.2.4.tar.gz", hash = "sha256:d99a474049376be431d9b4dec3a5c895dd76e19165748c5944e80b7905d1b1ff"},
|
{file = "openai-1.6.1.tar.gz", hash = "sha256:d553ca9dbf9486b08e75b09e8671e4f638462aaadccfced632bf490fc3d75fa2"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
anyio = ">=3.5.0,<4"
|
anyio = ">=3.5.0,<5"
|
||||||
distro = ">=1.7.0,<2"
|
distro = ">=1.7.0,<2"
|
||||||
httpx = ">=0.23.0,<1"
|
httpx = ">=0.23.0,<1"
|
||||||
pydantic = ">=1.9.0,<3"
|
pydantic = ">=1.9.0,<3"
|
||||||
|
sniffio = "*"
|
||||||
tqdm = ">4"
|
tqdm = ">4"
|
||||||
typing-extensions = ">=4.5,<5"
|
typing-extensions = ">=4.7,<5"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"]
|
datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"]
|
||||||
@ -6305,7 +6316,6 @@ files = [
|
|||||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
|
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
|
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
|
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
|
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
|
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
|
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
|
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
|
||||||
@ -6313,15 +6323,8 @@ files = [
|
|||||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
|
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
|
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
|
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
|
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
|
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
|
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
|
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
|
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
|
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
|
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
|
|
||||||
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
|
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
|
||||||
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
|
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
|
||||||
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
|
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
|
||||||
@ -6338,7 +6341,6 @@ files = [
|
|||||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
|
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
|
||||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
|
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
|
||||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
|
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
|
||||||
{file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
|
|
||||||
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
|
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
|
||||||
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
|
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
|
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
|
||||||
@ -6346,7 +6348,6 @@ files = [
|
|||||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
|
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
|
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
|
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
|
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
|
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
|
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
|
||||||
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
|
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
|
||||||
@ -7554,54 +7555,6 @@ description = "Database Abstraction Library"
|
|||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "SQLAlchemy-2.0.22-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f146c61ae128ab43ea3a0955de1af7e1633942c2b2b4985ac51cc292daf33222"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:875de9414393e778b655a3d97d60465eb3fae7c919e88b70cc10b40b9f56042d"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13790cb42f917c45c9c850b39b9941539ca8ee7917dacf099cc0b569f3d40da7"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e04ab55cf49daf1aeb8c622c54d23fa4bec91cb051a43cc24351ba97e1dd09f5"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a42c9fa3abcda0dcfad053e49c4f752eef71ecd8c155221e18b99d4224621176"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:14cd3bcbb853379fef2cd01e7c64a5d6f1d005406d877ed9509afb7a05ff40a5"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp310-cp310-win32.whl", hash = "sha256:d143c5a9dada696bcfdb96ba2de4a47d5a89168e71d05a076e88a01386872f97"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp310-cp310-win_amd64.whl", hash = "sha256:ccd87c25e4c8559e1b918d46b4fa90b37f459c9b4566f1dfbce0eb8122571547"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f6ff392b27a743c1ad346d215655503cec64405d3b694228b3454878bf21590"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f776c2c30f0e5f4db45c3ee11a5f2a8d9de68e81eb73ec4237de1e32e04ae81c"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8f1792d20d2f4e875ce7a113f43c3561ad12b34ff796b84002a256f37ce9437"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d80eeb5189d7d4b1af519fc3f148fe7521b9dfce8f4d6a0820e8f5769b005051"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:69fd9e41cf9368afa034e1c81f3570afb96f30fcd2eb1ef29cb4d9371c6eece2"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54bcceaf4eebef07dadfde424f5c26b491e4a64e61761dea9459103ecd6ccc95"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp311-cp311-win32.whl", hash = "sha256:7ee7ccf47aa503033b6afd57efbac6b9e05180f492aeed9fcf70752556f95624"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp311-cp311-win_amd64.whl", hash = "sha256:b560f075c151900587ade06706b0c51d04b3277c111151997ea0813455378ae0"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2c9bac865ee06d27a1533471405ad240a6f5d83195eca481f9fc4a71d8b87df8"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:625b72d77ac8ac23da3b1622e2da88c4aedaee14df47c8432bf8f6495e655de2"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b39a6e21110204a8c08d40ff56a73ba542ec60bab701c36ce721e7990df49fb9"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53a766cb0b468223cafdf63e2d37f14a4757476157927b09300c8c5832d88560"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0e1ce8ebd2e040357dde01a3fb7d30d9b5736b3e54a94002641dfd0aa12ae6ce"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:505f503763a767556fa4deae5194b2be056b64ecca72ac65224381a0acab7ebe"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp312-cp312-win32.whl", hash = "sha256:154a32f3c7b00de3d090bc60ec8006a78149e221f1182e3edcf0376016be9396"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp312-cp312-win_amd64.whl", hash = "sha256:129415f89744b05741c6f0b04a84525f37fbabe5dc3774f7edf100e7458c48cd"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3940677d341f2b685a999bffe7078697b5848a40b5f6952794ffcf3af150c301"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55914d45a631b81a8a2cb1a54f03eea265cf1783241ac55396ec6d735be14883"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2096d6b018d242a2bcc9e451618166f860bb0304f590d205173d317b69986c95"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:19c6986cf2fb4bc8e0e846f97f4135a8e753b57d2aaaa87c50f9acbe606bd1db"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6ac28bd6888fe3c81fbe97584eb0b96804bd7032d6100b9701255d9441373ec1"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp37-cp37m-win32.whl", hash = "sha256:cb9a758ad973e795267da334a92dd82bb7555cb36a0960dcabcf724d26299db8"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp37-cp37m-win_amd64.whl", hash = "sha256:40b1206a0d923e73aa54f0a6bd61419a96b914f1cd19900b6c8226899d9742ad"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3aa1472bf44f61dd27987cd051f1c893b7d3b17238bff8c23fceaef4f1133868"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:56a7e2bb639df9263bf6418231bc2a92a773f57886d371ddb7a869a24919face"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccca778c0737a773a1ad86b68bda52a71ad5950b25e120b6eb1330f0df54c3d0"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c6c3e9350f9fb16de5b5e5fbf17b578811a52d71bb784cc5ff71acb7de2a7f9"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:564e9f9e4e6466273dbfab0e0a2e5fe819eec480c57b53a2cdee8e4fdae3ad5f"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:af66001d7b76a3fab0d5e4c1ec9339ac45748bc4a399cbc2baa48c1980d3c1f4"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp38-cp38-win32.whl", hash = "sha256:9e55dff5ec115316dd7a083cdc1a52de63693695aecf72bc53a8e1468ce429e5"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp38-cp38-win_amd64.whl", hash = "sha256:4e869a8ff7ee7a833b74868a0887e8462445ec462432d8cbeff5e85f475186da"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9886a72c8e6371280cb247c5d32c9c8fa141dc560124348762db8a8b236f8692"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a571bc8ac092a3175a1d994794a8e7a1f2f651e7c744de24a19b4f740fe95034"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8db5ba8b7da759b727faebc4289a9e6a51edadc7fc32207a30f7c6203a181592"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b0b3f2686c3f162123adba3cb8b626ed7e9b8433ab528e36ed270b4f70d1cdb"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0c1fea8c0abcb070ffe15311853abfda4e55bf7dc1d4889497b3403629f3bf00"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4bb062784f37b2d75fd9b074c8ec360ad5df71f933f927e9e95c50eb8e05323c"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp39-cp39-win32.whl", hash = "sha256:58a3aba1bfb32ae7af68da3f277ed91d9f57620cf7ce651db96636790a78b736"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-cp39-cp39-win_amd64.whl", hash = "sha256:92e512a6af769e4725fa5b25981ba790335d42c5977e94ded07db7d641490a85"},
|
|
||||||
{file = "SQLAlchemy-2.0.22-py3-none-any.whl", hash = "sha256:3076740335e4aaadd7deb3fe6dcb96b3015f1613bd190a4e1634e1b99b02ec86"},
|
|
||||||
{file = "SQLAlchemy-2.0.22.tar.gz", hash = "sha256:5434cc601aa17570d79e5377f5fd45ff92f9379e2abed0be5e8c2fba8d353d2b"},
|
{file = "SQLAlchemy-2.0.22.tar.gz", hash = "sha256:5434cc601aa17570d79e5377f5fd45ff92f9379e2abed0be5e8c2fba8d353d2b"},
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -7611,7 +7564,7 @@ typing-extensions = ">=4.2.0"
|
|||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"]
|
aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"]
|
||||||
aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"]
|
aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"]
|
||||||
asyncio = ["greenlet (!=0.4.17)"]
|
asyncio = ["greenlet (!=0.4.17)"]
|
||||||
asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"]
|
asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"]
|
||||||
mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"]
|
mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"]
|
||||||
@ -7621,7 +7574,7 @@ mssql-pyodbc = ["pyodbc"]
|
|||||||
mypy = ["mypy (>=0.910)"]
|
mypy = ["mypy (>=0.910)"]
|
||||||
mysql = ["mysqlclient (>=1.4.0)"]
|
mysql = ["mysqlclient (>=1.4.0)"]
|
||||||
mysql-connector = ["mysql-connector-python"]
|
mysql-connector = ["mysql-connector-python"]
|
||||||
oracle = ["cx-oracle (>=7)"]
|
oracle = ["cx_oracle (>=7)"]
|
||||||
oracle-oracledb = ["oracledb (>=1.0.1)"]
|
oracle-oracledb = ["oracledb (>=1.0.1)"]
|
||||||
postgresql = ["psycopg2 (>=2.7)"]
|
postgresql = ["psycopg2 (>=2.7)"]
|
||||||
postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
|
postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
|
||||||
@ -7631,7 +7584,7 @@ postgresql-psycopg2binary = ["psycopg2-binary"]
|
|||||||
postgresql-psycopg2cffi = ["psycopg2cffi"]
|
postgresql-psycopg2cffi = ["psycopg2cffi"]
|
||||||
postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"]
|
postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"]
|
||||||
pymysql = ["pymysql"]
|
pymysql = ["pymysql"]
|
||||||
sqlcipher = ["sqlcipher3-binary"]
|
sqlcipher = ["sqlcipher3_binary"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlite-vss"
|
name = "sqlite-vss"
|
||||||
@ -7839,40 +7792,47 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tiktoken"
|
name = "tiktoken"
|
||||||
version = "0.3.3"
|
version = "0.5.2"
|
||||||
description = "tiktoken is a fast BPE tokeniser for use with OpenAI's models"
|
description = "tiktoken is a fast BPE tokeniser for use with OpenAI's models"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "tiktoken-0.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1f37fa75ba70c1bc7806641e8ccea1fba667d23e6341a1591ea333914c226a9"},
|
{file = "tiktoken-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c4e654282ef05ec1bd06ead22141a9a1687991cef2c6a81bdd1284301abc71d"},
|
||||||
{file = "tiktoken-0.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3d7296c38392a943c2ccc0b61323086b8550cef08dcf6855de9949890dbc1fd3"},
|
{file = "tiktoken-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7b3134aa24319f42c27718c6967f3c1916a38a715a0fa73d33717ba121231307"},
|
||||||
{file = "tiktoken-0.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c84491965e139a905280ac28b74baaa13445b3678e07f96767089ad1ef5ee7b"},
|
{file = "tiktoken-0.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6092e6e77730929c8c6a51bb0d7cfdf1b72b63c4d033d6258d1f2ee81052e9e5"},
|
||||||
{file = "tiktoken-0.3.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65970d77ea85ce6c7fce45131da9258cd58a802ffb29ead8f5552e331c025b2b"},
|
{file = "tiktoken-0.5.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ad8ae2a747622efae75837abba59be6c15a8f31b4ac3c6156bc56ec7a8e631"},
|
||||||
{file = "tiktoken-0.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bd3f72d0ba7312c25c1652292121a24c8f1711207b63c6d8dab21afe4be0bf04"},
|
{file = "tiktoken-0.5.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51cba7c8711afa0b885445f0637f0fcc366740798c40b981f08c5f984e02c9d1"},
|
||||||
{file = "tiktoken-0.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:719c9e13432602dc496b24f13e3c3ad3ec0d2fbdb9aace84abfb95e9c3a425a4"},
|
{file = "tiktoken-0.5.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3d8c7d2c9313f8e92e987d585ee2ba0f7c40a0de84f4805b093b634f792124f5"},
|
||||||
{file = "tiktoken-0.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:dc00772284c94e65045b984ed7e9f95d000034f6b2411df252011b069bd36217"},
|
{file = "tiktoken-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:692eca18c5fd8d1e0dde767f895c17686faaa102f37640e884eecb6854e7cca7"},
|
||||||
{file = "tiktoken-0.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db2c40f79f8f7a21a9fdbf1c6dee32dea77b0d7402355dc584a3083251d2e15"},
|
{file = "tiktoken-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:138d173abbf1ec75863ad68ca289d4da30caa3245f3c8d4bfb274c4d629a2f77"},
|
||||||
{file = "tiktoken-0.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3c0f2231aa3829a1a431a882201dc27858634fd9989898e0f7d991dbc6bcc9d"},
|
{file = "tiktoken-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7388fdd684690973fdc450b47dfd24d7f0cbe658f58a576169baef5ae4658607"},
|
||||||
{file = "tiktoken-0.3.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48c13186a479de16cfa2c72bb0631fa9c518350a5b7569e4d77590f7fee96be9"},
|
{file = "tiktoken-0.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a114391790113bcff670c70c24e166a841f7ea8f47ee2fe0e71e08b49d0bf2d4"},
|
||||||
{file = "tiktoken-0.3.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6674e4e37ab225020135cd66a392589623d5164c6456ba28cc27505abed10d9e"},
|
{file = "tiktoken-0.5.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca96f001e69f6859dd52926d950cfcc610480e920e576183497ab954e645e6ac"},
|
||||||
{file = "tiktoken-0.3.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4a0c1357f6191211c544f935d5aa3cb9d7abd118c8f3c7124196d5ecd029b4af"},
|
{file = "tiktoken-0.5.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:15fed1dd88e30dfadcdd8e53a8927f04e1f6f81ad08a5ca824858a593ab476c7"},
|
||||||
{file = "tiktoken-0.3.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2e948d167fc3b04483cbc33426766fd742e7cefe5346cd62b0cbd7279ef59539"},
|
{file = "tiktoken-0.5.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:93f8e692db5756f7ea8cb0cfca34638316dcf0841fb8469de8ed7f6a015ba0b0"},
|
||||||
{file = "tiktoken-0.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:5dca434c8680b987eacde2dbc449e9ea4526574dbf9f3d8938665f638095be82"},
|
{file = "tiktoken-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:bcae1c4c92df2ffc4fe9f475bf8148dbb0ee2404743168bbeb9dcc4b79dc1fdd"},
|
||||||
{file = "tiktoken-0.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:984758ebc07cd8c557345697c234f1f221bd730b388f4340dd08dffa50213a01"},
|
{file = "tiktoken-0.5.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b76a1e17d4eb4357d00f0622d9a48ffbb23401dcf36f9716d9bd9c8e79d421aa"},
|
||||||
{file = "tiktoken-0.3.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:891012f29e159a989541ae47259234fb29ff88c22e1097567316e27ad33a3734"},
|
{file = "tiktoken-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01d8b171bb5df4035580bc26d4f5339a6fd58d06f069091899d4a798ea279d3e"},
|
||||||
{file = "tiktoken-0.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:210f8602228e4c5d706deeb389da5a152b214966a5aa558eec87b57a1969ced5"},
|
{file = "tiktoken-0.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42adf7d4fb1ed8de6e0ff2e794a6a15005f056a0d83d22d1d6755a39bffd9e7f"},
|
||||||
{file = "tiktoken-0.3.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd783564f80d4dc44ff0a64b13756ded8390ed2548549aefadbe156af9188307"},
|
{file = "tiktoken-0.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c3f894dbe0adb44609f3d532b8ea10820d61fdcb288b325a458dfc60fefb7db"},
|
||||||
{file = "tiktoken-0.3.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:03f64bde9b4eb8338bf49c8532bfb4c3578f6a9a6979fc176d939f9e6f68b408"},
|
{file = "tiktoken-0.5.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:58ccfddb4e62f0df974e8f7e34a667981d9bb553a811256e617731bf1d007d19"},
|
||||||
{file = "tiktoken-0.3.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1ac369367b6f5e5bd80e8f9a7766ac2a9c65eda2aa856d5f3c556d924ff82986"},
|
{file = "tiktoken-0.5.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58902a8bad2de4268c2a701f1c844d22bfa3cbcc485b10e8e3e28a050179330b"},
|
||||||
{file = "tiktoken-0.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:94600798891f78db780e5aa9321456cf355e54a4719fbd554147a628de1f163f"},
|
{file = "tiktoken-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:5e39257826d0647fcac403d8fa0a474b30d02ec8ffc012cfaf13083e9b5e82c5"},
|
||||||
{file = "tiktoken-0.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e59db6fca8d5ccea302fe2888917364446d6f4201a25272a1a1c44975c65406a"},
|
{file = "tiktoken-0.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bde3b0fbf09a23072d39c1ede0e0821f759b4fa254a5f00078909158e90ae1f"},
|
||||||
{file = "tiktoken-0.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:19340d8ba4d6fd729b2e3a096a547ded85f71012843008f97475f9db484869ee"},
|
{file = "tiktoken-0.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2ddee082dcf1231ccf3a591d234935e6acf3e82ee28521fe99af9630bc8d2a60"},
|
||||||
{file = "tiktoken-0.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:542686cbc9225540e3a10f472f82fa2e1bebafce2233a211dee8459e95821cfd"},
|
{file = "tiktoken-0.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35c057a6a4e777b5966a7540481a75a31429fc1cb4c9da87b71c8b75b5143037"},
|
||||||
{file = "tiktoken-0.3.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a43612b2a09f4787c050163a216bf51123851859e9ab128ad03d2729826cde9"},
|
{file = "tiktoken-0.5.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c4a049b87e28f1dc60509f8eb7790bc8d11f9a70d99b9dd18dfdd81a084ffe6"},
|
||||||
{file = "tiktoken-0.3.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a11674f0275fa75fb59941b703650998bd4acb295adbd16fc8af17051aaed19d"},
|
{file = "tiktoken-0.5.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5bf5ce759089f4f6521ea6ed89d8f988f7b396e9f4afb503b945f5c949c6bec2"},
|
||||||
{file = "tiktoken-0.3.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:65fc0a449630bab28c30b4adec257442a4706d79cffc2337c1d9df3e91825cdd"},
|
{file = "tiktoken-0.5.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0c964f554af1a96884e01188f480dad3fc224c4bbcf7af75d4b74c4b74ae0125"},
|
||||||
{file = "tiktoken-0.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:0b9a7a9a8b781a50ee9289e85e28771d7e113cc0c656eadfb6fc6d3a106ff9bb"},
|
{file = "tiktoken-0.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:368dd5726d2e8788e47ea04f32e20f72a2012a8a67af5b0b003d1e059f1d30a3"},
|
||||||
{file = "tiktoken-0.3.3.tar.gz", hash = "sha256:97b58b7bfda945791ec855e53d166e8ec20c6378942b93851a6c919ddf9d0496"},
|
{file = "tiktoken-0.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a2deef9115b8cd55536c0a02c0203512f8deb2447f41585e6d929a0b878a0dd2"},
|
||||||
|
{file = "tiktoken-0.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2ed7d380195affbf886e2f8b92b14edfe13f4768ff5fc8de315adba5b773815e"},
|
||||||
|
{file = "tiktoken-0.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c76fce01309c8140ffe15eb34ded2bb94789614b7d1d09e206838fc173776a18"},
|
||||||
|
{file = "tiktoken-0.5.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60a5654d6a2e2d152637dd9a880b4482267dfc8a86ccf3ab1cec31a8c76bfae8"},
|
||||||
|
{file = "tiktoken-0.5.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:41d4d3228e051b779245a8ddd21d4336f8975563e92375662f42d05a19bdff41"},
|
||||||
|
{file = "tiktoken-0.5.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c1cdec2c92fcde8c17a50814b525ae6a88e8e5b02030dc120b76e11db93f13"},
|
||||||
|
{file = "tiktoken-0.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:84ddb36faedb448a50b246e13d1b6ee3437f60b7169b723a4b2abad75e914f3e"},
|
||||||
|
{file = "tiktoken-0.5.2.tar.gz", hash = "sha256:f54c581f134a8ea96ce2023ab221d4d4d81ab614efa0b2fbce926387deb56c80"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@ -9093,7 +9053,7 @@ cli = ["typer"]
|
|||||||
cohere = ["cohere"]
|
cohere = ["cohere"]
|
||||||
docarray = ["docarray"]
|
docarray = ["docarray"]
|
||||||
embeddings = ["sentence-transformers"]
|
embeddings = ["sentence-transformers"]
|
||||||
extended-testing = ["aiosqlite", "aleph-alpha-client", "anthropic", "arxiv", "assemblyai", "atlassian-python-api", "beautifulsoup4", "bibtexparser", "cassio", "chardet", "cohere", "couchbase", "dashvector", "databricks-vectorsearch", "datasets", "dgml-utils", "esprima", "faiss-cpu", "feedparser", "fireworks-ai", "geopandas", "gitpython", "google-cloud-documentai", "gql", "hologres-vector", "html2text", "javelin-sdk", "jinja2", "jq", "jsonschema", "lxml", "markdownify", "motor", "msal", "mwparserfromhell", "mwxml", "newspaper3k", "numexpr", "openai", "openai", "openapi-pydantic", "pandas", "pdfminer-six", "pgvector", "praw", "psychicapi", "py-trello", "pymupdf", "pypdf", "pypdfium2", "pyspark", "rank-bm25", "rapidfuzz", "rapidocr-onnxruntime", "requests-toolbelt", "rspace_client", "scikit-learn", "sqlite-vss", "streamlit", "sympy", "telethon", "timescale-vector", "tqdm", "upstash-redis", "xata", "xmltodict"]
|
extended-testing = ["aiosqlite", "aleph-alpha-client", "anthropic", "arxiv", "assemblyai", "atlassian-python-api", "beautifulsoup4", "bibtexparser", "cassio", "chardet", "cohere", "couchbase", "dashvector", "databricks-vectorsearch", "datasets", "dgml-utils", "esprima", "faiss-cpu", "feedparser", "fireworks-ai", "geopandas", "gitpython", "google-cloud-documentai", "gql", "hologres-vector", "html2text", "javelin-sdk", "jinja2", "jq", "jsonschema", "langchain-openai", "lxml", "markdownify", "motor", "msal", "mwparserfromhell", "mwxml", "newspaper3k", "numexpr", "openai", "openai", "openapi-pydantic", "pandas", "pdfminer-six", "pgvector", "praw", "psychicapi", "py-trello", "pymupdf", "pypdf", "pypdfium2", "pyspark", "rank-bm25", "rapidfuzz", "rapidocr-onnxruntime", "requests-toolbelt", "rspace_client", "scikit-learn", "sqlite-vss", "streamlit", "sympy", "telethon", "timescale-vector", "tqdm", "upstash-redis", "xata", "xmltodict"]
|
||||||
javascript = ["esprima"]
|
javascript = ["esprima"]
|
||||||
llms = ["clarifai", "cohere", "huggingface_hub", "manifest-ml", "nlpcloud", "openai", "openlm", "torch", "transformers"]
|
llms = ["clarifai", "cohere", "huggingface_hub", "manifest-ml", "nlpcloud", "openai", "openlm", "torch", "transformers"]
|
||||||
openai = ["openai", "tiktoken"]
|
openai = ["openai", "tiktoken"]
|
||||||
@ -9103,4 +9063,4 @@ text-helpers = ["chardet"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = ">=3.8.1,<4.0"
|
python-versions = ">=3.8.1,<4.0"
|
||||||
content-hash = "00dbfc7d9700a8ad488f42c4100abf615067e873bb6593b13a34738248606e83"
|
content-hash = "aa9f54772221cb8f6faa71e643fab30fc72761366b65cde9fd408e424478d77a"
|
||||||
|
@ -110,6 +110,7 @@ databricks-vectorsearch = {version = "^0.21", optional = true}
|
|||||||
couchbase = {version = "^4.1.9", optional = true}
|
couchbase = {version = "^4.1.9", optional = true}
|
||||||
dgml-utils = {version = "^0.3.0", optional = true}
|
dgml-utils = {version = "^0.3.0", optional = true}
|
||||||
datasets = {version = "^2.15.0", optional = true}
|
datasets = {version = "^2.15.0", optional = true}
|
||||||
|
langchain-openai = {path = "../partners/openai", optional = true}
|
||||||
|
|
||||||
[tool.poetry.group.test]
|
[tool.poetry.group.test]
|
||||||
optional = true
|
optional = true
|
||||||
@ -164,7 +165,7 @@ wrapt = "^1.15.0"
|
|||||||
openai = "^1"
|
openai = "^1"
|
||||||
python-dotenv = "^1.0.0"
|
python-dotenv = "^1.0.0"
|
||||||
cassio = "^0.1.0"
|
cassio = "^0.1.0"
|
||||||
tiktoken = "^0.3.2"
|
tiktoken = ">=0.3.2,<0.6.0"
|
||||||
anthropic = "^0.3.11"
|
anthropic = "^0.3.11"
|
||||||
langchain-core = {path = "../core", develop = true}
|
langchain-core = {path = "../core", develop = true}
|
||||||
langchain-community = {path = "../community", develop = true}
|
langchain-community = {path = "../community", develop = true}
|
||||||
@ -294,6 +295,7 @@ extended_testing = [
|
|||||||
"couchbase",
|
"couchbase",
|
||||||
"dgml-utils",
|
"dgml-utils",
|
||||||
"cohere",
|
"cohere",
|
||||||
|
"langchain-openai",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Test for Serializable base class"""
|
"""Test for Serializable base class"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from langchain_community.llms.openai import OpenAI
|
from langchain_community.llms.openai import OpenAI as CommunityOpenAI
|
||||||
from langchain_core.load.dump import dumpd, dumps
|
from langchain_core.load.dump import dumpd, dumps
|
||||||
from langchain_core.load.load import load, loads
|
from langchain_core.load.load import load, loads
|
||||||
from langchain_core.prompts.prompt import PromptTemplate
|
from langchain_core.prompts.prompt import PromptTemplate
|
||||||
@ -13,20 +13,25 @@ class NotSerializable:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.requires("openai")
|
@pytest.mark.requires("openai", "langchain_openai")
|
||||||
def test_loads_openai_llm() -> None:
|
def test_loads_openai_llm() -> None:
|
||||||
llm = OpenAI(model="davinci", temperature=0.5, openai_api_key="hello")
|
from langchain_openai import OpenAI
|
||||||
|
|
||||||
|
llm = CommunityOpenAI(model="davinci", temperature=0.5, openai_api_key="hello")
|
||||||
llm_string = dumps(llm)
|
llm_string = dumps(llm)
|
||||||
llm2 = loads(llm_string, secrets_map={"OPENAI_API_KEY": "hello"})
|
llm2 = loads(llm_string, secrets_map={"OPENAI_API_KEY": "hello"})
|
||||||
|
|
||||||
assert llm2 == llm
|
assert llm2 == llm
|
||||||
assert dumps(llm2) == llm_string
|
llm_string_2 = dumps(llm2)
|
||||||
|
assert llm_string_2 == llm_string
|
||||||
assert isinstance(llm2, OpenAI)
|
assert isinstance(llm2, OpenAI)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.requires("openai")
|
@pytest.mark.requires("openai", "langchain_openai")
|
||||||
def test_loads_llmchain() -> None:
|
def test_loads_llmchain() -> None:
|
||||||
llm = OpenAI(model="davinci", temperature=0.5, openai_api_key="hello")
|
from langchain_openai import OpenAI
|
||||||
|
|
||||||
|
llm = CommunityOpenAI(model="davinci", temperature=0.5, openai_api_key="hello")
|
||||||
prompt = PromptTemplate.from_template("hello {name}!")
|
prompt = PromptTemplate.from_template("hello {name}!")
|
||||||
chain = LLMChain(llm=llm, prompt=prompt)
|
chain = LLMChain(llm=llm, prompt=prompt)
|
||||||
chain_string = dumps(chain)
|
chain_string = dumps(chain)
|
||||||
@ -39,10 +44,12 @@ def test_loads_llmchain() -> None:
|
|||||||
assert isinstance(chain2.prompt, PromptTemplate)
|
assert isinstance(chain2.prompt, PromptTemplate)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.requires("openai")
|
@pytest.mark.requires("openai", "langchain_openai")
|
||||||
def test_loads_llmchain_env() -> None:
|
def test_loads_llmchain_env() -> None:
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from langchain_openai import OpenAI
|
||||||
|
|
||||||
has_env = "OPENAI_API_KEY" in os.environ
|
has_env = "OPENAI_API_KEY" in os.environ
|
||||||
if not has_env:
|
if not has_env:
|
||||||
os.environ["OPENAI_API_KEY"] = "env_variable"
|
os.environ["OPENAI_API_KEY"] = "env_variable"
|
||||||
@ -65,7 +72,7 @@ def test_loads_llmchain_env() -> None:
|
|||||||
|
|
||||||
@pytest.mark.requires("openai")
|
@pytest.mark.requires("openai")
|
||||||
def test_loads_llmchain_with_non_serializable_arg() -> None:
|
def test_loads_llmchain_with_non_serializable_arg() -> None:
|
||||||
llm = OpenAI(
|
llm = CommunityOpenAI(
|
||||||
model="davinci",
|
model="davinci",
|
||||||
temperature=0.5,
|
temperature=0.5,
|
||||||
openai_api_key="hello",
|
openai_api_key="hello",
|
||||||
@ -78,9 +85,11 @@ def test_loads_llmchain_with_non_serializable_arg() -> None:
|
|||||||
loads(chain_string, secrets_map={"OPENAI_API_KEY": "hello"})
|
loads(chain_string, secrets_map={"OPENAI_API_KEY": "hello"})
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.requires("openai")
|
@pytest.mark.requires("openai", "langchain_openai")
|
||||||
def test_load_openai_llm() -> None:
|
def test_load_openai_llm() -> None:
|
||||||
llm = OpenAI(model="davinci", temperature=0.5, openai_api_key="hello")
|
from langchain_openai import OpenAI
|
||||||
|
|
||||||
|
llm = CommunityOpenAI(model="davinci", temperature=0.5, openai_api_key="hello")
|
||||||
llm_obj = dumpd(llm)
|
llm_obj = dumpd(llm)
|
||||||
llm2 = load(llm_obj, secrets_map={"OPENAI_API_KEY": "hello"})
|
llm2 = load(llm_obj, secrets_map={"OPENAI_API_KEY": "hello"})
|
||||||
|
|
||||||
@ -89,9 +98,11 @@ def test_load_openai_llm() -> None:
|
|||||||
assert isinstance(llm2, OpenAI)
|
assert isinstance(llm2, OpenAI)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.requires("openai")
|
@pytest.mark.requires("openai", "langchain_openai")
|
||||||
def test_load_llmchain() -> None:
|
def test_load_llmchain() -> None:
|
||||||
llm = OpenAI(model="davinci", temperature=0.5, openai_api_key="hello")
|
from langchain_openai import OpenAI
|
||||||
|
|
||||||
|
llm = CommunityOpenAI(model="davinci", temperature=0.5, openai_api_key="hello")
|
||||||
prompt = PromptTemplate.from_template("hello {name}!")
|
prompt = PromptTemplate.from_template("hello {name}!")
|
||||||
chain = LLMChain(llm=llm, prompt=prompt)
|
chain = LLMChain(llm=llm, prompt=prompt)
|
||||||
chain_obj = dumpd(chain)
|
chain_obj = dumpd(chain)
|
||||||
@ -104,15 +115,17 @@ def test_load_llmchain() -> None:
|
|||||||
assert isinstance(chain2.prompt, PromptTemplate)
|
assert isinstance(chain2.prompt, PromptTemplate)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.requires("openai")
|
@pytest.mark.requires("openai", "langchain_openai")
|
||||||
def test_load_llmchain_env() -> None:
|
def test_load_llmchain_env() -> None:
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from langchain_openai import OpenAI
|
||||||
|
|
||||||
has_env = "OPENAI_API_KEY" in os.environ
|
has_env = "OPENAI_API_KEY" in os.environ
|
||||||
if not has_env:
|
if not has_env:
|
||||||
os.environ["OPENAI_API_KEY"] = "env_variable"
|
os.environ["OPENAI_API_KEY"] = "env_variable"
|
||||||
|
|
||||||
llm = OpenAI(model="davinci", temperature=0.5)
|
llm = CommunityOpenAI(model="davinci", temperature=0.5)
|
||||||
prompt = PromptTemplate.from_template("hello {name}!")
|
prompt = PromptTemplate.from_template("hello {name}!")
|
||||||
chain = LLMChain(llm=llm, prompt=prompt)
|
chain = LLMChain(llm=llm, prompt=prompt)
|
||||||
chain_obj = dumpd(chain)
|
chain_obj = dumpd(chain)
|
||||||
@ -130,7 +143,7 @@ def test_load_llmchain_env() -> None:
|
|||||||
|
|
||||||
@pytest.mark.requires("openai")
|
@pytest.mark.requires("openai")
|
||||||
def test_load_llmchain_with_non_serializable_arg() -> None:
|
def test_load_llmchain_with_non_serializable_arg() -> None:
|
||||||
llm = OpenAI(
|
llm = CommunityOpenAI(
|
||||||
model="davinci",
|
model="davinci",
|
||||||
temperature=0.5,
|
temperature=0.5,
|
||||||
openai_api_key="hello",
|
openai_api_key="hello",
|
||||||
|
1
libs/partners/openai/.gitignore
vendored
Normal file
1
libs/partners/openai/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
__pycache__
|
21
libs/partners/openai/LICENSE
Normal file
21
libs/partners/openai/LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 LangChain, Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
59
libs/partners/openai/Makefile
Normal file
59
libs/partners/openai/Makefile
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
.PHONY: all format lint test tests integration_tests docker_tests help extended_tests
|
||||||
|
|
||||||
|
# Default target executed when no arguments are given to make.
|
||||||
|
all: help
|
||||||
|
|
||||||
|
# Define a variable for the test file path.
|
||||||
|
TEST_FILE ?= tests/unit_tests/
|
||||||
|
|
||||||
|
test:
|
||||||
|
poetry run pytest $(TEST_FILE)
|
||||||
|
|
||||||
|
tests:
|
||||||
|
poetry run pytest $(TEST_FILE)
|
||||||
|
|
||||||
|
|
||||||
|
######################
|
||||||
|
# LINTING AND FORMATTING
|
||||||
|
######################
|
||||||
|
|
||||||
|
# Define a variable for Python and notebook files.
|
||||||
|
PYTHON_FILES=.
|
||||||
|
MYPY_CACHE=.mypy_cache
|
||||||
|
lint format: PYTHON_FILES=.
|
||||||
|
lint_diff format_diff: PYTHON_FILES=$(shell git diff --relative=libs/partners/openai --name-only --diff-filter=d master | grep -E '\.py$$|\.ipynb$$')
|
||||||
|
lint_package: PYTHON_FILES=langchain_openai
|
||||||
|
lint_tests: PYTHON_FILES=tests
|
||||||
|
lint_tests: MYPY_CACHE=.mypy_cache_test
|
||||||
|
|
||||||
|
lint lint_diff lint_package lint_tests:
|
||||||
|
poetry run ruff .
|
||||||
|
poetry run ruff format $(PYTHON_FILES) --diff
|
||||||
|
poetry run ruff --select I $(PYTHON_FILES)
|
||||||
|
mkdir $(MYPY_CACHE); poetry run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
|
||||||
|
|
||||||
|
format format_diff:
|
||||||
|
poetry run ruff format $(PYTHON_FILES)
|
||||||
|
poetry run ruff --select I --fix $(PYTHON_FILES)
|
||||||
|
|
||||||
|
spell_check:
|
||||||
|
poetry run codespell --toml pyproject.toml
|
||||||
|
|
||||||
|
spell_fix:
|
||||||
|
poetry run codespell --toml pyproject.toml -w
|
||||||
|
|
||||||
|
check_imports: $(shell find langchain_openai -name '*.py')
|
||||||
|
poetry run python ./scripts/check_imports.py $^
|
||||||
|
|
||||||
|
######################
|
||||||
|
# HELP
|
||||||
|
######################
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo '----'
|
||||||
|
@echo 'check_imports - check imports'
|
||||||
|
@echo 'format - run code formatters'
|
||||||
|
@echo 'lint - run linters'
|
||||||
|
@echo 'test - run unit tests'
|
||||||
|
@echo 'tests - run unit tests'
|
||||||
|
@echo 'test TEST_FILE=<test_file> - run all tests in file'
|
1
libs/partners/openai/README.md
Normal file
1
libs/partners/openai/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
# langchain-openai
|
18
libs/partners/openai/langchain_openai/__init__.py
Normal file
18
libs/partners/openai/langchain_openai/__init__.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from langchain_openai.chat_models import (
|
||||||
|
AzureChatOpenAI,
|
||||||
|
ChatOpenAI,
|
||||||
|
)
|
||||||
|
from langchain_openai.embeddings import (
|
||||||
|
AzureOpenAIEmbeddings,
|
||||||
|
OpenAIEmbeddings,
|
||||||
|
)
|
||||||
|
from langchain_openai.llms import AzureOpenAI, OpenAI
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"OpenAI",
|
||||||
|
"ChatOpenAI",
|
||||||
|
"OpenAIEmbeddings",
|
||||||
|
"AzureOpenAI",
|
||||||
|
"AzureChatOpenAI",
|
||||||
|
"AzureOpenAIEmbeddings",
|
||||||
|
]
|
@ -0,0 +1,7 @@
|
|||||||
|
from langchain_openai.chat_models.azure import AzureChatOpenAI
|
||||||
|
from langchain_openai.chat_models.base import ChatOpenAI
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ChatOpenAI",
|
||||||
|
"AzureChatOpenAI",
|
||||||
|
]
|
218
libs/partners/openai/langchain_openai/chat_models/azure.py
Normal file
218
libs/partners/openai/langchain_openai/chat_models/azure.py
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
"""Azure OpenAI chat wrapper."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from typing import Any, Callable, Dict, List, Union
|
||||||
|
|
||||||
|
import openai
|
||||||
|
from langchain_core.outputs import ChatResult
|
||||||
|
from langchain_core.pydantic_v1 import BaseModel, Field, root_validator
|
||||||
|
from langchain_core.utils import get_from_dict_or_env
|
||||||
|
|
||||||
|
from langchain_openai.chat_models.base import ChatOpenAI
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AzureChatOpenAI(ChatOpenAI):
|
||||||
|
"""`Azure OpenAI` Chat Completion API.
|
||||||
|
|
||||||
|
To use this class you
|
||||||
|
must have a deployed model on Azure OpenAI. Use `deployment_name` in the
|
||||||
|
constructor to refer to the "Model deployment name" in the Azure portal.
|
||||||
|
|
||||||
|
In addition, you should have the
|
||||||
|
following environment variables set or passed in constructor in lower case:
|
||||||
|
- ``AZURE_OPENAI_API_KEY``
|
||||||
|
- ``AZURE_OPENAI_ENDPOINT``
|
||||||
|
- ``AZURE_OPENAI_AD_TOKEN``
|
||||||
|
- ``OPENAI_API_VERSION``
|
||||||
|
- ``OPENAI_PROXY``
|
||||||
|
|
||||||
|
For example, if you have `gpt-3.5-turbo` deployed, with the deployment name
|
||||||
|
`35-turbo-dev`, the constructor should look like:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
AzureChatOpenAI(
|
||||||
|
azure_deployment="35-turbo-dev",
|
||||||
|
openai_api_version="2023-05-15",
|
||||||
|
)
|
||||||
|
|
||||||
|
Be aware the API version may change.
|
||||||
|
|
||||||
|
You can also specify the version of the model using ``model_version`` constructor
|
||||||
|
parameter, as Azure OpenAI doesn't return model version with the response.
|
||||||
|
|
||||||
|
Default is empty. When you specify the version, it will be appended to the
|
||||||
|
model name in the response. Setting correct version will help you to calculate the
|
||||||
|
cost properly. Model version is not validated, so make sure you set it correctly
|
||||||
|
to get the correct cost.
|
||||||
|
|
||||||
|
Any parameters that are valid to be passed to the openai.create call can be passed
|
||||||
|
in, even if not explicitly saved on this class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
azure_endpoint: Union[str, None] = None
|
||||||
|
"""Your Azure endpoint, including the resource.
|
||||||
|
|
||||||
|
Automatically inferred from env var `AZURE_OPENAI_ENDPOINT` if not provided.
|
||||||
|
|
||||||
|
Example: `https://example-resource.azure.openai.com/`
|
||||||
|
"""
|
||||||
|
deployment_name: Union[str, None] = Field(default=None, alias="azure_deployment")
|
||||||
|
"""A model deployment.
|
||||||
|
|
||||||
|
If given sets the base client URL to include `/deployments/{azure_deployment}`.
|
||||||
|
Note: this means you won't be able to use non-deployment endpoints.
|
||||||
|
"""
|
||||||
|
openai_api_version: str = Field(default="", alias="api_version")
|
||||||
|
"""Automatically inferred from env var `OPENAI_API_VERSION` if not provided."""
|
||||||
|
openai_api_key: Union[str, None] = Field(default=None, alias="api_key")
|
||||||
|
"""Automatically inferred from env var `AZURE_OPENAI_API_KEY` if not provided."""
|
||||||
|
azure_ad_token: Union[str, None] = None
|
||||||
|
"""Your Azure Active Directory token.
|
||||||
|
|
||||||
|
Automatically inferred from env var `AZURE_OPENAI_AD_TOKEN` if not provided.
|
||||||
|
|
||||||
|
For more:
|
||||||
|
https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-id.
|
||||||
|
""" # noqa: E501
|
||||||
|
azure_ad_token_provider: Union[Callable[[], str], None] = None
|
||||||
|
"""A function that returns an Azure Active Directory token.
|
||||||
|
|
||||||
|
Will be invoked on every request.
|
||||||
|
"""
|
||||||
|
model_version: str = ""
|
||||||
|
"""Legacy, for openai<1.0.0 support."""
|
||||||
|
openai_api_type: str = ""
|
||||||
|
"""Legacy, for openai<1.0.0 support."""
|
||||||
|
validate_base_url: bool = True
|
||||||
|
"""For backwards compatibility. If legacy val openai_api_base is passed in, try to
|
||||||
|
infer if it is a base_url or azure_endpoint and update accordingly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_lc_namespace(cls) -> List[str]:
|
||||||
|
"""Get the namespace of the langchain object."""
|
||||||
|
return ["langchain", "chat_models", "azure_openai"]
|
||||||
|
|
||||||
|
@root_validator()
|
||||||
|
def validate_environment(cls, values: Dict) -> Dict:
|
||||||
|
"""Validate that api key and python package exists in environment."""
|
||||||
|
if values["n"] < 1:
|
||||||
|
raise ValueError("n must be at least 1.")
|
||||||
|
if values["n"] > 1 and values["streaming"]:
|
||||||
|
raise ValueError("n must be 1 when streaming.")
|
||||||
|
|
||||||
|
# Check OPENAI_KEY for backwards compatibility.
|
||||||
|
# TODO: Remove OPENAI_API_KEY support to avoid possible conflict when using
|
||||||
|
# other forms of azure credentials.
|
||||||
|
values["openai_api_key"] = (
|
||||||
|
values["openai_api_key"]
|
||||||
|
or os.getenv("AZURE_OPENAI_API_KEY")
|
||||||
|
or os.getenv("OPENAI_API_KEY")
|
||||||
|
)
|
||||||
|
values["openai_api_base"] = values["openai_api_base"] or os.getenv(
|
||||||
|
"OPENAI_API_BASE"
|
||||||
|
)
|
||||||
|
values["openai_api_version"] = values["openai_api_version"] or os.getenv(
|
||||||
|
"OPENAI_API_VERSION"
|
||||||
|
)
|
||||||
|
# Check OPENAI_ORGANIZATION for backwards compatibility.
|
||||||
|
values["openai_organization"] = (
|
||||||
|
values["openai_organization"]
|
||||||
|
or os.getenv("OPENAI_ORG_ID")
|
||||||
|
or os.getenv("OPENAI_ORGANIZATION")
|
||||||
|
)
|
||||||
|
values["azure_endpoint"] = values["azure_endpoint"] or os.getenv(
|
||||||
|
"AZURE_OPENAI_ENDPOINT"
|
||||||
|
)
|
||||||
|
values["azure_ad_token"] = values["azure_ad_token"] or os.getenv(
|
||||||
|
"AZURE_OPENAI_AD_TOKEN"
|
||||||
|
)
|
||||||
|
|
||||||
|
values["openai_api_type"] = get_from_dict_or_env(
|
||||||
|
values, "openai_api_type", "OPENAI_API_TYPE", default="azure"
|
||||||
|
)
|
||||||
|
values["openai_proxy"] = get_from_dict_or_env(
|
||||||
|
values, "openai_proxy", "OPENAI_PROXY", default=""
|
||||||
|
)
|
||||||
|
# For backwards compatibility. Before openai v1, no distinction was made
|
||||||
|
# between azure_endpoint and base_url (openai_api_base).
|
||||||
|
openai_api_base = values["openai_api_base"]
|
||||||
|
if openai_api_base and values["validate_base_url"]:
|
||||||
|
if "/openai" not in openai_api_base:
|
||||||
|
raise ValueError(
|
||||||
|
"As of openai>=1.0.0, Azure endpoints should be specified via "
|
||||||
|
"the `azure_endpoint` param not `openai_api_base` "
|
||||||
|
"(or alias `base_url`)."
|
||||||
|
)
|
||||||
|
if values["deployment_name"]:
|
||||||
|
raise ValueError(
|
||||||
|
"As of openai>=1.0.0, if `deployment_name` (or alias "
|
||||||
|
"`azure_deployment`) is specified then "
|
||||||
|
"`openai_api_base` (or alias `base_url`) should not be. "
|
||||||
|
"Instead use `deployment_name` (or alias `azure_deployment`) "
|
||||||
|
"and `azure_endpoint`."
|
||||||
|
)
|
||||||
|
client_params = {
|
||||||
|
"api_version": values["openai_api_version"],
|
||||||
|
"azure_endpoint": values["azure_endpoint"],
|
||||||
|
"azure_deployment": values["deployment_name"],
|
||||||
|
"api_key": values["openai_api_key"],
|
||||||
|
"azure_ad_token": values["azure_ad_token"],
|
||||||
|
"azure_ad_token_provider": values["azure_ad_token_provider"],
|
||||||
|
"organization": values["openai_organization"],
|
||||||
|
"base_url": values["openai_api_base"],
|
||||||
|
"timeout": values["request_timeout"],
|
||||||
|
"max_retries": values["max_retries"],
|
||||||
|
"default_headers": values["default_headers"],
|
||||||
|
"default_query": values["default_query"],
|
||||||
|
"http_client": values["http_client"],
|
||||||
|
}
|
||||||
|
values["client"] = openai.AzureOpenAI(**client_params).chat.completions
|
||||||
|
values["async_client"] = openai.AsyncAzureOpenAI(
|
||||||
|
**client_params
|
||||||
|
).chat.completions
|
||||||
|
return values
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _identifying_params(self) -> Dict[str, Any]:
|
||||||
|
"""Get the identifying parameters."""
|
||||||
|
return {**self._default_params}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _llm_type(self) -> str:
|
||||||
|
return "azure-openai-chat"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lc_attributes(self) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"openai_api_type": self.openai_api_type,
|
||||||
|
"openai_api_version": self.openai_api_version,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _create_chat_result(self, response: Union[dict, BaseModel]) -> ChatResult:
|
||||||
|
if not isinstance(response, dict):
|
||||||
|
response = response.dict()
|
||||||
|
for res in response["choices"]:
|
||||||
|
if res.get("finish_reason", None) == "content_filter":
|
||||||
|
raise ValueError(
|
||||||
|
"Azure has not provided the response due to a content filter "
|
||||||
|
"being triggered"
|
||||||
|
)
|
||||||
|
chat_result = super()._create_chat_result(response)
|
||||||
|
|
||||||
|
if "model" in response:
|
||||||
|
model = response["model"]
|
||||||
|
if self.model_version:
|
||||||
|
model = f"{model}-{self.model_version}"
|
||||||
|
|
||||||
|
if chat_result.llm_output is not None and isinstance(
|
||||||
|
chat_result.llm_output, dict
|
||||||
|
):
|
||||||
|
chat_result.llm_output["model_name"] = model
|
||||||
|
|
||||||
|
return chat_result
|
655
libs/partners/openai/langchain_openai/chat_models/base.py
Normal file
655
libs/partners/openai/langchain_openai/chat_models/base.py
Normal file
@ -0,0 +1,655 @@
|
|||||||
|
"""OpenAI chat wrapper."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import warnings
|
||||||
|
from typing import (
|
||||||
|
Any,
|
||||||
|
AsyncIterator,
|
||||||
|
Callable,
|
||||||
|
Dict,
|
||||||
|
Iterator,
|
||||||
|
List,
|
||||||
|
Mapping,
|
||||||
|
Optional,
|
||||||
|
Sequence,
|
||||||
|
Tuple,
|
||||||
|
Type,
|
||||||
|
Union,
|
||||||
|
cast,
|
||||||
|
)
|
||||||
|
|
||||||
|
import openai
|
||||||
|
import tiktoken
|
||||||
|
from langchain_core.callbacks import (
|
||||||
|
AsyncCallbackManagerForLLMRun,
|
||||||
|
CallbackManagerForLLMRun,
|
||||||
|
)
|
||||||
|
from langchain_core.language_models import LanguageModelInput
|
||||||
|
from langchain_core.language_models.chat_models import (
|
||||||
|
BaseChatModel,
|
||||||
|
agenerate_from_stream,
|
||||||
|
generate_from_stream,
|
||||||
|
)
|
||||||
|
from langchain_core.messages import (
|
||||||
|
AIMessage,
|
||||||
|
AIMessageChunk,
|
||||||
|
BaseMessage,
|
||||||
|
BaseMessageChunk,
|
||||||
|
ChatMessage,
|
||||||
|
ChatMessageChunk,
|
||||||
|
FunctionMessage,
|
||||||
|
FunctionMessageChunk,
|
||||||
|
HumanMessage,
|
||||||
|
HumanMessageChunk,
|
||||||
|
SystemMessage,
|
||||||
|
SystemMessageChunk,
|
||||||
|
ToolMessage,
|
||||||
|
ToolMessageChunk,
|
||||||
|
)
|
||||||
|
from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult
|
||||||
|
from langchain_core.pydantic_v1 import BaseModel, Field, root_validator
|
||||||
|
from langchain_core.runnables import Runnable
|
||||||
|
from langchain_core.utils import (
|
||||||
|
get_from_dict_or_env,
|
||||||
|
get_pydantic_field_names,
|
||||||
|
)
|
||||||
|
from langchain_core.utils.function_calling import convert_to_openai_function
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_dict_to_message(_dict: Mapping[str, Any]) -> BaseMessage:
|
||||||
|
"""Convert a dictionary to a LangChain message.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
_dict: The dictionary.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The LangChain message.
|
||||||
|
"""
|
||||||
|
role = _dict.get("role")
|
||||||
|
if role == "user":
|
||||||
|
return HumanMessage(content=_dict.get("content", ""))
|
||||||
|
elif role == "assistant":
|
||||||
|
# Fix for azure
|
||||||
|
# Also OpenAI returns None for tool invocations
|
||||||
|
content = _dict.get("content", "") or ""
|
||||||
|
additional_kwargs: Dict = {}
|
||||||
|
if function_call := _dict.get("function_call"):
|
||||||
|
additional_kwargs["function_call"] = dict(function_call)
|
||||||
|
if tool_calls := _dict.get("tool_calls"):
|
||||||
|
additional_kwargs["tool_calls"] = tool_calls
|
||||||
|
return AIMessage(content=content, additional_kwargs=additional_kwargs)
|
||||||
|
elif role == "system":
|
||||||
|
return SystemMessage(content=_dict.get("content", ""))
|
||||||
|
elif role == "function":
|
||||||
|
return FunctionMessage(content=_dict.get("content", ""), name=_dict.get("name"))
|
||||||
|
elif role == "tool":
|
||||||
|
additional_kwargs = {}
|
||||||
|
if "name" in _dict:
|
||||||
|
additional_kwargs["name"] = _dict["name"]
|
||||||
|
return ToolMessage(
|
||||||
|
content=_dict.get("content", ""),
|
||||||
|
tool_call_id=_dict.get("tool_call_id"),
|
||||||
|
additional_kwargs=additional_kwargs,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return ChatMessage(content=_dict.get("content", ""), role=role)
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_message_to_dict(message: BaseMessage) -> dict:
|
||||||
|
"""Convert a LangChain message to a dictionary.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: The LangChain message.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The dictionary.
|
||||||
|
"""
|
||||||
|
message_dict: Dict[str, Any]
|
||||||
|
if isinstance(message, ChatMessage):
|
||||||
|
message_dict = {"role": message.role, "content": message.content}
|
||||||
|
elif isinstance(message, HumanMessage):
|
||||||
|
message_dict = {"role": "user", "content": message.content}
|
||||||
|
elif isinstance(message, AIMessage):
|
||||||
|
message_dict = {"role": "assistant", "content": message.content}
|
||||||
|
if "function_call" in message.additional_kwargs:
|
||||||
|
message_dict["function_call"] = message.additional_kwargs["function_call"]
|
||||||
|
# If function call only, content is None not empty string
|
||||||
|
if message_dict["content"] == "":
|
||||||
|
message_dict["content"] = None
|
||||||
|
if "tool_calls" in message.additional_kwargs:
|
||||||
|
message_dict["tool_calls"] = message.additional_kwargs["tool_calls"]
|
||||||
|
# If tool calls only, content is None not empty string
|
||||||
|
if message_dict["content"] == "":
|
||||||
|
message_dict["content"] = None
|
||||||
|
elif isinstance(message, SystemMessage):
|
||||||
|
message_dict = {"role": "system", "content": message.content}
|
||||||
|
elif isinstance(message, FunctionMessage):
|
||||||
|
message_dict = {
|
||||||
|
"role": "function",
|
||||||
|
"content": message.content,
|
||||||
|
"name": message.name,
|
||||||
|
}
|
||||||
|
elif isinstance(message, ToolMessage):
|
||||||
|
message_dict = {
|
||||||
|
"role": "tool",
|
||||||
|
"content": message.content,
|
||||||
|
"tool_call_id": message.tool_call_id,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise TypeError(f"Got unknown type {message}")
|
||||||
|
if "name" in message.additional_kwargs:
|
||||||
|
message_dict["name"] = message.additional_kwargs["name"]
|
||||||
|
return message_dict
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_delta_to_message_chunk(
|
||||||
|
_dict: Mapping[str, Any], default_class: Type[BaseMessageChunk]
|
||||||
|
) -> BaseMessageChunk:
|
||||||
|
role = cast(str, _dict.get("role"))
|
||||||
|
content = cast(str, _dict.get("content") or "")
|
||||||
|
additional_kwargs: Dict = {}
|
||||||
|
if _dict.get("function_call"):
|
||||||
|
function_call = dict(_dict["function_call"])
|
||||||
|
if "name" in function_call and function_call["name"] is None:
|
||||||
|
function_call["name"] = ""
|
||||||
|
additional_kwargs["function_call"] = function_call
|
||||||
|
if _dict.get("tool_calls"):
|
||||||
|
additional_kwargs["tool_calls"] = _dict["tool_calls"]
|
||||||
|
|
||||||
|
if role == "user" or default_class == HumanMessageChunk:
|
||||||
|
return HumanMessageChunk(content=content)
|
||||||
|
elif role == "assistant" or default_class == AIMessageChunk:
|
||||||
|
return AIMessageChunk(content=content, additional_kwargs=additional_kwargs)
|
||||||
|
elif role == "system" or default_class == SystemMessageChunk:
|
||||||
|
return SystemMessageChunk(content=content)
|
||||||
|
elif role == "function" or default_class == FunctionMessageChunk:
|
||||||
|
return FunctionMessageChunk(content=content, name=_dict["name"])
|
||||||
|
elif role == "tool" or default_class == ToolMessageChunk:
|
||||||
|
return ToolMessageChunk(content=content, tool_call_id=_dict["tool_call_id"])
|
||||||
|
elif role or default_class == ChatMessageChunk:
|
||||||
|
return ChatMessageChunk(content=content, role=role)
|
||||||
|
else:
|
||||||
|
return default_class(content=content) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class ChatOpenAI(BaseChatModel):
|
||||||
|
"""`OpenAI` Chat large language models API.
|
||||||
|
|
||||||
|
To use, you should have the
|
||||||
|
environment variable ``OPENAI_API_KEY`` set with your API key.
|
||||||
|
|
||||||
|
Any parameters that are valid to be passed to the openai.create call can be passed
|
||||||
|
in, even if not explicitly saved on this class.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from langchain_community.chat_models import ChatOpenAI
|
||||||
|
openai = ChatOpenAI(model_name="gpt-3.5-turbo")
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lc_secrets(self) -> Dict[str, str]:
|
||||||
|
return {"openai_api_key": "OPENAI_API_KEY"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_lc_namespace(cls) -> List[str]:
|
||||||
|
"""Get the namespace of the langchain object."""
|
||||||
|
return ["langchain", "chat_models", "openai"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lc_attributes(self) -> Dict[str, Any]:
|
||||||
|
attributes: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
if self.openai_organization:
|
||||||
|
attributes["openai_organization"] = self.openai_organization
|
||||||
|
|
||||||
|
if self.openai_api_base:
|
||||||
|
attributes["openai_api_base"] = self.openai_api_base
|
||||||
|
|
||||||
|
if self.openai_proxy:
|
||||||
|
attributes["openai_proxy"] = self.openai_proxy
|
||||||
|
|
||||||
|
return attributes
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_lc_serializable(cls) -> bool:
|
||||||
|
"""Return whether this model can be serialized by Langchain."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
client: Any = Field(default=None, exclude=True) #: :meta private:
|
||||||
|
async_client: Any = Field(default=None, exclude=True) #: :meta private:
|
||||||
|
model_name: str = Field(default="gpt-3.5-turbo", alias="model")
|
||||||
|
"""Model name to use."""
|
||||||
|
temperature: float = 0.7
|
||||||
|
"""What sampling temperature to use."""
|
||||||
|
model_kwargs: Dict[str, Any] = Field(default_factory=dict)
|
||||||
|
"""Holds any model parameters valid for `create` call not explicitly specified."""
|
||||||
|
# When updating this to use a SecretStr
|
||||||
|
# Check for classes that derive from this class (as some of them
|
||||||
|
# may assume openai_api_key is a str)
|
||||||
|
openai_api_key: Optional[str] = Field(default=None, alias="api_key")
|
||||||
|
"""Automatically inferred from env var `OPENAI_API_KEY` if not provided."""
|
||||||
|
openai_api_base: Optional[str] = Field(default=None, alias="base_url")
|
||||||
|
"""Base URL path for API requests, leave blank if not using a proxy or service
|
||||||
|
emulator."""
|
||||||
|
openai_organization: Optional[str] = Field(default=None, alias="organization")
|
||||||
|
"""Automatically inferred from env var `OPENAI_ORG_ID` if not provided."""
|
||||||
|
# to support explicit proxy for OpenAI
|
||||||
|
openai_proxy: Optional[str] = None
|
||||||
|
request_timeout: Union[float, Tuple[float, float], Any, None] = Field(
|
||||||
|
default=None, alias="timeout"
|
||||||
|
)
|
||||||
|
"""Timeout for requests to OpenAI completion API. Can be float, httpx.Timeout or
|
||||||
|
None."""
|
||||||
|
max_retries: int = 2
|
||||||
|
"""Maximum number of retries to make when generating."""
|
||||||
|
streaming: bool = False
|
||||||
|
"""Whether to stream the results or not."""
|
||||||
|
n: int = 1
|
||||||
|
"""Number of chat completions to generate for each prompt."""
|
||||||
|
max_tokens: Optional[int] = None
|
||||||
|
"""Maximum number of tokens to generate."""
|
||||||
|
tiktoken_model_name: Optional[str] = None
|
||||||
|
"""The model name to pass to tiktoken when using this class.
|
||||||
|
Tiktoken is used to count the number of tokens in documents to constrain
|
||||||
|
them to be under a certain limit. By default, when set to None, this will
|
||||||
|
be the same as the embedding model name. However, there are some cases
|
||||||
|
where you may want to use this Embedding class with a model name not
|
||||||
|
supported by tiktoken. This can include when using Azure embeddings or
|
||||||
|
when using one of the many model providers that expose an OpenAI-like
|
||||||
|
API but with different models. In those cases, in order to avoid erroring
|
||||||
|
when tiktoken is called, you can specify a model name to use here."""
|
||||||
|
default_headers: Union[Mapping[str, str], None] = None
|
||||||
|
default_query: Union[Mapping[str, object], None] = None
|
||||||
|
# Configure a custom httpx client. See the
|
||||||
|
# [httpx documentation](https://www.python-httpx.org/api/#client) for more details.
|
||||||
|
http_client: Union[Any, None] = None
|
||||||
|
"""Optional httpx.Client."""
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
"""Configuration for this pydantic object."""
|
||||||
|
|
||||||
|
allow_population_by_field_name = True
|
||||||
|
|
||||||
|
@root_validator(pre=True)
|
||||||
|
def build_extra(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Build extra kwargs from additional params that were passed in."""
|
||||||
|
all_required_field_names = get_pydantic_field_names(cls)
|
||||||
|
extra = values.get("model_kwargs", {})
|
||||||
|
for field_name in list(values):
|
||||||
|
if field_name in extra:
|
||||||
|
raise ValueError(f"Found {field_name} supplied twice.")
|
||||||
|
if field_name not in all_required_field_names:
|
||||||
|
warnings.warn(
|
||||||
|
f"""WARNING! {field_name} is not default parameter.
|
||||||
|
{field_name} was transferred to model_kwargs.
|
||||||
|
Please confirm that {field_name} is what you intended."""
|
||||||
|
)
|
||||||
|
extra[field_name] = values.pop(field_name)
|
||||||
|
|
||||||
|
invalid_model_kwargs = all_required_field_names.intersection(extra.keys())
|
||||||
|
if invalid_model_kwargs:
|
||||||
|
raise ValueError(
|
||||||
|
f"Parameters {invalid_model_kwargs} should be specified explicitly. "
|
||||||
|
f"Instead they were passed in as part of `model_kwargs` parameter."
|
||||||
|
)
|
||||||
|
|
||||||
|
values["model_kwargs"] = extra
|
||||||
|
return values
|
||||||
|
|
||||||
|
@root_validator()
|
||||||
|
def validate_environment(cls, values: Dict) -> Dict:
|
||||||
|
"""Validate that api key and python package exists in environment."""
|
||||||
|
if values["n"] < 1:
|
||||||
|
raise ValueError("n must be at least 1.")
|
||||||
|
if values["n"] > 1 and values["streaming"]:
|
||||||
|
raise ValueError("n must be 1 when streaming.")
|
||||||
|
|
||||||
|
values["openai_api_key"] = get_from_dict_or_env(
|
||||||
|
values, "openai_api_key", "OPENAI_API_KEY"
|
||||||
|
)
|
||||||
|
# Check OPENAI_ORGANIZATION for backwards compatibility.
|
||||||
|
values["openai_organization"] = (
|
||||||
|
values["openai_organization"]
|
||||||
|
or os.getenv("OPENAI_ORG_ID")
|
||||||
|
or os.getenv("OPENAI_ORGANIZATION")
|
||||||
|
)
|
||||||
|
values["openai_api_base"] = values["openai_api_base"] or os.getenv(
|
||||||
|
"OPENAI_API_BASE"
|
||||||
|
)
|
||||||
|
values["openai_proxy"] = get_from_dict_or_env(
|
||||||
|
values,
|
||||||
|
"openai_proxy",
|
||||||
|
"OPENAI_PROXY",
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
|
||||||
|
client_params = {
|
||||||
|
"api_key": values["openai_api_key"],
|
||||||
|
"organization": values["openai_organization"],
|
||||||
|
"base_url": values["openai_api_base"],
|
||||||
|
"timeout": values["request_timeout"],
|
||||||
|
"max_retries": values["max_retries"],
|
||||||
|
"default_headers": values["default_headers"],
|
||||||
|
"default_query": values["default_query"],
|
||||||
|
"http_client": values["http_client"],
|
||||||
|
}
|
||||||
|
|
||||||
|
if not values.get("client"):
|
||||||
|
values["client"] = openai.OpenAI(**client_params).chat.completions
|
||||||
|
if not values.get("async_client"):
|
||||||
|
values["async_client"] = openai.AsyncOpenAI(
|
||||||
|
**client_params
|
||||||
|
).chat.completions
|
||||||
|
return values
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _default_params(self) -> Dict[str, Any]:
|
||||||
|
"""Get the default parameters for calling OpenAI API."""
|
||||||
|
params = {
|
||||||
|
"model": self.model_name,
|
||||||
|
"stream": self.streaming,
|
||||||
|
"n": self.n,
|
||||||
|
"temperature": self.temperature,
|
||||||
|
**self.model_kwargs,
|
||||||
|
}
|
||||||
|
if self.max_tokens is not None:
|
||||||
|
params["max_tokens"] = self.max_tokens
|
||||||
|
return params
|
||||||
|
|
||||||
|
def _combine_llm_outputs(self, llm_outputs: List[Optional[dict]]) -> dict:
|
||||||
|
overall_token_usage: dict = {}
|
||||||
|
system_fingerprint = None
|
||||||
|
for output in llm_outputs:
|
||||||
|
if output is None:
|
||||||
|
# Happens in streaming
|
||||||
|
continue
|
||||||
|
token_usage = output["token_usage"]
|
||||||
|
if token_usage is not None:
|
||||||
|
for k, v in token_usage.items():
|
||||||
|
if k in overall_token_usage:
|
||||||
|
overall_token_usage[k] += v
|
||||||
|
else:
|
||||||
|
overall_token_usage[k] = v
|
||||||
|
if system_fingerprint is None:
|
||||||
|
system_fingerprint = output.get("system_fingerprint")
|
||||||
|
combined = {"token_usage": overall_token_usage, "model_name": self.model_name}
|
||||||
|
if system_fingerprint:
|
||||||
|
combined["system_fingerprint"] = system_fingerprint
|
||||||
|
return combined
|
||||||
|
|
||||||
|
def _stream(
|
||||||
|
self,
|
||||||
|
messages: List[BaseMessage],
|
||||||
|
stop: Optional[List[str]] = None,
|
||||||
|
run_manager: Optional[CallbackManagerForLLMRun] = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Iterator[ChatGenerationChunk]:
|
||||||
|
message_dicts, params = self._create_message_dicts(messages, stop)
|
||||||
|
params = {**params, **kwargs, "stream": True}
|
||||||
|
|
||||||
|
default_chunk_class = AIMessageChunk
|
||||||
|
for chunk in self.client.create(messages=message_dicts, **params):
|
||||||
|
if not isinstance(chunk, dict):
|
||||||
|
chunk = chunk.dict()
|
||||||
|
if len(chunk["choices"]) == 0:
|
||||||
|
continue
|
||||||
|
choice = chunk["choices"][0]
|
||||||
|
chunk = _convert_delta_to_message_chunk(
|
||||||
|
choice["delta"], default_chunk_class
|
||||||
|
)
|
||||||
|
finish_reason = choice.get("finish_reason")
|
||||||
|
generation_info = (
|
||||||
|
dict(finish_reason=finish_reason) if finish_reason is not None else None
|
||||||
|
)
|
||||||
|
default_chunk_class = chunk.__class__
|
||||||
|
chunk = ChatGenerationChunk(message=chunk, generation_info=generation_info)
|
||||||
|
yield chunk
|
||||||
|
if run_manager:
|
||||||
|
run_manager.on_llm_new_token(chunk.text, chunk=chunk)
|
||||||
|
|
||||||
|
def _generate(
|
||||||
|
self,
|
||||||
|
messages: List[BaseMessage],
|
||||||
|
stop: Optional[List[str]] = None,
|
||||||
|
run_manager: Optional[CallbackManagerForLLMRun] = None,
|
||||||
|
stream: Optional[bool] = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> ChatResult:
|
||||||
|
should_stream = stream if stream is not None else self.streaming
|
||||||
|
if should_stream:
|
||||||
|
stream_iter = self._stream(
|
||||||
|
messages, stop=stop, run_manager=run_manager, **kwargs
|
||||||
|
)
|
||||||
|
return generate_from_stream(stream_iter)
|
||||||
|
message_dicts, params = self._create_message_dicts(messages, stop)
|
||||||
|
params = {
|
||||||
|
**params,
|
||||||
|
**({"stream": stream} if stream is not None else {}),
|
||||||
|
**kwargs,
|
||||||
|
}
|
||||||
|
response = self.client.create(messages=message_dicts, **params)
|
||||||
|
return self._create_chat_result(response)
|
||||||
|
|
||||||
|
def _create_message_dicts(
|
||||||
|
self, messages: List[BaseMessage], stop: Optional[List[str]]
|
||||||
|
) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
|
||||||
|
params = self._default_params
|
||||||
|
if stop is not None:
|
||||||
|
if "stop" in params:
|
||||||
|
raise ValueError("`stop` found in both the input and default params.")
|
||||||
|
params["stop"] = stop
|
||||||
|
message_dicts = [_convert_message_to_dict(m) for m in messages]
|
||||||
|
return message_dicts, params
|
||||||
|
|
||||||
|
def _create_chat_result(self, response: Union[dict, BaseModel]) -> ChatResult:
|
||||||
|
generations = []
|
||||||
|
if not isinstance(response, dict):
|
||||||
|
response = response.dict()
|
||||||
|
for res in response["choices"]:
|
||||||
|
message = _convert_dict_to_message(res["message"])
|
||||||
|
generation_info = dict(finish_reason=res.get("finish_reason"))
|
||||||
|
if "logprobs" in res:
|
||||||
|
generation_info["logprobs"] = res["logprobs"]
|
||||||
|
gen = ChatGeneration(
|
||||||
|
message=message,
|
||||||
|
generation_info=generation_info,
|
||||||
|
)
|
||||||
|
generations.append(gen)
|
||||||
|
token_usage = response.get("usage", {})
|
||||||
|
llm_output = {
|
||||||
|
"token_usage": token_usage,
|
||||||
|
"model_name": self.model_name,
|
||||||
|
"system_fingerprint": response.get("system_fingerprint", ""),
|
||||||
|
}
|
||||||
|
return ChatResult(generations=generations, llm_output=llm_output)
|
||||||
|
|
||||||
|
async def _astream(
|
||||||
|
self,
|
||||||
|
messages: List[BaseMessage],
|
||||||
|
stop: Optional[List[str]] = None,
|
||||||
|
run_manager: Optional[AsyncCallbackManagerForLLMRun] = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> AsyncIterator[ChatGenerationChunk]:
|
||||||
|
message_dicts, params = self._create_message_dicts(messages, stop)
|
||||||
|
params = {**params, **kwargs, "stream": True}
|
||||||
|
|
||||||
|
default_chunk_class = AIMessageChunk
|
||||||
|
async for chunk in await self.async_client.create(
|
||||||
|
messages=message_dicts, **params
|
||||||
|
):
|
||||||
|
if not isinstance(chunk, dict):
|
||||||
|
chunk = chunk.dict()
|
||||||
|
if len(chunk["choices"]) == 0:
|
||||||
|
continue
|
||||||
|
choice = chunk["choices"][0]
|
||||||
|
chunk = _convert_delta_to_message_chunk(
|
||||||
|
choice["delta"], default_chunk_class
|
||||||
|
)
|
||||||
|
finish_reason = choice.get("finish_reason")
|
||||||
|
generation_info = (
|
||||||
|
dict(finish_reason=finish_reason) if finish_reason is not None else None
|
||||||
|
)
|
||||||
|
default_chunk_class = chunk.__class__
|
||||||
|
chunk = ChatGenerationChunk(message=chunk, generation_info=generation_info)
|
||||||
|
yield chunk
|
||||||
|
if run_manager:
|
||||||
|
await run_manager.on_llm_new_token(token=chunk.text, chunk=chunk)
|
||||||
|
|
||||||
|
async def _agenerate(
|
||||||
|
self,
|
||||||
|
messages: List[BaseMessage],
|
||||||
|
stop: Optional[List[str]] = None,
|
||||||
|
run_manager: Optional[AsyncCallbackManagerForLLMRun] = None,
|
||||||
|
stream: Optional[bool] = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> ChatResult:
|
||||||
|
should_stream = stream if stream is not None else self.streaming
|
||||||
|
if should_stream:
|
||||||
|
stream_iter = self._astream(
|
||||||
|
messages, stop=stop, run_manager=run_manager, **kwargs
|
||||||
|
)
|
||||||
|
return await agenerate_from_stream(stream_iter)
|
||||||
|
|
||||||
|
message_dicts, params = self._create_message_dicts(messages, stop)
|
||||||
|
params = {
|
||||||
|
**params,
|
||||||
|
**({"stream": stream} if stream is not None else {}),
|
||||||
|
**kwargs,
|
||||||
|
}
|
||||||
|
response = await self.async_client.create(messages=message_dicts, **params)
|
||||||
|
return self._create_chat_result(response)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _identifying_params(self) -> Dict[str, Any]:
|
||||||
|
"""Get the identifying parameters."""
|
||||||
|
return {"model_name": self.model_name, **self._default_params}
|
||||||
|
|
||||||
|
def _get_invocation_params(
|
||||||
|
self, stop: Optional[List[str]] = None, **kwargs: Any
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Get the parameters used to invoke the model."""
|
||||||
|
return {
|
||||||
|
"model": self.model_name,
|
||||||
|
**super()._get_invocation_params(stop=stop),
|
||||||
|
**self._default_params,
|
||||||
|
**kwargs,
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _llm_type(self) -> str:
|
||||||
|
"""Return type of chat model."""
|
||||||
|
return "openai-chat"
|
||||||
|
|
||||||
|
def _get_encoding_model(self) -> Tuple[str, tiktoken.Encoding]:
|
||||||
|
if self.tiktoken_model_name is not None:
|
||||||
|
model = self.tiktoken_model_name
|
||||||
|
else:
|
||||||
|
model = self.model_name
|
||||||
|
if model == "gpt-3.5-turbo":
|
||||||
|
# gpt-3.5-turbo may change over time.
|
||||||
|
# Returning num tokens assuming gpt-3.5-turbo-0301.
|
||||||
|
model = "gpt-3.5-turbo-0301"
|
||||||
|
elif model == "gpt-4":
|
||||||
|
# gpt-4 may change over time.
|
||||||
|
# Returning num tokens assuming gpt-4-0314.
|
||||||
|
model = "gpt-4-0314"
|
||||||
|
# Returns the number of tokens used by a list of messages.
|
||||||
|
try:
|
||||||
|
encoding = tiktoken.encoding_for_model(model)
|
||||||
|
except KeyError:
|
||||||
|
logger.warning("Warning: model not found. Using cl100k_base encoding.")
|
||||||
|
model = "cl100k_base"
|
||||||
|
encoding = tiktoken.get_encoding(model)
|
||||||
|
return model, encoding
|
||||||
|
|
||||||
|
def get_token_ids(self, text: str) -> List[int]:
|
||||||
|
"""Get the tokens present in the text with tiktoken package."""
|
||||||
|
# tiktoken NOT supported for Python 3.7 or below
|
||||||
|
if sys.version_info[1] <= 7:
|
||||||
|
return super().get_token_ids(text)
|
||||||
|
_, encoding_model = self._get_encoding_model()
|
||||||
|
return encoding_model.encode(text)
|
||||||
|
|
||||||
|
def get_num_tokens_from_messages(self, messages: List[BaseMessage]) -> int:
|
||||||
|
"""Calculate num tokens for gpt-3.5-turbo and gpt-4 with tiktoken package.
|
||||||
|
|
||||||
|
Official documentation: https://github.com/openai/openai-cookbook/blob/
|
||||||
|
main/examples/How_to_format_inputs_to_ChatGPT_models.ipynb"""
|
||||||
|
if sys.version_info[1] <= 7:
|
||||||
|
return super().get_num_tokens_from_messages(messages)
|
||||||
|
model, encoding = self._get_encoding_model()
|
||||||
|
if model.startswith("gpt-3.5-turbo-0301"):
|
||||||
|
# every message follows <im_start>{role/name}\n{content}<im_end>\n
|
||||||
|
tokens_per_message = 4
|
||||||
|
# if there's a name, the role is omitted
|
||||||
|
tokens_per_name = -1
|
||||||
|
elif model.startswith("gpt-3.5-turbo") or model.startswith("gpt-4"):
|
||||||
|
tokens_per_message = 3
|
||||||
|
tokens_per_name = 1
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(
|
||||||
|
f"get_num_tokens_from_messages() is not presently implemented "
|
||||||
|
f"for model {model}. See "
|
||||||
|
"https://platform.openai.com/docs/guides/text-generation/managing-tokens"
|
||||||
|
" for information on how messages are converted to tokens."
|
||||||
|
)
|
||||||
|
num_tokens = 0
|
||||||
|
messages_dict = [_convert_message_to_dict(m) for m in messages]
|
||||||
|
for message in messages_dict:
|
||||||
|
num_tokens += tokens_per_message
|
||||||
|
for key, value in message.items():
|
||||||
|
# Cast str(value) in case the message value is not a string
|
||||||
|
# This occurs with function messages
|
||||||
|
num_tokens += len(encoding.encode(str(value)))
|
||||||
|
if key == "name":
|
||||||
|
num_tokens += tokens_per_name
|
||||||
|
# every reply is primed with <im_start>assistant
|
||||||
|
num_tokens += 3
|
||||||
|
return num_tokens
|
||||||
|
|
||||||
|
def bind_functions(
|
||||||
|
self,
|
||||||
|
functions: Sequence[Union[Dict[str, Any], Type[BaseModel], Callable]],
|
||||||
|
function_call: Optional[str] = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Runnable[LanguageModelInput, BaseMessage]:
|
||||||
|
"""Bind functions (and other objects) to this chat model.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
functions: A list of function definitions to bind to this chat model.
|
||||||
|
Can be a dictionary, pydantic model, or callable. Pydantic
|
||||||
|
models and callables will be automatically converted to
|
||||||
|
their schema dictionary representation.
|
||||||
|
function_call: Which function to require the model to call.
|
||||||
|
Must be the name of the single provided function or
|
||||||
|
"auto" to automatically determine which function to call
|
||||||
|
(if any).
|
||||||
|
kwargs: Any additional parameters to pass to the
|
||||||
|
:class:`~langchain.runnable.Runnable` constructor.
|
||||||
|
"""
|
||||||
|
|
||||||
|
formatted_functions = [convert_to_openai_function(fn) for fn in functions]
|
||||||
|
if function_call is not None:
|
||||||
|
if len(formatted_functions) != 1:
|
||||||
|
raise ValueError(
|
||||||
|
"When specifying `function_call`, you must provide exactly one "
|
||||||
|
"function."
|
||||||
|
)
|
||||||
|
if formatted_functions[0]["name"] != function_call:
|
||||||
|
raise ValueError(
|
||||||
|
f"Function call {function_call} was specified, but the only "
|
||||||
|
f"provided function was {formatted_functions[0]['name']}."
|
||||||
|
)
|
||||||
|
function_call_ = {"name": function_call}
|
||||||
|
kwargs = {**kwargs, "function_call": function_call_}
|
||||||
|
return super().bind(
|
||||||
|
functions=formatted_functions,
|
||||||
|
**kwargs,
|
||||||
|
)
|
@ -0,0 +1,7 @@
|
|||||||
|
from langchain_openai.embeddings.azure import AzureOpenAIEmbeddings
|
||||||
|
from langchain_openai.embeddings.base import OpenAIEmbeddings
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"OpenAIEmbeddings",
|
||||||
|
"AzureOpenAIEmbeddings",
|
||||||
|
]
|
130
libs/partners/openai/langchain_openai/embeddings/azure.py
Normal file
130
libs/partners/openai/langchain_openai/embeddings/azure.py
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
"""Azure OpenAI embeddings wrapper."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Callable, Dict, Optional, Union
|
||||||
|
|
||||||
|
import openai
|
||||||
|
from langchain_core.pydantic_v1 import Field, root_validator
|
||||||
|
from langchain_core.utils import get_from_dict_or_env
|
||||||
|
|
||||||
|
from langchain_openai.embeddings.base import OpenAIEmbeddings
|
||||||
|
|
||||||
|
|
||||||
|
class AzureOpenAIEmbeddings(OpenAIEmbeddings):
|
||||||
|
"""`Azure OpenAI` Embeddings API."""
|
||||||
|
|
||||||
|
azure_endpoint: Union[str, None] = None
|
||||||
|
"""Your Azure endpoint, including the resource.
|
||||||
|
|
||||||
|
Automatically inferred from env var `AZURE_OPENAI_ENDPOINT` if not provided.
|
||||||
|
|
||||||
|
Example: `https://example-resource.azure.openai.com/`
|
||||||
|
"""
|
||||||
|
deployment: Optional[str] = Field(default=None, alias="azure_deployment")
|
||||||
|
"""A model deployment.
|
||||||
|
|
||||||
|
If given sets the base client URL to include `/deployments/{azure_deployment}`.
|
||||||
|
Note: this means you won't be able to use non-deployment endpoints.
|
||||||
|
"""
|
||||||
|
openai_api_key: Union[str, None] = Field(default=None, alias="api_key")
|
||||||
|
"""Automatically inferred from env var `AZURE_OPENAI_API_KEY` if not provided."""
|
||||||
|
azure_ad_token: Union[str, None] = None
|
||||||
|
"""Your Azure Active Directory token.
|
||||||
|
|
||||||
|
Automatically inferred from env var `AZURE_OPENAI_AD_TOKEN` if not provided.
|
||||||
|
|
||||||
|
For more:
|
||||||
|
https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-id.
|
||||||
|
""" # noqa: E501
|
||||||
|
azure_ad_token_provider: Union[Callable[[], str], None] = None
|
||||||
|
"""A function that returns an Azure Active Directory token.
|
||||||
|
|
||||||
|
Will be invoked on every request.
|
||||||
|
"""
|
||||||
|
openai_api_version: Optional[str] = Field(default=None, alias="api_version")
|
||||||
|
"""Automatically inferred from env var `OPENAI_API_VERSION` if not provided."""
|
||||||
|
validate_base_url: bool = True
|
||||||
|
|
||||||
|
@root_validator()
|
||||||
|
def validate_environment(cls, values: Dict) -> Dict:
|
||||||
|
"""Validate that api key and python package exists in environment."""
|
||||||
|
# Check OPENAI_KEY for backwards compatibility.
|
||||||
|
# TODO: Remove OPENAI_API_KEY support to avoid possible conflict when using
|
||||||
|
# other forms of azure credentials.
|
||||||
|
values["openai_api_key"] = (
|
||||||
|
values["openai_api_key"]
|
||||||
|
or os.getenv("AZURE_OPENAI_API_KEY")
|
||||||
|
or os.getenv("OPENAI_API_KEY")
|
||||||
|
)
|
||||||
|
values["openai_api_base"] = values["openai_api_base"] or os.getenv(
|
||||||
|
"OPENAI_API_BASE"
|
||||||
|
)
|
||||||
|
values["openai_api_version"] = values["openai_api_version"] or os.getenv(
|
||||||
|
"OPENAI_API_VERSION", default="2023-05-15"
|
||||||
|
)
|
||||||
|
values["openai_api_type"] = get_from_dict_or_env(
|
||||||
|
values, "openai_api_type", "OPENAI_API_TYPE", default="azure"
|
||||||
|
)
|
||||||
|
values["openai_organization"] = (
|
||||||
|
values["openai_organization"]
|
||||||
|
or os.getenv("OPENAI_ORG_ID")
|
||||||
|
or os.getenv("OPENAI_ORGANIZATION")
|
||||||
|
)
|
||||||
|
values["openai_proxy"] = get_from_dict_or_env(
|
||||||
|
values,
|
||||||
|
"openai_proxy",
|
||||||
|
"OPENAI_PROXY",
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
values["azure_endpoint"] = values["azure_endpoint"] or os.getenv(
|
||||||
|
"AZURE_OPENAI_ENDPOINT"
|
||||||
|
)
|
||||||
|
values["azure_ad_token"] = values["azure_ad_token"] or os.getenv(
|
||||||
|
"AZURE_OPENAI_AD_TOKEN"
|
||||||
|
)
|
||||||
|
# Azure OpenAI embedding models allow a maximum of 16 texts
|
||||||
|
# at a time in each batch
|
||||||
|
# See: https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#embeddings
|
||||||
|
values["chunk_size"] = min(values["chunk_size"], 16)
|
||||||
|
# For backwards compatibility. Before openai v1, no distinction was made
|
||||||
|
# between azure_endpoint and base_url (openai_api_base).
|
||||||
|
openai_api_base = values["openai_api_base"]
|
||||||
|
if openai_api_base and values["validate_base_url"]:
|
||||||
|
if "/openai" not in openai_api_base:
|
||||||
|
values["openai_api_base"] += "/openai"
|
||||||
|
raise ValueError(
|
||||||
|
"As of openai>=1.0.0, Azure endpoints should be specified via "
|
||||||
|
"the `azure_endpoint` param not `openai_api_base` "
|
||||||
|
"(or alias `base_url`). "
|
||||||
|
)
|
||||||
|
if values["deployment"]:
|
||||||
|
raise ValueError(
|
||||||
|
"As of openai>=1.0.0, if `deployment` (or alias "
|
||||||
|
"`azure_deployment`) is specified then "
|
||||||
|
"`openai_api_base` (or alias `base_url`) should not be. "
|
||||||
|
"Instead use `deployment` (or alias `azure_deployment`) "
|
||||||
|
"and `azure_endpoint`."
|
||||||
|
)
|
||||||
|
client_params = {
|
||||||
|
"api_version": values["openai_api_version"],
|
||||||
|
"azure_endpoint": values["azure_endpoint"],
|
||||||
|
"azure_deployment": values["deployment"],
|
||||||
|
"api_key": values["openai_api_key"],
|
||||||
|
"azure_ad_token": values["azure_ad_token"],
|
||||||
|
"azure_ad_token_provider": values["azure_ad_token_provider"],
|
||||||
|
"organization": values["openai_organization"],
|
||||||
|
"base_url": values["openai_api_base"],
|
||||||
|
"timeout": values["request_timeout"],
|
||||||
|
"max_retries": values["max_retries"],
|
||||||
|
"default_headers": values["default_headers"],
|
||||||
|
"default_query": values["default_query"],
|
||||||
|
"http_client": values["http_client"],
|
||||||
|
}
|
||||||
|
values["client"] = openai.AzureOpenAI(**client_params).embeddings
|
||||||
|
values["async_client"] = openai.AsyncAzureOpenAI(**client_params).embeddings
|
||||||
|
return values
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _llm_type(self) -> str:
|
||||||
|
return "azure-openai-chat"
|
523
libs/partners/openai/langchain_openai/embeddings/base.py
Normal file
523
libs/partners/openai/langchain_openai/embeddings/base.py
Normal file
@ -0,0 +1,523 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import warnings
|
||||||
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Dict,
|
||||||
|
Iterable,
|
||||||
|
List,
|
||||||
|
Literal,
|
||||||
|
Mapping,
|
||||||
|
Optional,
|
||||||
|
Sequence,
|
||||||
|
Set,
|
||||||
|
Tuple,
|
||||||
|
Union,
|
||||||
|
cast,
|
||||||
|
)
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import openai
|
||||||
|
import tiktoken
|
||||||
|
from langchain_core.embeddings import Embeddings
|
||||||
|
from langchain_core.pydantic_v1 import BaseModel, Extra, Field, root_validator
|
||||||
|
from langchain_core.utils import get_from_dict_or_env, get_pydantic_field_names
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class OpenAIEmbeddings(BaseModel, Embeddings):
|
||||||
|
"""OpenAI embedding models.
|
||||||
|
|
||||||
|
To use, you should have the
|
||||||
|
environment variable ``OPENAI_API_KEY`` set with your API key or pass it
|
||||||
|
as a named parameter to the constructor.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from langchain_community.embeddings import OpenAIEmbeddings
|
||||||
|
openai = OpenAIEmbeddings(openai_api_key="my-api-key")
|
||||||
|
|
||||||
|
In order to use the library with Microsoft Azure endpoints, you need to set
|
||||||
|
the OPENAI_API_TYPE, OPENAI_API_BASE, OPENAI_API_KEY and OPENAI_API_VERSION.
|
||||||
|
The OPENAI_API_TYPE must be set to 'azure' and the others correspond to
|
||||||
|
the properties of your endpoint.
|
||||||
|
In addition, the deployment name must be passed as the model parameter.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
os.environ["OPENAI_API_TYPE"] = "azure"
|
||||||
|
os.environ["OPENAI_API_BASE"] = "https://<your-endpoint.openai.azure.com/"
|
||||||
|
os.environ["OPENAI_API_KEY"] = "your AzureOpenAI key"
|
||||||
|
os.environ["OPENAI_API_VERSION"] = "2023-05-15"
|
||||||
|
os.environ["OPENAI_PROXY"] = "http://your-corporate-proxy:8080"
|
||||||
|
|
||||||
|
from langchain_community.embeddings.openai import OpenAIEmbeddings
|
||||||
|
embeddings = OpenAIEmbeddings(
|
||||||
|
deployment="your-embeddings-deployment-name",
|
||||||
|
model="your-embeddings-model-name",
|
||||||
|
openai_api_base="https://your-endpoint.openai.azure.com/",
|
||||||
|
openai_api_type="azure",
|
||||||
|
)
|
||||||
|
text = "This is a test query."
|
||||||
|
query_result = embeddings.embed_query(text)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
client: Any = Field(default=None, exclude=True) #: :meta private:
|
||||||
|
async_client: Any = Field(default=None, exclude=True) #: :meta private:
|
||||||
|
model: str = "text-embedding-ada-002"
|
||||||
|
# to support Azure OpenAI Service custom deployment names
|
||||||
|
deployment: Optional[str] = model
|
||||||
|
# TODO: Move to AzureOpenAIEmbeddings.
|
||||||
|
openai_api_version: Optional[str] = Field(default=None, alias="api_version")
|
||||||
|
"""Automatically inferred from env var `OPENAI_API_VERSION` if not provided."""
|
||||||
|
# to support Azure OpenAI Service custom endpoints
|
||||||
|
openai_api_base: Optional[str] = Field(default=None, alias="base_url")
|
||||||
|
"""Base URL path for API requests, leave blank if not using a proxy or service
|
||||||
|
emulator."""
|
||||||
|
# to support Azure OpenAI Service custom endpoints
|
||||||
|
openai_api_type: Optional[str] = None
|
||||||
|
# to support explicit proxy for OpenAI
|
||||||
|
openai_proxy: Optional[str] = None
|
||||||
|
embedding_ctx_length: int = 8191
|
||||||
|
"""The maximum number of tokens to embed at once."""
|
||||||
|
openai_api_key: Optional[str] = Field(default=None, alias="api_key")
|
||||||
|
"""Automatically inferred from env var `OPENAI_API_KEY` if not provided."""
|
||||||
|
openai_organization: Optional[str] = Field(default=None, alias="organization")
|
||||||
|
"""Automatically inferred from env var `OPENAI_ORG_ID` if not provided."""
|
||||||
|
allowed_special: Union[Literal["all"], Set[str]] = set()
|
||||||
|
disallowed_special: Union[Literal["all"], Set[str], Sequence[str]] = "all"
|
||||||
|
chunk_size: int = 1000
|
||||||
|
"""Maximum number of texts to embed in each batch"""
|
||||||
|
max_retries: int = 2
|
||||||
|
"""Maximum number of retries to make when generating."""
|
||||||
|
request_timeout: Optional[Union[float, Tuple[float, float], Any]] = Field(
|
||||||
|
default=None, alias="timeout"
|
||||||
|
)
|
||||||
|
"""Timeout for requests to OpenAI completion API. Can be float, httpx.Timeout or
|
||||||
|
None."""
|
||||||
|
headers: Any = None
|
||||||
|
tiktoken_enabled: bool = True
|
||||||
|
"""Set this to False for non-OpenAI implementations of the embeddings API, e.g.
|
||||||
|
the `--extensions openai` extension for `text-generation-webui`"""
|
||||||
|
tiktoken_model_name: Optional[str] = None
|
||||||
|
"""The model name to pass to tiktoken when using this class.
|
||||||
|
Tiktoken is used to count the number of tokens in documents to constrain
|
||||||
|
them to be under a certain limit. By default, when set to None, this will
|
||||||
|
be the same as the embedding model name. However, there are some cases
|
||||||
|
where you may want to use this Embedding class with a model name not
|
||||||
|
supported by tiktoken. This can include when using Azure embeddings or
|
||||||
|
when using one of the many model providers that expose an OpenAI-like
|
||||||
|
API but with different models. In those cases, in order to avoid erroring
|
||||||
|
when tiktoken is called, you can specify a model name to use here."""
|
||||||
|
show_progress_bar: bool = False
|
||||||
|
"""Whether to show a progress bar when embedding."""
|
||||||
|
model_kwargs: Dict[str, Any] = Field(default_factory=dict)
|
||||||
|
"""Holds any model parameters valid for `create` call not explicitly specified."""
|
||||||
|
skip_empty: bool = False
|
||||||
|
"""Whether to skip empty strings when embedding or raise an error.
|
||||||
|
Defaults to not skipping."""
|
||||||
|
default_headers: Union[Mapping[str, str], None] = None
|
||||||
|
default_query: Union[Mapping[str, object], None] = None
|
||||||
|
# Configure a custom httpx client. See the
|
||||||
|
# [httpx documentation](https://www.python-httpx.org/api/#client) for more details.
|
||||||
|
retry_min_seconds: int = 4
|
||||||
|
"""Min number of seconds to wait between retries"""
|
||||||
|
retry_max_seconds: int = 20
|
||||||
|
"""Max number of seconds to wait between retries"""
|
||||||
|
http_client: Union[Any, None] = None
|
||||||
|
"""Optional httpx.Client."""
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
"""Configuration for this pydantic object."""
|
||||||
|
|
||||||
|
extra = Extra.forbid
|
||||||
|
allow_population_by_field_name = True
|
||||||
|
|
||||||
|
@root_validator(pre=True)
|
||||||
|
def build_extra(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Build extra kwargs from additional params that were passed in."""
|
||||||
|
all_required_field_names = get_pydantic_field_names(cls)
|
||||||
|
extra = values.get("model_kwargs", {})
|
||||||
|
for field_name in list(values):
|
||||||
|
if field_name in extra:
|
||||||
|
raise ValueError(f"Found {field_name} supplied twice.")
|
||||||
|
if field_name not in all_required_field_names:
|
||||||
|
warnings.warn(
|
||||||
|
f"""WARNING! {field_name} is not default parameter.
|
||||||
|
{field_name} was transferred to model_kwargs.
|
||||||
|
Please confirm that {field_name} is what you intended."""
|
||||||
|
)
|
||||||
|
extra[field_name] = values.pop(field_name)
|
||||||
|
|
||||||
|
invalid_model_kwargs = all_required_field_names.intersection(extra.keys())
|
||||||
|
if invalid_model_kwargs:
|
||||||
|
raise ValueError(
|
||||||
|
f"Parameters {invalid_model_kwargs} should be specified explicitly. "
|
||||||
|
f"Instead they were passed in as part of `model_kwargs` parameter."
|
||||||
|
)
|
||||||
|
|
||||||
|
values["model_kwargs"] = extra
|
||||||
|
return values
|
||||||
|
|
||||||
|
@root_validator()
|
||||||
|
def validate_environment(cls, values: Dict) -> Dict:
|
||||||
|
"""Validate that api key and python package exists in environment."""
|
||||||
|
values["openai_api_key"] = get_from_dict_or_env(
|
||||||
|
values, "openai_api_key", "OPENAI_API_KEY"
|
||||||
|
)
|
||||||
|
values["openai_api_base"] = values["openai_api_base"] or os.getenv(
|
||||||
|
"OPENAI_API_BASE"
|
||||||
|
)
|
||||||
|
values["openai_api_type"] = get_from_dict_or_env(
|
||||||
|
values,
|
||||||
|
"openai_api_type",
|
||||||
|
"OPENAI_API_TYPE",
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
values["openai_proxy"] = get_from_dict_or_env(
|
||||||
|
values,
|
||||||
|
"openai_proxy",
|
||||||
|
"OPENAI_PROXY",
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
if values["openai_api_type"] in ("azure", "azure_ad", "azuread"):
|
||||||
|
default_api_version = "2023-05-15"
|
||||||
|
# Azure OpenAI embedding models allow a maximum of 16 texts
|
||||||
|
# at a time in each batch
|
||||||
|
# See: https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#embeddings
|
||||||
|
values["chunk_size"] = min(values["chunk_size"], 16)
|
||||||
|
else:
|
||||||
|
default_api_version = ""
|
||||||
|
values["openai_api_version"] = get_from_dict_or_env(
|
||||||
|
values,
|
||||||
|
"openai_api_version",
|
||||||
|
"OPENAI_API_VERSION",
|
||||||
|
default=default_api_version,
|
||||||
|
)
|
||||||
|
# Check OPENAI_ORGANIZATION for backwards compatibility.
|
||||||
|
values["openai_organization"] = (
|
||||||
|
values["openai_organization"]
|
||||||
|
or os.getenv("OPENAI_ORG_ID")
|
||||||
|
or os.getenv("OPENAI_ORGANIZATION")
|
||||||
|
)
|
||||||
|
if values["openai_api_type"] in ("azure", "azure_ad", "azuread"):
|
||||||
|
raise ValueError(
|
||||||
|
"If you are using Azure, "
|
||||||
|
"please use the `AzureOpenAIEmbeddings` class."
|
||||||
|
)
|
||||||
|
client_params = {
|
||||||
|
"api_key": values["openai_api_key"],
|
||||||
|
"organization": values["openai_organization"],
|
||||||
|
"base_url": values["openai_api_base"],
|
||||||
|
"timeout": values["request_timeout"],
|
||||||
|
"max_retries": values["max_retries"],
|
||||||
|
"default_headers": values["default_headers"],
|
||||||
|
"default_query": values["default_query"],
|
||||||
|
"http_client": values["http_client"],
|
||||||
|
}
|
||||||
|
if not values.get("client"):
|
||||||
|
values["client"] = openai.OpenAI(**client_params).embeddings
|
||||||
|
if not values.get("async_client"):
|
||||||
|
values["async_client"] = openai.AsyncOpenAI(**client_params).embeddings
|
||||||
|
return values
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _invocation_params(self) -> Dict[str, Any]:
|
||||||
|
return {"model": self.model, **self.model_kwargs}
|
||||||
|
|
||||||
|
# please refer to
|
||||||
|
# https://github.com/openai/openai-cookbook/blob/main/examples/Embedding_long_inputs.ipynb
|
||||||
|
def _get_len_safe_embeddings(
|
||||||
|
self, texts: List[str], *, engine: str, chunk_size: Optional[int] = None
|
||||||
|
) -> List[List[float]]:
|
||||||
|
"""
|
||||||
|
Generate length-safe embeddings for a list of texts.
|
||||||
|
|
||||||
|
This method handles tokenization and embedding generation, respecting the
|
||||||
|
set embedding context length and chunk size. It supports both tiktoken
|
||||||
|
and HuggingFace tokenizer based on the tiktoken_enabled flag.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
texts (List[str]): A list of texts to embed.
|
||||||
|
engine (str): The engine or model to use for embeddings.
|
||||||
|
chunk_size (Optional[int]): The size of chunks for processing embeddings.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[List[float]]: A list of embeddings for each input text.
|
||||||
|
"""
|
||||||
|
|
||||||
|
tokens = []
|
||||||
|
indices = []
|
||||||
|
model_name = self.tiktoken_model_name or self.model
|
||||||
|
_chunk_size = chunk_size or self.chunk_size
|
||||||
|
|
||||||
|
# If tiktoken flag set to False
|
||||||
|
if not self.tiktoken_enabled:
|
||||||
|
try:
|
||||||
|
from transformers import AutoTokenizer # noqa: F401
|
||||||
|
except ImportError:
|
||||||
|
raise ValueError(
|
||||||
|
"Could not import transformers python package. "
|
||||||
|
"This is needed in order to for OpenAIEmbeddings without "
|
||||||
|
"`tiktoken`. Please install it with `pip install transformers`. "
|
||||||
|
)
|
||||||
|
|
||||||
|
tokenizer = AutoTokenizer.from_pretrained(
|
||||||
|
pretrained_model_name_or_path=model_name
|
||||||
|
)
|
||||||
|
for i, text in enumerate(texts):
|
||||||
|
# Tokenize the text using HuggingFace transformers
|
||||||
|
tokenized = tokenizer.encode(text, add_special_tokens=False)
|
||||||
|
|
||||||
|
# Split tokens into chunks respecting the embedding_ctx_length
|
||||||
|
for j in range(0, len(tokenized), self.embedding_ctx_length):
|
||||||
|
token_chunk = tokenized[j : j + self.embedding_ctx_length]
|
||||||
|
|
||||||
|
# Convert token IDs back to a string
|
||||||
|
chunk_text = tokenizer.decode(token_chunk)
|
||||||
|
tokens.append(chunk_text)
|
||||||
|
indices.append(i)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
encoding = tiktoken.encoding_for_model(model_name)
|
||||||
|
except KeyError:
|
||||||
|
logger.warning("Warning: model not found. Using cl100k_base encoding.")
|
||||||
|
model = "cl100k_base"
|
||||||
|
encoding = tiktoken.get_encoding(model)
|
||||||
|
for i, text in enumerate(texts):
|
||||||
|
if self.model.endswith("001"):
|
||||||
|
# See: https://github.com/openai/openai-python/
|
||||||
|
# issues/418#issuecomment-1525939500
|
||||||
|
# replace newlines, which can negatively affect performance.
|
||||||
|
text = text.replace("\n", " ")
|
||||||
|
|
||||||
|
token = encoding.encode(
|
||||||
|
text=text,
|
||||||
|
allowed_special=self.allowed_special,
|
||||||
|
disallowed_special=self.disallowed_special,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Split tokens into chunks respecting the embedding_ctx_length
|
||||||
|
for j in range(0, len(token), self.embedding_ctx_length):
|
||||||
|
tokens.append(token[j : j + self.embedding_ctx_length])
|
||||||
|
indices.append(i)
|
||||||
|
|
||||||
|
if self.show_progress_bar:
|
||||||
|
try:
|
||||||
|
from tqdm.auto import tqdm
|
||||||
|
|
||||||
|
_iter: Iterable = tqdm(range(0, len(tokens), _chunk_size))
|
||||||
|
except ImportError:
|
||||||
|
_iter = range(0, len(tokens), _chunk_size)
|
||||||
|
else:
|
||||||
|
_iter = range(0, len(tokens), _chunk_size)
|
||||||
|
|
||||||
|
batched_embeddings: List[List[float]] = []
|
||||||
|
for i in _iter:
|
||||||
|
response = self.client.create(
|
||||||
|
input=tokens[i : i + _chunk_size], **self._invocation_params
|
||||||
|
)
|
||||||
|
if not isinstance(response, dict):
|
||||||
|
response = response.dict()
|
||||||
|
batched_embeddings.extend(r["embedding"] for r in response["data"])
|
||||||
|
|
||||||
|
results: List[List[List[float]]] = [[] for _ in range(len(texts))]
|
||||||
|
num_tokens_in_batch: List[List[int]] = [[] for _ in range(len(texts))]
|
||||||
|
for i in range(len(indices)):
|
||||||
|
if self.skip_empty and len(batched_embeddings[i]) == 1:
|
||||||
|
continue
|
||||||
|
results[indices[i]].append(batched_embeddings[i])
|
||||||
|
num_tokens_in_batch[indices[i]].append(len(tokens[i]))
|
||||||
|
|
||||||
|
embeddings: List[List[float]] = [[] for _ in range(len(texts))]
|
||||||
|
for i in range(len(texts)):
|
||||||
|
_result = results[i]
|
||||||
|
if len(_result) == 0:
|
||||||
|
average_embedded = self.client.create(
|
||||||
|
input="", **self._invocation_params
|
||||||
|
)
|
||||||
|
if not isinstance(average_embedded, dict):
|
||||||
|
average_embedded = average_embedded.dict()
|
||||||
|
average = average_embedded["data"][0]["embedding"]
|
||||||
|
else:
|
||||||
|
average = np.average(_result, axis=0, weights=num_tokens_in_batch[i])
|
||||||
|
embeddings[i] = (average / np.linalg.norm(average)).tolist()
|
||||||
|
|
||||||
|
return embeddings
|
||||||
|
|
||||||
|
# please refer to
|
||||||
|
# https://github.com/openai/openai-cookbook/blob/main/examples/Embedding_long_inputs.ipynb
|
||||||
|
async def _aget_len_safe_embeddings(
|
||||||
|
self, texts: List[str], *, engine: str, chunk_size: Optional[int] = None
|
||||||
|
) -> List[List[float]]:
|
||||||
|
"""
|
||||||
|
Asynchronously generate length-safe embeddings for a list of texts.
|
||||||
|
|
||||||
|
This method handles tokenization and asynchronous embedding generation,
|
||||||
|
respecting the set embedding context length and chunk size. It supports both
|
||||||
|
`tiktoken` and HuggingFace `tokenizer` based on the tiktoken_enabled flag.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
texts (List[str]): A list of texts to embed.
|
||||||
|
engine (str): The engine or model to use for embeddings.
|
||||||
|
chunk_size (Optional[int]): The size of chunks for processing embeddings.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[List[float]]: A list of embeddings for each input text.
|
||||||
|
"""
|
||||||
|
|
||||||
|
tokens = []
|
||||||
|
indices = []
|
||||||
|
model_name = self.tiktoken_model_name or self.model
|
||||||
|
_chunk_size = chunk_size or self.chunk_size
|
||||||
|
|
||||||
|
# If tiktoken flag set to False
|
||||||
|
if not self.tiktoken_enabled:
|
||||||
|
try:
|
||||||
|
from transformers import AutoTokenizer
|
||||||
|
except ImportError:
|
||||||
|
raise ValueError(
|
||||||
|
"Could not import transformers python package. "
|
||||||
|
"This is needed in order to for OpenAIEmbeddings without "
|
||||||
|
" `tiktoken`. Please install it with `pip install transformers`."
|
||||||
|
)
|
||||||
|
|
||||||
|
tokenizer = AutoTokenizer.from_pretrained(
|
||||||
|
pretrained_model_name_or_path=model_name
|
||||||
|
)
|
||||||
|
for i, text in enumerate(texts):
|
||||||
|
# Tokenize the text using HuggingFace transformers
|
||||||
|
tokenized = tokenizer.encode(text, add_special_tokens=False)
|
||||||
|
|
||||||
|
# Split tokens into chunks respecting the embedding_ctx_length
|
||||||
|
for j in range(0, len(tokenized), self.embedding_ctx_length):
|
||||||
|
token_chunk = tokenized[j : j + self.embedding_ctx_length]
|
||||||
|
|
||||||
|
# Convert token IDs back to a string
|
||||||
|
chunk_text = tokenizer.decode(token_chunk)
|
||||||
|
tokens.append(chunk_text)
|
||||||
|
indices.append(i)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
encoding = tiktoken.encoding_for_model(model_name)
|
||||||
|
except KeyError:
|
||||||
|
logger.warning("Warning: model not found. Using cl100k_base encoding.")
|
||||||
|
model = "cl100k_base"
|
||||||
|
encoding = tiktoken.get_encoding(model)
|
||||||
|
for i, text in enumerate(texts):
|
||||||
|
if self.model.endswith("001"):
|
||||||
|
# See: https://github.com/openai/openai-python/
|
||||||
|
# issues/418#issuecomment-1525939500
|
||||||
|
# replace newlines, which can negatively affect performance.
|
||||||
|
text = text.replace("\n", " ")
|
||||||
|
|
||||||
|
token = encoding.encode(
|
||||||
|
text=text,
|
||||||
|
allowed_special=self.allowed_special,
|
||||||
|
disallowed_special=self.disallowed_special,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Split tokens into chunks respecting the embedding_ctx_length
|
||||||
|
for j in range(0, len(token), self.embedding_ctx_length):
|
||||||
|
tokens.append(token[j : j + self.embedding_ctx_length])
|
||||||
|
indices.append(i)
|
||||||
|
|
||||||
|
batched_embeddings: List[List[float]] = []
|
||||||
|
_chunk_size = chunk_size or self.chunk_size
|
||||||
|
for i in range(0, len(tokens), _chunk_size):
|
||||||
|
response = await self.async_client.create(
|
||||||
|
input=tokens[i : i + _chunk_size], **self._invocation_params
|
||||||
|
)
|
||||||
|
|
||||||
|
if not isinstance(response, dict):
|
||||||
|
response = response.dict()
|
||||||
|
batched_embeddings.extend(r["embedding"] for r in response["data"])
|
||||||
|
|
||||||
|
results: List[List[List[float]]] = [[] for _ in range(len(texts))]
|
||||||
|
num_tokens_in_batch: List[List[int]] = [[] for _ in range(len(texts))]
|
||||||
|
for i in range(len(indices)):
|
||||||
|
results[indices[i]].append(batched_embeddings[i])
|
||||||
|
num_tokens_in_batch[indices[i]].append(len(tokens[i]))
|
||||||
|
|
||||||
|
embeddings: List[List[float]] = [[] for _ in range(len(texts))]
|
||||||
|
for i in range(len(texts)):
|
||||||
|
_result = results[i]
|
||||||
|
if len(_result) == 0:
|
||||||
|
average_embedded = await self.async_client.create(
|
||||||
|
input="", **self._invocation_params
|
||||||
|
)
|
||||||
|
if not isinstance(average_embedded, dict):
|
||||||
|
average_embedded = average_embedded.dict()
|
||||||
|
average = average_embedded["data"][0]["embedding"]
|
||||||
|
else:
|
||||||
|
average = np.average(_result, axis=0, weights=num_tokens_in_batch[i])
|
||||||
|
embeddings[i] = (average / np.linalg.norm(average)).tolist()
|
||||||
|
|
||||||
|
return embeddings
|
||||||
|
|
||||||
|
def embed_documents(
|
||||||
|
self, texts: List[str], chunk_size: Optional[int] = 0
|
||||||
|
) -> List[List[float]]:
|
||||||
|
"""Call out to OpenAI's embedding endpoint for embedding search docs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
texts: The list of texts to embed.
|
||||||
|
chunk_size: The chunk size of embeddings. If None, will use the chunk size
|
||||||
|
specified by the class.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of embeddings, one for each text.
|
||||||
|
"""
|
||||||
|
# NOTE: to keep things simple, we assume the list may contain texts longer
|
||||||
|
# than the maximum context and use length-safe embedding function.
|
||||||
|
engine = cast(str, self.deployment)
|
||||||
|
return self._get_len_safe_embeddings(texts, engine=engine)
|
||||||
|
|
||||||
|
async def aembed_documents(
|
||||||
|
self, texts: List[str], chunk_size: Optional[int] = 0
|
||||||
|
) -> List[List[float]]:
|
||||||
|
"""Call out to OpenAI's embedding endpoint async for embedding search docs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
texts: The list of texts to embed.
|
||||||
|
chunk_size: The chunk size of embeddings. If None, will use the chunk size
|
||||||
|
specified by the class.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of embeddings, one for each text.
|
||||||
|
"""
|
||||||
|
# NOTE: to keep things simple, we assume the list may contain texts longer
|
||||||
|
# than the maximum context and use length-safe embedding function.
|
||||||
|
engine = cast(str, self.deployment)
|
||||||
|
return await self._aget_len_safe_embeddings(texts, engine=engine)
|
||||||
|
|
||||||
|
def embed_query(self, text: str) -> List[float]:
|
||||||
|
"""Call out to OpenAI's embedding endpoint for embedding query text.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: The text to embed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Embedding for the text.
|
||||||
|
"""
|
||||||
|
return self.embed_documents([text])[0]
|
||||||
|
|
||||||
|
async def aembed_query(self, text: str) -> List[float]:
|
||||||
|
"""Call out to OpenAI's embedding endpoint async for embedding query text.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: The text to embed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Embedding for the text.
|
||||||
|
"""
|
||||||
|
embeddings = await self.aembed_documents([text])
|
||||||
|
return embeddings[0]
|
7
libs/partners/openai/langchain_openai/llms/__init__.py
Normal file
7
libs/partners/openai/langchain_openai/llms/__init__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from langchain_openai.llms.azure import AzureOpenAI
|
||||||
|
from langchain_openai.llms.base import OpenAI
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"OpenAI",
|
||||||
|
"AzureOpenAI",
|
||||||
|
]
|
190
libs/partners/openai/langchain_openai/llms/azure.py
Normal file
190
libs/partners/openai/langchain_openai/llms/azure.py
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Callable,
|
||||||
|
Dict,
|
||||||
|
List,
|
||||||
|
Mapping,
|
||||||
|
Union,
|
||||||
|
)
|
||||||
|
|
||||||
|
import openai
|
||||||
|
from langchain_core.pydantic_v1 import Field, root_validator
|
||||||
|
from langchain_core.utils import get_from_dict_or_env
|
||||||
|
|
||||||
|
from langchain_openai.llms.base import BaseOpenAI
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AzureOpenAI(BaseOpenAI):
|
||||||
|
"""Azure-specific OpenAI large language models.
|
||||||
|
|
||||||
|
To use, you should have the ``openai`` python package installed, and the
|
||||||
|
environment variable ``OPENAI_API_KEY`` set with your API key.
|
||||||
|
|
||||||
|
Any parameters that are valid to be passed to the openai.create call can be passed
|
||||||
|
in, even if not explicitly saved on this class.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from langchain_community.llms import AzureOpenAI
|
||||||
|
openai = AzureOpenAI(model_name="gpt-3.5-turbo-instruct")
|
||||||
|
"""
|
||||||
|
|
||||||
|
azure_endpoint: Union[str, None] = None
|
||||||
|
"""Your Azure endpoint, including the resource.
|
||||||
|
|
||||||
|
Automatically inferred from env var `AZURE_OPENAI_ENDPOINT` if not provided.
|
||||||
|
|
||||||
|
Example: `https://example-resource.azure.openai.com/`
|
||||||
|
"""
|
||||||
|
deployment_name: Union[str, None] = Field(default=None, alias="azure_deployment")
|
||||||
|
"""A model deployment.
|
||||||
|
|
||||||
|
If given sets the base client URL to include `/deployments/{azure_deployment}`.
|
||||||
|
Note: this means you won't be able to use non-deployment endpoints.
|
||||||
|
"""
|
||||||
|
openai_api_version: str = Field(default="", alias="api_version")
|
||||||
|
"""Automatically inferred from env var `OPENAI_API_VERSION` if not provided."""
|
||||||
|
openai_api_key: Union[str, None] = Field(default=None, alias="api_key")
|
||||||
|
"""Automatically inferred from env var `AZURE_OPENAI_API_KEY` if not provided."""
|
||||||
|
azure_ad_token: Union[str, None] = None
|
||||||
|
"""Your Azure Active Directory token.
|
||||||
|
|
||||||
|
Automatically inferred from env var `AZURE_OPENAI_AD_TOKEN` if not provided.
|
||||||
|
|
||||||
|
For more:
|
||||||
|
https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-id.
|
||||||
|
""" # noqa: E501
|
||||||
|
azure_ad_token_provider: Union[Callable[[], str], None] = None
|
||||||
|
"""A function that returns an Azure Active Directory token.
|
||||||
|
|
||||||
|
Will be invoked on every request.
|
||||||
|
"""
|
||||||
|
openai_api_type: str = ""
|
||||||
|
"""Legacy, for openai<1.0.0 support."""
|
||||||
|
validate_base_url: bool = True
|
||||||
|
"""For backwards compatibility. If legacy val openai_api_base is passed in, try to
|
||||||
|
infer if it is a base_url or azure_endpoint and update accordingly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_lc_namespace(cls) -> List[str]:
|
||||||
|
"""Get the namespace of the langchain object."""
|
||||||
|
return ["langchain", "llms", "openai"]
|
||||||
|
|
||||||
|
@root_validator()
|
||||||
|
def validate_environment(cls, values: Dict) -> Dict:
|
||||||
|
"""Validate that api key and python package exists in environment."""
|
||||||
|
if values["n"] < 1:
|
||||||
|
raise ValueError("n must be at least 1.")
|
||||||
|
if values["streaming"] and values["n"] > 1:
|
||||||
|
raise ValueError("Cannot stream results when n > 1.")
|
||||||
|
if values["streaming"] and values["best_of"] > 1:
|
||||||
|
raise ValueError("Cannot stream results when best_of > 1.")
|
||||||
|
|
||||||
|
# Check OPENAI_KEY for backwards compatibility.
|
||||||
|
# TODO: Remove OPENAI_API_KEY support to avoid possible conflict when using
|
||||||
|
# other forms of azure credentials.
|
||||||
|
values["openai_api_key"] = (
|
||||||
|
values["openai_api_key"]
|
||||||
|
or os.getenv("AZURE_OPENAI_API_KEY")
|
||||||
|
or os.getenv("OPENAI_API_KEY")
|
||||||
|
)
|
||||||
|
|
||||||
|
values["azure_endpoint"] = values["azure_endpoint"] or os.getenv(
|
||||||
|
"AZURE_OPENAI_ENDPOINT"
|
||||||
|
)
|
||||||
|
values["azure_ad_token"] = values["azure_ad_token"] or os.getenv(
|
||||||
|
"AZURE_OPENAI_AD_TOKEN"
|
||||||
|
)
|
||||||
|
values["openai_api_base"] = values["openai_api_base"] or os.getenv(
|
||||||
|
"OPENAI_API_BASE"
|
||||||
|
)
|
||||||
|
values["openai_proxy"] = get_from_dict_or_env(
|
||||||
|
values,
|
||||||
|
"openai_proxy",
|
||||||
|
"OPENAI_PROXY",
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
values["openai_organization"] = (
|
||||||
|
values["openai_organization"]
|
||||||
|
or os.getenv("OPENAI_ORG_ID")
|
||||||
|
or os.getenv("OPENAI_ORGANIZATION")
|
||||||
|
)
|
||||||
|
values["openai_api_version"] = values["openai_api_version"] or os.getenv(
|
||||||
|
"OPENAI_API_VERSION"
|
||||||
|
)
|
||||||
|
values["openai_api_type"] = get_from_dict_or_env(
|
||||||
|
values, "openai_api_type", "OPENAI_API_TYPE", default="azure"
|
||||||
|
)
|
||||||
|
# For backwards compatibility. Before openai v1, no distinction was made
|
||||||
|
# between azure_endpoint and base_url (openai_api_base).
|
||||||
|
openai_api_base = values["openai_api_base"]
|
||||||
|
if openai_api_base and values["validate_base_url"]:
|
||||||
|
if "/openai" not in openai_api_base:
|
||||||
|
values["openai_api_base"] = (
|
||||||
|
values["openai_api_base"].rstrip("/") + "/openai"
|
||||||
|
)
|
||||||
|
raise ValueError(
|
||||||
|
"As of openai>=1.0.0, Azure endpoints should be specified via "
|
||||||
|
"the `azure_endpoint` param not `openai_api_base` "
|
||||||
|
"(or alias `base_url`)."
|
||||||
|
)
|
||||||
|
if values["deployment_name"]:
|
||||||
|
raise ValueError(
|
||||||
|
"As of openai>=1.0.0, if `deployment_name` (or alias "
|
||||||
|
"`azure_deployment`) is specified then "
|
||||||
|
"`openai_api_base` (or alias `base_url`) should not be. "
|
||||||
|
"Instead use `deployment_name` (or alias `azure_deployment`) "
|
||||||
|
"and `azure_endpoint`."
|
||||||
|
)
|
||||||
|
values["deployment_name"] = None
|
||||||
|
client_params = {
|
||||||
|
"api_version": values["openai_api_version"],
|
||||||
|
"azure_endpoint": values["azure_endpoint"],
|
||||||
|
"azure_deployment": values["deployment_name"],
|
||||||
|
"api_key": values["openai_api_key"],
|
||||||
|
"azure_ad_token": values["azure_ad_token"],
|
||||||
|
"azure_ad_token_provider": values["azure_ad_token_provider"],
|
||||||
|
"organization": values["openai_organization"],
|
||||||
|
"base_url": values["openai_api_base"],
|
||||||
|
"timeout": values["request_timeout"],
|
||||||
|
"max_retries": values["max_retries"],
|
||||||
|
"default_headers": values["default_headers"],
|
||||||
|
"default_query": values["default_query"],
|
||||||
|
"http_client": values["http_client"],
|
||||||
|
}
|
||||||
|
values["client"] = openai.AzureOpenAI(**client_params).completions
|
||||||
|
values["async_client"] = openai.AsyncAzureOpenAI(**client_params).completions
|
||||||
|
|
||||||
|
return values
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _identifying_params(self) -> Mapping[str, Any]:
|
||||||
|
return {
|
||||||
|
**{"deployment_name": self.deployment_name},
|
||||||
|
**super()._identifying_params,
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _invocation_params(self) -> Dict[str, Any]:
|
||||||
|
openai_params = {"model": self.deployment_name}
|
||||||
|
return {**openai_params, **super()._invocation_params}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _llm_type(self) -> str:
|
||||||
|
"""Return type of llm."""
|
||||||
|
return "azure"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lc_attributes(self) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"openai_api_type": self.openai_api_type,
|
||||||
|
"openai_api_version": self.openai_api_version,
|
||||||
|
}
|
611
libs/partners/openai/langchain_openai/llms/base.py
Normal file
611
libs/partners/openai/langchain_openai/llms/base.py
Normal file
@ -0,0 +1,611 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from typing import (
|
||||||
|
AbstractSet,
|
||||||
|
Any,
|
||||||
|
AsyncIterator,
|
||||||
|
Collection,
|
||||||
|
Dict,
|
||||||
|
Iterator,
|
||||||
|
List,
|
||||||
|
Literal,
|
||||||
|
Mapping,
|
||||||
|
Optional,
|
||||||
|
Set,
|
||||||
|
Tuple,
|
||||||
|
Union,
|
||||||
|
)
|
||||||
|
|
||||||
|
import openai
|
||||||
|
import tiktoken
|
||||||
|
from langchain_core.callbacks import (
|
||||||
|
AsyncCallbackManagerForLLMRun,
|
||||||
|
CallbackManagerForLLMRun,
|
||||||
|
)
|
||||||
|
from langchain_core.language_models.llms import BaseLLM
|
||||||
|
from langchain_core.outputs import Generation, GenerationChunk, LLMResult
|
||||||
|
from langchain_core.pydantic_v1 import Field, root_validator
|
||||||
|
from langchain_core.utils import get_from_dict_or_env, get_pydantic_field_names
|
||||||
|
from langchain_core.utils.utils import build_extra_kwargs
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _update_token_usage(
|
||||||
|
keys: Set[str], response: Dict[str, Any], token_usage: Dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
"""Update token usage."""
|
||||||
|
_keys_to_use = keys.intersection(response["usage"])
|
||||||
|
for _key in _keys_to_use:
|
||||||
|
if _key not in token_usage:
|
||||||
|
token_usage[_key] = response["usage"][_key]
|
||||||
|
else:
|
||||||
|
token_usage[_key] += response["usage"][_key]
|
||||||
|
|
||||||
|
|
||||||
|
def _stream_response_to_generation_chunk(
|
||||||
|
stream_response: Dict[str, Any],
|
||||||
|
) -> GenerationChunk:
|
||||||
|
"""Convert a stream response to a generation chunk."""
|
||||||
|
if not stream_response["choices"]:
|
||||||
|
return GenerationChunk(text="")
|
||||||
|
return GenerationChunk(
|
||||||
|
text=stream_response["choices"][0]["text"],
|
||||||
|
generation_info=dict(
|
||||||
|
finish_reason=stream_response["choices"][0].get("finish_reason", None),
|
||||||
|
logprobs=stream_response["choices"][0].get("logprobs", None),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseOpenAI(BaseLLM):
|
||||||
|
"""Base OpenAI large language model class."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lc_secrets(self) -> Dict[str, str]:
|
||||||
|
return {"openai_api_key": "OPENAI_API_KEY"}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lc_attributes(self) -> Dict[str, Any]:
|
||||||
|
attributes: Dict[str, Any] = {}
|
||||||
|
if self.openai_api_base:
|
||||||
|
attributes["openai_api_base"] = self.openai_api_base
|
||||||
|
|
||||||
|
if self.openai_organization:
|
||||||
|
attributes["openai_organization"] = self.openai_organization
|
||||||
|
|
||||||
|
if self.openai_proxy:
|
||||||
|
attributes["openai_proxy"] = self.openai_proxy
|
||||||
|
|
||||||
|
return attributes
|
||||||
|
|
||||||
|
client: Any = Field(default=None, exclude=True) #: :meta private:
|
||||||
|
async_client: Any = Field(default=None, exclude=True) #: :meta private:
|
||||||
|
model_name: str = Field(default="gpt-3.5-turbo-instruct", alias="model")
|
||||||
|
"""Model name to use."""
|
||||||
|
temperature: float = 0.7
|
||||||
|
"""What sampling temperature to use."""
|
||||||
|
max_tokens: int = 256
|
||||||
|
"""The maximum number of tokens to generate in the completion.
|
||||||
|
-1 returns as many tokens as possible given the prompt and
|
||||||
|
the models maximal context size."""
|
||||||
|
top_p: float = 1
|
||||||
|
"""Total probability mass of tokens to consider at each step."""
|
||||||
|
frequency_penalty: float = 0
|
||||||
|
"""Penalizes repeated tokens according to frequency."""
|
||||||
|
presence_penalty: float = 0
|
||||||
|
"""Penalizes repeated tokens."""
|
||||||
|
n: int = 1
|
||||||
|
"""How many completions to generate for each prompt."""
|
||||||
|
best_of: int = 1
|
||||||
|
"""Generates best_of completions server-side and returns the "best"."""
|
||||||
|
model_kwargs: Dict[str, Any] = Field(default_factory=dict)
|
||||||
|
"""Holds any model parameters valid for `create` call not explicitly specified."""
|
||||||
|
# When updating this to use a SecretStr
|
||||||
|
# Check for classes that derive from this class (as some of them
|
||||||
|
# may assume openai_api_key is a str)
|
||||||
|
openai_api_key: Optional[str] = Field(default=None, alias="api_key")
|
||||||
|
"""Automatically inferred from env var `OPENAI_API_KEY` if not provided."""
|
||||||
|
openai_api_base: Optional[str] = Field(default=None, alias="base_url")
|
||||||
|
"""Base URL path for API requests, leave blank if not using a proxy or service
|
||||||
|
emulator."""
|
||||||
|
openai_organization: Optional[str] = Field(default=None, alias="organization")
|
||||||
|
"""Automatically inferred from env var `OPENAI_ORG_ID` if not provided."""
|
||||||
|
# to support explicit proxy for OpenAI
|
||||||
|
openai_proxy: Optional[str] = None
|
||||||
|
batch_size: int = 20
|
||||||
|
"""Batch size to use when passing multiple documents to generate."""
|
||||||
|
request_timeout: Union[float, Tuple[float, float], Any, None] = Field(
|
||||||
|
default=None, alias="timeout"
|
||||||
|
)
|
||||||
|
"""Timeout for requests to OpenAI completion API. Can be float, httpx.Timeout or
|
||||||
|
None."""
|
||||||
|
logit_bias: Optional[Dict[str, float]] = Field(default_factory=dict)
|
||||||
|
"""Adjust the probability of specific tokens being generated."""
|
||||||
|
max_retries: int = 2
|
||||||
|
"""Maximum number of retries to make when generating."""
|
||||||
|
streaming: bool = False
|
||||||
|
"""Whether to stream the results or not."""
|
||||||
|
allowed_special: Union[Literal["all"], AbstractSet[str]] = set()
|
||||||
|
"""Set of special tokens that are allowed。"""
|
||||||
|
disallowed_special: Union[Literal["all"], Collection[str]] = "all"
|
||||||
|
"""Set of special tokens that are not allowed。"""
|
||||||
|
tiktoken_model_name: Optional[str] = None
|
||||||
|
"""The model name to pass to tiktoken when using this class.
|
||||||
|
Tiktoken is used to count the number of tokens in documents to constrain
|
||||||
|
them to be under a certain limit. By default, when set to None, this will
|
||||||
|
be the same as the embedding model name. However, there are some cases
|
||||||
|
where you may want to use this Embedding class with a model name not
|
||||||
|
supported by tiktoken. This can include when using Azure embeddings or
|
||||||
|
when using one of the many model providers that expose an OpenAI-like
|
||||||
|
API but with different models. In those cases, in order to avoid erroring
|
||||||
|
when tiktoken is called, you can specify a model name to use here."""
|
||||||
|
default_headers: Union[Mapping[str, str], None] = None
|
||||||
|
default_query: Union[Mapping[str, object], None] = None
|
||||||
|
# Configure a custom httpx client. See the
|
||||||
|
# [httpx documentation](https://www.python-httpx.org/api/#client) for more details.
|
||||||
|
http_client: Union[Any, None] = None
|
||||||
|
"""Optional httpx.Client."""
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
"""Configuration for this pydantic object."""
|
||||||
|
|
||||||
|
allow_population_by_field_name = True
|
||||||
|
|
||||||
|
@root_validator(pre=True)
|
||||||
|
def build_extra(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Build extra kwargs from additional params that were passed in."""
|
||||||
|
all_required_field_names = get_pydantic_field_names(cls)
|
||||||
|
extra = values.get("model_kwargs", {})
|
||||||
|
values["model_kwargs"] = build_extra_kwargs(
|
||||||
|
extra, values, all_required_field_names
|
||||||
|
)
|
||||||
|
return values
|
||||||
|
|
||||||
|
@root_validator()
|
||||||
|
def validate_environment(cls, values: Dict) -> Dict:
|
||||||
|
"""Validate that api key and python package exists in environment."""
|
||||||
|
if values["n"] < 1:
|
||||||
|
raise ValueError("n must be at least 1.")
|
||||||
|
if values["streaming"] and values["n"] > 1:
|
||||||
|
raise ValueError("Cannot stream results when n > 1.")
|
||||||
|
if values["streaming"] and values["best_of"] > 1:
|
||||||
|
raise ValueError("Cannot stream results when best_of > 1.")
|
||||||
|
|
||||||
|
values["openai_api_key"] = get_from_dict_or_env(
|
||||||
|
values, "openai_api_key", "OPENAI_API_KEY"
|
||||||
|
)
|
||||||
|
values["openai_api_base"] = values["openai_api_base"] or os.getenv(
|
||||||
|
"OPENAI_API_BASE"
|
||||||
|
)
|
||||||
|
values["openai_proxy"] = get_from_dict_or_env(
|
||||||
|
values,
|
||||||
|
"openai_proxy",
|
||||||
|
"OPENAI_PROXY",
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
values["openai_organization"] = (
|
||||||
|
values["openai_organization"]
|
||||||
|
or os.getenv("OPENAI_ORG_ID")
|
||||||
|
or os.getenv("OPENAI_ORGANIZATION")
|
||||||
|
)
|
||||||
|
|
||||||
|
client_params = {
|
||||||
|
"api_key": values["openai_api_key"],
|
||||||
|
"organization": values["openai_organization"],
|
||||||
|
"base_url": values["openai_api_base"],
|
||||||
|
"timeout": values["request_timeout"],
|
||||||
|
"max_retries": values["max_retries"],
|
||||||
|
"default_headers": values["default_headers"],
|
||||||
|
"default_query": values["default_query"],
|
||||||
|
"http_client": values["http_client"],
|
||||||
|
}
|
||||||
|
if not values.get("client"):
|
||||||
|
values["client"] = openai.OpenAI(**client_params).completions
|
||||||
|
if not values.get("async_client"):
|
||||||
|
values["async_client"] = openai.AsyncOpenAI(**client_params).completions
|
||||||
|
|
||||||
|
return values
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _default_params(self) -> Dict[str, Any]:
|
||||||
|
"""Get the default parameters for calling OpenAI API."""
|
||||||
|
normal_params: Dict[str, Any] = {
|
||||||
|
"temperature": self.temperature,
|
||||||
|
"top_p": self.top_p,
|
||||||
|
"frequency_penalty": self.frequency_penalty,
|
||||||
|
"presence_penalty": self.presence_penalty,
|
||||||
|
"n": self.n,
|
||||||
|
"logit_bias": self.logit_bias,
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.max_tokens is not None:
|
||||||
|
normal_params["max_tokens"] = self.max_tokens
|
||||||
|
|
||||||
|
# Azure gpt-35-turbo doesn't support best_of
|
||||||
|
# don't specify best_of if it is 1
|
||||||
|
if self.best_of > 1:
|
||||||
|
normal_params["best_of"] = self.best_of
|
||||||
|
|
||||||
|
return {**normal_params, **self.model_kwargs}
|
||||||
|
|
||||||
|
def _stream(
|
||||||
|
self,
|
||||||
|
prompt: str,
|
||||||
|
stop: Optional[List[str]] = None,
|
||||||
|
run_manager: Optional[CallbackManagerForLLMRun] = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Iterator[GenerationChunk]:
|
||||||
|
params = {**self._invocation_params, **kwargs, "stream": True}
|
||||||
|
self.get_sub_prompts(params, [prompt], stop) # this mutates params
|
||||||
|
for stream_resp in self.client.create(prompt=prompt, **params):
|
||||||
|
if not isinstance(stream_resp, dict):
|
||||||
|
stream_resp = stream_resp.dict()
|
||||||
|
chunk = _stream_response_to_generation_chunk(stream_resp)
|
||||||
|
yield chunk
|
||||||
|
if run_manager:
|
||||||
|
run_manager.on_llm_new_token(
|
||||||
|
chunk.text,
|
||||||
|
chunk=chunk,
|
||||||
|
verbose=self.verbose,
|
||||||
|
logprobs=chunk.generation_info["logprobs"]
|
||||||
|
if chunk.generation_info
|
||||||
|
else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _astream(
|
||||||
|
self,
|
||||||
|
prompt: str,
|
||||||
|
stop: Optional[List[str]] = None,
|
||||||
|
run_manager: Optional[AsyncCallbackManagerForLLMRun] = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> AsyncIterator[GenerationChunk]:
|
||||||
|
params = {**self._invocation_params, **kwargs, "stream": True}
|
||||||
|
self.get_sub_prompts(params, [prompt], stop) # this mutates params
|
||||||
|
async for stream_resp in await self.async_client.create(
|
||||||
|
prompt=prompt, **params
|
||||||
|
):
|
||||||
|
if not isinstance(stream_resp, dict):
|
||||||
|
stream_resp = stream_resp.dict()
|
||||||
|
chunk = _stream_response_to_generation_chunk(stream_resp)
|
||||||
|
yield chunk
|
||||||
|
if run_manager:
|
||||||
|
await run_manager.on_llm_new_token(
|
||||||
|
chunk.text,
|
||||||
|
chunk=chunk,
|
||||||
|
verbose=self.verbose,
|
||||||
|
logprobs=chunk.generation_info["logprobs"]
|
||||||
|
if chunk.generation_info
|
||||||
|
else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _generate(
|
||||||
|
self,
|
||||||
|
prompts: List[str],
|
||||||
|
stop: Optional[List[str]] = None,
|
||||||
|
run_manager: Optional[CallbackManagerForLLMRun] = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> LLMResult:
|
||||||
|
"""Call out to OpenAI's endpoint with k unique prompts.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prompts: The prompts to pass into the model.
|
||||||
|
stop: Optional list of stop words to use when generating.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The full LLM output.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
response = openai.generate(["Tell me a joke."])
|
||||||
|
"""
|
||||||
|
# TODO: write a unit test for this
|
||||||
|
params = self._invocation_params
|
||||||
|
params = {**params, **kwargs}
|
||||||
|
sub_prompts = self.get_sub_prompts(params, prompts, stop)
|
||||||
|
choices = []
|
||||||
|
token_usage: Dict[str, int] = {}
|
||||||
|
# Get the token usage from the response.
|
||||||
|
# Includes prompt, completion, and total tokens used.
|
||||||
|
_keys = {"completion_tokens", "prompt_tokens", "total_tokens"}
|
||||||
|
system_fingerprint: Optional[str] = None
|
||||||
|
for _prompts in sub_prompts:
|
||||||
|
if self.streaming:
|
||||||
|
if len(_prompts) > 1:
|
||||||
|
raise ValueError("Cannot stream results with multiple prompts.")
|
||||||
|
|
||||||
|
generation: Optional[GenerationChunk] = None
|
||||||
|
for chunk in self._stream(_prompts[0], stop, run_manager, **kwargs):
|
||||||
|
if generation is None:
|
||||||
|
generation = chunk
|
||||||
|
else:
|
||||||
|
generation += chunk
|
||||||
|
assert generation is not None
|
||||||
|
choices.append(
|
||||||
|
{
|
||||||
|
"text": generation.text,
|
||||||
|
"finish_reason": generation.generation_info.get("finish_reason")
|
||||||
|
if generation.generation_info
|
||||||
|
else None,
|
||||||
|
"logprobs": generation.generation_info.get("logprobs")
|
||||||
|
if generation.generation_info
|
||||||
|
else None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
response = self.client.create(prompt=_prompts, **params)
|
||||||
|
if not isinstance(response, dict):
|
||||||
|
# V1 client returns the response in an PyDantic object instead of
|
||||||
|
# dict. For the transition period, we deep convert it to dict.
|
||||||
|
response = response.dict()
|
||||||
|
|
||||||
|
choices.extend(response["choices"])
|
||||||
|
_update_token_usage(_keys, response, token_usage)
|
||||||
|
if not system_fingerprint:
|
||||||
|
system_fingerprint = response.get("system_fingerprint")
|
||||||
|
return self.create_llm_result(
|
||||||
|
choices,
|
||||||
|
prompts,
|
||||||
|
params,
|
||||||
|
token_usage,
|
||||||
|
system_fingerprint=system_fingerprint,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _agenerate(
|
||||||
|
self,
|
||||||
|
prompts: List[str],
|
||||||
|
stop: Optional[List[str]] = None,
|
||||||
|
run_manager: Optional[AsyncCallbackManagerForLLMRun] = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> LLMResult:
|
||||||
|
"""Call out to OpenAI's endpoint async with k unique prompts."""
|
||||||
|
params = self._invocation_params
|
||||||
|
params = {**params, **kwargs}
|
||||||
|
sub_prompts = self.get_sub_prompts(params, prompts, stop)
|
||||||
|
choices = []
|
||||||
|
token_usage: Dict[str, int] = {}
|
||||||
|
# Get the token usage from the response.
|
||||||
|
# Includes prompt, completion, and total tokens used.
|
||||||
|
_keys = {"completion_tokens", "prompt_tokens", "total_tokens"}
|
||||||
|
system_fingerprint: Optional[str] = None
|
||||||
|
for _prompts in sub_prompts:
|
||||||
|
if self.streaming:
|
||||||
|
if len(_prompts) > 1:
|
||||||
|
raise ValueError("Cannot stream results with multiple prompts.")
|
||||||
|
|
||||||
|
generation: Optional[GenerationChunk] = None
|
||||||
|
async for chunk in self._astream(
|
||||||
|
_prompts[0], stop, run_manager, **kwargs
|
||||||
|
):
|
||||||
|
if generation is None:
|
||||||
|
generation = chunk
|
||||||
|
else:
|
||||||
|
generation += chunk
|
||||||
|
assert generation is not None
|
||||||
|
choices.append(
|
||||||
|
{
|
||||||
|
"text": generation.text,
|
||||||
|
"finish_reason": generation.generation_info.get("finish_reason")
|
||||||
|
if generation.generation_info
|
||||||
|
else None,
|
||||||
|
"logprobs": generation.generation_info.get("logprobs")
|
||||||
|
if generation.generation_info
|
||||||
|
else None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
response = await self.async_client.create(prompt=_prompts, **params)
|
||||||
|
if not isinstance(response, dict):
|
||||||
|
response = response.dict()
|
||||||
|
choices.extend(response["choices"])
|
||||||
|
_update_token_usage(_keys, response, token_usage)
|
||||||
|
return self.create_llm_result(
|
||||||
|
choices,
|
||||||
|
prompts,
|
||||||
|
params,
|
||||||
|
token_usage,
|
||||||
|
system_fingerprint=system_fingerprint,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_sub_prompts(
|
||||||
|
self,
|
||||||
|
params: Dict[str, Any],
|
||||||
|
prompts: List[str],
|
||||||
|
stop: Optional[List[str]] = None,
|
||||||
|
) -> List[List[str]]:
|
||||||
|
"""Get the sub prompts for llm call."""
|
||||||
|
if stop is not None:
|
||||||
|
if "stop" in params:
|
||||||
|
raise ValueError("`stop` found in both the input and default params.")
|
||||||
|
params["stop"] = stop
|
||||||
|
if params["max_tokens"] == -1:
|
||||||
|
if len(prompts) != 1:
|
||||||
|
raise ValueError(
|
||||||
|
"max_tokens set to -1 not supported for multiple inputs."
|
||||||
|
)
|
||||||
|
params["max_tokens"] = self.max_tokens_for_prompt(prompts[0])
|
||||||
|
sub_prompts = [
|
||||||
|
prompts[i : i + self.batch_size]
|
||||||
|
for i in range(0, len(prompts), self.batch_size)
|
||||||
|
]
|
||||||
|
return sub_prompts
|
||||||
|
|
||||||
|
def create_llm_result(
|
||||||
|
self,
|
||||||
|
choices: Any,
|
||||||
|
prompts: List[str],
|
||||||
|
params: Dict[str, Any],
|
||||||
|
token_usage: Dict[str, int],
|
||||||
|
*,
|
||||||
|
system_fingerprint: Optional[str] = None,
|
||||||
|
) -> LLMResult:
|
||||||
|
"""Create the LLMResult from the choices and prompts."""
|
||||||
|
generations = []
|
||||||
|
n = params.get("n", self.n)
|
||||||
|
for i, _ in enumerate(prompts):
|
||||||
|
sub_choices = choices[i * n : (i + 1) * n]
|
||||||
|
generations.append(
|
||||||
|
[
|
||||||
|
Generation(
|
||||||
|
text=choice["text"],
|
||||||
|
generation_info=dict(
|
||||||
|
finish_reason=choice.get("finish_reason"),
|
||||||
|
logprobs=choice.get("logprobs"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for choice in sub_choices
|
||||||
|
]
|
||||||
|
)
|
||||||
|
llm_output = {"token_usage": token_usage, "model_name": self.model_name}
|
||||||
|
if system_fingerprint:
|
||||||
|
llm_output["system_fingerprint"] = system_fingerprint
|
||||||
|
return LLMResult(generations=generations, llm_output=llm_output)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _invocation_params(self) -> Dict[str, Any]:
|
||||||
|
"""Get the parameters used to invoke the model."""
|
||||||
|
return self._default_params
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _identifying_params(self) -> Mapping[str, Any]:
|
||||||
|
"""Get the identifying parameters."""
|
||||||
|
return {**{"model_name": self.model_name}, **self._default_params}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _llm_type(self) -> str:
|
||||||
|
"""Return type of llm."""
|
||||||
|
return "openai"
|
||||||
|
|
||||||
|
def get_token_ids(self, text: str) -> List[int]:
|
||||||
|
"""Get the token IDs using the tiktoken package."""
|
||||||
|
# tiktoken NOT supported for Python < 3.8
|
||||||
|
if sys.version_info[1] < 8:
|
||||||
|
return super().get_num_tokens(text)
|
||||||
|
|
||||||
|
model_name = self.tiktoken_model_name or self.model_name
|
||||||
|
try:
|
||||||
|
enc = tiktoken.encoding_for_model(model_name)
|
||||||
|
except KeyError:
|
||||||
|
logger.warning("Warning: model not found. Using cl100k_base encoding.")
|
||||||
|
model = "cl100k_base"
|
||||||
|
enc = tiktoken.get_encoding(model)
|
||||||
|
|
||||||
|
return enc.encode(
|
||||||
|
text,
|
||||||
|
allowed_special=self.allowed_special,
|
||||||
|
disallowed_special=self.disallowed_special,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def modelname_to_contextsize(modelname: str) -> int:
|
||||||
|
"""Calculate the maximum number of tokens possible to generate for a model.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
modelname: The modelname we want to know the context size for.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The maximum context size
|
||||||
|
|
||||||
|
Example:
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
max_tokens = openai.modelname_to_contextsize("gpt-3.5-turbo-instruct")
|
||||||
|
"""
|
||||||
|
model_token_mapping = {
|
||||||
|
"gpt-4": 8192,
|
||||||
|
"gpt-4-0314": 8192,
|
||||||
|
"gpt-4-0613": 8192,
|
||||||
|
"gpt-4-32k": 32768,
|
||||||
|
"gpt-4-32k-0314": 32768,
|
||||||
|
"gpt-4-32k-0613": 32768,
|
||||||
|
"gpt-3.5-turbo": 4096,
|
||||||
|
"gpt-3.5-turbo-0301": 4096,
|
||||||
|
"gpt-3.5-turbo-0613": 4096,
|
||||||
|
"gpt-3.5-turbo-16k": 16385,
|
||||||
|
"gpt-3.5-turbo-16k-0613": 16385,
|
||||||
|
"gpt-3.5-turbo-instruct": 4096,
|
||||||
|
"text-ada-001": 2049,
|
||||||
|
"ada": 2049,
|
||||||
|
"text-babbage-001": 2040,
|
||||||
|
"babbage": 2049,
|
||||||
|
"text-curie-001": 2049,
|
||||||
|
"curie": 2049,
|
||||||
|
"davinci": 2049,
|
||||||
|
"text-davinci-003": 4097,
|
||||||
|
"text-davinci-002": 4097,
|
||||||
|
"code-davinci-002": 8001,
|
||||||
|
"code-davinci-001": 8001,
|
||||||
|
"code-cushman-002": 2048,
|
||||||
|
"code-cushman-001": 2048,
|
||||||
|
}
|
||||||
|
|
||||||
|
# handling finetuned models
|
||||||
|
if "ft-" in modelname:
|
||||||
|
modelname = modelname.split(":")[0]
|
||||||
|
|
||||||
|
context_size = model_token_mapping.get(modelname, None)
|
||||||
|
|
||||||
|
if context_size is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unknown model: {modelname}. Please provide a valid OpenAI model name."
|
||||||
|
"Known models are: " + ", ".join(model_token_mapping.keys())
|
||||||
|
)
|
||||||
|
|
||||||
|
return context_size
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_context_size(self) -> int:
|
||||||
|
"""Get max context size for this model."""
|
||||||
|
return self.modelname_to_contextsize(self.model_name)
|
||||||
|
|
||||||
|
def max_tokens_for_prompt(self, prompt: str) -> int:
|
||||||
|
"""Calculate the maximum number of tokens possible to generate for a prompt.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prompt: The prompt to pass into the model.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The maximum number of tokens to generate for a prompt.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
max_tokens = openai.max_token_for_prompt("Tell me a joke.")
|
||||||
|
"""
|
||||||
|
num_tokens = self.get_num_tokens(prompt)
|
||||||
|
return self.max_context_size - num_tokens
|
||||||
|
|
||||||
|
|
||||||
|
class OpenAI(BaseOpenAI):
|
||||||
|
"""OpenAI large language models.
|
||||||
|
|
||||||
|
To use, you should have the ``openai`` python package installed, and the
|
||||||
|
environment variable ``OPENAI_API_KEY`` set with your API key.
|
||||||
|
|
||||||
|
Any parameters that are valid to be passed to the openai.create call can be passed
|
||||||
|
in, even if not explicitly saved on this class.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from langchain_community.llms import OpenAI
|
||||||
|
openai = OpenAI(model_name="gpt-3.5-turbo-instruct")
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_lc_namespace(cls) -> List[str]:
|
||||||
|
"""Get the namespace of the langchain object."""
|
||||||
|
return ["langchain", "llms", "openai"]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_lc_serializable(cls) -> bool:
|
||||||
|
"""Return whether this model can be serialized by Langchain."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _invocation_params(self) -> Dict[str, Any]:
|
||||||
|
return {**{"model": self.model_name}, **super()._invocation_params}
|
0
libs/partners/openai/langchain_openai/py.typed
Normal file
0
libs/partners/openai/langchain_openai/py.typed
Normal file
1140
libs/partners/openai/poetry.lock
generated
Normal file
1140
libs/partners/openai/poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
97
libs/partners/openai/pyproject.toml
Normal file
97
libs/partners/openai/pyproject.toml
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
[tool.poetry]
|
||||||
|
name = "langchain-openai"
|
||||||
|
version = "0.0.1"
|
||||||
|
description = "An integration package connecting OpenAI and LangChain"
|
||||||
|
authors = []
|
||||||
|
readme = "README.md"
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = ">=3.8.1,<4.0"
|
||||||
|
langchain-core = ">=0.0.12"
|
||||||
|
openai = "^1.6.1"
|
||||||
|
numpy = "^1"
|
||||||
|
tiktoken = "^0.5.2"
|
||||||
|
|
||||||
|
[tool.poetry.group.test]
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[tool.poetry.group.test.dependencies]
|
||||||
|
pytest = "^7.3.0"
|
||||||
|
freezegun = "^1.2.2"
|
||||||
|
pytest-mock = "^3.10.0"
|
||||||
|
syrupy = "^4.0.2"
|
||||||
|
pytest-watcher = "^0.3.4"
|
||||||
|
pytest-asyncio = "^0.21.1"
|
||||||
|
langchain-core = {path = "../../core", develop = true}
|
||||||
|
|
||||||
|
[tool.poetry.group.codespell]
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[tool.poetry.group.codespell.dependencies]
|
||||||
|
codespell = "^2.2.0"
|
||||||
|
|
||||||
|
[tool.poetry.group.test_integration]
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[tool.poetry.group.test_integration.dependencies]
|
||||||
|
|
||||||
|
[tool.poetry.group.lint]
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[tool.poetry.group.lint.dependencies]
|
||||||
|
ruff = "^0.1.5"
|
||||||
|
|
||||||
|
[tool.poetry.group.typing.dependencies]
|
||||||
|
mypy = "^0.991"
|
||||||
|
langchain-core = {path = "../../core", develop = true}
|
||||||
|
types-tqdm = "^4.66.0.5"
|
||||||
|
|
||||||
|
[tool.poetry.group.dev]
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
langchain-core = {path = "../../core", develop = true}
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
select = [
|
||||||
|
"E", # pycodestyle
|
||||||
|
"F", # pyflakes
|
||||||
|
"I", # isort
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
disallow_untyped_defs = "True"
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = "transformers"
|
||||||
|
ignore_missing_imports = true
|
||||||
|
|
||||||
|
[tool.coverage.run]
|
||||||
|
omit = [
|
||||||
|
"tests/*",
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
# --strict-markers will raise errors on unknown marks.
|
||||||
|
# https://docs.pytest.org/en/7.1.x/how-to/mark.html#raising-errors-on-unknown-marks
|
||||||
|
#
|
||||||
|
# https://docs.pytest.org/en/7.1.x/reference/reference.html
|
||||||
|
# --strict-config any warnings encountered while parsing the `pytest`
|
||||||
|
# section of the configuration file raise errors.
|
||||||
|
#
|
||||||
|
# https://github.com/tophat/syrupy
|
||||||
|
# --snapshot-warn-unused Prints a warning on unused snapshots rather than fail the test suite.
|
||||||
|
addopts = "--snapshot-warn-unused --strict-markers --strict-config --durations=5"
|
||||||
|
# Registering custom markers.
|
||||||
|
# https://docs.pytest.org/en/7.1.x/example/markers.html#registering-markers
|
||||||
|
markers = [
|
||||||
|
"requires: mark tests as requiring a specific library",
|
||||||
|
"asyncio: mark tests as requiring asyncio",
|
||||||
|
"compile: mark placeholder test used to compile integration tests without running them",
|
||||||
|
"scheduled: mark tests to run in scheduled testing",
|
||||||
|
]
|
||||||
|
asyncio_mode = "auto"
|
17
libs/partners/openai/scripts/check_imports.py
Normal file
17
libs/partners/openai/scripts/check_imports.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
from importlib.machinery import SourceFileLoader
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
files = sys.argv[1:]
|
||||||
|
has_failure = False
|
||||||
|
for file in files:
|
||||||
|
try:
|
||||||
|
SourceFileLoader("x", file).load_module()
|
||||||
|
except Exception:
|
||||||
|
has_faillure = True
|
||||||
|
print(file)
|
||||||
|
traceback.print_exc()
|
||||||
|
print()
|
||||||
|
|
||||||
|
sys.exit(1 if has_failure else 0)
|
27
libs/partners/openai/scripts/check_pydantic.sh
Executable file
27
libs/partners/openai/scripts/check_pydantic.sh
Executable file
@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# This script searches for lines starting with "import pydantic" or "from pydantic"
|
||||||
|
# in tracked files within a Git repository.
|
||||||
|
#
|
||||||
|
# Usage: ./scripts/check_pydantic.sh /path/to/repository
|
||||||
|
|
||||||
|
# Check if a path argument is provided
|
||||||
|
if [ $# -ne 1 ]; then
|
||||||
|
echo "Usage: $0 /path/to/repository"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
repository_path="$1"
|
||||||
|
|
||||||
|
# Search for lines matching the pattern within the specified repository
|
||||||
|
result=$(git -C "$repository_path" grep -E '^import pydantic|^from pydantic')
|
||||||
|
|
||||||
|
# Check if any matching lines were found
|
||||||
|
if [ -n "$result" ]; then
|
||||||
|
echo "ERROR: The following lines need to be updated:"
|
||||||
|
echo "$result"
|
||||||
|
echo "Please replace the code with an import from langchain_core.pydantic_v1."
|
||||||
|
echo "For example, replace 'from pydantic import BaseModel'"
|
||||||
|
echo "with 'from langchain_core.pydantic_v1 import BaseModel'"
|
||||||
|
exit 1
|
||||||
|
fi
|
17
libs/partners/openai/scripts/lint_imports.sh
Executable file
17
libs/partners/openai/scripts/lint_imports.sh
Executable file
@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
# Initialize a variable to keep track of errors
|
||||||
|
errors=0
|
||||||
|
|
||||||
|
# make sure not importing from langchain or langchain_experimental
|
||||||
|
git --no-pager grep '^from langchain\.' . && errors=$((errors+1))
|
||||||
|
git --no-pager grep '^from langchain_experimental\.' . && errors=$((errors+1))
|
||||||
|
|
||||||
|
# Decide on an exit status based on the errors
|
||||||
|
if [ "$errors" -gt 0 ]; then
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
exit 0
|
||||||
|
fi
|
0
libs/partners/openai/tests/__init__.py
Normal file
0
libs/partners/openai/tests/__init__.py
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
"""Test AzureChatOpenAI wrapper."""
|
||||||
|
import os
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from langchain_core.callbacks import CallbackManager
|
||||||
|
from langchain_core.messages import BaseMessage, HumanMessage
|
||||||
|
from langchain_core.outputs import ChatGeneration, ChatResult, LLMResult
|
||||||
|
|
||||||
|
from langchain_openai import AzureChatOpenAI
|
||||||
|
from tests.unit_tests.fake.callbacks import FakeCallbackHandler
|
||||||
|
|
||||||
|
OPENAI_API_VERSION = os.environ.get("AZURE_OPENAI_API_VERSION", "")
|
||||||
|
OPENAI_API_BASE = os.environ.get("AZURE_OPENAI_API_BASE", "")
|
||||||
|
OPENAI_API_KEY = os.environ.get("AZURE_OPENAI_API_KEY", "")
|
||||||
|
DEPLOYMENT_NAME = os.environ.get(
|
||||||
|
"AZURE_OPENAI_DEPLOYMENT_NAME",
|
||||||
|
os.environ.get("AZURE_OPENAI_CHAT_DEPLOYMENT_NAME", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_llm(**kwargs: Any) -> AzureChatOpenAI:
|
||||||
|
return AzureChatOpenAI(
|
||||||
|
deployment_name=DEPLOYMENT_NAME,
|
||||||
|
openai_api_version=OPENAI_API_VERSION,
|
||||||
|
azure_endpoint=OPENAI_API_BASE,
|
||||||
|
openai_api_key=OPENAI_API_KEY,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
@pytest.fixture
|
||||||
|
def llm() -> AzureChatOpenAI:
|
||||||
|
return _get_llm(
|
||||||
|
max_tokens=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_chat_openai(llm: AzureChatOpenAI) -> None:
|
||||||
|
"""Test AzureChatOpenAI wrapper."""
|
||||||
|
message = HumanMessage(content="Hello")
|
||||||
|
response = llm([message])
|
||||||
|
assert isinstance(response, BaseMessage)
|
||||||
|
assert isinstance(response.content, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_chat_openai_generate() -> None:
|
||||||
|
"""Test AzureChatOpenAI wrapper with generate."""
|
||||||
|
chat = _get_llm(max_tokens=10, n=2)
|
||||||
|
message = HumanMessage(content="Hello")
|
||||||
|
response = chat.generate([[message], [message]])
|
||||||
|
assert isinstance(response, LLMResult)
|
||||||
|
assert len(response.generations) == 2
|
||||||
|
for generations in response.generations:
|
||||||
|
assert len(generations) == 2
|
||||||
|
for generation in generations:
|
||||||
|
assert isinstance(generation, ChatGeneration)
|
||||||
|
assert isinstance(generation.text, str)
|
||||||
|
assert generation.text == generation.message.content
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_chat_openai_multiple_completions() -> None:
|
||||||
|
"""Test AzureChatOpenAI wrapper with multiple completions."""
|
||||||
|
chat = _get_llm(max_tokens=10, n=5)
|
||||||
|
message = HumanMessage(content="Hello")
|
||||||
|
response = chat._generate([message])
|
||||||
|
assert isinstance(response, ChatResult)
|
||||||
|
assert len(response.generations) == 5
|
||||||
|
for generation in response.generations:
|
||||||
|
assert isinstance(generation.message, BaseMessage)
|
||||||
|
assert isinstance(generation.message.content, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_chat_openai_streaming() -> None:
|
||||||
|
"""Test that streaming correctly invokes on_llm_new_token callback."""
|
||||||
|
callback_handler = FakeCallbackHandler()
|
||||||
|
callback_manager = CallbackManager([callback_handler])
|
||||||
|
chat = _get_llm(
|
||||||
|
max_tokens=10,
|
||||||
|
streaming=True,
|
||||||
|
temperature=0,
|
||||||
|
callback_manager=callback_manager,
|
||||||
|
verbose=True,
|
||||||
|
)
|
||||||
|
message = HumanMessage(content="Hello")
|
||||||
|
response = chat([message])
|
||||||
|
assert callback_handler.llm_streams > 0
|
||||||
|
assert isinstance(response, BaseMessage)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_chat_openai_streaming_generation_info() -> None:
|
||||||
|
"""Test that generation info is preserved when streaming."""
|
||||||
|
|
||||||
|
class _FakeCallback(FakeCallbackHandler):
|
||||||
|
saved_things: dict = {}
|
||||||
|
|
||||||
|
def on_llm_end(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Any:
|
||||||
|
# Save the generation
|
||||||
|
self.saved_things["generation"] = args[0]
|
||||||
|
|
||||||
|
callback = _FakeCallback()
|
||||||
|
callback_manager = CallbackManager([callback])
|
||||||
|
chat = _get_llm(
|
||||||
|
max_tokens=2,
|
||||||
|
temperature=0,
|
||||||
|
callback_manager=callback_manager,
|
||||||
|
)
|
||||||
|
list(chat.stream("hi"))
|
||||||
|
generation = callback.saved_things["generation"]
|
||||||
|
# `Hello!` is two tokens, assert that that is what is returned
|
||||||
|
assert generation.generations[0][0].text == "Hello!"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
async def test_async_chat_openai() -> None:
|
||||||
|
"""Test async generation."""
|
||||||
|
chat = _get_llm(max_tokens=10, n=2)
|
||||||
|
message = HumanMessage(content="Hello")
|
||||||
|
response = await chat.agenerate([[message], [message]])
|
||||||
|
assert isinstance(response, LLMResult)
|
||||||
|
assert len(response.generations) == 2
|
||||||
|
for generations in response.generations:
|
||||||
|
assert len(generations) == 2
|
||||||
|
for generation in generations:
|
||||||
|
assert isinstance(generation, ChatGeneration)
|
||||||
|
assert isinstance(generation.text, str)
|
||||||
|
assert generation.text == generation.message.content
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
async def test_async_chat_openai_streaming() -> None:
|
||||||
|
"""Test that streaming correctly invokes on_llm_new_token callback."""
|
||||||
|
callback_handler = FakeCallbackHandler()
|
||||||
|
callback_manager = CallbackManager([callback_handler])
|
||||||
|
chat = _get_llm(
|
||||||
|
max_tokens=10,
|
||||||
|
streaming=True,
|
||||||
|
temperature=0,
|
||||||
|
callback_manager=callback_manager,
|
||||||
|
verbose=True,
|
||||||
|
)
|
||||||
|
message = HumanMessage(content="Hello")
|
||||||
|
response = await chat.agenerate([[message], [message]])
|
||||||
|
assert callback_handler.llm_streams > 0
|
||||||
|
assert isinstance(response, LLMResult)
|
||||||
|
assert len(response.generations) == 2
|
||||||
|
for generations in response.generations:
|
||||||
|
assert len(generations) == 1
|
||||||
|
for generation in generations:
|
||||||
|
assert isinstance(generation, ChatGeneration)
|
||||||
|
assert isinstance(generation.text, str)
|
||||||
|
assert generation.text == generation.message.content
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_openai_streaming(llm: AzureChatOpenAI) -> None:
|
||||||
|
"""Test streaming tokens from OpenAI."""
|
||||||
|
|
||||||
|
for token in llm.stream("I'm Pickle Rick"):
|
||||||
|
assert isinstance(token.content, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
async def test_openai_astream(llm: AzureChatOpenAI) -> None:
|
||||||
|
"""Test streaming tokens from OpenAI."""
|
||||||
|
async for token in llm.astream("I'm Pickle Rick"):
|
||||||
|
assert isinstance(token.content, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
async def test_openai_abatch(llm: AzureChatOpenAI) -> None:
|
||||||
|
"""Test streaming tokens from AzureChatOpenAI."""
|
||||||
|
|
||||||
|
result = await llm.abatch(["I'm Pickle Rick", "I'm not Pickle Rick"])
|
||||||
|
for token in result:
|
||||||
|
assert isinstance(token.content, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
async def test_openai_abatch_tags(llm: AzureChatOpenAI) -> None:
|
||||||
|
"""Test batch tokens from AzureChatOpenAI."""
|
||||||
|
|
||||||
|
result = await llm.abatch(
|
||||||
|
["I'm Pickle Rick", "I'm not Pickle Rick"], config={"tags": ["foo"]}
|
||||||
|
)
|
||||||
|
for token in result:
|
||||||
|
assert isinstance(token.content, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_openai_batch(llm: AzureChatOpenAI) -> None:
|
||||||
|
"""Test batch tokens from AzureChatOpenAI."""
|
||||||
|
|
||||||
|
result = llm.batch(["I'm Pickle Rick", "I'm not Pickle Rick"])
|
||||||
|
for token in result:
|
||||||
|
assert isinstance(token.content, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
async def test_openai_ainvoke(llm: AzureChatOpenAI) -> None:
|
||||||
|
"""Test invoke tokens from AzureChatOpenAI."""
|
||||||
|
|
||||||
|
result = await llm.ainvoke("I'm Pickle Rick", config={"tags": ["foo"]})
|
||||||
|
assert isinstance(result.content, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_openai_invoke(llm: AzureChatOpenAI) -> None:
|
||||||
|
"""Test invoke tokens from AzureChatOpenAI."""
|
||||||
|
|
||||||
|
result = llm.invoke("I'm Pickle Rick", config=dict(tags=["foo"]))
|
||||||
|
assert isinstance(result.content, str)
|
@ -0,0 +1,393 @@
|
|||||||
|
"""Test ChatOpenAI chat model."""
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from langchain_core.callbacks import CallbackManager
|
||||||
|
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage
|
||||||
|
from langchain_core.outputs import (
|
||||||
|
ChatGeneration,
|
||||||
|
ChatResult,
|
||||||
|
LLMResult,
|
||||||
|
)
|
||||||
|
from langchain_core.prompts import ChatPromptTemplate
|
||||||
|
from langchain_core.pydantic_v1 import BaseModel, Field
|
||||||
|
|
||||||
|
from langchain_openai import ChatOpenAI
|
||||||
|
from tests.unit_tests.fake.callbacks import FakeCallbackHandler
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_chat_openai() -> None:
|
||||||
|
"""Test ChatOpenAI wrapper."""
|
||||||
|
chat = ChatOpenAI(
|
||||||
|
temperature=0.7,
|
||||||
|
base_url=None,
|
||||||
|
organization=None,
|
||||||
|
openai_proxy=None,
|
||||||
|
timeout=10.0,
|
||||||
|
max_retries=3,
|
||||||
|
http_client=None,
|
||||||
|
n=1,
|
||||||
|
max_tokens=10,
|
||||||
|
default_headers=None,
|
||||||
|
default_query=None,
|
||||||
|
)
|
||||||
|
message = HumanMessage(content="Hello")
|
||||||
|
response = chat([message])
|
||||||
|
assert isinstance(response, BaseMessage)
|
||||||
|
assert isinstance(response.content, str)
|
||||||
|
|
||||||
|
|
||||||
|
def test_chat_openai_model() -> None:
|
||||||
|
"""Test ChatOpenAI wrapper handles model_name."""
|
||||||
|
chat = ChatOpenAI(model="foo")
|
||||||
|
assert chat.model_name == "foo"
|
||||||
|
chat = ChatOpenAI(model_name="bar")
|
||||||
|
assert chat.model_name == "bar"
|
||||||
|
|
||||||
|
|
||||||
|
def test_chat_openai_system_message() -> None:
|
||||||
|
"""Test ChatOpenAI wrapper with system message."""
|
||||||
|
chat = ChatOpenAI(max_tokens=10)
|
||||||
|
system_message = SystemMessage(content="You are to chat with the user.")
|
||||||
|
human_message = HumanMessage(content="Hello")
|
||||||
|
response = chat([system_message, human_message])
|
||||||
|
assert isinstance(response, BaseMessage)
|
||||||
|
assert isinstance(response.content, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_chat_openai_generate() -> None:
|
||||||
|
"""Test ChatOpenAI wrapper with generate."""
|
||||||
|
chat = ChatOpenAI(max_tokens=10, n=2)
|
||||||
|
message = HumanMessage(content="Hello")
|
||||||
|
response = chat.generate([[message], [message]])
|
||||||
|
assert isinstance(response, LLMResult)
|
||||||
|
assert len(response.generations) == 2
|
||||||
|
assert response.llm_output
|
||||||
|
for generations in response.generations:
|
||||||
|
assert len(generations) == 2
|
||||||
|
for generation in generations:
|
||||||
|
assert isinstance(generation, ChatGeneration)
|
||||||
|
assert isinstance(generation.text, str)
|
||||||
|
assert generation.text == generation.message.content
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_chat_openai_multiple_completions() -> None:
|
||||||
|
"""Test ChatOpenAI wrapper with multiple completions."""
|
||||||
|
chat = ChatOpenAI(max_tokens=10, n=5)
|
||||||
|
message = HumanMessage(content="Hello")
|
||||||
|
response = chat._generate([message])
|
||||||
|
assert isinstance(response, ChatResult)
|
||||||
|
assert len(response.generations) == 5
|
||||||
|
for generation in response.generations:
|
||||||
|
assert isinstance(generation.message, BaseMessage)
|
||||||
|
assert isinstance(generation.message.content, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_chat_openai_streaming() -> None:
|
||||||
|
"""Test that streaming correctly invokes on_llm_new_token callback."""
|
||||||
|
callback_handler = FakeCallbackHandler()
|
||||||
|
callback_manager = CallbackManager([callback_handler])
|
||||||
|
chat = ChatOpenAI(
|
||||||
|
max_tokens=10,
|
||||||
|
streaming=True,
|
||||||
|
temperature=0,
|
||||||
|
callback_manager=callback_manager,
|
||||||
|
verbose=True,
|
||||||
|
)
|
||||||
|
message = HumanMessage(content="Hello")
|
||||||
|
response = chat([message])
|
||||||
|
assert callback_handler.llm_streams > 0
|
||||||
|
assert isinstance(response, BaseMessage)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_chat_openai_streaming_generation_info() -> None:
|
||||||
|
"""Test that generation info is preserved when streaming."""
|
||||||
|
|
||||||
|
class _FakeCallback(FakeCallbackHandler):
|
||||||
|
saved_things: dict = {}
|
||||||
|
|
||||||
|
def on_llm_end(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Any:
|
||||||
|
# Save the generation
|
||||||
|
self.saved_things["generation"] = args[0]
|
||||||
|
|
||||||
|
callback = _FakeCallback()
|
||||||
|
callback_manager = CallbackManager([callback])
|
||||||
|
chat = ChatOpenAI(
|
||||||
|
max_tokens=2,
|
||||||
|
temperature=0,
|
||||||
|
callback_manager=callback_manager,
|
||||||
|
)
|
||||||
|
list(chat.stream("hi"))
|
||||||
|
generation = callback.saved_things["generation"]
|
||||||
|
# `Hello!` is two tokens, assert that that is what is returned
|
||||||
|
assert generation.generations[0][0].text == "Hello!"
|
||||||
|
|
||||||
|
|
||||||
|
def test_chat_openai_llm_output_contains_model_name() -> None:
|
||||||
|
"""Test llm_output contains model_name."""
|
||||||
|
chat = ChatOpenAI(max_tokens=10)
|
||||||
|
message = HumanMessage(content="Hello")
|
||||||
|
llm_result = chat.generate([[message]])
|
||||||
|
assert llm_result.llm_output is not None
|
||||||
|
assert llm_result.llm_output["model_name"] == chat.model_name
|
||||||
|
|
||||||
|
|
||||||
|
def test_chat_openai_streaming_llm_output_contains_model_name() -> None:
|
||||||
|
"""Test llm_output contains model_name."""
|
||||||
|
chat = ChatOpenAI(max_tokens=10, streaming=True)
|
||||||
|
message = HumanMessage(content="Hello")
|
||||||
|
llm_result = chat.generate([[message]])
|
||||||
|
assert llm_result.llm_output is not None
|
||||||
|
assert llm_result.llm_output["model_name"] == chat.model_name
|
||||||
|
|
||||||
|
|
||||||
|
def test_chat_openai_invalid_streaming_params() -> None:
|
||||||
|
"""Test that streaming correctly invokes on_llm_new_token callback."""
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ChatOpenAI(
|
||||||
|
max_tokens=10,
|
||||||
|
streaming=True,
|
||||||
|
temperature=0,
|
||||||
|
n=5,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
async def test_async_chat_openai() -> None:
|
||||||
|
"""Test async generation."""
|
||||||
|
chat = ChatOpenAI(max_tokens=10, n=2)
|
||||||
|
message = HumanMessage(content="Hello")
|
||||||
|
response = await chat.agenerate([[message], [message]])
|
||||||
|
assert isinstance(response, LLMResult)
|
||||||
|
assert len(response.generations) == 2
|
||||||
|
assert response.llm_output
|
||||||
|
for generations in response.generations:
|
||||||
|
assert len(generations) == 2
|
||||||
|
for generation in generations:
|
||||||
|
assert isinstance(generation, ChatGeneration)
|
||||||
|
assert isinstance(generation.text, str)
|
||||||
|
assert generation.text == generation.message.content
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
async def test_async_chat_openai_streaming() -> None:
|
||||||
|
"""Test that streaming correctly invokes on_llm_new_token callback."""
|
||||||
|
callback_handler = FakeCallbackHandler()
|
||||||
|
callback_manager = CallbackManager([callback_handler])
|
||||||
|
chat = ChatOpenAI(
|
||||||
|
max_tokens=10,
|
||||||
|
streaming=True,
|
||||||
|
temperature=0,
|
||||||
|
callback_manager=callback_manager,
|
||||||
|
verbose=True,
|
||||||
|
)
|
||||||
|
message = HumanMessage(content="Hello")
|
||||||
|
response = await chat.agenerate([[message], [message]])
|
||||||
|
assert callback_handler.llm_streams > 0
|
||||||
|
assert isinstance(response, LLMResult)
|
||||||
|
assert len(response.generations) == 2
|
||||||
|
for generations in response.generations:
|
||||||
|
assert len(generations) == 1
|
||||||
|
for generation in generations:
|
||||||
|
assert isinstance(generation, ChatGeneration)
|
||||||
|
assert isinstance(generation.text, str)
|
||||||
|
assert generation.text == generation.message.content
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
async def test_async_chat_openai_bind_functions() -> None:
|
||||||
|
"""Test ChatOpenAI wrapper with multiple completions."""
|
||||||
|
|
||||||
|
class Person(BaseModel):
|
||||||
|
"""Identifying information about a person."""
|
||||||
|
|
||||||
|
name: str = Field(..., title="Name", description="The person's name")
|
||||||
|
age: int = Field(..., title="Age", description="The person's age")
|
||||||
|
fav_food: Optional[str] = Field(
|
||||||
|
default=None, title="Fav Food", description="The person's favorite food"
|
||||||
|
)
|
||||||
|
|
||||||
|
chat = ChatOpenAI(
|
||||||
|
max_tokens=30,
|
||||||
|
n=1,
|
||||||
|
streaming=True,
|
||||||
|
).bind_functions(functions=[Person], function_call="Person")
|
||||||
|
|
||||||
|
prompt = ChatPromptTemplate.from_messages(
|
||||||
|
[
|
||||||
|
("system", "Use the provided Person function"),
|
||||||
|
("user", "{input}"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
chain = prompt | chat
|
||||||
|
|
||||||
|
message = HumanMessage(content="Sally is 13 years old")
|
||||||
|
response = await chain.abatch([{"input": message}])
|
||||||
|
|
||||||
|
assert isinstance(response, list)
|
||||||
|
assert len(response) == 1
|
||||||
|
for generation in response:
|
||||||
|
assert isinstance(generation, AIMessage)
|
||||||
|
|
||||||
|
|
||||||
|
def test_chat_openai_extra_kwargs() -> None:
|
||||||
|
"""Test extra kwargs to chat openai."""
|
||||||
|
# Check that foo is saved in extra_kwargs.
|
||||||
|
llm = ChatOpenAI(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 = ChatOpenAI(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):
|
||||||
|
ChatOpenAI(foo=3, model_kwargs={"foo": 2})
|
||||||
|
|
||||||
|
# Test that if explicit param is specified in kwargs it errors
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ChatOpenAI(model_kwargs={"temperature": 0.2})
|
||||||
|
|
||||||
|
# Test that "model" cannot be specified in kwargs
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ChatOpenAI(model_kwargs={"model": "gpt-3.5-turbo-instruct"})
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_openai_streaming() -> None:
|
||||||
|
"""Test streaming tokens from OpenAI."""
|
||||||
|
llm = ChatOpenAI(max_tokens=10)
|
||||||
|
|
||||||
|
for token in llm.stream("I'm Pickle Rick"):
|
||||||
|
assert isinstance(token.content, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
async def test_openai_astream() -> None:
|
||||||
|
"""Test streaming tokens from OpenAI."""
|
||||||
|
llm = ChatOpenAI(max_tokens=10)
|
||||||
|
|
||||||
|
async for token in llm.astream("I'm Pickle Rick"):
|
||||||
|
assert isinstance(token.content, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
async def test_openai_abatch() -> None:
|
||||||
|
"""Test streaming tokens from ChatOpenAI."""
|
||||||
|
llm = ChatOpenAI(max_tokens=10)
|
||||||
|
|
||||||
|
result = await llm.abatch(["I'm Pickle Rick", "I'm not Pickle Rick"])
|
||||||
|
for token in result:
|
||||||
|
assert isinstance(token.content, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
async def test_openai_abatch_tags() -> None:
|
||||||
|
"""Test batch tokens from ChatOpenAI."""
|
||||||
|
llm = ChatOpenAI(max_tokens=10)
|
||||||
|
|
||||||
|
result = await llm.abatch(
|
||||||
|
["I'm Pickle Rick", "I'm not Pickle Rick"], config={"tags": ["foo"]}
|
||||||
|
)
|
||||||
|
for token in result:
|
||||||
|
assert isinstance(token.content, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_openai_batch() -> None:
|
||||||
|
"""Test batch tokens from ChatOpenAI."""
|
||||||
|
llm = ChatOpenAI(max_tokens=10)
|
||||||
|
|
||||||
|
result = llm.batch(["I'm Pickle Rick", "I'm not Pickle Rick"])
|
||||||
|
for token in result:
|
||||||
|
assert isinstance(token.content, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
async def test_openai_ainvoke() -> None:
|
||||||
|
"""Test invoke tokens from ChatOpenAI."""
|
||||||
|
llm = ChatOpenAI(max_tokens=10)
|
||||||
|
|
||||||
|
result = await llm.ainvoke("I'm Pickle Rick", config={"tags": ["foo"]})
|
||||||
|
assert isinstance(result.content, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_openai_invoke() -> None:
|
||||||
|
"""Test invoke tokens from ChatOpenAI."""
|
||||||
|
llm = ChatOpenAI(max_tokens=10)
|
||||||
|
|
||||||
|
result = llm.invoke("I'm Pickle Rick", config=dict(tags=["foo"]))
|
||||||
|
assert isinstance(result.content, str)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stream() -> None:
|
||||||
|
"""Test streaming tokens from OpenAI."""
|
||||||
|
llm = ChatOpenAI()
|
||||||
|
|
||||||
|
for token in llm.stream("I'm Pickle Rick"):
|
||||||
|
assert isinstance(token.content, str)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_astream() -> None:
|
||||||
|
"""Test streaming tokens from OpenAI."""
|
||||||
|
llm = ChatOpenAI()
|
||||||
|
|
||||||
|
async for token in llm.astream("I'm Pickle Rick"):
|
||||||
|
assert isinstance(token.content, str)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_abatch() -> None:
|
||||||
|
"""Test streaming tokens from ChatOpenAI."""
|
||||||
|
llm = ChatOpenAI()
|
||||||
|
|
||||||
|
result = await llm.abatch(["I'm Pickle Rick", "I'm not Pickle Rick"])
|
||||||
|
for token in result:
|
||||||
|
assert isinstance(token.content, str)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_abatch_tags() -> None:
|
||||||
|
"""Test batch tokens from ChatOpenAI."""
|
||||||
|
llm = ChatOpenAI()
|
||||||
|
|
||||||
|
result = await llm.abatch(
|
||||||
|
["I'm Pickle Rick", "I'm not Pickle Rick"], config={"tags": ["foo"]}
|
||||||
|
)
|
||||||
|
for token in result:
|
||||||
|
assert isinstance(token.content, str)
|
||||||
|
|
||||||
|
|
||||||
|
def test_batch() -> None:
|
||||||
|
"""Test batch tokens from ChatOpenAI."""
|
||||||
|
llm = ChatOpenAI()
|
||||||
|
|
||||||
|
result = llm.batch(["I'm Pickle Rick", "I'm not Pickle Rick"])
|
||||||
|
for token in result:
|
||||||
|
assert isinstance(token.content, str)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_ainvoke() -> None:
|
||||||
|
"""Test invoke tokens from ChatOpenAI."""
|
||||||
|
llm = ChatOpenAI()
|
||||||
|
|
||||||
|
result = await llm.ainvoke("I'm Pickle Rick", config={"tags": ["foo"]})
|
||||||
|
assert isinstance(result.content, str)
|
||||||
|
|
||||||
|
|
||||||
|
def test_invoke() -> None:
|
||||||
|
"""Test invoke tokens from ChatOpenAI."""
|
||||||
|
llm = ChatOpenAI()
|
||||||
|
|
||||||
|
result = llm.invoke("I'm Pickle Rick", config=dict(tags=["foo"]))
|
||||||
|
assert isinstance(result.content, str)
|
@ -0,0 +1,132 @@
|
|||||||
|
"""Test azure openai embeddings."""
|
||||||
|
import os
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import openai
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from langchain_openai import AzureOpenAIEmbeddings
|
||||||
|
|
||||||
|
OPENAI_API_VERSION = os.environ.get("AZURE_OPENAI_API_VERSION", "")
|
||||||
|
OPENAI_API_BASE = os.environ.get("AZURE_OPENAI_API_BASE", "")
|
||||||
|
OPENAI_API_KEY = os.environ.get("AZURE_OPENAI_API_KEY", "")
|
||||||
|
DEPLOYMENT_NAME = os.environ.get(
|
||||||
|
"AZURE_OPENAI_DEPLOYMENT_NAME",
|
||||||
|
os.environ.get("AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT_NAME", ""),
|
||||||
|
)
|
||||||
|
print
|
||||||
|
|
||||||
|
|
||||||
|
def _get_embeddings(**kwargs: Any) -> AzureOpenAIEmbeddings:
|
||||||
|
return AzureOpenAIEmbeddings(
|
||||||
|
azure_deployment=DEPLOYMENT_NAME,
|
||||||
|
api_version=OPENAI_API_VERSION,
|
||||||
|
openai_api_base=OPENAI_API_BASE,
|
||||||
|
openai_api_key=OPENAI_API_KEY,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_azure_openai_embedding_documents() -> None:
|
||||||
|
"""Test openai embeddings."""
|
||||||
|
documents = ["foo bar"]
|
||||||
|
embedding = _get_embeddings()
|
||||||
|
output = embedding.embed_documents(documents)
|
||||||
|
assert len(output) == 1
|
||||||
|
assert len(output[0]) == 1536
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_azure_openai_embedding_documents_multiple() -> None:
|
||||||
|
"""Test openai embeddings."""
|
||||||
|
documents = ["foo bar", "bar foo", "foo"]
|
||||||
|
embedding = _get_embeddings(chunk_size=2)
|
||||||
|
embedding.embedding_ctx_length = 8191
|
||||||
|
output = embedding.embed_documents(documents)
|
||||||
|
assert embedding.chunk_size == 2
|
||||||
|
assert len(output) == 3
|
||||||
|
assert len(output[0]) == 1536
|
||||||
|
assert len(output[1]) == 1536
|
||||||
|
assert len(output[2]) == 1536
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_azure_openai_embedding_documents_chunk_size() -> None:
|
||||||
|
"""Test openai embeddings."""
|
||||||
|
documents = ["foo bar"] * 20
|
||||||
|
embedding = _get_embeddings()
|
||||||
|
embedding.embedding_ctx_length = 8191
|
||||||
|
output = embedding.embed_documents(documents)
|
||||||
|
# Max 16 chunks per batch on Azure OpenAI embeddings
|
||||||
|
assert embedding.chunk_size == 16
|
||||||
|
assert len(output) == 20
|
||||||
|
assert all([len(out) == 1536 for out in output])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
async def test_azure_openai_embedding_documents_async_multiple() -> None:
|
||||||
|
"""Test openai embeddings."""
|
||||||
|
documents = ["foo bar", "bar foo", "foo"]
|
||||||
|
embedding = _get_embeddings(chunk_size=2)
|
||||||
|
embedding.embedding_ctx_length = 8191
|
||||||
|
output = await embedding.aembed_documents(documents)
|
||||||
|
assert len(output) == 3
|
||||||
|
assert len(output[0]) == 1536
|
||||||
|
assert len(output[1]) == 1536
|
||||||
|
assert len(output[2]) == 1536
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_azure_openai_embedding_query() -> None:
|
||||||
|
"""Test openai embeddings."""
|
||||||
|
document = "foo bar"
|
||||||
|
embedding = _get_embeddings()
|
||||||
|
output = embedding.embed_query(document)
|
||||||
|
assert len(output) == 1536
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
async def test_azure_openai_embedding_async_query() -> None:
|
||||||
|
"""Test openai embeddings."""
|
||||||
|
document = "foo bar"
|
||||||
|
embedding = _get_embeddings()
|
||||||
|
output = await embedding.aembed_query(document)
|
||||||
|
assert len(output) == 1536
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_azure_openai_embedding_with_empty_string() -> None:
|
||||||
|
"""Test openai embeddings with empty string."""
|
||||||
|
|
||||||
|
document = ["", "abc"]
|
||||||
|
embedding = _get_embeddings()
|
||||||
|
output = embedding.embed_documents(document)
|
||||||
|
assert len(output) == 2
|
||||||
|
assert len(output[0]) == 1536
|
||||||
|
expected_output = (
|
||||||
|
openai.AzureOpenAI(
|
||||||
|
api_version=OPENAI_API_VERSION,
|
||||||
|
api_key=OPENAI_API_KEY,
|
||||||
|
base_url=embedding.openai_api_base,
|
||||||
|
azure_deployment=DEPLOYMENT_NAME,
|
||||||
|
) # type: ignore
|
||||||
|
.embeddings.create(input="", model="text-embedding-ada-002")
|
||||||
|
.data[0]
|
||||||
|
.embedding
|
||||||
|
)
|
||||||
|
assert np.allclose(output[0], expected_output)
|
||||||
|
assert len(output[1]) == 1536
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_embed_documents_normalized() -> None:
|
||||||
|
output = _get_embeddings().embed_documents(["foo walked to the market"])
|
||||||
|
assert np.isclose(np.linalg.norm(output[0]), 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_embed_query_normalized() -> None:
|
||||||
|
output = _get_embeddings().embed_query("foo walked to the market")
|
||||||
|
assert np.isclose(np.linalg.norm(output), 1.0)
|
@ -0,0 +1,19 @@
|
|||||||
|
"""Test OpenAI embeddings."""
|
||||||
|
from langchain_openai.embeddings.base import OpenAIEmbeddings
|
||||||
|
|
||||||
|
|
||||||
|
def test_langchain_openai_embedding_documents() -> None:
|
||||||
|
"""Test cohere embeddings."""
|
||||||
|
documents = ["foo bar"]
|
||||||
|
embedding = OpenAIEmbeddings()
|
||||||
|
output = embedding.embed_documents(documents)
|
||||||
|
assert len(output) == 1
|
||||||
|
assert len(output[0]) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_langchain_openai_embedding_query() -> None:
|
||||||
|
"""Test cohere embeddings."""
|
||||||
|
document = "foo bar"
|
||||||
|
embedding = OpenAIEmbeddings()
|
||||||
|
output = embedding.embed_query(document)
|
||||||
|
assert len(output) > 0
|
176
libs/partners/openai/tests/integration_tests/llms/test_azure.py
Normal file
176
libs/partners/openai/tests/integration_tests/llms/test_azure.py
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
"""Test AzureOpenAI wrapper."""
|
||||||
|
import os
|
||||||
|
from typing import Any, Generator
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from langchain_core.callbacks import CallbackManager
|
||||||
|
from langchain_core.outputs import LLMResult
|
||||||
|
|
||||||
|
from langchain_openai import AzureOpenAI
|
||||||
|
from tests.unit_tests.fake.callbacks import FakeCallbackHandler
|
||||||
|
|
||||||
|
OPENAI_API_VERSION = os.environ.get("AZURE_OPENAI_API_VERSION", "")
|
||||||
|
OPENAI_API_BASE = os.environ.get("AZURE_OPENAI_API_BASE", "")
|
||||||
|
OPENAI_API_KEY = os.environ.get("AZURE_OPENAI_API_KEY", "")
|
||||||
|
DEPLOYMENT_NAME = os.environ.get(
|
||||||
|
"AZURE_OPENAI_DEPLOYMENT_NAME",
|
||||||
|
os.environ.get("AZURE_OPENAI_LLM_DEPLOYMENT_NAME", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_llm(**kwargs: Any) -> AzureOpenAI:
|
||||||
|
return AzureOpenAI(
|
||||||
|
deployment_name=DEPLOYMENT_NAME,
|
||||||
|
openai_api_version=OPENAI_API_VERSION,
|
||||||
|
openai_api_base=OPENAI_API_BASE,
|
||||||
|
openai_api_key=OPENAI_API_KEY,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def llm() -> AzureOpenAI:
|
||||||
|
return _get_llm(
|
||||||
|
max_tokens=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_openai_call(llm: AzureOpenAI) -> None:
|
||||||
|
"""Test valid call to openai."""
|
||||||
|
output = llm("Say something nice:")
|
||||||
|
assert isinstance(output, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_openai_streaming(llm: AzureOpenAI) -> None:
|
||||||
|
"""Test streaming tokens from AzureOpenAI."""
|
||||||
|
generator = llm.stream("I'm Pickle Rick")
|
||||||
|
|
||||||
|
assert isinstance(generator, Generator)
|
||||||
|
|
||||||
|
full_response = ""
|
||||||
|
for token in generator:
|
||||||
|
assert isinstance(token, str)
|
||||||
|
full_response += token
|
||||||
|
assert full_response
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
async def test_openai_astream(llm: AzureOpenAI) -> None:
|
||||||
|
"""Test streaming tokens from AzureOpenAI."""
|
||||||
|
async for token in llm.astream("I'm Pickle Rick"):
|
||||||
|
assert isinstance(token, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
async def test_openai_abatch(llm: AzureOpenAI) -> None:
|
||||||
|
"""Test streaming tokens from AzureOpenAI."""
|
||||||
|
result = await llm.abatch(["I'm Pickle Rick", "I'm not Pickle Rick"])
|
||||||
|
for token in result:
|
||||||
|
assert isinstance(token, str)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_openai_abatch_tags(llm: AzureOpenAI) -> None:
|
||||||
|
"""Test streaming tokens from AzureOpenAI."""
|
||||||
|
result = await llm.abatch(
|
||||||
|
["I'm Pickle Rick", "I'm not Pickle Rick"], config={"tags": ["foo"]}
|
||||||
|
)
|
||||||
|
for token in result:
|
||||||
|
assert isinstance(token, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_openai_batch(llm: AzureOpenAI) -> None:
|
||||||
|
"""Test streaming tokens from AzureOpenAI."""
|
||||||
|
result = llm.batch(["I'm Pickle Rick", "I'm not Pickle Rick"])
|
||||||
|
for token in result:
|
||||||
|
assert isinstance(token, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
async def test_openai_ainvoke(llm: AzureOpenAI) -> None:
|
||||||
|
"""Test streaming tokens from AzureOpenAI."""
|
||||||
|
result = await llm.ainvoke("I'm Pickle Rick", config={"tags": ["foo"]})
|
||||||
|
assert isinstance(result, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_openai_invoke(llm: AzureOpenAI) -> None:
|
||||||
|
"""Test streaming tokens from AzureOpenAI."""
|
||||||
|
result = llm.invoke("I'm Pickle Rick", config=dict(tags=["foo"]))
|
||||||
|
assert isinstance(result, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_openai_multiple_prompts(llm: AzureOpenAI) -> None:
|
||||||
|
"""Test completion with multiple prompts."""
|
||||||
|
output = llm.generate(["I'm Pickle Rick", "I'm Pickle Rick"])
|
||||||
|
assert isinstance(output, LLMResult)
|
||||||
|
assert isinstance(output.generations, list)
|
||||||
|
assert len(output.generations) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_openai_streaming_best_of_error() -> None:
|
||||||
|
"""Test validation for streaming fails if best_of is not 1."""
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
_get_llm(best_of=2, streaming=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_openai_streaming_n_error() -> None:
|
||||||
|
"""Test validation for streaming fails if n is not 1."""
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
_get_llm(n=2, streaming=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_openai_streaming_multiple_prompts_error() -> None:
|
||||||
|
"""Test validation for streaming fails if multiple prompts are given."""
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
_get_llm(streaming=True).generate(["I'm Pickle Rick", "I'm Pickle Rick"])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_openai_streaming_call() -> None:
|
||||||
|
"""Test valid call to openai."""
|
||||||
|
llm = _get_llm(max_tokens=10, streaming=True)
|
||||||
|
output = llm("Say foo:")
|
||||||
|
assert isinstance(output, str)
|
||||||
|
|
||||||
|
|
||||||
|
def test_openai_streaming_callback() -> None:
|
||||||
|
"""Test that streaming correctly invokes on_llm_new_token callback."""
|
||||||
|
callback_handler = FakeCallbackHandler()
|
||||||
|
callback_manager = CallbackManager([callback_handler])
|
||||||
|
llm = _get_llm(
|
||||||
|
max_tokens=10,
|
||||||
|
streaming=True,
|
||||||
|
temperature=0,
|
||||||
|
callback_manager=callback_manager,
|
||||||
|
verbose=True,
|
||||||
|
)
|
||||||
|
llm("Write me a sentence with 100 words.")
|
||||||
|
assert callback_handler.llm_streams == 11
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
async def test_openai_async_generate() -> None:
|
||||||
|
"""Test async generation."""
|
||||||
|
llm = _get_llm(max_tokens=10)
|
||||||
|
output = await llm.agenerate(["Hello, how are you?"])
|
||||||
|
assert isinstance(output, LLMResult)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_openai_async_streaming_callback() -> None:
|
||||||
|
"""Test that streaming correctly invokes on_llm_new_token callback."""
|
||||||
|
callback_handler = FakeCallbackHandler()
|
||||||
|
callback_manager = CallbackManager([callback_handler])
|
||||||
|
llm = _get_llm(
|
||||||
|
max_tokens=10,
|
||||||
|
streaming=True,
|
||||||
|
temperature=0,
|
||||||
|
callback_manager=callback_manager,
|
||||||
|
verbose=True,
|
||||||
|
)
|
||||||
|
result = await llm.agenerate(["Write me a sentence with 100 words."])
|
||||||
|
assert callback_handler.llm_streams == 11
|
||||||
|
assert isinstance(result, LLMResult)
|
280
libs/partners/openai/tests/integration_tests/llms/test_base.py
Normal file
280
libs/partners/openai/tests/integration_tests/llms/test_base.py
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
"""Test OpenAI llm."""
|
||||||
|
from typing import Generator
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from langchain_core.callbacks import CallbackManager
|
||||||
|
from langchain_core.outputs import LLMResult
|
||||||
|
|
||||||
|
from langchain_openai import OpenAI
|
||||||
|
from tests.unit_tests.fake.callbacks import (
|
||||||
|
FakeCallbackHandler,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stream() -> None:
|
||||||
|
"""Test streaming tokens from OpenAI."""
|
||||||
|
llm = OpenAI()
|
||||||
|
|
||||||
|
for token in llm.stream("I'm Pickle Rick"):
|
||||||
|
assert isinstance(token, str)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_astream() -> None:
|
||||||
|
"""Test streaming tokens from OpenAI."""
|
||||||
|
llm = OpenAI()
|
||||||
|
|
||||||
|
async for token in llm.astream("I'm Pickle Rick"):
|
||||||
|
assert isinstance(token, str)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_abatch() -> None:
|
||||||
|
"""Test streaming tokens from OpenAI."""
|
||||||
|
llm = OpenAI()
|
||||||
|
|
||||||
|
result = await llm.abatch(["I'm Pickle Rick", "I'm not Pickle Rick"])
|
||||||
|
for token in result:
|
||||||
|
assert isinstance(token, str)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_abatch_tags() -> None:
|
||||||
|
"""Test batch tokens from OpenAI."""
|
||||||
|
llm = OpenAI()
|
||||||
|
|
||||||
|
result = await llm.abatch(
|
||||||
|
["I'm Pickle Rick", "I'm not Pickle Rick"], config={"tags": ["foo"]}
|
||||||
|
)
|
||||||
|
for token in result:
|
||||||
|
assert isinstance(token, str)
|
||||||
|
|
||||||
|
|
||||||
|
def test_batch() -> None:
|
||||||
|
"""Test batch tokens from OpenAI."""
|
||||||
|
llm = OpenAI()
|
||||||
|
|
||||||
|
result = llm.batch(["I'm Pickle Rick", "I'm not Pickle Rick"])
|
||||||
|
for token in result:
|
||||||
|
assert isinstance(token, str)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_ainvoke() -> None:
|
||||||
|
"""Test invoke tokens from OpenAI."""
|
||||||
|
llm = OpenAI()
|
||||||
|
|
||||||
|
result = await llm.ainvoke("I'm Pickle Rick", config={"tags": ["foo"]})
|
||||||
|
assert isinstance(result, str)
|
||||||
|
|
||||||
|
|
||||||
|
def test_invoke() -> None:
|
||||||
|
"""Test invoke tokens from OpenAI."""
|
||||||
|
llm = OpenAI()
|
||||||
|
|
||||||
|
result = llm.invoke("I'm Pickle Rick", config=dict(tags=["foo"]))
|
||||||
|
assert isinstance(result, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_openai_call() -> None:
|
||||||
|
"""Test valid call to openai."""
|
||||||
|
llm = OpenAI()
|
||||||
|
output = llm("Say something nice:")
|
||||||
|
assert isinstance(output, str)
|
||||||
|
|
||||||
|
|
||||||
|
def test_openai_llm_output_contains_model_name() -> None:
|
||||||
|
"""Test llm_output contains model_name."""
|
||||||
|
llm = OpenAI(max_tokens=10)
|
||||||
|
llm_result = llm.generate(["Hello, how are you?"])
|
||||||
|
assert llm_result.llm_output is not None
|
||||||
|
assert llm_result.llm_output["model_name"] == llm.model_name
|
||||||
|
|
||||||
|
|
||||||
|
def test_openai_stop_valid() -> None:
|
||||||
|
"""Test openai stop logic on valid configuration."""
|
||||||
|
query = "write an ordered list of five items"
|
||||||
|
first_llm = OpenAI(stop="3", temperature=0)
|
||||||
|
first_output = first_llm(query)
|
||||||
|
second_llm = OpenAI(temperature=0)
|
||||||
|
second_output = second_llm(query, stop=["3"])
|
||||||
|
# Because it stops on new lines, shouldn't return anything
|
||||||
|
assert first_output == second_output
|
||||||
|
|
||||||
|
|
||||||
|
def test_openai_stop_error() -> None:
|
||||||
|
"""Test openai stop logic on bad configuration."""
|
||||||
|
llm = OpenAI(stop="3", temperature=0)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
llm("write an ordered list of five items", stop=["\n"])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_openai_streaming() -> None:
|
||||||
|
"""Test streaming tokens from OpenAI."""
|
||||||
|
llm = OpenAI(max_tokens=10)
|
||||||
|
generator = llm.stream("I'm Pickle Rick")
|
||||||
|
|
||||||
|
assert isinstance(generator, Generator)
|
||||||
|
|
||||||
|
for token in generator:
|
||||||
|
assert isinstance(token, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
async def test_openai_astream() -> None:
|
||||||
|
"""Test streaming tokens from OpenAI."""
|
||||||
|
llm = OpenAI(max_tokens=10)
|
||||||
|
|
||||||
|
async for token in llm.astream("I'm Pickle Rick"):
|
||||||
|
assert isinstance(token, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
async def test_openai_abatch() -> None:
|
||||||
|
"""Test streaming tokens from OpenAI."""
|
||||||
|
llm = OpenAI(max_tokens=10)
|
||||||
|
|
||||||
|
result = await llm.abatch(["I'm Pickle Rick", "I'm not Pickle Rick"])
|
||||||
|
for token in result:
|
||||||
|
assert isinstance(token, str)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_openai_abatch_tags() -> None:
|
||||||
|
"""Test streaming tokens from OpenAI."""
|
||||||
|
llm = OpenAI(max_tokens=10)
|
||||||
|
|
||||||
|
result = await llm.abatch(
|
||||||
|
["I'm Pickle Rick", "I'm not Pickle Rick"], config={"tags": ["foo"]}
|
||||||
|
)
|
||||||
|
for token in result:
|
||||||
|
assert isinstance(token, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_openai_batch() -> None:
|
||||||
|
"""Test streaming tokens from OpenAI."""
|
||||||
|
llm = OpenAI(max_tokens=10)
|
||||||
|
|
||||||
|
result = llm.batch(["I'm Pickle Rick", "I'm not Pickle Rick"])
|
||||||
|
for token in result:
|
||||||
|
assert isinstance(token, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
async def test_openai_ainvoke() -> None:
|
||||||
|
"""Test streaming tokens from OpenAI."""
|
||||||
|
llm = OpenAI(max_tokens=10)
|
||||||
|
|
||||||
|
result = await llm.ainvoke("I'm Pickle Rick", config={"tags": ["foo"]})
|
||||||
|
assert isinstance(result, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_openai_invoke() -> None:
|
||||||
|
"""Test streaming tokens from OpenAI."""
|
||||||
|
llm = OpenAI(max_tokens=10)
|
||||||
|
|
||||||
|
result = llm.invoke("I'm Pickle Rick", config=dict(tags=["foo"]))
|
||||||
|
assert isinstance(result, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_openai_multiple_prompts() -> None:
|
||||||
|
"""Test completion with multiple prompts."""
|
||||||
|
llm = OpenAI(max_tokens=10)
|
||||||
|
output = llm.generate(["I'm Pickle Rick", "I'm Pickle Rick"])
|
||||||
|
assert isinstance(output, LLMResult)
|
||||||
|
assert isinstance(output.generations, list)
|
||||||
|
assert len(output.generations) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_openai_streaming_best_of_error() -> None:
|
||||||
|
"""Test validation for streaming fails if best_of is not 1."""
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
OpenAI(best_of=2, streaming=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_openai_streaming_n_error() -> None:
|
||||||
|
"""Test validation for streaming fails if n is not 1."""
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
OpenAI(n=2, streaming=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_openai_streaming_multiple_prompts_error() -> None:
|
||||||
|
"""Test validation for streaming fails if multiple prompts are given."""
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
OpenAI(streaming=True).generate(["I'm Pickle Rick", "I'm Pickle Rick"])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
def test_openai_streaming_call() -> None:
|
||||||
|
"""Test valid call to openai."""
|
||||||
|
llm = OpenAI(max_tokens=10, streaming=True)
|
||||||
|
output = llm("Say foo:")
|
||||||
|
assert isinstance(output, str)
|
||||||
|
|
||||||
|
|
||||||
|
def test_openai_streaming_callback() -> None:
|
||||||
|
"""Test that streaming correctly invokes on_llm_new_token callback."""
|
||||||
|
callback_handler = FakeCallbackHandler()
|
||||||
|
callback_manager = CallbackManager([callback_handler])
|
||||||
|
llm = OpenAI(
|
||||||
|
max_tokens=10,
|
||||||
|
streaming=True,
|
||||||
|
temperature=0,
|
||||||
|
callback_manager=callback_manager,
|
||||||
|
verbose=True,
|
||||||
|
)
|
||||||
|
llm("Write me a sentence with 100 words.")
|
||||||
|
|
||||||
|
# new client sometimes passes 2 tokens at once
|
||||||
|
assert callback_handler.llm_streams >= 5
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.scheduled
|
||||||
|
async def test_openai_async_generate() -> None:
|
||||||
|
"""Test async generation."""
|
||||||
|
llm = OpenAI(max_tokens=10)
|
||||||
|
output = await llm.agenerate(["Hello, how are you?"])
|
||||||
|
assert isinstance(output, LLMResult)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_openai_async_streaming_callback() -> None:
|
||||||
|
"""Test that streaming correctly invokes on_llm_new_token callback."""
|
||||||
|
callback_handler = FakeCallbackHandler()
|
||||||
|
callback_manager = CallbackManager([callback_handler])
|
||||||
|
llm = OpenAI(
|
||||||
|
max_tokens=10,
|
||||||
|
streaming=True,
|
||||||
|
temperature=0,
|
||||||
|
callback_manager=callback_manager,
|
||||||
|
verbose=True,
|
||||||
|
)
|
||||||
|
result = await llm.agenerate(["Write me a sentence with 100 words."])
|
||||||
|
|
||||||
|
# new client sometimes passes 2 tokens at once
|
||||||
|
assert callback_handler.llm_streams >= 5
|
||||||
|
assert isinstance(result, LLMResult)
|
||||||
|
|
||||||
|
|
||||||
|
def test_openai_modelname_to_contextsize_valid() -> None:
|
||||||
|
"""Test model name to context size on a valid model."""
|
||||||
|
assert OpenAI().modelname_to_contextsize("davinci") == 2049
|
||||||
|
|
||||||
|
|
||||||
|
def test_openai_modelname_to_contextsize_invalid() -> None:
|
||||||
|
"""Test model name to context size on an invalid model."""
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
OpenAI().modelname_to_contextsize("foobar")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_completion() -> dict:
|
||||||
|
return {
|
||||||
|
"id": "cmpl-3evkmQda5Hu7fcZavknQda3SQ",
|
||||||
|
"object": "text_completion",
|
||||||
|
"created": 1689989000,
|
||||||
|
"model": "gpt-3.5-turbo-instruct",
|
||||||
|
"choices": [
|
||||||
|
{"text": "Bar Baz", "index": 0, "logprobs": None, "finish_reason": "length"}
|
||||||
|
],
|
||||||
|
"usage": {"prompt_tokens": 1, "completion_tokens": 2, "total_tokens": 3},
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.compile
|
||||||
|
def test_placeholder() -> None:
|
||||||
|
"""Used for compiling integration tests without running any real tests."""
|
||||||
|
pass
|
0
libs/partners/openai/tests/unit_tests/__init__.py
Normal file
0
libs/partners/openai/tests/unit_tests/__init__.py
Normal file
120
libs/partners/openai/tests/unit_tests/chat_models/test_base.py
Normal file
120
libs/partners/openai/tests/unit_tests/chat_models/test_base.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
"""Test OpenAI Chat API wrapper."""
|
||||||
|
import json
|
||||||
|
from typing import Any
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from langchain_core.messages import (
|
||||||
|
AIMessage,
|
||||||
|
FunctionMessage,
|
||||||
|
HumanMessage,
|
||||||
|
SystemMessage,
|
||||||
|
)
|
||||||
|
|
||||||
|
from langchain_openai import ChatOpenAI
|
||||||
|
from langchain_openai.chat_models.base import _convert_dict_to_message
|
||||||
|
|
||||||
|
|
||||||
|
def test_openai_model_param() -> None:
|
||||||
|
llm = ChatOpenAI(model="foo")
|
||||||
|
assert llm.model_name == "foo"
|
||||||
|
llm = ChatOpenAI(model_name="foo")
|
||||||
|
assert llm.model_name == "foo"
|
||||||
|
|
||||||
|
|
||||||
|
def test_function_message_dict_to_function_message() -> None:
|
||||||
|
content = json.dumps({"result": "Example #1"})
|
||||||
|
name = "test_function"
|
||||||
|
result = _convert_dict_to_message(
|
||||||
|
{
|
||||||
|
"role": "function",
|
||||||
|
"name": name,
|
||||||
|
"content": content,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert isinstance(result, FunctionMessage)
|
||||||
|
assert result.name == name
|
||||||
|
assert result.content == content
|
||||||
|
|
||||||
|
|
||||||
|
def test__convert_dict_to_message_human() -> None:
|
||||||
|
message = {"role": "user", "content": "foo"}
|
||||||
|
result = _convert_dict_to_message(message)
|
||||||
|
expected_output = HumanMessage(content="foo")
|
||||||
|
assert result == expected_output
|
||||||
|
|
||||||
|
|
||||||
|
def test__convert_dict_to_message_ai() -> None:
|
||||||
|
message = {"role": "assistant", "content": "foo"}
|
||||||
|
result = _convert_dict_to_message(message)
|
||||||
|
expected_output = AIMessage(content="foo")
|
||||||
|
assert result == expected_output
|
||||||
|
|
||||||
|
|
||||||
|
def test__convert_dict_to_message_system() -> None:
|
||||||
|
message = {"role": "system", "content": "foo"}
|
||||||
|
result = _convert_dict_to_message(message)
|
||||||
|
expected_output = SystemMessage(content="foo")
|
||||||
|
assert result == expected_output
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_completion() -> dict:
|
||||||
|
return {
|
||||||
|
"id": "chatcmpl-7fcZavknQda3SQ",
|
||||||
|
"object": "chat.completion",
|
||||||
|
"created": 1689989000,
|
||||||
|
"model": "gpt-3.5-turbo-0613",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"message": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "Bar Baz",
|
||||||
|
},
|
||||||
|
"finish_reason": "stop",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_openai_predict(mock_completion: dict) -> None:
|
||||||
|
llm = ChatOpenAI()
|
||||||
|
mock_client = MagicMock()
|
||||||
|
completed = False
|
||||||
|
|
||||||
|
def mock_create(*args: Any, **kwargs: Any) -> Any:
|
||||||
|
nonlocal completed
|
||||||
|
completed = True
|
||||||
|
return mock_completion
|
||||||
|
|
||||||
|
mock_client.create = mock_create
|
||||||
|
with patch.object(
|
||||||
|
llm,
|
||||||
|
"client",
|
||||||
|
mock_client,
|
||||||
|
):
|
||||||
|
res = llm.predict("bar")
|
||||||
|
assert res == "Bar Baz"
|
||||||
|
assert completed
|
||||||
|
|
||||||
|
|
||||||
|
async def test_openai_apredict(mock_completion: dict) -> None:
|
||||||
|
llm = ChatOpenAI()
|
||||||
|
mock_client = MagicMock()
|
||||||
|
completed = False
|
||||||
|
|
||||||
|
def mock_create(*args: Any, **kwargs: Any) -> Any:
|
||||||
|
nonlocal completed
|
||||||
|
completed = True
|
||||||
|
return mock_completion
|
||||||
|
|
||||||
|
mock_client.create = mock_create
|
||||||
|
with patch.object(
|
||||||
|
llm,
|
||||||
|
"client",
|
||||||
|
mock_client,
|
||||||
|
):
|
||||||
|
res = llm.predict("bar")
|
||||||
|
assert res == "Bar Baz"
|
||||||
|
assert completed
|
@ -0,0 +1,7 @@
|
|||||||
|
from langchain_openai.chat_models import __all__
|
||||||
|
|
||||||
|
EXPECTED_ALL = ["ChatOpenAI", "AzureChatOpenAI"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_imports() -> None:
|
||||||
|
assert sorted(EXPECTED_ALL) == sorted(__all__)
|
@ -0,0 +1,18 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from langchain_openai import OpenAIEmbeddings
|
||||||
|
|
||||||
|
os.environ["OPENAI_API_KEY"] = "foo"
|
||||||
|
|
||||||
|
|
||||||
|
def test_openai_invalid_model_kwargs() -> None:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
OpenAIEmbeddings(model_kwargs={"model": "foo"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_openai_incorrect_field() -> None:
|
||||||
|
with pytest.warns(match="not default parameter"):
|
||||||
|
llm = OpenAIEmbeddings(foo="bar")
|
||||||
|
assert llm.model_kwargs == {"foo": "bar"}
|
@ -0,0 +1,7 @@
|
|||||||
|
from langchain_openai.embeddings import __all__
|
||||||
|
|
||||||
|
EXPECTED_ALL = ["OpenAIEmbeddings", "AzureOpenAIEmbeddings"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_imports() -> None:
|
||||||
|
assert sorted(EXPECTED_ALL) == sorted(__all__)
|
393
libs/partners/openai/tests/unit_tests/fake/callbacks.py
Normal file
393
libs/partners/openai/tests/unit_tests/fake/callbacks.py
Normal file
@ -0,0 +1,393 @@
|
|||||||
|
"""A fake callback handler for testing purposes."""
|
||||||
|
from itertools import chain
|
||||||
|
from typing import Any, Dict, List, Optional, Union
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from langchain_core.callbacks.base import AsyncCallbackHandler, BaseCallbackHandler
|
||||||
|
from langchain_core.messages import BaseMessage
|
||||||
|
from langchain_core.pydantic_v1 import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class BaseFakeCallbackHandler(BaseModel):
|
||||||
|
"""Base fake callback handler for testing."""
|
||||||
|
|
||||||
|
starts: int = 0
|
||||||
|
ends: int = 0
|
||||||
|
errors: int = 0
|
||||||
|
errors_args: List[Any] = []
|
||||||
|
text: int = 0
|
||||||
|
ignore_llm_: bool = False
|
||||||
|
ignore_chain_: bool = False
|
||||||
|
ignore_agent_: bool = False
|
||||||
|
ignore_retriever_: bool = False
|
||||||
|
ignore_chat_model_: bool = False
|
||||||
|
|
||||||
|
# to allow for similar callback handlers that are not technicall equal
|
||||||
|
fake_id: Union[str, None] = None
|
||||||
|
|
||||||
|
# add finer-grained counters for easier debugging of failing tests
|
||||||
|
chain_starts: int = 0
|
||||||
|
chain_ends: int = 0
|
||||||
|
llm_starts: int = 0
|
||||||
|
llm_ends: int = 0
|
||||||
|
llm_streams: int = 0
|
||||||
|
tool_starts: int = 0
|
||||||
|
tool_ends: int = 0
|
||||||
|
agent_actions: int = 0
|
||||||
|
agent_ends: int = 0
|
||||||
|
chat_model_starts: int = 0
|
||||||
|
retriever_starts: int = 0
|
||||||
|
retriever_ends: int = 0
|
||||||
|
retriever_errors: int = 0
|
||||||
|
retries: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
class BaseFakeCallbackHandlerMixin(BaseFakeCallbackHandler):
|
||||||
|
"""Base fake callback handler mixin for testing."""
|
||||||
|
|
||||||
|
def on_llm_start_common(self) -> None:
|
||||||
|
self.llm_starts += 1
|
||||||
|
self.starts += 1
|
||||||
|
|
||||||
|
def on_llm_end_common(self) -> None:
|
||||||
|
self.llm_ends += 1
|
||||||
|
self.ends += 1
|
||||||
|
|
||||||
|
def on_llm_error_common(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
self.errors += 1
|
||||||
|
self.errors_args.append({"args": args, "kwargs": kwargs})
|
||||||
|
|
||||||
|
def on_llm_new_token_common(self) -> None:
|
||||||
|
self.llm_streams += 1
|
||||||
|
|
||||||
|
def on_retry_common(self) -> None:
|
||||||
|
self.retries += 1
|
||||||
|
|
||||||
|
def on_chain_start_common(self) -> None:
|
||||||
|
self.chain_starts += 1
|
||||||
|
self.starts += 1
|
||||||
|
|
||||||
|
def on_chain_end_common(self) -> None:
|
||||||
|
self.chain_ends += 1
|
||||||
|
self.ends += 1
|
||||||
|
|
||||||
|
def on_chain_error_common(self) -> None:
|
||||||
|
self.errors += 1
|
||||||
|
|
||||||
|
def on_tool_start_common(self) -> None:
|
||||||
|
self.tool_starts += 1
|
||||||
|
self.starts += 1
|
||||||
|
|
||||||
|
def on_tool_end_common(self) -> None:
|
||||||
|
self.tool_ends += 1
|
||||||
|
self.ends += 1
|
||||||
|
|
||||||
|
def on_tool_error_common(self) -> None:
|
||||||
|
self.errors += 1
|
||||||
|
|
||||||
|
def on_agent_action_common(self) -> None:
|
||||||
|
self.agent_actions += 1
|
||||||
|
self.starts += 1
|
||||||
|
|
||||||
|
def on_agent_finish_common(self) -> None:
|
||||||
|
self.agent_ends += 1
|
||||||
|
self.ends += 1
|
||||||
|
|
||||||
|
def on_chat_model_start_common(self) -> None:
|
||||||
|
self.chat_model_starts += 1
|
||||||
|
self.starts += 1
|
||||||
|
|
||||||
|
def on_text_common(self) -> None:
|
||||||
|
self.text += 1
|
||||||
|
|
||||||
|
def on_retriever_start_common(self) -> None:
|
||||||
|
self.starts += 1
|
||||||
|
self.retriever_starts += 1
|
||||||
|
|
||||||
|
def on_retriever_end_common(self) -> None:
|
||||||
|
self.ends += 1
|
||||||
|
self.retriever_ends += 1
|
||||||
|
|
||||||
|
def on_retriever_error_common(self) -> None:
|
||||||
|
self.errors += 1
|
||||||
|
self.retriever_errors += 1
|
||||||
|
|
||||||
|
|
||||||
|
class FakeCallbackHandler(BaseCallbackHandler, BaseFakeCallbackHandlerMixin):
|
||||||
|
"""Fake callback handler for testing."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ignore_llm(self) -> bool:
|
||||||
|
"""Whether to ignore LLM callbacks."""
|
||||||
|
return self.ignore_llm_
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ignore_chain(self) -> bool:
|
||||||
|
"""Whether to ignore chain callbacks."""
|
||||||
|
return self.ignore_chain_
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ignore_agent(self) -> bool:
|
||||||
|
"""Whether to ignore agent callbacks."""
|
||||||
|
return self.ignore_agent_
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ignore_retriever(self) -> bool:
|
||||||
|
"""Whether to ignore retriever callbacks."""
|
||||||
|
return self.ignore_retriever_
|
||||||
|
|
||||||
|
def on_llm_start(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Any:
|
||||||
|
self.on_llm_start_common()
|
||||||
|
|
||||||
|
def on_llm_new_token(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Any:
|
||||||
|
self.on_llm_new_token_common()
|
||||||
|
|
||||||
|
def on_llm_end(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Any:
|
||||||
|
self.on_llm_end_common()
|
||||||
|
|
||||||
|
def on_llm_error(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Any:
|
||||||
|
self.on_llm_error_common(*args, **kwargs)
|
||||||
|
|
||||||
|
def on_retry(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Any:
|
||||||
|
self.on_retry_common()
|
||||||
|
|
||||||
|
def on_chain_start(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Any:
|
||||||
|
self.on_chain_start_common()
|
||||||
|
|
||||||
|
def on_chain_end(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Any:
|
||||||
|
self.on_chain_end_common()
|
||||||
|
|
||||||
|
def on_chain_error(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Any:
|
||||||
|
self.on_chain_error_common()
|
||||||
|
|
||||||
|
def on_tool_start(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Any:
|
||||||
|
self.on_tool_start_common()
|
||||||
|
|
||||||
|
def on_tool_end(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Any:
|
||||||
|
self.on_tool_end_common()
|
||||||
|
|
||||||
|
def on_tool_error(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Any:
|
||||||
|
self.on_tool_error_common()
|
||||||
|
|
||||||
|
def on_agent_action(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Any:
|
||||||
|
self.on_agent_action_common()
|
||||||
|
|
||||||
|
def on_agent_finish(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Any:
|
||||||
|
self.on_agent_finish_common()
|
||||||
|
|
||||||
|
def on_text(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Any:
|
||||||
|
self.on_text_common()
|
||||||
|
|
||||||
|
def on_retriever_start(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Any:
|
||||||
|
self.on_retriever_start_common()
|
||||||
|
|
||||||
|
def on_retriever_end(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Any:
|
||||||
|
self.on_retriever_end_common()
|
||||||
|
|
||||||
|
def on_retriever_error(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Any:
|
||||||
|
self.on_retriever_error_common()
|
||||||
|
|
||||||
|
def __deepcopy__(self, memo: dict) -> "FakeCallbackHandler":
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class FakeCallbackHandlerWithChatStart(FakeCallbackHandler):
|
||||||
|
def on_chat_model_start(
|
||||||
|
self,
|
||||||
|
serialized: Dict[str, Any],
|
||||||
|
messages: List[List[BaseMessage]],
|
||||||
|
*,
|
||||||
|
run_id: UUID,
|
||||||
|
parent_run_id: Optional[UUID] = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Any:
|
||||||
|
assert all(isinstance(m, BaseMessage) for m in chain(*messages))
|
||||||
|
self.on_chat_model_start_common()
|
||||||
|
|
||||||
|
|
||||||
|
class FakeAsyncCallbackHandler(AsyncCallbackHandler, BaseFakeCallbackHandlerMixin):
|
||||||
|
"""Fake async callback handler for testing."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ignore_llm(self) -> bool:
|
||||||
|
"""Whether to ignore LLM callbacks."""
|
||||||
|
return self.ignore_llm_
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ignore_chain(self) -> bool:
|
||||||
|
"""Whether to ignore chain callbacks."""
|
||||||
|
return self.ignore_chain_
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ignore_agent(self) -> bool:
|
||||||
|
"""Whether to ignore agent callbacks."""
|
||||||
|
return self.ignore_agent_
|
||||||
|
|
||||||
|
async def on_retry(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Any:
|
||||||
|
self.on_retry_common()
|
||||||
|
|
||||||
|
async def on_llm_start(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
self.on_llm_start_common()
|
||||||
|
|
||||||
|
async def on_llm_new_token(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
self.on_llm_new_token_common()
|
||||||
|
|
||||||
|
async def on_llm_end(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
self.on_llm_end_common()
|
||||||
|
|
||||||
|
async def on_llm_error(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
self.on_llm_error_common(*args, **kwargs)
|
||||||
|
|
||||||
|
async def on_chain_start(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
self.on_chain_start_common()
|
||||||
|
|
||||||
|
async def on_chain_end(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
self.on_chain_end_common()
|
||||||
|
|
||||||
|
async def on_chain_error(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
self.on_chain_error_common()
|
||||||
|
|
||||||
|
async def on_tool_start(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
self.on_tool_start_common()
|
||||||
|
|
||||||
|
async def on_tool_end(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
self.on_tool_end_common()
|
||||||
|
|
||||||
|
async def on_tool_error(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
self.on_tool_error_common()
|
||||||
|
|
||||||
|
async def on_agent_action(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
self.on_agent_action_common()
|
||||||
|
|
||||||
|
async def on_agent_finish(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
self.on_agent_finish_common()
|
||||||
|
|
||||||
|
async def on_text(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
self.on_text_common()
|
||||||
|
|
||||||
|
def __deepcopy__(self, memo: dict) -> "FakeAsyncCallbackHandler":
|
||||||
|
return self
|
48
libs/partners/openai/tests/unit_tests/llms/test_base.py
Normal file
48
libs/partners/openai/tests/unit_tests/llms/test_base.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from langchain_openai import OpenAI
|
||||||
|
|
||||||
|
os.environ["OPENAI_API_KEY"] = "foo"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.requires("openai")
|
||||||
|
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"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.requires("openai")
|
||||||
|
def test_openai_model_kwargs() -> None:
|
||||||
|
llm = OpenAI(model_kwargs={"foo": "bar"})
|
||||||
|
assert llm.model_kwargs == {"foo": "bar"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.requires("openai")
|
||||||
|
def test_openai_invalid_model_kwargs() -> None:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
OpenAI(model_kwargs={"model_name": "foo"})
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.requires("openai")
|
||||||
|
def test_openai_incorrect_field() -> None:
|
||||||
|
with pytest.warns(match="not default parameter"):
|
||||||
|
llm = OpenAI(foo="bar")
|
||||||
|
assert llm.model_kwargs == {"foo": "bar"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_completion() -> dict:
|
||||||
|
return {
|
||||||
|
"id": "cmpl-3evkmQda5Hu7fcZavknQda3SQ",
|
||||||
|
"object": "text_completion",
|
||||||
|
"created": 1689989000,
|
||||||
|
"model": "text-davinci-003",
|
||||||
|
"choices": [
|
||||||
|
{"text": "Bar Baz", "index": 0, "logprobs": None, "finish_reason": "length"}
|
||||||
|
],
|
||||||
|
"usage": {"prompt_tokens": 1, "completion_tokens": 2, "total_tokens": 3},
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
from langchain_openai.llms import __all__
|
||||||
|
|
||||||
|
EXPECTED_ALL = ["OpenAI", "AzureOpenAI"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_imports() -> None:
|
||||||
|
assert sorted(EXPECTED_ALL) == sorted(__all__)
|
14
libs/partners/openai/tests/unit_tests/test_imports.py
Normal file
14
libs/partners/openai/tests/unit_tests/test_imports.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from langchain_openai import __all__
|
||||||
|
|
||||||
|
EXPECTED_ALL = [
|
||||||
|
"OpenAI",
|
||||||
|
"ChatOpenAI",
|
||||||
|
"OpenAIEmbeddings",
|
||||||
|
"AzureOpenAI",
|
||||||
|
"AzureChatOpenAI",
|
||||||
|
"AzureOpenAIEmbeddings",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_imports() -> None:
|
||||||
|
assert sorted(EXPECTED_ALL) == sorted(__all__)
|
39
libs/partners/openai/tests/unit_tests/test_token_counts.py
Normal file
39
libs/partners/openai/tests/unit_tests/test_token_counts.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from langchain_openai import ChatOpenAI, OpenAI
|
||||||
|
|
||||||
|
_EXPECTED_NUM_TOKENS = {
|
||||||
|
"ada": 17,
|
||||||
|
"babbage": 17,
|
||||||
|
"curie": 17,
|
||||||
|
"davinci": 17,
|
||||||
|
"gpt-4": 12,
|
||||||
|
"gpt-4-32k": 12,
|
||||||
|
"gpt-3.5-turbo": 12,
|
||||||
|
}
|
||||||
|
|
||||||
|
_MODELS = models = [
|
||||||
|
"ada",
|
||||||
|
"babbage",
|
||||||
|
"curie",
|
||||||
|
"davinci",
|
||||||
|
]
|
||||||
|
_CHAT_MODELS = [
|
||||||
|
"gpt-4",
|
||||||
|
"gpt-4-32k",
|
||||||
|
"gpt-3.5-turbo",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("model", _MODELS)
|
||||||
|
def test_openai_get_num_tokens(model: str) -> None:
|
||||||
|
"""Test get_tokens."""
|
||||||
|
llm = OpenAI(model=model)
|
||||||
|
assert llm.get_num_tokens("表情符号是\n🦜🔗") == _EXPECTED_NUM_TOKENS[model]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("model", _CHAT_MODELS)
|
||||||
|
def test_chat_openai_get_num_tokens(model: str) -> None:
|
||||||
|
"""Test get_tokens."""
|
||||||
|
llm = ChatOpenAI(model=model)
|
||||||
|
assert llm.get_num_tokens("表情符号是\n🦜🔗") == _EXPECTED_NUM_TOKENS[model]
|
Loading…
Reference in New Issue
Block a user