From 29e993a5f2f597a157260dd0e847b054a39bb015 Mon Sep 17 00:00:00 2001 From: Lance Martin <122662504+rlancemartin@users.noreply.github.com> Date: Tue, 5 Dec 2023 15:31:10 -0800 Subject: [PATCH 01/20] Update OpenCLIP docs (#14319) --- docs/docs/integrations/text_embedding/open_clip.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/integrations/text_embedding/open_clip.ipynb b/docs/docs/integrations/text_embedding/open_clip.ipynb index eb5a2e4910cb9..b55be7d2587ee 100644 --- a/docs/docs/integrations/text_embedding/open_clip.ipynb +++ b/docs/docs/integrations/text_embedding/open_clip.ipynb @@ -92,7 +92,7 @@ "uri_house = \"/Users/rlm/Desktop/test/house.jpg\"\n", "\n", "# Embe images or text\n", - "clip_embd = OpenCLIPEmbeddings()\n", + "clip_embd = OpenCLIPEmbeddings(model_name=\"ViT-g-14\", checkpoint=\"laion2b_s34b_b88k\")\n", "img_feat_dog = clip_embd.embed_image([uri_dog])\n", "img_feat_house = clip_embd.embed_image([uri_house])\n", "text_feat_dog = clip_embd.embed_documents([\"dog\"])\n", From d22c13ec48be2828ead6bba46ca6611bb58e0b28 Mon Sep 17 00:00:00 2001 From: Ran Date: Wed, 6 Dec 2023 01:42:00 +0200 Subject: [PATCH 02/20] Mask API key for Minimax LLM (#14309) - **Description:** Added masking for the API key for Minimax LLM + tests inspired by https://github.com/langchain-ai/langchain/pull/12418. - **Issue:** the issue # fixes https://github.com/langchain-ai/langchain/issues/12165 - **Dependencies:** this fix is dependent on Minimax instantiation fix which is introduced in https://github.com/langchain-ai/langchain/pull/13439, so merge this one after. - **Tag maintainer:** @eyurtsev --------- Co-authored-by: Harrison Chase --- libs/langchain/langchain/llms/minimax.py | 29 ++++++------- .../tests/unit_tests/llms/test_minimax.py | 42 +++++++++++++++++++ 2 files changed, 55 insertions(+), 16 deletions(-) create mode 100644 libs/langchain/tests/unit_tests/llms/test_minimax.py diff --git a/libs/langchain/langchain/llms/minimax.py b/libs/langchain/langchain/llms/minimax.py index 488f296d5a864..dd5f00266b21d 100644 --- a/libs/langchain/langchain/llms/minimax.py +++ b/libs/langchain/langchain/llms/minimax.py @@ -10,14 +10,14 @@ ) import requests -from langchain_core.pydantic_v1 import BaseModel, Field, root_validator +from langchain_core.pydantic_v1 import BaseModel, Field, SecretStr, root_validator from langchain.callbacks.manager import ( CallbackManagerForLLMRun, ) from langchain.llms.base import LLM from langchain.llms.utils import enforce_stop_tokens -from langchain.utils import get_from_dict_or_env +from langchain.utils import convert_to_secret_str, get_from_dict_or_env logger = logging.getLogger(__name__) @@ -27,7 +27,7 @@ class _MinimaxEndpointClient(BaseModel): host: str group_id: str - api_key: str + api_key: SecretStr api_url: str @root_validator(pre=True, allow_reuse=True) @@ -40,7 +40,7 @@ def set_api_url(cls, values: Dict[str, Any]) -> Dict[str, Any]: return values def post(self, request: Any) -> Any: - headers = {"Authorization": f"Bearer {self.api_key}"} + headers = {"Authorization": f"Bearer {self.api_key.get_secret_value()}"} response = requests.post(self.api_url, headers=headers, json=request) # TODO: error handling and automatic retries if not response.ok: @@ -56,7 +56,7 @@ def post(self, request: Any) -> Any: class MinimaxCommon(BaseModel): """Common parameters for Minimax large language models.""" - _client: Any = None + _client: _MinimaxEndpointClient model: str = "abab5.5-chat" """Model name to use.""" max_tokens: int = 256 @@ -69,13 +69,13 @@ class MinimaxCommon(BaseModel): """Holds any model parameters valid for `create` call not explicitly specified.""" minimax_api_host: Optional[str] = None minimax_group_id: Optional[str] = None - minimax_api_key: Optional[str] = None + minimax_api_key: Optional[SecretStr] = None @root_validator() def validate_environment(cls, values: Dict) -> Dict: """Validate that api key and python package exists in environment.""" - values["minimax_api_key"] = get_from_dict_or_env( - values, "minimax_api_key", "MINIMAX_API_KEY" + values["minimax_api_key"] = convert_to_secret_str( + get_from_dict_or_env(values, "minimax_api_key", "MINIMAX_API_KEY") ) values["minimax_group_id"] = get_from_dict_or_env( values, "minimax_group_id", "MINIMAX_GROUP_ID" @@ -87,6 +87,11 @@ def validate_environment(cls, values: Dict) -> Dict: "MINIMAX_API_HOST", default="https://api.minimax.chat", ) + values["_client"] = _MinimaxEndpointClient( + host=values["minimax_api_host"], + api_key=values["minimax_api_key"], + group_id=values["minimax_group_id"], + ) return values @property @@ -110,14 +115,6 @@ def _llm_type(self) -> str: """Return type of llm.""" return "minimax" - def __init__(self, **data: Any): - super().__init__(**data) - self._client = _MinimaxEndpointClient( - host=self.minimax_api_host, - api_key=self.minimax_api_key, - group_id=self.minimax_group_id, - ) - class Minimax(MinimaxCommon, LLM): """Wrapper around Minimax large language models. diff --git a/libs/langchain/tests/unit_tests/llms/test_minimax.py b/libs/langchain/tests/unit_tests/llms/test_minimax.py new file mode 100644 index 0000000000000..9b53408f21d2a --- /dev/null +++ b/libs/langchain/tests/unit_tests/llms/test_minimax.py @@ -0,0 +1,42 @@ +"""Test Minimax llm""" +from typing import cast + +from langchain_core.pydantic_v1 import SecretStr +from pytest import CaptureFixture, MonkeyPatch + +from langchain.llms.minimax import Minimax + + +def test_api_key_is_secret_string() -> None: + llm = Minimax(minimax_api_key="secret-api-key", minimax_group_id="group_id") + assert isinstance(llm.minimax_api_key, SecretStr) + + +def test_api_key_masked_when_passed_from_env( + monkeypatch: MonkeyPatch, capsys: CaptureFixture +) -> None: + """Test initialization with an API key provided via an env variable""" + monkeypatch.setenv("MINIMAX_API_KEY", "secret-api-key") + monkeypatch.setenv("MINIMAX_GROUP_ID", "group_id") + llm = Minimax() + print(llm.minimax_api_key, end="") + captured = capsys.readouterr() + + assert captured.out == "**********" + + +def test_api_key_masked_when_passed_via_constructor( + capsys: CaptureFixture, +) -> None: + """Test initialization with an API key provided via the initializer""" + llm = Minimax(minimax_api_key="secret-api-key", minimax_group_id="group_id") + print(llm.minimax_api_key, end="") + captured = capsys.readouterr() + + assert captured.out == "**********" + + +def test_uses_actual_secret_value_from_secretstr() -> None: + """Test that actual secret is retrieved using `.get_secret_value()`.""" + llm = Minimax(minimax_api_key="secret-api-key", minimax_group_id="group_id") + assert cast(SecretStr, llm.minimax_api_key).get_secret_value() == "secret-api-key" From 9401539e430367a3c0ddeb1dd0c7e00e3ebb2f39 Mon Sep 17 00:00:00 2001 From: Karim Assi Date: Wed, 6 Dec 2023 00:56:31 +0100 Subject: [PATCH 03/20] Allow not enforcing function usage when a single function is passed to openai function executable (#14308) - **Description:** allows not enforcing function usage when a single function is passed to an openAI function executable (or corresponding legacy chain). This is a desired feature in the case where the model does not have enough information to call a function, and needs to get back to the user. - **Issue:** N/A - **Dependencies:** N/A - **Tag maintainer:** N/A --- .../langchain/chains/openai_functions/base.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/libs/langchain/langchain/chains/openai_functions/base.py b/libs/langchain/langchain/chains/openai_functions/base.py index 14259dff23e0f..6f162cc05245d 100644 --- a/libs/langchain/langchain/chains/openai_functions/base.py +++ b/libs/langchain/langchain/chains/openai_functions/base.py @@ -204,6 +204,7 @@ def create_openai_fn_runnable( llm: Runnable, prompt: BasePromptTemplate, *, + enforce_single_function_usage: bool = True, output_parser: Optional[Union[BaseOutputParser, BaseGenerationOutputParser]] = None, **kwargs: Any, ) -> Runnable: @@ -222,6 +223,9 @@ def create_openai_fn_runnable( pydantic.BaseModels for arguments. llm: Language model to use, assumed to support the OpenAI function-calling API. prompt: BasePromptTemplate to pass to the model. + enforce_single_function_usage: only used if a single function is passed in. If + True, then the model will be forced to use the given function. If False, + then the model will be given the option to use the given function or not. output_parser: BaseLLMOutputParser to use for parsing model outputs. By default will be inferred from the function types. If pydantic.BaseModels are passed in, then the OutputParser will try to parse outputs using those. Otherwise @@ -276,7 +280,7 @@ class RecordDog(BaseModel): raise ValueError("Need to pass in at least one function. Received zero.") openai_functions = [convert_to_openai_function(f) for f in functions] llm_kwargs: Dict[str, Any] = {"functions": openai_functions, **kwargs} - if len(openai_functions) == 1: + if len(openai_functions) == 1 and enforce_single_function_usage: llm_kwargs["function_call"] = {"name": openai_functions[0]["name"]} output_parser = output_parser or get_openai_output_parser(functions) return prompt | llm.bind(**llm_kwargs) | output_parser @@ -373,6 +377,7 @@ def create_openai_fn_chain( llm: BaseLanguageModel, prompt: BasePromptTemplate, *, + enforce_single_function_usage: bool = True, output_key: str = "function", output_parser: Optional[BaseLLMOutputParser] = None, **kwargs: Any, @@ -392,6 +397,9 @@ def create_openai_fn_chain( pydantic.BaseModels for arguments. llm: Language model to use, assumed to support the OpenAI function-calling API. prompt: BasePromptTemplate to pass to the model. + enforce_single_function_usage: only used if a single function is passed in. If + True, then the model will be forced to use the given function. If False, + then the model will be given the option to use the given function or not. output_key: The key to use when returning the output in LLMChain.__call__. output_parser: BaseLLMOutputParser to use for parsing model outputs. By default will be inferred from the function types. If pydantic.BaseModels are passed @@ -451,7 +459,7 @@ class RecordDog(BaseModel): llm_kwargs: Dict[str, Any] = { "functions": openai_functions, } - if len(openai_functions) == 1: + if len(openai_functions) == 1 and enforce_single_function_usage: llm_kwargs["function_call"] = {"name": openai_functions[0]["name"]} llm_chain = LLMChain( llm=llm, From 667ad6a5def90421fd036078578b869a26b0d0e3 Mon Sep 17 00:00:00 2001 From: Jarkko Lagus Date: Wed, 6 Dec 2023 02:05:40 +0200 Subject: [PATCH 04/20] Add support for CORS options for AzureSearch (#14305) - **Description:** Add support for setting the CORS options when using AzureSearch indexes --- libs/langchain/langchain/vectorstores/azuresearch.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libs/langchain/langchain/vectorstores/azuresearch.py b/libs/langchain/langchain/vectorstores/azuresearch.py index b818dc71fd59a..a0e48741c1abf 100644 --- a/libs/langchain/langchain/vectorstores/azuresearch.py +++ b/libs/langchain/langchain/vectorstores/azuresearch.py @@ -35,6 +35,7 @@ if TYPE_CHECKING: from azure.search.documents import SearchClient from azure.search.documents.indexes.models import ( + CorsOptions, ScoringProfile, SearchField, VectorSearch, @@ -78,6 +79,7 @@ def _get_search_client( default_scoring_profile: Optional[str] = None, default_fields: Optional[List[SearchField]] = None, user_agent: Optional[str] = "langchain", + cors_options: Optional[CorsOptions] = None, ) -> SearchClient: from azure.core.credentials import AzureKeyCredential from azure.core.exceptions import ResourceNotFoundError @@ -227,6 +229,7 @@ def fmt_err(x: str) -> str: semantic_settings=semantic_settings, scoring_profiles=scoring_profiles, default_scoring_profile=default_scoring_profile, + cors_options=cors_options, ) index_client.create_index(index) # Create the search client @@ -255,6 +258,7 @@ def __init__( semantic_settings: Optional[Union[SemanticSearch, SemanticSettings]] = None, scoring_profiles: Optional[List[ScoringProfile]] = None, default_scoring_profile: Optional[str] = None, + cors_options: Optional[CorsOptions] = None, **kwargs: Any, ): from azure.search.documents.indexes.models import ( @@ -305,6 +309,7 @@ def __init__( default_scoring_profile=default_scoring_profile, default_fields=default_fields, user_agent=user_agent, + cors_options=cors_options, ) self.search_type = search_type self.semantic_configuration_name = semantic_configuration_name From 63fdc6e818900c26a671c6c8923235d1397dd3e7 Mon Sep 17 00:00:00 2001 From: Bob Lin Date: Tue, 5 Dec 2023 18:08:03 -0600 Subject: [PATCH 05/20] Update docs (#14294) ### Description Fixed 3 doc issues: 1. `ConfigurableField ` needs to be imported in `docs/docs/expression_language/how_to/configure.ipynb` 2. use `error` instead of `RateLimitError()` in `docs/docs/expression_language/how_to/fallbacks.ipynb` 3. I think it might be better to output the fixed json data(when I looked at this example, I didn't understand its purpose at first, but then I suddenly realized): Screenshot 2023-12-05 at 10 34 13 PM --- .../how_to/configure.ipynb | 3 ++- .../how_to/fallbacks.ipynb | 2 +- .../how_to/functions.ipynb | 20 ++++++++++--------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/docs/docs/expression_language/how_to/configure.ipynb b/docs/docs/expression_language/how_to/configure.ipynb index c4e024b8c6514..c36bd7e5ad533 100644 --- a/docs/docs/expression_language/how_to/configure.ipynb +++ b/docs/docs/expression_language/how_to/configure.ipynb @@ -43,6 +43,7 @@ "source": [ "from langchain.chat_models import ChatOpenAI\n", "from langchain.prompts import PromptTemplate\n", + "from langchain.schema.runnable import ConfigurableField\n", "\n", "model = ChatOpenAI(temperature=0).configurable_fields(\n", " temperature=ConfigurableField(\n", @@ -594,7 +595,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.1" + "version": "3.11.5" } }, "nbformat": 4, diff --git a/docs/docs/expression_language/how_to/fallbacks.ipynb b/docs/docs/expression_language/how_to/fallbacks.ipynb index c5c241b7bf4d2..d7a47a4394dd3 100644 --- a/docs/docs/expression_language/how_to/fallbacks.ipynb +++ b/docs/docs/expression_language/how_to/fallbacks.ipynb @@ -190,7 +190,7 @@ ")\n", "\n", "chain = prompt | llm\n", - "with patch(\"openai.ChatCompletion.create\", side_effect=RateLimitError()):\n", + "with patch(\"openai.ChatCompletion.create\", side_effect=error):\n", " try:\n", " print(chain.invoke({\"animal\": \"kangaroo\"}))\n", " except:\n", diff --git a/docs/docs/expression_language/how_to/functions.ipynb b/docs/docs/expression_language/how_to/functions.ipynb index a11fc0b51025a..67e36114c0968 100644 --- a/docs/docs/expression_language/how_to/functions.ipynb +++ b/docs/docs/expression_language/how_to/functions.ipynb @@ -14,7 +14,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 1, "id": "6bb221b3", "metadata": {}, "outputs": [], @@ -56,17 +56,17 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 2, "id": "5488ec85", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "AIMessage(content='3 + 9 equals 12.', additional_kwargs={}, example=False)" + "AIMessage(content='3 + 9 equals 12.')" ] }, - "execution_count": 5, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } @@ -87,7 +87,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 3, "id": "80b3b5f6-5d58-44b9-807e-cce9a46bf49f", "metadata": {}, "outputs": [], @@ -98,7 +98,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 4, "id": "ff0daf0c-49dd-4d21-9772-e5fa133c5f36", "metadata": {}, "outputs": [], @@ -125,7 +125,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 5, "id": "1a5e709e-9d75-48c7-bb9c-503251990505", "metadata": {}, "outputs": [ @@ -133,6 +133,7 @@ "name": "stdout", "output_type": "stream", "text": [ + "{'foo': 'bar'}\n", "Tokens Used: 65\n", "\tPrompt Tokens: 56\n", "\tCompletion Tokens: 9\n", @@ -145,9 +146,10 @@ "from langchain.callbacks import get_openai_callback\n", "\n", "with get_openai_callback() as cb:\n", - " RunnableLambda(parse_or_fix).invoke(\n", + " output = RunnableLambda(parse_or_fix).invoke(\n", " \"{foo: bar}\", {\"tags\": [\"my-tag\"], \"callbacks\": [cb]}\n", " )\n", + " print(output)\n", " print(cb)" ] }, @@ -176,7 +178,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.1" + "version": "3.11.5" } }, "nbformat": 4, From 5a23608c4193f6622148206a4b99c32829c76fc4 Mon Sep 17 00:00:00 2001 From: Bob Lin Date: Tue, 5 Dec 2023 18:08:19 -0600 Subject: [PATCH 06/20] Add custom async generator example (#14299) Screenshot 2023-12-05 at 11 19 16 PM --- .../how_to/generators.ipynb | 86 +++++++++++++++++-- 1 file changed, 81 insertions(+), 5 deletions(-) diff --git a/docs/docs/expression_language/how_to/generators.ipynb b/docs/docs/expression_language/how_to/generators.ipynb index bf7319d27350a..8f0010c6c7ce5 100644 --- a/docs/docs/expression_language/how_to/generators.ipynb +++ b/docs/docs/expression_language/how_to/generators.ipynb @@ -17,6 +17,13 @@ "Let's implement a custom output parser for comma-separated lists." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Sync version" + ] + }, { "cell_type": "code", "execution_count": 1, @@ -57,7 +64,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 3, "metadata": {}, "outputs": [ { @@ -66,7 +73,7 @@ "'lion, tiger, wolf, gorilla, panda'" ] }, - "execution_count": 8, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -152,12 +159,81 @@ "list_chain.invoke({\"animal\": \"bear\"})" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Async version" + ] + }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, "outputs": [], - "source": [] + "source": [ + "from typing import AsyncIterator\n", + "\n", + "\n", + "async def asplit_into_list(\n", + " input: AsyncIterator[str]\n", + ") -> AsyncIterator[List[str]]: # async def\n", + " buffer = \"\"\n", + " async for (\n", + " chunk\n", + " ) in input: # `input` is a `async_generator` object, so use `async for`\n", + " buffer += chunk\n", + " while \",\" in buffer:\n", + " comma_index = buffer.index(\",\")\n", + " yield [buffer[:comma_index].strip()]\n", + " buffer = buffer[comma_index + 1 :]\n", + " yield [buffer.strip()]\n", + "\n", + "\n", + "list_chain = str_chain | asplit_into_list" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['lion']\n", + "['tiger']\n", + "['wolf']\n", + "['gorilla']\n", + "['panda']\n" + ] + } + ], + "source": [ + "async for chunk in list_chain.astream({\"animal\": \"bear\"}):\n", + " print(chunk, flush=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['lion', 'tiger', 'wolf', 'gorilla', 'panda']" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "await list_chain.ainvoke({\"animal\": \"bear\"})" + ] } ], "metadata": { @@ -176,7 +252,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.1" + "version": "3.11.5" } }, "nbformat": 4, From 62b59048de4921c6a21c7aa8906026fc4676524a Mon Sep 17 00:00:00 2001 From: Alex Kira Date: Tue, 5 Dec 2023 17:01:07 -0800 Subject: [PATCH 07/20] docs[patch] Add how-to doc for RunnablePassthrough and nav modifications (#14255) - **Description:** Add How To docs for `RunnablePassthrough` with examples. Also redo the ordering and some of the other How-To docs. --- .../how_to/functions.ipynb | 13 +- .../docs/expression_language/how_to/map.ipynb | 154 ++++++++++++++--- .../how_to/passthrough.ipynb | 159 ++++++++++++++++++ .../expression_language/how_to/routing.ipynb | 15 +- 4 files changed, 311 insertions(+), 30 deletions(-) create mode 100644 docs/docs/expression_language/how_to/passthrough.ipynb diff --git a/docs/docs/expression_language/how_to/functions.ipynb b/docs/docs/expression_language/how_to/functions.ipynb index 67e36114c0968..d1849ff0a7b3e 100644 --- a/docs/docs/expression_language/how_to/functions.ipynb +++ b/docs/docs/expression_language/how_to/functions.ipynb @@ -1,5 +1,16 @@ { "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "sidebar_position: 2\n", + "title: \"RunnableLambda: Run Custom Functions\"\n", + "keywords: [RunnableLambda, LCEL]\n", + "---" + ] + }, { "cell_type": "markdown", "id": "fbc4bf6e", @@ -7,7 +18,7 @@ "source": [ "# Run custom functions\n", "\n", - "You can use arbitrary functions in the pipeline\n", + "You can use arbitrary functions in the pipeline.\n", "\n", "Note that all inputs to these functions need to be a SINGLE argument. If you have a function that accepts multiple arguments, you should write a wrapper that accepts a single input and unpacks it into multiple argument." ] diff --git a/docs/docs/expression_language/how_to/map.ipynb b/docs/docs/expression_language/how_to/map.ipynb index 72df445bc1218..71c22a0b0e2d0 100644 --- a/docs/docs/expression_language/how_to/map.ipynb +++ b/docs/docs/expression_language/how_to/map.ipynb @@ -1,77 +1,136 @@ { "cells": [ + { + "cell_type": "markdown", + "id": "e2596041-9b76-4e74-836f-e6235086bbf0", + "metadata": {}, + "source": [ + "---\n", + "sidebar_position: 0\n", + "title: \"RunnableParallel: Manipulating data\"\n", + "keywords: [RunnableParallel, RunnableMap, LCEL]\n", + "---" + ] + }, { "cell_type": "markdown", "id": "b022ab74-794d-4c54-ad47-ff9549ddb9d2", "metadata": {}, "source": [ - "# Parallelize steps\n", + "# Manipulating inputs & output\n", "\n", - "RunnableParallel (aka. RunnableMap) makes it easy to execute multiple Runnables in parallel, and to return the output of these Runnables as a map." + "RunnableParallel can be useful for manipulating the output of one Runnable to match the input format of the next Runnable in a sequence.\n", + "\n", + "Here the input to prompt is expected to be a map with keys \"context\" and \"question\". The user input is just the question. So we need to get the context using our retriever and passthrough the user input under the \"question\" key.\n", + "\n", + "\n" ] }, { "cell_type": "code", - "execution_count": 2, - "id": "7e1873d6-d4b6-43ac-96a1-edcf178201e0", + "execution_count": 3, + "id": "267d1460-53c1-4fdb-b2c3-b6a1eb7fccff", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'joke': AIMessage(content=\"Why don't bears wear shoes? \\n\\nBecause they have bear feet!\", additional_kwargs={}, example=False),\n", - " 'poem': AIMessage(content=\"In woodland depths, bear prowls with might,\\nSilent strength, nature's sovereign, day and night.\", additional_kwargs={}, example=False)}" + "'Harrison worked at Kensho.'" ] }, - "execution_count": 2, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from langchain.chat_models import ChatOpenAI\n", + "from langchain.embeddings import OpenAIEmbeddings\n", "from langchain.prompts import ChatPromptTemplate\n", - "from langchain.schema.runnable import RunnableParallel\n", + "from langchain.schema.output_parser import StrOutputParser\n", + "from langchain.schema.runnable import RunnablePassthrough\n", + "from langchain.vectorstores import FAISS\n", "\n", + "vectorstore = FAISS.from_texts(\n", + " [\"harrison worked at kensho\"], embedding=OpenAIEmbeddings()\n", + ")\n", + "retriever = vectorstore.as_retriever()\n", + "template = \"\"\"Answer the question based only on the following context:\n", + "{context}\n", + "\n", + "Question: {question}\n", + "\"\"\"\n", + "prompt = ChatPromptTemplate.from_template(template)\n", "model = ChatOpenAI()\n", - "joke_chain = ChatPromptTemplate.from_template(\"tell me a joke about {topic}\") | model\n", - "poem_chain = (\n", - " ChatPromptTemplate.from_template(\"write a 2-line poem about {topic}\") | model\n", + "\n", + "retrieval_chain = (\n", + " {\"context\": retriever, \"question\": RunnablePassthrough()}\n", + " | prompt\n", + " | model\n", + " | StrOutputParser()\n", ")\n", "\n", - "map_chain = RunnableParallel(joke=joke_chain, poem=poem_chain)\n", + "retrieval_chain.invoke(\"where did harrison work?\")" + ] + }, + { + "cell_type": "markdown", + "id": "392cd4c4-e7ed-4ab8-934d-f7a4eca55ee1", + "metadata": {}, + "source": [ + "::: {.callout-tip}\n", + "Note that when composing a RunnableParallel with another Runnable we don't even need to wrap our dictionary in the RunnableParallel class — the type conversion is handled for us. In the context of a chain, these are equivalent:\n", + ":::\n", "\n", - "map_chain.invoke({\"topic\": \"bear\"})" + "```\n", + "{\"context\": retriever, \"question\": RunnablePassthrough()}\n", + "```\n", + "\n", + "```\n", + "RunnableParallel({\"context\": retriever, \"question\": RunnablePassthrough()})\n", + "```\n", + "\n", + "```\n", + "RunnableParallel(context=retriever, question=RunnablePassthrough())\n", + "```\n", + "\n" ] }, { "cell_type": "markdown", - "id": "df867ae9-1cec-4c9e-9fef-21969b206af5", + "id": "7c1b8baa-3a80-44f0-bb79-d22f79815d3d", "metadata": {}, "source": [ - "## Manipulating outputs/inputs\n", - "Maps can be useful for manipulating the output of one Runnable to match the input format of the next Runnable in a sequence." + "## Using itemgetter as shorthand\n", + "\n", + "Note that you can use Python's `itemgetter` as shorthand to extract data from the map when combining with `RunnableParallel`. You can find more information about itemgetter in the [Python Documentation](https://docs.python.org/3/library/operator.html#operator.itemgetter). \n", + "\n", + "In the example below, we use itemgetter to extract specific keys from the map:" ] }, { "cell_type": "code", - "execution_count": 3, - "id": "267d1460-53c1-4fdb-b2c3-b6a1eb7fccff", + "execution_count": 6, + "id": "84fc49e1-2daf-4700-ae33-a0a6ed47d5f6", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'Harrison worked at Kensho.'" + "'Harrison ha lavorato a Kensho.'" ] }, - "execution_count": 3, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ + "from operator import itemgetter\n", + "\n", + "from langchain.chat_models import ChatOpenAI\n", "from langchain.embeddings import OpenAIEmbeddings\n", + "from langchain.prompts import ChatPromptTemplate\n", "from langchain.schema.output_parser import StrOutputParser\n", "from langchain.schema.runnable import RunnablePassthrough\n", "from langchain.vectorstores import FAISS\n", @@ -80,31 +139,72 @@ " [\"harrison worked at kensho\"], embedding=OpenAIEmbeddings()\n", ")\n", "retriever = vectorstore.as_retriever()\n", + "\n", "template = \"\"\"Answer the question based only on the following context:\n", "{context}\n", "\n", "Question: {question}\n", + "\n", + "Answer in the following language: {language}\n", "\"\"\"\n", "prompt = ChatPromptTemplate.from_template(template)\n", "\n", - "retrieval_chain = (\n", - " {\"context\": retriever, \"question\": RunnablePassthrough()}\n", + "chain = (\n", + " {\n", + " \"context\": itemgetter(\"question\") | retriever,\n", + " \"question\": itemgetter(\"question\"),\n", + " \"language\": itemgetter(\"language\"),\n", + " }\n", " | prompt\n", " | model\n", " | StrOutputParser()\n", ")\n", "\n", - "retrieval_chain.invoke(\"where did harrison work?\")" + "chain.invoke({\"question\": \"where did harrison work\", \"language\": \"italian\"})" ] }, { "cell_type": "markdown", - "id": "392cd4c4-e7ed-4ab8-934d-f7a4eca55ee1", + "id": "bc2f9847-39aa-4fe4-9049-3a8969bc4bce", "metadata": {}, "source": [ - "Here the input to prompt is expected to be a map with keys \"context\" and \"question\". The user input is just the question. So we need to get the context using our retriever and passthrough the user input under the \"question\" key.\n", + "## Parallelize steps\n", + "\n", + "RunnableParallel (aka. RunnableMap) makes it easy to execute multiple Runnables in parallel, and to return the output of these Runnables as a map." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "31f18442-f837-463f-bef4-8729368f5f8b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'joke': AIMessage(content=\"Why don't bears wear shoes?\\n\\nBecause they have bear feet!\"),\n", + " 'poem': AIMessage(content=\"In the wild's embrace, bear roams free,\\nStrength and grace, a majestic decree.\")}" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain.chat_models import ChatOpenAI\n", + "from langchain.prompts import ChatPromptTemplate\n", + "from langchain.schema.runnable import RunnableParallel\n", + "\n", + "model = ChatOpenAI()\n", + "joke_chain = ChatPromptTemplate.from_template(\"tell me a joke about {topic}\") | model\n", + "poem_chain = (\n", + " ChatPromptTemplate.from_template(\"write a 2-line poem about {topic}\") | model\n", + ")\n", "\n", - "Note that when composing a RunnableParallel with another Runnable we don't even need to wrap our dictionary in the RunnableParallel class — the type conversion is handled for us." + "map_chain = RunnableParallel(joke=joke_chain, poem=poem_chain)\n", + "\n", + "map_chain.invoke({\"topic\": \"bear\"})" ] }, { @@ -194,7 +294,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.1" + "version": "3.11.6" } }, "nbformat": 4, diff --git a/docs/docs/expression_language/how_to/passthrough.ipynb b/docs/docs/expression_language/how_to/passthrough.ipynb new file mode 100644 index 0000000000000..4dc42d2e66c5b --- /dev/null +++ b/docs/docs/expression_language/how_to/passthrough.ipynb @@ -0,0 +1,159 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d35de667-0352-4bfb-a890-cebe7f676fe7", + "metadata": {}, + "source": [ + "---\n", + "sidebar_position: 1\n", + "title: \"RunnablePassthrough: Passing data through\"\n", + "keywords: [RunnablePassthrough, RunnableParallel, LCEL]\n", + "---" + ] + }, + { + "cell_type": "markdown", + "id": "b022ab74-794d-4c54-ad47-ff9549ddb9d2", + "metadata": {}, + "source": [ + "# Passing data through\n", + "\n", + "RunnablePassthrough allows to pass inputs unchanged or with the addition of extra keys. This typically is used in conjuction with RunnableParallel to assign data to a new key in the map. \n", + "\n", + "RunnablePassthrough() called on it's own, will simply take the input and pass it through. \n", + "\n", + "RunnablePassthrough called with assign (`RunnablePassthrough.assign(...)`) will take the input, and will add the extra arguments passed to the assign function. \n", + "\n", + "See the example below:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "03988b8d-d54c-4492-8707-1594372cf093", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'passed': {'num': 1}, 'extra': {'num': 1, 'mult': 3}, 'modified': 2}" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain.schema.runnable import RunnableParallel, RunnablePassthrough\n", + "\n", + "runnable = RunnableParallel(\n", + " passed=RunnablePassthrough(),\n", + " extra=RunnablePassthrough.assign(mult=lambda x: x[\"num\"] * 3),\n", + " modified=lambda x: x[\"num\"] + 1,\n", + ")\n", + "\n", + "runnable.invoke({\"num\": 1})" + ] + }, + { + "cell_type": "markdown", + "id": "702c7acc-cd31-4037-9489-647df192fd7c", + "metadata": {}, + "source": [ + "As seen above, `passed` key was called with `RunnablePassthrough()` and so it simply passed on `{'num': 1}`. \n", + "\n", + "In the second line, we used `RunnablePastshrough.assign` with a lambda that multiplies the numerical value by 3. In this cased, `extra` was set with `{'num': 1, 'mult': 3}` which is the original value with the `mult` key added. \n", + "\n", + "Finally, we also set a third key in the map with `modified` which uses a labmda to set a single value adding 1 to the num, which resulted in `modified` key with the value of `2`." + ] + }, + { + "cell_type": "markdown", + "id": "15187a3b-d666-4b9b-a258-672fc51fe0e2", + "metadata": {}, + "source": [ + "## Retrieval Example\n", + "\n", + "In the example below, we see a use case where we use RunnablePassthrough along with RunnableMap. " + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "267d1460-53c1-4fdb-b2c3-b6a1eb7fccff", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Harrison worked at Kensho.'" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain.chat_models import ChatOpenAI\n", + "from langchain.embeddings import OpenAIEmbeddings\n", + "from langchain.prompts import ChatPromptTemplate\n", + "from langchain.schema.output_parser import StrOutputParser\n", + "from langchain.schema.runnable import RunnablePassthrough\n", + "from langchain.vectorstores import FAISS\n", + "\n", + "vectorstore = FAISS.from_texts(\n", + " [\"harrison worked at kensho\"], embedding=OpenAIEmbeddings()\n", + ")\n", + "retriever = vectorstore.as_retriever()\n", + "template = \"\"\"Answer the question based only on the following context:\n", + "{context}\n", + "\n", + "Question: {question}\n", + "\"\"\"\n", + "prompt = ChatPromptTemplate.from_template(template)\n", + "model = ChatOpenAI()\n", + "\n", + "retrieval_chain = (\n", + " {\"context\": retriever, \"question\": RunnablePassthrough()}\n", + " | prompt\n", + " | model\n", + " | StrOutputParser()\n", + ")\n", + "\n", + "retrieval_chain.invoke(\"where did harrison work?\")" + ] + }, + { + "cell_type": "markdown", + "id": "392cd4c4-e7ed-4ab8-934d-f7a4eca55ee1", + "metadata": {}, + "source": [ + "Here the input to prompt is expected to be a map with keys \"context\" and \"question\". The user input is just the question. So we need to get the context using our retriever and passthrough the user input under the \"question\" key. In this case, the RunnablePassthrough allows us to pass on the user's question to the prompt and model. \n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/expression_language/how_to/routing.ipynb b/docs/docs/expression_language/how_to/routing.ipynb index 61f8598359b6b..e8242635d5f3b 100644 --- a/docs/docs/expression_language/how_to/routing.ipynb +++ b/docs/docs/expression_language/how_to/routing.ipynb @@ -1,5 +1,16 @@ { "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "sidebar_position: 3\n", + "title: \"RunnableBranch: Dynamically route logic based on input\"\n", + "keywords: [RunnableBranch, LCEL]\n", + "---" + ] + }, { "cell_type": "markdown", "id": "4b47436a", @@ -63,7 +74,7 @@ "chain = (\n", " PromptTemplate.from_template(\n", " \"\"\"Given the user question below, classify it as either being about `LangChain`, `Anthropic`, or `Other`.\n", - " \n", + "\n", "Do not respond with more than one word.\n", "\n", "\n", @@ -293,7 +304,7 @@ } ], "source": [ - "full_chain.invoke({\"question\": \"how do I use Anthroipc?\"})" + "full_chain.invoke({\"question\": \"how do I use Anthropic?\"})" ] }, { From 85b88c33f3ae210797e957758a83782973fe9c7c Mon Sep 17 00:00:00 2001 From: Tim Van Wassenhove Date: Wed, 6 Dec 2023 02:14:00 +0100 Subject: [PATCH 08/20] Fixes issue-14295: Correctly pass along the kwargs (#14296) - **Description:** Update code to correctly pass the kwargs - **Issue:** #14295 - **Dependencies:** - - **Tag maintainer:** <-- If no one reviews your PR within a few days, please @-mention one of @baskaryan, @eyurtsev, @hwchase17. --> #issue-14295 --- .../langchain/vectorstores/opensearch_vector_search.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/langchain/langchain/vectorstores/opensearch_vector_search.py b/libs/langchain/langchain/vectorstores/opensearch_vector_search.py index 81d58f8a8a249..711aa67e0dc8f 100644 --- a/libs/langchain/langchain/vectorstores/opensearch_vector_search.py +++ b/libs/langchain/langchain/vectorstores/opensearch_vector_search.py @@ -414,7 +414,7 @@ def add_texts( metadatas=metadatas, ids=ids, bulk_size=bulk_size, - kwargs=kwargs, + **kwargs, ) def add_embeddings( @@ -451,7 +451,7 @@ def add_embeddings( metadatas=metadatas, ids=ids, bulk_size=bulk_size, - kwargs=kwargs, + **kwargs, ) def similarity_search( From c215a4c9ec479ea9d0d66d68aa6afbd984ca0e82 Mon Sep 17 00:00:00 2001 From: Massimiliano Pronesti Date: Wed, 6 Dec 2023 02:22:05 +0100 Subject: [PATCH 09/20] feat(embeddings): text-embeddings-inference (#14288) - **Description:** Added a notebook to illustrate how to use `text-embeddings-inference` from huggingface. As `HuggingFaceHubEmbeddings` was using a deprecated client, I made the most of this PR updating that too. - **Issue:** #13286 - **Dependencies**: None - **Tag maintainer:** @baskaryan --- .../text_embeddings_inference.ipynb | 171 ++++++++++++++++++ .../langchain/embeddings/huggingface_hub.py | 44 +++-- 2 files changed, 196 insertions(+), 19 deletions(-) create mode 100644 docs/docs/integrations/text_embedding/text_embeddings_inference.ipynb diff --git a/docs/docs/integrations/text_embedding/text_embeddings_inference.ipynb b/docs/docs/integrations/text_embedding/text_embeddings_inference.ipynb new file mode 100644 index 0000000000000..dafedba496730 --- /dev/null +++ b/docs/docs/integrations/text_embedding/text_embeddings_inference.ipynb @@ -0,0 +1,171 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "ceabf1eb-ca96-4791-90ad-e9acb31edf5c", + "metadata": {}, + "source": [ + "# Text Embeddings Inference\n", + "\n", + "Text Embeddings Inference (TEI) is a toolkit for deploying and serving open source text embeddings and sequence classification models. TEI enables high-performance extraction for the most popular models, including FlagEmbedding, Ember, GTE and E5.\n", + "\n", + "To use it within langchain, first install `huggingface-hub`." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "579f0677-aa06-4ad8-a816-3520c8d6923c", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "!pip install huggingface-hub -q" + ] + }, + { + "cell_type": "markdown", + "id": "7c6b1015-bc3f-4283-93d5-11387be1b98d", + "metadata": {}, + "source": [ + "Then expose an embedding model using TEI. For instance, using Docker, you can serve `BAAI/bge-large-en-v1.5` as follows:\n", + "\n", + "```bash\n", + "model=BAAI/bge-large-en-v1.5\n", + "revision=refs/pr/5\n", + "volume=$PWD/data # share a volume with the Docker container to avoid downloading weights every run\n", + "\n", + "docker run --gpus all -p 8080:80 -v $volume:/data --pull always ghcr.io/huggingface/text-embeddings-inference:0.6 --model-id $model --revision $revision\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "48eebefc-a631-48dd-9bde-4a987f81aa20", + "metadata": {}, + "source": [ + "Finally, instantiate the client and embed your texts." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "22b09777-5ba3-4fbe-81cf-a702a55df9c4", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from langchain.embeddings import HuggingFaceHubEmbeddings" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "c26fca9f-cfdb-45e5-a0bd-f677ff8b9d92", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + "Enter your HF API Key:\n", + "\n", + " ········\n" + ] + } + ], + "source": [ + "from getpass import getpass\n", + "\n", + "huggingfacehub_api_token = getpass(\"Enter your HF API Key:\\n\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "f9a92970-16f4-458c-b186-2a83e9f7d840", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "embeddings = HuggingFaceHubEmbeddings(\n", + " model=\"http://localhost:8080\", huggingfacehub_api_token=huggingfacehub_api_token\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "42105438-9fee-460a-9c52-b7c595722758", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "text = \"What is deep learning?\"" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "20167762-0988-4205-bbd4-1f20fd9dd247", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[0.018113142, 0.00302585, -0.049911194]" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "query_result = embeddings.embed_query(text)\n", + "query_result[:3]" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "54b87cf6-86ad-46f5-b2cd-17eb43cb4d0b", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "doc_result = embeddings.embed_documents([text])" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "conda_python3", + "language": "python", + "name": "conda_python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/libs/langchain/langchain/embeddings/huggingface_hub.py b/libs/langchain/langchain/embeddings/huggingface_hub.py index c864d673d91c4..60b7808dbe3a9 100644 --- a/libs/langchain/langchain/embeddings/huggingface_hub.py +++ b/libs/langchain/langchain/embeddings/huggingface_hub.py @@ -1,3 +1,4 @@ +import json from typing import Any, Dict, List, Optional from langchain_core.embeddings import Embeddings @@ -5,7 +6,7 @@ from langchain.utils import get_from_dict_or_env -DEFAULT_REPO_ID = "sentence-transformers/all-mpnet-base-v2" +DEFAULT_MODEL = "sentence-transformers/all-mpnet-base-v2" VALID_TASKS = ("feature-extraction",) @@ -20,17 +21,19 @@ class HuggingFaceHubEmbeddings(BaseModel, Embeddings): .. code-block:: python from langchain.embeddings import HuggingFaceHubEmbeddings - repo_id = "sentence-transformers/all-mpnet-base-v2" + model = "sentence-transformers/all-mpnet-base-v2" hf = HuggingFaceHubEmbeddings( - repo_id=repo_id, + model=model, task="feature-extraction", huggingfacehub_api_token="my-api-key", ) """ client: Any #: :meta private: - repo_id: str = DEFAULT_REPO_ID + model: Optional[str] = None """Model name to use.""" + repo_id: Optional[str] = None + """Huggingfacehub repository id, for backward compatibility.""" task: Optional[str] = "feature-extraction" """Task to call the model with.""" model_kwargs: Optional[dict] = None @@ -50,22 +53,23 @@ def validate_environment(cls, values: Dict) -> Dict: values, "huggingfacehub_api_token", "HUGGINGFACEHUB_API_TOKEN" ) try: - from huggingface_hub.inference_api import InferenceApi - - repo_id = values["repo_id"] - if not repo_id.startswith("sentence-transformers"): - raise ValueError( - "Currently only 'sentence-transformers' embedding models " - f"are supported. Got invalid 'repo_id' {repo_id}." - ) - client = InferenceApi( - repo_id=repo_id, + from huggingface_hub import InferenceClient + + if values["model"]: + values["repo_id"] = values["model"] + elif values["repo_id"]: + values["model"] = values["repo_id"] + else: + values["model"] = DEFAULT_MODEL + values["repo_id"] = DEFAULT_MODEL + + client = InferenceClient( + model=values["model"], token=huggingfacehub_api_token, - task=values.get("task"), ) - if client.task not in VALID_TASKS: + if values["task"] not in VALID_TASKS: raise ValueError( - f"Got invalid task {client.task}, " + f"Got invalid task {values['task']}, " f"currently only {VALID_TASKS} are supported" ) values["client"] = client @@ -88,8 +92,10 @@ def embed_documents(self, texts: List[str]) -> List[List[float]]: # replace newlines, which can negatively affect performance. texts = [text.replace("\n", " ") for text in texts] _model_kwargs = self.model_kwargs or {} - responses = self.client(inputs=texts, params=_model_kwargs) - return responses + responses = self.client.post( + json={"inputs": texts, "parameters": _model_kwargs, "task": self.task} + ) + return json.loads(responses.decode()) def embed_query(self, text: str) -> List[float]: """Call out to HuggingFaceHub's embedding endpoint for embedding query text. From fd5be55a7bd44b4333f39e9942d8c525dbffdb90 Mon Sep 17 00:00:00 2001 From: Leonid Kuligin Date: Wed, 6 Dec 2023 02:24:19 +0100 Subject: [PATCH 10/20] added get_num_tokens to GooglePalm (#14282) added get_num_tokens to GooglePalm + a little bit of refactoring --- libs/langchain/langchain/llms/google_palm.py | 76 ++++++++----------- libs/langchain/langchain/llms/vertexai.py | 34 ++------- .../langchain/langchain/utilities/vertexai.py | 33 +++++++- .../llms/test_google_palm.py | 18 +++++ 4 files changed, 88 insertions(+), 73 deletions(-) diff --git a/libs/langchain/langchain/llms/google_palm.py b/libs/langchain/langchain/llms/google_palm.py index 1e40143c89790..16491df1f9069 100644 --- a/libs/langchain/langchain/llms/google_palm.py +++ b/libs/langchain/langchain/llms/google_palm.py @@ -1,62 +1,32 @@ from __future__ import annotations -import logging -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Dict, List, Optional from langchain_core.outputs import Generation, LLMResult from langchain_core.pydantic_v1 import BaseModel, root_validator -from tenacity import ( - before_sleep_log, - retry, - retry_if_exception_type, - stop_after_attempt, - wait_exponential, -) from langchain.callbacks.manager import CallbackManagerForLLMRun from langchain.llms import BaseLLM +from langchain.utilities.vertexai import create_retry_decorator from langchain.utils import get_from_dict_or_env -logger = logging.getLogger(__name__) - -def _create_retry_decorator() -> Callable[[Any], Any]: - """Returns a tenacity retry decorator, preconfigured to handle PaLM exceptions""" - try: - import google.api_core.exceptions - except ImportError: - raise ImportError( - "Could not import google-api-core python package. " - "Please install it with `pip install google-api-core`." - ) - - multiplier = 2 - min_seconds = 1 - max_seconds = 60 - max_retries = 10 - - return retry( - reraise=True, - stop=stop_after_attempt(max_retries), - wait=wait_exponential(multiplier=multiplier, min=min_seconds, max=max_seconds), - retry=( - retry_if_exception_type(google.api_core.exceptions.ResourceExhausted) - | retry_if_exception_type(google.api_core.exceptions.ServiceUnavailable) - | retry_if_exception_type(google.api_core.exceptions.GoogleAPIError) - ), - before_sleep=before_sleep_log(logger, logging.WARNING), - ) - - -def generate_with_retry(llm: GooglePalm, **kwargs: Any) -> Any: +def completion_with_retry( + llm: GooglePalm, + *args: Any, + run_manager: Optional[CallbackManagerForLLMRun] = None, + **kwargs: Any, +) -> Any: """Use tenacity to retry the completion call.""" - retry_decorator = _create_retry_decorator() + retry_decorator = create_retry_decorator( + llm, max_retries=llm.max_retries, run_manager=run_manager + ) @retry_decorator - def _generate_with_retry(**kwargs: Any) -> Any: - return llm.client.generate_text(**kwargs) + def _completion_with_retry(*args: Any, **kwargs: Any) -> Any: + return llm.client.generate_text(*args, **kwargs) - return _generate_with_retry(**kwargs) + return _completion_with_retry(*args, **kwargs) def _strip_erroneous_leading_spaces(text: str) -> str: @@ -94,6 +64,8 @@ class GooglePalm(BaseLLM, BaseModel): n: int = 1 """Number of chat completions to generate for each prompt. Note that the API may not return the full n completions if duplicates are generated.""" + max_retries: int = 6 + """The maximum number of retries to make when generating.""" @property def lc_secrets(self) -> Dict[str, str]: @@ -144,7 +116,7 @@ def _generate( ) -> LLMResult: generations = [] for prompt in prompts: - completion = generate_with_retry( + completion = completion_with_retry( self, model=self.model_name, prompt=prompt, @@ -170,3 +142,17 @@ def _generate( def _llm_type(self) -> str: """Return type of llm.""" return "google_palm" + + def get_num_tokens(self, text: str) -> int: + """Get the number of tokens present in the text. + + Useful for checking if an input will fit in a model's context window. + + Args: + text: The string input to tokenize. + + Returns: + The integer number of tokens in the text. + """ + result = self.client.count_text_tokens(model=self.model_name, prompt=text) + return result["token_count"] diff --git a/libs/langchain/langchain/llms/vertexai.py b/libs/langchain/langchain/llms/vertexai.py index f074f6578d051..0dedeaf0df853 100644 --- a/libs/langchain/langchain/llms/vertexai.py +++ b/libs/langchain/langchain/llms/vertexai.py @@ -4,13 +4,11 @@ from typing import ( TYPE_CHECKING, Any, - Callable, ClassVar, Dict, Iterator, List, Optional, - Union, ) from langchain_core.outputs import Generation, GenerationChunk, LLMResult @@ -20,8 +18,9 @@ AsyncCallbackManagerForLLMRun, CallbackManagerForLLMRun, ) -from langchain.llms.base import BaseLLM, create_base_retry_decorator +from langchain.llms.base import BaseLLM from langchain.utilities.vertexai import ( + create_retry_decorator, get_client_info, init_vertexai, raise_vertex_import_error, @@ -65,27 +64,6 @@ def is_codey_model(model_name: str) -> bool: return "code" in model_name -def _create_retry_decorator( - llm: VertexAI, - *, - run_manager: Optional[ - Union[AsyncCallbackManagerForLLMRun, CallbackManagerForLLMRun] - ] = None, -) -> Callable[[Any], Any]: - import google.api_core - - errors = [ - google.api_core.exceptions.ResourceExhausted, - google.api_core.exceptions.ServiceUnavailable, - google.api_core.exceptions.Aborted, - google.api_core.exceptions.DeadlineExceeded, - ] - decorator = create_base_retry_decorator( - error_types=errors, max_retries=llm.max_retries, run_manager=run_manager - ) - return decorator - - def completion_with_retry( llm: VertexAI, *args: Any, @@ -93,7 +71,7 @@ def completion_with_retry( **kwargs: Any, ) -> Any: """Use tenacity to retry the completion call.""" - retry_decorator = _create_retry_decorator(llm, run_manager=run_manager) + retry_decorator = create_retry_decorator(llm, run_manager=run_manager) @retry_decorator def _completion_with_retry(*args: Any, **kwargs: Any) -> Any: @@ -109,7 +87,9 @@ def stream_completion_with_retry( **kwargs: Any, ) -> Any: """Use tenacity to retry the completion call.""" - retry_decorator = _create_retry_decorator(llm, run_manager=run_manager) + retry_decorator = create_retry_decorator( + llm, max_retries=llm.max_retries, run_manager=run_manager + ) @retry_decorator def _completion_with_retry(*args: Any, **kwargs: Any) -> Any: @@ -125,7 +105,7 @@ async def acompletion_with_retry( **kwargs: Any, ) -> Any: """Use tenacity to retry the completion call.""" - retry_decorator = _create_retry_decorator(llm, run_manager=run_manager) + retry_decorator = create_retry_decorator(llm, run_manager=run_manager) @retry_decorator async def _acompletion_with_retry(*args: Any, **kwargs: Any) -> Any: diff --git a/libs/langchain/langchain/utilities/vertexai.py b/libs/langchain/langchain/utilities/vertexai.py index a60d5eea39ab3..b0c9644790383 100644 --- a/libs/langchain/langchain/utilities/vertexai.py +++ b/libs/langchain/langchain/utilities/vertexai.py @@ -1,12 +1,43 @@ """Utilities to init Vertex AI.""" from importlib import metadata -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Any, Callable, Optional, Union + +from langchain_core.language_models.llms import BaseLLM, create_base_retry_decorator + +from langchain.callbacks.manager import ( + AsyncCallbackManagerForLLMRun, + CallbackManagerForLLMRun, +) if TYPE_CHECKING: from google.api_core.gapic_v1.client_info import ClientInfo from google.auth.credentials import Credentials +def create_retry_decorator( + llm: BaseLLM, + *, + max_retries: int = 1, + run_manager: Optional[ + Union[AsyncCallbackManagerForLLMRun, CallbackManagerForLLMRun] + ] = None, +) -> Callable[[Any], Any]: + """Creates a retry decorator for Vertex / Palm LLMs.""" + import google.api_core + + errors = [ + google.api_core.exceptions.ResourceExhausted, + google.api_core.exceptions.ServiceUnavailable, + google.api_core.exceptions.Aborted, + google.api_core.exceptions.DeadlineExceeded, + google.api_core.exceptions.GoogleAPIError, + ] + decorator = create_base_retry_decorator( + error_types=errors, max_retries=max_retries, run_manager=run_manager + ) + return decorator + + def raise_vertex_import_error(minimum_expected_version: str = "1.36.0") -> None: """Raise ImportError related to Vertex SDK being not available. diff --git a/libs/langchain/tests/integration_tests/llms/test_google_palm.py b/libs/langchain/tests/integration_tests/llms/test_google_palm.py index ca02b185f0d77..b82895ba258db 100644 --- a/libs/langchain/tests/integration_tests/llms/test_google_palm.py +++ b/libs/langchain/tests/integration_tests/llms/test_google_palm.py @@ -6,6 +6,8 @@ from pathlib import Path +from langchain_core.outputs import LLMResult + from langchain.llms.google_palm import GooglePalm from langchain.llms.loading import load_llm @@ -15,6 +17,22 @@ def test_google_palm_call() -> None: llm = GooglePalm(max_output_tokens=10) output = llm("Say foo:") assert isinstance(output, str) + assert llm._llm_type == "google_palm" + assert llm.model_name == "models/text-bison-001" + + +def test_google_palm_generate() -> None: + llm = GooglePalm(temperature=0.3, n=2) + output = llm.generate(["Say foo:"]) + assert isinstance(output, LLMResult) + assert len(output.generations) == 1 + assert len(output.generations[0]) == 2 + + +def test_google_palm_get_num_tokens() -> None: + llm = GooglePalm() + output = llm.get_num_tokens("How are you?") + assert output == 4 def test_saving_loading_llm(tmp_path: Path) -> None: From 7205bfdd00eb964014ab4e63d8d00ec6834f88e9 Mon Sep 17 00:00:00 2001 From: Wang Wei Date: Wed, 6 Dec 2023 09:28:31 +0800 Subject: [PATCH 11/20] feat: 1. Add system parameters, 2. Align with the QianfanChatEndpoint for function calling (#14275) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - **Description:** 1. Add system parameters to the ERNIE LLM API to set the role of the LLM. 2. Add support for the ERNIE-Bot-turbo-AI model according from the document https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Alp0kdm0n. 3. For the function call of ErnieBotChat, align with the QianfanChatEndpoint. With this PR, the `QianfanChatEndpoint()` can use the `function calling` ability with `create_ernie_fn_chain()`. The example is as the following: ``` from langchain.prompts import ChatPromptTemplate import json from langchain.prompts.chat import ( ChatPromptTemplate, ) from langchain.chat_models import QianfanChatEndpoint from langchain.chains.ernie_functions import ( create_ernie_fn_chain, ) def get_current_news(location: str) -> str: """Get the current news based on the location.' Args: location (str): The location to query. Returs: str: Current news based on the location. """ news_info = { "location": location, "news": [ "I have a Book.", "It's a nice day, today." ] } return json.dumps(news_info) def get_current_weather(location: str, unit: str="celsius") -> str: """Get the current weather in a given location Args: location (str): location of the weather. unit (str): unit of the tempuature. Returns: str: weather in the given location. """ weather_info = { "location": location, "temperature": "27", "unit": unit, "forecast": ["sunny", "windy"], } return json.dumps(weather_info) template = ChatPromptTemplate.from_messages([ ("user", "{user_input}"), ]) chat = QianfanChatEndpoint(model="ERNIE-Bot-4") chain = create_ernie_fn_chain([get_current_weather, get_current_news], chat, template, verbose=True) res = chain.run("北京今天的新闻是什么?") print(res) ``` The result of the above code: ``` > Entering new LLMChain chain... Prompt after formatting: Human: 北京今天的新闻是什么? > Finished chain. {'name': 'get_current_news', 'arguments': {'location': '北京'}} ``` For the `ErnieBotChat`, now can use the `system` parameter to set the role of the LLM. ``` from langchain.prompts import ChatPromptTemplate from langchain.chains import LLMChain from langchain.chat_models import ErnieBotChat llm = ErnieBotChat(model_name="ERNIE-Bot-turbo-AI", system="你是一个能力很强的机器人,你的名字叫 小叮当。无论问你什么问题,你都可以给出答案。") prompt = ChatPromptTemplate.from_messages( [ ("human", "{query}"), ] ) chain = LLMChain(llm=llm, prompt=prompt, verbose=True) res = chain.run(query="你是谁?") print(res) ``` The result of the above code: ``` > Entering new LLMChain chain... Prompt after formatting: Human: 你是谁? > Finished chain. 我是小叮当,一个智能机器人。我可以为你提供各种服务,包括回答问题、提供信息、进行计算等。如果你需要任何帮助,请随时告诉我,我会尽力为你提供最好的服务。 ``` --- libs/langchain/langchain/chat_models/ernie.py | 30 +++++++++++++------ .../output_parsers/ernie_functions.py | 8 ++--- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/libs/langchain/langchain/chat_models/ernie.py b/libs/langchain/langchain/chat_models/ernie.py index 7687c1241e285..a46a15bbdb85d 100644 --- a/libs/langchain/langchain/chat_models/ernie.py +++ b/libs/langchain/langchain/chat_models/ernie.py @@ -1,4 +1,3 @@ -import json import logging import threading from typing import Any, Dict, List, Mapping, Optional @@ -46,7 +45,8 @@ class ErnieBotChat(BaseChatModel): and will be regenerated after expiration (30 days). Default model is `ERNIE-Bot-turbo`, - currently supported models are `ERNIE-Bot-turbo`, `ERNIE-Bot` + currently supported models are `ERNIE-Bot-turbo`, `ERNIE-Bot`, `ERNIE-Bot-8K`, + `ERNIE-Bot-4`, `ERNIE-Bot-turbo-AI`. Example: .. code-block:: python @@ -87,6 +87,11 @@ class ErnieBotChat(BaseChatModel): """model name of ernie, default is `ERNIE-Bot-turbo`. Currently supported `ERNIE-Bot-turbo`, `ERNIE-Bot`""" + system: Optional[str] = None + """system is mainly used for model character design, + for example, you are an AI assistant produced by xxx company. + The length of the system is limiting of 1024 characters.""" + request_timeout: Optional[int] = 60 """request timeout for chat http requests""" @@ -123,6 +128,7 @@ def _chat(self, payload: object) -> dict: "ERNIE-Bot": "completions", "ERNIE-Bot-8K": "ernie_bot_8k", "ERNIE-Bot-4": "completions_pro", + "ERNIE-Bot-turbo-AI": "ai_apaas", "BLOOMZ-7B": "bloomz_7b1", "Llama-2-7b-chat": "llama_2_7b", "Llama-2-13b-chat": "llama_2_13b", @@ -180,6 +186,7 @@ def _generate( "top_p": self.top_p, "temperature": self.temperature, "penalty_score": self.penalty_score, + "system": self.system, **kwargs, } logger.debug(f"Payload for ernie api is {payload}") @@ -195,14 +202,19 @@ def _generate( def _create_chat_result(self, response: Mapping[str, Any]) -> ChatResult: if "function_call" in response: - fc_str = '{{"function_call": {}}}'.format( - json.dumps(response.get("function_call")) - ) - generations = [ChatGeneration(message=AIMessage(content=fc_str))] + additional_kwargs = { + "function_call": dict(response.get("function_call", {})) + } else: - generations = [ - ChatGeneration(message=AIMessage(content=response.get("result"))) - ] + additional_kwargs = {} + generations = [ + ChatGeneration( + message=AIMessage( + content=response.get("result"), + additional_kwargs={**additional_kwargs}, + ) + ) + ] token_usage = response.get("usage", {}) llm_output = {"token_usage": token_usage, "model_name": self.model_name} return ChatResult(generations=generations, llm_output=llm_output) diff --git a/libs/langchain/langchain/output_parsers/ernie_functions.py b/libs/langchain/langchain/output_parsers/ernie_functions.py index b2682c4dc2166..dd5e45853454a 100644 --- a/libs/langchain/langchain/output_parsers/ernie_functions.py +++ b/libs/langchain/langchain/output_parsers/ernie_functions.py @@ -72,12 +72,8 @@ def parse_result(self, result: List[Generation], *, partial: bool = False) -> An "This output parser can only be used with a chat generation." ) message = generation.message - message.additional_kwargs["function_call"] = {} - if "function_call" in message.content: - function_call = json.loads(str(message.content)) - if "function_call" in function_call: - fc = function_call["function_call"] - message.additional_kwargs["function_call"] = fc + if "function_call" not in message.additional_kwargs: + return None try: function_call = message.additional_kwargs["function_call"] except KeyError as exc: From 20d2b4a6baea9ea570d2dac95a17acfcfe0ebffb Mon Sep 17 00:00:00 2001 From: lif <1835304752@qq.com> Date: Wed, 6 Dec 2023 09:31:28 +0800 Subject: [PATCH 12/20] feat: Increased compatibility with new and old versions for dalle (#14222) - **Description:** Increased compatibility with all versions openai for dalle, This pr add support for openai version from 0 ~ 1.3. --- .../utilities/dalle_image_generator.py | 141 +++++++++++++++--- 1 file changed, 118 insertions(+), 23 deletions(-) diff --git a/libs/langchain/langchain/utilities/dalle_image_generator.py b/libs/langchain/langchain/utilities/dalle_image_generator.py index c81027db700c0..5eaa9176c1b6d 100644 --- a/libs/langchain/langchain/utilities/dalle_image_generator.py +++ b/libs/langchain/langchain/utilities/dalle_image_generator.py @@ -1,9 +1,17 @@ """Utility that calls OpenAI's Dall-E Image Generator.""" -from typing import Any, Dict, Optional +import logging +import os +from typing import Any, Dict, Mapping, Optional, Tuple, Union -from langchain_core.pydantic_v1 import BaseModel, Extra, root_validator +from langchain_core.pydantic_v1 import BaseModel, Extra, Field, root_validator +from langchain_core.utils import ( + get_pydantic_field_names, +) from langchain.utils import get_from_dict_or_env +from langchain.utils.openai import is_openai_v1 + +logger = logging.getLogger(__name__) class DallEAPIWrapper(BaseModel): @@ -18,52 +26,139 @@ class DallEAPIWrapper(BaseModel): """ client: Any #: :meta private: - openai_api_key: Optional[str] = None + async_client: Any = Field(default=None, exclude=True) #: :meta private: + model_name: str = Field(default="dall-e-2", alias="model") + model_kwargs: Dict[str, Any] = Field(default_factory=dict) + 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" + ) n: int = 1 """Number of images to generate""" size: str = "1024x1024" """Size of image to generate""" separator: str = "\n" """Separator to use when multiple URLs are returned.""" - model: Optional[str] = "dall-e-2" - """Model to use for image generation""" quality: Optional[str] = "standard" """Quality of the image that will be generated""" + max_retries: int = 2 + """Maximum number of retries to make when generating.""" + 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.""" extra = Extra.forbid + @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: + logger.warning( + 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.""" - openai_api_key = get_from_dict_or_env( + values["openai_api_key"] = get_from_dict_or_env( values, "openai_api_key", "OPENAI_API_KEY" ) - try: - from openai import OpenAI + # Check OPENAI_ORGANIZATION for backwards compatibility. + values["openai_organization"] = ( + values["openai_organization"] + or os.getenv("OPENAI_ORG_ID") + or os.getenv("OPENAI_ORGANIZATION") + or None + ) + 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 = OpenAI( - api_key=openai_api_key, # this is also the default, it can be omitted - ) + try: + import openai - values["client"] = client - except ImportError as e: + except ImportError: raise ImportError( "Could not import openai python package. " - "Please it install it with `pip install openai`." - ) from e + "Please install it with `pip install openai`." + ) + + if is_openai_v1(): + 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).images + if not values.get("async_client"): + values["async_client"] = openai.AsyncOpenAI(**client_params).images + elif not values.get("client"): + values["client"] = openai.Image + else: + pass return values def run(self, query: str) -> str: """Run query through OpenAI and parse result.""" - response = self.client.images.generate( - prompt=query, - n=self.n, - size=self.size, - model=self.model, - quality=self.quality, - ) - image_urls = self.separator.join([item.url for item in response.data]) + + if is_openai_v1(): + response = self.client.generate( + prompt=query, + n=self.n, + size=self.size, + model=self.model_name, + quality=self.quality, + ) + image_urls = self.separator.join([item.url for item in response.data]) + else: + response = self.client.create( + prompt=query, n=self.n, size=self.size, model=self.model_name + ) + image_urls = self.separator.join([item["url"] for item in response["data"]]) + return image_urls if image_urls else "No image was generated" From 7c2ef06136c7d004e693ea01ddffb56dccaf96a9 Mon Sep 17 00:00:00 2001 From: jeffpezzone Date: Tue, 5 Dec 2023 22:07:33 -0500 Subject: [PATCH 13/20] Adds "NIN" metadata filter for pgvector to all checking for set absence (#14205) This PR adds support for metadata filters of the form: `{"filter": {"key": { "NIN" : ["list", "of", "values"]}}}` "IN" is already supported, so this is a quick & related update to add "NIN" --- .../langchain/vectorstores/pgvector.py | 20 ++++++++++++----- .../vectorstores/test_pgvector.py | 22 ++++++++++++++++++- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/libs/langchain/langchain/vectorstores/pgvector.py b/libs/langchain/langchain/vectorstores/pgvector.py index e1a58e81afdde..c10686bf2b4ba 100644 --- a/libs/langchain/langchain/vectorstores/pgvector.py +++ b/libs/langchain/langchain/vectorstores/pgvector.py @@ -434,16 +434,24 @@ def __query_collection( if filter is not None: filter_clauses = [] + IN, NIN = "in", "nin" for key, value in filter.items(): - IN = "in" - if isinstance(value, dict) and IN in map(str.lower, value): + if isinstance(value, dict): value_case_insensitive = { k.lower(): v for k, v in value.items() } - filter_by_metadata = self.EmbeddingStore.cmetadata[ - key - ].astext.in_(value_case_insensitive[IN]) - filter_clauses.append(filter_by_metadata) + if IN in map(str.lower, value): + filter_by_metadata = self.EmbeddingStore.cmetadata[ + key + ].astext.in_(value_case_insensitive[IN]) + elif NIN in map(str.lower, value): + filter_by_metadata = self.EmbeddingStore.cmetadata[ + key + ].astext.not_in(value_case_insensitive[NIN]) + else: + filter_by_metadata = None + if filter_by_metadata is not None: + filter_clauses.append(filter_by_metadata) else: filter_by_metadata = self.EmbeddingStore.cmetadata[ key diff --git a/libs/langchain/tests/integration_tests/vectorstores/test_pgvector.py b/libs/langchain/tests/integration_tests/vectorstores/test_pgvector.py index 6fe9fc6cb7a72..e799b427121c7 100644 --- a/libs/langchain/tests/integration_tests/vectorstores/test_pgvector.py +++ b/libs/langchain/tests/integration_tests/vectorstores/test_pgvector.py @@ -17,7 +17,6 @@ password=os.environ.get("TEST_PGVECTOR_PASSWORD", "postgres"), ) - ADA_TOKEN_COUNT = 1536 @@ -186,6 +185,27 @@ def test_pgvector_with_filter_in_set() -> None: ] +def test_pgvector_with_filter_nin_set() -> None: + """Test end to end construction and search.""" + texts = ["foo", "bar", "baz"] + metadatas = [{"page": str(i)} for i in range(len(texts))] + docsearch = PGVector.from_texts( + texts=texts, + collection_name="test_collection_filter", + embedding=FakeEmbeddingsWithAdaDimension(), + metadatas=metadatas, + connection_string=CONNECTION_STRING, + pre_delete_collection=True, + ) + output = docsearch.similarity_search_with_score( + "foo", k=2, filter={"page": {"NIN": ["1"]}} + ) + assert output == [ + (Document(page_content="foo", metadata={"page": "0"}), 0.0), + (Document(page_content="baz", metadata={"page": "2"}), 0.0013003906671379406), + ] + + def test_pgvector_delete_docs() -> None: """Add and delete documents.""" texts = ["foo", "bar", "baz"] From ab6b41937abb61fe14c4d34f5a61e696e69ca264 Mon Sep 17 00:00:00 2001 From: kavinraj A S <68454569+kavinask007@users.noreply.github.com> Date: Wed, 6 Dec 2023 08:46:18 +0530 Subject: [PATCH 14/20] Fixed a typo in smart_llm prompt (#13052) --- libs/experimental/langchain_experimental/smart_llm/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/experimental/langchain_experimental/smart_llm/base.py b/libs/experimental/langchain_experimental/smart_llm/base.py index 792b910b28bc1..8301c5df5345c 100644 --- a/libs/experimental/langchain_experimental/smart_llm/base.py +++ b/libs/experimental/langchain_experimental/smart_llm/base.py @@ -226,7 +226,7 @@ def get_prompt_strings( (AIMessagePromptTemplate, "Critique: {critique}"), ( HumanMessagePromptTemplate, - "You are a resolved tasked with 1) finding which of " + "You are a resolver tasked with 1) finding which of " f"the {self.n_ideas} answer options the researcher thought was " "best,2) improving that answer and 3) printing the answer in full. " "Don't output anything for step 1 or 2, only the full answer in 3. " From 64d5108f99f0d3630cbeb04792e3fd92004278af Mon Sep 17 00:00:00 2001 From: balaba-max Date: Wed, 6 Dec 2023 06:41:36 +0300 Subject: [PATCH 15/20] Feature: GitLab url from ENV (#14221) --------- Co-authored-by: Erick Friis --- docs/docs/integrations/toolkits/gitlab.ipynb | 2 ++ libs/langchain/langchain/utilities/gitlab.py | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/docs/integrations/toolkits/gitlab.ipynb b/docs/docs/integrations/toolkits/gitlab.ipynb index 3edb840ef2ad2..83b40ffdd6ac8 100644 --- a/docs/docs/integrations/toolkits/gitlab.ipynb +++ b/docs/docs/integrations/toolkits/gitlab.ipynb @@ -77,6 +77,7 @@ "\n", "Before initializing your agent, the following environmental variables need to be set:\n", "\n", + "* **GITLAB_URL** - The URL hosted Gitlab. Defaults to \"https://gitlab.com\". \n", "* **GITLAB_PERSONAL_ACCESS_TOKEN**- The personal access token you created in the last step\n", "* **GITLAB_REPOSITORY**- The name of the Gitlab repository you want your bot to act upon. Must follow the format {username}/{repo-name}.\n", "* **GITLAB_BRANCH**- The branch where the bot will make its commits. Defaults to 'main.'\n", @@ -111,6 +112,7 @@ "outputs": [], "source": [ "# Set your environment variables using os.environ\n", + "os.environ[\"GITLAB_URL\"] = \"https://gitlab.example.org\"\n", "os.environ[\"GITLAB_PERSONAL_ACCESS_TOKEN\"] = \"\"\n", "os.environ[\"GITLAB_REPOSITORY\"] = \"username/repo-name\"\n", "os.environ[\"GITLAB_BRANCH\"] = \"bot-branch-name\"\n", diff --git a/libs/langchain/langchain/utilities/gitlab.py b/libs/langchain/langchain/utilities/gitlab.py index 78765637c716d..c36f8479e952e 100644 --- a/libs/langchain/langchain/utilities/gitlab.py +++ b/libs/langchain/langchain/utilities/gitlab.py @@ -38,6 +38,10 @@ class Config: @root_validator() def validate_environment(cls, values: Dict) -> Dict: """Validate that api key and python package exists in environment.""" + + gitlab_url = get_from_dict_or_env( + values, "gitlab_url", "GITLAB_URL", default=None + ) gitlab_repository = get_from_dict_or_env( values, "gitlab_repository", "GITLAB_REPOSITORY" ) @@ -62,7 +66,11 @@ def validate_environment(cls, values: Dict) -> Dict: "Please install it with `pip install python-gitlab`" ) - g = gitlab.Gitlab(private_token=gitlab_personal_access_token) + g = gitlab.Gitlab( + url=gitlab_url, + private_token=gitlab_personal_access_token, + keep_base_url=True, + ) g.auth() From 8f403ea2d76ae463f9971f097edeb4ac5aa2d8f2 Mon Sep 17 00:00:00 2001 From: dudub12 <70527423+dudub12@users.noreply.github.com> Date: Wed, 6 Dec 2023 06:07:38 +0200 Subject: [PATCH 16/20] info sql tool remove whitespaces in table names (#13712) Remove whitespaces from the input of the ListSQLDatabaseTool for better support. for example, the input "table1,table2,table3" will throw an exception whiteout the change although it's a valid input. --------- Co-authored-by: Harrison Chase --- libs/langchain/langchain/tools/sql_database/tool.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/langchain/langchain/tools/sql_database/tool.py b/libs/langchain/langchain/tools/sql_database/tool.py index c90186cfc6441..97d45977e7f0b 100644 --- a/libs/langchain/langchain/tools/sql_database/tool.py +++ b/libs/langchain/langchain/tools/sql_database/tool.py @@ -60,7 +60,9 @@ def _run( run_manager: Optional[CallbackManagerForToolRun] = None, ) -> str: """Get the schema for tables in a comma-separated list.""" - return self.db.get_table_info_no_throw(table_names.split(", ")) + return self.db.get_table_info_no_throw( + [t.strip() for t in table_names.split(",")] + ) class ListSQLDatabaseTool(BaseSQLDatabaseTool, BaseTool): From 9e5d14640942a6b9c22d0f54a38aec74caac1d8d Mon Sep 17 00:00:00 2001 From: mogith-pn <143642606+mogith-pn@users.noreply.github.com> Date: Wed, 6 Dec 2023 09:38:00 +0530 Subject: [PATCH 17/20] Updated integration with Clarifai python SDK functions (#13671) Description : Updated the functions with new Clarifai python SDK. Enabled initialisation of Clarifai class with model URL. Updated docs with new functions examples. --- docs/docs/integrations/llms/clarifai.ipynb | 158 ++++++++++++-- .../text_embedding/clarifai.ipynb | 52 +++-- .../integrations/vectorstores/clarifai.ipynb | 203 +++++++++++------ .../langchain/embeddings/clarifai.py | 180 +++++++--------- libs/langchain/langchain/llms/clarifai.py | 182 +++++++--------- .../langchain/vectorstores/clarifai.py | 204 +++++------------- 6 files changed, 519 insertions(+), 460 deletions(-) diff --git a/docs/docs/integrations/llms/clarifai.ipynb b/docs/docs/integrations/llms/clarifai.ipynb index 02569a560170b..bac65941b3918 100644 --- a/docs/docs/integrations/llms/clarifai.ipynb +++ b/docs/docs/integrations/llms/clarifai.ipynb @@ -38,6 +38,19 @@ "!pip install clarifai" ] }, + { + "cell_type": "code", + "execution_count": 2, + "id": "326395b1-27e0-4cb6-9321-27c04466362b", + "metadata": {}, + "outputs": [], + "source": [ + "# Declare clarifai pat token as environment variable or you can pass it as argument in clarifai class.\n", + "import os\n", + "\n", + "os.environ[\"CLARIFAI_PAT\"] = \"CLARIFAI_PAT_TOKEN\"" + ] + }, { "attachments": {}, "cell_type": "markdown", @@ -50,20 +63,12 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "id": "3f5dc9d7-65e3-4b5b-9086-3327d016cfe0", "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stdin", - "output_type": "stream", - "text": [ - " ········\n" - ] - } - ], + "outputs": [], "source": [ "# Please login and get your API key from https://clarifai.com/settings/security\n", "from getpass import getpass\n", @@ -73,7 +78,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "6fb585dd", "metadata": { "tags": [] @@ -98,7 +103,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "035dea0f", "metadata": { "tags": [] @@ -121,7 +126,11 @@ "# Setup\n", "Setup the user id and app id where the model resides. You can find a list of public models on https://clarifai.com/explore/models\n", "\n", - "You will have to also initialize the model id and if needed, the model version id. Some models have many versions, you can choose the one appropriate for your task." + "You will have to also initialize the model id and if needed, the model version id. Some models have many versions, you can choose the one appropriate for your task.\n", + "\n", + " or\n", + " \n", + "You can use the model_url (for ex: \"https://clarifai.com/anthropic/completion/models/claude-v2\") for intialization." ] }, { @@ -136,7 +145,10 @@ "MODEL_ID = \"GPT-3_5-turbo\"\n", "\n", "# You can provide a specific model version as the model_version_id arg.\n", - "# MODEL_VERSION_ID = \"MODEL_VERSION_ID\"" + "# MODEL_VERSION_ID = \"MODEL_VERSION_ID\"\n", + "# or\n", + "\n", + "MODEL_URL = \"https://clarifai.com/openai/chat-completion/models/GPT-4\"" ] }, { @@ -149,14 +161,15 @@ "outputs": [], "source": [ "# Initialize a Clarifai LLM\n", - "clarifai_llm = Clarifai(\n", - " pat=CLARIFAI_PAT, user_id=USER_ID, app_id=APP_ID, model_id=MODEL_ID\n", - ")" + "clarifai_llm = Clarifai(user_id=USER_ID, app_id=APP_ID, model_id=MODEL_ID)\n", + "# or\n", + "# Initialize through Model URL\n", + "clarifai_llm = Clarifai(model_url=MODEL_URL)" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "id": "a641dbd9", "metadata": { "tags": [] @@ -178,17 +191,17 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "id": "9f844993", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'Justin Bieber was born on March 1, 1994. So, we need to figure out the Super Bowl winner for the 1994 season. The NFL season spans two calendar years, so the Super Bowl for the 1994 season would have taken place in early 1995. \\n\\nThe Super Bowl in question is Super Bowl XXIX, which was played on January 29, 1995. The game was won by the San Francisco 49ers, who defeated the San Diego Chargers by a score of 49-26. Therefore, the San Francisco 49ers won the Super Bowl in the year Justin Bieber was born.'" + "' Okay, here are the steps to figure this out:\\n\\n1. Justin Bieber was born on March 1, 1994.\\n\\n2. The Super Bowl that took place in the year of his birth was Super Bowl XXVIII. \\n\\n3. Super Bowl XXVIII was played on January 30, 1994.\\n\\n4. The two teams that played in Super Bowl XXVIII were the Dallas Cowboys and the Buffalo Bills. \\n\\n5. The Dallas Cowboys defeated the Buffalo Bills 30-13 to win Super Bowl XXVIII.\\n\\nTherefore, the NFL team that won the Super Bowl in the year Justin Bieber was born was the Dallas Cowboys.'" ] }, - "execution_count": 7, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -198,6 +211,107 @@ "\n", "llm_chain.run(question)" ] + }, + { + "cell_type": "markdown", + "id": "2604c17f-a410-4159-ac1c-fdd7c60b725c", + "metadata": {}, + "source": [ + "## Model Predict with Inference parameters for GPT.\n", + "Alternatively you can use GPT models with inference parameters (like temperature, max_tokens etc)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "59ae8cd9-3d07-40f2-b4b5-b7908b2fc71d", + "metadata": {}, + "outputs": [], + "source": [ + "# Intialize the parameters as dict.\n", + "params = dict(temperature=str(0.3), max_tokens=100)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "c14c7940-4fd3-41a4-9ed4-f4ecead08edf", + "metadata": {}, + "outputs": [], + "source": [ + "clarifai_llm = Clarifai(user_id=USER_ID, app_id=APP_ID, model_id=MODEL_ID)\n", + "llm_chain = LLMChain(\n", + " prompt=prompt, llm=clarifai_llm, llm_kwargs={\"inference_params\": params}\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "80fef923-0473-4119-aa7e-868374560fdd", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Step 1: The first digit can be any even number from 1 to 9, except for 5. So there are 4 choices for the first digit.\\n\\nStep 2: If the first digit is not 5, then the second digit must be 7. So there is only 1 choice for the second digit.\\n\\nStep 3: The third digit can be any even number from 0 to 9, except for 5 and 7. So there are '" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "question = \"How many 3 digit even numbers you can form that if one of the digits is 5 then the following digit must be 7?\"\n", + "\n", + "llm_chain.run(question)" + ] + }, + { + "cell_type": "markdown", + "id": "c9ab08e2-4bc9-49b8-9b7a-88ae840ac3f8", + "metadata": {}, + "source": [ + "Generate responses for list of prompts" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "fb3b8cea-5cc9-46f3-9334-1994f195cde3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "LLMResult(generations=[[Generation(text=' Here is a 5 sentence summary of the key events of the American Revolution:\\n\\nThe American Revolution began with growing tensions between American colonists and the British government over issues of taxation without representation. In 1775, fighting broke out between British troops and American militiamen in Lexington and Concord, starting the Revolutionary War. The Continental Congress appointed George Washington as commander of the Continental Army, which went on to win key victories over the British. In 1776, the Declaration of Independence was adopted, formally declaring the 13 American colonies free from British rule. After years of fighting, the Revolutionary War ended with the British defeat at Yorktown in 1781 and recognition of American independence.')], [Generation(text=\" Here's a humorous take on explaining rocket science:\\n\\nRocket science is so easy, it's practically child's play! Just strap a big metal tube full of explosive liquid to your butt and light the fuse. What could go wrong? Blastoff! Whoosh, you'll be zooming to the moon in no time. Just remember your helmet or your head might go pop like a zit when you leave the atmosphere. \\n\\nMaking rockets is a cinch too. Simply mix together some spicy spices, garlic powder, chili powder, a dash of gunpowder and voila - rocket fuel! Add a pinch of baking soda and vinegar if you want an extra kick. Shake well and pour into your DIY soda bottle rocket. Stand back and watch that baby soar!\\n\\nGuiding a rocket is fun for the whole family. Just strap in, push some random buttons and see where you end up. It's like the ultimate surprise vacation! You never know if you'll wind up on Venus, crash land on Mars, or take a quick dip through the rings of Saturn. \\n\\nAnd if anything goes wrong, don't sweat it. Rocket science is easy breezy. Just troubleshoot on the fly with some duct tape and crazy glue and you'll be back on course in a jiffy. Who needs mission control when you've got this!\")], [Generation(text=\" Here is a draft welcome speech for a college sports day:\\n\\nGood morning everyone and welcome to our college's annual sports day! It's wonderful to see so many students, faculty, staff, alumni, and guests gathered here today to celebrate sportsmanship and athletic achievement at our college. \\n\\nLet's begin by thanking all the organizers, volunteers, coaches, and staff members who worked tirelessly behind the scenes to make this event possible. Our sports day would not happen without your dedication and commitment. \\n\\nI also want to recognize all the student-athletes with us today. You inspire us with your talent, spirit, and determination. Sports have a unique power to unite and energize our community. Through both individual and team sports, you demonstrate focus, collaboration, perseverance and resilience – qualities that will serve you well both on and off the field.\\n\\nThe spirit of competition and fair play are core values of any sports event. I encourage all of you to compete enthusiastically today. Play to the best of your ability and have fun. Applaud the effort and sportsmanship of your fellow athletes, regardless of the outcome. \\n\\nWin or lose, this sports day is a day for us to build camaraderie and create lifelong memories. Let's make it a day of fitness and friendship for all. With that, let the games begin. Enjoy the day!\")]], llm_output=None, run=None)" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# We can use _generate to generate the response for list of prompts.\n", + "clarifai_llm._generate(\n", + " [\n", + " \"Help me summarize the events of american revolution in 5 sentences\",\n", + " \"Explain about rocket science in a funny way\",\n", + " \"Create a script for welcome speech for the college sports day\",\n", + " ],\n", + " inference_params=params,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb080d00-7b5c-4726-ae4f-df9374997c7f", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -216,7 +330,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.16" + "version": "3.9.10" } }, "nbformat": 4, diff --git a/docs/docs/integrations/text_embedding/clarifai.ipynb b/docs/docs/integrations/text_embedding/clarifai.ipynb index 33f9ac0abcaa1..335f597ddac0f 100644 --- a/docs/docs/integrations/text_embedding/clarifai.ipynb +++ b/docs/docs/integrations/text_embedding/clarifai.ipynb @@ -57,7 +57,7 @@ }, "outputs": [ { - "name": "stdin", + "name": "stdout", "output_type": "stream", "text": [ " ········\n" @@ -81,6 +81,7 @@ "outputs": [], "source": [ "# Import the required modules\n", + "from langchain.chains import LLMChain\n", "from langchain.embeddings import ClarifaiEmbeddings\n", "from langchain.prompts import PromptTemplate" ] @@ -125,16 +126,17 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "id": "1fe9bf15", "metadata": {}, "outputs": [], "source": [ - "USER_ID = \"salesforce\"\n", - "APP_ID = \"blip\"\n", - "MODEL_ID = \"multimodal-embedder-blip-2\"\n", + "USER_ID = \"clarifai\"\n", + "APP_ID = \"main\"\n", + "MODEL_ID = \"BAAI-bge-base-en-v15\"\n", + "MODEL_URL = \"https://clarifai.com/clarifai/main/models/BAAI-bge-base-en-v15\"\n", "\n", - "# You can provide a specific model version as the model_version_id arg.\n", + "# Further you can also provide a specific model version as the model_version_id arg.\n", "# MODEL_VERSION_ID = \"MODEL_VERSION_ID\"" ] }, @@ -148,26 +150,38 @@ "outputs": [], "source": [ "# Initialize a Clarifai embedding model\n", - "embeddings = ClarifaiEmbeddings(\n", - " pat=CLARIFAI_PAT, user_id=USER_ID, app_id=APP_ID, model_id=MODEL_ID\n", - ")" + "embeddings = ClarifaiEmbeddings(user_id=USER_ID, app_id=APP_ID, model_id=MODEL_ID)\n", + "\n", + "# Initialize a clarifai embedding model using model URL\n", + "embeddings = ClarifaiEmbeddings(model_url=MODEL_URL)\n", + "\n", + "# Alternatively you can initialize clarifai class with pat argument." ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 5, "id": "a641dbd9", "metadata": { "tags": [] }, "outputs": [], "source": [ - "text = \"This is a test document.\"" + "text = \"roses are red violets are blue.\"\n", + "text2 = \"Make hay while the sun shines.\"" + ] + }, + { + "cell_type": "markdown", + "id": "14544fbb-76df-43c9-b5ec-88941ff12889", + "metadata": {}, + "source": [ + "You can embed single line of your text using embed_query function !" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "id": "32b4d5f4-2b8e-4681-856f-19a3dd141ae4", "metadata": {}, "outputs": [], @@ -175,14 +189,22 @@ "query_result = embeddings.embed_query(text)" ] }, + { + "cell_type": "markdown", + "id": "ab9140c7-19c7-48fd-9a28-0c2351e5d2c5", + "metadata": {}, + "source": [ + "Further to embed list of texts/documents use embed_documents function." + ] + }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 9, "id": "47076457-1880-48ac-970f-872ead6f0d94", "metadata": {}, "outputs": [], "source": [ - "doc_result = embeddings.embed_documents([text])" + "doc_result = embeddings.embed_documents([text, text2])" ] } ], @@ -202,7 +224,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.16" + "version": "3.9.10" } }, "nbformat": 4, diff --git a/docs/docs/integrations/vectorstores/clarifai.ipynb b/docs/docs/integrations/vectorstores/clarifai.ipynb index 454872293ffae..f6f7b7ec9ac29 100644 --- a/docs/docs/integrations/vectorstores/clarifai.ipynb +++ b/docs/docs/integrations/vectorstores/clarifai.ipynb @@ -71,7 +71,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 3, "id": "aac9563e", "metadata": { "tags": [] @@ -98,14 +98,14 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 24, "id": "4d853395", "metadata": {}, "outputs": [], "source": [ "USER_ID = \"USERNAME_ID\"\n", "APP_ID = \"APPLICATION_ID\"\n", - "NUMBER_OF_DOCS = 4" + "NUMBER_OF_DOCS = 2" ] }, { @@ -120,7 +120,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 16, "id": "1d828f77", "metadata": {}, "outputs": [], @@ -139,49 +139,130 @@ "]" ] }, + { + "cell_type": "markdown", + "id": "8e467c0b-e218-4cb2-a02e-2948670bbab7", + "metadata": {}, + "source": [ + "Alternatively you have an option to give custom input ids to the inputs." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "ffab62d6-8ef4-4c5e-b45e-6f1b39d0c013", + "metadata": {}, + "outputs": [], + "source": [ + "idlist = [\"text1\", \"text2\", \"text3\", \"text4\", \"text5\"]\n", + "metadatas = [\n", + " {\"id\": idlist[i], \"text\": text, \"source\": \"book 1\", \"category\": [\"books\", \"modern\"]}\n", + " for i, text in enumerate(texts)\n", + "]" + ] + }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 27, "id": "738bff27", "metadata": {}, "outputs": [], + "source": [ + "# There is an option to initialize clarifai vector store with pat as argument!\n", + "clarifai_vector_db = Clarifai(\n", + " user_id=USER_ID,\n", + " app_id=APP_ID,\n", + " number_of_docs=NUMBER_OF_DOCS,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "d3ca631f-8182-461f-b581-b649f7176a5f", + "metadata": {}, + "source": [ + "Upload data into clarifai app." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "77e4f544-e766-4e0e-934a-4e85f68a0286", + "metadata": {}, + "outputs": [], + "source": [ + "# upload with metadata and custom input ids.\n", + "response = clarifai_vector_db.add_texts(texts=texts, ids=idlist, metadatas=metadatas)\n", + "\n", + "# upload without metadata (Not recommended)- Since you will not be able to perform Search operation with respect to metadata.\n", + "# custom input_id (optional)\n", + "response = clarifai_vector_db.add_texts(texts=texts)" + ] + }, + { + "cell_type": "markdown", + "id": "09d97edf-014b-4a5b-86a9-6a5b255554ba", + "metadata": {}, + "source": [ + "You can create a clarifai vector DB store and ingest all the inputs into your app directly by," + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7c466ac9-6b50-48ff-8b23-9fc6a3cbdf97", + "metadata": {}, + "outputs": [], "source": [ "clarifai_vector_db = Clarifai.from_texts(\n", " user_id=USER_ID,\n", " app_id=APP_ID,\n", " texts=texts,\n", - " pat=CLARIFAI_PAT,\n", - " number_of_docs=NUMBER_OF_DOCS,\n", " metadatas=metadatas,\n", ")" ] }, + { + "cell_type": "markdown", + "id": "0bb2affb-48ca-410b-85c0-9e1275429bcb", + "metadata": {}, + "source": [ + "Search similar texts using similarity search function." + ] + }, { "cell_type": "code", - "execution_count": null, + "execution_count": 31, "id": "e755cdce", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[Document(page_content='I really enjoy spending time with you', metadata={'text': 'I really enjoy spending time with you', 'id': 0.0, 'source': 'book 1', 'category': ['books', 'modern']}),\n", - " Document(page_content='I went to the movies yesterday', metadata={'text': 'I went to the movies yesterday', 'id': 3.0, 'source': 'book 1', 'category': ['books', 'modern']})]" + "[Document(page_content='I really enjoy spending time with you', metadata={'text': 'I really enjoy spending time with you', 'id': 'text1', 'source': 'book 1', 'category': ['books', 'modern']})]" ] }, - "execution_count": null, + "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "docs = clarifai_vector_db.similarity_search(\"I would love to see you\")\n", + "docs = clarifai_vector_db.similarity_search(\"I would like to see you\")\n", "docs" ] }, + { + "cell_type": "markdown", + "id": "bd703470-7efb-4be5-a556-eea896ca60f4", + "metadata": {}, + "source": [ + "Further you can filter your search results by metadata." + ] + }, { "cell_type": "code", - "execution_count": null, + "execution_count": 29, "id": "140103ec-0936-454a-9f4a-7d5beefc138f", "metadata": {}, "outputs": [], @@ -210,41 +291,17 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "a3c3999a", "metadata": {}, "outputs": [], "source": [ - "loader = TextLoader(\"../../modules/state_of_the_union.txt\")\n", + "loader = TextLoader(\"your_local_file_path.txt\")\n", "documents = loader.load()\n", "text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)\n", "docs = text_splitter.split_documents(documents)" ] }, - { - "cell_type": "code", - "execution_count": 9, - "id": "69ae7e35", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[Document(page_content='Madam Speaker, Madam Vice President, our First Lady and Second Gentleman. Members of Congress and the Cabinet. Justices of the Supreme Court. My fellow Americans. \\n\\nLast year COVID-19 kept us apart. This year we are finally together again. \\n\\nTonight, we meet as Democrats Republicans and Independents. But most importantly as Americans. \\n\\nWith a duty to one another to the American people to the Constitution. \\n\\nAnd with an unwavering resolve that freedom will always triumph over tyranny. \\n\\nSix days ago, Russia’s Vladimir Putin sought to shake the foundations of the free world thinking he could make it bend to his menacing ways. But he badly miscalculated. \\n\\nHe thought he could roll into Ukraine and the world would roll over. Instead he met a wall of strength he never imagined. \\n\\nHe met the Ukrainian people. \\n\\nFrom President Zelenskyy to every Ukrainian, their fearlessness, their courage, their determination, inspires the world.', metadata={'source': '../../../state_of_the_union.txt'}),\n", - " Document(page_content='Groups of citizens blocking tanks with their bodies. Everyone from students to retirees teachers turned soldiers defending their homeland. \\n\\nIn this struggle as President Zelenskyy said in his speech to the European Parliament “Light will win over darkness.” The Ukrainian Ambassador to the United States is here tonight. \\n\\nLet each of us here tonight in this Chamber send an unmistakable signal to Ukraine and to the world. \\n\\nPlease rise if you are able and show that, Yes, we the United States of America stand with the Ukrainian people. \\n\\nThroughout our history we’ve learned this lesson when dictators do not pay a price for their aggression they cause more chaos. \\n\\nThey keep moving. \\n\\nAnd the costs and the threats to America and the world keep rising. \\n\\nThat’s why the NATO Alliance was created to secure peace and stability in Europe after World War 2. \\n\\nThe United States is a member along with 29 other nations. \\n\\nIt matters. American diplomacy matters. American resolve matters.', metadata={'source': '../../../state_of_the_union.txt'}),\n", - " Document(page_content='Putin’s latest attack on Ukraine was premeditated and unprovoked. \\n\\nHe rejected repeated efforts at diplomacy. \\n\\nHe thought the West and NATO wouldn’t respond. And he thought he could divide us at home. Putin was wrong. We were ready. Here is what we did. \\n\\nWe prepared extensively and carefully. \\n\\nWe spent months building a coalition of other freedom-loving nations from Europe and the Americas to Asia and Africa to confront Putin. \\n\\nI spent countless hours unifying our European allies. We shared with the world in advance what we knew Putin was planning and precisely how he would try to falsely justify his aggression. \\n\\nWe countered Russia’s lies with truth. \\n\\nAnd now that he has acted the free world is holding him accountable. \\n\\nAlong with twenty-seven members of the European Union including France, Germany, Italy, as well as countries like the United Kingdom, Canada, Japan, Korea, Australia, New Zealand, and many others, even Switzerland.', metadata={'source': '../../../state_of_the_union.txt'}),\n", - " Document(page_content='We are inflicting pain on Russia and supporting the people of Ukraine. Putin is now isolated from the world more than ever. \\n\\nTogether with our allies –we are right now enforcing powerful economic sanctions. \\n\\nWe are cutting off Russia’s largest banks from the international financial system. \\n\\nPreventing Russia’s central bank from defending the Russian Ruble making Putin’s $630 Billion “war fund” worthless. \\n\\nWe are choking off Russia’s access to technology that will sap its economic strength and weaken its military for years to come. \\n\\nTonight I say to the Russian oligarchs and corrupt leaders who have bilked billions of dollars off this violent regime no more. \\n\\nThe U.S. Department of Justice is assembling a dedicated task force to go after the crimes of Russian oligarchs. \\n\\nWe are joining with our European allies to find and seize your yachts your luxury apartments your private jets. We are coming for your ill-begotten gains.', metadata={'source': '../../../state_of_the_union.txt'})]" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "docs[:4]" - ] - }, { "cell_type": "code", "execution_count": 10, @@ -257,9 +314,17 @@ "NUMBER_OF_DOCS = 4" ] }, + { + "cell_type": "markdown", + "id": "52d86f01-3462-440e-8960-3c0c17b98f09", + "metadata": {}, + "source": [ + "Create a clarifai vector DB class and ingest all your documents into clarifai App." + ] + }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "id": "6e104aee", "metadata": {}, "outputs": [], @@ -268,33 +333,18 @@ " user_id=USER_ID,\n", " app_id=APP_ID,\n", " documents=docs,\n", - " pat=CLARIFAI_PAT,\n", " number_of_docs=NUMBER_OF_DOCS,\n", ")" ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "9c608226", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[Document(page_content='And I will keep doing everything in my power to crack down on gun trafficking and ghost guns you can buy online and make at home—they have no serial numbers and can’t be traced. \\n\\nAnd I ask Congress to pass proven measures to reduce gun violence. Pass universal background checks. Why should anyone on a terrorist list be able to purchase a weapon? \\n\\nBan assault weapons and high-capacity magazines. \\n\\nRepeal the liability shield that makes gun manufacturers the only industry in America that can’t be sued. \\n\\nThese laws don’t infringe on the Second Amendment. They save lives. \\n\\nThe most fundamental right in America is the right to vote – and to have it counted. And it’s under assault. \\n\\nIn state after state, new laws have been passed, not only to suppress the vote, but to subvert entire elections. \\n\\nWe cannot let this happen.', metadata={'source': '../../../state_of_the_union.txt'}),\n", - " Document(page_content='We can’t change how divided we’ve been. But we can change how we move forward—on COVID-19 and other issues we must face together. \\n\\nI recently visited the New York City Police Department days after the funerals of Officer Wilbert Mora and his partner, Officer Jason Rivera. \\n\\nThey were responding to a 9-1-1 call when a man shot and killed them with a stolen gun. \\n\\nOfficer Mora was 27 years old. \\n\\nOfficer Rivera was 22. \\n\\nBoth Dominican Americans who’d grown up on the same streets they later chose to patrol as police officers. \\n\\nI spoke with their families and told them that we are forever in debt for their sacrifice, and we will carry on their mission to restore the trust and safety every community deserves. \\n\\nI’ve worked on these issues a long time. \\n\\nI know what works: Investing in crime prevention and community police officers who’ll walk the beat, who’ll know the neighborhood, and who can restore trust and safety.', metadata={'source': '../../../state_of_the_union.txt'}),\n", - " Document(page_content='A former top litigator in private practice. A former federal public defender. And from a family of public school educators and police officers. A consensus builder. Since she’s been nominated, she’s received a broad range of support—from the Fraternal Order of Police to former judges appointed by Democrats and Republicans. \\n\\nAnd if we are to advance liberty and justice, we need to secure the Border and fix the immigration system. \\n\\nWe can do both. At our border, we’ve installed new technology like cutting-edge scanners to better detect drug smuggling. \\n\\nWe’ve set up joint patrols with Mexico and Guatemala to catch more human traffickers. \\n\\nWe’re putting in place dedicated immigration judges so families fleeing persecution and violence can have their cases heard faster. \\n\\nWe’re securing commitments and supporting partners in South and Central America to host more refugees and secure their own borders.', metadata={'source': '../../../state_of_the_union.txt'}),\n", - " Document(page_content='So let’s not abandon our streets. Or choose between safety and equal justice. \\n\\nLet’s come together to protect our communities, restore trust, and hold law enforcement accountable. \\n\\nThat’s why the Justice Department required body cameras, banned chokeholds, and restricted no-knock warrants for its officers. \\n\\nThat’s why the American Rescue Plan provided $350 Billion that cities, states, and counties can use to hire more police and invest in proven strategies like community violence interruption—trusted messengers breaking the cycle of violence and trauma and giving young people hope. \\n\\nWe should all agree: The answer is not to Defund the police. The answer is to FUND the police with the resources and training they need to protect our communities. \\n\\nI ask Democrats and Republicans alike: Pass my budget and keep our neighborhoods safe.', metadata={'source': '../../../state_of_the_union.txt'})]" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "docs = clarifai_vector_db.similarity_search(\"Texts related to criminals and violence\")\n", + "docs = clarifai_vector_db.similarity_search(\"Texts related to population\")\n", "docs" ] }, @@ -330,7 +380,6 @@ "clarifai_vector_db = Clarifai(\n", " user_id=USER_ID,\n", " app_id=APP_ID,\n", - " pat=CLARIFAI_PAT,\n", " number_of_docs=NUMBER_OF_DOCS,\n", ")" ] @@ -342,9 +391,39 @@ "metadata": {}, "outputs": [], "source": [ - "docs = clarifai_vector_db.similarity_search(\"Texts related to criminals and violence\")\n", - "docs" + "docs = clarifai_vector_db.similarity_search(\n", + " \"Texts related to ammuniction and president wilson\"\n", + ")" ] + }, + { + "cell_type": "code", + "execution_count": 51, + "id": "55ee5fc7-94c4-45d0-84ca-00defeca871e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"President Wilson, generally acclaimed as the leader of the world's democracies,\\nphrased for civilization the arguments against autocracy in the great peace conference\\nafter the war. The President headed the American delegation to that conclave of world\\nre-construction. With him as delegates to the conference were Robert Lansing, Secretary\\nof State; Henry White, former Ambassador to France and Italy; Edward M. House and\\nGeneral Tasker H. Bliss.\\nRepresenting American Labor at the International Labor conference held in Paris\\nsimultaneously with the Peace Conference were Samuel Gompers, president of the\\nAmerican Federation of Labor; William Green, secretary-treasurer of the United Mine\\nWorkers of America; John R. Alpine, president of the Plumbers' Union; James Duncan,\\npresident of the International Association of Granite Cutters; Frank Duffy, president of\\nthe United Brotherhood of Carpenters and Joiners, and Frank Morrison, secretary of the\\nAmerican Federation of Labor.\\nEstimating the share of each Allied nation in the great victory, mankind will\\nconclude that the heaviest cost in proportion to prewar population and treasure was paid\\nby the nations that first felt the shock of war, Belgium, Serbia, Poland and France. All\\nfour were the battle-grounds of huge armies, oscillating in a bloody frenzy over once\\nfertile fields and once prosperous towns.\\nBelgium, with a population of 8,000,000, had a casualty list of more than 350,000;\\nFrance, with its casualties of 4,000,000 out of a population (including its colonies) of\\n90,000,000, is really the martyr nation of the world. Her gallant poilus showed the world\\nhow cheerfully men may die in defense of home and liberty. Huge Russia, including\\nhapless Poland, had a casualty list of 7,000,000 out of its entire population of\\n180,000,000. The United States out of a population of 110,000,000 had a casualty list of\\n236,117 for nineteen months of war; of these 53,169 were killed or died of disease;\\n179,625 were wounded; and 3,323 prisoners or missing.\"" + ] + }, + "execution_count": 51, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "docs[0].page_content" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aa7b3260-2ee3-4619-836f-da64370a855c", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -363,7 +442,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.16" + "version": "3.9.10" } }, "nbformat": 4, diff --git a/libs/langchain/langchain/embeddings/clarifai.py b/libs/langchain/langchain/embeddings/clarifai.py index 2f54bf5138fe9..e39c2bbc4b041 100644 --- a/libs/langchain/langchain/embeddings/clarifai.py +++ b/libs/langchain/langchain/embeddings/clarifai.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Dict, List, Optional +from typing import Dict, List, Optional from langchain_core.embeddings import Embeddings from langchain_core.pydantic_v1 import BaseModel, Extra, root_validator @@ -20,15 +20,15 @@ class ClarifaiEmbeddings(BaseModel, Embeddings): .. code-block:: python from langchain.embeddings import ClarifaiEmbeddings - clarifai = ClarifaiEmbeddings( - model="embed-english-light-v3.0", clarifai_api_key="my-api-key" - ) + clarifai = ClarifaiEmbeddings(user_id=USER_ID, + app_id=APP_ID, + model_id=MODEL_ID) + (or) + clarifai_llm = Clarifai(model_url=EXAMPLE_URL) """ - stub: Any #: :meta private: - """Clarifai stub.""" - userDataObject: Any - """Clarifai user data object.""" + model_url: Optional[str] = None + """Model url to use.""" model_id: Optional[str] = None """Model id to use.""" model_version_id: Optional[str] = None @@ -48,37 +48,24 @@ class Config: @root_validator() def validate_environment(cls, values: Dict) -> Dict: - """Validate that api key and python package exists in environment.""" + """Validate that we have all required info to access Clarifai + platform and python package exists in environment.""" + values["pat"] = get_from_dict_or_env(values, "pat", "CLARIFAI_PAT") user_id = values.get("user_id") app_id = values.get("app_id") model_id = values.get("model_id") + model_url = values.get("model_url") - if values["pat"] is None: - raise ValueError("Please provide a pat.") - if user_id is None: - raise ValueError("Please provide a user_id.") - if app_id is None: - raise ValueError("Please provide a app_id.") - if model_id is None: - raise ValueError("Please provide a model_id.") + if model_url is not None and model_id is not None: + raise ValueError("Please provide either model_url or model_id, not both.") - try: - from clarifai.client import create_stub - from clarifai.client.auth.helper import ClarifaiAuthHelper - except ImportError: - raise ImportError( - "Could not import clarifai python package. " - "Please install it with `pip install clarifai`." - ) - auth = ClarifaiAuthHelper( - user_id=user_id, - app_id=app_id, - pat=values["pat"], - base=values["api_base"], - ) - values["userDataObject"] = auth.get_user_app_id_proto() - values["stub"] = create_stub(auth) + if model_url is None and model_id is None: + raise ValueError("Please provide one of model_url or model_id.") + + if model_url is None and model_id is not None: + if user_id is None or app_id is None: + raise ValueError("Please provide a user_id and app_id.") return values @@ -91,57 +78,48 @@ def embed_documents(self, texts: List[str]) -> List[List[float]]: Returns: List of embeddings, one for each text. """ - try: - from clarifai_grpc.grpc.api import ( - resources_pb2, - service_pb2, - ) - from clarifai_grpc.grpc.api.status import status_code_pb2 + from clarifai.client.input import Inputs + from clarifai.client.model import Model except ImportError: raise ImportError( "Could not import clarifai python package. " "Please install it with `pip install clarifai`." ) + if self.pat is not None: + pat = self.pat + if self.model_url is not None: + _model_init = Model(url=self.model_url, pat=pat) + else: + _model_init = Model( + model_id=self.model_id, + user_id=self.user_id, + app_id=self.app_id, + pat=pat, + ) + input_obj = Inputs(pat=pat) batch_size = 32 embeddings = [] - for i in range(0, len(texts), batch_size): - batch = texts[i : i + batch_size] - - post_model_outputs_request = service_pb2.PostModelOutputsRequest( - user_app_id=self.userDataObject, - model_id=self.model_id, - version_id=self.model_version_id, - inputs=[ - resources_pb2.Input( - data=resources_pb2.Data(text=resources_pb2.Text(raw=t)) - ) - for t in batch - ], - ) - post_model_outputs_response = self.stub.PostModelOutputs( - post_model_outputs_request - ) - if post_model_outputs_response.status.code != status_code_pb2.SUCCESS: - logger.error(post_model_outputs_response.status) - first_output_failure = ( - post_model_outputs_response.outputs[0].status - if len(post_model_outputs_response.outputs) - else None - ) - raise Exception( - f"Post model outputs failed, status: " - f"{post_model_outputs_response.status}, first output failure: " - f"{first_output_failure}" - ) - embeddings.extend( - [ - list(o.data.embeddings[0].vector) - for o in post_model_outputs_response.outputs + try: + for i in range(0, len(texts), batch_size): + batch = texts[i : i + batch_size] + input_batch = [ + input_obj.get_text_input(input_id=str(id), raw_text=inp) + for id, inp in enumerate(batch) ] - ) + predict_response = _model_init.predict(input_batch) + embeddings.extend( + [ + list(output.data.embeddings[0].vector) + for output in predict_response.outputs + ] + ) + + except Exception as e: + logger.error(f"Predict failed, exception: {e}") + return embeddings def embed_query(self, text: str) -> List[float]: @@ -153,48 +131,34 @@ def embed_query(self, text: str) -> List[float]: Returns: Embeddings for the text. """ - try: - from clarifai_grpc.grpc.api import ( - resources_pb2, - service_pb2, - ) - from clarifai_grpc.grpc.api.status import status_code_pb2 + from clarifai.client.model import Model except ImportError: raise ImportError( "Could not import clarifai python package. " "Please install it with `pip install clarifai`." ) - - post_model_outputs_request = service_pb2.PostModelOutputsRequest( - user_app_id=self.userDataObject, - model_id=self.model_id, - version_id=self.model_version_id, - inputs=[ - resources_pb2.Input( - data=resources_pb2.Data(text=resources_pb2.Text(raw=text)) - ) - ], - ) - post_model_outputs_response = self.stub.PostModelOutputs( - post_model_outputs_request - ) - - if post_model_outputs_response.status.code != status_code_pb2.SUCCESS: - logger.error(post_model_outputs_response.status) - first_output_failure = ( - post_model_outputs_response.outputs[0].status - if len(post_model_outputs_response.outputs[0]) - else None + if self.pat is not None: + pat = self.pat + if self.model_url is not None: + _model_init = Model(url=self.model_url, pat=pat) + else: + _model_init = Model( + model_id=self.model_id, + user_id=self.user_id, + app_id=self.app_id, + pat=pat, ) - raise Exception( - f"Post model outputs failed, status: " - f"{post_model_outputs_response.status}, first output failure: " - f"{first_output_failure}" + + try: + predict_response = _model_init.predict_by_bytes( + bytes(text, "utf-8"), input_type="text" ) + embeddings = [ + list(op.data.embeddings[0].vector) for op in predict_response.outputs + ] + + except Exception as e: + logger.error(f"Predict failed, exception: {e}") - embeddings = [ - list(o.data.embeddings[0].vector) - for o in post_model_outputs_response.outputs - ] return embeddings[0] diff --git a/libs/langchain/langchain/llms/clarifai.py b/libs/langchain/langchain/llms/clarifai.py index 40fe0c536fcd9..7d56a5de7f67c 100644 --- a/libs/langchain/langchain/llms/clarifai.py +++ b/libs/langchain/langchain/llms/clarifai.py @@ -12,6 +12,9 @@ logger = logging.getLogger(__name__) +EXAMPLE_URL = "https://clarifai.com/openai/chat-completion/models/GPT-4" + + class Clarifai(LLM): """Clarifai large language models. @@ -24,27 +27,23 @@ class Clarifai(LLM): .. code-block:: python from langchain.llms import Clarifai - clarifai_llm = Clarifai(pat=CLARIFAI_PAT, \ - user_id=USER_ID, app_id=APP_ID, model_id=MODEL_ID) + clarifai_llm = Clarifai(user_id=USER_ID, app_id=APP_ID, model_id=MODEL_ID) + (or) + clarifai_llm = Clarifai(model_url=EXAMPLE_URL) """ - stub: Any #: :meta private: - userDataObject: Any - + model_url: Optional[str] = None + """Model url to use.""" model_id: Optional[str] = None """Model id to use.""" - model_version_id: Optional[str] = None """Model version id to use.""" - app_id: Optional[str] = None """Clarifai application id to use.""" - user_id: Optional[str] = None """Clarifai user id to use.""" - pat: Optional[str] = None - + """Clarifai personal access token to use.""" api_base: str = "https://api.clarifai.com" class Config: @@ -60,32 +59,17 @@ def validate_environment(cls, values: Dict) -> Dict: user_id = values.get("user_id") app_id = values.get("app_id") model_id = values.get("model_id") + model_url = values.get("model_url") - if values["pat"] is None: - raise ValueError("Please provide a pat.") - if user_id is None: - raise ValueError("Please provide a user_id.") - if app_id is None: - raise ValueError("Please provide a app_id.") - if model_id is None: - raise ValueError("Please provide a model_id.") + if model_url is not None and model_id is not None: + raise ValueError("Please provide either model_url or model_id, not both.") - try: - from clarifai.client import create_stub - from clarifai.client.auth.helper import ClarifaiAuthHelper - except ImportError: - raise ImportError( - "Could not import clarifai python package. " - "Please install it with `pip install clarifai`." - ) - auth = ClarifaiAuthHelper( - user_id=user_id, - app_id=app_id, - pat=values["pat"], - base=values["api_base"], - ) - values["userDataObject"] = auth.get_user_app_id_proto() - values["stub"] = create_stub(auth) + if model_url is None and model_id is None: + raise ValueError("Please provide one of model_url or model_id.") + + if model_url is None and model_id is not None: + if user_id is None or app_id is None: + raise ValueError("Please provide a user_id and app_id.") return values @@ -99,6 +83,7 @@ def _identifying_params(self) -> Dict[str, Any]: """Get the identifying parameters.""" return { **{ + "model_url": self.model_url, "user_id": self.user_id, "app_id": self.app_id, "model_id": self.model_id, @@ -115,6 +100,7 @@ def _call( prompt: str, stop: Optional[List[str]] = None, run_manager: Optional[CallbackManagerForLLMRun] = None, + inference_params: Optional[Dict[str, Any]] = None, **kwargs: Any, ) -> str: """Call out to Clarfai's PostModelOutputs endpoint. @@ -131,54 +117,39 @@ def _call( response = clarifai_llm("Tell me a joke.") """ - + # If version_id None, Defaults to the latest model version try: - from clarifai_grpc.grpc.api import ( - resources_pb2, - service_pb2, - ) - from clarifai_grpc.grpc.api.status import status_code_pb2 + from clarifai.client.model import Model except ImportError: raise ImportError( "Could not import clarifai python package. " "Please install it with `pip install clarifai`." ) - - # The userDataObject is created in the overview and - # is required when using a PAT - # If version_id None, Defaults to the latest model version - post_model_outputs_request = service_pb2.PostModelOutputsRequest( - user_app_id=self.userDataObject, - model_id=self.model_id, - version_id=self.model_version_id, - inputs=[ - resources_pb2.Input( - data=resources_pb2.Data(text=resources_pb2.Text(raw=prompt)) - ) - ], - ) - post_model_outputs_response = self.stub.PostModelOutputs( - post_model_outputs_request - ) - - if post_model_outputs_response.status.code != status_code_pb2.SUCCESS: - logger.error(post_model_outputs_response.status) - first_model_failure = ( - post_model_outputs_response.outputs[0].status - if len(post_model_outputs_response.outputs) - else None + if self.pat is not None: + pat = self.pat + if self.model_url is not None: + _model_init = Model(url=self.model_url, pat=pat) + else: + _model_init = Model( + model_id=self.model_id, + user_id=self.user_id, + app_id=self.app_id, + pat=pat, ) - raise Exception( - f"Post model outputs failed, status: " - f"{post_model_outputs_response.status}, first output failure: " - f"{first_model_failure}" + try: + (inference_params := {}) if inference_params is None else inference_params + predict_response = _model_init.predict_by_bytes( + bytes(prompt, "utf-8"), + input_type="text", + inference_params=inference_params, ) + text = predict_response.outputs[0].data.text.raw + if stop is not None: + text = enforce_stop_tokens(text, stop) - text = post_model_outputs_response.outputs[0].data.text.raw + except Exception as e: + logger.error(f"Predict failed, exception: {e}") - # In order to make this consistent with other endpoints, we strip them. - if stop is not None: - text = enforce_stop_tokens(text, stop) return text def _generate( @@ -186,56 +157,50 @@ def _generate( prompts: List[str], stop: Optional[List[str]] = None, run_manager: Optional[CallbackManagerForLLMRun] = None, + inference_params: Optional[Dict[str, Any]] = None, **kwargs: Any, ) -> LLMResult: """Run the LLM on the given prompt and input.""" + # TODO: add caching here. try: - from clarifai_grpc.grpc.api import ( - resources_pb2, - service_pb2, - ) - from clarifai_grpc.grpc.api.status import status_code_pb2 + from clarifai.client.input import Inputs + from clarifai.client.model import Model except ImportError: raise ImportError( "Could not import clarifai python package. " "Please install it with `pip install clarifai`." ) - - # TODO: add caching here. - generations = [] - batch_size = 32 - for i in range(0, len(prompts), batch_size): - batch = prompts[i : i + batch_size] - post_model_outputs_request = service_pb2.PostModelOutputsRequest( - user_app_id=self.userDataObject, + if self.pat is not None: + pat = self.pat + if self.model_url is not None: + _model_init = Model(url=self.model_url, pat=pat) + else: + _model_init = Model( model_id=self.model_id, - version_id=self.model_version_id, - inputs=[ - resources_pb2.Input( - data=resources_pb2.Data(text=resources_pb2.Text(raw=prompt)) - ) - for prompt in batch - ], - ) - post_model_outputs_response = self.stub.PostModelOutputs( - post_model_outputs_request + user_id=self.user_id, + app_id=self.app_id, + pat=pat, ) - if post_model_outputs_response.status.code != status_code_pb2.SUCCESS: - logger.error(post_model_outputs_response.status) - first_model_failure = ( - post_model_outputs_response.outputs[0].status - if len(post_model_outputs_response.outputs) - else None - ) - raise Exception( - f"Post model outputs failed, status: " - f"{post_model_outputs_response.status}, first output failure: " - f"{first_model_failure}" + generations = [] + batch_size = 32 + input_obj = Inputs(pat=pat) + try: + for i in range(0, len(prompts), batch_size): + batch = prompts[i : i + batch_size] + input_batch = [ + input_obj.get_text_input(input_id=str(id), raw_text=inp) + for id, inp in enumerate(batch) + ] + ( + inference_params := {} + ) if inference_params is None else inference_params + predict_response = _model_init.predict( + inputs=input_batch, inference_params=inference_params ) - for output in post_model_outputs_response.outputs: + for output in predict_response.outputs: if stop is not None: text = enforce_stop_tokens(output.data.text.raw, stop) else: @@ -243,4 +208,7 @@ def _generate( generations.append([Generation(text=text)]) + except Exception as e: + logger.error(f"Predict failed, exception: {e}") + return LLMResult(generations=generations) diff --git a/libs/langchain/langchain/vectorstores/clarifai.py b/libs/langchain/langchain/vectorstores/clarifai.py index f78565ef5532f..308bc3502b22d 100644 --- a/libs/langchain/langchain/vectorstores/clarifai.py +++ b/libs/langchain/langchain/vectorstores/clarifai.py @@ -3,10 +3,12 @@ import logging import os import traceback +import uuid from concurrent.futures import ThreadPoolExecutor from typing import Any, Iterable, List, Optional, Tuple import requests +from google.protobuf.struct_pb2 import Struct from langchain_core.documents import Document from langchain_core.embeddings import Embeddings from langchain_core.vectorstores import VectorStore @@ -17,7 +19,7 @@ class Clarifai(VectorStore): """`Clarifai AI` vector store. - To use, you should have the ``clarifai`` python package installed. + To use, you should have the ``clarifai`` python SDK package installed. Example: .. code-block:: python @@ -33,9 +35,8 @@ def __init__( self, user_id: Optional[str] = None, app_id: Optional[str] = None, - pat: Optional[str] = None, number_of_docs: Optional[int] = None, - api_base: Optional[str] = None, + pat: Optional[str] = None, ) -> None: """Initialize with Clarifai client. @@ -50,21 +51,11 @@ def __init__( Raises: ValueError: If user ID, app ID or personal access token is not provided. """ - try: - from clarifai.auth.helper import DEFAULT_BASE, ClarifaiAuthHelper - from clarifai.client import create_stub - except ImportError: - raise ImportError( - "Could not import clarifai python package. " - "Please install it with `pip install clarifai`." - ) - - if api_base is None: - self._api_base = DEFAULT_BASE - self._user_id = user_id or os.environ.get("CLARIFAI_USER_ID") self._app_id = app_id or os.environ.get("CLARIFAI_APP_ID") - self._pat = pat or os.environ.get("CLARIFAI_PAT") + if pat: + os.environ["CLARIFAI_PAT"] = pat + self._pat = os.environ.get("CLARIFAI_PAT") if self._user_id is None or self._app_id is None or self._pat is None: raise ValueError( "Could not find CLARIFAI_USER_ID, CLARIFAI_APP_ID or\ @@ -73,77 +64,8 @@ def __init__( app ID and personal access token \ from https://clarifai.com/settings/security." ) - - self._auth = ClarifaiAuthHelper( - user_id=self._user_id, - app_id=self._app_id, - pat=self._pat, - base=self._api_base, - ) - self._stub = create_stub(self._auth) - self._userDataObject = self._auth.get_user_app_id_proto() self._number_of_docs = number_of_docs - def _post_texts_as_inputs( - self, texts: List[str], metadatas: Optional[List[dict]] = None - ) -> List[str]: - """Post text to Clarifai and return the ID of the input. - - Args: - text (str): Text to post. - metadata (dict): Metadata to post. - - Returns: - str: ID of the input. - """ - try: - from clarifai_grpc.grpc.api import resources_pb2, service_pb2 - from clarifai_grpc.grpc.api.status import status_code_pb2 - from google.protobuf.struct_pb2 import Struct # type: ignore - except ImportError as e: - raise ImportError( - "Could not import clarifai python package. " - "Please install it with `pip install clarifai`." - ) from e - - if metadatas is not None: - assert len(list(texts)) == len( - metadatas - ), "Number of texts and metadatas should be the same." - - inputs = [] - for idx, text in enumerate(texts): - if metadatas is not None: - input_metadata = Struct() - input_metadata.update(metadatas[idx]) - inputs.append( - resources_pb2.Input( - data=resources_pb2.Data( - text=resources_pb2.Text(raw=text), - metadata=input_metadata, - ) - ) - ) - - post_inputs_response = self._stub.PostInputs( - service_pb2.PostInputsRequest( - user_app_id=self._userDataObject, - inputs=inputs, - ) - ) - - if post_inputs_response.status.code != status_code_pb2.SUCCESS: - logger.error(post_inputs_response.status) - raise Exception( - "Post inputs failed, status: " + post_inputs_response.status.description - ) - - input_ids = [] - for input in post_inputs_response.inputs: - input_ids.append(input.id) - - return input_ids - def add_texts( self, texts: Iterable[str], @@ -162,9 +84,14 @@ def add_texts( metadatas (Optional[List[dict]], optional): Optional list of metadatas. ids (Optional[List[str]], optional): Optional list of IDs. - Returns: - List[str]: List of IDs of the added texts. """ + try: + from clarifai.client.input import Inputs + except ImportError as e: + raise ImportError( + "Could not import clarifai python package. " + "Please install it with `pip install clarifai`." + ) from e ltexts = list(texts) length = len(ltexts) @@ -175,29 +102,51 @@ def add_texts( metadatas ), "Number of texts and metadatas should be the same." + if ids is not None: + assert len(ltexts) == len( + ids + ), "Number of text inputs and input ids should be the same." + + input_obj = Inputs(app_id=self._app_id, user_id=self._user_id) batch_size = 32 - input_ids = [] + input_job_ids = [] for idx in range(0, length, batch_size): try: batch_texts = ltexts[idx : idx + batch_size] batch_metadatas = ( metadatas[idx : idx + batch_size] if metadatas else None ) - result_ids = self._post_texts_as_inputs(batch_texts, batch_metadatas) - input_ids.extend(result_ids) - logger.debug(f"Input {result_ids} posted successfully.") + if batch_metadatas is not None: + meta_list = [] + for meta in batch_metadatas: + meta_struct = Struct() + meta_struct.update(meta) + meta_list.append(meta_struct) + if ids is None: + ids = [uuid.uuid4().hex for _ in range(len(batch_texts))] + input_batch = [ + input_obj.get_text_input( + input_id=ids[id], + raw_text=inp, + metadata=meta_list[id] if batch_metadatas else None, + ) + for id, inp in enumerate(batch_texts) + ] + result_id = input_obj.upload_inputs(inputs=input_batch) + input_job_ids.extend(result_id) + logger.debug("Input posted successfully.") + except Exception as error: logger.warning(f"Post inputs failed: {error}") traceback.print_exc() - return input_ids + return input_job_ids def similarity_search_with_score( self, query: str, k: int = 4, - filter: Optional[dict] = None, - namespace: Optional[str] = None, + filters: Optional[dict] = None, **kwargs: Any, ) -> List[Tuple[Document, float]]: """Run similarity search with score using Clarifai. @@ -212,10 +161,9 @@ def similarity_search_with_score( List[Document]: List of documents most similar to the query text. """ try: - from clarifai_grpc.grpc.api import resources_pb2, service_pb2 - from clarifai_grpc.grpc.api.status import status_code_pb2 + from clarifai.client.search import Search + from clarifai_grpc.grpc.api import resources_pb2 from google.protobuf import json_format # type: ignore - from google.protobuf.struct_pb2 import Struct # type: ignore except ImportError as e: raise ImportError( "Could not import clarifai python package. " @@ -226,50 +174,22 @@ def similarity_search_with_score( if self._number_of_docs is not None: k = self._number_of_docs - req = service_pb2.PostAnnotationsSearchesRequest( - user_app_id=self._userDataObject, - searches=[ - resources_pb2.Search( - query=resources_pb2.Query( - ranks=[ - resources_pb2.Rank( - annotation=resources_pb2.Annotation( - data=resources_pb2.Data( - text=resources_pb2.Text(raw=query), - ) - ) - ) - ] - ) - ) - ], - pagination=service_pb2.Pagination(page=1, per_page=k), - ) - + search_obj = Search(user_id=self._user_id, app_id=self._app_id, top_k=k) + rank = [{"text_raw": query}] # Add filter by metadata if provided. - if filter is not None: - search_metadata = Struct() - search_metadata.update(filter) - f = req.searches[0].query.filters.add() - f.annotation.data.metadata.update(search_metadata) - - post_annotations_searches_response = self._stub.PostAnnotationsSearches(req) - - # Check if search was successful - if post_annotations_searches_response.status.code != status_code_pb2.SUCCESS: - raise Exception( - "Post searches failed, status: " - + post_annotations_searches_response.status.description - ) + if filters is not None: + search_metadata = {"metadata": filters} + search_response = search_obj.query(ranks=rank, filters=[search_metadata]) + else: + search_response = search_obj.query(ranks=rank) # Retrieve hits - hits = post_annotations_searches_response.hits - + hits = [hit for data in search_response for hit in data.hits] executor = ThreadPoolExecutor(max_workers=10) def hit_to_document(hit: resources_pb2.Hit) -> Tuple[Document, float]: metadata = json_format.MessageToDict(hit.input.data.metadata) - h = {"Authorization": f"Key {self._auth.pat}"} + h = {"Authorization": f"Key {self._pat}"} request = requests.get(hit.input.data.text.url, headers=h) # override encoding by real educated guess as provided by chardet @@ -314,9 +234,8 @@ def from_texts( metadatas: Optional[List[dict]] = None, user_id: Optional[str] = None, app_id: Optional[str] = None, - pat: Optional[str] = None, number_of_docs: Optional[int] = None, - api_base: Optional[str] = None, + pat: Optional[str] = None, **kwargs: Any, ) -> Clarifai: """Create a Clarifai vectorstore from a list of texts. @@ -325,10 +244,8 @@ def from_texts( user_id (str): User ID. app_id (str): App ID. texts (List[str]): List of texts to add. - pat (Optional[str]): Personal access token. Defaults to None. number_of_docs (Optional[int]): Number of documents to return during vector search. Defaults to None. - api_base (Optional[str]): API base. Defaults to None. metadatas (Optional[List[dict]]): Optional list of metadatas. Defaults to None. @@ -338,9 +255,8 @@ def from_texts( clarifai_vector_db = cls( user_id=user_id, app_id=app_id, - pat=pat, number_of_docs=number_of_docs, - api_base=api_base, + pat=pat, ) clarifai_vector_db.add_texts(texts=texts, metadatas=metadatas) return clarifai_vector_db @@ -352,9 +268,8 @@ def from_documents( embedding: Optional[Embeddings] = None, user_id: Optional[str] = None, app_id: Optional[str] = None, - pat: Optional[str] = None, number_of_docs: Optional[int] = None, - api_base: Optional[str] = None, + pat: Optional[str] = None, **kwargs: Any, ) -> Clarifai: """Create a Clarifai vectorstore from a list of documents. @@ -363,10 +278,8 @@ def from_documents( user_id (str): User ID. app_id (str): App ID. documents (List[Document]): List of documents to add. - pat (Optional[str]): Personal access token. Defaults to None. number_of_docs (Optional[int]): Number of documents to return during vector search. Defaults to None. - api_base (Optional[str]): API base. Defaults to None. Returns: Clarifai: Clarifai vectorstore. @@ -377,8 +290,7 @@ def from_documents( user_id=user_id, app_id=app_id, texts=texts, - pat=pat, number_of_docs=number_of_docs, - api_base=api_base, + pat=pat, metadatas=metadatas, ) From b05c46074b200fc5cda93f30e9049a16f7406c5b Mon Sep 17 00:00:00 2001 From: Alexandre Dumont Date: Wed, 6 Dec 2023 05:08:17 +0100 Subject: [PATCH 18/20] OpenAIEmbeddings: retry_min_seconds/retry_max_seconds parameters (#13138) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - **Description:** new parameters in OpenAIEmbeddings() constructor (retry_min_seconds and retry_max_seconds) that allow parametrization by the user of the former min_seconds and max_seconds that were hidden in _create_retry_decorator() and _async_retry_decorator() - **Issue:** #9298, #12986 - **Dependencies:** none - **Tag maintainer:** @hwchase17 - **Twitter handle:** @adumont make format ✅ make lint ✅ make test ✅ Co-authored-by: Harrison Chase --- libs/langchain/langchain/embeddings/openai.py | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/libs/langchain/langchain/embeddings/openai.py b/libs/langchain/langchain/embeddings/openai.py index 62aa549bd4430..265fdc2f6cad7 100644 --- a/libs/langchain/langchain/embeddings/openai.py +++ b/libs/langchain/langchain/embeddings/openai.py @@ -41,14 +41,19 @@ def _create_retry_decorator(embeddings: OpenAIEmbeddings) -> Callable[[Any], Any]: import openai - min_seconds = 4 - max_seconds = 10 # Wait 2^x * 1 second between each retry starting with - # 4 seconds, then up to 10 seconds, then 10 seconds afterwards + # retry_min_seconds seconds, then up to retry_max_seconds seconds, + # then retry_max_seconds seconds afterwards + # retry_min_seconds and retry_max_seconds are optional arguments of + # OpenAIEmbeddings return retry( reraise=True, stop=stop_after_attempt(embeddings.max_retries), - wait=wait_exponential(multiplier=1, min=min_seconds, max=max_seconds), + wait=wait_exponential( + multiplier=1, + min=embeddings.retry_min_seconds, + max=embeddings.retry_max_seconds, + ), retry=( retry_if_exception_type(openai.error.Timeout) | retry_if_exception_type(openai.error.APIError) @@ -63,14 +68,19 @@ def _create_retry_decorator(embeddings: OpenAIEmbeddings) -> Callable[[Any], Any def _async_retry_decorator(embeddings: OpenAIEmbeddings) -> Any: import openai - min_seconds = 4 - max_seconds = 10 # Wait 2^x * 1 second between each retry starting with - # 4 seconds, then up to 10 seconds, then 10 seconds afterwards + # retry_min_seconds seconds, then up to retry_max_seconds seconds, + # then retry_max_seconds seconds afterwards + # retry_min_seconds and retry_max_seconds are optional arguments of + # OpenAIEmbeddings async_retrying = AsyncRetrying( reraise=True, stop=stop_after_attempt(embeddings.max_retries), - wait=wait_exponential(multiplier=1, min=min_seconds, max=max_seconds), + wait=wait_exponential( + multiplier=1, + min=embeddings.retry_min_seconds, + max=embeddings.retry_max_seconds, + ), retry=( retry_if_exception_type(openai.error.Timeout) | retry_if_exception_type(openai.error.APIError) @@ -234,6 +244,10 @@ class OpenAIEmbeddings(BaseModel, Embeddings): 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.""" From a1a11ffd78c8d184b0f3dc8fb90d6548fd2ba2d4 Mon Sep 17 00:00:00 2001 From: MinjiK Date: Wed, 6 Dec 2023 05:08:34 +0100 Subject: [PATCH 19/20] Amadeus toolkit minor update (#13002) - update `Amadeus` toolkit with ability to switch Amadeus environments - update minor code explanations --------- Co-authored-by: MinjiK --- docs/docs/integrations/toolkits/amadeus.ipynb | 11 ++++++++--- .../agents/agent_toolkits/amadeus/toolkit.py | 2 +- .../langchain/tools/amadeus/flight_search.py | 2 +- libs/langchain/langchain/tools/amadeus/utils.py | 6 +++++- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/docs/docs/integrations/toolkits/amadeus.ipynb b/docs/docs/integrations/toolkits/amadeus.ipynb index 7d0a118f7a778..a05604def37ac 100644 --- a/docs/docs/integrations/toolkits/amadeus.ipynb +++ b/docs/docs/integrations/toolkits/amadeus.ipynb @@ -6,9 +6,13 @@ "source": [ "# Amadeus\n", "\n", - "This notebook walks you through connecting LangChain to the `Amadeus` travel information API\n", + "This notebook walks you through connecting LangChain to the `Amadeus` travel APIs.\n", "\n", - "To use this toolkit, you will need to set up your credentials explained in the [Amadeus for developers getting started overview](https://developers.amadeus.com/get-started/get-started-with-self-service-apis-335). Once you've received a AMADEUS_CLIENT_ID and AMADEUS_CLIENT_SECRET, you can input them as environmental variables below." + "This `Amadeus` toolkit allows agents to make decision when it comes to travel, especially searching and booking trips with flights.\n", + "\n", + "To use this toolkit, you will need to have your Amadeus API keys ready, explained in the [Get started Amadeus Self-Service APIs](https://developers.amadeus.com/get-started/get-started-with-self-service-apis-335). Once you've received a AMADEUS_CLIENT_ID and AMADEUS_CLIENT_SECRET, you can input them as environmental variables below.\n", + "\n", + "Note: Amadeus Self-Service APIs offers a test enviornment with [free limited data](https://amadeus4dev.github.io/developer-guides/test-data/). This allows developers to build and test their applications before deploying them to production. To access real-time data, you will need to [move to the production environment](https://amadeus4dev.github.io/developer-guides/API-Keys/moving-to-production/)." ] }, { @@ -40,7 +44,8 @@ "\n", "os.environ[\"AMADEUS_CLIENT_ID\"] = \"CLIENT_ID\"\n", "os.environ[\"AMADEUS_CLIENT_SECRET\"] = \"CLIENT_SECRET\"\n", - "os.environ[\"OPENAI_API_KEY\"] = \"API_KEY\"" + "os.environ[\"OPENAI_API_KEY\"] = \"API_KEY\"\n", + "# os.environ[\"AMADEUS_HOSTNAME\"] = \"production\" or \"test\"" ] }, { diff --git a/libs/langchain/langchain/agents/agent_toolkits/amadeus/toolkit.py b/libs/langchain/langchain/agents/agent_toolkits/amadeus/toolkit.py index c1dd29925b282..b87a917952943 100644 --- a/libs/langchain/langchain/agents/agent_toolkits/amadeus/toolkit.py +++ b/libs/langchain/langchain/agents/agent_toolkits/amadeus/toolkit.py @@ -15,7 +15,7 @@ class AmadeusToolkit(BaseToolkit): - """Toolkit for interacting with Amadeus which offers APIs for travel search.""" + """Toolkit for interacting with Amadeus which offers APIs for travel.""" client: Client = Field(default_factory=authenticate) diff --git a/libs/langchain/langchain/tools/amadeus/flight_search.py b/libs/langchain/langchain/tools/amadeus/flight_search.py index 603a397a7d6fd..2276f9a72bbe1 100644 --- a/libs/langchain/langchain/tools/amadeus/flight_search.py +++ b/libs/langchain/langchain/tools/amadeus/flight_search.py @@ -96,7 +96,7 @@ def _run( ) return [None] - # Collect all results from the API + # Collect all results from the Amadeus Flight Offers Search API try: response = client.shopping.flight_offers_search.get( originLocationCode=originLocationCode, diff --git a/libs/langchain/langchain/tools/amadeus/utils.py b/libs/langchain/langchain/tools/amadeus/utils.py index 51e0740615f92..7c04ec0528c65 100644 --- a/libs/langchain/langchain/tools/amadeus/utils.py +++ b/libs/langchain/langchain/tools/amadeus/utils.py @@ -33,6 +33,10 @@ def authenticate() -> Client: ) return None - client = Client(client_id=client_id, client_secret=client_secret) + hostname = "test" # Default hostname + if "AMADEUS_HOSTNAME" in os.environ: + hostname = os.environ["AMADEUS_HOSTNAME"] + + client = Client(client_id=client_id, client_secret=client_secret, hostname=hostname) return client From e1ea1912377ca7c013e89fac4c1d26c0cb836009 Mon Sep 17 00:00:00 2001 From: Matt Wells Date: Wed, 6 Dec 2023 04:08:50 +0000 Subject: [PATCH 20/20] Demonstrate use of get_buffer_string (#13013) **Description** The docs for creating a RAG chain with Memory [currently use a manual lambda](https://python.langchain.com/docs/expression_language/cookbook/retrieval#with-memory-and-returning-source-documents) to format chat history messages. [There exists a helper method within the codebase](https://github.com/langchain-ai/langchain/blob/master/libs/langchain/langchain/schema/messages.py#L14C15-L14C15) to perform this task so I've updated the documentation to demonstrate its usage Also worth noting that the current documented method of using the included `_format_chat_history ` function actually results in an error: ``` TypeError: 'HumanMessage' object is not subscriptable ``` --------- Co-authored-by: Harrison Chase --- .../cookbook/retrieval.ipynb | 134 ++++++++++-------- 1 file changed, 73 insertions(+), 61 deletions(-) diff --git a/docs/docs/expression_language/cookbook/retrieval.ipynb b/docs/docs/expression_language/cookbook/retrieval.ipynb index 2740ef626a115..aef5c1fb6374f 100644 --- a/docs/docs/expression_language/cookbook/retrieval.ipynb +++ b/docs/docs/expression_language/cookbook/retrieval.ipynb @@ -31,7 +31,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 1, "id": "33be32af", "metadata": {}, "outputs": [], @@ -48,7 +48,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 2, "id": "bfc47ec1", "metadata": {}, "outputs": [], @@ -70,7 +70,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "id": "eae31755", "metadata": {}, "outputs": [], @@ -85,7 +85,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 4, "id": "f3040b0c", "metadata": {}, "outputs": [ @@ -95,7 +95,7 @@ "'Harrison worked at Kensho.'" ] }, - "execution_count": 5, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -106,7 +106,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "id": "e1d20c7c", "metadata": {}, "outputs": [], @@ -134,7 +134,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "id": "7ee8b2d4", "metadata": {}, "outputs": [ @@ -144,7 +144,7 @@ "'Harrison ha lavorato a Kensho.'" ] }, - "execution_count": 7, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -165,18 +165,20 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 21, "id": "3f30c348", "metadata": {}, "outputs": [], "source": [ "from langchain.schema import format_document\n", - "from langchain.schema.runnable import RunnableParallel" + "from langchain.schema.messages import get_buffer_string\n", + "from langchain.schema.runnable import RunnableParallel\n", + "from langchain_core.messages import AIMessage, HumanMessage" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "id": "64ab1dbf", "metadata": {}, "outputs": [], @@ -194,7 +196,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 9, "id": "7d628c97", "metadata": {}, "outputs": [], @@ -209,7 +211,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 10, "id": "f60a5d0f", "metadata": {}, "outputs": [], @@ -226,39 +228,14 @@ }, { "cell_type": "code", - "execution_count": 12, - "id": "7d007db6", - "metadata": {}, - "outputs": [], - "source": [ - "from typing import List, Tuple\n", - "\n", - "\n", - "def _format_chat_history(chat_history: List[Tuple[str, str]]) -> str:\n", - " # chat history is of format:\n", - " # [\n", - " # (human_message_str, ai_message_str),\n", - " # ...\n", - " # ]\n", - " # see below for an example of how it's invoked\n", - " buffer = \"\"\n", - " for dialogue_turn in chat_history:\n", - " human = \"Human: \" + dialogue_turn[0]\n", - " ai = \"Assistant: \" + dialogue_turn[1]\n", - " buffer += \"\\n\" + \"\\n\".join([human, ai])\n", - " return buffer" - ] - }, - { - "cell_type": "code", - "execution_count": 13, + "execution_count": 11, "id": "5c32cc89", "metadata": {}, "outputs": [], "source": [ "_inputs = RunnableParallel(\n", " standalone_question=RunnablePassthrough.assign(\n", - " chat_history=lambda x: _format_chat_history(x[\"chat_history\"])\n", + " chat_history=lambda x: get_buffer_string(x[\"chat_history\"])\n", " )\n", " | CONDENSE_QUESTION_PROMPT\n", " | ChatOpenAI(temperature=0)\n", @@ -273,17 +250,17 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 12, "id": "135c8205", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "AIMessage(content='Harrison was employed at Kensho.', additional_kwargs={}, example=False)" + "AIMessage(content='Harrison was employed at Kensho.')" ] }, - "execution_count": 14, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -299,17 +276,17 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 22, "id": "424e7e7a", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "AIMessage(content='Harrison worked at Kensho.', additional_kwargs={}, example=False)" + "AIMessage(content='Harrison worked at Kensho.')" ] }, - "execution_count": 15, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" } @@ -318,7 +295,10 @@ "conversational_qa_chain.invoke(\n", " {\n", " \"question\": \"where did he work?\",\n", - " \"chat_history\": [(\"Who wrote this notebook?\", \"Harrison\")],\n", + " \"chat_history\": [\n", + " HumanMessage(content=\"Who wrote this notebook?\"),\n", + " AIMessage(content=\"Harrison\"),\n", + " ],\n", " }\n", ")" ] @@ -335,7 +315,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 14, "id": "e31dd17c", "metadata": {}, "outputs": [], @@ -347,7 +327,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 15, "id": "d4bffe94", "metadata": {}, "outputs": [], @@ -359,7 +339,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 16, "id": "733be985", "metadata": {}, "outputs": [], @@ -373,7 +353,7 @@ "standalone_question = {\n", " \"standalone_question\": {\n", " \"question\": lambda x: x[\"question\"],\n", - " \"chat_history\": lambda x: _format_chat_history(x[\"chat_history\"]),\n", + " \"chat_history\": lambda x: get_buffer_string(x[\"chat_history\"]),\n", " }\n", " | CONDENSE_QUESTION_PROMPT\n", " | ChatOpenAI(temperature=0)\n", @@ -400,18 +380,18 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 17, "id": "806e390c", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'answer': AIMessage(content='Harrison was employed at Kensho.', additional_kwargs={}, example=False),\n", - " 'docs': [Document(page_content='harrison worked at kensho', metadata={})]}" + "{'answer': AIMessage(content='Harrison was employed at Kensho.'),\n", + " 'docs': [Document(page_content='harrison worked at kensho')]}" ] }, - "execution_count": 19, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -424,7 +404,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 18, "id": "977399fd", "metadata": {}, "outputs": [], @@ -437,18 +417,18 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 19, "id": "f94f7de4", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'history': [HumanMessage(content='where did harrison work?', additional_kwargs={}, example=False),\n", - " AIMessage(content='Harrison was employed at Kensho.', additional_kwargs={}, example=False)]}" + "{'history': [HumanMessage(content='where did harrison work?'),\n", + " AIMessage(content='Harrison was employed at Kensho.')]}" ] }, - "execution_count": 21, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } @@ -456,6 +436,38 @@ "source": [ "memory.load_memory_variables({})" ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "88f2b7cd", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'answer': AIMessage(content='Harrison actually worked at Kensho.'),\n", + " 'docs': [Document(page_content='harrison worked at kensho')]}" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "inputs = {\"question\": \"but where did he really work?\"}\n", + "result = final_chain.invoke(inputs)\n", + "result" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "207a2782", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -474,7 +486,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.1" + "version": "3.10.1" } }, "nbformat": 4,